ARC imports: record Buildium decision votes + richer decision notes; PDF spacing

- Buildium API exposes no ARC comment threads or member votes (verified live);
  surface the final decision instead: vote row dated to the actual decision,
  decision_notes 'Approved by X on date (via Buildium)'
- record_buildium_arc_vote RPC bypasses the finalized-ARC write lock that was
  silently swallowing import votes; 69 existing imports backfilled
- Application Record PDF: paragraph break between comments and decision notes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 14:26:48 -04:00
parent ff65c8a656
commit c1fad194f7
3 changed files with 22 additions and 19 deletions
+2 -1
View File
@@ -231,8 +231,9 @@ export async function generateArcApplicationRecord(application: any) {
} }
} }
// Decision notes // Decision notes — full paragraph break after the comments thread
if (application.decision_notes) { if (application.decision_notes) {
y += 14;
if (y > pageHeight - 100) { doc.addPage(); y = 40; } if (y > pageHeight - 100) { doc.addPage(); y = 40; }
doc.setFont("helvetica", "bold"); doc.setFont("helvetica", "bold");
doc.setFontSize(11); doc.setFontSize(11);
@@ -261,6 +261,7 @@ Deno.serve(async (req) => {
const buildiumAssocId: string | null = p._arc_buildium_association_id || null; const buildiumAssocId: string | null = p._arc_buildium_association_id || null;
const buildiumArcId: string | null = p.buildium_arc_request_id || null; const buildiumArcId: string | null = p.buildium_arc_request_id || null;
const deciderName: string | null = p._arc_decider_name || null; const deciderName: string | null = p._arc_decider_name || null;
const deciderDate: string | null = p._arc_decider_date || null;
const clean = stripPrivate(p); const clean = stripPrivate(p);
@@ -288,21 +289,16 @@ Deno.serve(async (req) => {
? "approve" ? "approve"
: (decisionRaw.includes("den") || decisionRaw.includes("reject") ? "deny" : null); : (decisionRaw.includes("den") || decisionRaw.includes("reject") ? "deny" : null);
if (appId && voteDir) { if (appId && voteDir) {
const voterName = `${deciderName || "Buildium"} (Buildium)`; // Finalized ARC apps are write-locked (prevent_writes_on_locked_arc),
await supabase // and Buildium imports arrive already finalized — so the vote goes
.from("entity_votes") // through the import RPC, which bypasses the lock for this one write.
.delete() const { error: voteErr } = await supabase.rpc("record_buildium_arc_vote", {
.eq("entity_type", "arc_application") p_application_id: appId,
.eq("entity_id", appId) p_vote: voteDir,
.is("user_id", null) p_voter_name: `${deciderName || "Buildium"} (Buildium)`,
.ilike("voter_name", "% (Buildium)"); // Date the vote when the decision was actually made in Buildium,
const { error: voteErr } = await supabase.from("entity_votes").insert({ // not when the import ran.
entity_type: "arc_application", p_vote_date: deciderDate,
entity_id: appId,
vote: voteDir,
user_id: null,
voter_name: voterName,
recorded_by: null,
}); });
if (voteErr) console.warn(`ARC vote record failed for ${appId}: ${voteErr.message}`); if (voteErr) console.warn(`ARC vote record failed for ${appId}: ${voteErr.message}`);
} }
@@ -519,9 +519,6 @@ Deno.serve(async (req) => {
const decisionDate = isFinalDecision const decisionDate = isFinalDecision
? (String(r.DecisionDateTime || r.LastUpdatedDateTime || "").split("T")[0] || null) ? (String(r.DecisionDateTime || r.LastUpdatedDateTime || "").split("T")[0] || null)
: null; : null;
const decisionNotes = r.DecisionDescription
? String(r.DecisionDescription)
: (r.Decision && !/pending/i.test(String(r.Decision)) ? `Decision: ${r.Decision}` : null);
const status = mapArcStatus(r.Status, r.Decision); const status = mapArcStatus(r.Status, r.Decision);
// Buildium exposes the user who last touched the request — use as best-available "decider" // Buildium exposes the user who last touched the request — use as best-available "decider"
@@ -533,6 +530,15 @@ Deno.serve(async (req) => {
?.toString() ?.toString()
.split("T")[0] || null; .split("T")[0] || null;
// Decision notes: Buildium's API has no comment thread, so record the
// richest decision summary it does expose (who + when). Keep the format
// in sync with the SQL backfill so re-staging doesn't flap diffs.
const decisionNotes = r.DecisionDescription
? String(r.DecisionDescription)
: (r.Decision && !/pending/i.test(String(r.Decision))
? `${r.Decision} by ${deciderName || "Buildium"}${deciderDate ? ` on ${deciderDate}` : ""} (via Buildium)`
: null);
// Optional file pull // Optional file pull
let files: Array<{ id: string; name: string; download_url: string | null }> = []; let files: Array<{ id: string; name: string; download_url: string | null }> = [];
if (includeArcFiles) { if (includeArcFiles) {