Add ACMCC app source, Supabase backend, and project config

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 20:19:26 -04:00
parent 313b51b412
commit 183fe0a93c
1422 changed files with 259271 additions and 0 deletions
@@ -0,0 +1,121 @@
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.99.1";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
// Compute next run date from a base date and frequency, ensuring result > today.
function computeNextRunDate(baseISO: string, frequency: string, today: Date): string {
const d = new Date(baseISO + "T12:00:00");
const addByFreq = (date: Date) => {
switch (frequency) {
case "monthly": date.setMonth(date.getMonth() + 1); break;
case "quarterly": date.setMonth(date.getMonth() + 3); break;
case "semi-annual": date.setMonth(date.getMonth() + 6); break;
case "annual": date.setFullYear(date.getFullYear() + 1); break;
default: date.setMonth(date.getMonth() + 1);
}
};
// Advance until strictly after today
while (d <= today) addByFreq(d);
return d.toISOString().split("T")[0];
}
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });
try {
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const supabase = createClient(supabaseUrl, serviceKey);
const today = new Date();
const todayISO = today.toISOString().split("T")[0];
// Find all rules due to run
const { data: dueRules, error: rulesErr } = await supabase
.from("association_fee_rules")
.select("*")
.eq("auto_post_enabled", true)
.lte("next_run_date", todayISO);
if (rulesErr) throw rulesErr;
if (!dueRules || dueRules.length === 0) {
return new Response(JSON.stringify({ processed: 0, posted: 0 }), {
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
let totalPosted = 0;
const results: any[] = [];
for (const rule of dueRules) {
const associationId = rule.association_id;
// Load units + active owners for this association
const [{ data: units }, { data: owners }] = await Promise.all([
supabase
.from("units")
.select("id, monthly_assessment, assessment_amount_one, assessment_account_id")
.eq("association_id", associationId),
supabase
.from("owners")
.select("id, unit_id")
.eq("association_id", associationId)
.eq("status", "active"),
]);
const ownerByUnit = new Map<string, string>();
(owners || []).forEach((o: any) => { if (o.unit_id) ownerByUnit.set(o.unit_id, o.id); });
const entries = (units || []).flatMap((u: any) => {
const ownerId = ownerByUnit.get(u.id);
if (!ownerId) return [];
const amount = u.assessment_amount_one ?? u.monthly_assessment ?? rule.default_assessment_amount ?? 0;
if (!amount || amount <= 0) return [];
return [{
association_id: associationId,
owner_id: ownerId,
unit_id: u.id,
date: todayISO,
credit: 0,
debit: amount,
transaction_type: "assessment",
description: "Assessment (auto-posted)",
}];
});
let posted = 0;
if (entries.length) {
const { error: insErr } = await supabase.from("owner_ledger_entries").insert(entries);
if (insErr) {
console.error(`Failed posting for association ${associationId}:`, insErr);
results.push({ associationId, error: insErr.message });
continue;
}
posted = entries.length;
totalPosted += posted;
}
// Advance the cycle
const newNext = computeNextRunDate(rule.next_run_date || todayISO, rule.assessment_frequency || "monthly", today);
await supabase
.from("association_fee_rules")
.update({ last_run_date: todayISO, next_run_date: newNext })
.eq("id", rule.id);
results.push({ associationId, posted, next_run_date: newNext });
}
return new Response(JSON.stringify({ processed: dueRules.length, posted: totalPosted, results }), {
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
} catch (err) {
console.error("post-recurring-assessments error:", err);
return new Response(JSON.stringify({ error: (err as Error).message }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
});