mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Add ACMCC app source, Supabase backend, and project config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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" },
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user