feat(auth): add script for fixing subscriptions with missing id state (#1030)
* fix(auth): add subscription id safe guards on handlers * feat(domain-events): add subscription state events * feat(domain-events): add subscription state events * feat(auth): add handling of subscription state fetched events * feat(auth): add script for fixing subscriptions state
This commit is contained in:
parent
6f07aaf87a
commit
86b050865f
22 changed files with 363 additions and 0 deletions
74
packages/auth/bin/fix_subscriptions.ts
Normal file
74
packages/auth/bin/fix_subscriptions.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import 'reflect-metadata'
|
||||||
|
|
||||||
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
|
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||||
|
import TYPES from '../src/Bootstrap/Types'
|
||||||
|
import { Env } from '../src/Bootstrap/Env'
|
||||||
|
import { UserSubscriptionRepositoryInterface } from '../src/Domain/Subscription/UserSubscriptionRepositoryInterface'
|
||||||
|
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
|
||||||
|
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||||
|
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
|
||||||
|
import { Uuid } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
const fixSubscriptions = async (
|
||||||
|
userRepository: UserRepositoryInterface,
|
||||||
|
userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||||
|
domainEventFactory: DomainEventFactoryInterface,
|
||||||
|
domainEventPublisher: DomainEventPublisherInterface,
|
||||||
|
): Promise<void> => {
|
||||||
|
const subscriptions = await userSubscriptionRepository.findBySubscriptionId(0)
|
||||||
|
|
||||||
|
for (const subscription of subscriptions) {
|
||||||
|
const userUuidOrError = Uuid.create(subscription.userUuid)
|
||||||
|
if (userUuidOrError.isFailed()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const userUuid = userUuidOrError.getValue()
|
||||||
|
|
||||||
|
const user = await userRepository.findOneByUuid(userUuid)
|
||||||
|
if (!user) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
await domainEventPublisher.publish(
|
||||||
|
domainEventFactory.createSubscriptionStateRequestedEvent({
|
||||||
|
userEmail: user.email,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = new ContainerConfigLoader('worker')
|
||||||
|
void container.load().then((container) => {
|
||||||
|
const env: Env = new Env()
|
||||||
|
env.load()
|
||||||
|
|
||||||
|
const logger: Logger = container.get(TYPES.Auth_Logger)
|
||||||
|
|
||||||
|
logger.info('Starting to fix subscriptions with missing subscriptionId ...')
|
||||||
|
|
||||||
|
const userRepository = container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository)
|
||||||
|
const userSubscriptionRepository = container.get<UserSubscriptionRepositoryInterface>(
|
||||||
|
TYPES.Auth_UserSubscriptionRepository,
|
||||||
|
)
|
||||||
|
const domainEventFactory = container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory)
|
||||||
|
const domainEventPublisher = container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher)
|
||||||
|
|
||||||
|
Promise.resolve(
|
||||||
|
fixSubscriptions(userRepository, userSubscriptionRepository, domainEventFactory, domainEventPublisher),
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
logger.info('Finished fixing subscriptions with missing subscriptionId.')
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error('Failed to fix subscriptions with missing subscriptionId.', {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
})
|
||||||
|
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
})
|
11
packages/auth/docker/entrypoint-fix-subscriptions.js
Normal file
11
packages/auth/docker/entrypoint-fix-subscriptions.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()
|
||||||
|
|
||||||
|
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/fix_subscriptions.js')))
|
||||||
|
|
||||||
|
Object.defineProperty(exports, '__esModule', { value: true })
|
||||||
|
|
||||||
|
exports.default = index
|
|
@ -42,6 +42,10 @@ case "$COMMAND" in
|
||||||
exec node docker/entrypoint-fix-roles.js
|
exec node docker/entrypoint-fix-roles.js
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
'fix-subscriptions' )
|
||||||
|
exec node docker/entrypoint-fix-subscriptions.js
|
||||||
|
;;
|
||||||
|
|
||||||
'delete-accounts' )
|
'delete-accounts' )
|
||||||
FILE_NAME=$1 && shift 1
|
FILE_NAME=$1 && shift 1
|
||||||
MODE=$1 && shift 1
|
MODE=$1 && shift 1
|
||||||
|
|
|
@ -285,6 +285,7 @@ import { RenewSharedSubscriptions } from '../Domain/UseCase/RenewSharedSubscript
|
||||||
import { FixStorageQuotaForUser } from '../Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser'
|
import { FixStorageQuotaForUser } from '../Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser'
|
||||||
import { FileQuotaRecalculatedEventHandler } from '../Domain/Handler/FileQuotaRecalculatedEventHandler'
|
import { FileQuotaRecalculatedEventHandler } from '../Domain/Handler/FileQuotaRecalculatedEventHandler'
|
||||||
import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
|
import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
|
||||||
|
import { SubscriptionStateFetchedEventHandler } from '../Domain/Handler/SubscriptionStateFetchedEventHandler'
|
||||||
|
|
||||||
export class ContainerConfigLoader {
|
export class ContainerConfigLoader {
|
||||||
constructor(private mode: 'server' | 'worker' = 'server') {}
|
constructor(private mode: 'server' | 'worker' = 'server') {}
|
||||||
|
@ -1579,6 +1580,16 @@ export class ContainerConfigLoader {
|
||||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
container
|
||||||
|
.bind<SubscriptionStateFetchedEventHandler>(TYPES.Auth_SubscriptionStateFetchedEventHandler)
|
||||||
|
.toConstantValue(
|
||||||
|
new SubscriptionStateFetchedEventHandler(
|
||||||
|
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||||
|
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
|
||||||
|
container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
|
||||||
|
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Auth_AccountDeletionRequestedEventHandler)],
|
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Auth_AccountDeletionRequestedEventHandler)],
|
||||||
|
@ -1620,6 +1631,7 @@ export class ContainerConfigLoader {
|
||||||
'FILE_QUOTA_RECALCULATED',
|
'FILE_QUOTA_RECALCULATED',
|
||||||
container.get<FileQuotaRecalculatedEventHandler>(TYPES.Auth_FileQuotaRecalculatedEventHandler),
|
container.get<FileQuotaRecalculatedEventHandler>(TYPES.Auth_FileQuotaRecalculatedEventHandler),
|
||||||
],
|
],
|
||||||
|
['SUBSCRIPTION_STATE_FETCHED', container.get(TYPES.Auth_SubscriptionStateFetchedEventHandler)],
|
||||||
])
|
])
|
||||||
|
|
||||||
if (isConfiguredForHomeServer) {
|
if (isConfiguredForHomeServer) {
|
||||||
|
|
|
@ -205,6 +205,7 @@ const TYPES = {
|
||||||
),
|
),
|
||||||
Auth_UserInvitedToSharedVaultEventHandler: Symbol.for('Auth_UserInvitedToSharedVaultEventHandler'),
|
Auth_UserInvitedToSharedVaultEventHandler: Symbol.for('Auth_UserInvitedToSharedVaultEventHandler'),
|
||||||
Auth_FileQuotaRecalculatedEventHandler: Symbol.for('Auth_FileQuotaRecalculatedEventHandler'),
|
Auth_FileQuotaRecalculatedEventHandler: Symbol.for('Auth_FileQuotaRecalculatedEventHandler'),
|
||||||
|
Auth_SubscriptionStateFetchedEventHandler: Symbol.for('Auth_SubscriptionStateFetchedEventHandler'),
|
||||||
// Services
|
// Services
|
||||||
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
|
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
|
||||||
Auth_SessionService: Symbol.for('Auth_SessionService'),
|
Auth_SessionService: Symbol.for('Auth_SessionService'),
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
SessionRefreshedEvent,
|
SessionRefreshedEvent,
|
||||||
AccountDeletionVerificationRequestedEvent,
|
AccountDeletionVerificationRequestedEvent,
|
||||||
FileQuotaRecalculationRequestedEvent,
|
FileQuotaRecalculationRequestedEvent,
|
||||||
|
SubscriptionStateRequestedEvent,
|
||||||
} from '@standardnotes/domain-events'
|
} from '@standardnotes/domain-events'
|
||||||
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
|
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
|
||||||
import { TimerInterface } from '@standardnotes/time'
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
@ -34,6 +35,21 @@ import { KeyParamsData } from '@standardnotes/responses'
|
||||||
@injectable()
|
@injectable()
|
||||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||||
constructor(@inject(TYPES.Auth_Timer) private timer: TimerInterface) {}
|
constructor(@inject(TYPES.Auth_Timer) private timer: TimerInterface) {}
|
||||||
|
createSubscriptionStateRequestedEvent(dto: { userEmail: string }): SubscriptionStateRequestedEvent {
|
||||||
|
return {
|
||||||
|
type: 'SUBSCRIPTION_STATE_REQUESTED',
|
||||||
|
createdAt: this.timer.getUTCDate(),
|
||||||
|
meta: {
|
||||||
|
correlation: {
|
||||||
|
userIdentifier: dto.userEmail,
|
||||||
|
userIdentifierType: 'email',
|
||||||
|
},
|
||||||
|
origin: DomainEventService.Auth,
|
||||||
|
},
|
||||||
|
payload: dto,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent {
|
createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent {
|
||||||
return {
|
return {
|
||||||
type: 'FILE_QUOTA_RECALCULATION_REQUESTED',
|
type: 'FILE_QUOTA_RECALCULATION_REQUESTED',
|
||||||
|
|
|
@ -20,11 +20,13 @@ import {
|
||||||
SessionRefreshedEvent,
|
SessionRefreshedEvent,
|
||||||
AccountDeletionVerificationRequestedEvent,
|
AccountDeletionVerificationRequestedEvent,
|
||||||
FileQuotaRecalculationRequestedEvent,
|
FileQuotaRecalculationRequestedEvent,
|
||||||
|
SubscriptionStateRequestedEvent,
|
||||||
} from '@standardnotes/domain-events'
|
} from '@standardnotes/domain-events'
|
||||||
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
|
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
|
||||||
import { KeyParamsData } from '@standardnotes/responses'
|
import { KeyParamsData } from '@standardnotes/responses'
|
||||||
|
|
||||||
export interface DomainEventFactoryInterface {
|
export interface DomainEventFactoryInterface {
|
||||||
|
createSubscriptionStateRequestedEvent(dto: { userEmail: string }): SubscriptionStateRequestedEvent
|
||||||
createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent
|
createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent
|
||||||
createWebSocketMessageRequestedEvent(dto: { userUuid: string; message: JSONString }): WebSocketMessageRequestedEvent
|
createWebSocketMessageRequestedEvent(dto: { userUuid: string; message: JSONString }): WebSocketMessageRequestedEvent
|
||||||
createEmailRequestedEvent(dto: {
|
createEmailRequestedEvent(dto: {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { inject, injectable } from 'inversify'
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||||
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
|
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
|
||||||
|
@ -12,9 +13,20 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
||||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||||
@inject(TYPES.Auth_OfflineUserSubscriptionRepository)
|
@inject(TYPES.Auth_OfflineUserSubscriptionRepository)
|
||||||
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
|
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
|
||||||
|
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionCancelledEvent): Promise<void> {
|
async handle(event: SubscriptionCancelledEvent): Promise<void> {
|
||||||
|
if (!event.payload.subscriptionId) {
|
||||||
|
this.logger.error('Subscription ID is missing', {
|
||||||
|
codeTag: 'SubscriptionCancelledEventHandler.handle',
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
userId: event.payload.userEmail,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (event.payload.offline) {
|
if (event.payload.offline) {
|
||||||
await this.updateOfflineSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
|
await this.updateOfflineSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,16 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionExpiredEvent): Promise<void> {
|
async handle(event: SubscriptionExpiredEvent): Promise<void> {
|
||||||
|
if (!event.payload.subscriptionId) {
|
||||||
|
this.logger.error('Subscription ID is missing', {
|
||||||
|
codeTag: 'SubscriptionExpiredEventHandler.handle',
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
userId: event.payload.userEmail,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (event.payload.offline) {
|
if (event.payload.offline) {
|
||||||
await this.updateOfflineSubscriptionEndsAt(event.payload.subscriptionId, event.payload.timestamp)
|
await this.updateOfflineSubscriptionEndsAt(event.payload.subscriptionId, event.payload.timestamp)
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,16 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
|
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
|
||||||
|
if (!event.payload.subscriptionId) {
|
||||||
|
this.logger.error('Subscription ID is missing', {
|
||||||
|
codeTag: 'SubscriptionPurchasedEventHandler.handle',
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
userId: event.payload.userEmail,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (event.payload.offline) {
|
if (event.payload.offline) {
|
||||||
const offlineUserSubscription = await this.createOfflineSubscription(
|
const offlineUserSubscription = await this.createOfflineSubscription(
|
||||||
event.payload.subscriptionId,
|
event.payload.subscriptionId,
|
||||||
|
|
|
@ -22,6 +22,16 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionReassignedEvent): Promise<void> {
|
async handle(event: SubscriptionReassignedEvent): Promise<void> {
|
||||||
|
if (!event.payload.subscriptionId) {
|
||||||
|
this.logger.error('Subscription ID is missing', {
|
||||||
|
codeTag: 'SubscriptionReassignedEventHandler.handle',
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
userId: event.payload.userEmail,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const usernameOrError = Username.create(event.payload.userEmail)
|
const usernameOrError = Username.create(event.payload.userEmail)
|
||||||
if (usernameOrError.isFailed()) {
|
if (usernameOrError.isFailed()) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -22,6 +22,16 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionRefundedEvent): Promise<void> {
|
async handle(event: SubscriptionRefundedEvent): Promise<void> {
|
||||||
|
if (!event.payload.subscriptionId) {
|
||||||
|
this.logger.error('Subscription ID is missing', {
|
||||||
|
codeTag: 'SubscriptionRefundedEventHandler.handle',
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
userId: event.payload.userEmail,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (event.payload.offline) {
|
if (event.payload.offline) {
|
||||||
await this.updateOfflineSubscriptionEndsAt(event.payload.subscriptionId, event.payload.timestamp)
|
await this.updateOfflineSubscriptionEndsAt(event.payload.subscriptionId, event.payload.timestamp)
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,16 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionRenewedEvent): Promise<void> {
|
async handle(event: SubscriptionRenewedEvent): Promise<void> {
|
||||||
|
if (!event.payload.subscriptionId) {
|
||||||
|
this.logger.error('Subscription ID is missing', {
|
||||||
|
codeTag: 'SubscriptionRenewedEventHandler.handle',
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
userId: event.payload.userEmail,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (event.payload.offline) {
|
if (event.payload.offline) {
|
||||||
const offlineUserSubscription = await this.offlineUserSubscriptionRepository.findOneBySubscriptionId(
|
const offlineUserSubscription = await this.offlineUserSubscriptionRepository.findOneBySubscriptionId(
|
||||||
event.payload.subscriptionId,
|
event.payload.subscriptionId,
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { Username } from '@standardnotes/domain-core'
|
||||||
|
import { DomainEventHandlerInterface, SubscriptionStateFetchedEvent } from '@standardnotes/domain-events'
|
||||||
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
|
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||||
|
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||||
|
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||||
|
|
||||||
|
export class SubscriptionStateFetchedEventHandler implements DomainEventHandlerInterface {
|
||||||
|
constructor(
|
||||||
|
private userRepository: UserRepositoryInterface,
|
||||||
|
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||||
|
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
|
||||||
|
private logger: Logger,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async handle(event: SubscriptionStateFetchedEvent): Promise<void> {
|
||||||
|
if (!event.payload.subscriptionId) {
|
||||||
|
this.logger.error('Subscription ID is missing', {
|
||||||
|
codeTag: 'SubscriptionStateFetchedEventHandler.handle',
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
userId: event.payload.userEmail,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info('Subscription state update fetched', {
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (event.payload.offline) {
|
||||||
|
this.logger.info('Updating offline subscription', {
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const subscription = await this.offlineUserSubscriptionRepository.findOneByEmailAndSubscriptionId(
|
||||||
|
event.payload.userEmail,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if (!subscription) {
|
||||||
|
this.logger.error('Offline subscription not found', {
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.planName = event.payload.subscriptionName
|
||||||
|
subscription.email = event.payload.userEmail
|
||||||
|
subscription.endsAt = event.payload.subscriptionExpiresAt
|
||||||
|
subscription.cancelled = event.payload.canceled
|
||||||
|
if (subscription.subscriptionId !== event.payload.subscriptionId) {
|
||||||
|
this.logger.warn('Subscription IDs do not match', {
|
||||||
|
previousSubscriptionId: subscription.subscriptionId,
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
subscription.subscriptionId = event.payload.subscriptionId
|
||||||
|
|
||||||
|
await this.offlineUserSubscriptionRepository.save(subscription)
|
||||||
|
|
||||||
|
this.logger.info('Offline subscription updated', {
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const usernameOrError = Username.create(event.payload.userEmail)
|
||||||
|
if (usernameOrError.isFailed()) {
|
||||||
|
this.logger.warn(`Could not update subscription: ${usernameOrError.getError()}`, {
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const username = usernameOrError.getValue()
|
||||||
|
|
||||||
|
const user = await this.userRepository.findOneByUsernameOrEmail(username)
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
this.logger.warn(`Could not find user with email: ${username.value}`, {
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info('Updating subscription', {
|
||||||
|
userId: user.uuid,
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const subscription = await this.userSubscriptionRepository.findOneByUserUuidAndSubscriptionId(user.uuid, 0)
|
||||||
|
if (!subscription) {
|
||||||
|
this.logger.error('Subscription not found', {
|
||||||
|
userId: user.uuid,
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.planName = event.payload.subscriptionName
|
||||||
|
subscription.endsAt = event.payload.subscriptionExpiresAt
|
||||||
|
subscription.cancelled = event.payload.canceled
|
||||||
|
if (subscription.subscriptionId !== event.payload.subscriptionId) {
|
||||||
|
this.logger.warn('Subscription IDs do not match', {
|
||||||
|
previousSubscriptionId: subscription.subscriptionId,
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
subscription.subscriptionId = event.payload.subscriptionId
|
||||||
|
|
||||||
|
await this.userSubscriptionRepository.save(subscription)
|
||||||
|
|
||||||
|
this.logger.info('Subscription updated to current state', {
|
||||||
|
userId: user.uuid,
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,6 +33,16 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionSyncRequestedEvent): Promise<void> {
|
async handle(event: SubscriptionSyncRequestedEvent): Promise<void> {
|
||||||
|
if (!event.payload.subscriptionId) {
|
||||||
|
this.logger.error('Subscription ID is missing', {
|
||||||
|
codeTag: 'SubscriptionSyncRequestedEventHandler.handle',
|
||||||
|
subscriptionId: event.payload.subscriptionId,
|
||||||
|
userId: event.payload.userEmail,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.info('Subscription sync requested', {
|
this.logger.info('Subscription sync requested', {
|
||||||
subscriptionId: event.payload.subscriptionId,
|
subscriptionId: event.payload.subscriptionId,
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { OfflineUserSubscription } from './OfflineUserSubscription'
|
||||||
|
|
||||||
export interface OfflineUserSubscriptionRepositoryInterface {
|
export interface OfflineUserSubscriptionRepositoryInterface {
|
||||||
findOneByEmail(email: string): Promise<OfflineUserSubscription | null>
|
findOneByEmail(email: string): Promise<OfflineUserSubscription | null>
|
||||||
|
findOneByEmailAndSubscriptionId(email: string, subscriptionId: number): Promise<OfflineUserSubscription | null>
|
||||||
findOneBySubscriptionId(subscriptionId: number): Promise<OfflineUserSubscription | null>
|
findOneBySubscriptionId(subscriptionId: number): Promise<OfflineUserSubscription | null>
|
||||||
findByEmail(email: string, activeAfter: number): Promise<OfflineUserSubscription[]>
|
findByEmail(email: string, activeAfter: number): Promise<OfflineUserSubscription[]>
|
||||||
updateEndsAt(subscriptionId: number, endsAt: number, updatedAt: number): Promise<void>
|
updateEndsAt(subscriptionId: number, endsAt: number, updatedAt: number): Promise<void>
|
||||||
|
|
|
@ -12,6 +12,19 @@ export class TypeORMOfflineUserSubscriptionRepository implements OfflineUserSubs
|
||||||
private ormRepository: Repository<OfflineUserSubscription>,
|
private ormRepository: Repository<OfflineUserSubscription>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async findOneByEmailAndSubscriptionId(
|
||||||
|
email: string,
|
||||||
|
subscriptionId: number,
|
||||||
|
): Promise<OfflineUserSubscription | null> {
|
||||||
|
return await this.ormRepository
|
||||||
|
.createQueryBuilder()
|
||||||
|
.where('email = :email AND subscription_id = :subscriptionId', {
|
||||||
|
email,
|
||||||
|
subscriptionId,
|
||||||
|
})
|
||||||
|
.getOne()
|
||||||
|
}
|
||||||
|
|
||||||
async save(offlineUserSubscription: OfflineUserSubscription): Promise<OfflineUserSubscription> {
|
async save(offlineUserSubscription: OfflineUserSubscription): Promise<OfflineUserSubscription> {
|
||||||
return this.ormRepository.save(offlineUserSubscription)
|
return this.ormRepository.save(offlineUserSubscription)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { DomainEventInterface } from './DomainEventInterface'
|
||||||
|
|
||||||
|
import { SubscriptionStateFetchedEventPayload } from './SubscriptionStateFetchedEventPayload'
|
||||||
|
|
||||||
|
export interface SubscriptionStateFetchedEvent extends DomainEventInterface {
|
||||||
|
type: 'SUBSCRIPTION_STATE_FETCHED'
|
||||||
|
payload: SubscriptionStateFetchedEventPayload
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
export interface SubscriptionStateFetchedEventPayload {
|
||||||
|
userEmail: string
|
||||||
|
subscriptionId: number
|
||||||
|
subscriptionName: string
|
||||||
|
subscriptionExpiresAt: number
|
||||||
|
timestamp: number
|
||||||
|
offline: boolean
|
||||||
|
canceled: boolean
|
||||||
|
extensionKey: string
|
||||||
|
offlineFeaturesToken: string
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { DomainEventInterface } from './DomainEventInterface'
|
||||||
|
|
||||||
|
import { SubscriptionStateRequestedEventPayload } from './SubscriptionStateRequestedEventPayload'
|
||||||
|
|
||||||
|
export interface SubscriptionStateRequestedEvent extends DomainEventInterface {
|
||||||
|
type: 'SUBSCRIPTION_STATE_REQUESTED'
|
||||||
|
payload: SubscriptionStateRequestedEventPayload
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface SubscriptionStateRequestedEventPayload {
|
||||||
|
userEmail: string
|
||||||
|
}
|
|
@ -110,6 +110,10 @@ export * from './Event/SubscriptionExpiredEvent'
|
||||||
export * from './Event/SubscriptionExpiredEventPayload'
|
export * from './Event/SubscriptionExpiredEventPayload'
|
||||||
export * from './Event/SubscriptionRevertRequestedEvent'
|
export * from './Event/SubscriptionRevertRequestedEvent'
|
||||||
export * from './Event/SubscriptionRevertRequestedEventPayload'
|
export * from './Event/SubscriptionRevertRequestedEventPayload'
|
||||||
|
export * from './Event/SubscriptionStateFetchedEvent'
|
||||||
|
export * from './Event/SubscriptionStateFetchedEventPayload'
|
||||||
|
export * from './Event/SubscriptionStateRequestedEvent'
|
||||||
|
export * from './Event/SubscriptionStateRequestedEventPayload'
|
||||||
export * from './Event/SubscriptionSyncRequestedEvent'
|
export * from './Event/SubscriptionSyncRequestedEvent'
|
||||||
export * from './Event/SubscriptionSyncRequestedEventPayload'
|
export * from './Event/SubscriptionSyncRequestedEventPayload'
|
||||||
export * from './Event/UserAddedToSharedVaultEvent'
|
export * from './Event/UserAddedToSharedVaultEvent'
|
||||||
|
|
Loading…
Reference in a new issue