Preuve · code, extrait

backend/flexylead-create-sales.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 464–524, 703–792, sur 965 (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 : 494 733–762 753

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

464  const callRefs = uniqueReferences(rows.flatMap((row) => [row.appel_id, row.appel_source_id]));
465  const reservationRefs = uniqueReferences(rows.map((row) => row.reservation_id));
466  const finalized = { prospects: 0, calls: 0, reservations: 0, assignments: 0 };
467  const ownedProspects = [];
468  const prospectReferences = new Set();
469  const callReferences = new Set(callRefs);
470  const callsByReference = new Map();
471  const updatedCallIds = new Set();
472  const lockUntil = saleLockUntil(rows, now);
473
474  for (const ref of prospectRefs) {
475    const prospect = await findLogicalRow(context, "prospects", ref);
476    if (!prospect) throw new Error(`Prospect lié introuvable (${ref}).`);
477    if (!commercial.aliases.has(clean(prospect.assigned_commercial_id))) {
478      // A contact can legitimately be shared with an agent through a historical
479      // call. Never rewrite another agent's prospect from that shared view.
480      continue;
481    }
482    await context.call("tables", "rows_update", {
483      table: "prospects",
484      id: Number(prospect.id),
485      fields: {
486        statut: "converti",
487        lead_hidden_until: null,
488        next_callback_at: null,
489        last_qualification_event: "vente",
490        last_qualification_at: now,
491        qualification_status_reason: "sale_created",
492        centre_lock_id: commercial.centreId,
493        centre_lock_until: lockUntil,
494        centre_lock_source: "sale",
495        updated_at_iso: now,
496      },
497    });
498    ownedProspects.push(prospect);
499    for (const prospectRef of uniqueReferences([prospect.id, prospect.local_id])) prospectReferences.add(prospectRef);
500    finalized.prospects += 1;
501  }
502
503  for (const ref of callRefs) {
504    const call = await findLogicalRow(context, "appels", ref);
505    if (!call) throw new Error(`Appel lié introuvable (${ref}).`);
506    const owner = clean(call.commercial_id);
507    if (owner && !commercial.aliases.has(owner)) throw new Error("L’appel lié appartient à un autre commercial.");
508    await context.call("tables", "rows_update", {
509      table: "appels",
510      id: Number(call.id),
511      fields: {
512        status: "termine",
513        issue: "vente",
514        motif: "vente",
515        next_callback_at: null,
516        followup_status: "done",
517        callback_status: "done",
518        missed_pool_status: "completed",
519        missed_claim_expires_at: null,
520        status_at: now,
521      },
522    });
523    updatedCallIds.add(Number(call.id));
524    rememberCall(callsByReference, callReferences, call);
···
703export function creationPayloadSignature(rows, body = {}) {
704  // These are submission timestamps emitted anew by the browser on retry,
705  // not business dates. Keep every sale/contract/amount/person field binding.
706  const semanticRows = rows.map(input => {
707    const { commercial_status_at: _submittedAt, source, ...sale } = input;
708    const { created_at: _createdAt, ...metadata } = jsonObject(source);
709    return { ...sale, source: metadata };
710  });
711  return JSON.stringify(stablePayload({ rows: semanticRows,
712    new_prospect: body.new_prospect || null, origin_prospect_id: body.origin_prospect_id || null }));
713}
714
715function saleCreationId(input, index, requestId, commercial) {
716  const localId = clean(input?.local_id) || `vente:${commercial.id}:${requestId}:${index + 1}:${clean(input?.vente_role) || "principale"}`;
717  if (!localId.startsWith(`vente:${commercial.id}:${requestId}:`)) throw new Error("Identifiant de vente incohérent.");
718  return localId;
719}
720
721function buildSale({ input, index, requestId, commercial, offers, rules, remunerationRules, now }) {
722  if (!input || typeof input !== "object") throw new Error("Vente invalide.");
723  const offerId = clean(input.offre_id);
724  const offer = offers.get(offerId);
725  if (!offer) throw new Error(`Offre introuvable (${offerId || index + 1}).`);
726  const saleType = clean(input.vente_type || "standard") || "standard";
727  const saleDate = effectiveDate(input.date_vente || now);
728  const revenue = resolveRevenueSnapshot({ rules, offer, offerId, saleType, centreId: commercial.centreId, saleDate });
729  const remuneration = resolveRemunerationSnapshot({
730    rules: remunerationRules,
731    offerId,
732    saleType,
733    centreId: commercial.centreId,
734    saleDate,
735    revenueCents: revenue.revenue_cents_snapshot,
736  });
737  const localId = saleCreationId(input, index, requestId, commercial);
738  const source = jsonObject(input.source);
739  return {
740    ...input,
741    local_id: localId,
742    centre_appel_id: commercial.centreId,
743    commercial: commercial.name,
744    commercial_id: commercial.id,
745    offre_id: offerId,
746    vente_type: saleType,
747    etat: clean(input.etat || "signee") || "signee",
748    economic_role: revenue.economic_role,
749    revenue_cents_snapshot: revenue.revenue_cents_snapshot,
750    revenue_currency: revenue.revenue_currency,
751    revenue_rule_id: revenue.revenue_rule_id,
752    validated_at: countableStatus(input.etat || "signee") ? now : null,
753    prime_montant: remuneration?.prime_montant ?? null,
754    prime_devise: remuneration?.prime_devise || null,
755    prime_rule_id: remuneration?.prime_rule_id || null,
756    source: {
757      ...source,
758      app: clean(source.app) || (commercial.actorId === commercial.authUserId ? "commercial_app" : "admin_manual"),
759      request_id: requestId,
760      actor_auth_user_id: commercial.actorId,
761      target_commercial_id: commercial.id,
762    },
763  };
764}
765
766export function resolveRevenueSnapshot({ rules, offer, offerId, saleType, centreId, saleDate }) {
767  const candidates = array(rules)
768    .filter((row) => clean(row.offre_id) === offerId)
769    .filter((row) => clean(row.vente_type || "standard") === saleType)
770    .filter((row) => clean(row.calculation_type || "fixed").toLowerCase() === "fixed")
771    .filter((row) => active(row.active) && effectiveOn(row, saleDate))
772    .filter((row) => clean(row.scope_type || "global") === "global"
773      || (clean(row.scope_type) === "centre" && clean(row.scope_id) === centreId))
774    .sort(compareRules);
775  const selected = candidates[0] || null;
776  const role = economicRole(selected?.economic_role, inferEconomicRole(offer));
777  return {
778    economic_role: role,
779    revenue_cents_snapshot: selected && Number.isFinite(Number(selected.revenue_cents))
780      ? Math.max(0, Number(selected.revenue_cents))
781      : null,
782    revenue_currency: selected ? clean(selected.currency || "EUR") || "EUR" : null,
783    revenue_rule_id: selected ? clean(selected.local_id || selected.id) || null : null,
784  };
785}
786
787function resolveRemunerationSnapshot({ rules, offerId, saleType, centreId, saleDate, revenueCents }) {
788  const applicable = array(rules)
789    .filter((row) => clean(row.offre_id) === offerId)
790    .filter((row) => active(row.active) && effectiveOn(row, saleDate))
791    .filter((row) => clean(row.scope_type || "global") === "global"
792      || (clean(row.scope_type) === "centre" && clean(row.scope_id) === centreId))