Preuve · code, extrait

backend/flexylead-manage-production-economics.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 494–600, sur 760 (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 : 524–541 535–537 562–570 563

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

494      amount: selected ? Number(selected.amount_cents || 0) / 100 : 0,
495      inherited: Boolean(selected && clean(selected.scope_type || "global").toLowerCase() === "global"),
496      unit: definition.unit,
497      label: definition.label,
498      effective_from: dateOnly(selected?.starts_at) || null,
499      scheduled: Boolean(dateOnly(selected?.starts_at) && dateOnly(selected?.starts_at) > today()),
500    };
501  }
502  return { ok: true, centre_id: centreId, costs };
503}
504
505async function listScopedRules(context, table, centreId) {
506  const [globalResult, centreResult] = await Promise.all([
507    context.call("tables", "rows_search", {
508      table,
509      where: [{ col: "scope_type", op: "eq", value: "global" }],
510      order_by: "id desc",
511      limit: 500,
512    }),
513    context.call("tables", "rows_search", {
514      table,
515      where: [
516        { col: "scope_type", op: "eq", value: "centre" },
517        { col: "scope_id", op: "eq", value: centreId },
518      ],
519      order_by: "id desc",
520      limit: 500,
521    }),
522  ]);
523  return { rows: [...(globalResult?.rows || []), ...(centreResult?.rows || [])] };
524}
525
526async function saveCosts(context, centreId, input, existingRows = [], effectiveFrom) {
527  const now = new Date().toISOString();
528  const pending = pendingRuleChanges();
529  for (const [costType, definition] of Object.entries(COST_DEFINITIONS)) {
530    const amount = Number(input?.[costType]);
531    if (!Number.isFinite(amount) || amount < 0 || amount > 100_000) {
532      throw new Error(`Montant invalide pour ${definition.label}.`);
533    }
534    const localId = `centre:${centreId}:${costType}:${definition.unit}`;
535    const fields = {
536      scope_type: "centre",
537      scope_id: centreId,
538      cost_type: costType,
539      amount_cents: Math.round(amount * 100),
540      currency: "EUR",
541      unit: definition.unit,
542      cost_owner: "centre",
543      active: true,
544      starts_at: ruleDateTime(effectiveFrom),
545      ends_at: null,
546      updated_at_iso: now,
547    };
548    versionRule(pending, existingRows, {
549      baseLocalId: localId,
550      effectiveFrom,
551      now,
552      matches: (row) => clean(row.scope_type) === "centre"
553        && clean(row.scope_id) === centreId
554        && clean(row.cost_type) === costType
555        && clean(row.unit) === definition.unit,
556      fields,
557    });
558  }
559  await applyRuleChanges(context, "production_cost_rules", pending);
560}
561
562function pendingRuleChanges() {
563  return { updates: [], inserts: [] };
564}
565
566function versionRule(pending, existingRows, { baseLocalId, effectiveFrom, now, matches, fields }) {
567  const candidates = existingRows.filter(matches).filter(isActiveRow);
568  const sameStart = candidates.find((row) => dateOnly(row.starts_at) === effectiveFrom);
569  if (sameStart?.id) {
570    pending.updates.push({ id: Number(sameStart.id), fields });
571    Object.assign(sameStart, fields);
572    return;
573  }
574
575  const previousDay = shiftDate(effectiveFrom, -1);
576  for (const row of candidates) {
577    const starts = dateOnly(row.starts_at);
578    if (starts && starts > effectiveFrom) {
579      pending.updates.push({ id: Number(row.id), fields: { active: false, updated_at_iso: now } });
580      row.active = false;
581      continue;
582    }
583    if ((!starts || starts < effectiveFrom) && (!dateOnly(row.ends_at) || dateOnly(row.ends_at) >= effectiveFrom)) {
584      pending.updates.push({ id: Number(row.id), fields: { ends_at: ruleDateTime(previousDay), updated_at_iso: now } });
585      row.ends_at = previousDay;
586    }
587  }
588
589  const version = `${baseLocalId}:${effectiveFrom.replaceAll("-", "")}`;
590  const inserted = { local_id: version, ...fields, created_at_iso: now };
591  pending.inserts.push(inserted);
592  existingRows.push(inserted);
593}
594
595async function applyRuleChanges(context, table, pending) {
596  await runWithConcurrency(
597    pending.updates.map((update) => () => context.call("tables", "rows_update", { table, ...update })),
598    6,
599  );
600  if (pending.inserts.length) {