Preuve · code, extrait

backend/flexylead-app-pilotage-remuneration-query.mjs

Dépôt jeremy-flexylead/flexylead-crm · commit 730a5e79cbbb4191930f2de2f208f42ba483fb46 · livraison du 8 septembre 2026 à 13 h 51

Extrait tel qu'à ce commit : lignes 2234–2590, sur 4 540 (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 : 2264–2560

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

2234    .filter((rule) => clean(rule.scope_type || "global").toLowerCase() === "global" || clean(rule.scope_id) === clean(centreId))
2235    .sort((left, right) => scopeRank(right.scope_type) - scopeRank(left.scope_type))[0] || null;
2236}
2237
2238function monthlyFormulaConfig(rule) {
2239  const parsed = jsonObject(rule?.tiers_json);
2240  const transformationNumerator = clean(parsed.transformation_numerator).toLowerCase();
2241  const opportunityBasis = clean(parsed.opportunity_basis).toLowerCase();
2242  return {
2243    enabled: parsed.enabled !== false,
2244    label: clean(parsed.label) || "Rémunération Énergie partenaires",
2245    period_basis: clean(parsed.period_basis) === "calendar_month" ? "calendar_month" : "stats_period",
2246    base_amount: finiteNumber(parsed.base_amount, cents(rule?.revenue_cents)),
2247    eligibility_metric: formulaMetric(parsed.eligibility_metric, "transformation_rate"),
2248    eligibility_operator: formulaOperator(parsed.eligibility_operator, "gte"),
2249    eligibility_threshold: finiteNumber(parsed.eligibility_threshold, 0),
2250    ineligible_amount: finiteNumber(parsed.ineligible_amount, 0),
2251    minimum_amount: finiteNumber(parsed.minimum_amount, 0),
2252    maximum_amount: finiteNumber(parsed.maximum_amount, 100_000),
2253    include_additional_revenue_in_ca: parsed.include_additional_revenue_in_ca === true,
2254    transformation_numerator: transformationNumerator === "energy_contracts" ? "energy_contracts" : "main_sales",
2255    opportunity_basis: ["unique_inbound", "assigned_prospects", "routed_work_items", "mixed"].includes(opportunityBasis)
2256      ? opportunityBasis
2257      : "",
2258    opportunity_sources: unique(array(parsed.opportunity_sources).map((value) => clean(value).toLowerCase()).filter(Boolean)),
2259    adjustments: array(parsed.adjustments).map((adjustment, index) => normalizeFormulaAdjustment(adjustment, index)),
2260    sale_rules: array(parsed.sale_rules).map((saleRule, index) => normalizeSaleRule(saleRule, index)),
2261  };
2262}
2263
2264export function evaluateMonthlyFormula(formula, performance = {}) {
2265  const eligible = formulaConditionMatches(
2266    finiteNumber(performance[formula.eligibility_metric], 0),
2267    formula.eligibility_operator,
2268    formula.eligibility_threshold,
2269  );
2270  const breakdown = [{
2271    id: "base",
2272    label: formula.label || "Base mensuelle",
2273    metric: formula.eligibility_metric,
2274    value: finiteNumber(performance[formula.eligibility_metric], 0),
2275    threshold: formula.eligibility_threshold,
2276    operator: formula.eligibility_operator,
2277    delta: eligible ? formula.base_amount : formula.ineligible_amount,
2278    applied: eligible,
2279  }];
2280  let amount = eligible ? formula.base_amount : formula.ineligible_amount;
2281  if (eligible) {
2282    const adjustments = array(formula.adjustments).map((adjustment, index) => normalizeFormulaAdjustment(adjustment, index));
2283    const matched = new Set(adjustments
2284      .map((adjustment, index) => ({ adjustment, index }))
2285      .filter(({ adjustment }) => adjustment.enabled !== false && adjustment.conditions.every((condition) => (
2286        formulaConditionMatches(finiteNumber(performance[condition.metric], 0), condition.operator, condition.threshold)
2287      )))
2288      .map(({ index }) => index));
2289    const selectedByGroup = new Map();
2290    for (const index of matched) {
2291      const adjustment = adjustments[index];
2292      if (!adjustment.exclusive_group) continue;
2293      const current = selectedByGroup.get(adjustment.exclusive_group);
2294      if (current == null || adjustment.priority > adjustments[current].priority) {
2295        selectedByGroup.set(adjustment.exclusive_group, index);
2296      }
2297    }
2298    for (let index = 0; index < adjustments.length; index += 1) {
2299      const adjustment = adjustments[index];
2300      if (adjustment.enabled === false) continue;
2301      const isMatch = matched.has(index);
2302      const applied = isMatch && (!adjustment.exclusive_group || selectedByGroup.get(adjustment.exclusive_group) === index);
2303      const conditionValues = adjustment.conditions.map((condition) => ({
2304        ...condition,
2305        value: finiteNumber(performance[condition.metric], 0),
2306      }));
2307      breakdown.push({
2308        ...adjustment,
2309        value: conditionValues[0]?.value ?? 0,
2310        condition_values: conditionValues,
2311        matched: isMatch,
2312        applied,
2313        skipped_reason: isMatch && !applied ? "exclusive_group" : null,
2314      });
2315      if (!applied) continue;
2316      const effectValue = finiteNumber(adjustment.effect_value, 0);
2317      if (adjustment.effect === "set") amount = effectValue;
2318      else if (adjustment.effect === "cap") amount = Math.min(amount, effectValue);
2319      else if (adjustment.effect === "floor") amount = Math.max(amount, effectValue);
2320      else if (adjustment.effect !== "none") amount += effectValue;
2321    }
2322  }
2323  const minimum = finiteNumber(formula.minimum_amount, 0);
2324  const maximum = Math.max(minimum, finiteNumber(formula.maximum_amount, 100_000));
2325  return {
2326    amount: Math.min(maximum, Math.max(minimum, Math.round(amount * 100) / 100)),
2327    eligible,
2328    breakdown,
2329  };
2330}
2331
2332function normalizeFormulaAdjustment(adjustment, index) {
2333  const metric = formulaMetric(adjustment?.metric, "attachment_rate");
2334  const operator = formulaOperator(adjustment?.operator, "gte");
2335  const threshold = finiteNumber(adjustment?.threshold, 0);
2336  const effect = ["add", "set", "cap", "floor", "none"].includes(clean(adjustment?.effect).toLowerCase())
2337    ? clean(adjustment.effect).toLowerCase()
2338    : "add";
2339  const delta = finiteNumber(adjustment?.delta, 0);
2340  const conditions = array(adjustment?.conditions).length
2341    ? array(adjustment.conditions).slice(0, 8).map((condition) => ({
2342      metric: formulaMetric(condition?.metric, metric),
2343      operator: formulaOperator(condition?.operator, "gte"),
2344      threshold: finiteNumber(condition?.threshold, 0),
2345    }))
2346    : [{ metric, operator, threshold }];
2347  return {
2348    id: clean(adjustment?.id) || `adjustment-${index + 1}`,
2349    label: clean(adjustment?.label) || `Ajustement ${index + 1}`,
2350    metric,
2351    operator,
2352    threshold,
2353    conditions,
2354    effect,
2355    effect_value: finiteNumber(adjustment?.value, finiteNumber(adjustment?.effect_value, effect === "add" ? delta : 0)),
2356    delta,
2357    exclusive_group: clean(adjustment?.exclusive_group) || null,
2358    priority: finiteNumber(adjustment?.priority, index),
2359    enabled: adjustment?.enabled !== false,
2360  };
2361}
2362
2363function normalizeSaleRule(rule, index) {
2364  return {
2365    id: clean(rule?.id) || `sale-rule-${index + 1}`,
2366    label: clean(rule?.label) || `Règle vente ${index + 1}`,
2367    conditions: array(rule?.conditions).slice(0, 8).map((condition) => ({
2368      metric: clean(condition?.metric).toLowerCase() === "ai_score" ? "ai_score" : "ai_score",
2369      operator: formulaOperator(condition?.operator, "gte"),
2370      threshold: finiteNumber(condition?.threshold, 0),
2371    })),
2372    effect: ["exclude", "multiply", "set"].includes(clean(rule?.effect).toLowerCase()) ? clean(rule.effect).toLowerCase() : "exclude",
2373    value: finiteNumber(rule?.value, clean(rule?.effect).toLowerCase() === "multiply" ? 1 : 0),
2374    missing: clean(rule?.missing).toLowerCase() === "match" ? "match" : "ignore",
2375    enabled: rule?.enabled !== false,
2376  };
2377}
2378
2379function latestReportScoreIndex(reports) {
2380  const latest = new Map();
2381  const remember = (key, modified, score) => {
2382    if (!key) return;
2383    const current = latest.get(key);
2384    if (!current || modified >= current.modified) latest.set(key, { modified, score });
2385  };
2386  for (const report of array(reports)) {
2387    if (Number(report.aggregate_score) === 1) continue;
2388    const maximum = finiteNumber(report.score_max, 0);
2389    const total = finiteNumber(report.score_total, NaN);
2390    if (!maximum || !Number.isFinite(total)) continue;
2391    const score = total / maximum * 100;
2392    const modified = clean(report.modified_time);
2393    const saleIds = jsonValues(report.vente_ids);
2394    for (const id of saleIds) remember(`sale:${id}`, modified, score);
2395    if (!saleIds.length && clean(report.appel_id)) remember(`call:${clean(report.appel_id)}`, modified, score);
2396  }
2397  return latest;
2398}
2399
2400function reportScoreForSale(sale, reportScoreIndex) {
2401  for (const id of [sale?.id, sale?.local_id].map(clean).filter(Boolean)) {
2402    const entry = reportScoreIndex.get(`sale:${id}`);
2403    if (entry) return entry.score;
2404  }
2405  for (const id of [sale?.appel_id, sale?.appel_source_id].map(clean).filter(Boolean)) {
2406    const entry = reportScoreIndex.get(`call:${id}`);
2407    if (entry) return entry.score;
2408  }
2409  return null;
2410}
2411
2412function saleRuleMatches(rule, score) {
2413  if (!array(rule?.conditions).length) return false;
2414  return rule.conditions.every((condition) => {
2415    if (condition.metric !== "ai_score") return false;
2416    if (!Number.isFinite(Number(score))) return rule.missing === "match";
2417    return formulaConditionMatches(Number(score), condition.operator, condition.threshold);
2418  });
2419}
2420
2421function evaluateSaleRemuneration(saleRules, score, baseAmount) {
2422  let amount = finiteNumber(baseAmount, 0);
2423  let excluded = false;
2424  const breakdown = [];
2425  for (const rule of array(saleRules)) {
2426    if (rule?.enabled === false) continue;
2427    const applied = saleRuleMatches(rule, score);
2428    breakdown.push({ ...rule, score: Number.isFinite(Number(score)) ? Number(score) : null, applied });
2429    if (!applied) continue;
2430    if (rule.effect === "exclude") {
2431      amount = 0;
2432      excluded = true;
2433    } else if (rule.effect === "multiply") {
2434      amount *= finiteNumber(rule.value, 1);
2435    } else if (rule.effect === "set") {
2436      amount = finiteNumber(rule.value, 0);
2437    }
2438  }
2439  return { amount: Math.max(0, Math.round(amount * 100) / 100), excluded, breakdown };
2440}
2441
2442function saleRuleMetricsForPeriod({ revenueEntries, reportScoreIndex, centreId, period, saleRules }) {
2443  let energyContracts = 0;
2444  let remuneratedEnergyContracts = 0;
2445  let excludedEnergyContracts = 0;
2446  let unscoredEnergyContracts = 0;
2447  for (const entry of array(revenueEntries)) {
2448    if (!entry.energyContract || entry.row.centreId !== centreId || !dateInRange(entry.row.date, period.dateFrom, period.dateTo)) continue;
2449    const count = Number.isFinite(Number(entry.factCount)) ? nonNegative(entry.factCount) : 1;
2450    energyContracts += count;
2451    const score = reportScoreForSale(entry.sale, reportScoreIndex);
2452    if (!Number.isFinite(Number(score))) unscoredEnergyContracts += count;
2453    const evaluation = evaluateSaleRemuneration(saleRules, score, 1);
2454    if (evaluation.excluded) excludedEnergyContracts += count;
2455    else remuneratedEnergyContracts += count;
2456  }
2457  return { energyContracts, remuneratedEnergyContracts, excludedEnergyContracts, unscoredEnergyContracts };
2458}
2459
2460function saleRemunerationForEntry(entry, monthly, reportScoreIndex) {
2461  const score = reportScoreForSale(entry.sale, reportScoreIndex);
2462  const evaluation = evaluateSaleRemuneration(monthly.saleRules, score, monthly.amount);
2463  const count = Number.isFinite(Number(entry.factCount)) ? nonNegative(entry.factCount) : 1;
2464  return {
2465    total: evaluation.amount * count,
2466    remuneratedCount: evaluation.excluded ? 0 : count,
2467    excludedCount: evaluation.excluded ? count : 0,
2468  };
2469}
2470
2471function formulaConditionMatches(value, operator, threshold) {
2472  const target = finiteNumber(threshold, 0);
2473  if (operator === "gt") return value > target;
2474  if (operator === "lte") return value <= target;
2475  if (operator === "lt") return value < target;
2476  return value >= target;
2477}
2478
2479function formulaMetric(value, fallback) {
2480  const metric = clean(value).toLowerCase();
2481  return ["transformation_rate", "attachment_rate", "meteore_attachment_rate", "consopilote_attachment_rate", "average_ai_score"].includes(metric)
2482    ? metric
2483    : fallback;
2484}
2485
2486function formulaOperator(value, fallback) {
2487  const operator = clean(value).toLowerCase();
2488  return ["gte", "gt", "lte", "lt"].includes(operator) ? operator : fallback;
2489}
2490
2491function remunerationCoverage({ rules, statsPeriods, centreIds, dateFrom, dateTo }) {
2492  let from = dateFrom;
2493  let to = dateTo;
2494  for (const centreId of centreIds) {
2495    for (const date of dateSequence(dateFrom, dateTo)) {
2496      const rule = monthlyFormulaRuleFor(rules, centreId, date);
2497      if (!rule) continue;
2498      const formula = monthlyFormulaConfig(rule);
2499      if (!formula.enabled) continue;
2500      const period = remunerationPeriodForDate(date, centreId, statsPeriods, formula.period_basis);
2501      if (period.dateFrom < from) from = period.dateFrom;
2502      if (period.dateTo > to) to = period.dateTo;
2503    }
2504  }
2505  return { dateFrom: from, dateTo: to };
2506}
2507
2508function remunerationPeriodForDate(date, centreId, statsPeriods, basis) {
2509  if (basis === "stats_period") {
2510    const selected = array(statsPeriods)
2511      .filter(activeRow)
2512      .filter((period) => !clean(period.archived_at_iso))
2513      .filter((period) => dateInRange(date, dateOnly(period.starts_at), dateOnly(period.ends_at)))
2514      .filter((period) => clean(period.scope_type || "global").toLowerCase() === "global" || clean(period.scope_id) === clean(centreId))
2515      .sort((left, right) => scopeRank(right.scope_type) - scopeRank(left.scope_type))[0];
2516    if (selected) {
2517      return {
2518        id: clean(selected.local_id || selected.id),
2519        label: clean(selected.label) || "Période statistique",
2520        dateFrom: dateOnly(selected.starts_at),
2521        dateTo: dateOnly(selected.ends_at),
2522      };
2523    }
2524  }
2525  const start = `${date.slice(0, 7)}-01`;
2526  const endDate = new Date(`${start}T12:00:00Z`);
2527  endDate.setUTCMonth(endDate.getUTCMonth() + 1);
2528  endDate.setUTCDate(0);
2529  const end = endDate.toISOString().slice(0, 10);
2530  return {
2531    id: `calendar:${date.slice(0, 7)}`,
2532    label: MONTH_LABEL_FORMATTER.format(new Date(`${start}T12:00:00Z`)),
2533    dateFrom: start,
2534    dateTo: end,
2535  };
2536}
2537
2538function performanceForPeriod({ rows, reports, saleById, centreId, period, operationalCentreIndex, formula }) {
2539  const entries = rows.filter((row) => row.centreId === centreId && dateInRange(row.date, period.dateFrom, period.dateTo));
2540  const received = sum(entries, "received");
2541  const mainSales = sum(entries, "mainSales");
2542  const energyContracts = sum(entries, "energyContracts");
2543  const transformationNumerator = formula?.transformation_numerator === "energy_contracts" ? energyContracts : mainSales;
2544  const denominator = energyContracts || mainSales;
2545  const aggregateScores = reports.filter((report) => Number(report.aggregate_score) === 1)
2546    .filter((report) => {
2547      const rawCentre = clean(report.centre_appel_id);
2548      const reportCentre = operationalCentreIndex.canonicalByAlias.get(rawCentre) || rawCentre;
2549      return reportCentre === centreId && dateInRange(clean(report.score_date), period.dateFrom, period.dateTo);
2550    });
2551  const aggregateScoreCount = sum(aggregateScores, "score_count");
2552  const aggregateScoreSum = sum(aggregateScores, "score_sum");
2553  const scores = aggregateScores.length ? [] : latestReportScoresForPeriod({
2554      reports,
2555      saleById,
2556      centreId,
2557      period,
2558      operationalCentreIndex,
2559    });
2560  return {
2561    transformation_rate: received ? transformationNumerator / received * 100 : 0,
2562    attachment_rate: denominator ? sum(entries, "attachedMainSales") / denominator * 100 : 0,
2563    meteore_attachment_rate: denominator ? sum(entries, "meteoreAttachedMainSales") / denominator * 100 : 0,
2564    consopilote_attachment_rate: denominator ? sum(entries, "consoPiloteAttachedMainSales") / denominator * 100 : 0,
2565    average_ai_score: aggregateScoreCount ? aggregateScoreSum / aggregateScoreCount : scores.length ? scores.reduce((total, score) => total + score, 0) / scores.length : 0,
2566    received,
2567    mainSales,
2568    energyContracts,
2569    transformation_numerator: formula?.transformation_numerator || "main_sales",
2570    transformation_numerator_count: transformationNumerator,
2571    opportunity_basis: formula?.opportunity_basis || "",
2572    opportunity_sources: formula?.opportunity_sources || [],
2573    scoredSales: aggregateScoreCount || scores.length,
2574  };
2575}
2576
2577function latestReportScoresForPeriod({ reports, saleById, centreId, period, operationalCentreIndex }) {
2578  const latest = new Map();
2579  for (const report of reports) {
2580    const scoreMax = finiteNumber(report.score_max, 0);
2581    const scoreTotal = finiteNumber(report.score_total, NaN);
2582    if (!scoreMax || !Number.isFinite(scoreTotal)) continue;
2583    const saleIds = jsonValues(report.vente_ids);
2584    const linkedSale = saleIds.map((id) => saleById.get(clean(id))).find(Boolean);
2585    const rawCentre = clean(report.centre_appel_id) || clean(linkedSale?.centre_appel_id);
2586    const reportCentre = operationalCentreIndex.canonicalByAlias.get(rawCentre) || rawCentre;
2587    if (reportCentre !== centreId) continue;
2588    const date = linkedSale ? saleDateOnly(linkedSale) : dateOnly(report.modified_time);
2589    if (!dateInRange(date, period.dateFrom, period.dateTo)) continue;
2590    const key = saleIds[0] || clean(report.appel_id) || clean(report.id);