mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
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:
@@ -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;
|
||||||
Reference in New Issue
Block a user