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>();
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);
// 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[] = [];
let vendorHits = 0, customerHits = 0;
let vendorHits = 0, customerHits = 0, categoryHits = 0;
for (const tx of txns) {
const id = String(tx?.Id ?? "");
if (!id) continue;
@@ -323,14 +327,30 @@ Deno.serve(async (req) => {
if (payeeType === "Vendor" && name) vendor_id = vendorByName.get(norm(name)) ?? null;
const unitNo = tx?.UnitNumber;
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") {
// fall through — name match handled in SQL if needed; leave null here
// Category: the dominant income/expense GL account across the journal
// 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 (customer_id) customerHits++;
if (name || vendor_id || customer_id)
rows.push({ company_id: companyId, external_id: id, party_name: name, vendor_id, customer_id, party_type: payeeType || null });
if (coa_account_id) categoryHits++;
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.
let staged = 0;
@@ -340,7 +360,7 @@ Deno.serve(async (req) => {
if (error) throw error;
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.