feat(260521-fci-02): wire tickets-reconcile schedule + migration 090

- sync-scheduler.ts: extend sync_type union with 'tickets-reconcile', add a
  default schedule entry (disabled, 30 4 * * *), and a dispatch case using
  the device-link-reconcile / integration-health dynamic-import pattern.
- migrations/090_ticket_reconcile_schedule.sql: idempotent INSERT (ON CONFLICT
  DO NOTHING) so existing installs pick up the row without disturbing the
  fresh-DB default-seed path.
This commit is contained in:
lorentz 2026-05-21 11:09:36 -04:00
parent 51f0b32cb3
commit badd718194
2 changed files with 52 additions and 1 deletions

View file

@ -22,7 +22,7 @@ export interface ScheduleConfig {
name: string;
description: string;
cron_expression: string;
sync_type: 'incremental' | 'full' | 'veeam-incremental' | 'veeam-full' | 'veeam-rpo-check' | 'contract-services' | 'engagement-daily' | 'zoom-daily' | 'morning-summary' | 'ticket-digest-daily' | 'ticket-digest-weekly' | 'ticket-digest-monthly' | 'device-link-reconcile' | 'integration-health';
sync_type: 'incremental' | 'full' | 'veeam-incremental' | 'veeam-full' | 'veeam-rpo-check' | 'contract-services' | 'engagement-daily' | 'zoom-daily' | 'morning-summary' | 'ticket-digest-daily' | 'ticket-digest-weekly' | 'ticket-digest-monthly' | 'device-link-reconcile' | 'integration-health' | 'qbo' | 'appgate-sessions' | 'appgate-daily' | 'tickets-reconcile';
years_back?: number;
is_enabled: boolean;
last_run?: Date;
@ -291,6 +291,14 @@ class SyncScheduler {
sync_type: 'integration-health',
is_enabled: false,
},
{
id: 'tickets-reconcile',
name: 'Tickets Reconciliation',
description: 'STOPGAP backstop: re-fetches stale-open tickets (>7d since last sync) from Autotask and reconciles status / soft-deletes missing rows. Runs daily at 4:30 AM. Capped at 500 tickets per run.',
cron_expression: '30 4 * * *',
sync_type: 'tickets-reconcile',
is_enabled: false,
},
];
for (const schedule of defaultSchedules) {
@ -434,6 +442,25 @@ class SyncScheduler {
console.log(
`[SCHEDULER] integration-health: failed=${result.summary.failed} expired=${result.summary.expired} expiringSoon=${result.summary.expiringWithin14Days} alertSent=${result.alertSent}`
);
} else if (config.sync_type === 'appgate-sessions' || config.sync_type === 'appgate-daily') {
const { isAppgateConfigured } = await import('@/lib/services/appgate-factory');
if (!isAppgateConfigured()) {
console.log(`[SCHEDULER] Skipping ${config.sync_type} — AppGate not configured`);
} else {
const { getAppgateSyncService } = await import('@/lib/services/appgate-sync-service');
const svc = getAppgateSyncService();
if (config.sync_type === 'appgate-daily') {
await svc.dailySync('scheduled');
} else {
await svc.sessionsSync('scheduled');
}
}
} else if (config.sync_type === 'tickets-reconcile') {
const { reconcileStaleTickets } = await import('@/lib/services/ticket-reconciliation-service');
const result = await reconcileStaleTickets();
console.log(
`[SCHEDULER] tickets-reconcile: scanned=${result.scanned} updated=${result.updated} flippedComplete=${result.statusFlippedToComplete} softDeleted=${result.softDeleted} errors=${result.errors}`
);
} else if (config.sync_type === 'incremental') {
await this.syncService.incrementalSync('scheduled');
} else {

View file

@ -0,0 +1,24 @@
-- Migration 090: Seed the tickets-reconcile sync schedule (STOPGAP).
--
-- The sync_scheduler.createDefaultSchedules() path only seeds defaults on a
-- virgin sync_schedules table; this migration covers existing installs.
-- Idempotent via ON CONFLICT (id) DO NOTHING.
INSERT INTO sync_schedules (
id,
name,
description,
cron_expression,
sync_type,
years_back,
is_enabled
) VALUES (
'tickets-reconcile',
'Tickets Reconciliation',
'STOPGAP backstop: re-fetches stale-open tickets (>7d since last sync) from Autotask and reconciles status / soft-deletes missing rows. Runs daily at 4:30 AM. Capped at 500 tickets per run.',
'30 4 * * *',
'tickets-reconcile',
NULL,
false
)
ON CONFLICT (id) DO NOTHING;