Accounting: categorize register transactions from Buildium GL lines

partymap mode now also resolves each Buildium transaction's dominant
income/expense account (from its Journal lines) to a local category, staged
alongside the party. Used to backfill accounting.transactions.category on the
materialized bank register. Applied to Village Woods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 20:16:00 -04:00
parent 0faee9994d
commit 9aa1f94eb4
2 changed files with 34 additions and 7 deletions
@@ -310,9 +310,13 @@ Deno.serve(async (req) => {
const customerByUnitNo = new Map<string, string>(); const customerByUnitNo = new Map<string, string>();
for (const c of (await supabase.schema("accounting").from("customers").select("id,unit_number").eq("company_id", companyId)).data ?? []) for (const c of (await supabase.schema("accounting").from("customers").select("id,unit_number").eq("company_id", companyId)).data ?? [])
if (c.unit_number) customerByUnitNo.set(norm(c.unit_number), c.id); if (c.unit_number) customerByUnitNo.set(norm(c.unit_number), c.id);
// Local income/expense accounts by code → {id,name}, for the category.
const acctByCode = new Map<string, { id: string; name: string }>();
for (const a of (await supabase.schema("accounting").from("accounts").select("id,code,name,type").eq("company_id", companyId)).data ?? [])
if (a.code && (a.type === "income" || a.type === "expense")) acctByCode.set(String(a.code).trim(), { id: a.id, name: a.name });
const rows: any[] = []; const rows: any[] = [];
let vendorHits = 0, customerHits = 0; let vendorHits = 0, customerHits = 0, categoryHits = 0;
for (const tx of txns) { for (const tx of txns) {
const id = String(tx?.Id ?? ""); const id = String(tx?.Id ?? "");
if (!id) continue; if (!id) continue;
@@ -323,14 +327,30 @@ Deno.serve(async (req) => {
if (payeeType === "Vendor" && name) vendor_id = vendorByName.get(norm(name)) ?? null; if (payeeType === "Vendor" && name) vendor_id = vendorByName.get(norm(name)) ?? null;
const unitNo = tx?.UnitNumber; const unitNo = tx?.UnitNumber;
if (!vendor_id && typeof unitNo === "string" && unitNo.trim()) customer_id = customerByUnitNo.get(norm(unitNo)) ?? null; if (!vendor_id && typeof unitNo === "string" && unitNo.trim()) customer_id = customerByUnitNo.get(norm(unitNo)) ?? null;
// owner payment with no UnitNumber: try the payee name against customers
if (!vendor_id && !customer_id && name && payeeType && payeeType !== "Vendor") { // Category: the dominant income/expense GL account across the journal
// fall through — name match handled in SQL if needed; leave null here // lines (Buildium expands a check/bill/payment to its category lines).
const byCode = new Map<string, number>();
for (const ln of (tx?.Journal?.Lines ?? [])) {
const t = String(ln?.GLAccount?.Type ?? "");
if (t !== "Expense" && t !== "Income") continue;
const code = String(ln?.GLAccount?.AccountNumber ?? "").trim();
if (!code) continue;
byCode.set(code, (byCode.get(code) ?? 0) + Math.abs(Number(ln?.Amount) || 0));
} }
let category_name: string | null = null;
let coa_account_id: string | null = null;
if (byCode.size) {
const topCode = [...byCode.entries()].sort((a, b) => b[1] - a[1])[0][0];
const acct = acctByCode.get(topCode);
if (acct) { category_name = acct.name; coa_account_id = acct.id; }
}
if (vendor_id) vendorHits++; if (vendor_id) vendorHits++;
if (customer_id) customerHits++; if (customer_id) customerHits++;
if (name || vendor_id || customer_id) if (coa_account_id) categoryHits++;
rows.push({ company_id: companyId, external_id: id, party_name: name, vendor_id, customer_id, party_type: payeeType || null }); if (name || vendor_id || customer_id || coa_account_id)
rows.push({ company_id: companyId, external_id: id, party_name: name, vendor_id, customer_id, party_type: payeeType || null, category_name, coa_account_id });
} }
// Upsert in batches. // Upsert in batches.
let staged = 0; let staged = 0;
@@ -340,7 +360,7 @@ Deno.serve(async (req) => {
if (error) throw error; if (error) throw error;
staged += batch.length; staged += batch.length;
} }
return json({ mode, company: company.name, window: [dateFrom, dateTo], txns: txns.length, staged, vendorHits, customerHits }); return json({ mode, company: company.name, window: [dateFrom, dateTo], txns: txns.length, staged, vendorHits, customerHits, categoryHits });
} }
// Load this company's imported entries and compute the new description. // Load this company's imported entries and compute the new description.
@@ -0,0 +1,7 @@
-- Extend the payee/payor staging table to also carry the resolved income/expense
-- category (the dominant Journal line of each Buildium GL transaction) and its
-- local account, used to backfill accounting.transactions.category on the
-- materialized bank register of imported-GL companies.
alter table accounting.buildium_party_map
add column if not exists category_name text,
add column if not exists coa_account_id uuid;