Preuve · code, extrait

backend/flexylead-route-work-items.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 1010–1096, sur 2 166 (numérotation d'origine, 30 lignes de contexte autour de chaque citation) · fichier intégral sur GitHub (dépôt privé, accès requis)

Lignes citées par l'audit : 1040–1066

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

1010        updated_at_iso: now.toISOString(),
1011      });
1012  }
1013  return rows.length;
1014}
1015
1016export function routingSnapshot(table, row, columns) {
1017  return {table,min_count:1,max_count:1,where:["id",...columns].map(col=>({col,op:row[col]==null?"is_null":"eq",...(row[col]==null?{}:{value:row[col]})}))};
1018}
1019
1020async function guardedMaintenanceUpdate(context, table, row, fields, dependencies = []) {
1021  return guardedRoutingWrites(context, {
1022    checks: [routingSnapshot(table,row,Object.keys(row).filter(k=>!["id","created_at","updated_at"].includes(k))), ...dependencies],
1023    mutations: [{operation:"update",table,id:Number(row.id),fields}],
1024  });
1025}
1026
1027export async function reconcileActiveAssignments(context, now, dryRun, limit = 300) {
1028  // Least-recently checked first: progress is durable across workers/restarts.
1029  // Every inspected row is touched, including rows whose state is unchanged.
1030  const assignments = await search(context, ASSIGNMENT_TABLE, [
1031    { col: "status", op: "in", value: [...ACTIVE_ASSIGNMENT_STATES] },
1032  ], "reconciled_at_iso asc", boundedInteger(limit, 300, 1, 300));
1033  let pending=[];
1034  const flush=async()=>{
1035    if(!pending.length)return;
1036    const batch=pending;pending=[];
1037    const committed=await guardedRoutingWrites(context, {checks:batch.flatMap(r=>r.checks),mutations:batch.flatMap(r=>r.mutations)});
1038    // One changing row must not starve the other rows in its batch.
1039    if(committed?.committed!==true)for(const item of batch)await guardedRoutingWrites(context, item);
1040  };
1041  const [calls, reservations, prospects] = await Promise.all([
1042    rowsById(
1043      context,
1044      "appels",
1045      assignments.filter((row) => clean(row.resource_type) === "missed_call").map((row) => row.resource_id),
1046      "id, handled_by, direction, status, ended_at, answered_at, talk_duration_seconds, followup_appel_id, followup_status, missed_pool_status, missed_claimed_by",
1047    ),
1048    rowsById(
1049      context,
1050      "reservations",
1051      assignments.filter((row) => ["ai_appointment", "planned_callback"].includes(clean(row.resource_type))).map((row) => row.resource_id),
1052      "id, status, commercial_outcome, outcome_at, claimed_by",
1053    ),
1054    rowsById(
1055      context,
1056      "prospects",
1057      assignments.filter((row) => clean(row.resource_type) === "assigned_lead").map((row) => row.resource_id),
1058      "id, statut, updated_at_iso",
1059    ),
1060  ]);
1061  for (const assignment of assignments) {
1062    const type = clean(assignment.resource_type);
1063    const rowId = Number(assignment.resource_id || 0);
1064    if (!rowId) {
1065      if(!dryRun)pending.push({checks:[routingSnapshot(ASSIGNMENT_TABLE,assignment,["status","resource_id"])],mutations:[{operation:"update",table:ASSIGNMENT_TABLE,id:Number(assignment.id),fields:{reconciled_at_iso:now.toISOString(),last_error:"invalid_resource_id"}}]});
1066      if(pending.length>=25)await flush();
1067      continue;
1068    }
1069    let status = "";
1070    let completedAt = null;
1071    let claimedBy = clean(assignment.claimed_by) || null;
1072    if (type === "missed_call") {
1073      const call = calls.get(clean(rowId));
1074      if (!call) status = "cancelled";
1075      else if (clean(call.followup_appel_id) || clean(call.followup_status) === "callback_started") status = "in_progress";
1076      else if (!isActionableMissedCall(call) || ["done", "discarded"].includes(clean(call.missed_pool_status).toLowerCase())) {
1077        status = "completed";
1078        completedAt = clean(call.ended_at) || now.toISOString();
1079      } else if (clean(call.missed_claimed_by)) {
1080        status = "claimed";
1081        claimedBy = clean(call.missed_claimed_by);
1082      } else status = "pooled";
1083    } else if (type === "ai_appointment" || type === "planned_callback") {
1084      const reservation = reservations.get(clean(rowId));
1085      if (!reservation) status = "cancelled";
1086      else if (TERMINAL_RESERVATION_STATES.has(clean(reservation.status).toLowerCase()) || TERMINAL_OUTCOMES.has(clean(reservation.commercial_outcome).toLowerCase())) {
1087        status = "completed";
1088        completedAt = clean(reservation.outcome_at) || now.toISOString();
1089      } else if (clean(reservation.status).toLowerCase() === "in_progress") status = "in_progress";
1090      else if (clean(reservation.claimed_by)) {
1091        status = "claimed";
1092        claimedBy = clean(reservation.claimed_by);
1093      }
1094    } else if (type === "assigned_lead") {
1095      const prospect = prospects.get(clean(rowId));
1096      const prospectStatus = clean(prospect?.statut).toLowerCase();