wulf-pulse/app/api/qbo/sync/route.ts
lorentz b98c67482a feat: QuickBooks Online integration
- Add QBO OAuth2 client with token refresh (lib/services/qbo-client.ts)
- Add QBO sync service for invoices, payments, deposits, purchases, journal entries, reports (lib/services/qbo-sync-service.ts)
- Add QBO types (lib/types/qbo.ts)
- Add API routes: /api/qbo/auth, /api/qbo/sync, /api/qbo/disconnect
- Add /admin/qbo status and sync management page
- Add legal pages: /legal/eula, /legal/privacy (Intuit app assessment)
- Add QBO nav link under Admin
- Fix reports: remove invalid summarize_column_by, add accounting_method from Preferences API, add showrows=all&showcols=all
- Add CashFlow report type alongside P&L and BalanceSheet
- Add NoReportData check to skip empty report months
- Add intuit_tid capture in error messages
- Add redirect: follow for cluster routing
- Migration 051: qbo_tokens, qbo_invoices, qbo_payments, qbo_deposits, qbo_transactions, qbo_reports tables

Also includes earlier work:
- Ping flap suppression pipeline step
- Ticket digest reports with LLM analysis
- Zabbix WAN monitor and gap analysis
- Kiosk is_deleted filter fixes
- Datto RMM ping target enrichment
- Entity sync soft-delete detection
2026-03-17 07:39:55 -04:00

83 lines
3.1 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`),
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 });
}
}