Buildium -> Accounting: auto-provision companies + opening-balance migration

- buildium-sync now ensures every active association has an accounting company
  after syncing associations. Once it exists, the existing DB triggers flow
  units -> customers, owner ledger -> A/R + income, and bills -> A/P + expense
  into Accounting automatically (closing the gap where Buildium synced only the
  main dashboard, not Accounting).
- New buildium-opening-balances function: fetches an association's Buildium GL
  trial balance as of a cutoff (default 2025-12-31, Accrual), maps GL accounts
  to accounting accounts (flagging bank accounts), rolls prior-year P&L into
  Retained Earnings, writes accounting.opening_balances, and posts the opening
  GL entry. Idempotent; service-role gated.

Applied to 6 Buildium associations (opening balances + 2026 activity); all
balance. New columns/data applied to the project directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 02:31:21 -04:00
parent 5436396a23
commit f7dc5d8177
2 changed files with 140 additions and 1 deletions
+24 -1
View File
@@ -656,7 +656,30 @@ Deno.serve(async (req) => {
const { error } = await supabase.from("associations").upsert(upserts, { onConflict: "name", ignoreDuplicates: true });
if (error) throw new Error(`Associations upsert failed: ${error.message}`);
}
results.associations = { fetched: associations.length, upserted: upserts.length };
// Ensure every active association has an accounting company. Once it exists,
// the existing DB triggers automatically flow units -> customers, owner
// ledger -> A/R + income, and bills -> A/P + expense into Accounting.
// (Opening balances are a separate one-time migration: buildium-opening-balances.)
let accountingCompaniesCreated = 0;
try {
const { data: activeAssocs } = await supabase.from("associations").select("id, name").eq("status", "active");
const acctSchema = (supabase as any).schema("accounting");
const { data: existingCompanies } = await acctSchema.from("companies").select("association_id");
const haveCompany = new Set((existingCompanies || []).map((c: any) => c.association_id));
const toCreate = (activeAssocs || []).filter((a: any) => a.id && !haveCompany.has(a.id));
if (toCreate.length > 0) {
const { error: compErr } = await acctSchema.from("companies").insert(
toCreate.map((a: any) => ({ association_id: a.id, name: a.name || "Association", created_by: userId }))
);
if (compErr) console.warn(`Accounting company provisioning failed: ${compErr.message}`);
else accountingCompaniesCreated = toCreate.length;
}
} catch (e) {
console.warn("Accounting company provisioning error:", (e as Error)?.message);
}
results.associations = { fetched: associations.length, upserted: upserts.length, accounting_companies_created: accountingCompaniesCreated };
}
if (syncType === "all" || syncType === "units") {