// Daily-ish generator for the partner payout ledger. Mirrors ArchiveWorker: // a cheap hourly tick with a once-per-day guard, no cron lib needed. Each run // (re)generates the CURRENT month's payout rows from live MRR × marginPct, so // the partner Payouts tab reflects this month's accrual; past months freeze at // their last snapshot once the month rolls over. The operator UI can force a // run (any period) via POST /billing/payouts/generate regardless of this timer. import { Injectable, Logger, type OnApplicationBootstrap, type OnModuleDestroy } from '@nestjs/common' import { ConfigService } from '@nestjs/config' import { BillingService } from './billing.service.js' const TICK_MS = 60 * 60 * 1000 // 1 hour; a day-guard limits real work to once/day. const DEFAULT_RUN_HOUR_UTC = 2 // 02:00 UTC daily (before the audit archive at 03:00). @Injectable() export class PayoutWorker implements OnApplicationBootstrap, OnModuleDestroy { private readonly logger = new Logger(PayoutWorker.name) private readonly enabled: boolean private readonly runHour: number private timer: NodeJS.Timeout | null = null private lastRunDay: string | null = null constructor( private readonly billing: BillingService, config: ConfigService, ) { // On by default — payout generation is cheap, derived, and Stripe-independent. // Set PAYOUTS_AUTOGEN=false to leave it to manual operator runs only. this.enabled = config.get('PAYOUTS_AUTOGEN') !== 'false' this.runHour = Number(config.get('PAYOUTS_RUN_HOUR_UTC') ?? DEFAULT_RUN_HOUR_UTC) } onApplicationBootstrap(): void { if (!this.enabled) { this.logger.log('PAYOUTS_AUTOGEN=false — payout scheduler dormant. Operator UI can force runs.') return } this.logger.log(`Payout scheduler active · runs daily at ${this.runHour}:00 UTC`) // Generate once on startup so the current month's ledger exists immediately; // the day-guard then prevents repeat runs within the same UTC day. void this.run('startup') this.timer = setInterval(() => void this.tick(), TICK_MS) } onModuleDestroy(): void { if (this.timer) clearInterval(this.timer) } private async tick(): Promise { if (new Date().getUTCHours() !== this.runHour) return const today = new Date().toISOString().slice(0, 10) if (this.lastRunDay === today) return await this.run('tick') } private async run(trigger: string): Promise { this.lastRunDay = new Date().toISOString().slice(0, 10) try { const res = await this.billing.generatePayouts() this.logger.log(`Payout run (${trigger}) for ${res.periodMonth}: ${res.rows} row(s)`) } catch (err) { this.logger.error( `Payout run (${trigger}) failed: ${err instanceof Error ? err.message : String(err)}`, ) } } }