mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
183fe0a93c
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
122 lines
4.3 KiB
TypeScript
122 lines
4.3 KiB
TypeScript
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" },
|
|
});
|
|
}
|
|
});
|