diff --git a/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts index 60672a53b..8a19a6b2b 100644 --- a/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts +++ b/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.spec.ts @@ -132,6 +132,7 @@ describe('SubscriptionPurchasedEventHandler', () => { expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith( subscription, SubscriptionName.ProPlan, + '123', ) }) diff --git a/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts b/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts index b7e5deed3..356110e27 100644 --- a/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/SubscriptionPurchasedEventHandler.ts @@ -76,6 +76,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription( userSubscription, event.payload.subscriptionName, + user.uuid, ) const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid }) diff --git a/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.spec.ts index dfad0871a..6098722d2 100644 --- a/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.spec.ts +++ b/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.spec.ts @@ -94,6 +94,7 @@ describe('SubscriptionReassignedEventHandler', () => { expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith( subscription, SubscriptionName.ProPlan, + '123', ) }) diff --git a/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.ts b/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.ts index eb8b90a42..cb5abd69a 100644 --- a/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/SubscriptionReassignedEventHandler.ts @@ -58,6 +58,7 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription( userSubscription, event.payload.subscriptionName, + user.uuid, ) } diff --git a/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.spec.ts b/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.spec.ts index bafd37075..648f6a73d 100644 --- a/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.spec.ts +++ b/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.spec.ts @@ -130,6 +130,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => { expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith( subscription, SubscriptionName.ProPlan, + '123', ) expect(settingService.createOrReplace).toHaveBeenCalledWith({ diff --git a/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.ts b/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.ts index d889b59b1..fb0cfbbd4 100644 --- a/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.ts +++ b/packages/auth/src/Domain/Handler/SubscriptionSyncRequestedEventHandler.ts @@ -89,6 +89,7 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription( userSubscription, event.payload.subscriptionName, + user.uuid, ) await this.settingService.createOrReplace({ diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingService.spec.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingService.spec.ts index 28c3e2fd9..2642077e7 100644 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingService.spec.ts +++ b/packages/auth/src/Domain/Setting/SubscriptionSettingService.spec.ts @@ -13,6 +13,7 @@ import { SubscriptionName } from '@standardnotes/common' import { User } from '../User/User' import { SettingFactoryInterface } from './SettingFactoryInterface' import { SubscriptionSettingsAssociationServiceInterface } from './SubscriptionSettingsAssociationServiceInterface' +import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' describe('SubscriptionSettingService', () => { let setting: SubscriptionSetting @@ -22,6 +23,7 @@ describe('SubscriptionSettingService', () => { let subscriptionSettingRepository: SubscriptionSettingRepositoryInterface let subscriptionSettingsAssociationService: SubscriptionSettingsAssociationServiceInterface let settingDecrypter: SettingDecrypterInterface + let userSubscriptionRepository: UserSubscriptionRepositoryInterface let logger: Logger const createService = () => @@ -30,6 +32,7 @@ describe('SubscriptionSettingService', () => { subscriptionSettingRepository, subscriptionSettingsAssociationService, settingDecrypter, + userSubscriptionRepository, logger, ) @@ -51,6 +54,16 @@ describe('SubscriptionSettingService', () => { subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(null) subscriptionSettingRepository.save = jest.fn().mockImplementation((setting) => setting) + userSubscriptionRepository = {} as jest.Mocked + userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([ + { + uuid: 's-1-2-3', + } as jest.Mocked, + { + uuid: 's-2-3-4', + } as jest.Mocked, + ]) + subscriptionSettingsAssociationService = {} as jest.Mocked subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( new Map([ @@ -76,7 +89,11 @@ describe('SubscriptionSettingService', () => { }) it('should create default settings for a subscription', async () => { - await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription, SubscriptionName.PlusPlan) + await createService().applyDefaultSubscriptionSettingsForSubscription( + userSubscription, + SubscriptionName.PlusPlan, + '1-2-3', + ) expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting) }) @@ -97,7 +114,11 @@ describe('SubscriptionSettingService', () => { ) subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(setting) - await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription, SubscriptionName.PlusPlan) + await createService().applyDefaultSubscriptionSettingsForSubscription( + userSubscription, + SubscriptionName.PlusPlan, + '1-2-3', + ) expect(subscriptionSettingRepository.save).toHaveBeenCalled() }) @@ -118,7 +139,41 @@ describe('SubscriptionSettingService', () => { ) subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(null) - await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription, SubscriptionName.PlusPlan) + await createService().applyDefaultSubscriptionSettingsForSubscription( + userSubscription, + SubscriptionName.PlusPlan, + '1-2-3', + ) + + expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting) + }) + + it('should create default settings for a subscription if it is not replaceable and no previous subscription existed', async () => { + subscriptionSettingsAssociationService.getDefaultSettingsAndValuesForSubscriptionName = jest.fn().mockReturnValue( + new Map([ + [ + SubscriptionSettingName.FileUploadBytesUsed, + { + value: '0', + sensitive: 0, + serverEncryptionVersion: EncryptionVersion.Unencrypted, + replaceable: false, + }, + ], + ]), + ) + subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid = jest.fn().mockReturnValue(null) + userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([ + { + uuid: '1-2-3', + } as jest.Mocked, + ]) + + await createService().applyDefaultSubscriptionSettingsForSubscription( + userSubscription, + SubscriptionName.PlusPlan, + '1-2-3', + ) expect(subscriptionSettingRepository.save).toHaveBeenCalledWith(setting) }) @@ -128,7 +183,11 @@ describe('SubscriptionSettingService', () => { .fn() .mockReturnValue(undefined) - await createService().applyDefaultSubscriptionSettingsForSubscription(userSubscription, SubscriptionName.PlusPlan) + await createService().applyDefaultSubscriptionSettingsForSubscription( + userSubscription, + SubscriptionName.PlusPlan, + '1-2-3', + ) expect(subscriptionSettingRepository.save).not.toHaveBeenCalled() }) diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingService.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingService.ts index baa22d4e5..22ccd3c72 100644 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingService.ts +++ b/packages/auth/src/Domain/Setting/SubscriptionSettingService.ts @@ -1,4 +1,4 @@ -import { SubscriptionName } from '@standardnotes/common' +import { SubscriptionName, Uuid } from '@standardnotes/common' import { SubscriptionSettingName } from '@standardnotes/settings' import { inject, injectable } from 'inversify' import { Logger } from 'winston' @@ -16,6 +16,7 @@ import { FindSubscriptionSettingDTO } from './FindSubscriptionSettingDTO' import { SubscriptionSettingRepositoryInterface } from './SubscriptionSettingRepositoryInterface' import { SettingFactoryInterface } from './SettingFactoryInterface' import { SubscriptionSettingsAssociationServiceInterface } from './SubscriptionSettingsAssociationServiceInterface' +import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface' @injectable() export class SubscriptionSettingService implements SubscriptionSettingServiceInterface { @@ -26,12 +27,14 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt @inject(TYPES.SubscriptionSettingsAssociationService) private subscriptionSettingAssociationService: SubscriptionSettingsAssociationServiceInterface, @inject(TYPES.SettingDecrypter) private settingDecrypter: SettingDecrypterInterface, + @inject(TYPES.UserSubscriptionRepository) private userSubscriptionRepository: UserSubscriptionRepositoryInterface, @inject(TYPES.Logger) private logger: Logger, ) {} async applyDefaultSubscriptionSettingsForSubscription( userSubscription: UserSubscription, subscriptionName: SubscriptionName, + userUuid: Uuid, ): Promise { const defaultSettingsWithValues = await this.subscriptionSettingAssociationService.getDefaultSettingsAndValuesForSubscriptionName(subscriptionName) @@ -44,10 +47,7 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt for (const settingName of defaultSettingsWithValues.keys()) { const setting = defaultSettingsWithValues.get(settingName) as SettingDescription if (!setting.replaceable) { - const existingSetting = await this.subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid( - settingName, - userSubscription.uuid, - ) + const existingSetting = await this.findPreviousSubscriptionSetting(settingName, userSubscription.uuid, userUuid) if (existingSetting !== null) { existingSetting.userSubscription = Promise.resolve(userSubscription) await this.subscriptionSettingRepository.save(existingSetting) @@ -126,4 +126,22 @@ export class SubscriptionSettingService implements SubscriptionSettingServiceInt subscriptionSetting, } } + + private async findPreviousSubscriptionSetting( + settingName: SubscriptionSettingName, + currentUserSubscriptionUuid: Uuid, + userUuid: Uuid, + ): Promise { + const userSubscriptions = await this.userSubscriptionRepository.findByUserUuid(userUuid) + const previousSubscriptions = userSubscriptions.filter( + (subscription) => subscription.uuid !== currentUserSubscriptionUuid, + ) + const lastSubscription = previousSubscriptions.shift() + + if (!lastSubscription) { + return null + } + + return this.subscriptionSettingRepository.findLastByNameAndUserSubscriptionUuid(settingName, lastSubscription.uuid) + } } diff --git a/packages/auth/src/Domain/Setting/SubscriptionSettingServiceInterface.ts b/packages/auth/src/Domain/Setting/SubscriptionSettingServiceInterface.ts index 664c82a4c..7d086b034 100644 --- a/packages/auth/src/Domain/Setting/SubscriptionSettingServiceInterface.ts +++ b/packages/auth/src/Domain/Setting/SubscriptionSettingServiceInterface.ts @@ -1,4 +1,4 @@ -import { SubscriptionName } from '@standardnotes/common' +import { SubscriptionName, Uuid } from '@standardnotes/common' import { UserSubscription } from '../Subscription/UserSubscription' import { CreateOrReplaceSubscriptionSettingDTO } from './CreateOrReplaceSubscriptionSettingDTO' @@ -10,6 +10,7 @@ export interface SubscriptionSettingServiceInterface { applyDefaultSubscriptionSettingsForSubscription( userSubscription: UserSubscription, subscriptionName: SubscriptionName, + userUuid: Uuid, ): Promise createOrReplace(dto: CreateOrReplaceSubscriptionSettingDTO): Promise findSubscriptionSettingWithDecryptedValue(dto: FindSubscriptionSettingDTO): Promise diff --git a/packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts b/packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts index ad2cef6ba..f0996dd0c 100644 --- a/packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts +++ b/packages/auth/src/Domain/Subscription/UserSubscriptionRepositoryInterface.ts @@ -6,6 +6,7 @@ export interface UserSubscriptionRepositoryInterface { findOneByUuid(uuid: Uuid): Promise countByUserUuid(userUuid: Uuid): Promise findOneByUserUuid(userUuid: Uuid): Promise + findByUserUuid(userUuid: Uuid): Promise findOneByUserUuidAndSubscriptionId(userUuid: Uuid, subscriptionId: number): Promise findBySubscriptionIdAndType(subscriptionId: number, type: UserSubscriptionType): Promise findBySubscriptionId(subscriptionId: number): Promise diff --git a/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.spec.ts b/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.spec.ts index 1f7e504be..ed9e13291 100644 --- a/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.spec.ts +++ b/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.spec.ts @@ -104,6 +104,7 @@ describe('AcceptSharedSubscriptionInvitation', () => { expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith( inviteeSubscription, 'PLUS_PLAN', + '123', ) }) diff --git a/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.ts b/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.ts index b776a05aa..88f64728b 100644 --- a/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.ts +++ b/packages/auth/src/Domain/UseCase/AcceptSharedSubscriptionInvitation/AcceptSharedSubscriptionInvitation.ts @@ -75,6 +75,7 @@ export class AcceptSharedSubscriptionInvitation implements UseCaseInterface { await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription( inviteeSubscription, inviteeSubscription.planName as SubscriptionName, + invitee.uuid, ) return { diff --git a/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.spec.ts b/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.spec.ts index cea967e30..d85ad24c1 100644 --- a/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.spec.ts +++ b/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.spec.ts @@ -36,6 +36,28 @@ describe('MySQLUserSubscriptionRepository', () => { expect(ormRepository.save).toHaveBeenCalledWith(subscription) }) + it('should find all subscriptions by user uuid', async () => { + const canceledSubscription = { + planName: SubscriptionName.ProPlan, + cancelled: true, + } as jest.Mocked + + ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder) + + selectQueryBuilder.where = jest.fn().mockReturnThis() + selectQueryBuilder.orderBy = jest.fn().mockReturnThis() + selectQueryBuilder.getMany = jest.fn().mockReturnValue([canceledSubscription, subscription]) + + const result = await createRepository().findByUserUuid('123') + + expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', { + user_uuid: '123', + }) + expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC') + expect(selectQueryBuilder.getMany).toHaveBeenCalled() + expect(result).toEqual([canceledSubscription, subscription]) + }) + it('should find one longest lasting uncanceled subscription by user uuid if there are canceled ones', async () => { const canceledSubscription = { planName: SubscriptionName.ProPlan, diff --git a/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.ts b/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.ts index f5bb2d5fe..a5d845970 100644 --- a/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.ts +++ b/packages/auth/src/Infra/MySQL/MySQLUserSubscriptionRepository.ts @@ -14,6 +14,16 @@ export class MySQLUserSubscriptionRepository implements UserSubscriptionReposito private ormRepository: Repository, ) {} + async findByUserUuid(userUuid: string): Promise { + return await this.ormRepository + .createQueryBuilder() + .where('user_uuid = :user_uuid', { + user_uuid: userUuid, + }) + .orderBy('ends_at', 'DESC') + .getMany() + } + async countByUserUuid(userUuid: Uuid): Promise { return await this.ormRepository .createQueryBuilder()