Files
pn-new-crm/src/lib/queue/scheduler.ts
Matt Ciaccio 43f68ca093 chore(hardening): maintenance jobs, defense-in-depth, redis-backed public rate limit
- maintenance worker now expires GDPR export bundles (db row + MinIO object)
  on the gdpr_exports.expires_at boundary, plus 90-day retention sweep on
  ai_usage_ledger; both jobs scheduled daily.
- portId scoping added to listClientRelationships and listClientExports
  (defense-in-depth — parent-resource gates already prevent cross-tenant
  reads, but service layer should enforce on its own).
- SELECT FOR UPDATE on parent client/company row inside add/update address
  transactions to serialize concurrent isPrimary toggles.
- public /interests + /residential-inquiries endpoints swap their
  in-memory ipHits maps for the redis sliding-window limiter via the
  new rateLimiters.publicForm config (5/hr/IP), so the cap survives
  restarts and is shared across worker processes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 01:52:41 +02:00

77 lines
3.0 KiB
TypeScript

import { getQueue, type QueueName } from './index';
import { logger } from '@/lib/logger';
interface RecurringJobDef {
queue: QueueName;
name: string;
pattern: string;
}
/**
* Register all recurring jobs from 11-REALTIME-AND-BACKGROUND-JOBS.md Section 3.2.
* Called once on server startup.
*/
export async function registerRecurringJobs(): Promise<void> {
const recurring: RecurringJobDef[] = [
// Documenso signature fallback poll — primary is webhooks, this is safety net
{ queue: 'documents', name: 'signature-poll', pattern: '0 */6 * * *' },
// Reminder checks
{ queue: 'notifications', name: 'reminder-check', pattern: '0 * * * *' },
{ queue: 'notifications', name: 'reminder-overdue-check', pattern: '*/15 * * * *' },
// Google Calendar background sync
{ queue: 'maintenance', name: 'calendar-sync', pattern: '*/30 * * * *' },
// Daily checks at 08:00
{ queue: 'notifications', name: 'invoice-overdue-check', pattern: '0 8 * * *' },
{ queue: 'notifications', name: 'tenure-expiry-check', pattern: '0 8 * * *' },
// Exchange rate refresh every 6 hours
{ queue: 'maintenance', name: 'currency-refresh', pattern: '0 */6 * * *' },
// Database backup / cleanup
{ queue: 'maintenance', name: 'database-backup', pattern: '0 2 * * *' },
{ queue: 'maintenance', name: 'backup-cleanup', pattern: '0 3 * * 0' }, // Sunday 03:00
// Session cleanup
{ queue: 'maintenance', name: 'session-cleanup', pattern: '0 4 * * *' },
// Report scheduler — checks every minute for reports due to run
{ queue: 'reports', name: 'report-scheduler', pattern: '* * * * *' },
// Notification digest — configurable per user; placeholder fires hourly
// TODO(L2): make per-user schedule configurable (read from user_settings)
{ queue: 'email', name: 'notification-digest', pattern: '0 * * * *' },
// Cleanup jobs
{ queue: 'maintenance', name: 'temp-file-cleanup', pattern: '0 5 * * *' },
{ queue: 'maintenance', name: 'form-expiry-check', pattern: '0 * * * *' },
// Phase B: alert rule engine sweep
{ queue: 'maintenance', name: 'alerts-evaluate', pattern: '*/5 * * * *' },
// Phase B: analytics snapshot warm
{ queue: 'maintenance', name: 'analytics-refresh', pattern: '*/15 * * * *' },
// Phase 3d: GDPR Article 17 — actually delete expired export bundles
{ queue: 'maintenance', name: 'gdpr-export-cleanup', pattern: '0 4 * * *' },
// Phase 3b: AI usage ledger retention (90-day rolling window)
{ queue: 'maintenance', name: 'ai-usage-retention', pattern: '0 5 * * *' },
];
for (const job of recurring) {
const queue = getQueue(job.queue);
await queue.upsertJobScheduler(
job.name,
{ pattern: job.pattern },
{ data: {}, name: job.name },
);
logger.info(
{ queue: job.queue, job: job.name, pattern: job.pattern },
'Registered recurring job',
);
}
logger.info({ count: recurring.length }, 'All recurring jobs registered');
}