1const DEFAULT_BATCH_SIZE = 50; 2const DISPATCH_CLAIM_TABLE = "prospect_dispatch_claims"; 3const DISPATCH_CLAIM_TTL_MS = 5 * 60 * 1000; 4const DISPATCH_PERMISSION = "commercial:dispatch"; 5const ADMIN_ROLES = new Set(["admin", "owner", "super_admin", "project_admin"]); 6 7export default async function handler(event = {}, context = null) { 8 if (!context?.call) { 9 return { ok: false, skipped: true, reason: "no_tables_context" }; 10 } 11 if (String(event?.method || event?.httpMethod || "").toUpperCase() === "GET") throw new Error("POST required"); 12 13 const body = event?.body && typeof event.body === "object" ? event.body : event; 14 const actor = await authenticateActor(context, body); 15 assertDispatcher(actor); 16 const dryRun = Boolean(body?.dry_run || body?.dryRun); 17 const batchSize = Math.min(Math.max(Number(body?.limit || DEFAULT_BATCH_SIZE) || DEFAULT_BATCH_SIZE, 1), 200); 18 const now = context?.testMode === true && validIsoDate(body?.now) 19 ? validIsoDate(body.now) 20 : new Date().toISOString(); 21 22 if (body?.schema_only === true) throw new Error("Runtime schema changes are disabled."); 23 const dueProspects = await loadDueProspects(context, now, batchSize); 24 const results = []; 25 26 for (const prospect of dueProspects) { 27 const outcome = await dispatchProspect(context, prospect, now, dryRun); 28 results.push(outcome); 29 } 30 31 return { 32 ok: results.every((result) => result.ok || result.skipped), 33 dry_run: dryRun, 34 now, 35 checked: dueProspects.length, 36 assigned: results.filter((result) => result.action === "assigned").length, 37 failed: results.filter((result) => result.action === "failed").length, 38 skipped: results.filter((result) => result.skipped).length, 39 results, 40 }; 41} 42 43async function authenticateActor(context, body) { 44 // Jobs reaches Functions through the platform's authenticated app-tool path. 45 // This branch is safe only while the Function URL remains disabled by the 46 // local unified-app deployment. 47 if (clean(body?.source).toLowerCase() === "jobs") { 48 return { 49 id: "jobs", 50 email: "jobs@local.flexylead", 51 role: "system", 52 permissions: [DISPATCH_PERMISSION], 53 }; 54 } 55 const claimed = body?.actor && typeof body.actor === "object" ? body.actor : {}; 56 const claimedId = clean(claimed.auth_user_id); 57 const claimedEmail = clean(claimed.email).toLowerCase(); 58 if (context?.testMode === true) { 59 if (!claimedId || !claimedEmail) throw new Error("Session opérateur non authentifiée."); 60 return {