mirror of
https://github.com/standardnotes/server
synced 2026-05-11 12:57:18 -04:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f2616ef0a | |||
| 04ffc69e00 | |||
| 5b4bb6e7a7 | |||
| 2e953ba998 | |||
| ed5a4eb960 | |||
| 31b2c05084 | |||
| 6e1662038c | |||
| df78d88f79 | |||
| addedb3091 | |||
| 2ea17b2dea | |||
| 85d2f42f47 | |||
| cdb655c1bd | |||
| 3064d03aa9 | |||
| 6af6417ca2 | |||
| a35271fbb3 | |||
| 63aef71f60 | |||
| 0f8457534c | |||
| 2984582e62 | |||
| 147d8fd9af |
@@ -3,6 +3,43 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.17.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.0...@standardnotes/analytics@2.17.1) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** container binding ([04ffc69](https://github.com/standardnotes/server/commit/04ffc69e000803107d8834c286de97b3d213a842))
|
||||||
|
* **auth:** replace date object with number timestamp ([5b4bb6e](https://github.com/standardnotes/server/commit/5b4bb6e7a78a1b0f4e663bb990619f65f6a5c757))
|
||||||
|
|
||||||
|
# [2.17.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.16.0...@standardnotes/analytics@2.17.0) (2022-12-20)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add users activit to the report email ([ed5a4eb](https://github.com/standardnotes/server/commit/ed5a4eb960a6c8fe9d0c77331f29dc3c7ffb9100))
|
||||||
|
|
||||||
|
# [2.16.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.15.1...@standardnotes/analytics@2.16.0) (2022-12-20)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add active users stats to report ([6e16620](https://github.com/standardnotes/server/commit/6e1662038c3340fb60939464616789bab7639160))
|
||||||
|
|
||||||
|
## [2.15.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.15.0...@standardnotes/analytics@2.15.1) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **auth:** add persisting statistics for all subscription plans ([addedb3](https://github.com/standardnotes/server/commit/addedb3091ddae81618d56663e18f2ae76a43c4e))
|
||||||
|
|
||||||
|
# [2.15.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.14.0...@standardnotes/analytics@2.15.0) (2022-12-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **auth:** add requesting persisting statistics ([a35271f](https://github.com/standardnotes/server/commit/a35271fbb399b68a3ac7021395d8063707fba222))
|
||||||
|
|
||||||
|
# [2.14.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.13.0...@standardnotes/analytics@2.14.0) (2022-12-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add persisting statistics on demand ([0f84575](https://github.com/standardnotes/server/commit/0f8457534c1829c58f3c036749d262307ddeb779))
|
||||||
|
|
||||||
# [2.13.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.27...@standardnotes/analytics@2.13.0) (2022-12-19)
|
# [2.13.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.27...@standardnotes/analytics@2.13.0) (2022-12-19)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { EmailLevel } from '@standardnotes/domain-core'
|
|||||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||||
import { AnalyticsActivity } from '../src/Domain/Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../src/Domain/Analytics/AnalyticsActivity'
|
||||||
import { Period } from '../src/Domain/Time/Period'
|
import { Period } from '../src/Domain/Time/Period'
|
||||||
import { StatisticsMeasure } from '../src/Domain/Statistics/StatisticsMeasure'
|
|
||||||
import { AnalyticsStoreInterface } from '../src/Domain/Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../src/Domain/Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsStoreInterface } from '../src/Domain/Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../src/Domain/Statistics/StatisticsStoreInterface'
|
||||||
import { PeriodKeyGeneratorInterface } from '../src/Domain/Time/PeriodKeyGeneratorInterface'
|
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 { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
|
||||||
import { getBody, getSubject } from '../src/Domain/Email/DailyAnalyticsReport'
|
import { getBody, getSubject } from '../src/Domain/Email/DailyAnalyticsReport'
|
||||||
import { TimerInterface } from '@standardnotes/time'
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
import { StatisticMeasureName } from '../src/Domain/Statistics/StatisticMeasureName'
|
||||||
|
|
||||||
const requestReport = async (
|
const requestReport = async (
|
||||||
analyticsStore: AnalyticsStoreInterface,
|
analyticsStore: AnalyticsStoreInterface,
|
||||||
@@ -115,12 +115,16 @@ const requestReport = async (
|
|||||||
}> = []
|
}> = []
|
||||||
|
|
||||||
const thirtyDaysStatisticsNames = [
|
const thirtyDaysStatisticsNames = [
|
||||||
StatisticsMeasure.MRR,
|
StatisticMeasureName.NAMES.MRR,
|
||||||
StatisticsMeasure.AnnualPlansMRR,
|
StatisticMeasureName.NAMES.AnnualPlansMRR,
|
||||||
StatisticsMeasure.MonthlyPlansMRR,
|
StatisticMeasureName.NAMES.MonthlyPlansMRR,
|
||||||
StatisticsMeasure.FiveYearPlansMRR,
|
StatisticMeasureName.NAMES.FiveYearPlansMRR,
|
||||||
StatisticsMeasure.PlusPlansMRR,
|
StatisticMeasureName.NAMES.PlusPlansMRR,
|
||||||
StatisticsMeasure.ProPlansMRR,
|
StatisticMeasureName.NAMES.ProPlansMRR,
|
||||||
|
StatisticMeasureName.NAMES.ActiveUsers,
|
||||||
|
StatisticMeasureName.NAMES.ActiveFreeUsers,
|
||||||
|
StatisticMeasureName.NAMES.ActivePlusUsers,
|
||||||
|
StatisticMeasureName.NAMES.ActiveProUsers,
|
||||||
]
|
]
|
||||||
for (const statisticName of thirtyDaysStatisticsNames) {
|
for (const statisticName of thirtyDaysStatisticsNames) {
|
||||||
statisticsOverTime.push({
|
statisticsOverTime.push({
|
||||||
@@ -130,7 +134,7 @@ const requestReport = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const monthlyStatisticsNames = [StatisticsMeasure.MRR]
|
const monthlyStatisticsNames = [StatisticMeasureName.NAMES.MRR]
|
||||||
for (const statisticName of monthlyStatisticsNames) {
|
for (const statisticName of monthlyStatisticsNames) {
|
||||||
statisticsOverTime.push({
|
statisticsOverTime.push({
|
||||||
name: statisticName,
|
name: statisticName,
|
||||||
@@ -140,22 +144,22 @@ const requestReport = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const statisticMeasureNames = [
|
const statisticMeasureNames = [
|
||||||
StatisticsMeasure.Income,
|
StatisticMeasureName.NAMES.Income,
|
||||||
StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome,
|
StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome,
|
||||||
StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome,
|
StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome,
|
||||||
StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome,
|
StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome,
|
||||||
StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome,
|
StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome,
|
||||||
StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome,
|
StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome,
|
||||||
StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome,
|
StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome,
|
||||||
StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome,
|
StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome,
|
||||||
StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome,
|
StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome,
|
||||||
StatisticsMeasure.Refunds,
|
StatisticMeasureName.NAMES.Refunds,
|
||||||
StatisticsMeasure.RegistrationLength,
|
StatisticMeasureName.NAMES.RegistrationLength,
|
||||||
StatisticsMeasure.SubscriptionLength,
|
StatisticMeasureName.NAMES.SubscriptionLength,
|
||||||
StatisticsMeasure.RegistrationToSubscriptionTime,
|
StatisticMeasureName.NAMES.RegistrationToSubscriptionTime,
|
||||||
StatisticsMeasure.RemainingSubscriptionTimePercentage,
|
StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage,
|
||||||
StatisticsMeasure.NewCustomers,
|
StatisticMeasureName.NAMES.NewCustomers,
|
||||||
StatisticsMeasure.TotalCustomers,
|
StatisticMeasureName.NAMES.TotalCustomers,
|
||||||
]
|
]
|
||||||
const statisticMeasures: Array<{
|
const statisticMeasures: Array<{
|
||||||
name: string
|
name: string
|
||||||
@@ -190,7 +194,10 @@ const requestReport = async (
|
|||||||
|
|
||||||
const totalCustomerCounts: Array<number> = []
|
const totalCustomerCounts: Array<number> = []
|
||||||
for (const dailyPeriodKey of dailyPeriodKeys) {
|
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)
|
totalCustomerCounts.push(customersCount)
|
||||||
}
|
}
|
||||||
const filteredTotalCustomerCounts = totalCustomerCounts.filter((count) => !!count)
|
const filteredTotalCustomerCounts = totalCustomerCounts.filter((count) => !!count)
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ module.exports = {
|
|||||||
transform: {
|
transform: {
|
||||||
...tsjPreset.transform,
|
...tsjPreset.transform,
|
||||||
},
|
},
|
||||||
coveragePathIgnorePatterns: ['/Infra/', '/Domain/Email/'],
|
coveragePathIgnorePatterns: ['/Infra/', '/Domain/Email/', '/Handler/'],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/analytics",
|
"name": "@standardnotes/analytics",
|
||||||
"version": "2.13.0",
|
"version": "2.17.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ import { RevenueModification } from '../Domain/Revenue/RevenueModification'
|
|||||||
import { RevenueModificationMap } from '../Domain/Map/RevenueModificationMap'
|
import { RevenueModificationMap } from '../Domain/Map/RevenueModificationMap'
|
||||||
import { SaveRevenueModification } from '../Domain/UseCase/SaveRevenueModification/SaveRevenueModification'
|
import { SaveRevenueModification } from '../Domain/UseCase/SaveRevenueModification/SaveRevenueModification'
|
||||||
import { CalculateMonthlyRecurringRevenue } from '../Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
|
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
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
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.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
|
||||||
container.bind(TYPES.ADMIN_EMAILS).toConstantValue(env.get('ADMIN_EMAILS').split(','))
|
container.bind(TYPES.ADMIN_EMAILS).toConstantValue(env.get('ADMIN_EMAILS').split(','))
|
||||||
|
|
||||||
|
// Services
|
||||||
|
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
||||||
|
container.bind<PeriodKeyGeneratorInterface>(TYPES.PeriodKeyGenerator).toConstantValue(new PeriodKeyGenerator())
|
||||||
|
container
|
||||||
|
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
|
||||||
|
.toConstantValue(new RedisAnalyticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
||||||
|
container
|
||||||
|
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
|
||||||
|
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
||||||
|
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
|
||||||
|
|
||||||
|
if (env.get('SNS_TOPIC_ARN', true)) {
|
||||||
|
container
|
||||||
|
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||||
|
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
|
||||||
|
} else {
|
||||||
|
container
|
||||||
|
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||||
|
.toConstantValue(
|
||||||
|
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
container
|
container
|
||||||
.bind<AnalyticsEntityRepositoryInterface>(TYPES.AnalyticsEntityRepository)
|
.bind<AnalyticsEntityRepositoryInterface>(TYPES.AnalyticsEntityRepository)
|
||||||
@@ -139,6 +165,9 @@ export class ContainerConfigLoader {
|
|||||||
container
|
container
|
||||||
.bind<RevenueModificationRepositoryInterface>(TYPES.RevenueModificationRepository)
|
.bind<RevenueModificationRepositoryInterface>(TYPES.RevenueModificationRepository)
|
||||||
.to(MySQLRevenueModificationRepository)
|
.to(MySQLRevenueModificationRepository)
|
||||||
|
container
|
||||||
|
.bind<StatisticMeasureRepositoryInterface>(TYPES.StatisticMeasureRepository)
|
||||||
|
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
||||||
|
|
||||||
// ORM
|
// ORM
|
||||||
container
|
container
|
||||||
@@ -154,6 +183,9 @@ export class ContainerConfigLoader {
|
|||||||
container
|
container
|
||||||
.bind<CalculateMonthlyRecurringRevenue>(TYPES.CalculateMonthlyRecurringRevenue)
|
.bind<CalculateMonthlyRecurringRevenue>(TYPES.CalculateMonthlyRecurringRevenue)
|
||||||
.to(CalculateMonthlyRecurringRevenue)
|
.to(CalculateMonthlyRecurringRevenue)
|
||||||
|
container
|
||||||
|
.bind<PersistStatistic>(TYPES.PersistStatistic)
|
||||||
|
.toConstantValue(new PersistStatistic(container.get(TYPES.StatisticMeasureRepository)))
|
||||||
|
|
||||||
// Hanlders
|
// Hanlders
|
||||||
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
||||||
@@ -181,35 +213,21 @@ export class ContainerConfigLoader {
|
|||||||
.bind<SubscriptionReactivatedEventHandler>(TYPES.SubscriptionReactivatedEventHandler)
|
.bind<SubscriptionReactivatedEventHandler>(TYPES.SubscriptionReactivatedEventHandler)
|
||||||
.to(SubscriptionReactivatedEventHandler)
|
.to(SubscriptionReactivatedEventHandler)
|
||||||
container.bind<RefundProcessedEventHandler>(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler)
|
container.bind<RefundProcessedEventHandler>(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler)
|
||||||
|
container
|
||||||
|
.bind<StatisticPersistenceRequestedEventHandler>(TYPES.StatisticPersistenceRequestedEventHandler)
|
||||||
|
.toConstantValue(
|
||||||
|
new StatisticPersistenceRequestedEventHandler(
|
||||||
|
container.get(TYPES.PersistStatistic),
|
||||||
|
container.get(TYPES.Timer),
|
||||||
|
container.get(TYPES.Logger),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
// Maps
|
// Maps
|
||||||
container
|
container
|
||||||
.bind<MapperInterface<RevenueModification, TypeORMRevenueModification>>(TYPES.RevenueModificationMap)
|
.bind<MapperInterface<RevenueModification, TypeORMRevenueModification>>(TYPES.RevenueModificationMap)
|
||||||
.to(RevenueModificationMap)
|
.to(RevenueModificationMap)
|
||||||
|
|
||||||
// Services
|
|
||||||
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
|
||||||
container.bind<PeriodKeyGeneratorInterface>(TYPES.PeriodKeyGenerator).toConstantValue(new PeriodKeyGenerator())
|
|
||||||
container
|
|
||||||
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
|
|
||||||
.toConstantValue(new RedisAnalyticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
|
||||||
container
|
|
||||||
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
|
|
||||||
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
|
||||||
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
|
|
||||||
|
|
||||||
if (env.get('SNS_TOPIC_ARN', true)) {
|
|
||||||
container
|
|
||||||
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
|
|
||||||
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
|
|
||||||
} else {
|
|
||||||
container
|
|
||||||
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
|
|
||||||
.toConstantValue(
|
|
||||||
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||||
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
|
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
|
||||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
|
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
|
||||||
@@ -222,6 +240,7 @@ export class ContainerConfigLoader {
|
|||||||
['SUBSCRIPTION_EXPIRED', container.get(TYPES.SubscriptionExpiredEventHandler)],
|
['SUBSCRIPTION_EXPIRED', container.get(TYPES.SubscriptionExpiredEventHandler)],
|
||||||
['SUBSCRIPTION_REACTIVATED', container.get(TYPES.SubscriptionReactivatedEventHandler)],
|
['SUBSCRIPTION_REACTIVATED', container.get(TYPES.SubscriptionReactivatedEventHandler)],
|
||||||
['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)],
|
['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)],
|
||||||
|
['STATISTIC_PERSISTENCE_REQUESTED', container.get(TYPES.StatisticPersistenceRequestedEventHandler)],
|
||||||
])
|
])
|
||||||
|
|
||||||
if (env.get('SQS_QUEUE_URL', true)) {
|
if (env.get('SQS_QUEUE_URL', true)) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const TYPES = {
|
|||||||
// Repositories
|
// Repositories
|
||||||
AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
|
AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
|
||||||
RevenueModificationRepository: Symbol.for('RevenueModificationRepository'),
|
RevenueModificationRepository: Symbol.for('RevenueModificationRepository'),
|
||||||
|
StatisticMeasureRepository: Symbol.for('StatisticMeasureRepository'),
|
||||||
// ORM
|
// ORM
|
||||||
ORMAnalyticsEntityRepository: Symbol.for('ORMAnalyticsEntityRepository'),
|
ORMAnalyticsEntityRepository: Symbol.for('ORMAnalyticsEntityRepository'),
|
||||||
ORMRevenueModificationRepository: Symbol.for('ORMRevenueModificationRepository'),
|
ORMRevenueModificationRepository: Symbol.for('ORMRevenueModificationRepository'),
|
||||||
@@ -22,6 +23,7 @@ const TYPES = {
|
|||||||
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
|
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
|
||||||
SaveRevenueModification: Symbol.for('SaveRevenueModification'),
|
SaveRevenueModification: Symbol.for('SaveRevenueModification'),
|
||||||
CalculateMonthlyRecurringRevenue: Symbol.for('CalculateMonthlyRecurringRevenue'),
|
CalculateMonthlyRecurringRevenue: Symbol.for('CalculateMonthlyRecurringRevenue'),
|
||||||
|
PersistStatistic: Symbol.for('PersistStatistic'),
|
||||||
// Handlers
|
// Handlers
|
||||||
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
||||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||||
@@ -34,6 +36,7 @@ const TYPES = {
|
|||||||
SubscriptionExpiredEventHandler: Symbol.for('SubscriptionExpiredEventHandler'),
|
SubscriptionExpiredEventHandler: Symbol.for('SubscriptionExpiredEventHandler'),
|
||||||
SubscriptionReactivatedEventHandler: Symbol.for('SubscriptionReactivatedEventHandler'),
|
SubscriptionReactivatedEventHandler: Symbol.for('SubscriptionReactivatedEventHandler'),
|
||||||
RefundProcessedEventHandler: Symbol.for('RefundProcessedEventHandler'),
|
RefundProcessedEventHandler: Symbol.for('RefundProcessedEventHandler'),
|
||||||
|
StatisticPersistenceRequestedEventHandler: Symbol.for('StatisticPersistenceRequestedEventHandler'),
|
||||||
// Maps
|
// Maps
|
||||||
RevenueModificationMap: Symbol.for('RevenueModificationMap'),
|
RevenueModificationMap: Symbol.for('RevenueModificationMap'),
|
||||||
// Services
|
// Services
|
||||||
|
|||||||
@@ -2,9 +2,32 @@
|
|||||||
import { TimerInterface } from '@standardnotes/time'
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
|
|
||||||
|
const countActiveUsers = (measureName: string, data: any): { yesterday: number; last30Days: number } => {
|
||||||
|
const totalActiveUsersLast30DaysIncludingToday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: number }) => a.name === measureName && a.period === 27,
|
||||||
|
)
|
||||||
|
const totalActiveUsersYesterday =
|
||||||
|
totalActiveUsersLast30DaysIncludingToday.counts[totalActiveUsersLast30DaysIncludingToday.counts.length - 2]
|
||||||
|
.totalCount
|
||||||
|
|
||||||
|
const filteredStats = totalActiveUsersLast30DaysIncludingToday.counts.filter(
|
||||||
|
(count: { totalCount: number }) => count.totalCount !== 0,
|
||||||
|
)
|
||||||
|
const averageActiveUsersLast30Days = Math.floor(
|
||||||
|
filteredStats.counts.reduce((previousValue: { totalCount: any }, currentValue: { totalCount: any }) => {
|
||||||
|
return previousValue.totalCount + currentValue.totalCount
|
||||||
|
}) / filteredStats.counts.length,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
yesterday: totalActiveUsersYesterday,
|
||||||
|
last30Days: averageActiveUsersLast30Days,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getChartUrls = (
|
const getChartUrls = (
|
||||||
data: any,
|
data: any,
|
||||||
): {
|
): {
|
||||||
@@ -12,7 +35,6 @@ const getChartUrls = (
|
|||||||
users: string
|
users: string
|
||||||
quarterlyPerformance: string
|
quarterlyPerformance: string
|
||||||
churn: string
|
churn: string
|
||||||
mrr: string
|
|
||||||
mrrMonthly: string
|
mrrMonthly: string
|
||||||
} => {
|
} => {
|
||||||
const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
|
const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
|
||||||
@@ -237,82 +259,6 @@ const getChartUrls = (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const mrrOverTime = data.statisticsOverTime.find(
|
|
||||||
(a: { name: string; period: number }) => a.name === 'mrr' && a.period === 27,
|
|
||||||
)
|
|
||||||
const monthlyPlansMrrOverTime = data.statisticsOverTime.find(
|
|
||||||
(a: { name: string; period: number }) => a.name === 'monthly-plans-mrr' && a.period === 27,
|
|
||||||
)
|
|
||||||
const annualPlansMrrOverTime = data.statisticsOverTime.find(
|
|
||||||
(a: { name: string; period: number }) => a.name === 'annual-plans-mrr' && a.period === 27,
|
|
||||||
)
|
|
||||||
const fiveYearPlansMrrOverTime = data.statisticsOverTime.find(
|
|
||||||
(a: { name: string; period: number }) => a.name === 'five-year-plans-mrr' && a.period === 27,
|
|
||||||
)
|
|
||||||
const proPlansMrrOverTime = data.statisticsOverTime.find(
|
|
||||||
(a: { name: string; period: number }) => a.name === 'pro-plans-mrr' && a.period === 27,
|
|
||||||
)
|
|
||||||
const plusPlansMrrOverTime = data.statisticsOverTime.find(
|
|
||||||
(a: { name: string; period: number }) => a.name === 'plus-plans-mrr' && a.period === 27,
|
|
||||||
)
|
|
||||||
|
|
||||||
const mrrOverTimeConfig = {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: mrrOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: 'MRR',
|
|
||||||
backgroundColor: 'rgb(25, 255, 140)',
|
|
||||||
borderColor: 'rgb(25, 255, 140)',
|
|
||||||
data: mrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
|
||||||
fill: false,
|
|
||||||
pointRadius: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'MRR - Monthly Plans',
|
|
||||||
backgroundColor: 'rgb(54, 162, 235)',
|
|
||||||
borderColor: 'rgb(54, 162, 235)',
|
|
||||||
data: monthlyPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
|
||||||
fill: false,
|
|
||||||
pointRadius: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'MRR - Annual Plans',
|
|
||||||
backgroundColor: 'rgb(255, 221, 51)',
|
|
||||||
borderColor: 'rgb(255, 221, 51)',
|
|
||||||
data: annualPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
|
||||||
fill: false,
|
|
||||||
pointRadius: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'MRR - Five Year Plans',
|
|
||||||
backgroundColor: 'rgb(255, 120, 120)',
|
|
||||||
borderColor: 'rgb(255, 120, 120)',
|
|
||||||
data: fiveYearPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
|
||||||
fill: false,
|
|
||||||
pointRadius: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'MRR - PRO Plans',
|
|
||||||
backgroundColor: 'rgb(255, 99, 132)',
|
|
||||||
borderColor: 'rgb(255, 99, 132)',
|
|
||||||
data: proPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
|
||||||
fill: false,
|
|
||||||
pointRadius: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'MRR - PLUS Plans',
|
|
||||||
backgroundColor: 'rgb(221, 51, 255)',
|
|
||||||
borderColor: 'rgb(221, 51, 255)',
|
|
||||||
data: plusPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
|
||||||
fill: false,
|
|
||||||
pointRadius: 2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const mrrMonthlyOverTime = data.statisticsOverTime
|
const mrrMonthlyOverTime = data.statisticsOverTime
|
||||||
.find((a: { name: string; period: Period }) => a.name === 'mrr' && a.period === Period.ThisYear)
|
.find((a: { name: string; period: Period }) => a.name === 'mrr' && a.period === Period.ThisYear)
|
||||||
?.counts.map((count: { totalCount: number }) => +count.totalCount.toFixed(2))
|
?.counts.map((count: { totalCount: number }) => +count.totalCount.toFixed(2))
|
||||||
@@ -371,7 +317,6 @@ const getChartUrls = (
|
|||||||
JSON.stringify(quarterlyConfig),
|
JSON.stringify(quarterlyConfig),
|
||||||
)}`,
|
)}`,
|
||||||
churn: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(churnConfig))}`,
|
churn: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(churnConfig))}`,
|
||||||
mrr: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrOverTimeConfig))}`,
|
|
||||||
mrrMonthly: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrMonthlyConfig))}`,
|
mrrMonthly: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrMonthlyConfig))}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,156 +362,170 @@ export const html = (data: any, timer: TimerInterface) => {
|
|||||||
a.name === AnalyticsActivity.DeleteAccount && a.period === Period.Last30Days,
|
a.name === AnalyticsActivity.DeleteAccount && a.period === Period.Last30Days,
|
||||||
)
|
)
|
||||||
const incomeMeasureYesterday = data.statisticMeasures.find(
|
const incomeMeasureYesterday = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.Income && a.period === Period.Yesterday,
|
a.name === StatisticMeasureName.NAMES.Income && a.period === Period.Yesterday,
|
||||||
)
|
)
|
||||||
const refundMeasureYesterday = data.statisticMeasures.find(
|
const refundMeasureYesterday = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.Refunds && a.period === Period.Yesterday,
|
a.name === StatisticMeasureName.NAMES.Refunds && a.period === Period.Yesterday,
|
||||||
)
|
)
|
||||||
const incomeYesterday = incomeMeasureYesterday?.totalValue ?? 0
|
const incomeYesterday = incomeMeasureYesterday?.totalValue ?? 0
|
||||||
const refundsYesterday = refundMeasureYesterday?.totalValue ?? 0
|
const refundsYesterday = refundMeasureYesterday?.totalValue ?? 0
|
||||||
const revenueYesterday = incomeYesterday - refundsYesterday
|
const revenueYesterday = incomeYesterday - refundsYesterday
|
||||||
|
|
||||||
const subscriptionLengthMeasureYesterday = data.statisticMeasures.find(
|
const subscriptionLengthMeasureYesterday = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.SubscriptionLength && a.period === Period.Yesterday,
|
a.name === StatisticMeasureName.NAMES.SubscriptionLength && a.period === Period.Yesterday,
|
||||||
)
|
)
|
||||||
const subscriptionLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
|
const subscriptionLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
|
||||||
Math.floor(subscriptionLengthMeasureYesterday?.average ?? 0),
|
Math.floor(subscriptionLengthMeasureYesterday?.average ?? 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
const subscriptionRemainingTimePercentageMeasureYesterday = data.statisticMeasures.find(
|
const subscriptionRemainingTimePercentageMeasureYesterday = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.RemainingSubscriptionTimePercentage && a.period === Period.Yesterday,
|
a.name === StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage && a.period === Period.Yesterday,
|
||||||
)
|
)
|
||||||
const subscriptionRemainingTimePercentageYesterday = Math.floor(
|
const subscriptionRemainingTimePercentageYesterday = Math.floor(
|
||||||
subscriptionRemainingTimePercentageMeasureYesterday?.average ?? 0,
|
subscriptionRemainingTimePercentageMeasureYesterday?.average ?? 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
const registrationLengthMeasureYesterday = data.statisticMeasures.find(
|
const registrationLengthMeasureYesterday = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.RegistrationLength && a.period === Period.Yesterday,
|
a.name === StatisticMeasureName.NAMES.RegistrationLength && a.period === Period.Yesterday,
|
||||||
)
|
)
|
||||||
const registrationLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
|
const registrationLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
|
||||||
Math.floor(registrationLengthMeasureYesterday?.average ?? 0),
|
Math.floor(registrationLengthMeasureYesterday?.average ?? 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
const registrationToSubscriptionMeasureYesterday = data.statisticMeasures.find(
|
const registrationToSubscriptionMeasureYesterday = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.RegistrationToSubscriptionTime && a.period === Period.Yesterday,
|
a.name === StatisticMeasureName.NAMES.RegistrationToSubscriptionTime && a.period === Period.Yesterday,
|
||||||
)
|
)
|
||||||
const registrationToSubscriptionDurationYesterday = timer.convertMicrosecondsToTimeStructure(
|
const registrationToSubscriptionDurationYesterday = timer.convertMicrosecondsToTimeStructure(
|
||||||
Math.floor(registrationToSubscriptionMeasureYesterday?.average ?? 0),
|
Math.floor(registrationToSubscriptionMeasureYesterday?.average ?? 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
const incomeMeasureThisMonth = data.statisticMeasures.find(
|
const incomeMeasureThisMonth = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.Income && a.period === Period.ThisMonth,
|
a.name === StatisticMeasureName.NAMES.Income && a.period === Period.ThisMonth,
|
||||||
)
|
)
|
||||||
const refundMeasureThisMonth = data.statisticMeasures.find(
|
const refundMeasureThisMonth = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.Refunds && a.period === Period.ThisMonth,
|
a.name === StatisticMeasureName.NAMES.Refunds && a.period === Period.ThisMonth,
|
||||||
)
|
)
|
||||||
const incomeThisMonth = incomeMeasureThisMonth?.totalValue ?? 0
|
const incomeThisMonth = incomeMeasureThisMonth?.totalValue ?? 0
|
||||||
const refundsThisMonth = refundMeasureThisMonth?.totalValue ?? 0
|
const refundsThisMonth = refundMeasureThisMonth?.totalValue ?? 0
|
||||||
const revenueThisMonth = incomeThisMonth - refundsThisMonth
|
const revenueThisMonth = incomeThisMonth - refundsThisMonth
|
||||||
|
|
||||||
const subscriptionLengthMeasureThisMonth = data.statisticMeasures.find(
|
const subscriptionLengthMeasureThisMonth = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.SubscriptionLength && a.period === Period.ThisMonth,
|
a.name === StatisticMeasureName.NAMES.SubscriptionLength && a.period === Period.ThisMonth,
|
||||||
)
|
)
|
||||||
const subscriptionLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
|
const subscriptionLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
|
||||||
Math.floor(subscriptionLengthMeasureThisMonth?.average ?? 0),
|
Math.floor(subscriptionLengthMeasureThisMonth?.average ?? 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
const subscriptionRemainingTimePercentageMeasureThisMonth = data.statisticMeasures.find(
|
const subscriptionRemainingTimePercentageMeasureThisMonth = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.RemainingSubscriptionTimePercentage && a.period === Period.ThisMonth,
|
a.name === StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage && a.period === Period.ThisMonth,
|
||||||
)
|
)
|
||||||
const subscriptionRemainingTimePercentageThisMonth = Math.floor(
|
const subscriptionRemainingTimePercentageThisMonth = Math.floor(
|
||||||
subscriptionRemainingTimePercentageMeasureThisMonth?.average ?? 0,
|
subscriptionRemainingTimePercentageMeasureThisMonth?.average ?? 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
const registrationLengthMeasureThisMonth = data.statisticMeasures.find(
|
const registrationLengthMeasureThisMonth = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.RegistrationLength && a.period === Period.ThisMonth,
|
a.name === StatisticMeasureName.NAMES.RegistrationLength && a.period === Period.ThisMonth,
|
||||||
)
|
)
|
||||||
const registrationLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
|
const registrationLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
|
||||||
Math.floor(registrationLengthMeasureThisMonth?.average ?? 0),
|
Math.floor(registrationLengthMeasureThisMonth?.average ?? 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
const registrationToSubscriptionMeasureThisMonth = data.statisticMeasures.find(
|
const registrationToSubscriptionMeasureThisMonth = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.RegistrationToSubscriptionTime && a.period === Period.ThisMonth,
|
a.name === StatisticMeasureName.NAMES.RegistrationToSubscriptionTime && a.period === Period.ThisMonth,
|
||||||
)
|
)
|
||||||
const registrationToSubscriptionDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
|
const registrationToSubscriptionDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
|
||||||
Math.floor(registrationToSubscriptionMeasureThisMonth?.average ?? 0),
|
Math.floor(registrationToSubscriptionMeasureThisMonth?.average ?? 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
const plusSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
|
const plusSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
)
|
)
|
||||||
const plusSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
const plusSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday,
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
)
|
)
|
||||||
const plusSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
|
const plusSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday,
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
)
|
)
|
||||||
const plusSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
const plusSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday,
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
)
|
)
|
||||||
const proSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
|
const proSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
|
||||||
)
|
)
|
||||||
const proSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
const proSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday,
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
)
|
)
|
||||||
const proSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
|
const proSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday,
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
)
|
)
|
||||||
const proSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
const proSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday,
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
)
|
)
|
||||||
const plusSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
const plusSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
)
|
)
|
||||||
const plusSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
const plusSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth,
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
)
|
)
|
||||||
const plusSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
const plusSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth,
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
)
|
)
|
||||||
const plusSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
const plusSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth,
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
)
|
)
|
||||||
const proSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
const proSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
|
||||||
)
|
)
|
||||||
const proSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
const proSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth,
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
)
|
)
|
||||||
const proSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
const proSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth,
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
)
|
)
|
||||||
const proSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
const proSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
(a: { name: string; period: Period }) =>
|
||||||
a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth,
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
)
|
)
|
||||||
|
|
||||||
const mrrOverTime = data.statisticsOverTime.find(
|
const mrrOverTime = data.statisticsOverTime.find(
|
||||||
@@ -594,12 +553,39 @@ export const html = (data: any, timer: TimerInterface) => {
|
|||||||
(value: { periodKey: string }) => value.periodKey === thisMonthPeriodKey,
|
(value: { periodKey: string }) => value.periodKey === thisMonthPeriodKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const totalActiveUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveUsers, data)
|
||||||
|
const totalActiveFreeUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveFreeUsers, data)
|
||||||
|
const totalActivePlusUsers = countActiveUsers(StatisticMeasureName.NAMES.ActivePlusUsers, data)
|
||||||
|
const totalActiveProUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveProUsers, data)
|
||||||
|
|
||||||
return ` <div>
|
return ` <div>
|
||||||
<p>Hello,</p>
|
<p>Hello,</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Here are some statistics from yesterday:</strong>
|
<strong>Here are some statistics from yesterday:</strong>
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Active Users</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Total:</b> ${totalActiveUsers.yesterday.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>By Subscription Type:</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>FREE:</b> ${totalActiveFreeUsers.yesterday.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>PLUS:</b> ${totalActivePlusUsers.yesterday.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>PRO:</b> ${totalActiveProUsers.yesterday.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<b>Payments</b>
|
<b>Payments</b>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -798,6 +784,28 @@ export const html = (data: any, timer: TimerInterface) => {
|
|||||||
<strong>Here are some statistics from last 30 days:</strong>
|
<strong>Here are some statistics from last 30 days:</strong>
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Active Users (Average)</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Total:</b> ${totalActiveUsers.last30Days.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>By Subscription Type:</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>FREE:</b> ${totalActiveFreeUsers.last30Days.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>PLUS:</b> ${totalActivePlusUsers.last30Days.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>PRO:</b> ${totalActiveProUsers.last30Days.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<b>Payments (This Month)</b>
|
<b>Payments (This Month)</b>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -930,10 +938,6 @@ export const html = (data: any, timer: TimerInterface) => {
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
|
||||||
<strong>Here is the MRR chart over 30 days:</strong>
|
|
||||||
</p>
|
|
||||||
<img src=${chartUrls.mrr}></img>
|
|
||||||
<p>
|
<p>
|
||||||
<strong>Here is the MRR Monthly chart this year:</strong>
|
<strong>Here is the MRR Monthly chart this year:</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -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<AccountDeletionRequestedEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.payload = {
|
|
||||||
userUuid: '1-2-3',
|
|
||||||
userCreatedAtTimestamp: 1,
|
|
||||||
regularSubscriptionUuid: '2-3-4',
|
|
||||||
}
|
|
||||||
|
|
||||||
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
|
|
||||||
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue({ id: 3 })
|
|
||||||
analyticsEntityRepository.remove = jest.fn()
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
|
|
||||||
timer = {} as jest.Mocked<TimerInterface>
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -6,7 +6,7 @@ import TYPES from '../../Bootstrap/Types'
|
|||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
|
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
|||||||
])
|
])
|
||||||
|
|
||||||
const registrationLength = this.timer.getTimestampInMicroseconds() - event.payload.userCreatedAtTimestamp
|
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.Today,
|
||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
|
|||||||
@@ -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>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<PaymentFailedEvent>
|
|
||||||
event.payload = {
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark payment failed for analytics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<PaymentSuccessEvent>
|
|
||||||
event.payload = {
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
amount: 12.45,
|
|
||||||
billingFrequency: 12,
|
|
||||||
paymentType: 'initial',
|
|
||||||
subscriptionName: 'PRO_PLAN',
|
|
||||||
}
|
|
||||||
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
|
||||||
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,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -6,7 +6,7 @@ import { Logger } from 'winston'
|
|||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||||
@@ -20,15 +20,27 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
|||||||
[
|
[
|
||||||
PaymentType.Initial,
|
PaymentType.Initial,
|
||||||
new Map([
|
new Map([
|
||||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome],
|
[
|
||||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome],
|
SubscriptionBillingFrequency.Monthly,
|
||||||
|
StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
SubscriptionBillingFrequency.Annual,
|
||||||
|
StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome,
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
PaymentType.Renewal,
|
PaymentType.Renewal,
|
||||||
new Map([
|
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,
|
PaymentType.Initial,
|
||||||
new Map([
|
new Map([
|
||||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome],
|
[
|
||||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome],
|
SubscriptionBillingFrequency.Monthly,
|
||||||
|
StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
SubscriptionBillingFrequency.Annual,
|
||||||
|
StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome,
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
PaymentType.Renewal,
|
PaymentType.Renewal,
|
||||||
new Map([
|
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,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
|
|
||||||
const statisticMeasures = [StatisticsMeasure.Income]
|
const statisticMeasures = [StatisticMeasureName.NAMES.Income]
|
||||||
|
|
||||||
const detailedMeasure = this.DETAILED_MEASURES.get(event.payload.subscriptionName as SubscriptionName)
|
const detailedMeasure = this.DETAILED_MEASURES.get(event.payload.subscriptionName as SubscriptionName)
|
||||||
?.get(event.payload.paymentType as PaymentType)
|
?.get(event.payload.paymentType as PaymentType)
|
||||||
|
|||||||
@@ -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<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<RefundProcessedEvent>
|
|
||||||
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,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -2,7 +2,7 @@ import { DomainEventHandlerInterface, RefundProcessedEvent } from '@standardnote
|
|||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable } from 'inversify'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export class RefundProcessedEventHandler implements DomainEventHandlerInterface
|
|||||||
constructor(@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface) {}
|
constructor(@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface) {}
|
||||||
|
|
||||||
async handle(event: RefundProcessedEvent): Promise<void> {
|
async handle(event: RefundProcessedEvent): Promise<void> {
|
||||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.Refunds, event.payload.amount, [
|
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.Refunds, event.payload.amount, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { DomainEventHandlerInterface, StatisticPersistenceRequestedEvent } from '@standardnotes/domain-events'
|
||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
import { Logger } from 'winston'
|
||||||
|
import { PersistStatistic } from '../UseCase/PersistStatistic/PersistStatistic'
|
||||||
|
|
||||||
|
export class StatisticPersistenceRequestedEventHandler implements DomainEventHandlerInterface {
|
||||||
|
constructor(private persistStatistic: PersistStatistic, private timer: TimerInterface, private logger: Logger) {}
|
||||||
|
|
||||||
|
async handle(event: StatisticPersistenceRequestedEvent): Promise<void> {
|
||||||
|
const result = await this.persistStatistic.execute({
|
||||||
|
date: this.timer.convertMicrosecondsToDate(event.payload.date),
|
||||||
|
statisticMeasureName: event.payload.statisticMeasureName,
|
||||||
|
value: event.payload.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.isFailed()) {
|
||||||
|
this.logger.error(result.getError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionCancelledEvent>
|
|
||||||
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>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -6,13 +6,13 @@ import { Username } from '@standardnotes/domain-core'
|
|||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
||||||
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
|
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
|
||||||
@@ -58,7 +58,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
|||||||
}
|
}
|
||||||
|
|
||||||
const subscriptionLength = event.payload.timestamp - event.payload.subscriptionCreatedAt
|
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.Today,
|
||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
@@ -70,7 +70,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
|||||||
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
|
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
|
||||||
|
|
||||||
await this.statisticsStore.incrementMeasure(
|
await this.statisticsStore.incrementMeasure(
|
||||||
StatisticsMeasure.RemainingSubscriptionTimePercentage,
|
StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage,
|
||||||
remainingSubscriptionPercentage,
|
remainingSubscriptionPercentage,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionExpiredEvent>
|
|
||||||
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>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.setMeasure = jest.fn()
|
|
||||||
|
|
||||||
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -6,7 +6,7 @@ import { Logger } from 'winston'
|
|||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||||
@@ -33,7 +33,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
|||||||
)
|
)
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(
|
await this.statisticsStore.setMeasure(
|
||||||
StatisticsMeasure.TotalCustomers,
|
StatisticMeasureName.NAMES.TotalCustomers,
|
||||||
event.payload.totalActiveSubscriptionsCount,
|
event.payload.totalActiveSubscriptionsCount,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
statisticsStore.setMeasure = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionPurchasedEvent>
|
|
||||||
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>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
analyticsStore.unmarkActivity = jest.fn()
|
|
||||||
|
|
||||||
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -6,7 +6,7 @@ import { Logger } from 'winston'
|
|||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||||
@@ -45,18 +45,18 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
|||||||
|
|
||||||
if (event.payload.newSubscriber) {
|
if (event.payload.newSubscriber) {
|
||||||
await this.statisticsStore.incrementMeasure(
|
await this.statisticsStore.incrementMeasure(
|
||||||
StatisticsMeasure.RegistrationToSubscriptionTime,
|
StatisticMeasureName.NAMES.RegistrationToSubscriptionTime,
|
||||||
event.payload.timestamp - event.payload.userRegisteredAt,
|
event.payload.timestamp - event.payload.userRegisteredAt,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||||
)
|
)
|
||||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.NewCustomers, 1, [
|
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.NewCustomers, 1, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
])
|
])
|
||||||
await this.statisticsStore.setMeasure(
|
await this.statisticsStore.setMeasure(
|
||||||
StatisticsMeasure.TotalCustomers,
|
StatisticMeasureName.NAMES.TotalCustomers,
|
||||||
event.payload.totalActiveSubscriptionsCount,
|
event.payload.totalActiveSubscriptionsCount,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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<SubscriptionReactivatedEvent>
|
|
||||||
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>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
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,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionRefundedEvent>
|
|
||||||
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>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
analyticsStore.wasActivityDone = jest.fn().mockReturnValue(true)
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.setMeasure = jest.fn()
|
|
||||||
|
|
||||||
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -6,7 +6,7 @@ import { Logger } from 'winston'
|
|||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||||
@@ -70,7 +70,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(
|
await this.statisticsStore.setMeasure(
|
||||||
StatisticsMeasure.TotalCustomers,
|
StatisticMeasureName.NAMES.TotalCustomers,
|
||||||
event.payload.totalActiveSubscriptionsCount,
|
event.payload.totalActiveSubscriptionsCount,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionRenewedEvent>
|
|
||||||
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>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
analyticsStore.unmarkActivity = jest.fn()
|
|
||||||
|
|
||||||
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -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<UserRegisteredEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.payload = {
|
|
||||||
userUuid: '1-2-3',
|
|
||||||
email: 'test@test.te',
|
|
||||||
protocolVersion: ProtocolVersion.V004,
|
|
||||||
}
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
|
|
||||||
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,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Result, Entity, UniqueEntityId } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { StatisticMeasureProps } from './StatisticMeasureProps'
|
||||||
|
|
||||||
|
export class StatisticMeasure extends Entity<StatisticMeasureProps> {
|
||||||
|
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<StatisticMeasure> {
|
||||||
|
return Result.ok<StatisticMeasure>(new StatisticMeasure(props, id))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { ValueObject, Result } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { StatisticMeasureNameProps } from './StatisticMeasureNameProps'
|
||||||
|
|
||||||
|
export class StatisticMeasureName extends ValueObject<StatisticMeasureNameProps> {
|
||||||
|
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',
|
||||||
|
ActiveUsers: 'active-users',
|
||||||
|
ActiveProUsers: 'active-pro-users',
|
||||||
|
ActivePlusUsers: 'active-plus-users',
|
||||||
|
ActiveFreeUsers: 'active-free-users',
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): string {
|
||||||
|
return this.props.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(props: StatisticMeasureNameProps) {
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(name: string): Result<StatisticMeasureName> {
|
||||||
|
const isValidName = Object.values(this.NAMES).includes(name)
|
||||||
|
if (!isValidName) {
|
||||||
|
return Result.fail<StatisticMeasureName>(`Invalid statistics measure name: ${name}`)
|
||||||
|
} else {
|
||||||
|
return Result.ok<StatisticMeasureName>(new StatisticMeasureName({ value: name }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface StatisticMeasureNameProps {
|
||||||
|
value: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { StatisticMeasureName } from './StatisticMeasureName'
|
||||||
|
|
||||||
|
export interface StatisticMeasureProps {
|
||||||
|
name: StatisticMeasureName
|
||||||
|
value: number
|
||||||
|
date: Date
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { StatisticMeasure } from './StatisticMeasure'
|
||||||
|
|
||||||
|
export interface StatisticMeasureRepositoryInterface {
|
||||||
|
save(statisticMeasure: StatisticMeasure): Promise<void>
|
||||||
|
}
|
||||||
@@ -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',
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
import { StatisticsMeasure } from './StatisticsMeasure'
|
|
||||||
|
|
||||||
export interface StatisticsStoreInterface {
|
export interface StatisticsStoreInterface {
|
||||||
incrementSNJSVersionUsage(snjsVersion: string): Promise<void>
|
incrementSNJSVersionUsage(snjsVersion: string): Promise<void>
|
||||||
@@ -8,13 +7,13 @@ export interface StatisticsStoreInterface {
|
|||||||
getYesterdaySNJSUsage(): Promise<Array<{ version: string; count: number }>>
|
getYesterdaySNJSUsage(): Promise<Array<{ version: string; count: number }>>
|
||||||
getYesterdayApplicationUsage(): Promise<Array<{ version: string; count: number }>>
|
getYesterdayApplicationUsage(): Promise<Array<{ version: string; count: number }>>
|
||||||
getYesterdayOutOfSyncIncidents(): Promise<number>
|
getYesterdayOutOfSyncIncidents(): Promise<number>
|
||||||
incrementMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void>
|
incrementMeasure(measure: string, value: number, periods: Period[]): Promise<void>
|
||||||
setMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void>
|
setMeasure(measure: string, value: number, periods: Period[]): Promise<void>
|
||||||
getMeasureAverage(measure: StatisticsMeasure, period: Period): Promise<number>
|
getMeasureAverage(measure: string, period: Period): Promise<number>
|
||||||
getMeasureTotal(measure: StatisticsMeasure, periodOrPeriodKey: Period | string): Promise<number>
|
getMeasureTotal(measure: string, periodOrPeriodKey: Period | string): Promise<number>
|
||||||
getMeasureIncrementCounts(measure: StatisticsMeasure, period: Period): Promise<number>
|
getMeasureIncrementCounts(measure: string, period: Period): Promise<number>
|
||||||
calculateTotalCountOverPeriod(
|
calculateTotalCountOverPeriod(
|
||||||
measure: StatisticsMeasure,
|
measure: string,
|
||||||
period: Period,
|
period: Period,
|
||||||
): Promise<Array<{ periodKey: string; totalCount: number }>>
|
): Promise<Array<{ periodKey: string; totalCount: number }>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export class PeriodKeyGenerator implements PeriodKeyGeneratorInterface {
|
|||||||
return `${this.getYear(date)}-${this.getMonth(date)}`
|
return `${this.getYear(date)}-${this.getMonth(date)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDailyKey(date?: Date): string {
|
getDailyKey(date?: Date): string {
|
||||||
date = date ?? new Date()
|
date = date ?? new Date()
|
||||||
|
|
||||||
return `${this.getYear(date)}-${this.getMonth(date)}-${this.getDayOfTheMonth(date)}`
|
return `${this.getYear(date)}-${this.getMonth(date)}-${this.getDayOfTheMonth(date)}`
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Period } from './Period'
|
|||||||
|
|
||||||
export interface PeriodKeyGeneratorInterface {
|
export interface PeriodKeyGeneratorInterface {
|
||||||
getPeriodKey(period: Period): string
|
getPeriodKey(period: Period): string
|
||||||
|
getDailyKey(date?: Date): string
|
||||||
convertPeriodKeyToPeriod(periodKey: string): Period
|
convertPeriodKeyToPeriod(periodKey: string): Period
|
||||||
getDiscretePeriodKeys(period: Period): string[]
|
getDiscretePeriodKeys(period: Period): string[]
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import 'reflect-metadata'
|
import 'reflect-metadata'
|
||||||
|
|
||||||
import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface'
|
import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface'
|
||||||
import { StatisticsMeasure } from '../../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../../Time/Period'
|
import { Period } from '../../Time/Period'
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ describe('CalculateMonthlyRecurringRevenue', () => {
|
|||||||
it('should calculate the MRR diff and persist it as a statistic', async () => {
|
it('should calculate the MRR diff and persist it as a statistic', async () => {
|
||||||
await createUseCase().execute({})
|
await createUseCase().execute({})
|
||||||
|
|
||||||
expect(statisticsStore.setMeasure).toHaveBeenCalledWith(StatisticsMeasure.MRR, 123.45, [
|
expect(statisticsStore.setMeasure).toHaveBeenCalledWith(StatisticMeasureName.NAMES.MRR, 123.45, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
|
|||||||
+7
-7
@@ -5,11 +5,11 @@ import { Result } from '@standardnotes/domain-core'
|
|||||||
import TYPES from '../../../Bootstrap/Types'
|
import TYPES from '../../../Bootstrap/Types'
|
||||||
import { MonthlyRevenue } from '../../Revenue/MonthlyRevenue'
|
import { MonthlyRevenue } from '../../Revenue/MonthlyRevenue'
|
||||||
import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface'
|
import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface'
|
||||||
import { StatisticsMeasure } from '../../Statistics/StatisticsMeasure'
|
|
||||||
import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../../Time/Period'
|
import { Period } from '../../Time/Period'
|
||||||
import { DomainUseCaseInterface } from '../DomainUseCaseInterface'
|
import { DomainUseCaseInterface } from '../DomainUseCaseInterface'
|
||||||
import { CalculateMonthlyRecurringRevenueDTO } from './CalculateMonthlyRecurringRevenueDTO'
|
import { CalculateMonthlyRecurringRevenueDTO } from './CalculateMonthlyRecurringRevenueDTO'
|
||||||
|
import { StatisticMeasureName } from '../../Statistics/StatisticMeasureName'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<MonthlyRevenue> {
|
export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<MonthlyRevenue> {
|
||||||
@@ -24,7 +24,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
|
|||||||
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
|
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(StatisticsMeasure.MRR, mrrDiff, [
|
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.MRR, mrrDiff, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
@@ -34,7 +34,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
|
|||||||
billingFrequencies: [SubscriptionBillingFrequency.Monthly],
|
billingFrequencies: [SubscriptionBillingFrequency.Monthly],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(StatisticsMeasure.MonthlyPlansMRR, monthlyPlansMrrDiff, [
|
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.MonthlyPlansMRR, monthlyPlansMrrDiff, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
@@ -44,7 +44,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
|
|||||||
billingFrequencies: [SubscriptionBillingFrequency.Annual],
|
billingFrequencies: [SubscriptionBillingFrequency.Annual],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(StatisticsMeasure.AnnualPlansMRR, annualPlansMrrDiff, [
|
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.AnnualPlansMRR, annualPlansMrrDiff, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
@@ -54,7 +54,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
|
|||||||
billingFrequencies: [SubscriptionBillingFrequency.FiveYear],
|
billingFrequencies: [SubscriptionBillingFrequency.FiveYear],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(StatisticsMeasure.FiveYearPlansMRR, fiveYearPlansMrrDiff, [
|
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.FiveYearPlansMRR, fiveYearPlansMrrDiff, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
@@ -65,7 +65,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
|
|||||||
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
|
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(StatisticsMeasure.ProPlansMRR, proPlansMrrDiff, [
|
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.ProPlansMRR, proPlansMrrDiff, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
@@ -76,7 +76,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
|
|||||||
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
|
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(StatisticsMeasure.PlusPlansMRR, plusPlansMrrDiff, [
|
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.PlusPlansMRR, plusPlansMrrDiff, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
|
|||||||
@@ -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<StatisticMeasure> {
|
||||||
|
constructor(private statisticMeasureRepository: StatisticMeasureRepositoryInterface) {}
|
||||||
|
|
||||||
|
async execute(dto: PersistStatisticDTO): Promise<Result<StatisticMeasure>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface PersistStatisticDTO {
|
||||||
|
statisticMeasureName: string
|
||||||
|
value: number
|
||||||
|
date: Date
|
||||||
|
}
|
||||||
@@ -1,16 +1,23 @@
|
|||||||
import * as IORedis from 'ioredis'
|
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 { StatisticsStoreInterface } from '../../Domain/Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../../Domain/Time/Period'
|
import { Period } from '../../Domain/Time/Period'
|
||||||
import { PeriodKeyGeneratorInterface } from '../../Domain/Time/PeriodKeyGeneratorInterface'
|
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) {}
|
constructor(private periodKeyGenerator: PeriodKeyGeneratorInterface, private redisClient: IORedis.Redis) {}
|
||||||
|
|
||||||
|
async save(statisticMeasure: StatisticMeasure): Promise<void> {
|
||||||
|
const periodKey = this.periodKeyGenerator.getDailyKey(statisticMeasure.props.date)
|
||||||
|
|
||||||
|
await this.setMeasure(statisticMeasure.name, statisticMeasure.value, [periodKey])
|
||||||
|
}
|
||||||
|
|
||||||
async calculateTotalCountOverPeriod(
|
async calculateTotalCountOverPeriod(
|
||||||
measure: StatisticsMeasure,
|
measure: string,
|
||||||
period: Period,
|
period: Period,
|
||||||
): Promise<{ periodKey: string; totalCount: number }[]> {
|
): Promise<{ periodKey: string; totalCount: number }[]> {
|
||||||
if (
|
if (
|
||||||
@@ -38,7 +45,7 @@ export class RedisStatisticsStore implements StatisticsStoreInterface {
|
|||||||
return counts
|
return counts
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMeasureIncrementCounts(measure: StatisticsMeasure, period: Period): Promise<number> {
|
async getMeasureIncrementCounts(measure: string, period: Period): Promise<number> {
|
||||||
const increments = await this.redisClient.get(
|
const increments = await this.redisClient.get(
|
||||||
`count:increments:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`,
|
`count:increments:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`,
|
||||||
)
|
)
|
||||||
@@ -49,17 +56,22 @@ export class RedisStatisticsStore implements StatisticsStoreInterface {
|
|||||||
return +increments
|
return +increments
|
||||||
}
|
}
|
||||||
|
|
||||||
async setMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void> {
|
async setMeasure(measure: string, value: number, periodsOrPeriodKeys: Period[] | string[]): Promise<void> {
|
||||||
const pipeline = this.redisClient.pipeline()
|
const pipeline = this.redisClient.pipeline()
|
||||||
|
|
||||||
for (const period of periods) {
|
for (const periodOrPeriodKey of periodsOrPeriodKeys) {
|
||||||
pipeline.set(`count:measure:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`, value)
|
let periodKey = periodOrPeriodKey
|
||||||
|
if (!isNaN(+periodOrPeriodKey)) {
|
||||||
|
periodKey = this.periodKeyGenerator.getPeriodKey(periodOrPeriodKey as Period)
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline.set(`count:measure:${measure}:timespan:${periodKey}`, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
await pipeline.exec()
|
await pipeline.exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMeasureTotal(measure: StatisticsMeasure, periodOrPeriodKey: Period | string): Promise<number> {
|
async getMeasureTotal(measure: string, periodOrPeriodKey: Period | string): Promise<number> {
|
||||||
let periodKey = periodOrPeriodKey
|
let periodKey = periodOrPeriodKey
|
||||||
if (!isNaN(+periodOrPeriodKey)) {
|
if (!isNaN(+periodOrPeriodKey)) {
|
||||||
periodKey = this.periodKeyGenerator.getPeriodKey(periodOrPeriodKey as Period)
|
periodKey = this.periodKeyGenerator.getPeriodKey(periodOrPeriodKey as Period)
|
||||||
@@ -74,7 +86,7 @@ export class RedisStatisticsStore implements StatisticsStoreInterface {
|
|||||||
return +totalValue
|
return +totalValue
|
||||||
}
|
}
|
||||||
|
|
||||||
async incrementMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void> {
|
async incrementMeasure(measure: string, value: number, periods: Period[]): Promise<void> {
|
||||||
const pipeline = this.redisClient.pipeline()
|
const pipeline = this.redisClient.pipeline()
|
||||||
|
|
||||||
for (const period of periods) {
|
for (const period of periods) {
|
||||||
@@ -85,7 +97,7 @@ export class RedisStatisticsStore implements StatisticsStoreInterface {
|
|||||||
await pipeline.exec()
|
await pipeline.exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMeasureAverage(measure: StatisticsMeasure, period: Period): Promise<number> {
|
async getMeasureAverage(measure: string, period: Period): Promise<number> {
|
||||||
const increments = await this.getMeasureIncrementCounts(measure, period)
|
const increments = await this.getMeasureIncrementCounts(measure, period)
|
||||||
|
|
||||||
if (increments === 0) {
|
if (increments === 0) {
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.41.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.41.1...@standardnotes/api-gateway@1.41.2) (2022-12-20)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|
||||||
|
## [1.41.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.41.0...@standardnotes/api-gateway@1.41.1) (2022-12-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|
||||||
# [1.41.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.40.2...@standardnotes/api-gateway@1.41.0) (2022-12-19)
|
# [1.41.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.40.2...@standardnotes/api-gateway@1.41.0) (2022-12-19)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/api-gateway",
|
"name": "@standardnotes/api-gateway",
|
||||||
"version": "1.41.0",
|
"version": "1.41.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,46 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.70.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.3...@standardnotes/auth-server@1.70.4) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **auth:** replace date object with number timestamp ([5b4bb6e](https://github.com/standardnotes/server/commit/5b4bb6e7a78a1b0f4e663bb990619f65f6a5c757))
|
||||||
|
|
||||||
|
## [1.70.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.2...@standardnotes/auth-server@1.70.3) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **auth:** add persisting statistics for all subscription plans ([addedb3](https://github.com/standardnotes/server/commit/addedb3091ddae81618d56663e18f2ae76a43c4e))
|
||||||
|
|
||||||
|
## [1.70.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.1...@standardnotes/auth-server@1.70.2) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **auth:** docker command ([85d2f42](https://github.com/standardnotes/server/commit/85d2f42f473110e8dfca975bfecc7a56823bdef4))
|
||||||
|
|
||||||
|
## [1.70.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.0...@standardnotes/auth-server@1.70.1) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **auth:** saving subscription plan name in session traces ([3064d03](https://github.com/standardnotes/server/commit/3064d03aa9a2ac9ca3acfff30480ea8629faeb14))
|
||||||
|
|
||||||
|
# [1.70.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.69.1...@standardnotes/auth-server@1.70.0) (2022-12-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **auth:** add requesting persisting statistics ([a35271f](https://github.com/standardnotes/server/commit/a35271fbb399b68a3ac7021395d8063707fba222))
|
||||||
|
|
||||||
|
## [1.69.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.69.0...@standardnotes/auth-server@1.69.1) (2022-12-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/auth-server
|
||||||
|
|
||||||
|
# [1.69.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.68.0...@standardnotes/auth-server@1.69.0) (2022-12-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **auth:** add session traces cleanup procedure ([147d8fd](https://github.com/standardnotes/server/commit/147d8fd9af89d2b97cd68eefac36e53d14d511bf))
|
||||||
|
|
||||||
# [1.68.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.67.3...@standardnotes/auth-server@1.68.0) (2022-12-19)
|
# [1.68.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.67.3...@standardnotes/auth-server@1.68.0) (2022-12-19)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import 'reflect-metadata'
|
||||||
|
|
||||||
|
import 'newrelic'
|
||||||
|
|
||||||
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
|
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||||
|
import TYPES from '../src/Bootstrap/Types'
|
||||||
|
import { Env } from '../src/Bootstrap/Env'
|
||||||
|
import { CleanupSessionTraces } from '../src/Domain/UseCase/CleanupSessionTraces/CleanupSessionTraces'
|
||||||
|
|
||||||
|
const container = new ContainerConfigLoader()
|
||||||
|
void container.load().then((container) => {
|
||||||
|
const env: Env = new Env()
|
||||||
|
env.load()
|
||||||
|
|
||||||
|
const logger: Logger = container.get(TYPES.Logger)
|
||||||
|
|
||||||
|
logger.info('Starting session traces cleanup')
|
||||||
|
|
||||||
|
const cleanupSessionTraces: CleanupSessionTraces = container.get(TYPES.CleanupSessionTraces)
|
||||||
|
|
||||||
|
Promise.resolve(
|
||||||
|
cleanupSessionTraces.execute({
|
||||||
|
date: new Date(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
logger.info('Expired session traces cleaned.')
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(`Could not clean session traces: ${error.message}`)
|
||||||
|
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import 'reflect-metadata'
|
||||||
|
|
||||||
|
import 'newrelic'
|
||||||
|
|
||||||
|
import { Logger } from 'winston'
|
||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
|
||||||
|
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||||
|
import TYPES from '../src/Bootstrap/Types'
|
||||||
|
import { Env } from '../src/Bootstrap/Env'
|
||||||
|
import { PersistStatistics } from '../src/Domain/UseCase/PersistStatistics/PersistStatistics'
|
||||||
|
|
||||||
|
const container = new ContainerConfigLoader()
|
||||||
|
void container.load().then((container) => {
|
||||||
|
const env: Env = new Env()
|
||||||
|
env.load()
|
||||||
|
|
||||||
|
const logger: Logger = container.get(TYPES.Logger)
|
||||||
|
|
||||||
|
logger.info('Starting session traces cleanup')
|
||||||
|
|
||||||
|
const persistStats: PersistStatistics = container.get(TYPES.PersistStatistics)
|
||||||
|
const timer: TimerInterface = container.get(TYPES.Timer)
|
||||||
|
|
||||||
|
Promise.resolve(
|
||||||
|
persistStats.execute({
|
||||||
|
sessionsInADay: timer.getUTCDateNDaysAgo(1),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
logger.info('Stats persisted.')
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(`Could not persist stats: ${error.message}`)
|
||||||
|
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -19,6 +19,16 @@ case "$COMMAND" in
|
|||||||
yarn workspace @standardnotes/auth-server worker
|
yarn workspace @standardnotes/auth-server worker
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
'cleanup' )
|
||||||
|
echo "[Docker] Starting Cleanup..."
|
||||||
|
yarn workspace @standardnotes/auth-server cleanup
|
||||||
|
;;
|
||||||
|
|
||||||
|
'stats' )
|
||||||
|
echo "[Docker] Starting Persisting Stats..."
|
||||||
|
yarn workspace @standardnotes/auth-server stats
|
||||||
|
;;
|
||||||
|
|
||||||
'email-daily-backup' )
|
'email-daily-backup' )
|
||||||
echo "[Docker] Starting Email Daily Backup..."
|
echo "[Docker] Starting Email Daily Backup..."
|
||||||
yarn workspace @standardnotes/auth-server daily-backup:email
|
yarn workspace @standardnotes/auth-server daily-backup:email
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/auth-server",
|
"name": "@standardnotes/auth-server",
|
||||||
"version": "1.68.0",
|
"version": "1.70.4",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||||
"start": "yarn node dist/bin/server.js",
|
"start": "yarn node dist/bin/server.js",
|
||||||
"worker": "yarn node dist/bin/worker.js",
|
"worker": "yarn node dist/bin/worker.js",
|
||||||
|
"cleanup": "yarn node dist/bin/cleanup.js",
|
||||||
|
"stats": "yarn node dist/bin/stats.js",
|
||||||
"daily-backup:email": "yarn node dist/bin/backup.js email daily",
|
"daily-backup:email": "yarn node dist/bin/backup.js email daily",
|
||||||
"user-email-backup": "yarn node dist/bin/user_email_backup.js",
|
"user-email-backup": "yarn node dist/bin/user_email_backup.js",
|
||||||
"daily-backup:dropbox": "yarn node dist/bin/backup.js dropbox daily",
|
"daily-backup:dropbox": "yarn node dist/bin/backup.js dropbox daily",
|
||||||
|
|||||||
@@ -201,6 +201,8 @@ import { SessionTracePersistenceMapper } from '../Mapping/SessionTracePersistenc
|
|||||||
import { SessionTrace } from '../Domain/Session/SessionTrace'
|
import { SessionTrace } from '../Domain/Session/SessionTrace'
|
||||||
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
|
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
|
||||||
import { TraceSession } from '../Domain/UseCase/TraceSession/TraceSession'
|
import { TraceSession } from '../Domain/UseCase/TraceSession/TraceSession'
|
||||||
|
import { CleanupSessionTraces } from '../Domain/UseCase/CleanupSessionTraces/CleanupSessionTraces'
|
||||||
|
import { PersistStatistics } from '../Domain/UseCase/PersistStatistics/PersistStatistics'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||||
@@ -499,6 +501,20 @@ export class ContainerConfigLoader {
|
|||||||
container.get(TYPES.SESSION_TRACE_DAYS_TTL),
|
container.get(TYPES.SESSION_TRACE_DAYS_TTL),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
container
|
||||||
|
.bind<PersistStatistics>(TYPES.PersistStatistics)
|
||||||
|
.toConstantValue(
|
||||||
|
new PersistStatistics(
|
||||||
|
container.get(TYPES.SessionTraceRepository),
|
||||||
|
container.get(TYPES.DomainEventPublisher),
|
||||||
|
container.get(TYPES.DomainEventFactory),
|
||||||
|
container.get(TYPES.Timer),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
container
|
||||||
|
.bind<CleanupSessionTraces>(TYPES.CleanupSessionTraces)
|
||||||
|
.toConstantValue(new CleanupSessionTraces(container.get(TYPES.SessionTraceRepository)))
|
||||||
container.bind<AuthenticateUser>(TYPES.AuthenticateUser).to(AuthenticateUser)
|
container.bind<AuthenticateUser>(TYPES.AuthenticateUser).to(AuthenticateUser)
|
||||||
container.bind<AuthenticateRequest>(TYPES.AuthenticateRequest).to(AuthenticateRequest)
|
container.bind<AuthenticateRequest>(TYPES.AuthenticateRequest).to(AuthenticateRequest)
|
||||||
container.bind<RefreshSessionToken>(TYPES.RefreshSessionToken).to(RefreshSessionToken)
|
container.bind<RefreshSessionToken>(TYPES.RefreshSessionToken).to(RefreshSessionToken)
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ const TYPES = {
|
|||||||
CreateCrossServiceToken: Symbol.for('CreateCrossServiceToken'),
|
CreateCrossServiceToken: Symbol.for('CreateCrossServiceToken'),
|
||||||
ProcessUserRequest: Symbol.for('ProcessUserRequest'),
|
ProcessUserRequest: Symbol.for('ProcessUserRequest'),
|
||||||
TraceSession: Symbol.for('TraceSession'),
|
TraceSession: Symbol.for('TraceSession'),
|
||||||
|
CleanupSessionTraces: Symbol.for('CleanupSessionTraces'),
|
||||||
|
PersistStatistics: Symbol.for('PersistStatistics'),
|
||||||
// Handlers
|
// Handlers
|
||||||
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
||||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
UserContentSizeRecalculationRequestedEvent,
|
UserContentSizeRecalculationRequestedEvent,
|
||||||
MuteEmailsSettingChangedEvent,
|
MuteEmailsSettingChangedEvent,
|
||||||
EmailRequestedEvent,
|
EmailRequestedEvent,
|
||||||
|
StatisticPersistenceRequestedEvent,
|
||||||
} from '@standardnotes/domain-events'
|
} from '@standardnotes/domain-events'
|
||||||
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
|
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
|
||||||
import { TimerInterface } from '@standardnotes/time'
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
@@ -31,6 +32,25 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
|||||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||||
|
|
||||||
|
createStatisticPersistenceRequestedEvent(dto: {
|
||||||
|
statisticMeasureName: string
|
||||||
|
value: number
|
||||||
|
date: number
|
||||||
|
}): StatisticPersistenceRequestedEvent {
|
||||||
|
return {
|
||||||
|
type: 'STATISTIC_PERSISTENCE_REQUESTED',
|
||||||
|
createdAt: this.timer.getUTCDate(),
|
||||||
|
meta: {
|
||||||
|
correlation: {
|
||||||
|
userIdentifier: '-',
|
||||||
|
userIdentifierType: 'email',
|
||||||
|
},
|
||||||
|
origin: DomainEventService.Auth,
|
||||||
|
},
|
||||||
|
payload: dto,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createMuteEmailsSettingChangedEvent(dto: {
|
createMuteEmailsSettingChangedEvent(dto: {
|
||||||
username: string
|
username: string
|
||||||
mute: boolean
|
mute: boolean
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
UserContentSizeRecalculationRequestedEvent,
|
UserContentSizeRecalculationRequestedEvent,
|
||||||
MuteEmailsSettingChangedEvent,
|
MuteEmailsSettingChangedEvent,
|
||||||
EmailRequestedEvent,
|
EmailRequestedEvent,
|
||||||
|
StatisticPersistenceRequestedEvent,
|
||||||
} from '@standardnotes/domain-events'
|
} from '@standardnotes/domain-events'
|
||||||
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
|
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
|
||||||
|
|
||||||
@@ -88,4 +89,9 @@ export interface DomainEventFactoryInterface {
|
|||||||
mute: boolean
|
mute: boolean
|
||||||
emailSubscriptionRejectionLevel: string
|
emailSubscriptionRejectionLevel: string
|
||||||
}): MuteEmailsSettingChangedEvent
|
}): MuteEmailsSettingChangedEvent
|
||||||
|
createStatisticPersistenceRequestedEvent(dto: {
|
||||||
|
statisticMeasureName: string
|
||||||
|
value: number
|
||||||
|
date: number
|
||||||
|
}): StatisticPersistenceRequestedEvent
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,4 +41,23 @@ describe('RoleToSubscriptionMap', () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should filter our subscription roles from an array of roles', () => {
|
||||||
|
const roles = [
|
||||||
|
{
|
||||||
|
name: RoleName.CoreUser,
|
||||||
|
} as jest.Mocked<Role>,
|
||||||
|
{
|
||||||
|
name: RoleName.FilesBetaUser,
|
||||||
|
} as jest.Mocked<Role>,
|
||||||
|
{
|
||||||
|
name: RoleName.PlusUser,
|
||||||
|
} as jest.Mocked<Role>,
|
||||||
|
]
|
||||||
|
expect(createMap().filterSubscriptionRoles(roles)).toEqual([
|
||||||
|
{
|
||||||
|
name: RoleName.PlusUser,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ export class RoleToSubscriptionMap implements RoleToSubscriptionMapInterface {
|
|||||||
return roles.filter((role) => this.nonSubscriptionRoles.includes(role.name as RoleName))
|
return roles.filter((role) => this.nonSubscriptionRoles.includes(role.name as RoleName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filterSubscriptionRoles(roles: Role[]): Array<Role> {
|
||||||
|
return roles.filter((role) => !this.nonSubscriptionRoles.includes(role.name as RoleName))
|
||||||
|
}
|
||||||
|
|
||||||
getSubscriptionNameForRoleName(roleName: RoleName): SubscriptionName | undefined {
|
getSubscriptionNameForRoleName(roleName: RoleName): SubscriptionName | undefined {
|
||||||
return this.roleNameToSubscriptionNameMap.get(roleName)
|
return this.roleNameToSubscriptionNameMap.get(roleName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Role } from './Role'
|
|||||||
|
|
||||||
export interface RoleToSubscriptionMapInterface {
|
export interface RoleToSubscriptionMapInterface {
|
||||||
filterNonSubscriptionRoles(roles: Role[]): Array<Role>
|
filterNonSubscriptionRoles(roles: Role[]): Array<Role>
|
||||||
|
filterSubscriptionRoles(roles: Role[]): Array<Role>
|
||||||
getSubscriptionNameForRoleName(roleName: RoleName): SubscriptionName | undefined
|
getSubscriptionNameForRoleName(roleName: RoleName): SubscriptionName | undefined
|
||||||
getRoleNameForSubscriptionName(subscriptionName: SubscriptionName): RoleName | undefined
|
getRoleNameForSubscriptionName(subscriptionName: SubscriptionName): RoleName | undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Uuid } from '@standardnotes/domain-core'
|
import { SubscriptionPlanName, Uuid } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
import { SessionTrace } from './SessionTrace'
|
import { SessionTrace } from './SessionTrace'
|
||||||
|
|
||||||
export interface SessionTraceRepositoryInterface {
|
export interface SessionTraceRepositoryInterface {
|
||||||
save(sessionTrace: SessionTrace): Promise<void>
|
save(sessionTrace: SessionTrace): Promise<void>
|
||||||
|
removeExpiredBefore(date: Date): Promise<void>
|
||||||
findOneByUserUuidAndDate(userUuid: Uuid, date: Date): Promise<SessionTrace | null>
|
findOneByUserUuidAndDate(userUuid: Uuid, date: Date): Promise<SessionTrace | null>
|
||||||
|
countByDate(date: Date): Promise<number>
|
||||||
|
countByDateAndSubscriptionPlanName(date: Date, subscriptionPlanName: SubscriptionPlanName): Promise<number>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { SessionTraceRepositoryInterface } from '../../Session/SessionTraceRepositoryInterface'
|
||||||
|
import { CleanupSessionTraces } from './CleanupSessionTraces'
|
||||||
|
|
||||||
|
describe('CleanupSessionTraces', () => {
|
||||||
|
let sessionTracesRepository: SessionTraceRepositoryInterface
|
||||||
|
|
||||||
|
const createUseCase = () => new CleanupSessionTraces(sessionTracesRepository)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sessionTracesRepository = {} as jest.Mocked<SessionTraceRepositoryInterface>
|
||||||
|
sessionTracesRepository.removeExpiredBefore = jest.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove stale session traces', async () => {
|
||||||
|
await createUseCase().execute({ date: new Date() })
|
||||||
|
|
||||||
|
expect(sessionTracesRepository.removeExpiredBefore).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { SessionTraceRepositoryInterface } from '../../Session/SessionTraceRepositoryInterface'
|
||||||
|
import { CleanupSessionTracesDTO } from './CleanupSessionTracesDTO'
|
||||||
|
|
||||||
|
export class CleanupSessionTraces implements UseCaseInterface<string> {
|
||||||
|
constructor(private sessionTracesRepository: SessionTraceRepositoryInterface) {}
|
||||||
|
|
||||||
|
async execute(dto: CleanupSessionTracesDTO): Promise<Result<string>> {
|
||||||
|
await this.sessionTracesRepository.removeExpiredBefore(dto.date)
|
||||||
|
|
||||||
|
return Result.ok('Session traces removed')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface CleanupSessionTracesDTO {
|
||||||
|
date: Date
|
||||||
|
}
|
||||||
+2
-2
@@ -67,7 +67,7 @@ describe('CreateCrossServiceToken', () => {
|
|||||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||||
|
|
||||||
roleToSubscriptionMap = {} as jest.Mocked<RoleToSubscriptionMapInterface>
|
roleToSubscriptionMap = {} as jest.Mocked<RoleToSubscriptionMapInterface>
|
||||||
roleToSubscriptionMap.filterNonSubscriptionRoles = jest.fn().mockReturnValue([RoleName.NAMES.PlusUser])
|
roleToSubscriptionMap.filterSubscriptionRoles = jest.fn().mockReturnValue([RoleName.NAMES.PlusUser])
|
||||||
roleToSubscriptionMap.getSubscriptionNameForRoleName = jest
|
roleToSubscriptionMap.getSubscriptionNameForRoleName = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue(SubscriptionPlanName.NAMES.PlusPlan)
|
.mockReturnValue(SubscriptionPlanName.NAMES.PlusPlan)
|
||||||
@@ -170,7 +170,7 @@ describe('CreateCrossServiceToken', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should trace session without a subscription role', async () => {
|
it('should trace session without a subscription role', async () => {
|
||||||
roleToSubscriptionMap.filterNonSubscriptionRoles = jest.fn().mockReturnValue([])
|
roleToSubscriptionMap.filterSubscriptionRoles = jest.fn().mockReturnValue([])
|
||||||
|
|
||||||
await createUseCase().execute({
|
await createUseCase().execute({
|
||||||
user,
|
user,
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export class CreateCrossServiceToken implements UseCaseInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getSubscriptionNameFromRoles(roles: Array<Role>): string | null {
|
private getSubscriptionNameFromRoles(roles: Array<Role>): string | null {
|
||||||
const nonSubscriptionRoles = this.roleToSubscriptionMap.filterNonSubscriptionRoles(roles)
|
const nonSubscriptionRoles = this.roleToSubscriptionMap.filterSubscriptionRoles(roles)
|
||||||
if (nonSubscriptionRoles.length === 0) {
|
if (nonSubscriptionRoles.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { DomainEventPublisherInterface, StatisticPersistenceRequestedEvent } from '@standardnotes/domain-events'
|
||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
|
||||||
|
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||||
|
import { SessionTraceRepositoryInterface } from '../../Session/SessionTraceRepositoryInterface'
|
||||||
|
|
||||||
|
import { PersistStatistics } from './PersistStatistics'
|
||||||
|
|
||||||
|
describe('PersistStatistics', () => {
|
||||||
|
let sessionTracesRepository: SessionTraceRepositoryInterface
|
||||||
|
let domainEventPublisher: DomainEventPublisherInterface
|
||||||
|
let domainEventFactory: DomainEventFactoryInterface
|
||||||
|
let timer: TimerInterface
|
||||||
|
|
||||||
|
const createUseCase = () =>
|
||||||
|
new PersistStatistics(sessionTracesRepository, domainEventPublisher, domainEventFactory, timer)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sessionTracesRepository = {} as jest.Mocked<SessionTraceRepositoryInterface>
|
||||||
|
sessionTracesRepository.countByDate = jest.fn().mockReturnValue(1)
|
||||||
|
sessionTracesRepository.countByDateAndSubscriptionPlanName = jest.fn().mockReturnValue(2)
|
||||||
|
|
||||||
|
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||||
|
domainEventFactory.createStatisticPersistenceRequestedEvent = jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({} as jest.Mocked<StatisticPersistenceRequestedEvent>)
|
||||||
|
|
||||||
|
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||||
|
domainEventPublisher.publish = jest.fn()
|
||||||
|
|
||||||
|
timer = {} as jest.Mocked<TimerInterface>
|
||||||
|
timer.convertDateToMicroseconds = jest.fn().mockReturnValue(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should request statistic persistence', async () => {
|
||||||
|
await createUseCase().execute({ sessionsInADay: new Date() })
|
||||||
|
|
||||||
|
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(4)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Result, SubscriptionPlanName, UseCaseInterface } from '@standardnotes/domain-core'
|
||||||
|
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
|
||||||
|
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||||
|
import { SessionTraceRepositoryInterface } from '../../Session/SessionTraceRepositoryInterface'
|
||||||
|
import { PersistStatisticsDTO } from './PersistStatisticsDTO'
|
||||||
|
|
||||||
|
export class PersistStatistics implements UseCaseInterface<string> {
|
||||||
|
constructor(
|
||||||
|
private sessionTracesRepository: SessionTraceRepositoryInterface,
|
||||||
|
private domainEventPublisher: DomainEventPublisherInterface,
|
||||||
|
private domainEventFactory: DomainEventFactoryInterface,
|
||||||
|
private timer: TimerInterface,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(dto: PersistStatisticsDTO): Promise<Result<string>> {
|
||||||
|
const countSessionsInADay = await this.sessionTracesRepository.countByDate(dto.sessionsInADay)
|
||||||
|
await this.domainEventPublisher.publish(
|
||||||
|
this.domainEventFactory.createStatisticPersistenceRequestedEvent({
|
||||||
|
statisticMeasureName: 'active-users',
|
||||||
|
value: countSessionsInADay,
|
||||||
|
date: this.timer.convertDateToMicroseconds(dto.sessionsInADay),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const proSubscriptionPlanName = SubscriptionPlanName.create(SubscriptionPlanName.NAMES.ProPlan).getValue()
|
||||||
|
const countProSessionsInADay = await this.sessionTracesRepository.countByDateAndSubscriptionPlanName(
|
||||||
|
dto.sessionsInADay,
|
||||||
|
proSubscriptionPlanName,
|
||||||
|
)
|
||||||
|
await this.domainEventPublisher.publish(
|
||||||
|
this.domainEventFactory.createStatisticPersistenceRequestedEvent({
|
||||||
|
statisticMeasureName: 'active-pro-users',
|
||||||
|
value: countProSessionsInADay,
|
||||||
|
date: this.timer.convertDateToMicroseconds(dto.sessionsInADay),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const plusSubscriptionPlanName = SubscriptionPlanName.create(SubscriptionPlanName.NAMES.PlusPlan).getValue()
|
||||||
|
const countPlusSessionsInADay = await this.sessionTracesRepository.countByDateAndSubscriptionPlanName(
|
||||||
|
dto.sessionsInADay,
|
||||||
|
plusSubscriptionPlanName,
|
||||||
|
)
|
||||||
|
await this.domainEventPublisher.publish(
|
||||||
|
this.domainEventFactory.createStatisticPersistenceRequestedEvent({
|
||||||
|
statisticMeasureName: 'active-plus-users',
|
||||||
|
value: countPlusSessionsInADay,
|
||||||
|
date: this.timer.convertDateToMicroseconds(dto.sessionsInADay),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const countFreeSessionsInADay = countSessionsInADay - countProSessionsInADay - countPlusSessionsInADay
|
||||||
|
await this.domainEventPublisher.publish(
|
||||||
|
this.domainEventFactory.createStatisticPersistenceRequestedEvent({
|
||||||
|
statisticMeasureName: 'active-free-users',
|
||||||
|
value: countFreeSessionsInADay,
|
||||||
|
date: this.timer.convertDateToMicroseconds(dto.sessionsInADay),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return Result.ok('Statistics persisted.')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface PersistStatisticsDTO {
|
||||||
|
sessionsInADay: Date
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
|
import { MapperInterface, SubscriptionPlanName, Uuid } from '@standardnotes/domain-core'
|
||||||
import { Repository } from 'typeorm'
|
import { Repository } from 'typeorm'
|
||||||
import { SessionTrace } from '../../Domain/Session/SessionTrace'
|
import { SessionTrace } from '../../Domain/Session/SessionTrace'
|
||||||
import { SessionTraceRepositoryInterface } from '../../Domain/Session/SessionTraceRepositoryInterface'
|
import { SessionTraceRepositoryInterface } from '../../Domain/Session/SessionTraceRepositoryInterface'
|
||||||
@@ -10,6 +10,35 @@ export class MySQLSessionTraceRepository implements SessionTraceRepositoryInterf
|
|||||||
private mapper: MapperInterface<SessionTrace, TypeORMSessionTrace>,
|
private mapper: MapperInterface<SessionTrace, TypeORMSessionTrace>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async countByDateAndSubscriptionPlanName(date: Date, subscriptionPlanName: SubscriptionPlanName): Promise<number> {
|
||||||
|
return this.ormRepository
|
||||||
|
.createQueryBuilder('trace')
|
||||||
|
.where('trace.creation_date = :creationDate', {
|
||||||
|
creationDate: new Date(date.getFullYear(), date.getMonth(), date.getDate()),
|
||||||
|
})
|
||||||
|
.andWhere('trace.subscription_plan_name = :subscriptionPlanName', {
|
||||||
|
subscriptionPlanName: subscriptionPlanName.value,
|
||||||
|
})
|
||||||
|
.getCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
async countByDate(date: Date): Promise<number> {
|
||||||
|
return this.ormRepository
|
||||||
|
.createQueryBuilder('trace')
|
||||||
|
.where('trace.creation_date = :creationDate', {
|
||||||
|
creationDate: new Date(date.getFullYear(), date.getMonth(), date.getDate()),
|
||||||
|
})
|
||||||
|
.getCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeExpiredBefore(date: Date): Promise<void> {
|
||||||
|
await this.ormRepository
|
||||||
|
.createQueryBuilder()
|
||||||
|
.delete()
|
||||||
|
.where('expires_at < :date', { date: date.toISOString() })
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
|
||||||
async findOneByUserUuidAndDate(userUuid: Uuid, date: Date): Promise<SessionTrace | null> {
|
async findOneByUserUuidAndDate(userUuid: Uuid, date: Date): Promise<SessionTrace | null> {
|
||||||
const typeOrm = await this.ormRepository
|
const typeOrm = await this.ormRepository
|
||||||
.createQueryBuilder('trace')
|
.createQueryBuilder('trace')
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.9.59](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.58...@standardnotes/domain-events-infra@1.9.59) (2022-12-20)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||||
|
|
||||||
|
## [1.9.58](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.57...@standardnotes/domain-events-infra@1.9.58) (2022-12-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||||
|
|
||||||
## [1.9.57](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.56...@standardnotes/domain-events-infra@1.9.57) (2022-12-19)
|
## [1.9.57](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.56...@standardnotes/domain-events-infra@1.9.57) (2022-12-19)
|
||||||
|
|
||||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/domain-events-infra",
|
"name": "@standardnotes/domain-events-infra",
|
||||||
"version": "1.9.57",
|
"version": "1.9.59",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,18 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.105.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.105.0...@standardnotes/domain-events@2.105.1) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **auth:** replace date object with number timestamp ([5b4bb6e](https://github.com/standardnotes/server/commit/5b4bb6e7a78a1b0f4e663bb990619f65f6a5c757))
|
||||||
|
|
||||||
|
# [2.105.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.104.2...@standardnotes/domain-events@2.105.0) (2022-12-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add persisting statistics on demand ([0f84575](https://github.com/standardnotes/server/commit/0f8457534c1829c58f3c036749d262307ddeb779))
|
||||||
|
|
||||||
## [2.104.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.104.1...@standardnotes/domain-events@2.104.2) (2022-12-19)
|
## [2.104.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.104.1...@standardnotes/domain-events@2.104.2) (2022-12-19)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/domain-events",
|
"name": "@standardnotes/domain-events",
|
||||||
"version": "2.104.2",
|
"version": "2.105.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { DomainEventInterface } from './DomainEventInterface'
|
||||||
|
|
||||||
|
import { StatisticPersistenceRequestedEventPayload } from './StatisticPersistenceRequestedEventPayload'
|
||||||
|
|
||||||
|
export interface StatisticPersistenceRequestedEvent extends DomainEventInterface {
|
||||||
|
type: 'STATISTIC_PERSISTENCE_REQUESTED'
|
||||||
|
payload: StatisticPersistenceRequestedEventPayload
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface StatisticPersistenceRequestedEventPayload {
|
||||||
|
statisticMeasureName: string
|
||||||
|
value: number
|
||||||
|
date: number
|
||||||
|
}
|
||||||
@@ -58,6 +58,8 @@ export * from './Event/SharedSubscriptionInvitationCanceledEvent'
|
|||||||
export * from './Event/SharedSubscriptionInvitationCanceledEventPayload'
|
export * from './Event/SharedSubscriptionInvitationCanceledEventPayload'
|
||||||
export * from './Event/SharedSubscriptionInvitationCreatedEvent'
|
export * from './Event/SharedSubscriptionInvitationCreatedEvent'
|
||||||
export * from './Event/SharedSubscriptionInvitationCreatedEventPayload'
|
export * from './Event/SharedSubscriptionInvitationCreatedEventPayload'
|
||||||
|
export * from './Event/StatisticPersistenceRequestedEvent'
|
||||||
|
export * from './Event/StatisticPersistenceRequestedEventPayload'
|
||||||
export * from './Event/SubscriptionCancelledEvent'
|
export * from './Event/SubscriptionCancelledEvent'
|
||||||
export * from './Event/SubscriptionCancelledEventPayload'
|
export * from './Event/SubscriptionCancelledEventPayload'
|
||||||
export * from './Event/SubscriptionPurchasedEvent'
|
export * from './Event/SubscriptionPurchasedEvent'
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.6.56](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.55...@standardnotes/event-store@1.6.56) (2022-12-20)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/event-store
|
||||||
|
|
||||||
|
## [1.6.55](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.54...@standardnotes/event-store@1.6.55) (2022-12-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/event-store
|
||||||
|
|
||||||
## [1.6.54](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.53...@standardnotes/event-store@1.6.54) (2022-12-19)
|
## [1.6.54](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.53...@standardnotes/event-store@1.6.54) (2022-12-19)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/event-store",
|
"name": "@standardnotes/event-store",
|
||||||
"version": "1.6.54",
|
"version": "1.6.56",
|
||||||
"description": "Event Store Service",
|
"description": "Event Store Service",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.9.2](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.9.1...@standardnotes/files-server@1.9.2) (2022-12-20)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/files-server
|
||||||
|
|
||||||
|
## [1.9.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.9.0...@standardnotes/files-server@1.9.1) (2022-12-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/files-server
|
||||||
|
|
||||||
# [1.9.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.52...@standardnotes/files-server@1.9.0) (2022-12-19)
|
# [1.9.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.52...@standardnotes/files-server@1.9.0) (2022-12-19)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/files-server",
|
"name": "@standardnotes/files-server",
|
||||||
"version": "1.9.0",
|
"version": "1.9.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.10.2](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.1...@standardnotes/revisions-server@1.10.2) (2022-12-20)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||||
|
|
||||||
|
## [1.10.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.0...@standardnotes/revisions-server@1.10.1) (2022-12-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||||
|
|
||||||
# [1.10.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.28...@standardnotes/revisions-server@1.10.0) (2022-12-19)
|
# [1.10.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.28...@standardnotes/revisions-server@1.10.0) (2022-12-19)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/revisions-server",
|
"name": "@standardnotes/revisions-server",
|
||||||
"version": "1.10.0",
|
"version": "1.10.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.16.2](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.16.1...@standardnotes/scheduler-server@1.16.2) (2022-12-20)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||||
|
|
||||||
|
## [1.16.1](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.16.0...@standardnotes/scheduler-server@1.16.1) (2022-12-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||||
|
|
||||||
# [1.16.0](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.15.8...@standardnotes/scheduler-server@1.16.0) (2022-12-19)
|
# [1.16.0](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.15.8...@standardnotes/scheduler-server@1.16.0) (2022-12-19)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/scheduler-server",
|
"name": "@standardnotes/scheduler-server",
|
||||||
"version": "1.16.0",
|
"version": "1.16.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.28.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.28.1...@standardnotes/syncing-server@1.28.2) (2022-12-20)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||||
|
|
||||||
|
## [1.28.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.28.0...@standardnotes/syncing-server@1.28.1) (2022-12-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||||
|
|
||||||
# [1.28.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.27.0...@standardnotes/syncing-server@1.28.0) (2022-12-19)
|
# [1.28.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.27.0...@standardnotes/syncing-server@1.28.0) (2022-12-19)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/syncing-server",
|
"name": "@standardnotes/syncing-server",
|
||||||
"version": "1.28.0",
|
"version": "1.28.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.5.2](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.5.1...@standardnotes/websockets-server@1.5.2) (2022-12-20)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||||
|
|
||||||
|
## [1.5.1](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.5.0...@standardnotes/websockets-server@1.5.1) (2022-12-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||||
|
|
||||||
# [1.5.0](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.53...@standardnotes/websockets-server@1.5.0) (2022-12-19)
|
# [1.5.0](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.53...@standardnotes/websockets-server@1.5.0) (2022-12-19)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/websockets-server",
|
"name": "@standardnotes/websockets-server",
|
||||||
"version": "1.5.0",
|
"version": "1.5.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.19.2](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.19.1...@standardnotes/workspace-server@1.19.2) (2022-12-20)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||||
|
|
||||||
|
## [1.19.1](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.19.0...@standardnotes/workspace-server@1.19.1) (2022-12-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||||
|
|
||||||
# [1.19.0](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.18.6...@standardnotes/workspace-server@1.19.0) (2022-12-19)
|
# [1.19.0](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.18.6...@standardnotes/workspace-server@1.19.0) (2022-12-19)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/workspace-server",
|
"name": "@standardnotes/workspace-server",
|
||||||
"version": "1.19.0",
|
"version": "1.19.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user