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
This commit is contained in:
parent
b638189cb0
commit
672f17b7f9
35 changed files with 2801 additions and 92 deletions
|
|
@ -62,8 +62,9 @@ export class QboSyncService {
|
|||
updatedSince = await this.getLastSyncTime();
|
||||
}
|
||||
|
||||
// Sync invoices
|
||||
entities.push(await this.syncInvoices(realmId, updatedSince));
|
||||
// Sync invoices. Pass syncType so a full sync can tombstone any
|
||||
// rows QBO no longer returns (voided / deleted there).
|
||||
entities.push(await this.syncInvoices(realmId, updatedSince, syncType));
|
||||
|
||||
// Sync payments
|
||||
entities.push(await this.syncPayments(realmId, updatedSince));
|
||||
|
|
@ -111,14 +112,20 @@ export class QboSyncService {
|
|||
|
||||
// ─── Entity Sync Methods ───────────────────────────────────────────────────
|
||||
|
||||
private async syncInvoices(realmId: string, updatedSince?: Date): Promise<QboEntitySyncResult> {
|
||||
private async syncInvoices(
|
||||
realmId: string,
|
||||
updatedSince?: Date,
|
||||
syncType: 'full' | 'incremental' = 'incremental',
|
||||
): Promise<QboEntitySyncResult> {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const invoices = await this.client.getInvoices(updatedSince);
|
||||
console.log(`[QboSync] Fetched ${invoices.length} invoices`);
|
||||
|
||||
let upserted = 0;
|
||||
const seenIds: string[] = [];
|
||||
for (const inv of invoices) {
|
||||
if (inv.Id) seenIds.push(inv.Id);
|
||||
const status = this.deriveInvoiceStatus(inv);
|
||||
await postgresClient.query(
|
||||
`INSERT INTO qbo_invoices
|
||||
|
|
@ -141,7 +148,9 @@ export class QboSyncService {
|
|||
linked_txns = EXCLUDED.linked_txns,
|
||||
sync_token = EXCLUDED.sync_token,
|
||||
qbo_updated_at = EXCLUDED.qbo_updated_at,
|
||||
synced_at = NOW()`,
|
||||
synced_at = NOW(),
|
||||
is_deleted = false,
|
||||
deleted_at = NULL`,
|
||||
[
|
||||
inv.Id, realmId, inv.DocNumber ?? null,
|
||||
inv.TxnDate ?? null, inv.DueDate ?? null,
|
||||
|
|
@ -159,7 +168,40 @@ export class QboSyncService {
|
|||
upserted++;
|
||||
}
|
||||
|
||||
return { entity: 'invoices', success: true, recordsUpserted: upserted, duration: Date.now() - start };
|
||||
// Tombstone pass — full syncs only.
|
||||
//
|
||||
// QBO's Invoice query never reports deletions; voided/deleted invoices
|
||||
// just stop appearing. A full sync just fetched every live invoice
|
||||
// for this realm, so any Pulse row not in `seenIds` is an orphan and
|
||||
// should be soft-deleted so it stops counting toward A/R.
|
||||
//
|
||||
// Incremental syncs are skipped here — they only see recently-changed
|
||||
// rows, so applying the same logic would tombstone the entire ledger.
|
||||
let tombstoned = 0;
|
||||
if (syncType === 'full' && seenIds.length > 0) {
|
||||
const tombstoneRes = await postgresClient.query<{ id: string }>(
|
||||
`UPDATE qbo_invoices
|
||||
SET is_deleted = true,
|
||||
deleted_at = NOW()
|
||||
WHERE realm_id = $1
|
||||
AND is_deleted = false
|
||||
AND id <> ALL($2::text[])
|
||||
RETURNING id`,
|
||||
[realmId, seenIds],
|
||||
);
|
||||
tombstoned = tombstoneRes.rowCount ?? tombstoneRes.rows.length;
|
||||
if (tombstoned > 0) {
|
||||
console.log(`[QboSync] Tombstoned ${tombstoned} invoice(s) absent from full-sync response`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
entity: 'invoices',
|
||||
success: true,
|
||||
recordsUpserted: upserted,
|
||||
recordsDeleted: tombstoned,
|
||||
duration: Date.now() - start,
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[QboSync] Invoice sync failed: ${msg}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue