Compare commits

...

30 Commits

Author SHA1 Message Date
standardci
3ad95afa84 chore(release): publish new version
- @standardnotes/analytics@2.9.5
 - @standardnotes/api-gateway@1.37.11
 - @standardnotes/auth-server@1.59.6
 - @standardnotes/domain-events-infra@1.9.22
 - @standardnotes/domain-events@2.86.0
 - @standardnotes/event-store@1.6.17
 - @standardnotes/files-server@1.8.17
 - @standardnotes/scheduler-server@1.13.18
 - @standardnotes/syncing-server@1.12.0
 - @standardnotes/websockets-server@1.4.19
 - @standardnotes/workspace-server@1.17.17
2022-11-11 12:45:17 +00:00
Karol Sójko
1a13861647 feat(syncing-server): add item content size recalculation 2022-11-11 13:43:22 +01:00
standardci
6d84c819c0 chore(release): publish new version
- @standardnotes/analytics@2.9.4
 - @standardnotes/api-gateway@1.37.10
 - @standardnotes/auth-server@1.59.5
 - @standardnotes/domain-events-infra@1.9.21
 - @standardnotes/domain-events@2.85.0
 - @standardnotes/event-store@1.6.16
 - @standardnotes/files-server@1.8.16
 - @standardnotes/scheduler-server@1.13.17
 - @standardnotes/syncing-server@1.11.10
 - @standardnotes/websockets-server@1.4.18
 - @standardnotes/workspace-server@1.17.16
2022-11-11 12:11:40 +00:00
Karol Sójko
36ec39d2fb feat(domain-events): add user content size recalculation requested event 2022-11-11 13:09:33 +01:00
standardci
eaafc12c8a chore(release): publish new version
- @standardnotes/analytics@2.9.3
 - @standardnotes/api-gateway@1.37.9
 - @standardnotes/auth-server@1.59.4
 - @standardnotes/common@1.44.1
 - @standardnotes/domain-events-infra@1.9.20
 - @standardnotes/domain-events@2.84.1
 - @standardnotes/event-store@1.6.15
 - @standardnotes/files-server@1.8.15
 - @standardnotes/predicates@1.5.4
 - @standardnotes/scheduler-server@1.13.16
 - @standardnotes/security@1.6.1
 - @standardnotes/syncing-server@1.11.9
 - @standardnotes/websockets-server@1.4.17
 - @standardnotes/workspace-server@1.17.15
2022-11-10 18:20:16 +00:00
Karol Sójko
a03c5bceea fix(analytics): add five year plans mrr calculation 2022-11-10 19:18:25 +01:00
standardci
53c51fd204 chore(release): publish new version
- @standardnotes/analytics@2.9.2
2022-11-10 15:21:59 +00:00
Karol Sójko
9b593f2a6b fix(analytics): add missing period for stats report 2022-11-10 16:19:45 +01:00
standardci
363609cb1b chore(release): publish new version
- @standardnotes/api-gateway@1.37.8
 - @standardnotes/auth-server@1.59.3
 - @standardnotes/syncing-server@1.11.8
 - @standardnotes/websockets-server@1.4.16
2022-11-10 15:19:21 +00:00
Karol Sójko
68e6d30093 chore(deps): fix axios imports 2022-11-10 16:17:11 +01:00
standardci
c53a40ef8d chore(release): publish new version
- @standardnotes/api-gateway@1.37.7
 - @standardnotes/auth-server@1.59.2
 - @standardnotes/syncing-server@1.11.7
 - @standardnotes/websockets-server@1.4.15
2022-11-10 14:42:52 +00:00
Karol Sójko
3c2ac05c60 fix(api-gateway): setting headers 2022-11-10 15:39:57 +01:00
Karol Sójko
bffab433f6 chore(deps): upgrade ua-parser-js 2022-11-10 15:37:31 +01:00
Karol Sójko
200b6ce01f chore(deps): upgrade axios 2022-11-10 15:35:39 +01:00
standardci
0d29dc1012 chore(release): publish new version
- @standardnotes/analytics@2.9.1
2022-11-10 14:24:45 +00:00
Karol Sójko
b92c4ae650 fix(analytics): generate mrr stats for last 30 days including Today 2022-11-10 15:22:52 +01:00
standardci
e15d1e52bd chore(release): publish new version
- @standardnotes/analytics@2.9.0
2022-11-10 14:19:41 +00:00
Karol Sójko
ce3e259bde feat(analytics): add mrr for annual, monthly, pro and plus subscription plans 2022-11-10 15:17:35 +01:00
standardci
87361f90b1 chore(release): publish new version
- @standardnotes/analytics@2.8.3
2022-11-10 11:27:40 +00:00
Karol Sójko
81be06598c fix(analytics): add subscription id to error logs 2022-11-10 12:25:46 +01:00
standardci
9492da6789 chore(release): publish new version
- @standardnotes/analytics@2.8.2
2022-11-10 10:54:18 +00:00
Karol Sójko
fce47a0a37 fix(analytics): add monthly mrr to the report 2022-11-10 11:52:24 +01:00
standardci
92ba682198 chore(release): publish new version
- @standardnotes/analytics@2.8.1
2022-11-10 10:43:40 +00:00
Karol Sójko
8df0482eb4 fix(analytics): add persisting mrr for this month and this year as well 2022-11-10 11:41:24 +01:00
standardci
37a5cb347d chore(release): publish new version
- @standardnotes/analytics@2.8.0
 - @standardnotes/api-gateway@1.37.6
 - @standardnotes/auth-server@1.59.1
 - @standardnotes/domain-events-infra@1.9.19
 - @standardnotes/domain-events@2.84.0
 - @standardnotes/event-store@1.6.14
 - @standardnotes/files-server@1.8.14
 - @standardnotes/scheduler-server@1.13.15
 - @standardnotes/syncing-server@1.11.6
 - @standardnotes/websockets-server@1.4.14
 - @standardnotes/workspace-server@1.17.14
2022-11-10 10:35:38 +00:00
Karol Sójko
77e50655f6 feat(analytics): add calculating monthly recurring revenue 2022-11-10 11:33:46 +01:00
standardci
eacd2abc00 chore(release): publish new version
- @standardnotes/analytics@2.7.3
2022-11-10 06:55:58 +00:00
Karol Sójko
7393954ff6 fix(analytics): arhcitecture arrangements for use case execution 2022-11-10 07:54:06 +01:00
standardci
68744379a6 chore(release): publish new version
- @standardnotes/analytics@2.7.2
2022-11-09 12:11:11 +00:00
Karol Sójko
90aef905af fix(analytics): mrr column types 2022-11-09 13:09:14 +01:00
92 changed files with 1201 additions and 598 deletions

44
.pnp.cjs generated
View File

@@ -2611,7 +2611,7 @@ const RAW_RUNTIME_STATE =
["@types/prettyjson", "npm:0.0.30"],\
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.40.1"],\
["aws-sdk", "npm:2.1234.0"],\
["axios", "npm:0.27.2"],\
["axios", "npm:1.1.3"],\
["cors", "npm:2.8.5"],\
["dotenv", "npm:16.0.1"],\
["eslint", "npm:8.25.0"],\
@@ -2677,7 +2677,7 @@ const RAW_RUNTIME_STATE =
["@types/uuid", "npm:8.3.4"],\
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.40.1"],\
["aws-sdk", "npm:2.1234.0"],\
["axios", "npm:0.27.2"],\
["axios", "npm:1.1.3"],\
["bcryptjs", "npm:2.4.3"],\
["cors", "npm:2.8.5"],\
["dayjs", "npm:1.11.6"],\
@@ -2699,7 +2699,7 @@ const RAW_RUNTIME_STATE =
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
["typeorm", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:0.3.10"],\
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"],\
["ua-parser-js", "npm:1.0.2"],\
["ua-parser-js", "npm:1.0.32"],\
["uuid", "npm:9.0.0"],\
["winston", "npm:3.8.2"]\
],\
@@ -3139,7 +3139,7 @@ const RAW_RUNTIME_STATE =
["@types/uuid", "npm:8.3.4"],\
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.40.1"],\
["aws-sdk", "npm:2.1234.0"],\
["axios", "npm:0.27.2"],\
["axios", "npm:1.1.3"],\
["cors", "npm:2.8.5"],\
["dotenv", "npm:16.0.1"],\
["eslint", "npm:8.25.0"],\
@@ -3160,7 +3160,7 @@ const RAW_RUNTIME_STATE =
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
["typeorm", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:0.3.10"],\
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"],\
["ua-parser-js", "npm:1.0.2"],\
["ua-parser-js", "npm:1.0.32"],\
["uuid", "npm:9.0.0"],\
["winston", "npm:3.8.2"]\
],\
@@ -3229,7 +3229,7 @@ const RAW_RUNTIME_STATE =
["@types/newrelic", "npm:7.0.3"],\
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.40.1"],\
["aws-sdk", "npm:2.1234.0"],\
["axios", "npm:0.27.2"],\
["axios", "npm:1.1.3"],\
["cors", "npm:2.8.5"],\
["dotenv", "npm:16.0.1"],\
["eslint", "npm:8.25.0"],\
@@ -4749,12 +4749,13 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["axios", [\
["npm:0.27.2", {\
"packageLocation": "./.yarn/cache/axios-npm-0.27.2-dbe3a48aea-4cd898afe9.zip/node_modules/axios/",\
["npm:1.1.3", {\
"packageLocation": "./.yarn/cache/axios-npm-1.1.3-4b63965ac1-2e28acd01c.zip/node_modules/axios/",\
"packageDependencies": [\
["axios", "npm:0.27.2"],\
["follow-redirects", "virtual:dbe3a48aea1dd5649e16abaf23d4ae05582d2149e16141955113766a0f84f681baf358c77ddccfc82eb23e4ccc66c6c912df62a9c01f2a83f1842bf86cc297b1#npm:1.15.2"],\
["form-data", "npm:4.0.0"]\
["axios", "npm:1.1.3"],\
["follow-redirects", "virtual:4b63965ac1b2157b91a1875529bea3b0bbc3068d3676d1bef28bff5cf6689705374a86cc3832f95ba8d934037a93cc0e09c3662c13ca0e747800d7ca279a53c0#npm:1.15.2"],\
["form-data", "npm:4.0.0"],\
["proxy-from-env", "npm:1.1.0"]\
],\
"linkType": "HARD"\
}]\
@@ -7299,10 +7300,10 @@ const RAW_RUNTIME_STATE =
],\
"linkType": "SOFT"\
}],\
["virtual:dbe3a48aea1dd5649e16abaf23d4ae05582d2149e16141955113766a0f84f681baf358c77ddccfc82eb23e4ccc66c6c912df62a9c01f2a83f1842bf86cc297b1#npm:1.15.2", {\
"packageLocation": "./.yarn/__virtual__/follow-redirects-virtual-42073a9d6a/0/cache/follow-redirects-npm-1.15.2-1ec1dd82be-930171f8b8.zip/node_modules/follow-redirects/",\
["virtual:4b63965ac1b2157b91a1875529bea3b0bbc3068d3676d1bef28bff5cf6689705374a86cc3832f95ba8d934037a93cc0e09c3662c13ca0e747800d7ca279a53c0#npm:1.15.2", {\
"packageLocation": "./.yarn/__virtual__/follow-redirects-virtual-b0bb08d690/0/cache/follow-redirects-npm-1.15.2-1ec1dd82be-930171f8b8.zip/node_modules/follow-redirects/",\
"packageDependencies": [\
["follow-redirects", "virtual:dbe3a48aea1dd5649e16abaf23d4ae05582d2149e16141955113766a0f84f681baf358c77ddccfc82eb23e4ccc66c6c912df62a9c01f2a83f1842bf86cc297b1#npm:1.15.2"],\
["follow-redirects", "virtual:4b63965ac1b2157b91a1875529bea3b0bbc3068d3676d1bef28bff5cf6689705374a86cc3832f95ba8d934037a93cc0e09c3662c13ca0e747800d7ca279a53c0#npm:1.15.2"],\
["@types/debug", null],\
["debug", null]\
],\
@@ -11479,6 +11480,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["proxy-from-env", [\
["npm:1.1.0", {\
"packageLocation": "./.yarn/cache/proxy-from-env-npm-1.1.0-c13d07f26b-0bba2ef7c8.zip/node_modules/proxy-from-env/",\
"packageDependencies": [\
["proxy-from-env", "npm:1.1.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["pseudomap", [\
["npm:1.0.2", {\
"packageLocation": "./.yarn/cache/pseudomap-npm-1.0.2-0d0e40fee0-33cfbb99ac.zip/node_modules/pseudomap/",\
@@ -13484,10 +13494,10 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["ua-parser-js", [\
["npm:1.0.2", {\
"packageLocation": "./.yarn/cache/ua-parser-js-npm-1.0.2-c3376785e2-5ee14b105c.zip/node_modules/ua-parser-js/",\
["npm:1.0.32", {\
"packageLocation": "./.yarn/cache/ua-parser-js-npm-1.0.32-95b0b6a78d-9d320c6742.zip/node_modules/ua-parser-js/",\
"packageDependencies": [\
["ua-parser-js", "npm:1.0.2"]\
["ua-parser-js", "npm:1.0.32"]\
],\
"linkType": "HARD"\
}]\

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -3,6 +3,74 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.9.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.9.4...@standardnotes/analytics@2.9.5) (2022-11-11)
**Note:** Version bump only for package @standardnotes/analytics
## [2.9.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.9.3...@standardnotes/analytics@2.9.4) (2022-11-11)
**Note:** Version bump only for package @standardnotes/analytics
## [2.9.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.9.2...@standardnotes/analytics@2.9.3) (2022-11-10)
### Bug Fixes
* **analytics:** add five year plans mrr calculation ([a03c5bc](https://github.com/standardnotes/server/commit/a03c5bceea2a9b166b1d5ad75181021462a86627))
## [2.9.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.9.1...@standardnotes/analytics@2.9.2) (2022-11-10)
### Bug Fixes
* **analytics:** add missing period for stats report ([9b593f2](https://github.com/standardnotes/server/commit/9b593f2a6b105ab8f9c7cef8bdda6892c42e20ef))
## [2.9.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.9.0...@standardnotes/analytics@2.9.1) (2022-11-10)
### Bug Fixes
* **analytics:** generate mrr stats for last 30 days including Today ([b92c4ae](https://github.com/standardnotes/server/commit/b92c4ae650b53db5c0bb2a9cf9afb01caeb8d822))
# [2.9.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.8.3...@standardnotes/analytics@2.9.0) (2022-11-10)
### Features
* **analytics:** add mrr for annual, monthly, pro and plus subscription plans ([ce3e259](https://github.com/standardnotes/server/commit/ce3e259bdedd10796fb4469f0eabd64bc326a115))
## [2.8.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.8.2...@standardnotes/analytics@2.8.3) (2022-11-10)
### Bug Fixes
* **analytics:** add subscription id to error logs ([81be065](https://github.com/standardnotes/server/commit/81be06598c918279f98a8ba6b59ea1b3803c949c))
## [2.8.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.8.1...@standardnotes/analytics@2.8.2) (2022-11-10)
### Bug Fixes
* **analytics:** add monthly mrr to the report ([fce47a0](https://github.com/standardnotes/server/commit/fce47a0a37a67b3edf3ea0b6ccda43c54dbd9870))
## [2.8.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.8.0...@standardnotes/analytics@2.8.1) (2022-11-10)
### Bug Fixes
* **analytics:** add persisting mrr for this month and this year as well ([8df0482](https://github.com/standardnotes/server/commit/8df0482eb4bfd63b9639fd786c9b6952ad7f801d))
# [2.8.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.7.3...@standardnotes/analytics@2.8.0) (2022-11-10)
### Features
* **analytics:** add calculating monthly recurring revenue ([77e5065](https://github.com/standardnotes/server/commit/77e50655f6fa7f9c28e13f8b8bc6de246c0454f0))
## [2.7.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.7.2...@standardnotes/analytics@2.7.3) (2022-11-10)
### Bug Fixes
* **analytics:** arhcitecture arrangements for use case execution ([7393954](https://github.com/standardnotes/server/commit/7393954ff6ece6143f7661104299172548db90ee))
## [2.7.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.7.1...@standardnotes/analytics@2.7.2) (2022-11-09)
### Bug Fixes
* **analytics:** mrr column types ([90aef90](https://github.com/standardnotes/server/commit/90aef905af05b8c1c86c7bd383df6b2b502f7c91))
## [2.7.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.7.0...@standardnotes/analytics@2.7.1) (2022-11-09)
### Bug Fixes

View File

@@ -15,6 +15,7 @@ import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
const requestReport = async (
analyticsStore: AnalyticsStoreInterface,
@@ -22,7 +23,10 @@ const requestReport = async (
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
periodKeyGenerator: PeriodKeyGeneratorInterface,
calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue,
): Promise<void> => {
await calculateMonthlyRecurringRevenue.execute({})
const analyticsOverTime: Array<{
name: string
period: number
@@ -96,6 +100,40 @@ const requestReport = async (
})
}
const statisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
}> = []
const thirtyDaysStatisticsNames = [
StatisticsMeasure.MRR,
StatisticsMeasure.AnnualPlansMRR,
StatisticsMeasure.MonthlyPlansMRR,
StatisticsMeasure.FiveYearPlansMRR,
StatisticsMeasure.PlusPlansMRR,
StatisticsMeasure.ProPlansMRR,
]
for (const statisticName of thirtyDaysStatisticsNames) {
statisticsOverTime.push({
name: statisticName,
period: Period.Last30DaysIncludingToday,
counts: await statisticsStore.calculateTotalCountOverPeriod(statisticName, Period.Last30DaysIncludingToday),
})
}
const monthlyStatisticsNames = [StatisticsMeasure.MRR]
for (const statisticName of monthlyStatisticsNames) {
statisticsOverTime.push({
name: statisticName,
period: Period.ThisYear,
counts: await statisticsStore.calculateTotalCountOverPeriod(statisticName, Period.ThisYear),
})
}
const statisticMeasureNames = [
StatisticsMeasure.Income,
StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome,
@@ -170,13 +208,10 @@ const requestReport = async (
}
const event = domainEventFactory.createDailyAnalyticsReportGeneratedEvent({
applicationStatistics: await statisticsStore.getYesterdayApplicationUsage(),
snjsStatistics: await statisticsStore.getYesterdaySNJSUsage(),
outOfSyncIncidents: await statisticsStore.getYesterdayOutOfSyncIncidents(),
activityStatistics: yesterdayActivityStatistics,
activityStatisticsOverTime: analyticsOverTime,
statisticsOverTime,
statisticMeasures,
retentionStatistics: [],
churn: {
periodKeys: monthlyPeriodKeys,
values: churnRates,
@@ -200,9 +235,19 @@ void container.load().then((container) => {
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
const periodKeyGenerator: PeriodKeyGeneratorInterface = container.get(TYPES.PeriodKeyGenerator)
const calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue = container.get(
TYPES.CalculateMonthlyRecurringRevenue,
)
Promise.resolve(
requestReport(analyticsStore, statisticsStore, domainEventFactory, domainEventPublisher, periodKeyGenerator),
requestReport(
analyticsStore,
statisticsStore,
domainEventFactory,
domainEventPublisher,
periodKeyGenerator,
calculateMonthlyRecurringRevenue,
),
)
.then(() => {
logger.info('Usage report generation complete')

View File

@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class fixMrrFloatingColumns1667995681714 implements MigrationInterface {
name = 'fixMrrFloatingColumns1667995681714'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `revenue_modifications` DROP COLUMN `previous_mrr`')
await queryRunner.query('ALTER TABLE `revenue_modifications` ADD `previous_mrr` float NOT NULL')
await queryRunner.query('ALTER TABLE `revenue_modifications` DROP COLUMN `new_mrr`')
await queryRunner.query('ALTER TABLE `revenue_modifications` ADD `new_mrr` float NOT NULL')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `revenue_modifications` DROP COLUMN `new_mrr`')
await queryRunner.query('ALTER TABLE `revenue_modifications` ADD `new_mrr` int NOT NULL')
await queryRunner.query('ALTER TABLE `revenue_modifications` DROP COLUMN `previous_mrr`')
await queryRunner.query('ALTER TABLE `revenue_modifications` ADD `previous_mrr` int NOT NULL')
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.7.1",
"version": "2.9.5",
"engines": {
"node": ">=14.0.0 <17.0.0"
},

View File

@@ -51,6 +51,7 @@ import { MapInterface } from '../Domain/Map/MapInterface'
import { RevenueModification } from '../Domain/Revenue/RevenueModification'
import { RevenueModificationMap } from '../Domain/Map/RevenueModificationMap'
import { SaveRevenueModification } from '../Domain/UseCase/SaveRevenueModification/SaveRevenueModification'
import { CalculateMonthlyRecurringRevenue } from '../Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -138,6 +139,9 @@ export class ContainerConfigLoader {
// Use Case
container.bind<GetUserAnalyticsId>(TYPES.GetUserAnalyticsId).to(GetUserAnalyticsId)
container.bind<SaveRevenueModification>(TYPES.SaveRevenueModification).to(SaveRevenueModification)
container
.bind<CalculateMonthlyRecurringRevenue>(TYPES.CalculateMonthlyRecurringRevenue)
.to(CalculateMonthlyRecurringRevenue)
// Hanlders
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)

View File

@@ -20,6 +20,7 @@ const TYPES = {
// Use Case
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
SaveRevenueModification: Symbol.for('SaveRevenueModification'),
CalculateMonthlyRecurringRevenue: Symbol.for('CalculateMonthlyRecurringRevenue'),
// Handlers
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),

View File

@@ -1,7 +1,7 @@
/* istanbul ignore file */
export class Result<T> {
constructor(private isSuccess: boolean, private error?: T | string, private value?: T) {
constructor(private isSuccess: boolean, private error?: string, private value?: T) {
Object.freeze(this)
}
@@ -11,13 +11,13 @@ export class Result<T> {
getValue(): T {
if (!this.isSuccess) {
throw new Error('Cannot get value of an unsuccessfull result')
throw new Error(`Cannot get value of an unsuccessfull result: ${this.error}`)
}
return this.value as T
}
getError(): T | string {
getError(): string {
if (this.isSuccess || this.error === undefined) {
throw new Error('Cannot get an error of a successfull result')
}
@@ -29,7 +29,7 @@ export class Result<T> {
return new Result<U>(true, undefined, value)
}
static fail<U>(error: U | string): Result<U> {
static fail<U>(error: string): Result<U> {
return new Result<U>(false, error)
}
}

View File

@@ -22,18 +22,6 @@ describe('DomainEventFactory', () => {
it('should create a DAILY_ANALYTICS_REPORT_GENERATED event', () => {
expect(
createFactory().createDailyAnalyticsReportGeneratedEvent({
snjsStatistics: [
{
version: '1-2-3',
count: 2,
},
],
applicationStatistics: [
{
version: '2-3-4',
count: 45,
},
],
activityStatistics: [
{
name: AnalyticsActivity.Register,
@@ -63,8 +51,18 @@ describe('DomainEventFactory', () => {
totalCount: 123,
},
],
outOfSyncIncidents: 324,
retentionStatistics: [],
statisticsOverTime: [
{
name: StatisticsMeasure.MRR,
period: Period.Last30Days,
counts: [
{
periodKey: '2022-10-9',
totalCount: 3,
},
],
},
],
churn: {
periodKeys: ['2022-10-9'],
values: [
@@ -105,10 +103,16 @@ describe('DomainEventFactory', () => {
totalCount: 123,
},
],
applicationStatistics: [
statisticsOverTime: [
{
count: 45,
version: '2-3-4',
counts: [
{
periodKey: '2022-10-9',
totalCount: 3,
},
],
name: 'mrr',
period: 9,
},
],
churn: {
@@ -120,14 +124,6 @@ describe('DomainEventFactory', () => {
},
],
},
outOfSyncIncidents: 324,
retentionStatistics: [],
snjsStatistics: [
{
count: 2,
version: '1-2-3',
},
],
statisticMeasures: [
{
average: 23,

View File

@@ -9,14 +9,6 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
createDailyAnalyticsReportGeneratedEvent(dto: {
snjsStatistics: Array<{
version: string
count: number
}>
applicationStatistics: Array<{
version: string
count: number
}>
activityStatistics: Array<{
name: string
retention: number
@@ -38,18 +30,13 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
}>
totalCount: number
}>
outOfSyncIncidents: number
retentionStatistics: Array<{
firstActivity: string
secondActivity: string
retention: {
periodKeys: Array<string>
values: Array<{
firstPeriodKey: string
secondPeriodKey: string
value: number
}>
}
statisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
}>
churn: {
periodKeys: Array<string>

View File

@@ -2,14 +2,6 @@ import { DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events
export interface DomainEventFactoryInterface {
createDailyAnalyticsReportGeneratedEvent(dto: {
snjsStatistics: Array<{
version: string
count: number
}>
applicationStatistics: Array<{
version: string
count: number
}>
activityStatistics: Array<{
name: string
retention: number
@@ -31,18 +23,13 @@ export interface DomainEventFactoryInterface {
}>
totalCount: number
}>
outOfSyncIncidents: number
retentionStatistics: Array<{
firstActivity: string
secondActivity: string
retention: {
periodKeys: Array<string>
values: Array<{
firstPeriodKey: string
secondPeriodKey: string
value: number
}>
}
statisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
}>
churn: {
periodKeys: Array<string>

View File

@@ -12,6 +12,7 @@ import { Period } from '../Time/Period'
import { Result } from '../Core/Result'
import { RevenueModification } from '../Revenue/RevenueModification'
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
import { Logger } from 'winston'
describe('SubscriptionCancelledEventHandler', () => {
let event: SubscriptionCancelledEvent
@@ -19,11 +20,21 @@ describe('SubscriptionCancelledEventHandler', () => {
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let saveRevenueModification: SaveRevenueModification
let logger: Logger
const createHandler = () =>
new SubscriptionCancelledEventHandler(getUserAnalyticsId, analyticsStore, statisticsStore, saveRevenueModification)
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 })
@@ -80,4 +91,14 @@ describe('SubscriptionCancelledEventHandler', () => {
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()
})
})

View File

@@ -1,5 +1,6 @@
import { DomainEventHandlerInterface, SubscriptionCancelledEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -20,6 +21,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
) {}
async handle(event: SubscriptionCancelledEvent): Promise<void> {
@@ -32,7 +34,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
await this.trackSubscriptionStatistics(event)
await this.saveRevenueModification.execute({
const result = await this.saveRevenueModification.execute({
billingFrequency: event.payload.billingFrequency,
eventType: SubscriptionEventType.create(event.type).getValue(),
newSubscriber: event.payload.userExistingSubscriptionsCount === 1,
@@ -42,6 +44,12 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
userEmail: Email.create(event.payload.userEmail).getValue(),
userUuid,
})
if (result.isFailed()) {
this.logger.error(
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
}
private async trackSubscriptionStatistics(event: SubscriptionCancelledEvent) {

View File

@@ -10,6 +10,7 @@ import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
import { Result } from '../Core/Result'
import { RevenueModification } from '../Revenue/RevenueModification'
import { Logger } from 'winston'
describe('SubscriptionExpiredEventHandler', () => {
let event: SubscriptionExpiredEvent
@@ -17,11 +18,21 @@ describe('SubscriptionExpiredEventHandler', () => {
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let saveRevenueModification: SaveRevenueModification
let logger: Logger
const createHandler = () =>
new SubscriptionExpiredEventHandler(getUserAnalyticsId, analyticsStore, statisticsStore, saveRevenueModification)
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'
@@ -57,4 +68,12 @@ describe('SubscriptionExpiredEventHandler', () => {
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()
})
})

View File

@@ -1,5 +1,6 @@
import { DomainEventHandlerInterface, SubscriptionExpiredEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -20,6 +21,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
) {}
async handle(event: SubscriptionExpiredEvent): Promise<void> {
@@ -36,7 +38,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
)
await this.saveRevenueModification.execute({
const result = await this.saveRevenueModification.execute({
billingFrequency: event.payload.billingFrequency,
eventType: SubscriptionEventType.create(event.type).getValue(),
newSubscriber: event.payload.userExistingSubscriptionsCount === 1,
@@ -46,5 +48,11 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
userEmail: Email.create(event.payload.userEmail).getValue(),
userUuid,
})
if (result.isFailed()) {
this.logger.error(
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
}
}

View File

@@ -11,6 +11,7 @@ import { Period } from '../Time/Period'
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
import { Result } from '../Core/Result'
import { RevenueModification } from '../Revenue/RevenueModification'
import { Logger } from 'winston'
describe('SubscriptionPurchasedEventHandler', () => {
let event: SubscriptionPurchasedEvent
@@ -19,11 +20,21 @@ describe('SubscriptionPurchasedEventHandler', () => {
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let saveRevenueModification: SaveRevenueModification
let logger: Logger
const createHandler = () =>
new SubscriptionPurchasedEventHandler(getUserAnalyticsId, analyticsStore, statisticsStore, saveRevenueModification)
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()
@@ -80,4 +91,12 @@ describe('SubscriptionPurchasedEventHandler', () => {
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()
})
})

View File

@@ -1,5 +1,6 @@
import { DomainEventHandlerInterface, SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -20,6 +21,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
) {}
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
@@ -60,7 +62,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
)
}
await this.saveRevenueModification.execute({
const result = await this.saveRevenueModification.execute({
billingFrequency: event.payload.billingFrequency,
eventType: SubscriptionEventType.create(event.type).getValue(),
newSubscriber: event.payload.newSubscriber,
@@ -70,5 +72,11 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
userEmail: Email.create(event.payload.userEmail).getValue(),
userUuid,
})
if (result.isFailed()) {
this.logger.error(
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
}
}

View File

@@ -13,6 +13,7 @@ import { Period } from '../Time/Period'
import { Result } from '../Core/Result'
import { RevenueModification } from '../Revenue/RevenueModification'
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
import { Logger } from 'winston'
describe('SubscriptionRefundedEventHandler', () => {
let event: SubscriptionRefundedEvent
@@ -20,11 +21,21 @@ describe('SubscriptionRefundedEventHandler', () => {
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let saveRevenueModification: SaveRevenueModification
let logger: Logger
const createHandler = () =>
new SubscriptionRefundedEventHandler(getUserAnalyticsId, analyticsStore, statisticsStore, saveRevenueModification)
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'
@@ -88,4 +99,12 @@ describe('SubscriptionRefundedEventHandler', () => {
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()
})
})

View File

@@ -1,5 +1,6 @@
import { DomainEventHandlerInterface, SubscriptionRefundedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -20,6 +21,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
) {}
async handle(event: SubscriptionRefundedEvent): Promise<void> {
@@ -32,7 +34,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
await this.markChurnActivity(analyticsId, event)
await this.saveRevenueModification.execute({
const result = await this.saveRevenueModification.execute({
billingFrequency: event.payload.billingFrequency,
eventType: SubscriptionEventType.create(event.type).getValue(),
newSubscriber: event.payload.userExistingSubscriptionsCount === 1,
@@ -42,6 +44,12 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
userEmail: Email.create(event.payload.userEmail).getValue(),
userUuid,
})
if (result.isFailed()) {
this.logger.error(
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
}
private async markChurnActivity(analyticsId: number, event: SubscriptionRefundedEvent): Promise<void> {

View File

@@ -9,17 +9,22 @@ import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
import { RevenueModification } from '../Revenue/RevenueModification'
import { Result } from '../Core/Result'
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)
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'
@@ -52,4 +57,12 @@ describe('SubscriptionRenewedEventHandler', () => {
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()
})
})

View File

@@ -10,6 +10,7 @@ import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/Save
import { Email } from '../Common/Email'
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
import { Logger } from 'winston'
@injectable()
export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterface {
@@ -17,6 +18,7 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
) {}
async handle(event: SubscriptionRenewedEvent): Promise<void> {
@@ -32,7 +34,7 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)
await this.saveRevenueModification.execute({
const result = await this.saveRevenueModification.execute({
billingFrequency: event.payload.billingFrequency,
eventType: SubscriptionEventType.create(event.type).getValue(),
newSubscriber: false,
@@ -42,5 +44,11 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
userEmail: Email.create(event.payload.userEmail).getValue(),
userUuid,
})
if (result.isFailed()) {
this.logger.error(
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
}
}

View File

@@ -14,13 +14,18 @@ import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
@injectable()
export class RevenueModificationMap implements MapInterface<RevenueModification, TypeORMRevenueModification> {
toDomain(persistence: TypeORMRevenueModification): RevenueModification {
const user = User.create(
const userOrError = User.create(
{
email: Email.create(persistence.userEmail).getValue(),
},
new UniqueEntityId(persistence.userUuid),
)
const subscription = Subscription.create(
if (userOrError.isFailed()) {
throw new Error(`Could not create user: ${userOrError.getError()}`)
}
const user = userOrError.getValue()
const subscriptionOrError = Subscription.create(
{
billingFrequency: persistence.billingFrequency,
isFirstSubscriptionForUser: persistence.isNewCustomer,
@@ -29,18 +34,31 @@ export class RevenueModificationMap implements MapInterface<RevenueModification,
},
new UniqueEntityId(persistence.subscriptionId),
)
const previousMonthlyRevenueOrError = MonthlyRevenue.create(persistence.previousMonthlyRevenue)
if (subscriptionOrError.isFailed()) {
throw new Error(`Could not create subscription: ${subscriptionOrError.getError()}`)
}
const subscription = subscriptionOrError.getValue()
return RevenueModification.create(
const previousMonthlyRevenueOrError = MonthlyRevenue.create(persistence.previousMonthlyRevenue)
const newMonthlyRevenueOrError = MonthlyRevenue.create(persistence.newMonthlyRevenue)
const revenuModificationOrError = RevenueModification.create(
{
user,
subscription,
eventType: SubscriptionEventType.create(persistence.eventType).getValue(),
previousMonthlyRevenue: previousMonthlyRevenueOrError.getValue(),
newMonthlyRevenue: newMonthlyRevenueOrError.getValue(),
createdAt: persistence.createdAt,
},
new UniqueEntityId(persistence.uuid),
)
if (revenuModificationOrError.isFailed()) {
throw new Error(`Could not map revenue modification to domain: ${revenuModificationOrError.getError()}`)
}
return revenuModificationOrError.getValue()
}
toPersistence(domain: RevenueModification): TypeORMRevenueModification {
@@ -50,7 +68,7 @@ export class RevenueModificationMap implements MapInterface<RevenueModification,
persistence.billingFrequency = subscription.props.billingFrequency
persistence.eventType = domain.props.eventType.value
persistence.isNewCustomer = subscription.props.isFirstSubscriptionForUser
persistence.newMonthlyRevenue = domain.newMonthlyRevenue.value
persistence.newMonthlyRevenue = domain.props.newMonthlyRevenue.value
persistence.previousMonthlyRevenue = domain.props.previousMonthlyRevenue.value
persistence.subscriptionId = subscription.id.toValue() as number
persistence.subscriptionPlan = subscription.props.planName.value

View File

@@ -13,7 +13,7 @@ export class MonthlyRevenue extends ValueObject<MonthlyRevenueProps> {
static create(revenue: number): Result<MonthlyRevenue> {
if (isNaN(revenue) || revenue < 0) {
return Result.fail<MonthlyRevenue>('Monthly revenue must be a non-negative number')
return Result.fail<MonthlyRevenue>(`Monthly revenue must be a non-negative number. Supplied: ${revenue}`)
} else {
return Result.ok<MonthlyRevenue>(new MonthlyRevenue({ value: revenue }))
}

View File

@@ -16,10 +16,10 @@ describe('RevenueModification', () => {
isFirstSubscriptionForUser: true,
payedAmount: 123,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
})
}).getValue()
user = User.create({
email: Email.create('test@test.te').getValue(),
})
}).getValue()
})
it('should create an aggregate for purchased subscription', () => {
@@ -27,37 +27,12 @@ describe('RevenueModification', () => {
createdAt: 2,
eventType: SubscriptionEventType.create('SUBSCRIPTION_PURCHASED').getValue(),
previousMonthlyRevenue: MonthlyRevenue.create(123).getValue(),
newMonthlyRevenue: MonthlyRevenue.create(45).getValue(),
subscription,
user,
})
}).getValue()
expect(revenueModification.id.toString()).toHaveLength(36)
expect(revenueModification.newMonthlyRevenue.value).toEqual(123 / 12)
})
it('should create an aggregate for subscription expired', () => {
const revenueModification = RevenueModification.create({
createdAt: 1,
eventType: SubscriptionEventType.create('SUBSCRIPTION_EXPIRED').getValue(),
previousMonthlyRevenue: MonthlyRevenue.create(123).getValue(),
subscription,
user,
})
expect(revenueModification.id.toString()).toHaveLength(36)
expect(revenueModification.newMonthlyRevenue.value).toEqual(0)
})
it('should create an aggregate for subscription cancelled', () => {
const revenueModification = RevenueModification.create({
createdAt: 2,
eventType: SubscriptionEventType.create('SUBSCRIPTION_CANCELLED').getValue(),
previousMonthlyRevenue: MonthlyRevenue.create(123).getValue(),
subscription,
user,
})
expect(revenueModification.id.toString()).toHaveLength(36)
expect(revenueModification.newMonthlyRevenue.value).toEqual(123)
expect(revenueModification.props.newMonthlyRevenue.value).toEqual(45)
})
})

View File

@@ -1,6 +1,6 @@
import { Aggregate } from '../Core/Aggregate'
import { Result } from '../Core/Result'
import { UniqueEntityId } from '../Core/UniqueEntityId'
import { MonthlyRevenue } from './MonthlyRevenue'
import { RevenueModificationProps } from './RevenueModificationProps'
export class RevenueModification extends Aggregate<RevenueModificationProps> {
@@ -8,31 +8,7 @@ export class RevenueModification extends Aggregate<RevenueModificationProps> {
super(props, id)
}
static create(props: RevenueModificationProps, id?: UniqueEntityId): RevenueModification {
return new RevenueModification(props, id)
}
get newMonthlyRevenue(): MonthlyRevenue {
const { subscription } = this.props
let revenue = 0
switch (this.props.eventType.value) {
case 'SUBSCRIPTION_PURCHASED':
case 'SUBSCRIPTION_RENEWED':
case 'SUBSCRIPTION_DATA_MIGRATED':
revenue = subscription.props.payedAmount / subscription.props.billingFrequency
break
case 'SUBSCRIPTION_EXPIRED':
case 'SUBSCRIPTION_REFUNDED':
revenue = 0
break
case 'SUBSCRIPTION_CANCELLED':
revenue = this.props.previousMonthlyRevenue.value
break
}
const monthlyRevenueOrError = MonthlyRevenue.create(revenue)
return monthlyRevenueOrError.getValue()
static create(props: RevenueModificationProps, id?: UniqueEntityId): Result<RevenueModification> {
return Result.ok<RevenueModification>(new RevenueModification(props, id))
}
}

View File

@@ -8,5 +8,6 @@ export interface RevenueModificationProps {
subscription: Subscription
eventType: SubscriptionEventType
previousMonthlyRevenue: MonthlyRevenue
newMonthlyRevenue: MonthlyRevenue
createdAt: number
}

View File

@@ -3,5 +3,6 @@ import { RevenueModification } from './RevenueModification'
export interface RevenueModificationRepositoryInterface {
findLastByUserUuid(userUuid: Uuid): Promise<RevenueModification | null>
sumMRRDiff(dto: { planName?: string; billingFrequency?: number }): Promise<number>
save(revenueModification: RevenueModification): Promise<RevenueModification>
}

View File

@@ -15,4 +15,10 @@ export enum StatisticsMeasure {
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',
}

View File

@@ -13,4 +13,8 @@ export interface StatisticsStoreInterface {
getMeasureAverage(measure: StatisticsMeasure, period: Period): Promise<number>
getMeasureTotal(measure: StatisticsMeasure, periodOrPeriodKey: Period | string): Promise<number>
getMeasureIncrementCounts(measure: StatisticsMeasure, period: Period): Promise<number>
calculateTotalCountOverPeriod(
measure: StatisticsMeasure,
period: Period,
): Promise<Array<{ periodKey: string; totalCount: number }>>
}

View File

@@ -8,7 +8,7 @@ describe('Subscription', () => {
isFirstSubscriptionForUser: true,
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
})
}).getValue()
expect(subscription.id.toString()).toHaveLength(36)
})

View File

@@ -1,4 +1,5 @@
import { Entity } from '../Core/Entity'
import { Result } from '../Core/Result'
import { UniqueEntityId } from '../Core/UniqueEntityId'
import { SubscriptionProps } from './SubscriptionProps'
@@ -11,7 +12,7 @@ export class Subscription extends Entity<SubscriptionProps> {
super(props, id)
}
static create(props: SubscriptionProps, id?: UniqueEntityId): Subscription {
return new Subscription(props, id)
static create(props: SubscriptionProps, id?: UniqueEntityId): Result<Subscription> {
return Result.ok<Subscription>(new Subscription(props, id))
}
}

View File

@@ -26,4 +26,5 @@ export enum Period {
OctoberThisYear,
NovemberThisYear,
DecemberThisYear,
Last30DaysIncludingToday,
}

View File

@@ -62,6 +62,41 @@ describe('PeriodKeyGenerator', () => {
])
})
it('should generate period keys for last 30 days including Today', () => {
expect(createGenerator().getDiscretePeriodKeys(Period.Last30DaysIncludingToday)).toEqual([
'2022-4-25',
'2022-4-26',
'2022-4-27',
'2022-4-28',
'2022-4-29',
'2022-4-30',
'2022-5-1',
'2022-5-2',
'2022-5-3',
'2022-5-4',
'2022-5-5',
'2022-5-6',
'2022-5-7',
'2022-5-8',
'2022-5-9',
'2022-5-10',
'2022-5-11',
'2022-5-12',
'2022-5-13',
'2022-5-14',
'2022-5-15',
'2022-5-16',
'2022-5-17',
'2022-5-18',
'2022-5-19',
'2022-5-20',
'2022-5-21',
'2022-5-22',
'2022-5-23',
'2022-5-24',
])
})
it('should generate period keys for this year', () => {
expect(createGenerator().getDiscretePeriodKeys(Period.ThisYear)).toEqual([
'2022-1',

View File

@@ -33,6 +33,12 @@ export class PeriodKeyGenerator implements PeriodKeyGeneratorInterface {
periodKeys.unshift(this.getDailyKey(this.getDateNDaysBefore(i)))
}
return periodKeys
case Period.Last30DaysIncludingToday:
for (let i = 0; i <= 29; i++) {
periodKeys.unshift(this.getDailyKey(this.getDateNDaysBefore(i)))
}
return periodKeys
case Period.Last7Days:
for (let i = 1; i <= 7; i++) {

View File

@@ -0,0 +1,33 @@
import 'reflect-metadata'
import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface'
import { StatisticsMeasure } from '../../Statistics/StatisticsMeasure'
import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface'
import { Period } from '../../Time/Period'
import { CalculateMonthlyRecurringRevenue } from './CalculateMonthlyRecurringRevenue'
describe('CalculateMonthlyRecurringRevenue', () => {
let revenueModificationRepository: RevenueModificationRepositoryInterface
let statisticsStore: StatisticsStoreInterface
const createUseCase = () => new CalculateMonthlyRecurringRevenue(revenueModificationRepository, statisticsStore)
beforeEach(() => {
revenueModificationRepository = {} as jest.Mocked<RevenueModificationRepositoryInterface>
revenueModificationRepository.sumMRRDiff = jest.fn().mockReturnValue(123.45)
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.setMeasure = jest.fn()
})
it('should calculate the MRR diff and persist it as a statistic', async () => {
await createUseCase().execute({})
expect(statisticsStore.setMeasure).toHaveBeenCalledWith(StatisticsMeasure.MRR, 123.45, [
Period.Today,
Period.ThisMonth,
Period.ThisYear,
])
})
})

View File

@@ -0,0 +1,82 @@
import { SubscriptionBillingFrequency, SubscriptionName } from '@standardnotes/common'
import { inject, injectable } from 'inversify'
import TYPES from '../../../Bootstrap/Types'
import { Result } from '../../Core/Result'
import { MonthlyRevenue } from '../../Revenue/MonthlyRevenue'
import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface'
import { StatisticsMeasure } from '../../Statistics/StatisticsMeasure'
import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface'
import { Period } from '../../Time/Period'
import { DomainUseCaseInterface } from '../DomainUseCaseInterface'
import { CalculateMonthlyRecurringRevenueDTO } from './CalculateMonthlyRecurringRevenueDTO'
@injectable()
export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<MonthlyRevenue> {
constructor(
@inject(TYPES.RevenueModificationRepository)
private revenueModificationRepository: RevenueModificationRepositoryInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
) {}
async execute(_dto: CalculateMonthlyRecurringRevenueDTO): Promise<Result<MonthlyRevenue>> {
const mrrDiff = await this.revenueModificationRepository.sumMRRDiff({})
await this.statisticsStore.setMeasure(StatisticsMeasure.MRR, mrrDiff, [
Period.Today,
Period.ThisMonth,
Period.ThisYear,
])
const monthlyPlansMrrDiff = await this.revenueModificationRepository.sumMRRDiff({
billingFrequency: SubscriptionBillingFrequency.Monthly,
})
await this.statisticsStore.setMeasure(StatisticsMeasure.MonthlyPlansMRR, monthlyPlansMrrDiff, [
Period.Today,
Period.ThisMonth,
Period.ThisYear,
])
const annualPlansMrrDiff = await this.revenueModificationRepository.sumMRRDiff({
billingFrequency: SubscriptionBillingFrequency.Annual,
})
await this.statisticsStore.setMeasure(StatisticsMeasure.AnnualPlansMRR, annualPlansMrrDiff, [
Period.Today,
Period.ThisMonth,
Period.ThisYear,
])
const fiveYearPlansMrrDiff = await this.revenueModificationRepository.sumMRRDiff({
billingFrequency: SubscriptionBillingFrequency.FiveYear,
})
await this.statisticsStore.setMeasure(StatisticsMeasure.FiveYearPlansMRR, fiveYearPlansMrrDiff, [
Period.Today,
Period.ThisMonth,
Period.ThisYear,
])
const proPlansMrrDiff = await this.revenueModificationRepository.sumMRRDiff({
planName: SubscriptionName.ProPlan,
})
await this.statisticsStore.setMeasure(StatisticsMeasure.ProPlansMRR, proPlansMrrDiff, [
Period.Today,
Period.ThisMonth,
Period.ThisYear,
])
const plusPlansMrrDiff = await this.revenueModificationRepository.sumMRRDiff({
planName: SubscriptionName.PlusPlan,
})
await this.statisticsStore.setMeasure(StatisticsMeasure.PlusPlansMRR, plusPlansMrrDiff, [
Period.Today,
Period.ThisMonth,
Period.ThisYear,
])
return MonthlyRevenue.create(mrrDiff)
}
}

View File

@@ -0,0 +1,2 @@
/* eslint-disable @typescript-eslint/no-empty-interface */
export interface CalculateMonthlyRecurringRevenueDTO {}

View File

@@ -11,28 +11,35 @@ import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueMod
import { SubscriptionEventType } from '../../Subscription/SubscriptionEventType'
import { SubscriptionPlanName } from '../../Subscription/SubscriptionPlanName'
import { SaveRevenueModification } from './SaveRevenueModification'
import { User } from '../../User/User'
import { Result } from '../../Core/Result'
import { Subscription } from '../../Subscription/Subscription'
describe('SaveRevenueModification', () => {
let revenueModificationRepository: RevenueModificationRepositoryInterface
let previousMonthlyRevenue: RevenueModification
let previousMonthlyRevenueModification: RevenueModification
let timer: TimerInterface
const createUseCase = () => new SaveRevenueModification(revenueModificationRepository, timer)
beforeEach(() => {
previousMonthlyRevenue = {
newMonthlyRevenue: MonthlyRevenue.create(2).getValue(),
const previousMonthlyRevenue = {
value: 2,
} as jest.Mocked<MonthlyRevenue>
previousMonthlyRevenueModification = {
props: {},
} as jest.Mocked<RevenueModification>
previousMonthlyRevenueModification.props.newMonthlyRevenue = previousMonthlyRevenue
revenueModificationRepository = {} as jest.Mocked<RevenueModificationRepositoryInterface>
revenueModificationRepository.findLastByUserUuid = jest.fn().mockReturnValue(previousMonthlyRevenue)
revenueModificationRepository.findLastByUserUuid = jest.fn().mockReturnValue(previousMonthlyRevenueModification)
revenueModificationRepository.save = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
})
it('should persist a revenue modification', async () => {
it('should persist a revenue modification for subscription purchased event', async () => {
const revenueOrError = await createUseCase().execute({
billingFrequency: 1,
eventType: SubscriptionEventType.create('SUBSCRIPTION_PURCHASED').getValue(),
@@ -43,8 +50,166 @@ describe('SaveRevenueModification', () => {
userEmail: Email.create('test@test.te').getValue(),
userUuid: Uuid.create('1-2-3').getValue(),
})
expect(revenueOrError.isFailed()).toBeFalsy()
const revenue = revenueOrError.getValue()
expect(revenue.newMonthlyRevenue.value).toEqual(12.99)
expect(revenue.props.newMonthlyRevenue.value).toEqual(12.99)
})
it('should persist a revenue modification for subscription expired event', async () => {
const revenueOrError = await createUseCase().execute({
billingFrequency: 1,
eventType: SubscriptionEventType.create('SUBSCRIPTION_EXPIRED').getValue(),
newSubscriber: true,
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
userUuid: Uuid.create('1-2-3').getValue(),
})
expect(revenueOrError.isFailed()).toBeFalsy()
const revenue = revenueOrError.getValue()
expect(revenue.props.newMonthlyRevenue.value).toEqual(0)
})
it('should persist a revenue modification for subscription cancelled event', async () => {
const revenueOrError = await createUseCase().execute({
billingFrequency: 1,
eventType: SubscriptionEventType.create('SUBSCRIPTION_CANCELLED').getValue(),
newSubscriber: true,
payedAmount: 2,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
userUuid: Uuid.create('1-2-3').getValue(),
})
expect(revenueOrError.isFailed()).toBeFalsy()
const revenue = revenueOrError.getValue()
expect(revenue.props.newMonthlyRevenue.value).toEqual(2)
})
it('should persist a revenue modification for subscription purchased event if previous revenue modification did not exist', async () => {
revenueModificationRepository.findLastByUserUuid = jest.fn().mockReturnValue(null)
const revenueOrError = await createUseCase().execute({
billingFrequency: 1,
eventType: SubscriptionEventType.create('SUBSCRIPTION_PURCHASED').getValue(),
newSubscriber: true,
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
userUuid: Uuid.create('1-2-3').getValue(),
})
expect(revenueOrError.isFailed()).toBeFalsy()
const revenue = revenueOrError.getValue()
expect(revenue.props.newMonthlyRevenue.value).toEqual(12.99)
})
it('should not persist a revenue modification if failed to create user', async () => {
const mock = jest.spyOn(User, 'create')
mock.mockReturnValue(Result.fail('Oops'))
const revenueOrError = await createUseCase().execute({
billingFrequency: 1,
eventType: SubscriptionEventType.create('SUBSCRIPTION_PURCHASED').getValue(),
newSubscriber: true,
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
userUuid: Uuid.create('1-2-3').getValue(),
})
expect(revenueOrError.isFailed()).toBeTruthy()
mock.mockRestore()
})
it('should not persist a revenue modification if failed to create a subscription', async () => {
const mock = jest.spyOn(Subscription, 'create')
mock.mockReturnValue(Result.fail('Oops'))
const revenueOrError = await createUseCase().execute({
billingFrequency: 1,
eventType: SubscriptionEventType.create('SUBSCRIPTION_PURCHASED').getValue(),
newSubscriber: true,
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
userUuid: Uuid.create('1-2-3').getValue(),
})
expect(revenueOrError.isFailed()).toBeTruthy()
mock.mockRestore()
})
it('should not persist a revenue modification if failed to create a previous monthly revenue', async () => {
const mock = jest.spyOn(MonthlyRevenue, 'create')
mock.mockReturnValue(Result.fail('Oops'))
const revenueOrError = await createUseCase().execute({
billingFrequency: 1,
eventType: SubscriptionEventType.create('SUBSCRIPTION_PURCHASED').getValue(),
newSubscriber: true,
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
userUuid: Uuid.create('1-2-3').getValue(),
})
expect(revenueOrError.isFailed()).toBeTruthy()
mock.mockRestore()
})
it('should not persist a revenue modification if failed to create a next monthly revenue', async () => {
const mock = jest.spyOn(MonthlyRevenue, 'create')
mock.mockReturnValueOnce(Result.ok()).mockReturnValueOnce(Result.fail('Oops'))
const revenueOrError = await createUseCase().execute({
billingFrequency: 1,
eventType: SubscriptionEventType.create('SUBSCRIPTION_PURCHASED').getValue(),
newSubscriber: true,
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
userUuid: Uuid.create('1-2-3').getValue(),
})
expect(revenueOrError.isFailed()).toBeTruthy()
mock.mockRestore()
})
it('should not persist a revenue modification if failed to create it', async () => {
const mock = jest.spyOn(RevenueModification, 'create')
mock.mockReturnValue(Result.fail('Oops'))
const revenueOrError = await createUseCase().execute({
billingFrequency: 1,
eventType: SubscriptionEventType.create('SUBSCRIPTION_PURCHASED').getValue(),
newSubscriber: true,
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
userUuid: Uuid.create('1-2-3').getValue(),
})
expect(revenueOrError.isFailed()).toBeTruthy()
mock.mockRestore()
})
})

View File

@@ -1,4 +1,5 @@
import { inject, injectable } from 'inversify'
import TYPES from '../../../Bootstrap/Types'
import { UniqueEntityId } from '../../Core/UniqueEntityId'
import { MonthlyRevenue } from '../../Revenue/MonthlyRevenue'
@@ -10,6 +11,7 @@ import { Result } from '../../Core/Result'
import { DomainUseCaseInterface } from '../DomainUseCaseInterface'
import { SaveRevenueModificationDTO } from './SaveRevenueModificationDTO'
import { TimerInterface } from '@standardnotes/time'
import { SubscriptionEventType } from '../../Subscription/SubscriptionEventType'
@injectable()
export class SaveRevenueModification implements DomainUseCaseInterface<RevenueModification> {
@@ -20,13 +22,18 @@ export class SaveRevenueModification implements DomainUseCaseInterface<RevenueMo
) {}
async execute(dto: SaveRevenueModificationDTO): Promise<Result<RevenueModification>> {
const user = User.create(
const userOrError = User.create(
{
email: dto.userEmail,
},
new UniqueEntityId(dto.userUuid.value),
)
const subscription = Subscription.create(
if (userOrError.isFailed()) {
return Result.fail<RevenueModification>(userOrError.getError())
}
const user = userOrError.getValue()
const subscriptionOrError = Subscription.create(
{
isFirstSubscriptionForUser: dto.newSubscriber,
payedAmount: dto.payedAmount,
@@ -35,23 +42,77 @@ export class SaveRevenueModification implements DomainUseCaseInterface<RevenueMo
},
new UniqueEntityId(dto.subscriptionId),
)
if (subscriptionOrError.isFailed()) {
return Result.fail<RevenueModification>(subscriptionOrError.getError())
}
const subscription = subscriptionOrError.getValue()
const previousMonthlyRevenueOrError = MonthlyRevenue.create(0)
if (previousMonthlyRevenueOrError.isFailed()) {
return Result.fail<RevenueModification>(previousMonthlyRevenueOrError.getError())
}
let previousMonthlyRevenue = previousMonthlyRevenueOrError.getValue()
let previousMonthlyRevenue = MonthlyRevenue.create(0).getValue()
const previousRevenueModification = await this.revenueModificationRepository.findLastByUserUuid(dto.userUuid)
if (previousRevenueModification !== null) {
previousMonthlyRevenue = previousRevenueModification.newMonthlyRevenue
previousMonthlyRevenue = previousRevenueModification.props.newMonthlyRevenue
}
const newMonthlyRevenueOrError = this.calculateNewMonthlyRevenue(
subscription,
previousMonthlyRevenue,
dto.eventType,
)
if (newMonthlyRevenueOrError.isFailed()) {
return Result.fail<RevenueModification>(newMonthlyRevenueOrError.getError())
}
const newMonthlyRevenue = newMonthlyRevenueOrError.getValue()
const revenueModification = RevenueModification.create({
const revenueModificationOrError = RevenueModification.create({
eventType: dto.eventType,
subscription,
user,
previousMonthlyRevenue,
newMonthlyRevenue,
createdAt: this.timer.getTimestampInMicroseconds(),
})
if (revenueModificationOrError.isFailed()) {
return Result.fail<RevenueModification>(revenueModificationOrError.getError())
}
const revenueModification = revenueModificationOrError.getValue()
await this.revenueModificationRepository.save(revenueModification)
return Result.ok<RevenueModification>(revenueModification)
}
private calculateNewMonthlyRevenue(
subscription: Subscription,
previousMonthlyRevenue: MonthlyRevenue,
eventType: SubscriptionEventType,
): Result<MonthlyRevenue> {
let revenue = 0
switch (eventType.value) {
case 'SUBSCRIPTION_PURCHASED':
case 'SUBSCRIPTION_RENEWED':
case 'SUBSCRIPTION_DATA_MIGRATED':
revenue = subscription.props.payedAmount / subscription.props.billingFrequency
break
case 'SUBSCRIPTION_EXPIRED':
case 'SUBSCRIPTION_REFUNDED':
revenue = 0
break
case 'SUBSCRIPTION_CANCELLED':
revenue = previousMonthlyRevenue.value
break
}
const monthlyRevenueOrError = MonthlyRevenue.create(revenue)
if (monthlyRevenueOrError.isFailed()) {
return Result.fail<MonthlyRevenue>(monthlyRevenueOrError.getError())
}
return Result.ok<MonthlyRevenue>(monthlyRevenueOrError.getValue())
}
}

View File

@@ -5,7 +5,7 @@ describe('User', () => {
it('should create an entity', () => {
const user = User.create({
email: Email.create('test@test.te').getValue(),
})
}).getValue()
expect(user.id.toString()).toHaveLength(36)
})

View File

@@ -1,4 +1,5 @@
import { Entity } from '../Core/Entity'
import { Result } from '../Core/Result'
import { UniqueEntityId } from '../Core/UniqueEntityId'
import { UserProps } from './UserProps'
@@ -11,7 +12,7 @@ export class User extends Entity<UserProps> {
super(props, id)
}
public static create(props: UserProps, id?: UniqueEntityId): User {
return new User(props, id)
public static create(props: UserProps, id?: UniqueEntityId): Result<User> {
return Result.ok<User>(new User(props, id))
}
}

View File

@@ -17,6 +17,25 @@ export class MySQLRevenueModificationRepository implements RevenueModificationRe
private revenueModificationMap: MapInterface<RevenueModification, TypeORMRevenueModification>,
) {}
async sumMRRDiff(dto: { planName?: string; billingFrequency?: number }): Promise<number> {
const query = this.ormRepository.createQueryBuilder().select('sum(new_mrr - previous_mrr)', 'mrrDiff')
if (dto.planName !== undefined) {
query.where('subscription_plan = :planName', { planName: dto.planName })
}
if (dto.billingFrequency !== undefined) {
query.where('billing_frequency = :billingFrequency', { billingFrequency: dto.billingFrequency })
}
const result = await query.getRawOne()
if (result === undefined) {
return 0
}
return +(+result.mrrDiff).toFixed(2)
}
async findLastByUserUuid(userUuid: Uuid): Promise<RevenueModification | null> {
const persistence = await this.ormRepository
.createQueryBuilder()

View File

@@ -1,209 +0,0 @@
import * as IORedis from 'ioredis'
import { AnalyticsActivity } from '../../Domain/Analytics/AnalyticsActivity'
import { Period } from '../../Domain/Time/Period'
import { PeriodKeyGeneratorInterface } from '../../Domain/Time/PeriodKeyGeneratorInterface'
import { RedisAnalyticsStore } from './RedisAnalyticsStore'
describe('RedisAnalyticsStore', () => {
let redisClient: IORedis.Redis
let pipeline: IORedis.Pipeline
let periodKeyGenerator: PeriodKeyGeneratorInterface
const createStore = () => new RedisAnalyticsStore(periodKeyGenerator, redisClient)
beforeEach(() => {
pipeline = {} as jest.Mocked<IORedis.Pipeline>
pipeline.incr = jest.fn()
pipeline.setbit = jest.fn()
pipeline.exec = jest.fn()
redisClient = {} as jest.Mocked<IORedis.Redis>
redisClient.pipeline = jest.fn().mockReturnValue(pipeline)
redisClient.incr = jest.fn()
redisClient.setbit = jest.fn()
redisClient.getbit = jest.fn().mockReturnValue(1)
redisClient.bitop = jest.fn()
redisClient.expire = jest.fn()
periodKeyGenerator = {} as jest.Mocked<PeriodKeyGeneratorInterface>
periodKeyGenerator.getPeriodKey = jest.fn().mockReturnValue('period-key')
})
it('should calculate total count over time of activities', async () => {
redisClient.bitcount = jest.fn().mockReturnValue(70)
periodKeyGenerator.getDiscretePeriodKeys = jest.fn().mockReturnValue(['2022-4-24', '2022-4-25', '2022-4-26'])
await createStore().calculateActivityTotalCountOverTime(AnalyticsActivity.Register, Period.Last30Days)
expect(redisClient.bitop).toHaveBeenCalledTimes(1)
expect(redisClient.bitop).toHaveBeenNthCalledWith(
1,
'OR',
'bitmap:action:register:timespan:2022-4-24-2022-4-26',
'bitmap:action:register:timespan:2022-4-24',
'bitmap:action:register:timespan:2022-4-25',
'bitmap:action:register:timespan:2022-4-26',
)
expect(redisClient.bitcount).toHaveBeenCalledWith('bitmap:action:register:timespan:2022-4-24-2022-4-26')
})
it('should not calculate total count over time of activities if period is unsupported', async () => {
let caughtError = null
try {
await createStore().calculateActivityTotalCountOverTime(AnalyticsActivity.Register, Period.LastWeek)
} catch (error) {
caughtError = error
}
expect(caughtError).not.toBeNull()
})
it('should calculate total count changes of activities', async () => {
periodKeyGenerator.getDiscretePeriodKeys = jest.fn().mockReturnValue(['2022-4-24', '2022-4-25', '2022-4-26'])
redisClient.bitcount = jest.fn().mockReturnValueOnce(70).mockReturnValueOnce(71).mockReturnValueOnce(72)
expect(
await createStore().calculateActivityChangesTotalCount(AnalyticsActivity.Register, Period.Last30Days),
).toEqual([
{
periodKey: '2022-4-24',
totalCount: 70,
},
{
periodKey: '2022-4-25',
totalCount: 71,
},
{
periodKey: '2022-4-26',
totalCount: 72,
},
])
expect(redisClient.bitcount).toHaveBeenNthCalledWith(1, 'bitmap:action:register:timespan:2022-4-24')
expect(redisClient.bitcount).toHaveBeenNthCalledWith(2, 'bitmap:action:register:timespan:2022-4-25')
expect(redisClient.bitcount).toHaveBeenNthCalledWith(3, 'bitmap:action:register:timespan:2022-4-26')
})
it('should throw error on calculating total count changes of activities on unsupported period', async () => {
periodKeyGenerator.getDiscretePeriodKeys = jest.fn().mockReturnValue(['2022-4-24', '2022-4-25', '2022-4-26'])
redisClient.bitcount = jest.fn().mockReturnValueOnce(70).mockReturnValueOnce(71).mockReturnValueOnce(72)
let caughtError = null
try {
await createStore().calculateActivityChangesTotalCount(AnalyticsActivity.Register, Period.LastWeek)
} catch (error) {
caughtError = error
}
expect(caughtError).not.toBeNull()
})
it('should calculate total count of activities by period', async () => {
redisClient.bitcount = jest.fn().mockReturnValue(70)
expect(await createStore().calculateActivityTotalCount(AnalyticsActivity.Register, Period.Yesterday)).toEqual(70)
expect(redisClient.bitcount).toHaveBeenCalledWith('bitmap:action:register:timespan:period-key')
})
it('should calculate total count of activities by period key', async () => {
redisClient.bitcount = jest.fn().mockReturnValue(70)
expect(await createStore().calculateActivityTotalCount(AnalyticsActivity.Register, '2022-10-03')).toEqual(70)
expect(redisClient.bitcount).toHaveBeenCalledWith('bitmap:action:register:timespan:2022-10-03')
})
it('should calculate activity retention', async () => {
redisClient.bitcount = jest.fn().mockReturnValueOnce(7).mockReturnValueOnce(10)
expect(
await createStore().calculateActivityRetention(
AnalyticsActivity.Register,
Period.DayBeforeYesterday,
Period.Yesterday,
),
).toEqual(70)
expect(redisClient.bitop).toHaveBeenCalledWith(
'AND',
'bitmap:action:register-register:timespan:period-key',
'bitmap:action:register:timespan:period-key',
'bitmap:action:register:timespan:period-key',
)
})
it('shoud tell if activity was done', async () => {
await createStore().wasActivityDone(AnalyticsActivity.Register, 123, Period.Yesterday)
expect(redisClient.getbit).toHaveBeenCalledWith('bitmap:action:register:timespan:period-key', 123)
})
it('should mark activity as done', async () => {
await createStore().markActivity([AnalyticsActivity.Register], 123, [Period.Today])
expect(pipeline.setbit).toBeCalledTimes(1)
expect(pipeline.setbit).toHaveBeenNthCalledWith(1, 'bitmap:action:register:timespan:period-key', 123, 1)
expect(pipeline.exec).toHaveBeenCalled()
})
it('should mark activities as done', async () => {
await createStore().markActivity([AnalyticsActivity.Register, AnalyticsActivity.SubscriptionPurchased], 123, [
Period.Today,
Period.ThisWeek,
])
expect(pipeline.setbit).toBeCalledTimes(4)
expect(pipeline.setbit).toHaveBeenNthCalledWith(1, 'bitmap:action:register:timespan:period-key', 123, 1)
expect(pipeline.setbit).toHaveBeenNthCalledWith(2, 'bitmap:action:register:timespan:period-key', 123, 1)
expect(pipeline.setbit).toHaveBeenNthCalledWith(
3,
'bitmap:action:subscription-purchased:timespan:period-key',
123,
1,
)
expect(pipeline.setbit).toHaveBeenNthCalledWith(
4,
'bitmap:action:subscription-purchased:timespan:period-key',
123,
1,
)
expect(pipeline.exec).toHaveBeenCalled()
})
it('should unmark activity as done', async () => {
await createStore().unmarkActivity([AnalyticsActivity.Register], 123, [Period.Today])
expect(pipeline.setbit).toBeCalledTimes(1)
expect(pipeline.setbit).toHaveBeenNthCalledWith(1, 'bitmap:action:register:timespan:period-key', 123, 0)
expect(pipeline.exec).toHaveBeenCalled()
})
it('should unmark activities as done', async () => {
await createStore().unmarkActivity([AnalyticsActivity.Register, AnalyticsActivity.SubscriptionPurchased], 123, [
Period.Today,
Period.ThisWeek,
])
expect(pipeline.setbit).toBeCalledTimes(4)
expect(pipeline.setbit).toHaveBeenNthCalledWith(1, 'bitmap:action:register:timespan:period-key', 123, 0)
expect(pipeline.setbit).toHaveBeenNthCalledWith(2, 'bitmap:action:register:timespan:period-key', 123, 0)
expect(pipeline.setbit).toHaveBeenNthCalledWith(
3,
'bitmap:action:subscription-purchased:timespan:period-key',
123,
0,
)
expect(pipeline.setbit).toHaveBeenNthCalledWith(
4,
'bitmap:action:subscription-purchased:timespan:period-key',
123,
0,
)
expect(pipeline.exec).toHaveBeenCalled()
})
})

View File

@@ -1,145 +0,0 @@
import * as IORedis from 'ioredis'
import { StatisticsMeasure } from '../../Domain/Statistics/StatisticsMeasure'
import { Period } from '../../Domain/Time/Period'
import { PeriodKeyGeneratorInterface } from '../../Domain/Time/PeriodKeyGeneratorInterface'
import { RedisStatisticsStore } from './RedisStatisticsStore'
describe('RedisStatisticsStore', () => {
let redisClient: IORedis.Redis
let periodKeyGenerator: PeriodKeyGeneratorInterface
let pipeline: IORedis.Pipeline
const createStore = () => new RedisStatisticsStore(periodKeyGenerator, redisClient)
beforeEach(() => {
pipeline = {} as jest.Mocked<IORedis.Pipeline>
pipeline.incr = jest.fn()
pipeline.incrbyfloat = jest.fn()
pipeline.set = jest.fn()
pipeline.setbit = jest.fn()
pipeline.exec = jest.fn()
redisClient = {} as jest.Mocked<IORedis.Redis>
redisClient.pipeline = jest.fn().mockReturnValue(pipeline)
redisClient.incr = jest.fn()
redisClient.setbit = jest.fn()
redisClient.getbit = jest.fn().mockReturnValue(1)
periodKeyGenerator = {} as jest.Mocked<PeriodKeyGeneratorInterface>
periodKeyGenerator.getPeriodKey = jest.fn().mockReturnValue('period-key')
})
it('should get yesterday out of sync incidents', async () => {
redisClient.get = jest.fn().mockReturnValue(1)
expect(await createStore().getYesterdayOutOfSyncIncidents()).toEqual(1)
})
it('should default to 0 yesterday out of sync incidents', async () => {
redisClient.get = jest.fn().mockReturnValue(null)
expect(await createStore().getYesterdayOutOfSyncIncidents()).toEqual(0)
})
it('should get yesterday application version usage', async () => {
redisClient.keys = jest
.fn()
.mockReturnValue([
'count:action:application-request:1.2.3:timespan:2022-3-10',
'count:action:application-request:2.3.4:timespan:2022-3-10',
])
redisClient.get = jest.fn().mockReturnValueOnce(3).mockReturnValueOnce(4)
expect(await createStore().getYesterdayApplicationUsage()).toEqual([
{ count: 3, version: '1.2.3' },
{ count: 4, version: '2.3.4' },
])
})
it('should get yesterday snjs version usage', async () => {
redisClient.keys = jest
.fn()
.mockReturnValue([
'count:action:snjs-request:1.2.3:timespan:2022-3-10',
'count:action:snjs-request:2.3.4:timespan:2022-3-10',
])
redisClient.get = jest.fn().mockReturnValueOnce(3).mockReturnValueOnce(4)
expect(await createStore().getYesterdaySNJSUsage()).toEqual([
{ count: 3, version: '1.2.3' },
{ count: 4, version: '2.3.4' },
])
})
it('should increment application version usage', async () => {
await createStore().incrementApplicationVersionUsage('1.2.3')
expect(pipeline.incr).toHaveBeenCalled()
expect(pipeline.exec).toHaveBeenCalled()
})
it('should increment snjs version usage', async () => {
await createStore().incrementSNJSVersionUsage('1.2.3')
expect(pipeline.incr).toHaveBeenCalled()
expect(pipeline.exec).toHaveBeenCalled()
})
it('should increment out of sync incedent count', async () => {
await createStore().incrementOutOfSyncIncidents()
expect(pipeline.incr).toHaveBeenCalled()
expect(pipeline.exec).toHaveBeenCalled()
})
it('should set a value to a measure', async () => {
await createStore().setMeasure(StatisticsMeasure.Income, 2, [Period.Today, Period.ThisMonth])
expect(pipeline.set).toHaveBeenCalledTimes(2)
expect(pipeline.exec).toHaveBeenCalled()
})
it('should increment measure by a value', async () => {
await createStore().incrementMeasure(StatisticsMeasure.Income, 2, [Period.Today, Period.ThisMonth])
expect(pipeline.incr).toHaveBeenCalledTimes(2)
expect(pipeline.incrbyfloat).toHaveBeenCalledTimes(2)
expect(pipeline.exec).toHaveBeenCalled()
})
it('should count a measurement average', async () => {
redisClient.get = jest.fn().mockReturnValueOnce('5').mockReturnValueOnce('2')
expect(await createStore().getMeasureAverage(StatisticsMeasure.Income, Period.Today)).toEqual(2 / 5)
})
it('should count a measurement average - 0 increments', async () => {
redisClient.get = jest.fn().mockReturnValueOnce(null).mockReturnValueOnce(null)
expect(await createStore().getMeasureAverage(StatisticsMeasure.Income, Period.Today)).toEqual(0)
})
it('should count a measurement average - 0 total value', async () => {
redisClient.get = jest.fn().mockReturnValueOnce(5).mockReturnValueOnce(null)
expect(await createStore().getMeasureAverage(StatisticsMeasure.Income, Period.Today)).toEqual(0)
})
it('should retrieve a measurement total for period', async () => {
redisClient.get = jest.fn().mockReturnValueOnce(5)
expect(await createStore().getMeasureTotal(StatisticsMeasure.Income, Period.Today)).toEqual(5)
expect(redisClient.get).toHaveBeenCalledWith('count:measure:income:timespan:period-key')
})
it('should retrieve a measurement total for period key', async () => {
redisClient.get = jest.fn().mockReturnValueOnce(5)
expect(await createStore().getMeasureTotal(StatisticsMeasure.Income, '2022-10-03')).toEqual(5)
expect(redisClient.get).toHaveBeenCalledWith('count:measure:income:timespan:2022-10-03')
})
})

View File

@@ -9,6 +9,35 @@ import { PeriodKeyGeneratorInterface } from '../../Domain/Time/PeriodKeyGenerato
export class RedisStatisticsStore implements StatisticsStoreInterface {
constructor(private periodKeyGenerator: PeriodKeyGeneratorInterface, private redisClient: IORedis.Redis) {}
async calculateTotalCountOverPeriod(
measure: StatisticsMeasure,
period: Period,
): Promise<{ periodKey: string; totalCount: number }[]> {
if (
![
Period.Last30Days,
Period.Last30DaysIncludingToday,
Period.ThisYear,
Period.Q1ThisYear,
Period.Q2ThisYear,
Period.Q3ThisYear,
Period.Q4ThisYear,
].includes(period)
) {
throw new Error(`Unsuporrted period: ${period}`)
}
const periodKeys = this.periodKeyGenerator.getDiscretePeriodKeys(period)
const counts = []
for (const periodKey of periodKeys) {
counts.push({
periodKey,
totalCount: await this.getMeasureTotal(measure, periodKey),
})
}
return counts
}
async getMeasureIncrementCounts(measure: StatisticsMeasure, period: Period): Promise<number> {
const increments = await this.redisClient.get(
`count:increments:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`,

View File

@@ -49,11 +49,13 @@ export class TypeORMRevenueModification {
@Column({
name: 'previous_mrr',
type: 'float',
})
declare previousMonthlyRevenue: number
@Column({
name: 'new_mrr',
type: 'float',
})
declare newMonthlyRevenue: number

View File

@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.11](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.37.10...@standardnotes/api-gateway@1.37.11) (2022-11-11)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.37.10](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.37.9...@standardnotes/api-gateway@1.37.10) (2022-11-11)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.37.9](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.37.8...@standardnotes/api-gateway@1.37.9) (2022-11-10)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.37.8](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.37.7...@standardnotes/api-gateway@1.37.8) (2022-11-10)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.37.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.37.6...@standardnotes/api-gateway@1.37.7) (2022-11-10)
### Bug Fixes
* **api-gateway:** setting headers ([3c2ac05](https://github.com/standardnotes/api-gateway/commit/3c2ac05c606371305b76dd368d5fe9287045f380))
## [1.37.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.37.5...@standardnotes/api-gateway@1.37.6) (2022-11-10)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.37.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.37.4...@standardnotes/api-gateway@1.37.5) (2022-11-09)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.37.5",
"version": "1.37.11",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -28,7 +28,7 @@
"@standardnotes/security": "workspace:*",
"@standardnotes/time": "workspace:*",
"aws-sdk": "^2.1160.0",
"axios": "^0.27.2",
"axios": "^1.1.3",
"cors": "2.8.5",
"dotenv": "^16.0.1",
"express": "^4.18.1",

View File

@@ -1,5 +1,7 @@
import * as winston from 'winston'
import axios, { AxiosInstance } from 'axios'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const axios = require('axios')
import { AxiosInstance } from 'axios'
import Redis from 'ioredis'
import { Container } from 'inversify'
import { Timer, TimerInterface } from '@standardnotes/time'

View File

@@ -60,7 +60,7 @@ export class AuthMiddleware extends BaseMiddleware {
})
if (authResponse.status > 200) {
response.setHeader('content-type', authResponse.headers['content-type'])
response.setHeader('content-type', authResponse.headers['content-type'] as string)
response.status(authResponse.status).send(authResponse.data)
return

View File

@@ -58,7 +58,7 @@ export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
})
if (authResponse.status > 200) {
response.setHeader('content-type', authResponse.headers['content-type'])
response.setHeader('content-type', authResponse.headers['content-type'] as string)
response.status(authResponse.status).send(authResponse.data)
return

View File

@@ -48,7 +48,7 @@ export class WebSocketAuthMiddleware extends BaseMiddleware {
})
if (authResponse.status > 200) {
response.setHeader('content-type', authResponse.headers['content-type'])
response.setHeader('content-type', authResponse.headers['content-type'] as string)
response.status(authResponse.status).send(authResponse.data)
return

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.59.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.59.5...@standardnotes/auth-server@1.59.6) (2022-11-11)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.59.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.59.4...@standardnotes/auth-server@1.59.5) (2022-11-11)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.59.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.59.3...@standardnotes/auth-server@1.59.4) (2022-11-10)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.59.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.59.2...@standardnotes/auth-server@1.59.3) (2022-11-10)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.59.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.59.1...@standardnotes/auth-server@1.59.2) (2022-11-10)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.59.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.59.0...@standardnotes/auth-server@1.59.1) (2022-11-10)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.59.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.58.0...@standardnotes/auth-server@1.59.0) (2022-11-09)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.59.0",
"version": "1.59.6",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -45,7 +45,7 @@
"@standardnotes/sncrypto-node": "workspace:*",
"@standardnotes/time": "workspace:*",
"aws-sdk": "^2.1159.0",
"axios": "^0.27.2",
"axios": "^1.1.3",
"bcryptjs": "2.4.3",
"cors": "2.8.5",
"dayjs": "^1.11.6",
@@ -60,7 +60,7 @@
"prettyjson": "^1.2.5",
"reflect-metadata": "0.1.13",
"typeorm": "^0.3.6",
"ua-parser-js": "1.0.2",
"ua-parser-js": "^1.0.32",
"uuid": "^9.0.0",
"winston": "^3.8.1"
},

View File

@@ -72,7 +72,9 @@ import { DeleteAccount } from '../Domain/UseCase/DeleteAccount/DeleteAccount'
import { DeleteSetting } from '../Domain/UseCase/DeleteSetting/DeleteSetting'
import { SettingFactory } from '../Domain/Setting/SettingFactory'
import { SettingService } from '../Domain/Setting/SettingService'
import axios, { AxiosInstance } from 'axios'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const axios = require('axios')
import { AxiosInstance } from 'axios'
import { UserSubscription } from '../Domain/Subscription/UserSubscription'
import { MySQLUserSubscriptionRepository } from '../Infra/MySQL/MySQLUserSubscriptionRepository'
import { WebSocketsClientService } from '../Infra/WebSockets/WebSocketsClientService'

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.44.1](https://github.com/standardnotes/server/compare/@standardnotes/common@1.44.0...@standardnotes/common@1.44.1) (2022-11-10)
### Bug Fixes
* **analytics:** add five year plans mrr calculation ([a03c5bc](https://github.com/standardnotes/server/commit/a03c5bceea2a9b166b1d5ad75181021462a86627))
# [1.44.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.43.0...@standardnotes/common@1.44.0) (2022-11-03)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/common",
"version": "1.44.0",
"version": "1.44.1",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -2,4 +2,5 @@
export enum SubscriptionBillingFrequency {
Monthly = 1,
Annual = 12,
FiveYear = 60,
}

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.9.22](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.21...@standardnotes/domain-events-infra@1.9.22) (2022-11-11)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.21](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.20...@standardnotes/domain-events-infra@1.9.21) (2022-11-11)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.20](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.19...@standardnotes/domain-events-infra@1.9.20) (2022-11-10)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.19](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.18...@standardnotes/domain-events-infra@1.9.19) (2022-11-10)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.18](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.17...@standardnotes/domain-events-infra@1.9.18) (2022-11-09)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.9.18",
"version": "1.9.22",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.86.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.85.0...@standardnotes/domain-events@2.86.0) (2022-11-11)
### Features
* **syncing-server:** add item content size recalculation ([1a13861](https://github.com/standardnotes/server/commit/1a138616478a646d76404c425800937d2049a226))
# [2.85.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.84.1...@standardnotes/domain-events@2.85.0) (2022-11-11)
### Features
* **domain-events:** add user content size recalculation requested event ([36ec39d](https://github.com/standardnotes/server/commit/36ec39d2fb5caec5952d820bb0d5d08d825a770c))
## [2.84.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.84.0...@standardnotes/domain-events@2.84.1) (2022-11-10)
**Note:** Version bump only for package @standardnotes/domain-events
# [2.84.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.83.0...@standardnotes/domain-events@2.84.0) (2022-11-10)
### Features
* **analytics:** add calculating monthly recurring revenue ([77e5065](https://github.com/standardnotes/server/commit/77e50655f6fa7f9c28e13f8b8bc6de246c0454f0))
# [2.83.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.82.0...@standardnotes/domain-events@2.83.0) (2022-11-09)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.83.0",
"version": "2.86.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -1,12 +1,4 @@
export interface DailyAnalyticsReportGeneratedEventPayload {
snjsStatistics: Array<{
version: string
count: number
}>
applicationStatistics: Array<{
version: string
count: number
}>
activityStatistics: Array<{
name: string
retention: number
@@ -28,18 +20,13 @@ export interface DailyAnalyticsReportGeneratedEventPayload {
}>
totalCount: number
}>
outOfSyncIncidents: number
retentionStatistics: Array<{
firstActivity: string
secondActivity: string
retention: {
periodKeys: Array<string>
values: Array<{
firstPeriodKey: string
secondPeriodKey: string
value: number
}>
}
statisticsOverTime: Array<{
name: string
period: number
counts: Array<{
periodKey: string
totalCount: number
}>
}>
churn: {
periodKeys: Array<string>

View File

@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { UserContentSizeRecalculationRequestedEventPayload } from './UserContentSizeRecalculationRequestedEventPayload'
export interface UserContentSizeRecalculationRequestedEvent extends DomainEventInterface {
type: 'USER_CONTENT_SIZE_RECALCULATION_REQUESTED'
payload: UserContentSizeRecalculationRequestedEventPayload
}

View File

@@ -0,0 +1,5 @@
import { Uuid } from '@standardnotes/common'
export interface UserContentSizeRecalculationRequestedEventPayload {
userUuid: Uuid
}

View File

@@ -98,6 +98,8 @@ export * from './Event/SubscriptionRevertRequestedEvent'
export * from './Event/SubscriptionRevertRequestedEventPayload'
export * from './Event/SubscriptionSyncRequestedEvent'
export * from './Event/SubscriptionSyncRequestedEventPayload'
export * from './Event/UserContentSizeRecalculationRequestedEvent'
export * from './Event/UserContentSizeRecalculationRequestedEventPayload'
export * from './Event/UserDisabledSessionUserAgentLoggingEvent'
export * from './Event/UserDisabledSessionUserAgentLoggingEventPayload'
export * from './Event/UserEmailChangedEvent'

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.6.17](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.16...@standardnotes/event-store@1.6.17) (2022-11-11)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.16](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.15...@standardnotes/event-store@1.6.16) (2022-11-11)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.15](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.14...@standardnotes/event-store@1.6.15) (2022-11-10)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.14](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.13...@standardnotes/event-store@1.6.14) (2022-11-10)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.13](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.12...@standardnotes/event-store@1.6.13) (2022-11-09)
**Note:** Version bump only for package @standardnotes/event-store

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.6.13",
"version": "1.6.17",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.8.17](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.16...@standardnotes/files-server@1.8.17) (2022-11-11)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.16](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.15...@standardnotes/files-server@1.8.16) (2022-11-11)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.15](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.14...@standardnotes/files-server@1.8.15) (2022-11-10)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.14](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.13...@standardnotes/files-server@1.8.14) (2022-11-10)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.13](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.12...@standardnotes/files-server@1.8.13) (2022-11-09)
**Note:** Version bump only for package @standardnotes/files-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.8.13",
"version": "1.8.17",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.5.4](https://github.com/standardnotes/server/compare/@standardnotes/predicates@1.5.3...@standardnotes/predicates@1.5.4) (2022-11-10)
**Note:** Version bump only for package @standardnotes/predicates
## [1.5.3](https://github.com/standardnotes/server/compare/@standardnotes/predicates@1.5.2...@standardnotes/predicates@1.5.3) (2022-11-03)
**Note:** Version bump only for package @standardnotes/predicates

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/predicates",
"version": "1.5.3",
"version": "1.5.4",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.13.18](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.17...@standardnotes/scheduler-server@1.13.18) (2022-11-11)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.13.17](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.16...@standardnotes/scheduler-server@1.13.17) (2022-11-11)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.13.16](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.15...@standardnotes/scheduler-server@1.13.16) (2022-11-10)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.13.15](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.14...@standardnotes/scheduler-server@1.13.15) (2022-11-10)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.13.14](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.13...@standardnotes/scheduler-server@1.13.14) (2022-11-09)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.13.14",
"version": "1.13.18",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.6.1](https://github.com/standardnotes/server/compare/@standardnotes/security@1.6.0...@standardnotes/security@1.6.1) (2022-11-10)
**Note:** Version bump only for package @standardnotes/security
# [1.6.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.5.3...@standardnotes/security@1.6.0) (2022-11-07)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/security",
"version": "1.6.0",
"version": "1.6.1",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.12.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.11.10...@standardnotes/syncing-server@1.12.0) (2022-11-11)
### Features
* **syncing-server:** add item content size recalculation ([1a13861](https://github.com/standardnotes/syncing-server-js/commit/1a138616478a646d76404c425800937d2049a226))
## [1.11.10](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.11.9...@standardnotes/syncing-server@1.11.10) (2022-11-11)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.11.9](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.11.8...@standardnotes/syncing-server@1.11.9) (2022-11-10)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.11.8](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.11.7...@standardnotes/syncing-server@1.11.8) (2022-11-10)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.11.7](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.11.6...@standardnotes/syncing-server@1.11.7) (2022-11-10)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.11.6](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.11.5...@standardnotes/syncing-server@1.11.6) (2022-11-10)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.11.5](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.11.4...@standardnotes/syncing-server@1.11.5) (2022-11-09)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.11.5",
"version": "1.12.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -34,7 +34,7 @@
"@standardnotes/settings": "workspace:*",
"@standardnotes/time": "workspace:*",
"aws-sdk": "^2.1159.0",
"axios": "^0.27.2",
"axios": "^1.1.3",
"cors": "2.8.5",
"dotenv": "^16.0.1",
"express": "^4.18.1",
@@ -49,7 +49,7 @@
"prettyjson": "^1.2.5",
"reflect-metadata": "0.1.13",
"typeorm": "^0.3.6",
"ua-parser-js": "1.0.2",
"ua-parser-js": "^1.0.32",
"uuid": "^9.0.0",
"winston": "^3.8.1"
},

View File

@@ -47,7 +47,9 @@ import { OwnershipFilter } from '../Domain/Item/SaveRule/OwnershipFilter'
import { TimeDifferenceFilter } from '../Domain/Item/SaveRule/TimeDifferenceFilter'
import { ItemFactoryInterface } from '../Domain/Item/ItemFactoryInterface'
import { ItemFactory } from '../Domain/Item/ItemFactory'
import axios, { AxiosInstance } from 'axios'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const axios = require('axios')
import { AxiosInstance } from 'axios'
import { UuidFilter } from '../Domain/Item/SaveRule/UuidFilter'
import { ContentTypeFilter } from '../Domain/Item/SaveRule/ContentTypeFilter'
import { ContentFilter } from '../Domain/Item/SaveRule/ContentFilter'
@@ -77,6 +79,7 @@ import { AppDataSource } from './DataSource'
import { RevisionRepositoryInterface } from '../Domain/Revision/RevisionRepositoryInterface'
import { ItemRepositoryInterface } from '../Domain/Item/ItemRepositoryInterface'
import { Repository } from 'typeorm'
import { UserContentSizeRecalculationRequestedEventHandler } from '../Domain/Handler/UserContentSizeRecalculationRequestedEventHandler'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -218,6 +221,9 @@ export class ContainerConfigLoader {
container
.bind<CloudBackupRequestedEventHandler>(TYPES.CloudBackupRequestedEventHandler)
.to(CloudBackupRequestedEventHandler)
container
.bind<UserContentSizeRecalculationRequestedEventHandler>(TYPES.UserContentSizeRecalculationRequestedEventHandler)
.to(UserContentSizeRecalculationRequestedEventHandler)
// Services
container.bind<ContentDecoder>(TYPES.ContentDecoder).to(ContentDecoder)

View File

@@ -48,6 +48,7 @@ const TYPES = {
EmailArchiveExtensionSyncedEventHandler: Symbol.for('EmailArchiveExtensionSyncedEventHandler'),
EmailBackupRequestedEventHandler: Symbol.for('EmailBackupRequestedEventHandler'),
CloudBackupRequestedEventHandler: Symbol.for('CloudBackupRequestedEventHandler'),
UserContentSizeRecalculationRequestedEventHandler: Symbol.for('UserContentSizeRecalculationRequestedEventHandler'),
// Services
ContentDecoder: Symbol.for('ContentDecoder'),
DomainEventPublisher: Symbol.for('DomainEventPublisher'),

View File

@@ -0,0 +1,52 @@
/* istanbul ignore file */
import { DomainEventHandlerInterface, UserContentSizeRecalculationRequestedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Stream } from 'stream'
import TYPES from '../../Bootstrap/Types'
import { ItemProjection } from '../../Projection/ItemProjection'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { Item } from '../Item/Item'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
@injectable()
export class UserContentSizeRecalculationRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
) {}
async handle(event: UserContentSizeRecalculationRequestedEvent): Promise<void> {
const stream = await this.itemRepository.streamAll({
deleted: false,
userUuid: event.payload.userUuid,
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
})
await new Promise((resolve, reject) => {
stream
.pipe(
new Stream.Transform({
objectMode: true,
transform: async (item, _encoding, callback) => {
const modelItem = await this.itemRepository.findByUuid(item.item_uuid)
if (modelItem !== null) {
modelItem.contentSize = Buffer.byteLength(JSON.stringify(this.itemProjector.projectFull(modelItem)))
await this.itemRepository.save(modelItem)
callback()
return
}
callback()
return
},
}),
)
.on('finish', resolve)
.on('error', reject)
})
}
}

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.19](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.18...@standardnotes/websockets-server@1.4.19) (2022-11-11)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.4.18](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.17...@standardnotes/websockets-server@1.4.18) (2022-11-11)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.4.17](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.16...@standardnotes/websockets-server@1.4.17) (2022-11-10)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.4.16](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.15...@standardnotes/websockets-server@1.4.16) (2022-11-10)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.4.15](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.14...@standardnotes/websockets-server@1.4.15) (2022-11-10)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.4.14](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.13...@standardnotes/websockets-server@1.4.14) (2022-11-10)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.4.13](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.12...@standardnotes/websockets-server@1.4.13) (2022-11-09)
**Note:** Version bump only for package @standardnotes/websockets-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.4.13",
"version": "1.4.19",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -30,7 +30,7 @@
"@standardnotes/domain-events-infra": "workspace:^",
"@standardnotes/security": "workspace:^",
"aws-sdk": "^2.1159.0",
"axios": "^0.27.2",
"axios": "^1.1.3",
"cors": "2.8.5",
"dotenv": "^16.0.1",
"express": "^4.18.1",

View File

@@ -1,5 +1,7 @@
import * as winston from 'winston'
import axios, { AxiosInstance } from 'axios'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const axios = require('axios')
import { AxiosInstance } from 'axios'
import Redis from 'ioredis'
import * as AWS from 'aws-sdk'
import { Container } from 'inversify'

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.17.17](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.16...@standardnotes/workspace-server@1.17.17) (2022-11-11)
**Note:** Version bump only for package @standardnotes/workspace-server
## [1.17.16](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.15...@standardnotes/workspace-server@1.17.16) (2022-11-11)
**Note:** Version bump only for package @standardnotes/workspace-server
## [1.17.15](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.14...@standardnotes/workspace-server@1.17.15) (2022-11-10)
**Note:** Version bump only for package @standardnotes/workspace-server
## [1.17.14](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.13...@standardnotes/workspace-server@1.17.14) (2022-11-10)
**Note:** Version bump only for package @standardnotes/workspace-server
## [1.17.13](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.12...@standardnotes/workspace-server@1.17.13) (2022-11-09)
**Note:** Version bump only for package @standardnotes/workspace-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/workspace-server",
"version": "1.17.13",
"version": "1.17.17",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -1857,7 +1857,7 @@ __metadata:
"@types/prettyjson": "npm:^0.0.30"
"@typescript-eslint/eslint-plugin": "npm:^5.29.0"
aws-sdk: "npm:^2.1160.0"
axios: "npm:^0.27.2"
axios: "npm:^1.1.3"
cors: "npm:2.8.5"
dotenv: "npm:^16.0.1"
eslint: "npm:^8.14.0"
@@ -1925,7 +1925,7 @@ __metadata:
"@types/uuid": "npm:^8.3.0"
"@typescript-eslint/eslint-plugin": "npm:^5.29.0"
aws-sdk: "npm:^2.1159.0"
axios: "npm:^0.27.2"
axios: "npm:^1.1.3"
bcryptjs: "npm:2.4.3"
cors: "npm:2.8.5"
dayjs: "npm:^1.11.6"
@@ -1947,7 +1947,7 @@ __metadata:
ts-jest: "npm:^29.0.3"
typeorm: "npm:^0.3.6"
typescript: "npm:^4.8.4"
ua-parser-js: "npm:1.0.2"
ua-parser-js: "npm:^1.0.32"
uuid: "npm:^9.0.0"
winston: "npm:^3.8.1"
languageName: unknown
@@ -2369,7 +2369,7 @@ __metadata:
"@types/uuid": "npm:^8.3.0"
"@typescript-eslint/eslint-plugin": "npm:^5.29.0"
aws-sdk: "npm:^2.1159.0"
axios: "npm:^0.27.2"
axios: "npm:^1.1.3"
cors: "npm:2.8.5"
dotenv: "npm:^16.0.1"
eslint: "npm:^8.14.0"
@@ -2390,7 +2390,7 @@ __metadata:
ts-jest: "npm:^29.0.3"
typeorm: "npm:^0.3.6"
typescript: "npm:^4.8.4"
ua-parser-js: "npm:1.0.2"
ua-parser-js: "npm:^1.0.32"
uuid: "npm:^9.0.0"
winston: "npm:^3.8.1"
languageName: unknown
@@ -2455,7 +2455,7 @@ __metadata:
"@types/newrelic": "npm:^7.0.3"
"@typescript-eslint/eslint-plugin": "npm:^5.29.0"
aws-sdk: "npm:^2.1159.0"
axios: "npm:^0.27.2"
axios: "npm:^1.1.3"
cors: "npm:2.8.5"
dotenv: "npm:^16.0.1"
eslint: "npm:^8.14.0"
@@ -3528,13 +3528,14 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^0.27.2":
version: 0.27.2
resolution: "axios@npm:0.27.2"
"axios@npm:^1.1.3":
version: 1.1.3
resolution: "axios@npm:1.1.3"
dependencies:
follow-redirects: "npm:^1.14.9"
follow-redirects: "npm:^1.15.0"
form-data: "npm:^4.0.0"
checksum: 4cd898afe90caaf05307fc5a0da4c61012493b6bfd4937fff9774455c01d368db583b17c4737e73853f149b32e615487930b491661682a1f69a1973b1f533bb7
proxy-from-env: "npm:^1.1.0"
checksum: 2e28acd01cc06f9f10dfce3531ebbcd74f2f2a364f44ff4aa59363239baf699c58361e9bb6425ff91283f5fc623051c55bdd2d08c1c06dd7780f0d0762297ca7
languageName: node
linkType: hard
@@ -5592,7 +5593,7 @@ __metadata:
languageName: node
linkType: hard
"follow-redirects@npm:^1.14.9":
"follow-redirects@npm:^1.15.0":
version: 1.15.2
resolution: "follow-redirects@npm:1.15.2"
peerDependenciesMeta:
@@ -9296,6 +9297,13 @@ __metadata:
languageName: node
linkType: hard
"proxy-from-env@npm:^1.1.0":
version: 1.1.0
resolution: "proxy-from-env@npm:1.1.0"
checksum: 0bba2ef7c8374b384e94e4477764e53df66fcdfa7d19e2c4a063cb39eea979c139ce13981970223665422e72b7d149609a927046e2e40ab340b84d91af082591
languageName: node
linkType: hard
"pseudomap@npm:^1.0.2":
version: 1.0.2
resolution: "pseudomap@npm:1.0.2"
@@ -11023,10 +11031,10 @@ __metadata:
languageName: node
linkType: hard
"ua-parser-js@npm:1.0.2":
version: 1.0.2
resolution: "ua-parser-js@npm:1.0.2"
checksum: 5ee14b105c4982bd86ca358e334187cde40aaf1dde2f1626fbd9c76de98b845191f02fc4e015e68885d047db336344f9ffba5b33cac5f6de6ad9a1adba88ea79
"ua-parser-js@npm:^1.0.32":
version: 1.0.32
resolution: "ua-parser-js@npm:1.0.32"
checksum: 9d320c6742248cf25264ece5f7756df097be7a7843cd71c10a9adaea35fe304600cd487da0bdb7add7239a5801905d720835ba4293a5add9568b868cd4079428
languageName: node
linkType: hard