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
This commit is contained in:
lorentz 2026-03-17 07:39:55 -04:00
parent c518eefdb2
commit b98c67482a
40 changed files with 6223 additions and 15 deletions

161
lib/types/qbo.ts Normal file
View file

@ -0,0 +1,161 @@
export interface QboTokenRecord {
id: number;
realm_id: string;
access_token: string;
refresh_token: string;
access_token_expires_at: Date;
refresh_token_expires_at: Date;
created_at: Date;
updated_at: Date;
}
export interface QboTokenResponse {
access_token: string;
refresh_token: string;
expires_in: number;
x_refresh_token_expires_in: number;
token_type: string;
}
export interface QboRef {
value: string;
name?: string;
}
export interface QboMetaData {
CreateTime: string;
LastUpdatedTime: string;
}
export interface QboLineItem {
Id?: string;
LineNum?: number;
Description?: string;
Amount: number;
DetailType: string;
[key: string]: any;
}
export interface QboLinkedTxn {
TxnId: string;
TxnType: string;
TxnLineId?: string;
}
// Invoice
export interface QboInvoice {
Id: string;
SyncToken: string;
DocNumber?: string;
TxnDate?: string;
DueDate?: string;
CustomerRef?: QboRef;
BillEmail?: { Address?: string };
TotalAmt?: number;
Balance?: number;
EmailStatus?: string;
PrintStatus?: string;
CurrencyRef?: QboRef;
Line?: QboLineItem[];
LinkedTxn?: QboLinkedTxn[];
MetaData?: QboMetaData;
}
// Payment
export interface QboPayment {
Id: string;
SyncToken: string;
TxnDate?: string;
CustomerRef?: QboRef;
TotalAmt?: number;
UnappliedAmt?: number;
CurrencyRef?: QboRef;
PaymentMethodRef?: QboRef;
DepositToAccountRef?: QboRef;
Line?: Array<{ Amount: number; LinkedTxn?: QboLinkedTxn[] }>;
MetaData?: QboMetaData;
}
// Deposit
export interface QboDeposit {
Id: string;
SyncToken: string;
TxnDate?: string;
DepositToAccountRef?: QboRef;
TotalAmt?: number;
Line?: QboLineItem[];
MetaData?: QboMetaData;
}
// Purchase (expense/credit card)
export interface QboPurchase {
Id: string;
SyncToken: string;
TxnDate?: string;
DocNumber?: string;
EntityRef?: QboRef & { type?: string };
AccountRef?: QboRef;
TotalAmt?: number;
CurrencyRef?: QboRef;
PrivateNote?: string;
Line?: QboLineItem[];
MetaData?: QboMetaData;
}
// Journal Entry
export interface QboJournalEntry {
Id: string;
SyncToken: string;
TxnDate?: string;
DocNumber?: string;
TotalAmt?: number;
CurrencyRef?: QboRef;
PrivateNote?: string;
Line?: QboLineItem[];
MetaData?: QboMetaData;
}
// Report (P&L, Balance Sheet)
export interface QboReport {
Header?: {
ReportName?: string;
StartPeriod?: string;
EndPeriod?: string;
Time?: string;
Currency?: string;
ReportBasis?: string;
NoReportData?: string;
[key: string]: any;
};
Columns?: any;
Rows?: any;
}
export interface QboQueryResponse<T> {
QueryResponse: {
[key: string]: T[] | number | undefined;
startPosition?: number;
maxResults?: number;
totalCount?: number;
};
time: string;
}
export interface QboSyncResult {
syncId: string;
realmId: string;
status: 'completed' | 'failed';
startedAt: Date;
completedAt: Date;
duration: number;
entities: QboEntitySyncResult[];
errors: string[];
}
export interface QboEntitySyncResult {
entity: string;
success: boolean;
recordsUpserted: number;
duration: number;
error?: string;
}

View file

@ -28,7 +28,8 @@ export enum WebhookEntityType {
}
/**
* Autotask entities that support webhooks via their REST API
* Entities Autotask actually exposes a webhook REST endpoint for.
* TimeEntries, Tasks, Projects, Contracts return 404 not supported by Autotask.
*/
export const WEBHOOK_SUPPORTED_ENTITIES: WebhookEntityType[] = [
WebhookEntityType.COMPANIES,
@ -115,6 +116,10 @@ const ENTITY_TYPE_MAP: Record<string, WebhookEntityType> = {
'InstalledProduct': WebhookEntityType.CONFIGURATION_ITEMS, // Autotask actual payload name
'Ticket': WebhookEntityType.TICKETS,
'TicketNote': WebhookEntityType.TICKET_NOTES,
'TimeEntry': WebhookEntityType.TIME_ENTRIES,
'Task': WebhookEntityType.TASKS,
'Project': WebhookEntityType.PROJECTS,
'Contract': WebhookEntityType.CONTRACTS,
};
/**