Bills import: A/P cutover guard — only post JEs dated after the GL watermark

Anything on/before buildium_gl.last_synced_date is already in the books as
buildium_gl entries; ap_cutover_date freezes that boundary so direct
buildium_bill/billpay postings never double-count history.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 18:46:24 -04:00
parent 25064d8418
commit cc5f70bc5b
+29 -7
View File
@@ -2207,11 +2207,21 @@ Deno.serve(async (req) => {
return m; return m;
} }
const companyByAssoc = new Map<string, { id: string; gl_auto_post: boolean } | null>(); const companyByAssoc = new Map<string, { id: string; gl_auto_post: boolean; apCutover: string | null } | null>();
async function getCompanyForAssoc(assocLocalId: string) { async function getCompanyForAssoc(assocLocalId: string) {
if (companyByAssoc.has(assocLocalId)) return companyByAssoc.get(assocLocalId)!; if (companyByAssoc.has(assocLocalId)) return companyByAssoc.get(assocLocalId)!;
const { data } = await acct.from("companies").select("id, gl_auto_post").eq("association_id", assocLocalId).maybeSingle(); const { data } = await acct.from("companies").select("id, gl_auto_post, acmacc_sync_config").eq("association_id", assocLocalId).maybeSingle();
const out = data ? { id: data.id as string, gl_auto_post: data.gl_auto_post !== false } : null; // Cutover: anything dated on/before the GL pull's watermark is already
// in the books as buildium_gl journal entries — the direct import must
// only post A/P dated AFTER it, or we'd double count the history.
const cfg = (data?.acmacc_sync_config ?? {}) as Record<string, any>;
const out = data
? {
id: data.id as string,
gl_auto_post: data.gl_auto_post !== false,
apCutover: (cfg?.buildium_gl?.ap_cutover_date ?? cfg?.buildium_gl?.last_synced_date ?? null) as string | null,
}
: null;
companyByAssoc.set(assocLocalId, out); companyByAssoc.set(assocLocalId, out);
return out; return out;
} }
@@ -2533,7 +2543,7 @@ Deno.serve(async (req) => {
|| [buildiumVendor?.FirstName, buildiumVendor?.LastName].filter(Boolean).join(" ").trim() || "Vendor"; || [buildiumVendor?.FirstName, buildiumVendor?.LastName].filter(Boolean).join(" ").trim() || "Vendor";
const billDesc = `${vendorDisplayName}${invoiceNumber ? ` Inv # ${invoiceNumber}` : ""}`; const billDesc = `${vendorDisplayName}${invoiceNumber ? ` Inv # ${invoiceNumber}` : ""}`;
if (company && !company.gl_auto_post && billLineItems.length > 0) { if (company && !company.gl_auto_post && billLineItems.length > 0 && (!company.apCutover || billDate > company.apCutover)) {
const ap = await getApAccount(company.id); const ap = await getApAccount(company.id);
if (ap) { if (ap) {
const ok = await postDirectJe( const ok = await postDirectJe(
@@ -2562,6 +2572,8 @@ Deno.serve(async (req) => {
const pAmount = Array.isArray(p.Lines) ? p.Lines.reduce((s: number, l: any) => s + Number(l?.Amount || 0), 0) : 0; const pAmount = Array.isArray(p.Lines) ? p.Lines.reduce((s: number, l: any) => s + Number(l?.Amount || 0), 0) : 0;
if (!pAmount) continue; if (!pAmount) continue;
const pDate = String(p.EntryDate || billDate).slice(0, 10); const pDate = String(p.EntryDate || billDate).slice(0, 10);
// Payments on/before the GL watermark already came in via buildium_gl
if (company.apCutover && pDate <= company.apCutover) continue;
const checkNo = p.CheckNumber ? String(p.CheckNumber) : null; const checkNo = p.CheckNumber ? String(p.CheckNumber) : null;
const acctBill = await getAcctBill(finalBillId, company.id); const acctBill = await getAcctBill(finalBillId, company.id);
await postPaymentSide(assocLocalId, company, pid, pDate, billDesc, checkNo, p.BankAccountId ?? null, pAmount, acctBill?.id ?? null, acctBill?.vendor_id ?? null); await postPaymentSide(assocLocalId, company, pid, pDate, billDesc, checkNo, p.BankAccountId ?? null, pAmount, acctBill?.id ?? null, acctBill?.vendor_id ?? null);
@@ -2689,7 +2701,7 @@ Deno.serve(async (req) => {
} }
const company = await getCompanyForAssoc(assocLocalId); const company = await getCompanyForAssoc(assocLocalId);
if (company && !company.gl_auto_post && pubBillId) { if (company && !company.gl_auto_post && pubBillId && (!company.apCutover || ckDate > company.apCutover)) {
const ap = await getApAccount(company.id); const ap = await getApAccount(company.id);
if (ap) { if (ap) {
const ok = await postDirectJe(company.id, "buildium_bill", externalKey, ckDate, desc, checkNo, [ const ok = await postDirectJe(company.id, "buildium_bill", externalKey, ckDate, desc, checkNo, [
@@ -2729,9 +2741,19 @@ Deno.serve(async (req) => {
for (const companyId of directPostedCompanies) { for (const companyId of directPostedCompanies) {
const { data: comp } = await acct.from("companies").select("acmacc_sync_config").eq("id", companyId).maybeSingle(); const { data: comp } = await acct.from("companies").select("acmacc_sync_config").eq("id", companyId).maybeSingle();
const cfg = (comp?.acmacc_sync_config ?? {}) as Record<string, any>; const cfg = (comp?.acmacc_sync_config ?? {}) as Record<string, any>;
if (cfg?.buildium_gl?.exclude_ap) continue; if (cfg?.buildium_gl?.exclude_ap && cfg?.buildium_gl?.ap_cutover_date) continue;
await acct.from("companies").update({ await acct.from("companies").update({
acmacc_sync_config: { ...cfg, buildium_gl: { ...(cfg.buildium_gl ?? {}), exclude_ap: true } }, acmacc_sync_config: {
...cfg,
buildium_gl: {
...(cfg.buildium_gl ?? {}),
exclude_ap: true,
// Freeze the cutover so the boundary never moves as the GL
// watermark advances: A/P <= cutover lives in buildium_gl
// entries, A/P > cutover comes from the direct import.
ap_cutover_date: cfg?.buildium_gl?.ap_cutover_date ?? cfg?.buildium_gl?.last_synced_date ?? "1900-01-01",
},
},
}).eq("id", companyId); }).eq("id", companyId);
} }