Preuve · code, extrait

backend/flexylead-manage-offer-catalog.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 104–173, sur 338 (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 : 134–143

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

104        detection: null,
105        description: null,
106        contrat_nombre: 1,
107        contrat_groupe_partage: null,
108        contrat_labels: [],
109        ...offer.fields,
110      }] });
111      audit.offer_created = true;
112      journal.push(() => deleteByLocalId(context, 'offres', offer.local_id));
113    }
114    await applyPricingPlan(context, pricingPlan, journal, audit);
115    for (const centre of centres) {
116      const centreId = clean(centre.local_id || centre.id);
117      const current = stringArray(centre.offres_proposees);
118      const next = selectedCentreIds.has(centreId)
119        ? unique([...current, offer.local_id])
120        : current.filter((id) => id !== offer.local_id);
121      if (sameArray(current, next)) continue;
122      await context.call('tables', 'rows_update', { table: 'centres', id: Number(centre.id), fields: { offres_proposees: next } });
123      journal.push(() => context.call('tables', 'rows_update', { table: 'centres', id: Number(centre.id), fields: { offres_proposees: current } }));
124      audit.centres_updated += 1;
125    }
126    return { ok: true, ...audit };
127  } catch (error) {
128    const rollbackErrors = await rollback(journal);
129    if (rollbackErrors.length) {
130      throw new Error(`Enregistrement incomplet et restauration partielle (${rollbackErrors.length} erreur(s)). Vérification administrateur requise. Cause : ${message(error)}`);
131    }
132    throw new Error(`Aucune modification conservée. ${message(error)}`);
133  }
134}
135
136async function savePricingOnly(context, body, actor) {
137  const offerId = normalizeOfferId(body.offer_id);
138  const [offerResult, ruleResult] = await Promise.all([
139    context.call('tables', 'rows_search', { table: 'offres', where: [{ col: 'local_id', op: 'eq', value: offerId }], limit: 20 }),
140    context.call('tables', 'rows_search', { table: 'offer_remuneration_rules', order_by: 'id asc', limit: 1000 }),
141  ]);
142  const offer = (offerResult?.rows || []).find((row) => clean(row.local_id || row.id) === offerId);
143  if (!offer) throw new Error(`Offre introuvable : ${offerId}.`);
144  const pricingPlan = buildPricingPlan(offerId, body.rules, body.removed_rule_ids, ruleResult?.rows || []);
145  const journal = [];
146  const audit = { action: 'save_pricing', offer_id: offerId, actor_id: actor.id, rules_inserted: 0, rules_updated: 0, rules_disabled: 0 };
147  try {
148    await applyPricingPlan(context, pricingPlan, journal, audit);
149    return { ok: true, ...audit };
150  } catch (error) {
151    const rollbackErrors = await rollback(journal);
152    if (rollbackErrors.length) throw new Error(`Tarifs partiellement restaurés (${rollbackErrors.length} erreur(s)). Vérification administrateur requise. Cause : ${message(error)}`);
153    throw new Error(`Aucune modification tarifaire conservée. ${message(error)}`);
154  }
155}
156
157function buildPricingPlan(offerId, inputRules, removedRuleIds, allRules) {
158  if (!Array.isArray(inputRules) || !inputRules.length) throw new Error('Ajoutez au moins une règle de prime.');
159  const existing = allRules.filter((row) => clean(row.offre_id) === offerId);
160  const existingByLocalId = new Map(existing.map((row) => [clean(row.local_id || row.id), row]));
161  const removed = new Set(values(removedRuleIds));
162  const now = new Date().toISOString();
163  const usedLocalIds = new Set(allRules.map((row) => clean(row.local_id || row.id)).filter(Boolean));
164  const seen = new Set();
165  const updates = [];
166  const inserts = [];
167  for (let index = 0; index < inputRules.length; index += 1) {
168    const input = inputRules[index] || {};
169    const suppliedId = clean(input.local_id);
170    if (suppliedId && removed.has(suppliedId)) throw new Error('Une règle supprimée ne peut pas être enregistrée simultanément.');
171    const current = suppliedId ? existingByLocalId.get(suppliedId) : null;
172    if (suppliedId && !current) throw new Error(`Règle de rémunération introuvable : ${suppliedId}.`);
173    if (current && seen.has(suppliedId)) throw new Error('Une règle de rémunération est présente plusieurs fois.');