瀏覽代碼

feat(auth): add measuring registration to subscription time statistics

Karol Sójko 2 年之前
父節點
當前提交
b61825235e

+ 1 - 0
packages/analytics/src/Domain/Statistics/StatisticsMeasure.ts

@@ -2,5 +2,6 @@ export enum StatisticsMeasure {
   Income = 'income',
   SubscriptionLength = 'subscription-length',
   RegistrationLength = 'registration-length',
+  RegistrationToSubscriptionTime = 'registration-to-subscription-time',
   Refunds = 'refunds',
 }

+ 22 - 1
packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts

@@ -16,9 +16,10 @@ import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/Offl
 import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
 import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
 import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
-import { AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
+import { AnalyticsStoreInterface, Period, StatisticsStoreInterface } from '@standardnotes/analytics'
 import { AnalyticsEntity } from '../Analytics/AnalyticsEntity'
 import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
+import { TimerInterface } from '@standardnotes/time'
 
 describe('SubscriptionPurchasedEventHandler', () => {
   let userRepository: UserRepositoryInterface
@@ -35,6 +36,8 @@ describe('SubscriptionPurchasedEventHandler', () => {
   let getUserAnalyticsId: GetUserAnalyticsId
   let analyticsStore: AnalyticsStoreInterface
   let timestamp: number
+  let statisticsStore: StatisticsStoreInterface
+  let timer: TimerInterface
 
   const createHandler = () =>
     new SubscriptionPurchasedEventHandler(
@@ -45,6 +48,8 @@ describe('SubscriptionPurchasedEventHandler', () => {
       subscriptionSettingService,
       getUserAnalyticsId,
       analyticsStore,
+      statisticsStore,
+      timer,
       logger,
     )
 
@@ -66,7 +71,14 @@ describe('SubscriptionPurchasedEventHandler', () => {
     userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
     userRepository.save = jest.fn().mockReturnValue(user)
 
+    statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
+    statisticsStore.incrementMeasure = jest.fn()
+
+    timer = {} as jest.Mocked<TimerInterface>
+    timer.convertDateToMicroseconds = jest.fn().mockReturnValue(1)
+
     userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
+    userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(0)
     userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
 
     offlineUserSubscription = {} as jest.Mocked<OfflineUserSubscription>
@@ -146,6 +158,15 @@ describe('SubscriptionPurchasedEventHandler', () => {
       updatedAt: expect.any(Number),
       cancelled: false,
     })
+    expect(statisticsStore.incrementMeasure).toHaveBeenCalled()
+  })
+
+  it("should not measure registration to subscription time if this is not user's first subscription", async () => {
+    userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(1)
+
+    await createHandler().handle(event)
+
+    expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
   })
 
   it('should update analytics on limited discount offer purchasing', async () => {

+ 20 - 1
packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts

@@ -13,8 +13,15 @@ import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription
 import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
 import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
 import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
-import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
+import {
+  AnalyticsActivity,
+  AnalyticsStoreInterface,
+  Period,
+  StatisticsMeasure,
+  StatisticsStoreInterface,
+} from '@standardnotes/analytics'
 import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
+import { TimerInterface } from '@standardnotes/time'
 
 @injectable()
 export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInterface {
@@ -27,6 +34,8 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
     @inject(TYPES.SubscriptionSettingService) private subscriptionSettingService: SubscriptionSettingServiceInterface,
     @inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
     @inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
+    @inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
+    @inject(TYPES.Timer) private timer: TimerInterface,
     @inject(TYPES.Logger) private logger: Logger,
   ) {}
 
@@ -52,6 +61,8 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
       return
     }
 
+    const previousSubscriptionCount = await this.userSubscriptionRepository.countByUserUuid(user.uuid)
+
     const userSubscription = await this.createSubscription(
       event.payload.subscriptionId,
       event.payload.subscriptionName,
@@ -80,6 +91,14 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
         Period.Today,
       ])
     }
+
+    if (previousSubscriptionCount === 0) {
+      await this.statisticsStore.incrementMeasure(
+        StatisticsMeasure.RegistrationToSubscriptionTime,
+        event.payload.timestamp - this.timer.convertDateToMicroseconds(user.createdAt),
+        [Period.Today, Period.ThisWeek, Period.ThisMonth],
+      )
+    }
   }
 
   private async addUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {

+ 1 - 0
packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts

@@ -4,6 +4,7 @@ import { UserSubscriptionType } from './UserSubscriptionType'
 
 export interface UserSubscriptionRepositoryInterface {
   findOneByUuid(uuid: Uuid): Promise<UserSubscription | null>
+  countByUserUuid(userUuid: Uuid): Promise<number>
   findOneByUserUuid(userUuid: Uuid): Promise<UserSubscription | null>
   findOneByUserUuidAndSubscriptionId(userUuid: Uuid, subscriptionId: number): Promise<UserSubscription | null>
   findBySubscriptionIdAndType(subscriptionId: number, type: UserSubscriptionType): Promise<UserSubscription[]>

+ 15 - 0
packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.spec.ts

@@ -75,6 +75,21 @@ describe('MySQLUserSubscriptionRepository', () => {
     expect(result).toEqual(subscription)
   })
 
+  it('should count by user uuid', async () => {
+    ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
+
+    selectQueryBuilder.where = jest.fn().mockReturnThis()
+    selectQueryBuilder.getCount = jest.fn().mockReturnValue(2)
+
+    const result = await createRepository().countByUserUuid('123')
+
+    expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
+      user_uuid: '123',
+    })
+    expect(selectQueryBuilder.getCount).toHaveBeenCalled()
+    expect(result).toEqual(2)
+  })
+
   it('should find one, longest lasting subscription by user uuid if there are no ucanceled ones', async () => {
     subscription.cancelled = true
 

+ 9 - 0
packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.ts

@@ -14,6 +14,15 @@ export class MySQLUserSubscriptionRepository implements UserSubscriptionReposito
     private ormRepository: Repository<UserSubscription>,
   ) {}
 
+  async countByUserUuid(userUuid: Uuid): Promise<number> {
+    return await this.ormRepository
+      .createQueryBuilder()
+      .where('user_uuid = :user_uuid', {
+        user_uuid: userUuid,
+      })
+      .getCount()
+  }
+
   async save(subscription: UserSubscription): Promise<UserSubscription> {
     return this.ormRepository.save(subscription)
   }