wulf-pulse/app/api/qbo/sync/route.ts
lorentz 672f17b7f9 chore: check in pending work — queue preferences, QBO AR diagnostics, mobile engagement fixes, ops scripts
Bundles several in-progress efforts that were sitting uncommitted:
- User queue-preferences (migration 087, API route, popover component)
- QBO invoice soft-delete (migration 088) and AR diagnostics route
- Dashboard/mobile engagement route and page adjustments
- Docker Compose log-rotation config
- One-off ticket/RMM investigation scripts (scripts/)
- Planning docs: phase verification/pattern notes, mobile shell design spec
- .gitignore: exclude local scratch financial/inventory data and Claude Code
  worktree/local-settings runtime state (never meant for version control)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6RuWdiUiXrPK6FLBHjtpY
2026-07-18 06:34:57 -04:00

83 lines
3.2 KiB
TypeScript

/**
* QBO Sync API
* POST /api/qbo/sync — trigger a full or incremental sync
* GET /api/qbo/sync — get last sync status and record counts
*/
import { NextRequest, NextResponse } from 'next/server';
import { QboSyncService } from '@/lib/services/qbo-sync-service';
import { QboClient } from '@/lib/services/qbo-client';
import postgresClient from '@/lib/services/postgres-client';
export async function POST(request: NextRequest) {
try {
const body = await request.json().catch(() => ({}));
const syncType: 'full' | 'incremental' = body.syncType === 'incremental' ? 'incremental' : 'full';
const triggeredBy = body.triggeredBy || 'api';
const client = new QboClient();
const service = new QboSyncService(client);
if (service.isSyncInProgress()) {
return NextResponse.json({ error: 'QBO sync already in progress' }, { status: 409 });
}
// Run async — return immediately
service[syncType === 'full' ? 'fullSync' : 'incrementalSync'](triggeredBy).catch((err) => {
console.error('[QBO Sync API] Sync failed:', err);
});
return NextResponse.json({
message: `QBO ${syncType} sync started`,
triggeredBy,
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return NextResponse.json({ error: msg }, { status: 500 });
}
}
export async function GET() {
try {
const [invoices, payments, deposits, transactions, reports] = await Promise.all([
postgresClient.query(`SELECT COUNT(*) as count, MAX(synced_at) as last_sync FROM qbo_invoices WHERE is_deleted = false`),
postgresClient.query(`SELECT COUNT(*) as count, MAX(synced_at) as last_sync FROM qbo_payments`),
postgresClient.query(`SELECT COUNT(*) as count, MAX(synced_at) as last_sync FROM qbo_deposits`),
postgresClient.query(`SELECT COUNT(*) as count, MAX(synced_at) as last_sync FROM qbo_transactions`),
postgresClient.query(`SELECT COUNT(*) as count, MAX(synced_at) as last_sync FROM qbo_reports`),
]);
// Check token status
let tokenStatus: 'valid' | 'expired' | 'missing' = 'missing';
try {
const client = new QboClient();
const token = await client.loadToken();
if (token) {
tokenStatus = new Date() < new Date(token.access_token_expires_at) ? 'valid' : 'expired';
}
} catch {
tokenStatus = 'missing';
}
return NextResponse.json({
tokenStatus,
counts: {
invoices: parseInt(invoices.rows[0]?.count || '0'),
payments: parseInt(payments.rows[0]?.count || '0'),
deposits: parseInt(deposits.rows[0]?.count || '0'),
transactions: parseInt(transactions.rows[0]?.count || '0'),
reports: parseInt(reports.rows[0]?.count || '0'),
},
lastSync: {
invoices: invoices.rows[0]?.last_sync ?? null,
payments: payments.rows[0]?.last_sync ?? null,
deposits: deposits.rows[0]?.last_sync ?? null,
transactions: transactions.rows[0]?.last_sync ?? null,
reports: reports.rows[0]?.last_sync ?? null,
},
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return NextResponse.json({ error: msg }, { status: 500 });
}
}