mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Buildium GL sync: keep import-mode bank registers current
Root cause of 'syncs stop at 5/31': the nightly buildium-gl-sync writes journal entries (so reports stay current) but never created the matching bank-register transactions for import-mode companies (gl_auto_post=false). Those registers were materialized once at import, so the reconciliation/register views froze ~5/31 while the GL kept advancing. - Add accounting.transactions.journal_entry_line_id (FK + unique index) to link register rows to their source GL line, making materialization idempotent - buildium-gl-sync now materializes a register transaction for each bank line it inserts, for import-mode companies only (bank debit -> deposit/credit, bank credit -> withdrawal/debit; category/coa from the single offset account), upserting on journal_entry_line_id so re-runs never double-insert - One-time backfill (run against prod) filled the existing gap: 231 register rows across Bridgewater/Casuarina/Village Grove/Bent Oak, skipping 3 near-miss rows that share a day with an existing register row (incl. a Casuarina transfer) for manual review gl_managed companies are unaffected — their register drives the GL via post_transaction_gl, not the other way around. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -164,7 +164,7 @@ Deno.serve(async (req) => {
|
||||
// ---- Companies in scope: Buildium-managed = has buildium_gl entries ----
|
||||
const { data: companies, error: cErr } = await supabase
|
||||
.from("companies")
|
||||
.select("id, name, association_id, acmacc_sync_config")
|
||||
.select("id, name, association_id, acmacc_sync_config, gl_auto_post")
|
||||
.not("association_id", "is", null);
|
||||
if (cErr) throw cErr;
|
||||
|
||||
@@ -331,11 +331,23 @@ Deno.serve(async (req) => {
|
||||
// flagged for the GL Account Map UI, and re-pulled once mapped.
|
||||
const { data: localAccounts, error: aErr } = await supabase
|
||||
.from("accounts")
|
||||
.select("id, type")
|
||||
.select("id, type, is_bank, name")
|
||||
.eq("company_id", company.id);
|
||||
if (aErr) throw aErr;
|
||||
const localById = new Map<string, any>();
|
||||
for (const a of localAccounts || []) localById.set(a.id, a);
|
||||
const bankIds = new Set<string>();
|
||||
const acctNameById = new Map<string, string>();
|
||||
for (const a of localAccounts || []) {
|
||||
localById.set(a.id, a);
|
||||
acctNameById.set(a.id, a.name);
|
||||
if (a.is_bank) bankIds.add(a.id);
|
||||
}
|
||||
// Import-mode companies (gl_auto_post=false) don't post Banking entries
|
||||
// to the GL; their bank registers are fed only from the GL pull. Mirror
|
||||
// each inserted bank line into accounting.transactions so the registers
|
||||
// and reconciliation stay current. gl_managed companies skip this — the
|
||||
// register is their source of truth and post_transaction_gl drives the GL.
|
||||
const materializeRegister = company.gl_auto_post === false;
|
||||
|
||||
const { data: linkRows, error: lkErr } = await pub
|
||||
.from("buildium_gl_account_links")
|
||||
@@ -427,15 +439,48 @@ Deno.serve(async (req) => {
|
||||
}
|
||||
throw jeErr;
|
||||
}
|
||||
const { error: lErr } = await supabase
|
||||
const { data: insertedLines, error: lErr } = await supabase
|
||||
.from("journal_entry_lines")
|
||||
.insert(lineRows.map((l) => ({ ...l, journal_entry_id: je.id })));
|
||||
.insert(lineRows.map((l) => ({ ...l, journal_entry_id: je.id })))
|
||||
.select("id, account_id, debit, credit");
|
||||
if (lErr) {
|
||||
// Don't leave a headerless entry behind.
|
||||
await supabase.from("journal_entries").delete().eq("id", je.id);
|
||||
throw lErr;
|
||||
}
|
||||
companyResult.inserted += 1;
|
||||
|
||||
// ---- Materialize bank register rows (import-mode companies) ----
|
||||
if (materializeRegister) {
|
||||
const lines = insertedLines || [];
|
||||
const offsetIds = [...new Set(lines.filter((l: any) => !bankIds.has(l.account_id)).map((l: any) => l.account_id))];
|
||||
const offsetId = offsetIds.length === 1 ? offsetIds[0] : null;
|
||||
const offsetName = offsetId ? (acctNameById.get(offsetId) ?? null) : null;
|
||||
const regRows = lines
|
||||
.filter((l: any) => bankIds.has(l.account_id))
|
||||
.map((l: any) => {
|
||||
const moneyIn = Number(l.debit) > 0; // bank debit = deposit (money in)
|
||||
return {
|
||||
company_id: company.id,
|
||||
account_id: l.account_id,
|
||||
date: tx.date || today,
|
||||
description: tx.description || "Buildium GL",
|
||||
amount: moneyIn ? Number(l.debit) : Number(l.credit),
|
||||
type: moneyIn ? "credit" : "debit", // register: credit = deposit, debit = withdrawal
|
||||
category: offsetName,
|
||||
coa_account_id: offsetId,
|
||||
cleared: false,
|
||||
journal_entry_line_id: l.id,
|
||||
};
|
||||
});
|
||||
if (regRows.length > 0) {
|
||||
const { error: regErr } = await supabase
|
||||
.from("transactions")
|
||||
.upsert(regRows, { onConflict: "journal_entry_line_id", ignoreDuplicates: true });
|
||||
if (regErr) companyResult.errors.push(`register materialize (tx ${txId}): ${regErr.message}`);
|
||||
else companyResult.register_rows = (companyResult.register_rows ?? 0) + regRows.length;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
companyResult.errors.push(`tx ${txId}: ${e?.message || String(e)}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user