Hostinger Reach integration UI + ARC Buildium matching, drop Mailchimp

- HostingerReachPage (replaces MailchimpPage): connect Reach via
  reach-connection, per-association segment sync via reach-sync
- ARC Applications: Buildium import review/matching updates
- buildium-import-stage/apply: latest staging + apply changes (already
  deployed to Supabase)
- migrations: hostinger_reach_integration + arc_finalized_lock service
  role (already applied to live DB)
- CI: note that deployment is VPS-side polling (auto-deploy.sh cron)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 23:07:30 -04:00
parent 220892203c
commit abd46bcb2b
15 changed files with 1041 additions and 726 deletions
@@ -260,10 +260,7 @@ Deno.serve(async (req) => {
const files: Array<{ id: string; name: string }> = Array.isArray(p._arc_files) ? p._arc_files : [];
const buildiumAssocId: string | null = p._arc_buildium_association_id || null;
const buildiumArcId: string | null = p.buildium_arc_request_id || null;
const decisionNotes: string | null = p.decision_notes || null;
const reviewDate: string | null = p.review_date || null;
const deciderName: string | null = p._arc_decider_name || null;
const deciderDate: string | null = p._arc_decider_date || null;
const clean = stripPrivate(p);
@@ -282,27 +279,32 @@ Deno.serve(async (req) => {
appId = ins.id;
}
// Seed a system comment with the Buildium decision (since comments/voters aren't exposed via API)
if (appId && (decisionNotes || deciderName)) {
const { data: existingComment } = await supabase
.from("arc_application_comments")
.select("id")
.eq("application_id", appId)
// Buildium's API exposes no comment threads or per-member votes — only the final decision
// and who recorded it. Surface that decision as a recorded vote in the Committee Review
// (entity_votes is what the ARC review UI reads). The decision text itself already lives in
// arc_applications.decision_notes. Idempotent: re-syncing replaces the prior Buildium vote.
const decisionRaw: string = String(p._arc_decision || "").toLowerCase();
const voteDir: "approve" | "deny" | null = decisionRaw.includes("approve")
? "approve"
: (decisionRaw.includes("den") || decisionRaw.includes("reject") ? "deny" : null);
if (appId && voteDir) {
const voterName = `${deciderName || "Buildium"} (Buildium)`;
await supabase
.from("entity_votes")
.delete()
.eq("entity_type", "arc_application")
.eq("entity_id", appId)
.is("user_id", null)
.ilike("comment", "%[Imported from Buildium]%")
.maybeSingle();
if (!existingComment) {
const seed =
`[Imported from Buildium]\n` +
(deciderName ? `Decision by: ${deciderName}${deciderDate ? ` on ${deciderDate}` : ""}\n` : "") +
(reviewDate && !deciderDate ? `Decision date: ${reviewDate}\n` : "") +
(decisionNotes ? `Decision notes: ${decisionNotes}` : "");
await supabase.from("arc_application_comments").insert({
application_id: appId,
user_id: null,
comment: seed.trim(),
});
}
.ilike("voter_name", "% (Buildium)");
const { error: voteErr } = await supabase.from("entity_votes").insert({
entity_type: "arc_application",
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}`);
}
// Download attached files from Buildium and upload into the arc-files bucket