diff --git a/packages/analytics/src/Domain/Statistics/StatisticsMeasure.ts b/packages/analytics/src/Domain/Statistics/StatisticsMeasure.ts index 510a97230..1a22024ce 100644 --- a/packages/analytics/src/Domain/Statistics/StatisticsMeasure.ts +++ b/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', } diff --git a/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts index 28ae845b9..60672a53b 100644 --- a/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts +++ b/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 + statisticsStore.incrementMeasure = jest.fn() + + timer = {} as jest.Mocked + timer.convertDateToMicroseconds = jest.fn().mockReturnValue(1) + userSubscriptionRepository = {} as jest.Mocked + userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(0) userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription) offlineUserSubscription = {} as jest.Mocked @@ -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 () => { diff --git a/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts b/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts index 24747df23..b7e5deed3 100644 --- a/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts +++ b/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 { diff --git a/packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts b/packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts index 6db62ca93..ad2cef6ba 100644 --- a/packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts +++ b/packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts @@ -4,6 +4,7 @@ import { UserSubscriptionType } from './UserSubscriptionType' export interface UserSubscriptionRepositoryInterface { findOneByUuid(uuid: Uuid): Promise + countByUserUuid(userUuid: Uuid): Promise findOneByUserUuid(userUuid: Uuid): Promise findOneByUserUuidAndSubscriptionId(userUuid: Uuid, subscriptionId: number): Promise findBySubscriptionIdAndType(subscriptionId: number, type: UserSubscriptionType): Promise diff --git a/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.spec.ts b/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.spec.ts index d857fdb9a..78c0a7b15 100644 --- a/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.spec.ts +++ b/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 diff --git a/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.ts b/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.ts index 0fcbd34eb..7cd63814f 100644 --- a/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.ts +++ b/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.ts @@ -14,6 +14,15 @@ export class MySQLUserSubscriptionRepository implements UserSubscriptionReposito private ormRepository: Repository, ) {} + async countByUserUuid(userUuid: Uuid): Promise { + return await this.ormRepository + .createQueryBuilder() + .where('user_uuid = :user_uuid', { + user_uuid: userUuid, + }) + .getCount() + } + async save(subscription: UserSubscription): Promise { return this.ormRepository.save(subscription) }