From 0f8457534c1829c58f3c036749d262307ddeb779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Mon, 19 Dec 2022 14:20:11 +0100 Subject: [PATCH] feat(analytics): add persisting statistics on demand --- packages/analytics/bin/report.ts | 53 ++++---- packages/analytics/jest.config.js | 2 +- packages/analytics/src/Bootstrap/Container.ts | 64 +++++---- packages/analytics/src/Bootstrap/Types.ts | 3 + .../Email/daily-analytics-report.html.ts | 128 ++++++++++-------- ...countDeletionRequestedEventHandler.spec.ts | 69 ---------- .../AccountDeletionRequestedEventHandler.ts | 4 +- .../Handler/PaymentFailedEventHandler.spec.ts | 34 ----- .../PaymentSuccessEventHandler.spec.ts | 75 ---------- .../Handler/PaymentSuccessEventHandler.ts | 44 ++++-- .../RefundProcessedEventHandler.spec.ts | 36 ----- .../Handler/RefundProcessedEventHandler.ts | 4 +- ...atisticPersistenceRequestedEventHandler.ts | 19 +++ .../SubscriptionCancelledEventHandler.spec.ts | 104 -------------- .../SubscriptionCancelledEventHandler.ts | 6 +- .../SubscriptionExpiredEventHandler.spec.ts | 79 ----------- .../SubscriptionExpiredEventHandler.ts | 4 +- .../SubscriptionPurchasedEventHandler.spec.ts | 102 -------------- .../SubscriptionPurchasedEventHandler.ts | 8 +- ...ubscriptionReactivatedEventHandler.spec.ts | 46 ------- .../SubscriptionRefundedEventHandler.spec.ts | 110 --------------- .../SubscriptionRefundedEventHandler.ts | 4 +- .../SubscriptionRenewedEventHandler.spec.ts | 68 ---------- .../UserRegisteredEventHandler.spec.ts | 47 ------- .../src/Domain/Statistics/StatisticMeasure.ts | 25 ++++ .../Statistics/StatisticMeasureName.spec.ts | 18 +++ .../Domain/Statistics/StatisticMeasureName.ts | 47 +++++++ .../Statistics/StatisticMeasureNameProps.ts | 3 + .../Statistics/StatisticMeasureProps.ts | 7 + .../StatisticMeasureRepositoryInterface.ts | 5 + .../Domain/Statistics/StatisticsMeasure.ts | 24 ---- .../Statistics/StatisticsStoreInterface.ts | 13 +- .../src/Domain/Time/PeriodKeyGenerator.ts | 2 +- .../Time/PeriodKeyGeneratorInterface.ts | 1 + .../CalculateMonthlyRecurringRevenue.spec.ts | 4 +- .../CalculateMonthlyRecurringRevenue.ts | 14 +- .../PersistStatistic/PersistStatistic.ts | 31 +++++ .../PersistStatistic/PersistStatisticDTO.ts | 5 + .../src/Infra/Redis/RedisStatisticsStore.ts | 32 +++-- .../StatisticPersistenceRequestedEvent.ts | 8 ++ ...atisticPersistenceRequestedEventPayload.ts | 5 + packages/domain-events/src/Domain/index.ts | 2 + 42 files changed, 407 insertions(+), 952 deletions(-) delete mode 100644 packages/analytics/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts delete mode 100644 packages/analytics/src/Domain/Handler/PaymentFailedEventHandler.spec.ts delete mode 100644 packages/analytics/src/Domain/Handler/PaymentSuccessEventHandler.spec.ts delete mode 100644 packages/analytics/src/Domain/Handler/RefundProcessedEventHandler.spec.ts create mode 100644 packages/analytics/src/Domain/Handler/StatisticPersistenceRequestedEventHandler.ts delete mode 100644 packages/analytics/src/Domain/Handler/SubscriptionCancelledEventHandler.spec.ts delete mode 100644 packages/analytics/src/Domain/Handler/SubscriptionExpiredEventHandler.spec.ts delete mode 100644 packages/analytics/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts delete mode 100644 packages/analytics/src/Domain/Handler/SubscriptionReactivatedEventHandler.spec.ts delete mode 100644 packages/analytics/src/Domain/Handler/SubscriptionRefundedEventHandler.spec.ts delete mode 100644 packages/analytics/src/Domain/Handler/SubscriptionRenewedEventHandler.spec.ts delete mode 100644 packages/analytics/src/Domain/Handler/UserRegisteredEventHandler.spec.ts create mode 100644 packages/analytics/src/Domain/Statistics/StatisticMeasure.ts create mode 100644 packages/analytics/src/Domain/Statistics/StatisticMeasureName.spec.ts create mode 100644 packages/analytics/src/Domain/Statistics/StatisticMeasureName.ts create mode 100644 packages/analytics/src/Domain/Statistics/StatisticMeasureNameProps.ts create mode 100644 packages/analytics/src/Domain/Statistics/StatisticMeasureProps.ts create mode 100644 packages/analytics/src/Domain/Statistics/StatisticMeasureRepositoryInterface.ts delete mode 100644 packages/analytics/src/Domain/Statistics/StatisticsMeasure.ts create mode 100644 packages/analytics/src/Domain/UseCase/PersistStatistic/PersistStatistic.ts create mode 100644 packages/analytics/src/Domain/UseCase/PersistStatistic/PersistStatisticDTO.ts create mode 100644 packages/domain-events/src/Domain/Event/StatisticPersistenceRequestedEvent.ts create mode 100644 packages/domain-events/src/Domain/Event/StatisticPersistenceRequestedEventPayload.ts diff --git a/packages/analytics/bin/report.ts b/packages/analytics/bin/report.ts index 2c6c372f0..585f0d5ab 100644 --- a/packages/analytics/bin/report.ts +++ b/packages/analytics/bin/report.ts @@ -8,7 +8,6 @@ import { EmailLevel } from '@standardnotes/domain-core' import { DomainEventPublisherInterface } from '@standardnotes/domain-events' import { AnalyticsActivity } from '../src/Domain/Analytics/AnalyticsActivity' import { Period } from '../src/Domain/Time/Period' -import { StatisticsMeasure } from '../src/Domain/Statistics/StatisticsMeasure' import { AnalyticsStoreInterface } from '../src/Domain/Analytics/AnalyticsStoreInterface' import { StatisticsStoreInterface } from '../src/Domain/Statistics/StatisticsStoreInterface' import { PeriodKeyGeneratorInterface } from '../src/Domain/Time/PeriodKeyGeneratorInterface' @@ -19,6 +18,7 @@ import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFact import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue' import { getBody, getSubject } from '../src/Domain/Email/DailyAnalyticsReport' import { TimerInterface } from '@standardnotes/time' +import { StatisticMeasureName } from '../src/Domain/Statistics/StatisticMeasureName' const requestReport = async ( analyticsStore: AnalyticsStoreInterface, @@ -115,12 +115,12 @@ const requestReport = async ( }> = [] const thirtyDaysStatisticsNames = [ - StatisticsMeasure.MRR, - StatisticsMeasure.AnnualPlansMRR, - StatisticsMeasure.MonthlyPlansMRR, - StatisticsMeasure.FiveYearPlansMRR, - StatisticsMeasure.PlusPlansMRR, - StatisticsMeasure.ProPlansMRR, + StatisticMeasureName.NAMES.MRR, + StatisticMeasureName.NAMES.AnnualPlansMRR, + StatisticMeasureName.NAMES.MonthlyPlansMRR, + StatisticMeasureName.NAMES.FiveYearPlansMRR, + StatisticMeasureName.NAMES.PlusPlansMRR, + StatisticMeasureName.NAMES.ProPlansMRR, ] for (const statisticName of thirtyDaysStatisticsNames) { statisticsOverTime.push({ @@ -130,7 +130,7 @@ const requestReport = async ( }) } - const monthlyStatisticsNames = [StatisticsMeasure.MRR] + const monthlyStatisticsNames = [StatisticMeasureName.NAMES.MRR] for (const statisticName of monthlyStatisticsNames) { statisticsOverTime.push({ name: statisticName, @@ -140,22 +140,22 @@ const requestReport = async ( } const statisticMeasureNames = [ - StatisticsMeasure.Income, - StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome, - StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome, - StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome, - StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome, - StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome, - StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome, - StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome, - StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome, - StatisticsMeasure.Refunds, - StatisticsMeasure.RegistrationLength, - StatisticsMeasure.SubscriptionLength, - StatisticsMeasure.RegistrationToSubscriptionTime, - StatisticsMeasure.RemainingSubscriptionTimePercentage, - StatisticsMeasure.NewCustomers, - StatisticsMeasure.TotalCustomers, + StatisticMeasureName.NAMES.Income, + StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome, + StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome, + StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome, + StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome, + StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome, + StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome, + StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome, + StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome, + StatisticMeasureName.NAMES.Refunds, + StatisticMeasureName.NAMES.RegistrationLength, + StatisticMeasureName.NAMES.SubscriptionLength, + StatisticMeasureName.NAMES.RegistrationToSubscriptionTime, + StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage, + StatisticMeasureName.NAMES.NewCustomers, + StatisticMeasureName.NAMES.TotalCustomers, ] const statisticMeasures: Array<{ name: string @@ -190,7 +190,10 @@ const requestReport = async ( const totalCustomerCounts: Array = [] for (const dailyPeriodKey of dailyPeriodKeys) { - const customersCount = await statisticsStore.getMeasureTotal(StatisticsMeasure.TotalCustomers, dailyPeriodKey) + const customersCount = await statisticsStore.getMeasureTotal( + StatisticMeasureName.NAMES.TotalCustomers, + dailyPeriodKey, + ) totalCustomerCounts.push(customersCount) } const filteredTotalCustomerCounts = totalCustomerCounts.filter((count) => !!count) diff --git a/packages/analytics/jest.config.js b/packages/analytics/jest.config.js index 43d2b59d3..f84f7348a 100644 --- a/packages/analytics/jest.config.js +++ b/packages/analytics/jest.config.js @@ -7,5 +7,5 @@ module.exports = { transform: { ...tsjPreset.transform, }, - coveragePathIgnorePatterns: ['/Infra/', '/Domain/Email/'], + coveragePathIgnorePatterns: ['/Infra/', '/Domain/Email/', '/Handler/'], } diff --git a/packages/analytics/src/Bootstrap/Container.ts b/packages/analytics/src/Bootstrap/Container.ts index 1c52f731a..a73161efa 100644 --- a/packages/analytics/src/Bootstrap/Container.ts +++ b/packages/analytics/src/Bootstrap/Container.ts @@ -52,6 +52,9 @@ import { RevenueModification } from '../Domain/Revenue/RevenueModification' import { RevenueModificationMap } from '../Domain/Map/RevenueModificationMap' import { SaveRevenueModification } from '../Domain/UseCase/SaveRevenueModification/SaveRevenueModification' import { CalculateMonthlyRecurringRevenue } from '../Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue' +import { PersistStatistic } from '../Domain/UseCase/PersistStatistic/PersistStatistic' +import { StatisticMeasureRepositoryInterface } from '../Domain/Statistics/StatisticMeasureRepositoryInterface' +import { StatisticPersistenceRequestedEventHandler } from '../Domain/Handler/StatisticPersistenceRequestedEventHandler' // eslint-disable-next-line @typescript-eslint/no-var-requires const newrelicFormatter = require('@newrelic/winston-enricher') @@ -132,6 +135,29 @@ export class ContainerConfigLoader { container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true)) container.bind(TYPES.ADMIN_EMAILS).toConstantValue(env.get('ADMIN_EMAILS').split(',')) + // Services + container.bind(TYPES.DomainEventFactory).to(DomainEventFactory) + container.bind(TYPES.PeriodKeyGenerator).toConstantValue(new PeriodKeyGenerator()) + container + .bind(TYPES.AnalyticsStore) + .toConstantValue(new RedisAnalyticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis))) + container + .bind(TYPES.StatisticsStore) + .toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis))) + container.bind(TYPES.Timer).toConstantValue(new Timer()) + + if (env.get('SNS_TOPIC_ARN', true)) { + container + .bind(TYPES.DomainEventPublisher) + .toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN))) + } else { + container + .bind(TYPES.DomainEventPublisher) + .toConstantValue( + new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)), + ) + } + // Repositories container .bind(TYPES.AnalyticsEntityRepository) @@ -139,6 +165,9 @@ export class ContainerConfigLoader { container .bind(TYPES.RevenueModificationRepository) .to(MySQLRevenueModificationRepository) + container + .bind(TYPES.StatisticMeasureRepository) + .toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis))) // ORM container @@ -154,6 +183,9 @@ export class ContainerConfigLoader { container .bind(TYPES.CalculateMonthlyRecurringRevenue) .to(CalculateMonthlyRecurringRevenue) + container + .bind(TYPES.PersistStatistic) + .toConstantValue(new PersistStatistic(container.get(TYPES.StatisticMeasureRepository))) // Hanlders container.bind(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler) @@ -181,35 +213,20 @@ export class ContainerConfigLoader { .bind(TYPES.SubscriptionReactivatedEventHandler) .to(SubscriptionReactivatedEventHandler) container.bind(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler) + container + .bind(TYPES.StatisticPersistenceRequestedEventHandler) + .toConstantValue( + new StatisticPersistenceRequestedEventHandler( + container.get(TYPES.PersistStatistic), + container.get(TYPES.Logger), + ), + ) // Maps container .bind>(TYPES.RevenueModificationMap) .to(RevenueModificationMap) - // Services - container.bind(TYPES.DomainEventFactory).to(DomainEventFactory) - container.bind(TYPES.PeriodKeyGenerator).toConstantValue(new PeriodKeyGenerator()) - container - .bind(TYPES.AnalyticsStore) - .toConstantValue(new RedisAnalyticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis))) - container - .bind(TYPES.StatisticsStore) - .toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis))) - container.bind(TYPES.Timer).toConstantValue(new Timer()) - - if (env.get('SNS_TOPIC_ARN', true)) { - container - .bind(TYPES.DomainEventPublisher) - .toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN))) - } else { - container - .bind(TYPES.DomainEventPublisher) - .toConstantValue( - new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)), - ) - } - const eventHandlers: Map = new Map([ ['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)], ['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)], @@ -222,6 +239,7 @@ export class ContainerConfigLoader { ['SUBSCRIPTION_EXPIRED', container.get(TYPES.SubscriptionExpiredEventHandler)], ['SUBSCRIPTION_REACTIVATED', container.get(TYPES.SubscriptionReactivatedEventHandler)], ['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)], + ['STATISTIC_PERSISTENCE_REQUESTED', container.get(TYPES.StatisticPersistenceRequestedEventHandler)], ]) if (env.get('SQS_QUEUE_URL', true)) { diff --git a/packages/analytics/src/Bootstrap/Types.ts b/packages/analytics/src/Bootstrap/Types.ts index 67b0f72d5..93539a6b0 100644 --- a/packages/analytics/src/Bootstrap/Types.ts +++ b/packages/analytics/src/Bootstrap/Types.ts @@ -15,6 +15,7 @@ const TYPES = { // Repositories AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'), RevenueModificationRepository: Symbol.for('RevenueModificationRepository'), + StatisticMeasureRepository: Symbol.for('StatisticMeasureRepository'), // ORM ORMAnalyticsEntityRepository: Symbol.for('ORMAnalyticsEntityRepository'), ORMRevenueModificationRepository: Symbol.for('ORMRevenueModificationRepository'), @@ -22,6 +23,7 @@ const TYPES = { GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'), SaveRevenueModification: Symbol.for('SaveRevenueModification'), CalculateMonthlyRecurringRevenue: Symbol.for('CalculateMonthlyRecurringRevenue'), + PersistStatistic: Symbol.for('PersistStatistic'), // Handlers UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'), AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'), @@ -34,6 +36,7 @@ const TYPES = { SubscriptionExpiredEventHandler: Symbol.for('SubscriptionExpiredEventHandler'), SubscriptionReactivatedEventHandler: Symbol.for('SubscriptionReactivatedEventHandler'), RefundProcessedEventHandler: Symbol.for('RefundProcessedEventHandler'), + StatisticPersistenceRequestedEventHandler: Symbol.for('StatisticPersistenceRequestedEventHandler'), // Maps RevenueModificationMap: Symbol.for('RevenueModificationMap'), // Services diff --git a/packages/analytics/src/Domain/Email/daily-analytics-report.html.ts b/packages/analytics/src/Domain/Email/daily-analytics-report.html.ts index 34f84967a..803dcad26 100644 --- a/packages/analytics/src/Domain/Email/daily-analytics-report.html.ts +++ b/packages/analytics/src/Domain/Email/daily-analytics-report.html.ts @@ -2,7 +2,7 @@ import { TimerInterface } from '@standardnotes/time' import { AnalyticsActivity } from '../Analytics/AnalyticsActivity' -import { StatisticsMeasure } from '../Statistics/StatisticsMeasure' +import { StatisticMeasureName } from '../Statistics/StatisticMeasureName' import { Period } from '../Time/Period' const getChartUrls = ( @@ -417,156 +417,170 @@ export const html = (data: any, timer: TimerInterface) => { a.name === AnalyticsActivity.DeleteAccount && a.period === Period.Last30Days, ) const incomeMeasureYesterday = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.Income && a.period === Period.Yesterday, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.Income && a.period === Period.Yesterday, ) const refundMeasureYesterday = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.Refunds && a.period === Period.Yesterday, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.Refunds && a.period === Period.Yesterday, ) const incomeYesterday = incomeMeasureYesterday?.totalValue ?? 0 const refundsYesterday = refundMeasureYesterday?.totalValue ?? 0 const revenueYesterday = incomeYesterday - refundsYesterday const subscriptionLengthMeasureYesterday = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.SubscriptionLength && a.period === Period.Yesterday, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.SubscriptionLength && a.period === Period.Yesterday, ) const subscriptionLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure( Math.floor(subscriptionLengthMeasureYesterday?.average ?? 0), ) const subscriptionRemainingTimePercentageMeasureYesterday = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.RemainingSubscriptionTimePercentage && a.period === Period.Yesterday, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage && a.period === Period.Yesterday, ) const subscriptionRemainingTimePercentageYesterday = Math.floor( subscriptionRemainingTimePercentageMeasureYesterday?.average ?? 0, ) const registrationLengthMeasureYesterday = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.RegistrationLength && a.period === Period.Yesterday, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.RegistrationLength && a.period === Period.Yesterday, ) const registrationLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure( Math.floor(registrationLengthMeasureYesterday?.average ?? 0), ) const registrationToSubscriptionMeasureYesterday = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.RegistrationToSubscriptionTime && a.period === Period.Yesterday, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.RegistrationToSubscriptionTime && a.period === Period.Yesterday, ) const registrationToSubscriptionDurationYesterday = timer.convertMicrosecondsToTimeStructure( Math.floor(registrationToSubscriptionMeasureYesterday?.average ?? 0), ) const incomeMeasureThisMonth = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.Income && a.period === Period.ThisMonth, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.Income && a.period === Period.ThisMonth, ) const refundMeasureThisMonth = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.Refunds && a.period === Period.ThisMonth, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.Refunds && a.period === Period.ThisMonth, ) const incomeThisMonth = incomeMeasureThisMonth?.totalValue ?? 0 const refundsThisMonth = refundMeasureThisMonth?.totalValue ?? 0 const revenueThisMonth = incomeThisMonth - refundsThisMonth const subscriptionLengthMeasureThisMonth = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.SubscriptionLength && a.period === Period.ThisMonth, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.SubscriptionLength && a.period === Period.ThisMonth, ) const subscriptionLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure( Math.floor(subscriptionLengthMeasureThisMonth?.average ?? 0), ) const subscriptionRemainingTimePercentageMeasureThisMonth = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.RemainingSubscriptionTimePercentage && a.period === Period.ThisMonth, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage && a.period === Period.ThisMonth, ) const subscriptionRemainingTimePercentageThisMonth = Math.floor( subscriptionRemainingTimePercentageMeasureThisMonth?.average ?? 0, ) const registrationLengthMeasureThisMonth = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.RegistrationLength && a.period === Period.ThisMonth, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.RegistrationLength && a.period === Period.ThisMonth, ) const registrationLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure( Math.floor(registrationLengthMeasureThisMonth?.average ?? 0), ) const registrationToSubscriptionMeasureThisMonth = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.RegistrationToSubscriptionTime && a.period === Period.ThisMonth, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.RegistrationToSubscriptionTime && a.period === Period.ThisMonth, ) const registrationToSubscriptionDurationThisMonth = timer.convertMicrosecondsToTimeStructure( Math.floor(registrationToSubscriptionMeasureThisMonth?.average ?? 0), ) const plusSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome && + a.period === Period.Yesterday, ) const plusSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome && + a.period === Period.Yesterday, ) const plusSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome && + a.period === Period.Yesterday, ) const plusSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome && + a.period === Period.Yesterday, ) const proSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday, ) const proSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome && + a.period === Period.Yesterday, ) const proSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome && + a.period === Period.Yesterday, ) const proSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome && + a.period === Period.Yesterday, ) const plusSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome && + a.period === Period.ThisMonth, ) const plusSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome && + a.period === Period.ThisMonth, ) const plusSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome && + a.period === Period.ThisMonth, ) const plusSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome && + a.period === Period.ThisMonth, ) const proSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth, ) const proSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome && + a.period === Period.ThisMonth, ) const proSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome && + a.period === Period.ThisMonth, ) const proSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find( - (a: { name: StatisticsMeasure; period: Period }) => - a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth, + (a: { name: string; period: Period }) => + a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome && + a.period === Period.ThisMonth, ) const mrrOverTime = data.statisticsOverTime.find( diff --git a/packages/analytics/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts b/packages/analytics/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts deleted file mode 100644 index 68168fd86..000000000 --- a/packages/analytics/src/Domain/Handler/AccountDeletionRequestedEventHandler.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import 'reflect-metadata' - -import { AccountDeletionRequestedEvent } from '@standardnotes/domain-events' -import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler' -import { TimerInterface } from '@standardnotes/time' -import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' -import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface' -import { Period } from '../Time/Period' -import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface' - -describe('AccountDeletionRequestedEventHandler', () => { - let event: AccountDeletionRequestedEvent - let analyticsEntityRepository: AnalyticsEntityRepositoryInterface - let analyticsStore: AnalyticsStoreInterface - let statisticsStore: StatisticsStoreInterface - let timer: TimerInterface - - const createHandler = () => - new AccountDeletionRequestedEventHandler(analyticsEntityRepository, analyticsStore, statisticsStore, timer) - - beforeEach(() => { - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.payload = { - userUuid: '1-2-3', - userCreatedAtTimestamp: 1, - regularSubscriptionUuid: '2-3-4', - } - - analyticsEntityRepository = {} as jest.Mocked - analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue({ id: 3 }) - analyticsEntityRepository.remove = jest.fn() - - analyticsStore = {} as jest.Mocked - analyticsStore.markActivity = jest.fn() - - statisticsStore = {} as jest.Mocked - statisticsStore.incrementMeasure = jest.fn() - - timer = {} as jest.Mocked - timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123) - }) - - it('should mark account deletion and registration length', async () => { - await createHandler().handle(event) - - expect(analyticsStore.markActivity).toHaveBeenCalledWith(['DeleteAccount'], 3, [ - Period.Today, - Period.ThisWeek, - Period.ThisMonth, - ]) - expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith('registration-length', 122, [ - Period.Today, - Period.ThisWeek, - Period.ThisMonth, - ]) - expect(analyticsEntityRepository.remove).toHaveBeenCalled() - }) - - it('should not mark anything if entity is not found', async () => { - analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue(null) - - await createHandler().handle(event) - - expect(analyticsStore.markActivity).not.toHaveBeenCalled() - expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled() - expect(analyticsEntityRepository.remove).not.toHaveBeenCalled() - }) -}) diff --git a/packages/analytics/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts b/packages/analytics/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts index 1aa03417b..94c876e8a 100644 --- a/packages/analytics/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts +++ b/packages/analytics/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts @@ -6,7 +6,7 @@ import TYPES from '../../Bootstrap/Types' import { AnalyticsActivity } from '../Analytics/AnalyticsActivity' import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface' -import { StatisticsMeasure } from '../Statistics/StatisticsMeasure' +import { StatisticMeasureName } from '../Statistics/StatisticMeasureName' import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface' import { Period } from '../Time/Period' @@ -33,7 +33,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI ]) const registrationLength = this.timer.getTimestampInMicroseconds() - event.payload.userCreatedAtTimestamp - await this.statisticsStore.incrementMeasure(StatisticsMeasure.RegistrationLength, registrationLength, [ + await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.RegistrationLength, registrationLength, [ Period.Today, Period.ThisWeek, Period.ThisMonth, diff --git a/packages/analytics/src/Domain/Handler/PaymentFailedEventHandler.spec.ts b/packages/analytics/src/Domain/Handler/PaymentFailedEventHandler.spec.ts deleted file mode 100644 index 40c1d2f50..000000000 --- a/packages/analytics/src/Domain/Handler/PaymentFailedEventHandler.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import 'reflect-metadata' - -import { PaymentFailedEvent } from '@standardnotes/domain-events' - -import { PaymentFailedEventHandler } from './PaymentFailedEventHandler' -import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId' -import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' - -describe('PaymentFailedEventHandler', () => { - let event: PaymentFailedEvent - let getUserAnalyticsId: GetUserAnalyticsId - let analyticsStore: AnalyticsStoreInterface - - const createHandler = () => new PaymentFailedEventHandler(getUserAnalyticsId, analyticsStore) - - beforeEach(() => { - getUserAnalyticsId = {} as jest.Mocked - getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 }) - - analyticsStore = {} as jest.Mocked - analyticsStore.markActivity = jest.fn() - - event = {} as jest.Mocked - event.payload = { - userEmail: 'test@test.com', - } - }) - - it('should mark payment failed for analytics', async () => { - await createHandler().handle(event) - - expect(analyticsStore.markActivity).toHaveBeenCalled() - }) -}) diff --git a/packages/analytics/src/Domain/Handler/PaymentSuccessEventHandler.spec.ts b/packages/analytics/src/Domain/Handler/PaymentSuccessEventHandler.spec.ts deleted file mode 100644 index b07f5d42a..000000000 --- a/packages/analytics/src/Domain/Handler/PaymentSuccessEventHandler.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import 'reflect-metadata' - -import { PaymentSuccessEvent } from '@standardnotes/domain-events' - -import { PaymentSuccessEventHandler } from './PaymentSuccessEventHandler' -import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId' -import { Logger } from 'winston' -import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface' -import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' -import { Period } from '../Time/Period' - -describe('PaymentSuccessEventHandler', () => { - let event: PaymentSuccessEvent - let getUserAnalyticsId: GetUserAnalyticsId - let analyticsStore: AnalyticsStoreInterface - let statisticsStore: StatisticsStoreInterface - let logger: Logger - - const createHandler = () => - new PaymentSuccessEventHandler(getUserAnalyticsId, analyticsStore, statisticsStore, logger) - - beforeEach(() => { - getUserAnalyticsId = {} as jest.Mocked - getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 }) - - analyticsStore = {} as jest.Mocked - analyticsStore.markActivity = jest.fn() - - statisticsStore = {} as jest.Mocked - statisticsStore.incrementMeasure = jest.fn() - - event = {} as jest.Mocked - event.payload = { - userEmail: 'test@test.com', - amount: 12.45, - billingFrequency: 12, - paymentType: 'initial', - subscriptionName: 'PRO_PLAN', - } - - logger = {} as jest.Mocked - logger.warn = jest.fn() - }) - - it('should mark payment success for analytics', async () => { - await createHandler().handle(event) - - expect(analyticsStore.markActivity).toHaveBeenCalled() - expect(statisticsStore.incrementMeasure).toHaveBeenNthCalledWith( - 2, - 'pro-subscription-initial-annual-payments-income', - 12.45, - [Period.Today, Period.ThisWeek, Period.ThisMonth], - ) - }) - - it('should mark non-detailed payment success statistics for analytics', async () => { - event.payload = { - userEmail: 'test@test.com', - amount: 12.45, - billingFrequency: 13, - paymentType: 'initial', - subscriptionName: 'PRO_PLAN', - } - - await createHandler().handle(event) - - expect(statisticsStore.incrementMeasure).toBeCalledTimes(1) - expect(statisticsStore.incrementMeasure).toHaveBeenNthCalledWith(1, 'income', 12.45, [ - Period.Today, - Period.ThisWeek, - Period.ThisMonth, - ]) - }) -}) diff --git a/packages/analytics/src/Domain/Handler/PaymentSuccessEventHandler.ts b/packages/analytics/src/Domain/Handler/PaymentSuccessEventHandler.ts index a299bdde7..25fa91bff 100644 --- a/packages/analytics/src/Domain/Handler/PaymentSuccessEventHandler.ts +++ b/packages/analytics/src/Domain/Handler/PaymentSuccessEventHandler.ts @@ -6,7 +6,7 @@ import { Logger } from 'winston' import TYPES from '../../Bootstrap/Types' import { AnalyticsActivity } from '../Analytics/AnalyticsActivity' import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' -import { StatisticsMeasure } from '../Statistics/StatisticsMeasure' +import { StatisticMeasureName } from '../Statistics/StatisticMeasureName' import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface' import { Period } from '../Time/Period' import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId' @@ -20,15 +20,27 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface { [ PaymentType.Initial, new Map([ - [SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome], - [SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome], + [ + SubscriptionBillingFrequency.Monthly, + StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome, + ], + [ + SubscriptionBillingFrequency.Annual, + StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome, + ], ]), ], [ PaymentType.Renewal, new Map([ - [SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome], - [SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome], + [ + SubscriptionBillingFrequency.Monthly, + StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome, + ], + [ + SubscriptionBillingFrequency.Annual, + StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome, + ], ]), ], ]), @@ -39,15 +51,27 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface { [ PaymentType.Initial, new Map([ - [SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome], - [SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome], + [ + SubscriptionBillingFrequency.Monthly, + StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome, + ], + [ + SubscriptionBillingFrequency.Annual, + StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome, + ], ]), ], [ PaymentType.Renewal, new Map([ - [SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome], - [SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome], + [ + SubscriptionBillingFrequency.Monthly, + StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome, + ], + [ + SubscriptionBillingFrequency.Annual, + StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome, + ], ]), ], ]), @@ -69,7 +93,7 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface { Period.ThisMonth, ]) - const statisticMeasures = [StatisticsMeasure.Income] + const statisticMeasures = [StatisticMeasureName.NAMES.Income] const detailedMeasure = this.DETAILED_MEASURES.get(event.payload.subscriptionName as SubscriptionName) ?.get(event.payload.paymentType as PaymentType) diff --git a/packages/analytics/src/Domain/Handler/RefundProcessedEventHandler.spec.ts b/packages/analytics/src/Domain/Handler/RefundProcessedEventHandler.spec.ts deleted file mode 100644 index 459f767a7..000000000 --- a/packages/analytics/src/Domain/Handler/RefundProcessedEventHandler.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import 'reflect-metadata' - -import { RefundProcessedEvent } from '@standardnotes/domain-events' - -import { RefundProcessedEventHandler } from './RefundProcessedEventHandler' -import { StatisticsMeasure } from '../Statistics/StatisticsMeasure' -import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface' -import { Period } from '../Time/Period' - -describe('RefundProcessedEventHandler', () => { - let event: RefundProcessedEvent - let statisticsStore: StatisticsStoreInterface - - const createHandler = () => new RefundProcessedEventHandler(statisticsStore) - - beforeEach(() => { - statisticsStore = {} as jest.Mocked - statisticsStore.incrementMeasure = jest.fn() - - event = {} as jest.Mocked - event.payload = { - userEmail: 'test@test.com', - amount: 12.45, - } - }) - - it('should mark refunds for statistics', async () => { - await createHandler().handle(event) - - expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith(StatisticsMeasure.Refunds, 12.45, [ - Period.Today, - Period.ThisWeek, - Period.ThisMonth, - ]) - }) -}) diff --git a/packages/analytics/src/Domain/Handler/RefundProcessedEventHandler.ts b/packages/analytics/src/Domain/Handler/RefundProcessedEventHandler.ts index 68a57946c..275e412de 100644 --- a/packages/analytics/src/Domain/Handler/RefundProcessedEventHandler.ts +++ b/packages/analytics/src/Domain/Handler/RefundProcessedEventHandler.ts @@ -2,7 +2,7 @@ import { DomainEventHandlerInterface, RefundProcessedEvent } from '@standardnote import { inject, injectable } from 'inversify' import TYPES from '../../Bootstrap/Types' -import { StatisticsMeasure } from '../Statistics/StatisticsMeasure' +import { StatisticMeasureName } from '../Statistics/StatisticMeasureName' import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface' import { Period } from '../Time/Period' @@ -11,7 +11,7 @@ export class RefundProcessedEventHandler implements DomainEventHandlerInterface constructor(@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface) {} async handle(event: RefundProcessedEvent): Promise { - await this.statisticsStore.incrementMeasure(StatisticsMeasure.Refunds, event.payload.amount, [ + await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.Refunds, event.payload.amount, [ Period.Today, Period.ThisWeek, Period.ThisMonth, diff --git a/packages/analytics/src/Domain/Handler/StatisticPersistenceRequestedEventHandler.ts b/packages/analytics/src/Domain/Handler/StatisticPersistenceRequestedEventHandler.ts new file mode 100644 index 000000000..418573ad3 --- /dev/null +++ b/packages/analytics/src/Domain/Handler/StatisticPersistenceRequestedEventHandler.ts @@ -0,0 +1,19 @@ +import { DomainEventHandlerInterface, StatisticPersistenceRequestedEvent } from '@standardnotes/domain-events' +import { Logger } from 'winston' +import { PersistStatistic } from '../UseCase/PersistStatistic/PersistStatistic' + +export class StatisticPersistenceRequestedEventHandler implements DomainEventHandlerInterface { + constructor(private persistStatistic: PersistStatistic, private logger: Logger) {} + + async handle(event: StatisticPersistenceRequestedEvent): Promise { + const result = await this.persistStatistic.execute({ + date: event.payload.date, + statisticMeasureName: event.payload.statisticMeasureName, + value: event.payload.value, + }) + + if (result.isFailed()) { + this.logger.error(result.getError()) + } + } +} diff --git a/packages/analytics/src/Domain/Handler/SubscriptionCancelledEventHandler.spec.ts b/packages/analytics/src/Domain/Handler/SubscriptionCancelledEventHandler.spec.ts deleted file mode 100644 index c45a0e53f..000000000 --- a/packages/analytics/src/Domain/Handler/SubscriptionCancelledEventHandler.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import 'reflect-metadata' - -import { SubscriptionName } from '@standardnotes/common' -import { Result } from '@standardnotes/domain-core' -import { SubscriptionCancelledEvent } from '@standardnotes/domain-events' - -import { SubscriptionCancelledEventHandler } from './SubscriptionCancelledEventHandler' -import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId' -import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' -import { StatisticsMeasure } from '../Statistics/StatisticsMeasure' -import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface' -import { Period } from '../Time/Period' -import { RevenueModification } from '../Revenue/RevenueModification' -import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification' -import { Logger } from 'winston' - -describe('SubscriptionCancelledEventHandler', () => { - let event: SubscriptionCancelledEvent - let getUserAnalyticsId: GetUserAnalyticsId - let analyticsStore: AnalyticsStoreInterface - let statisticsStore: StatisticsStoreInterface - let saveRevenueModification: SaveRevenueModification - let logger: Logger - - const createHandler = () => - new SubscriptionCancelledEventHandler( - getUserAnalyticsId, - analyticsStore, - statisticsStore, - saveRevenueModification, - logger, - ) - - beforeEach(() => { - logger = {} as jest.Mocked - logger.error = jest.fn() - - getUserAnalyticsId = {} as jest.Mocked - getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 }) - - analyticsStore = {} as jest.Mocked - analyticsStore.markActivity = jest.fn() - - statisticsStore = {} as jest.Mocked - statisticsStore.incrementMeasure = jest.fn() - - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.type = 'SUBSCRIPTION_CANCELLED' - event.payload = { - subscriptionId: 1, - userEmail: 'test@test.com', - subscriptionName: SubscriptionName.ProPlan, - subscriptionCreatedAt: 1642395451515000, - subscriptionUpdatedAt: 1642395451515001, - lastPayedAt: 1642395451515001, - subscriptionEndsAt: 1642395451515000 + 10, - timestamp: 1, - offline: false, - replaced: false, - userExistingSubscriptionsCount: 1, - billingFrequency: 1, - payAmount: 12.99, - } - - saveRevenueModification = {} as jest.Mocked - saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok()) - }) - - it('should track subscription cancelled statistics', async () => { - event.payload.timestamp = 1642395451516000 - - await createHandler().handle(event) - - expect(analyticsStore.markActivity).toHaveBeenCalled() - expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith(StatisticsMeasure.SubscriptionLength, 1000, [ - Period.Today, - Period.ThisWeek, - Period.ThisMonth, - ]) - expect(saveRevenueModification.execute).toHaveBeenCalled() - }) - - it('should not track statistics for subscriptions that are in a legacy 5 year plan', async () => { - event.payload.timestamp = 1642395451516000 - event.payload.subscriptionEndsAt = 1642395451515000 + 126_230_400_000_001 - event.payload.subscriptionCreatedAt = 1642395451515000 - - await createHandler().handle(event) - - expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled() - expect(saveRevenueModification.execute).toHaveBeenCalled() - }) - - it('should log failure to save revenue modification', async () => { - saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops')) - - event.payload.timestamp = 1642395451516000 - - await createHandler().handle(event) - - expect(logger.error).toHaveBeenCalled() - }) -}) diff --git a/packages/analytics/src/Domain/Handler/SubscriptionCancelledEventHandler.ts b/packages/analytics/src/Domain/Handler/SubscriptionCancelledEventHandler.ts index 4d912b070..79a9c832f 100644 --- a/packages/analytics/src/Domain/Handler/SubscriptionCancelledEventHandler.ts +++ b/packages/analytics/src/Domain/Handler/SubscriptionCancelledEventHandler.ts @@ -6,13 +6,13 @@ import { Username } from '@standardnotes/domain-core' import TYPES from '../../Bootstrap/Types' import { AnalyticsActivity } from '../Analytics/AnalyticsActivity' import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' -import { StatisticsMeasure } from '../Statistics/StatisticsMeasure' import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface' import { SubscriptionEventType } from '../Subscription/SubscriptionEventType' import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName' import { Period } from '../Time/Period' import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId' import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification' +import { StatisticMeasureName } from '../Statistics/StatisticMeasureName' @injectable() export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface { @@ -58,7 +58,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte } const subscriptionLength = event.payload.timestamp - event.payload.subscriptionCreatedAt - await this.statisticsStore.incrementMeasure(StatisticsMeasure.SubscriptionLength, subscriptionLength, [ + await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.SubscriptionLength, subscriptionLength, [ Period.Today, Period.ThisWeek, Period.ThisMonth, @@ -70,7 +70,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100) await this.statisticsStore.incrementMeasure( - StatisticsMeasure.RemainingSubscriptionTimePercentage, + StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage, remainingSubscriptionPercentage, [Period.Today, Period.ThisWeek, Period.ThisMonth], ) diff --git a/packages/analytics/src/Domain/Handler/SubscriptionExpiredEventHandler.spec.ts b/packages/analytics/src/Domain/Handler/SubscriptionExpiredEventHandler.spec.ts deleted file mode 100644 index 53941fbea..000000000 --- a/packages/analytics/src/Domain/Handler/SubscriptionExpiredEventHandler.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -import 'reflect-metadata' - -import { SubscriptionName } from '@standardnotes/common' -import { SubscriptionExpiredEvent } from '@standardnotes/domain-events' -import { Result } from '@standardnotes/domain-core' - -import { SubscriptionExpiredEventHandler } from './SubscriptionExpiredEventHandler' -import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId' -import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface' -import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' -import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification' -import { RevenueModification } from '../Revenue/RevenueModification' -import { Logger } from 'winston' - -describe('SubscriptionExpiredEventHandler', () => { - let event: SubscriptionExpiredEvent - let getUserAnalyticsId: GetUserAnalyticsId - let analyticsStore: AnalyticsStoreInterface - let statisticsStore: StatisticsStoreInterface - let saveRevenueModification: SaveRevenueModification - let logger: Logger - - const createHandler = () => - new SubscriptionExpiredEventHandler( - getUserAnalyticsId, - analyticsStore, - statisticsStore, - saveRevenueModification, - logger, - ) - - beforeEach(() => { - logger = {} as jest.Mocked - logger.error = jest.fn() - - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.type = 'SUBSCRIPTION_EXPIRED' - event.payload = { - subscriptionId: 1, - userEmail: 'test@test.com', - subscriptionName: SubscriptionName.PlusPlan, - timestamp: 1, - offline: false, - totalActiveSubscriptionsCount: 123, - userExistingSubscriptionsCount: 2, - billingFrequency: 1, - payAmount: 12.99, - } - - getUserAnalyticsId = {} as jest.Mocked - getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 }) - - analyticsStore = {} as jest.Mocked - analyticsStore.markActivity = jest.fn() - - statisticsStore = {} as jest.Mocked - statisticsStore.setMeasure = jest.fn() - - saveRevenueModification = {} as jest.Mocked - saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok()) - }) - - it('should update analytics and statistics', async () => { - await createHandler().handle(event) - - expect(analyticsStore.markActivity).toHaveBeenCalled() - expect(statisticsStore.setMeasure).toHaveBeenCalled() - expect(saveRevenueModification.execute).toHaveBeenCalled() - }) - - it('should log failure to save revenue modification', async () => { - saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops')) - - await createHandler().handle(event) - - expect(logger.error).toHaveBeenCalled() - }) -}) diff --git a/packages/analytics/src/Domain/Handler/SubscriptionExpiredEventHandler.ts b/packages/analytics/src/Domain/Handler/SubscriptionExpiredEventHandler.ts index 6b35eddf3..e534bef0e 100644 --- a/packages/analytics/src/Domain/Handler/SubscriptionExpiredEventHandler.ts +++ b/packages/analytics/src/Domain/Handler/SubscriptionExpiredEventHandler.ts @@ -6,7 +6,7 @@ import { Logger } from 'winston' import TYPES from '../../Bootstrap/Types' import { AnalyticsActivity } from '../Analytics/AnalyticsActivity' import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' -import { StatisticsMeasure } from '../Statistics/StatisticsMeasure' +import { StatisticMeasureName } from '../Statistics/StatisticMeasureName' import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface' import { SubscriptionEventType } from '../Subscription/SubscriptionEventType' import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName' @@ -33,7 +33,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf ) await this.statisticsStore.setMeasure( - StatisticsMeasure.TotalCustomers, + StatisticMeasureName.NAMES.TotalCustomers, event.payload.totalActiveSubscriptionsCount, [Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear], ) diff --git a/packages/analytics/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts b/packages/analytics/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts deleted file mode 100644 index b93c233bf..000000000 --- a/packages/analytics/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import 'reflect-metadata' - -import { SubscriptionName } from '@standardnotes/common' -import { SubscriptionPurchasedEvent } from '@standardnotes/domain-events' -import { Result } from '@standardnotes/domain-core' - -import { SubscriptionPurchasedEventHandler } from './SubscriptionPurchasedEventHandler' -import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId' -import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' -import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface' -import { Period } from '../Time/Period' -import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification' -import { RevenueModification } from '../Revenue/RevenueModification' -import { Logger } from 'winston' - -describe('SubscriptionPurchasedEventHandler', () => { - let event: SubscriptionPurchasedEvent - let subscriptionExpiresAt: number - let getUserAnalyticsId: GetUserAnalyticsId - let analyticsStore: AnalyticsStoreInterface - let statisticsStore: StatisticsStoreInterface - let saveRevenueModification: SaveRevenueModification - let logger: Logger - - const createHandler = () => - new SubscriptionPurchasedEventHandler( - getUserAnalyticsId, - analyticsStore, - statisticsStore, - saveRevenueModification, - logger, - ) - - beforeEach(() => { - logger = {} as jest.Mocked - logger.error = jest.fn() - - statisticsStore = {} as jest.Mocked - statisticsStore.incrementMeasure = jest.fn() - statisticsStore.setMeasure = jest.fn() - - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.type = 'SUBSCRIPTION_PURCHASED' - event.payload = { - subscriptionId: 1, - userEmail: 'test@test.com', - subscriptionName: SubscriptionName.ProPlan, - subscriptionExpiresAt, - timestamp: 60, - offline: false, - discountCode: null, - limitedDiscountPurchased: false, - newSubscriber: true, - totalActiveSubscriptionsCount: 123, - userRegisteredAt: 23, - billingFrequency: 12, - payAmount: 29.99, - } - - getUserAnalyticsId = {} as jest.Mocked - getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 }) - - analyticsStore = {} as jest.Mocked - analyticsStore.markActivity = jest.fn() - analyticsStore.unmarkActivity = jest.fn() - - saveRevenueModification = {} as jest.Mocked - saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok()) - }) - - it('should mark subscription creation statistics', async () => { - await createHandler().handle(event) - - expect(statisticsStore.incrementMeasure).toHaveBeenCalled() - expect(saveRevenueModification.execute).toHaveBeenCalled() - }) - - it("should not measure registration to subscription time if this is not user's first subscription", async () => { - event.payload.newSubscriber = false - - await createHandler().handle(event) - - expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled() - }) - - it('should update analytics on limited discount offer purchasing', async () => { - event.payload.limitedDiscountPurchased = true - - await createHandler().handle(event) - - expect(analyticsStore.markActivity).toHaveBeenCalledWith(['limited-discount-offer-purchased'], 3, [Period.Today]) - }) - - it('should log failure to save revenue modification', async () => { - saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops')) - - await createHandler().handle(event) - - expect(logger.error).toHaveBeenCalled() - }) -}) diff --git a/packages/analytics/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts b/packages/analytics/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts index 0366cea89..6936d0daf 100644 --- a/packages/analytics/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts +++ b/packages/analytics/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts @@ -6,7 +6,7 @@ import { Logger } from 'winston' import TYPES from '../../Bootstrap/Types' import { AnalyticsActivity } from '../Analytics/AnalyticsActivity' import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' -import { StatisticsMeasure } from '../Statistics/StatisticsMeasure' +import { StatisticMeasureName } from '../Statistics/StatisticMeasureName' import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface' import { SubscriptionEventType } from '../Subscription/SubscriptionEventType' import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName' @@ -45,18 +45,18 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte if (event.payload.newSubscriber) { await this.statisticsStore.incrementMeasure( - StatisticsMeasure.RegistrationToSubscriptionTime, + StatisticMeasureName.NAMES.RegistrationToSubscriptionTime, event.payload.timestamp - event.payload.userRegisteredAt, [Period.Today, Period.ThisWeek, Period.ThisMonth], ) - await this.statisticsStore.incrementMeasure(StatisticsMeasure.NewCustomers, 1, [ + await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.NewCustomers, 1, [ Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear, ]) await this.statisticsStore.setMeasure( - StatisticsMeasure.TotalCustomers, + StatisticMeasureName.NAMES.TotalCustomers, event.payload.totalActiveSubscriptionsCount, [Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear], ) diff --git a/packages/analytics/src/Domain/Handler/SubscriptionReactivatedEventHandler.spec.ts b/packages/analytics/src/Domain/Handler/SubscriptionReactivatedEventHandler.spec.ts deleted file mode 100644 index 3cb6a280c..000000000 --- a/packages/analytics/src/Domain/Handler/SubscriptionReactivatedEventHandler.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import 'reflect-metadata' - -import { SubscriptionName } from '@standardnotes/common' -import { SubscriptionReactivatedEvent } from '@standardnotes/domain-events' - -import { SubscriptionReactivatedEventHandler } from './SubscriptionReactivatedEventHandler' -import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId' -import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' -import { Period } from '../Time/Period' - -describe('SubscriptionReactivatedEventHandler', () => { - let event: SubscriptionReactivatedEvent - let getUserAnalyticsId: GetUserAnalyticsId - let analyticsStore: AnalyticsStoreInterface - - const createHandler = () => new SubscriptionReactivatedEventHandler(analyticsStore, getUserAnalyticsId) - - beforeEach(() => { - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.payload = { - previousSubscriptionId: 1, - currentSubscriptionId: 2, - userEmail: 'test@test.com', - subscriptionName: SubscriptionName.PlusPlan, - subscriptionExpiresAt: 5, - discountCode: 'exit-20', - } - - getUserAnalyticsId = {} as jest.Mocked - getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 }) - - analyticsStore = {} as jest.Mocked - analyticsStore.markActivity = jest.fn() - }) - - it('should mark subscription reactivated activity for analytics', async () => { - await createHandler().handle(event) - - expect(analyticsStore.markActivity).toHaveBeenCalledWith(['subscription-reactivated'], 3, [ - Period.Today, - Period.ThisWeek, - Period.ThisMonth, - ]) - }) -}) diff --git a/packages/analytics/src/Domain/Handler/SubscriptionRefundedEventHandler.spec.ts b/packages/analytics/src/Domain/Handler/SubscriptionRefundedEventHandler.spec.ts deleted file mode 100644 index 05cb4ead7..000000000 --- a/packages/analytics/src/Domain/Handler/SubscriptionRefundedEventHandler.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import 'reflect-metadata' - -import { SubscriptionName } from '@standardnotes/common' -import { SubscriptionRefundedEvent } from '@standardnotes/domain-events' -import { Result } from '@standardnotes/domain-core' - -import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId' -import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' - -import { SubscriptionRefundedEventHandler } from './SubscriptionRefundedEventHandler' -import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface' -import { AnalyticsActivity } from '../Analytics/AnalyticsActivity' -import { Period } from '../Time/Period' -import { RevenueModification } from '../Revenue/RevenueModification' -import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification' -import { Logger } from 'winston' - -describe('SubscriptionRefundedEventHandler', () => { - let event: SubscriptionRefundedEvent - let getUserAnalyticsId: GetUserAnalyticsId - let analyticsStore: AnalyticsStoreInterface - let statisticsStore: StatisticsStoreInterface - let saveRevenueModification: SaveRevenueModification - let logger: Logger - - const createHandler = () => - new SubscriptionRefundedEventHandler( - getUserAnalyticsId, - analyticsStore, - statisticsStore, - saveRevenueModification, - logger, - ) - - beforeEach(() => { - logger = {} as jest.Mocked - logger.error = jest.fn() - - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.type = 'SUBSCRIPTION_REFUNDED' - event.payload = { - subscriptionId: 1, - userEmail: 'test@test.com', - subscriptionName: SubscriptionName.PlusPlan, - timestamp: 1, - offline: false, - userExistingSubscriptionsCount: 3, - totalActiveSubscriptionsCount: 1, - billingFrequency: 1, - payAmount: 12.99, - } - - getUserAnalyticsId = {} as jest.Mocked - getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 }) - - analyticsStore = {} as jest.Mocked - analyticsStore.markActivity = jest.fn() - analyticsStore.wasActivityDone = jest.fn().mockReturnValue(true) - - statisticsStore = {} as jest.Mocked - statisticsStore.setMeasure = jest.fn() - - saveRevenueModification = {} as jest.Mocked - saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok()) - }) - - it('should mark churn for new customer', async () => { - event.payload.userExistingSubscriptionsCount = 1 - await createHandler().handle(event) - - expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.SubscriptionRefunded], 3, [ - Period.Today, - Period.ThisWeek, - Period.ThisMonth, - ]) - - expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.NewCustomersChurn], 3, [ - Period.ThisMonth, - ]) - - expect(saveRevenueModification.execute).toHaveBeenCalled() - }) - - it('should mark churn for existing customer', async () => { - await createHandler().handle(event) - - expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.ExistingCustomersChurn], 3, [ - Period.ThisMonth, - ]) - }) - - it('should not mark churn if customer did not purchase subscription in defined analytic periods', async () => { - analyticsStore.wasActivityDone = jest.fn().mockReturnValue(false) - - await createHandler().handle(event) - - expect(analyticsStore.markActivity).not.toHaveBeenCalledWith([AnalyticsActivity.ExistingCustomersChurn], 3, [ - Period.ThisMonth, - ]) - }) - - it('should log failure to save revenue modification', async () => { - saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops')) - - await createHandler().handle(event) - - expect(logger.error).toHaveBeenCalled() - }) -}) diff --git a/packages/analytics/src/Domain/Handler/SubscriptionRefundedEventHandler.ts b/packages/analytics/src/Domain/Handler/SubscriptionRefundedEventHandler.ts index b8e7f59cd..6d1e3af70 100644 --- a/packages/analytics/src/Domain/Handler/SubscriptionRefundedEventHandler.ts +++ b/packages/analytics/src/Domain/Handler/SubscriptionRefundedEventHandler.ts @@ -6,7 +6,7 @@ import { Logger } from 'winston' import TYPES from '../../Bootstrap/Types' import { AnalyticsActivity } from '../Analytics/AnalyticsActivity' import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' -import { StatisticsMeasure } from '../Statistics/StatisticsMeasure' +import { StatisticMeasureName } from '../Statistics/StatisticMeasureName' import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface' import { SubscriptionEventType } from '../Subscription/SubscriptionEventType' import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName' @@ -70,7 +70,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter } await this.statisticsStore.setMeasure( - StatisticsMeasure.TotalCustomers, + StatisticMeasureName.NAMES.TotalCustomers, event.payload.totalActiveSubscriptionsCount, [Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear], ) diff --git a/packages/analytics/src/Domain/Handler/SubscriptionRenewedEventHandler.spec.ts b/packages/analytics/src/Domain/Handler/SubscriptionRenewedEventHandler.spec.ts deleted file mode 100644 index 42d8d1cf6..000000000 --- a/packages/analytics/src/Domain/Handler/SubscriptionRenewedEventHandler.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import 'reflect-metadata' - -import { SubscriptionName } from '@standardnotes/common' -import { SubscriptionRenewedEvent } from '@standardnotes/domain-events' -import { Result } from '@standardnotes/domain-core' - -import { SubscriptionRenewedEventHandler } from './SubscriptionRenewedEventHandler' -import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId' -import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' -import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification' -import { RevenueModification } from '../Revenue/RevenueModification' -import { Logger } from 'winston' - -describe('SubscriptionRenewedEventHandler', () => { - let event: SubscriptionRenewedEvent - let getUserAnalyticsId: GetUserAnalyticsId - let analyticsStore: AnalyticsStoreInterface - let saveRevenueModification: SaveRevenueModification - let logger: Logger - - const createHandler = () => - new SubscriptionRenewedEventHandler(getUserAnalyticsId, analyticsStore, saveRevenueModification, logger) - - beforeEach(() => { - logger = {} as jest.Mocked - logger.error = jest.fn() - - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.type = 'SUBSCRIPTION_RENEWED' - event.payload = { - subscriptionId: 1, - userEmail: 'test@test.com', - subscriptionName: SubscriptionName.ProPlan, - subscriptionExpiresAt: 2, - timestamp: 1, - offline: false, - billingFrequency: 1, - payAmount: 12.99, - } - - getUserAnalyticsId = {} as jest.Mocked - getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 }) - - analyticsStore = {} as jest.Mocked - analyticsStore.markActivity = jest.fn() - analyticsStore.unmarkActivity = jest.fn() - - saveRevenueModification = {} as jest.Mocked - saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok()) - }) - - it('should track subscription renewed statistics', async () => { - await createHandler().handle(event) - - expect(analyticsStore.markActivity).toHaveBeenCalled() - expect(analyticsStore.unmarkActivity).toHaveBeenCalled() - expect(saveRevenueModification.execute).toHaveBeenCalled() - }) - - it('should log failure to save revenue modification', async () => { - saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops')) - - await createHandler().handle(event) - - expect(logger.error).toHaveBeenCalled() - }) -}) diff --git a/packages/analytics/src/Domain/Handler/UserRegisteredEventHandler.spec.ts b/packages/analytics/src/Domain/Handler/UserRegisteredEventHandler.spec.ts deleted file mode 100644 index f1ce95e22..000000000 --- a/packages/analytics/src/Domain/Handler/UserRegisteredEventHandler.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import 'reflect-metadata' - -import { UserRegisteredEvent } from '@standardnotes/domain-events' -import { ProtocolVersion } from '@standardnotes/common' - -import { UserRegisteredEventHandler } from './UserRegisteredEventHandler' -import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface' -import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface' -import { Period } from '../Time/Period' - -describe('UserRegisteredEventHandler', () => { - let analyticsEntityRepository: AnalyticsEntityRepositoryInterface - let event: UserRegisteredEvent - let analyticsStore: AnalyticsStoreInterface - - const createHandler = () => new UserRegisteredEventHandler(analyticsEntityRepository, analyticsStore) - - beforeEach(() => { - event = {} as jest.Mocked - event.createdAt = new Date(1) - event.payload = { - userUuid: '1-2-3', - email: 'test@test.te', - protocolVersion: ProtocolVersion.V004, - } - - analyticsStore = {} as jest.Mocked - analyticsStore.markActivity = jest.fn() - - analyticsEntityRepository = {} as jest.Mocked - analyticsEntityRepository.save = jest.fn().mockImplementation((entity) => ({ - ...entity, - id: 1, - })) - }) - - it('should save analytics entity upon user registration', async () => { - await createHandler().handle(event) - - expect(analyticsEntityRepository.save).toHaveBeenCalled() - expect(analyticsStore.markActivity).toHaveBeenCalledWith(['register'], 1, [ - Period.Today, - Period.ThisWeek, - Period.ThisMonth, - ]) - }) -}) diff --git a/packages/analytics/src/Domain/Statistics/StatisticMeasure.ts b/packages/analytics/src/Domain/Statistics/StatisticMeasure.ts new file mode 100644 index 000000000..b7576ca31 --- /dev/null +++ b/packages/analytics/src/Domain/Statistics/StatisticMeasure.ts @@ -0,0 +1,25 @@ +import { Result, Entity, UniqueEntityId } from '@standardnotes/domain-core' + +import { StatisticMeasureProps } from './StatisticMeasureProps' + +export class StatisticMeasure extends Entity { + get id(): UniqueEntityId { + return this._id + } + + get name(): string { + return this.props.name.value + } + + get value(): number { + return this.props.value + } + + private constructor(props: StatisticMeasureProps, id?: UniqueEntityId) { + super(props, id) + } + + static create(props: StatisticMeasureProps, id?: UniqueEntityId): Result { + return Result.ok(new StatisticMeasure(props, id)) + } +} diff --git a/packages/analytics/src/Domain/Statistics/StatisticMeasureName.spec.ts b/packages/analytics/src/Domain/Statistics/StatisticMeasureName.spec.ts new file mode 100644 index 000000000..dff46d2f2 --- /dev/null +++ b/packages/analytics/src/Domain/Statistics/StatisticMeasureName.spec.ts @@ -0,0 +1,18 @@ +import { StatisticMeasureName } from './StatisticMeasureName' + +describe('StatisticMeasureName', () => { + it('should create a value object', () => { + const valueOrError = StatisticMeasureName.create('pro-subscription-initial-monthly-payments-income') + + expect(valueOrError.isFailed()).toBeFalsy() + expect(valueOrError.getValue().value).toEqual('pro-subscription-initial-monthly-payments-income') + }) + + it('should not create an invalid value object', () => { + for (const value of ['', undefined, null, 0, 'foobar']) { + const valueOrError = StatisticMeasureName.create(value as string) + + expect(valueOrError.isFailed()).toBeTruthy() + } + }) +}) diff --git a/packages/analytics/src/Domain/Statistics/StatisticMeasureName.ts b/packages/analytics/src/Domain/Statistics/StatisticMeasureName.ts new file mode 100644 index 000000000..12c40eb33 --- /dev/null +++ b/packages/analytics/src/Domain/Statistics/StatisticMeasureName.ts @@ -0,0 +1,47 @@ +import { ValueObject, Result } from '@standardnotes/domain-core' + +import { StatisticMeasureNameProps } from './StatisticMeasureNameProps' + +export class StatisticMeasureName extends ValueObject { + static readonly NAMES = { + Income: 'income', + PlusSubscriptionInitialMonthlyPaymentsIncome: 'plus-subscription-initial-monthly-payments-income', + ProSubscriptionInitialMonthlyPaymentsIncome: 'pro-subscription-initial-monthly-payments-income', + PlusSubscriptionInitialAnnualPaymentsIncome: 'plus-subscription-initial-annual-payments-income', + ProSubscriptionInitialAnnualPaymentsIncome: 'pro-subscription-initial-annual-payments-income', + PlusSubscriptionRenewingMonthlyPaymentsIncome: 'plus-subscription-renewing-monthly-payments-income', + ProSubscriptionRenewingMonthlyPaymentsIncome: 'pro-subscription-renewing-monthly-payments-income', + PlusSubscriptionRenewingAnnualPaymentsIncome: 'plus-subscription-renewing-annual-payments-income', + ProSubscriptionRenewingAnnualPaymentsIncome: 'pro-subscription-renewing-annual-payments-income', + SubscriptionLength: 'subscription-length', + RegistrationLength: 'registration-length', + RegistrationToSubscriptionTime: 'registration-to-subscription-time', + RemainingSubscriptionTimePercentage: 'remaining-subscription-time-percentage', + Refunds: 'refunds', + NewCustomers: 'new-customers', + TotalCustomers: 'total-customers', + MRR: 'mrr', + MonthlyPlansMRR: 'monthly-plans-mrr', + AnnualPlansMRR: 'annual-plans-mrr', + FiveYearPlansMRR: 'five-year-plans-mrr', + ProPlansMRR: 'pro-plans-mrr', + PlusPlansMRR: 'plus-plans-mrr', + } + + get value(): string { + return this.props.value + } + + private constructor(props: StatisticMeasureNameProps) { + super(props) + } + + static create(name: string): Result { + const isValidName = Object.values(this.NAMES).includes(name) + if (!isValidName) { + return Result.fail(`Invalid statistics measure name: ${name}`) + } else { + return Result.ok(new StatisticMeasureName({ value: name })) + } + } +} diff --git a/packages/analytics/src/Domain/Statistics/StatisticMeasureNameProps.ts b/packages/analytics/src/Domain/Statistics/StatisticMeasureNameProps.ts new file mode 100644 index 000000000..2f71605ab --- /dev/null +++ b/packages/analytics/src/Domain/Statistics/StatisticMeasureNameProps.ts @@ -0,0 +1,3 @@ +export interface StatisticMeasureNameProps { + value: string +} diff --git a/packages/analytics/src/Domain/Statistics/StatisticMeasureProps.ts b/packages/analytics/src/Domain/Statistics/StatisticMeasureProps.ts new file mode 100644 index 000000000..56879f8af --- /dev/null +++ b/packages/analytics/src/Domain/Statistics/StatisticMeasureProps.ts @@ -0,0 +1,7 @@ +import { StatisticMeasureName } from './StatisticMeasureName' + +export interface StatisticMeasureProps { + name: StatisticMeasureName + value: number + date: Date +} diff --git a/packages/analytics/src/Domain/Statistics/StatisticMeasureRepositoryInterface.ts b/packages/analytics/src/Domain/Statistics/StatisticMeasureRepositoryInterface.ts new file mode 100644 index 000000000..bc11dded9 --- /dev/null +++ b/packages/analytics/src/Domain/Statistics/StatisticMeasureRepositoryInterface.ts @@ -0,0 +1,5 @@ +import { StatisticMeasure } from './StatisticMeasure' + +export interface StatisticMeasureRepositoryInterface { + save(statisticMeasure: StatisticMeasure): Promise +} diff --git a/packages/analytics/src/Domain/Statistics/StatisticsMeasure.ts b/packages/analytics/src/Domain/Statistics/StatisticsMeasure.ts deleted file mode 100644 index b7647e7e7..000000000 --- a/packages/analytics/src/Domain/Statistics/StatisticsMeasure.ts +++ /dev/null @@ -1,24 +0,0 @@ -export enum StatisticsMeasure { - Income = 'income', - PlusSubscriptionInitialMonthlyPaymentsIncome = 'plus-subscription-initial-monthly-payments-income', - ProSubscriptionInitialMonthlyPaymentsIncome = 'pro-subscription-initial-monthly-payments-income', - PlusSubscriptionInitialAnnualPaymentsIncome = 'plus-subscription-initial-annual-payments-income', - ProSubscriptionInitialAnnualPaymentsIncome = 'pro-subscription-initial-annual-payments-income', - PlusSubscriptionRenewingMonthlyPaymentsIncome = 'plus-subscription-renewing-monthly-payments-income', - ProSubscriptionRenewingMonthlyPaymentsIncome = 'pro-subscription-renewing-monthly-payments-income', - PlusSubscriptionRenewingAnnualPaymentsIncome = 'plus-subscription-renewing-annual-payments-income', - ProSubscriptionRenewingAnnualPaymentsIncome = 'pro-subscription-renewing-annual-payments-income', - SubscriptionLength = 'subscription-length', - RegistrationLength = 'registration-length', - RegistrationToSubscriptionTime = 'registration-to-subscription-time', - RemainingSubscriptionTimePercentage = 'remaining-subscription-time-percentage', - Refunds = 'refunds', - NewCustomers = 'new-customers', - TotalCustomers = 'total-customers', - MRR = 'mrr', - MonthlyPlansMRR = 'monthly-plans-mrr', - AnnualPlansMRR = 'annual-plans-mrr', - FiveYearPlansMRR = 'five-year-plans-mrr', - ProPlansMRR = 'pro-plans-mrr', - PlusPlansMRR = 'plus-plans-mrr', -} diff --git a/packages/analytics/src/Domain/Statistics/StatisticsStoreInterface.ts b/packages/analytics/src/Domain/Statistics/StatisticsStoreInterface.ts index 4e1bdb868..f1e22daed 100644 --- a/packages/analytics/src/Domain/Statistics/StatisticsStoreInterface.ts +++ b/packages/analytics/src/Domain/Statistics/StatisticsStoreInterface.ts @@ -1,5 +1,4 @@ import { Period } from '../Time/Period' -import { StatisticsMeasure } from './StatisticsMeasure' export interface StatisticsStoreInterface { incrementSNJSVersionUsage(snjsVersion: string): Promise @@ -8,13 +7,13 @@ export interface StatisticsStoreInterface { getYesterdaySNJSUsage(): Promise> getYesterdayApplicationUsage(): Promise> getYesterdayOutOfSyncIncidents(): Promise - incrementMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise - setMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise - getMeasureAverage(measure: StatisticsMeasure, period: Period): Promise - getMeasureTotal(measure: StatisticsMeasure, periodOrPeriodKey: Period | string): Promise - getMeasureIncrementCounts(measure: StatisticsMeasure, period: Period): Promise + incrementMeasure(measure: string, value: number, periods: Period[]): Promise + setMeasure(measure: string, value: number, periods: Period[]): Promise + getMeasureAverage(measure: string, period: Period): Promise + getMeasureTotal(measure: string, periodOrPeriodKey: Period | string): Promise + getMeasureIncrementCounts(measure: string, period: Period): Promise calculateTotalCountOverPeriod( - measure: StatisticsMeasure, + measure: string, period: Period, ): Promise> } diff --git a/packages/analytics/src/Domain/Time/PeriodKeyGenerator.ts b/packages/analytics/src/Domain/Time/PeriodKeyGenerator.ts index f5aa41123..a62227b4c 100644 --- a/packages/analytics/src/Domain/Time/PeriodKeyGenerator.ts +++ b/packages/analytics/src/Domain/Time/PeriodKeyGenerator.ts @@ -137,7 +137,7 @@ export class PeriodKeyGenerator implements PeriodKeyGeneratorInterface { return `${this.getYear(date)}-${this.getMonth(date)}` } - private getDailyKey(date?: Date): string { + getDailyKey(date?: Date): string { date = date ?? new Date() return `${this.getYear(date)}-${this.getMonth(date)}-${this.getDayOfTheMonth(date)}` diff --git a/packages/analytics/src/Domain/Time/PeriodKeyGeneratorInterface.ts b/packages/analytics/src/Domain/Time/PeriodKeyGeneratorInterface.ts index 0651a5f6a..4eaca9332 100644 --- a/packages/analytics/src/Domain/Time/PeriodKeyGeneratorInterface.ts +++ b/packages/analytics/src/Domain/Time/PeriodKeyGeneratorInterface.ts @@ -2,6 +2,7 @@ import { Period } from './Period' export interface PeriodKeyGeneratorInterface { getPeriodKey(period: Period): string + getDailyKey(date?: Date): string convertPeriodKeyToPeriod(periodKey: string): Period getDiscretePeriodKeys(period: Period): string[] } diff --git a/packages/analytics/src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue.spec.ts b/packages/analytics/src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue.spec.ts index d0016729b..dd87d4ab7 100644 --- a/packages/analytics/src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue.spec.ts +++ b/packages/analytics/src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue.spec.ts @@ -1,7 +1,7 @@ import 'reflect-metadata' import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface' -import { StatisticsMeasure } from '../../Statistics/StatisticsMeasure' +import { StatisticMeasureName } from '../../Statistics/StatisticMeasureName' import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface' import { Period } from '../../Time/Period' @@ -24,7 +24,7 @@ describe('CalculateMonthlyRecurringRevenue', () => { it('should calculate the MRR diff and persist it as a statistic', async () => { await createUseCase().execute({}) - expect(statisticsStore.setMeasure).toHaveBeenCalledWith(StatisticsMeasure.MRR, 123.45, [ + expect(statisticsStore.setMeasure).toHaveBeenCalledWith(StatisticMeasureName.NAMES.MRR, 123.45, [ Period.Today, Period.ThisMonth, Period.ThisYear, diff --git a/packages/analytics/src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue.ts b/packages/analytics/src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue.ts index b0a34a3bf..d0b3177d7 100644 --- a/packages/analytics/src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue.ts +++ b/packages/analytics/src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue.ts @@ -5,11 +5,11 @@ import { Result } from '@standardnotes/domain-core' import TYPES from '../../../Bootstrap/Types' import { MonthlyRevenue } from '../../Revenue/MonthlyRevenue' import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface' -import { StatisticsMeasure } from '../../Statistics/StatisticsMeasure' import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface' import { Period } from '../../Time/Period' import { DomainUseCaseInterface } from '../DomainUseCaseInterface' import { CalculateMonthlyRecurringRevenueDTO } from './CalculateMonthlyRecurringRevenueDTO' +import { StatisticMeasureName } from '../../Statistics/StatisticMeasureName' @injectable() export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface { @@ -24,7 +24,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface< billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly], }) - await this.statisticsStore.setMeasure(StatisticsMeasure.MRR, mrrDiff, [ + await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.MRR, mrrDiff, [ Period.Today, Period.ThisMonth, Period.ThisYear, @@ -34,7 +34,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface< billingFrequencies: [SubscriptionBillingFrequency.Monthly], }) - await this.statisticsStore.setMeasure(StatisticsMeasure.MonthlyPlansMRR, monthlyPlansMrrDiff, [ + await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.MonthlyPlansMRR, monthlyPlansMrrDiff, [ Period.Today, Period.ThisMonth, Period.ThisYear, @@ -44,7 +44,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface< billingFrequencies: [SubscriptionBillingFrequency.Annual], }) - await this.statisticsStore.setMeasure(StatisticsMeasure.AnnualPlansMRR, annualPlansMrrDiff, [ + await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.AnnualPlansMRR, annualPlansMrrDiff, [ Period.Today, Period.ThisMonth, Period.ThisYear, @@ -54,7 +54,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface< billingFrequencies: [SubscriptionBillingFrequency.FiveYear], }) - await this.statisticsStore.setMeasure(StatisticsMeasure.FiveYearPlansMRR, fiveYearPlansMrrDiff, [ + await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.FiveYearPlansMRR, fiveYearPlansMrrDiff, [ Period.Today, Period.ThisMonth, Period.ThisYear, @@ -65,7 +65,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface< billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly], }) - await this.statisticsStore.setMeasure(StatisticsMeasure.ProPlansMRR, proPlansMrrDiff, [ + await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.ProPlansMRR, proPlansMrrDiff, [ Period.Today, Period.ThisMonth, Period.ThisYear, @@ -76,7 +76,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface< billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly], }) - await this.statisticsStore.setMeasure(StatisticsMeasure.PlusPlansMRR, plusPlansMrrDiff, [ + await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.PlusPlansMRR, plusPlansMrrDiff, [ Period.Today, Period.ThisMonth, Period.ThisYear, diff --git a/packages/analytics/src/Domain/UseCase/PersistStatistic/PersistStatistic.ts b/packages/analytics/src/Domain/UseCase/PersistStatistic/PersistStatistic.ts new file mode 100644 index 000000000..04d95ee46 --- /dev/null +++ b/packages/analytics/src/Domain/UseCase/PersistStatistic/PersistStatistic.ts @@ -0,0 +1,31 @@ +import { Result, UseCaseInterface } from '@standardnotes/domain-core' + +import { StatisticMeasure } from '../../Statistics/StatisticMeasure' +import { StatisticMeasureName } from '../../Statistics/StatisticMeasureName' +import { StatisticMeasureRepositoryInterface } from '../../Statistics/StatisticMeasureRepositoryInterface' +import { PersistStatisticDTO } from './PersistStatisticDTO' + +export class PersistStatistic implements UseCaseInterface { + constructor(private statisticMeasureRepository: StatisticMeasureRepositoryInterface) {} + + async execute(dto: PersistStatisticDTO): Promise> { + const statisticMeasureNameOrError = StatisticMeasureName.create(dto.statisticMeasureName) + if (statisticMeasureNameOrError.isFailed()) { + return Result.fail(`Could not persist statistic measure: ${statisticMeasureNameOrError.getError()}`) + } + + const statisticMeasureOrError = StatisticMeasure.create({ + date: dto.date, + name: statisticMeasureNameOrError.getValue(), + value: dto.value, + }) + if (statisticMeasureOrError.isFailed()) { + return Result.fail(`Could not persist statistic measure: ${statisticMeasureOrError.getError()}`) + } + const statisticMeasure = statisticMeasureOrError.getValue() + + await this.statisticMeasureRepository.save(statisticMeasure) + + return Result.ok(statisticMeasure) + } +} diff --git a/packages/analytics/src/Domain/UseCase/PersistStatistic/PersistStatisticDTO.ts b/packages/analytics/src/Domain/UseCase/PersistStatistic/PersistStatisticDTO.ts new file mode 100644 index 000000000..bf7a918fb --- /dev/null +++ b/packages/analytics/src/Domain/UseCase/PersistStatistic/PersistStatisticDTO.ts @@ -0,0 +1,5 @@ +export interface PersistStatisticDTO { + statisticMeasureName: string + value: number + date: Date +} diff --git a/packages/analytics/src/Infra/Redis/RedisStatisticsStore.ts b/packages/analytics/src/Infra/Redis/RedisStatisticsStore.ts index d9de00b9f..38e5e0063 100644 --- a/packages/analytics/src/Infra/Redis/RedisStatisticsStore.ts +++ b/packages/analytics/src/Infra/Redis/RedisStatisticsStore.ts @@ -1,16 +1,23 @@ import * as IORedis from 'ioredis' -import { StatisticsMeasure } from '../../Domain/Statistics/StatisticsMeasure' +import { StatisticMeasure } from '../../Domain/Statistics/StatisticMeasure' +import { StatisticMeasureRepositoryInterface } from '../../Domain/Statistics/StatisticMeasureRepositoryInterface' import { StatisticsStoreInterface } from '../../Domain/Statistics/StatisticsStoreInterface' import { Period } from '../../Domain/Time/Period' import { PeriodKeyGeneratorInterface } from '../../Domain/Time/PeriodKeyGeneratorInterface' -export class RedisStatisticsStore implements StatisticsStoreInterface { +export class RedisStatisticsStore implements StatisticsStoreInterface, StatisticMeasureRepositoryInterface { constructor(private periodKeyGenerator: PeriodKeyGeneratorInterface, private redisClient: IORedis.Redis) {} + async save(statisticMeasure: StatisticMeasure): Promise { + const periodKey = this.periodKeyGenerator.getDailyKey(statisticMeasure.props.date) + + await this.setMeasure(statisticMeasure.name, statisticMeasure.value, [periodKey]) + } + async calculateTotalCountOverPeriod( - measure: StatisticsMeasure, + measure: string, period: Period, ): Promise<{ periodKey: string; totalCount: number }[]> { if ( @@ -38,7 +45,7 @@ export class RedisStatisticsStore implements StatisticsStoreInterface { return counts } - async getMeasureIncrementCounts(measure: StatisticsMeasure, period: Period): Promise { + async getMeasureIncrementCounts(measure: string, period: Period): Promise { const increments = await this.redisClient.get( `count:increments:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`, ) @@ -49,17 +56,22 @@ export class RedisStatisticsStore implements StatisticsStoreInterface { return +increments } - async setMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise { + async setMeasure(measure: string, value: number, periodsOrPeriodKeys: Period[] | string[]): Promise { const pipeline = this.redisClient.pipeline() - for (const period of periods) { - pipeline.set(`count:measure:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`, value) + for (const periodOrPeriodKey of periodsOrPeriodKeys) { + let periodKey = periodOrPeriodKey + if (!isNaN(+periodOrPeriodKey)) { + periodKey = this.periodKeyGenerator.getPeriodKey(periodOrPeriodKey as Period) + } + + pipeline.set(`count:measure:${measure}:timespan:${periodKey}`, value) } await pipeline.exec() } - async getMeasureTotal(measure: StatisticsMeasure, periodOrPeriodKey: Period | string): Promise { + async getMeasureTotal(measure: string, periodOrPeriodKey: Period | string): Promise { let periodKey = periodOrPeriodKey if (!isNaN(+periodOrPeriodKey)) { periodKey = this.periodKeyGenerator.getPeriodKey(periodOrPeriodKey as Period) @@ -74,7 +86,7 @@ export class RedisStatisticsStore implements StatisticsStoreInterface { return +totalValue } - async incrementMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise { + async incrementMeasure(measure: string, value: number, periods: Period[]): Promise { const pipeline = this.redisClient.pipeline() for (const period of periods) { @@ -85,7 +97,7 @@ export class RedisStatisticsStore implements StatisticsStoreInterface { await pipeline.exec() } - async getMeasureAverage(measure: StatisticsMeasure, period: Period): Promise { + async getMeasureAverage(measure: string, period: Period): Promise { const increments = await this.getMeasureIncrementCounts(measure, period) if (increments === 0) { diff --git a/packages/domain-events/src/Domain/Event/StatisticPersistenceRequestedEvent.ts b/packages/domain-events/src/Domain/Event/StatisticPersistenceRequestedEvent.ts new file mode 100644 index 000000000..c78299fa6 --- /dev/null +++ b/packages/domain-events/src/Domain/Event/StatisticPersistenceRequestedEvent.ts @@ -0,0 +1,8 @@ +import { DomainEventInterface } from './DomainEventInterface' + +import { StatisticPersistenceRequestedEventPayload } from './StatisticPersistenceRequestedEventPayload' + +export interface StatisticPersistenceRequestedEvent extends DomainEventInterface { + type: 'STATISTIC_PERSISTENCE_REQUESTED' + payload: StatisticPersistenceRequestedEventPayload +} diff --git a/packages/domain-events/src/Domain/Event/StatisticPersistenceRequestedEventPayload.ts b/packages/domain-events/src/Domain/Event/StatisticPersistenceRequestedEventPayload.ts new file mode 100644 index 000000000..806bdf526 --- /dev/null +++ b/packages/domain-events/src/Domain/Event/StatisticPersistenceRequestedEventPayload.ts @@ -0,0 +1,5 @@ +export interface StatisticPersistenceRequestedEventPayload { + statisticMeasureName: string + value: number + date: Date +} diff --git a/packages/domain-events/src/Domain/index.ts b/packages/domain-events/src/Domain/index.ts index 3ca462b85..b88900aa9 100644 --- a/packages/domain-events/src/Domain/index.ts +++ b/packages/domain-events/src/Domain/index.ts @@ -58,6 +58,8 @@ export * from './Event/SharedSubscriptionInvitationCanceledEvent' export * from './Event/SharedSubscriptionInvitationCanceledEventPayload' export * from './Event/SharedSubscriptionInvitationCreatedEvent' export * from './Event/SharedSubscriptionInvitationCreatedEventPayload' +export * from './Event/StatisticPersistenceRequestedEvent' +export * from './Event/StatisticPersistenceRequestedEventPayload' export * from './Event/SubscriptionCancelledEvent' export * from './Event/SubscriptionCancelledEventPayload' export * from './Event/SubscriptionPurchasedEvent'