Accounting: add account/txn detail modes to buildium-payee-backfill

account mode lists a single account's ledger entries; txn mode reconstructs
one transaction's full double-entry across accounts. Used to trace the VW
5098 discrepancy to a missing 2025-12-31 reserve-funded reclass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 17:27:00 -04:00
parent e999890ee5
commit 12e551f578
@@ -193,6 +193,68 @@ Deno.serve(async (req) => {
return json({ mode, company: company.name, window: [dateFrom, dateTo], distinctTxns: txnIds.size, accounts: rows.length, rows });
}
// account mode: list every /v1/generalledger entry for a single account
// (by AccountNumber) in the window — used to chase per-account discrepancies.
if (mode === "account") {
const wantNum = String(body?.accountNumber ?? "");
let gid = "";
const findGl = (g: any) => {
if (String(g?.AccountNumber ?? "") === wantNum) gid = String(g.Id);
if (Array.isArray(g.SubAccounts)) for (const s of g.SubAccounts) findGl(s);
};
for (const g of glAccounts) findGl(g);
if (!gid) return json({ error: `account number ${wantNum} not found in Buildium chart` }, 404);
const params = new URLSearchParams();
params.set("accountingbasis", "Accrual");
params.set("startdate", dateFrom);
params.set("enddate", dateTo);
params.set("entitytype", "Association");
params.set("entityid", String(bAssocId));
params.append("glaccountids", gid);
const ledgers = await buildiumFetchAll("/v1/generalledger", clientId, clientSecret, params);
const entries: any[] = [];
let net = 0;
for (const ledger of ledgers) for (const e of ledger.Entries ?? []) {
net += Number(e.Amount) || 0;
entries.push({ txnId: String(e.Id ?? ""), date: String(e.Date ?? "").split("T")[0], type: e.TransactionType, amount: Number(e.Amount) || 0, memo: e.Memo ?? e.Description ?? "" });
}
entries.sort((a, b) => a.date.localeCompare(b.date));
return json({ mode, accountNumber: wantNum, gid, window: [dateFrom, dateTo], net: Math.round(net * 100) / 100, count: entries.length, entries });
}
// txn mode: reconstruct one transaction's full double-entry by scanning the
// ledger across all accounts for entries carrying its id.
if (mode === "txn") {
const wantId = String(body?.txnId ?? "");
const numByGid = new Map<string, { number: string; name: string }>();
const collect = (g: any) => {
if (g?.Id) numByGid.set(String(g.Id), { number: String(g.AccountNumber ?? ""), name: String(g.Name ?? "") });
if (Array.isArray(g.SubAccounts)) for (const s of g.SubAccounts) collect(s);
};
for (const g of glAccounts) collect(g);
const lines: any[] = [];
for (let i = 0; i < allGlIds.length; i += CHUNK) {
const params = new URLSearchParams();
params.set("accountingbasis", "Accrual");
params.set("startdate", dateFrom);
params.set("enddate", dateTo);
params.set("entitytype", "Association");
params.set("entityid", String(bAssocId));
for (const id of allGlIds.slice(i, i + CHUNK)) params.append("glaccountids", id);
const ledgers = await buildiumFetchAll("/v1/generalledger", clientId, clientSecret, params);
for (const ledger of ledgers) {
const gid = String(ledger.GLAccountId ?? ledger.GLAccount?.Id ?? "");
for (const e of ledger.Entries ?? []) {
if (String(e.Id ?? "") !== wantId) continue;
const m = numByGid.get(gid) ?? { number: "", name: gid };
lines.push({ account: `${m.number} ${m.name}`.trim(), amount: Number(e.Amount) || 0, date: String(e.Date ?? "").split("T")[0], type: e.TransactionType, memo: e.Memo ?? e.Description ?? "" });
}
}
}
const sum = Math.round(lines.reduce((s, l) => s + l.amount, 0) * 100) / 100;
return json({ mode, txnId: wantId, balanceCheck: sum, lineCount: lines.length, lines });
}
// Pull GL transactions for the window. /v1/generalledger/transactions is the
// Journal view — one row per transaction with its Id, type and party data.
const txById = new Map<string, any>();