diff --git a/supabase/functions/buildium-gl-sync/index.ts b/supabase/functions/buildium-gl-sync/index.ts index 7f525ef..afa683b 100644 --- a/supabase/functions/buildium-gl-sync/index.ts +++ b/supabase/functions/buildium-gl-sync/index.ts @@ -164,7 +164,7 @@ Deno.serve(async (req) => { // ---- Companies in scope: Buildium-managed = has buildium_gl entries ---- const { data: companies, error: cErr } = await supabase .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); if (cErr) throw cErr; @@ -331,11 +331,23 @@ Deno.serve(async (req) => { // flagged for the GL Account Map UI, and re-pulled once mapped. const { data: localAccounts, error: aErr } = await supabase .from("accounts") - .select("id, type") + .select("id, type, is_bank, name") .eq("company_id", company.id); if (aErr) throw aErr; const localById = new Map(); - for (const a of localAccounts || []) localById.set(a.id, a); + const bankIds = new Set(); + const acctNameById = new Map(); + 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 .from("buildium_gl_account_links") @@ -427,15 +439,48 @@ Deno.serve(async (req) => { } throw jeErr; } - const { error: lErr } = await supabase + const { data: insertedLines, error: lErr } = await supabase .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) { // Don't leave a headerless entry behind. await supabase.from("journal_entries").delete().eq("id", je.id); throw lErr; } 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) { companyResult.errors.push(`tx ${txId}: ${e?.message || String(e)}`); } diff --git a/supabase/migrations/20260613120000_transactions_journal_entry_line_link.sql b/supabase/migrations/20260613120000_transactions_journal_entry_line_link.sql new file mode 100644 index 0000000..89d477d --- /dev/null +++ b/supabase/migrations/20260613120000_transactions_journal_entry_line_link.sql @@ -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);