///
// Answers "did SimpleFIN send it, or did we drop it?" from the raw archive.
//
// Usage:
// deno run -A scripts/diagnose-sync.ts [path-to-quantum.db]
// deno run -A scripts/diagnose-sync.ts [path] --dump
// prints the first few VERBATIM transaction objects the Bridge sent for
// matching accounts (shows whether `extra` carries payee/memo detail).
//
// Against a deployed container, copy the file out first:
// podman cp :/data/quantum.db ./quantum.db
// deno run -A scripts/diagnose-sync.ts ./quantum.db
import { DatabaseSync } from "node:sqlite";
const args = [...Deno.args];
const dumpIndex = args.indexOf("--dump");
const dumpFilter = dumpIndex >= 0
? (args.splice(dumpIndex, 2)[1] ?? "").toLowerCase()
: null;
const path = args[0] ?? "./data/quantum.db";
const db = new DatabaseSync(path);
const fmtDate = (unix: number | null) =>
unix ? new Date(unix * 1000).toISOString().slice(0, 10) : "—";
console.log(`\n=== Sync attempts (latest 5) ===`);
const syncs = db
.prepare(
`SELECT id, fetched_at, ok, error, LENGTH(payload) AS bytes
FROM raw_syncs ORDER BY id DESC LIMIT 5`,
)
.all() as Record[];
for (const s of syncs) {
console.log(
`#${s.id} ${s.fetched_at} ${s.ok ? "ok" : "FAILED"} ${
s.bytes ?? 0
} bytes${s.error ? ` error: ${s.error}` : ""}`,
);
}
const latest = db
.prepare(
"SELECT id, fetched_at, payload FROM raw_syncs WHERE ok = 1 ORDER BY id DESC LIMIT 1",
)
.get() as { id: number; fetched_at: string; payload: string } | undefined;
if (!latest?.payload) {
console.log(
"\nNo successful sync payload archived yet — nothing to compare.",
);
Deno.exit(0);
}
console.log(
`\n=== Latest successful payload (raw_syncs #${latest.id}, ${latest.fetched_at}) ===`,
);
const parsed = JSON.parse(latest.payload) as {
errors?: unknown[];
accounts?: Record[];
};
if (parsed.errors?.length) {
console.log(`Connection errors reported by the Bridge:`);
for (const e of parsed.errors) console.log(` ! ${e}`);
} else {
console.log("No connection errors in payload.");
}
console.log("\nWhat the Bridge sent, per account:");
for (const account of parsed.accounts ?? []) {
const txns = (account.transactions ?? []) as Record[];
const posted = txns
.map((t) => (typeof t.posted === "number" ? t.posted : null))
.filter((p): p is number => p != null && p > 0);
const range = posted.length
? `${fmtDate(Math.min(...posted))} … ${fmtDate(Math.max(...posted))}`
: "no posted dates";
console.log(
` ${String(account.id).padEnd(28)} ${String(account.name).padEnd(24)} ` +
`${String(txns.length).padStart(4)} txns in payload (${range})`,
);
}
console.log("\nWhat Quantum has normalized, per account:");
const rows = db
.prepare(
`SELECT a.id, COALESCE(a.display_name, a.name) AS name, a.state,
COUNT(t.id) AS n, MIN(t.posted) AS min_posted, MAX(t.posted) AS max_posted
FROM accounts a LEFT JOIN transactions t ON t.account_id = a.id AND t.removed_at IS NULL
GROUP BY a.id ORDER BY a.id`,
)
.all() as Record[];
for (const r of rows) {
console.log(
` ${String(r.id).padEnd(28)} ${String(r.name).padEnd(24)} ` +
`${String(r.n).padStart(4)} txns in db ` +
`(${fmtDate(r.min_posted as number | null)} … ${
fmtDate(r.max_posted as number | null)
}) [${r.state}]`,
);
}
if (dumpFilter) {
console.log(
`\n=== Verbatim transactions for accounts matching "${dumpFilter}" ===`,
);
for (const account of parsed.accounts ?? []) {
const label = `${account.id} ${account.name}`.toLowerCase();
if (!label.includes(dumpFilter)) continue;
console.log(`\n--- ${account.name} (${account.id}) ---`);
const txns = (account.transactions ?? []) as Record[];
for (const txn of txns.slice(0, 5)) {
console.log(JSON.stringify(txn, null, 2));
}
if (txns.length > 5) console.log(`… and ${txns.length - 5} more`);
}
}
console.log(`\nReading the result:
- Account has txns in payload but 0 in db -> Quantum bug; please report.
- Account has 0 txns in payload -> the provider isn't delivering
transactions for that institution (common for investment/savings accounts
via some aggregators). Check the connection at SimpleFIN Bridge.
- FAILED sync rows with a normalization error -> a malformed amount or
payload; the error text says which.`);
db.close();