Preuve · code, extrait

backend/lead-qualification-update.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 827–887, 950–1010, sur 1 245 (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 : 857 980

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

827  if (!note || note === clean(prospect?.notes)) return {};
828
829  const commercialCentreId = clean(commercial?.row?.centre_appel_id);
830  if (!commercialCentreId) {
831    throw new Error("Le centre du commercial est introuvable; le commentaire n’a pas été enregistré.");
832  }
833
834  const existingCentreId = clean(prospect?.centre_lock_id);
835  const existingSource = clean(prospect?.centre_lock_source).toLowerCase();
836  const existingUntilMs = Date.parse(clean(prospect?.centre_lock_until));
837  const nowMs = Date.parse(clean(now));
838  const activeSaleLock = existingSource === "sale"
839    && existingCentreId
840    && Number.isFinite(existingUntilMs)
841    && Number.isFinite(nowMs)
842    && existingUntilMs > nowMs;
843  const activeAffinityLock = ["answered_call", "commercial_comment"].includes(existingSource)
844    && existingCentreId
845    && (!Number.isFinite(existingUntilMs) || !Number.isFinite(nowMs) || existingUntilMs > nowMs);
846
847  if (activeSaleLock || activeAffinityLock) {
848    if (normalizeCentre(existingCentreId) !== normalizeCentre(commercialCentreId)) {
849      throw new Error(`Ce prospect est protégé pour le centre ${existingCentreId}.`);
850    }
851    return {};
852  }
853
854  return {
855    centre_lock_id: commercialCentreId,
856    centre_lock_until: null,
857    centre_lock_source: "commercial_comment",
858  };
859}
860
861function callTelephone(appel) {
862  return clean(appel?.direction).toLowerCase() === 'sortant'
863    ? clean(appel?.to_number) || clean(appel?.from_number)
864    : clean(appel?.from_number) || clean(appel?.to_number);
865}
866
867function qualificationStateForCall(prospect, appel) {
868  const event = clean(appel?.qualification_event).toLowerCase();
869  const status = event.startsWith("nrp")
870    ? "nrp"
871    : ["qualifie", "ko", "rdv", "en_cours"].includes(event)
872      ? event === "rdv" ? "rappel" : event
873      : "nouveau";
874  return {
875    ...prospect,
876    statut: status,
877    qualification_cycle: Math.max(1, integer(appel?.qualification_cycle) || 1),
878    nrp_count: Math.max(0, integer(appel?.nrp_number) || integer(appel?.nrp_count) || 0),
879    last_qualification_event: event || null,
880    last_qualification_at: event ? clean(appel?.status_at) || null : null,
881    next_callback_at: clean(appel?.next_callback_at) || null,
882  };
883}
884
885function buildProspectPatch(prospect, body, eventName, now) {
886  const cycle = Math.max(1, integer(prospect.qualification_cycle) || 1);
887  const currentNrp = Math.max(0, integer(prospect.nrp_count) || 0);
···
950        lead_hidden_until: null,
951        next_callback_at: null,
952        last_qualification_event: "qualifie",
953        qualification_status_reason: clean(body?.reason) || "qualified",
954        last_appel_id: appelRef || undefined,
955        notes: appendNote(prospect.notes, notes),
956      }),
957    };
958  }
959
960  if (eventName === "ko") {
961    const releasesAnsweredCallAffinity = ["answered_call", "commercial_comment"]
962      .includes(clean(prospect?.centre_lock_source).toLowerCase());
963    return {
964      event: "ko",
965      nrp: currentNrp,
966      fields: compactFields({
967        ...base,
968        statut: "ko",
969        nrp_count: currentNrp,
970        qualification_cycle: cycle,
971        lead_hidden_until: null,
972        next_callback_at: null,
973        last_qualification_event: "ko",
974        qualification_status_reason: clean(body?.reason) || "ko",
975        last_appel_id: appelRef || undefined,
976        notes: appendNote(prospect.notes, notes),
977        ...(releasesAnsweredCallAffinity ? {
978          centre_lock_id: null,
979          centre_lock_until: now,
980          centre_lock_source: "released_ko",
981          assigned_commercial_id: null,
982          assigned_at: null,
983          assignment_source: "released_ko",
984        } : {}),
985      }),
986    };
987  }
988
989  if (eventName === "en_cours") {
990    return {
991      event: "en_cours",
992      nrp: currentNrp,
993      fields: compactFields({
994        ...base,
995        statut: "en_cours",
996        nrp_count: currentNrp,
997        qualification_cycle: cycle,
998        lead_hidden_until: null,
999        next_callback_at: null,
1000        last_qualification_event: "en_cours",
1001        qualification_status_reason: clean(body?.reason) || "work_started",
1002        notes: appendNote(prospect.notes, notes),
1003      }),
1004    };
1005  }
1006
1007  throw new Error(`Unsupported qualification event: ${eventName}`);
1008}
1009
1010async function resolveActorCommercial(context, body) {