Preuve · code, extrait

backend/leadbyte-prospect-dispatcher.mjs

Dépôt jeremy-flexylead/flexylead-crm · commit ccbbaa07870e0f786729003dd6f8862e4bcc8256 · dossier de production synchronisé le 14 septembre 2026 à 08 h 46

Extrait tel qu'à ce commit : lignes 1–60, sur 441 (numérotation d'origine, 30 lignes de contexte autour de chaque citation) · fichier intégral sur GitHub (dépôt privé, accès requis)

60 lignes reproduites · ← retour à l'audit · catalogue des preuves

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 {