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(); (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" }, }); } });