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 ----
|
// ---- Companies in scope: Buildium-managed = has buildium_gl entries ----
|
||||||
const { data: companies, error: cErr } = await supabase
|
const { data: companies, error: cErr } = await supabase
|
||||||
.from("companies")
|
.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);
|
.not("association_id", "is", null);
|
||||||
if (cErr) throw cErr;
|
if (cErr) throw cErr;
|
||||||
|
|
||||||
@@ -331,11 +331,23 @@ Deno.serve(async (req) => {
|
|||||||
// flagged for the GL Account Map UI, and re-pulled once mapped.
|
// flagged for the GL Account Map UI, and re-pulled once mapped.
|
||||||
const { data: localAccounts, error: aErr } = await supabase
|
const { data: localAccounts, error: aErr } = await supabase
|
||||||
.from("accounts")
|
.from("accounts")
|
||||||
.select("id, type")
|
.select("id, type, is_bank, name")
|
||||||
.eq("company_id", company.id);
|
.eq("company_id", company.id);
|
||||||
if (aErr) throw aErr;
|
if (aErr) throw aErr;
|
||||||
const localById = new Map<string, any>();
|
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
|
const { data: linkRows, error: lkErr } = await pub
|
||||||
.from("buildium_gl_account_links")
|
.from("buildium_gl_account_links")
|
||||||
@@ -427,15 +439,48 @@ Deno.serve(async (req) => {
|
|||||||
}
|
}
|
||||||
throw jeErr;
|
throw jeErr;
|
||||||
}
|
}
|
||||||
const { error: lErr } = await supabase
|
const { data: insertedLines, error: lErr } = await supabase
|
||||||
.from("journal_entry_lines")
|
.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) {
|
if (lErr) {
|
||||||
// Don't leave a headerless entry behind.
|
// Don't leave a headerless entry behind.
|
||||||
await supabase.from("journal_entries").delete().eq("id", je.id);
|
await supabase.from("journal_entries").delete().eq("id", je.id);
|
||||||
throw lErr;
|
throw lErr;
|
||||||
}
|
}
|
||||||
companyResult.inserted += 1;
|
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) {
|
} catch (e: any) {
|
||||||
companyResult.errors.push(`tx ${txId}: ${e?.message || String(e)}`);
|
companyResult.errors.push(`tx ${txId}: ${e?.message || String(e)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Link bank-register transactions back to the GL line that produced them, so
|
||||||
|
-- the Buildium GL pull can materialize register rows idempotently for
|
||||||
|
-- import-mode companies (gl_auto_post = false). NULL = manually-entered or
|
||||||
|
-- legacy register row with no GL source. NULLs are distinct, so many unlinked
|
||||||
|
-- rows coexist; the unique index only prevents materializing the same GL line
|
||||||
|
-- into the register twice (and is usable as an ON CONFLICT target).
|
||||||
|
alter table accounting.transactions
|
||||||
|
add column if not exists journal_entry_line_id uuid
|
||||||
|
references accounting.journal_entry_lines(id) on delete set null;
|
||||||
|
|
||||||
|
create unique index if not exists transactions_jel_id_uidx
|
||||||
|
on accounting.transactions(journal_entry_line_id);
|
||||||
Reference in New Issue
Block a user