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:
lorentz 2026-07-18 06:34:57 -04:00
parent b638189cb0
commit 672f17b7f9
35 changed files with 2801 additions and 92 deletions

View file

@ -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}`);