feat: billing sync (cloud) (#899)

* Set page history to 5 minutes interval

* * Configure default queue options

* sync

* * stripe seats sync (cloud)
This commit is contained in:
Philip Okugbe
2025-03-17 11:00:23 +00:00
committed by GitHub
parent 21c3ad0ecc
commit f45bdddb23
8 changed files with 44 additions and 14 deletions

View File

@ -48,7 +48,7 @@
"@nestjs/platform-socket.io": "^11.0.10", "@nestjs/platform-socket.io": "^11.0.10",
"@nestjs/terminus": "^11.0.0", "@nestjs/terminus": "^11.0.0",
"@nestjs/websockets": "^11.0.10", "@nestjs/websockets": "^11.0.10",
"@node-saml/passport-saml": "^5.0.0", "@node-saml/passport-saml": "^5.0.1",
"@react-email/components": "0.0.28", "@react-email/components": "0.0.28",
"@react-email/render": "1.0.2", "@react-email/render": "1.0.2",
"@socket.io/redis-adapter": "^8.3.0", "@socket.io/redis-adapter": "^8.3.0",

View File

@ -20,9 +20,9 @@ export class HistoryListener {
const pageCreationTime = new Date(page.createdAt).getTime(); const pageCreationTime = new Date(page.createdAt).getTime();
const currentTime = Date.now(); const currentTime = Date.now();
const TEN_MINUTES = 10 * 60 * 1000; const FIVE_MINUTES = 5 * 60 * 1000;
if (currentTime - pageCreationTime < TEN_MINUTES) { if (currentTime - pageCreationTime < FIVE_MINUTES) {
return; return;
} }
@ -31,13 +31,13 @@ export class HistoryListener {
if ( if (
!lastHistory || !lastHistory ||
(!isDeepStrictEqual(lastHistory.content, page.content) && (!isDeepStrictEqual(lastHistory.content, page.content) &&
currentTime - new Date(lastHistory.createdAt).getTime() >= TEN_MINUTES) currentTime - new Date(lastHistory.createdAt).getTime() >= FIVE_MINUTES)
) { ) {
try { try {
await this.pageHistoryRepo.saveHistory(page); await this.pageHistoryRepo.saveHistory(page);
this.logger.debug(`New history created for: ${page.id}`); this.logger.debug(`New history created for: ${page.id}`);
} catch (err) { } catch (err) {
this.logger.error(`Failed to create history for: ${page.id}`, err); this.logger.error(`Failed to create history for page: ${page.id}`, err);
} }
} }
} }

View File

@ -24,6 +24,10 @@ import { nanoIdGen } from '../../../common/helpers';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { executeWithPagination } from '@docmost/db/pagination/pagination'; import { executeWithPagination } from '@docmost/db/pagination/pagination';
import { DomainService } from 'src/integrations/environment/domain.service'; import { DomainService } from 'src/integrations/environment/domain.service';
import { InjectQueue } from '@nestjs/bullmq';
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
import { Queue } from 'bullmq';
import { EnvironmentService } from '../../../integrations/environment/environment.service';
@Injectable() @Injectable()
export class WorkspaceInvitationService { export class WorkspaceInvitationService {
@ -35,6 +39,8 @@ export class WorkspaceInvitationService {
private domainService: DomainService, private domainService: DomainService,
private tokenService: TokenService, private tokenService: TokenService,
@InjectKysely() private readonly db: KyselyDB, @InjectKysely() private readonly db: KyselyDB,
@InjectQueue(QueueName.BILLING_QUEUE) private billingQueue: Queue,
private readonly environmentService: EnvironmentService,
) {} ) {}
async getInvitations(workspaceId: string, pagination: PaginationOptions) { async getInvitations(workspaceId: string, pagination: PaginationOptions) {
@ -266,6 +272,10 @@ export class WorkspaceInvitationService {
}); });
} }
if (this.environmentService.isCloud()) {
await this.billingQueue.add(QueueJob.STRIPE_SEATS_SYNC, { workspaceId });
}
return this.tokenService.generateAccessToken(newUser); return this.tokenService.generateAccessToken(newUser);
} }

View File

@ -2,6 +2,7 @@ export enum QueueName {
EMAIL_QUEUE = '{email-queue}', EMAIL_QUEUE = '{email-queue}',
ATTACHMENT_QUEUE = '{attachment-queue}', ATTACHMENT_QUEUE = '{attachment-queue}',
GENERAL_QUEUE = '{general-queue}', GENERAL_QUEUE = '{general-queue}',
BILLING_QUEUE = '{billing-queue}',
} }
export enum QueueJob { export enum QueueJob {
@ -11,6 +12,6 @@ export enum QueueJob {
PAGE_CONTENT_UPDATE = 'page-content-update', PAGE_CONTENT_UPDATE = 'page-content-update',
PAGE_BACKLINKS = 'page-backlinks', PAGE_BACKLINKS = 'page-backlinks',
STRIPE_SEATS_SYNC = 'sync-stripe-seats',
} }

View File

@ -6,3 +6,7 @@ export interface IPageBacklinkJob {
workspaceId: string; workspaceId: string;
mentions: MentionNode[]; mentions: MentionNode[];
} }
export interface IStripeSeatsSyncJob {
workspaceId: string;
}

View File

@ -106,20 +106,26 @@ export class BacklinksProcessor extends WorkerHost implements OnModuleDestroy {
@OnWorkerEvent('active') @OnWorkerEvent('active')
onActive(job: Job) { onActive(job: Job) {
if (job.name === QueueJob.PAGE_BACKLINKS) {
this.logger.debug(`Processing ${job.name} job`); this.logger.debug(`Processing ${job.name} job`);
} }
}
@OnWorkerEvent('failed') @OnWorkerEvent('failed')
onError(job: Job) { onError(job: Job) {
if (job.name === QueueJob.PAGE_BACKLINKS) {
this.logger.error( this.logger.error(
`Error processing ${job.name} job. Reason: ${job.failedReason}`, `Error processing ${job.name} job. Reason: ${job.failedReason}`,
); );
} }
}
@OnWorkerEvent('completed') @OnWorkerEvent('completed')
onCompleted(job: Job) { onCompleted(job: Job) {
if (job.name === QueueJob.PAGE_BACKLINKS) {
this.logger.debug(`Completed ${job.name} job`); this.logger.debug(`Completed ${job.name} job`);
} }
}
async onModuleDestroy(): Promise<void> { async onModuleDestroy(): Promise<void> {
if (this.worker) { if (this.worker) {

View File

@ -24,7 +24,13 @@ import { BacklinksProcessor } from './processors/backlinks.processor';
attempts: 3, attempts: 3,
backoff: { backoff: {
type: 'exponential', type: 'exponential',
delay: 10000, delay: 20 * 1000,
},
removeOnComplete: {
count: 200,
},
removeOnFail: {
count: 100,
}, },
}, },
}; };
@ -40,6 +46,9 @@ import { BacklinksProcessor } from './processors/backlinks.processor';
BullModule.registerQueue({ BullModule.registerQueue({
name: QueueName.GENERAL_QUEUE, name: QueueName.GENERAL_QUEUE,
}), }),
BullModule.registerQueue({
name: QueueName.BILLING_QUEUE,
}),
], ],
exports: [BullModule], exports: [BullModule],
providers: [BacklinksProcessor], providers: [BacklinksProcessor],