Files
motovaultpro/backend/src/core/scheduler/index.ts
Eric Gullickson e7461a4836 feat: add subscription API endpoints and grace period job - M3 (refs #55)
API Endpoints (all authenticated):
- GET /api/subscriptions - current subscription status
- POST /api/subscriptions/checkout - create Stripe subscription
- POST /api/subscriptions/cancel - schedule cancellation at period end
- POST /api/subscriptions/reactivate - cancel pending cancellation
- PUT /api/subscriptions/payment-method - update payment method
- GET /api/subscriptions/invoices - billing history

Grace Period Job:
- Daily cron at 2:30 AM to check expired grace periods
- Downgrades to free tier when 30-day grace period expires
- Syncs tier to user_profiles.subscription_tier

Email Templates:
- payment_failed_immediate (first failure)
- payment_failed_7day (7 days before grace ends)
- payment_failed_1day (1 day before grace ends)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 16:16:58 -06:00

153 lines
4.8 KiB
TypeScript

/**
* @ai-summary Centralized cron job scheduler
* @ai-context Uses node-cron for scheduling background tasks
*/
import cron from 'node-cron';
import { logger } from '../logging/logger';
import { processScheduledNotifications } from '../../features/notifications/jobs/notification-processor.job';
import { processAccountPurges } from '../../features/user-profile/jobs/account-purge.job';
import {
processScheduledBackups,
setBackupJobPool,
} from '../../features/backup/jobs/backup-scheduled.job';
import {
processBackupRetention,
setBackupCleanupJobPool,
} from '../../features/backup/jobs/backup-cleanup.job';
import {
processAuditLogCleanup,
setAuditLogCleanupJobPool,
} from '../../features/audit-log/jobs/cleanup.job';
import {
processGracePeriodExpirations,
setGracePeriodJobPool,
} from '../../features/subscriptions/jobs/grace-period.job';
import { pool } from '../config/database';
let schedulerInitialized = false;
export function initializeScheduler(): void {
if (schedulerInitialized) {
logger.warn('Scheduler already initialized, skipping');
return;
}
logger.info('Initializing cron scheduler');
// Initialize backup job pools
setBackupJobPool(pool);
setBackupCleanupJobPool(pool);
// Initialize audit log cleanup job pool
setAuditLogCleanupJobPool(pool);
// Initialize grace period job pool
setGracePeriodJobPool(pool);
// Daily notification processing at 8 AM
cron.schedule('0 8 * * *', async () => {
logger.info('Running scheduled notification job');
try {
await processScheduledNotifications();
} catch (error) {
logger.error('Scheduled notification job failed', {
error: error instanceof Error ? error.message : String(error)
});
}
});
// Daily account purge job at 2 AM (GDPR compliance - 30-day grace period)
cron.schedule('0 2 * * *', async () => {
logger.info('Running account purge job');
try {
const result = await processAccountPurges();
logger.info('Account purge job completed successfully', {
processed: result.processed,
deleted: result.deleted,
errors: result.errors.length,
});
} catch (error) {
logger.error('Account purge job failed', {
error: error instanceof Error ? error.message : String(error)
});
}
});
// Grace period expiration check at 2:30 AM daily
cron.schedule('30 2 * * *', async () => {
logger.info('Running grace period expiration job');
try {
const result = await processGracePeriodExpirations();
logger.info('Grace period job completed', {
processed: result.processed,
downgraded: result.downgraded,
errors: result.errors.length,
});
} catch (error) {
logger.error('Grace period job failed', {
error: error instanceof Error ? error.message : String(error)
});
}
});
// Check for scheduled backups every minute
cron.schedule('* * * * *', async () => {
logger.debug('Checking for scheduled backups');
try {
await processScheduledBackups();
} catch (error) {
logger.error('Scheduled backup check failed', {
error: error instanceof Error ? error.message : String(error)
});
}
});
// Backup retention cleanup at 4 AM daily (after backups complete)
cron.schedule('0 4 * * *', async () => {
logger.info('Running backup retention cleanup job');
try {
const result = await processBackupRetention();
logger.info('Backup retention cleanup completed', {
processed: result.processed,
totalDeleted: result.totalDeleted,
totalFreedBytes: result.totalFreedBytes,
errors: result.errors.length,
});
} catch (error) {
logger.error('Backup retention cleanup failed', {
error: error instanceof Error ? error.message : String(error)
});
}
});
// Audit log retention cleanup at 3 AM daily (90-day retention)
cron.schedule('0 3 * * *', async () => {
logger.info('Running audit log cleanup job');
try {
const result = await processAuditLogCleanup();
if (result.success) {
logger.info('Audit log cleanup job completed', {
deletedCount: result.deletedCount,
retentionDays: result.retentionDays,
});
} else {
logger.error('Audit log cleanup job failed', {
error: result.error,
});
}
} catch (error) {
logger.error('Audit log cleanup job failed', {
error: error instanceof Error ? error.message : String(error)
});
}
});
schedulerInitialized = true;
logger.info('Cron scheduler initialized - notification (8 AM), account purge (2 AM), grace period (2:30 AM), audit log cleanup (3 AM), backup check (every min), retention cleanup (4 AM)');
}
export function isSchedulerInitialized(): boolean {
return schedulerInitialized;
}