Preuve · code, extrait

backend/process-appel.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 1–55, 1214–1276, sur 1 610 (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 : 25 1244–1246

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

1// process-appel — fires once per newly-inserted appel row. Invoked by the
2// `process-appel` workflow (triggered by tables.row.inserted on appels).
3//
4// Flow:
5//   1. Hydrate the appel (rows_get).
6//   2. Gate: skip if status != pending_transcript or no mirrored audio.
7//   3. For each audio_file (in order), get a signed storage URL.
8//   4. Transcribe each audio part via Deepgram (context.integration).
9//   5. Concatenate transcript pieces, save transcript_text, → pending_eval.
10//   6. Look up linked ventes (for writeback) + grille + criteres (for eval).
11//   7. Evaluate via Claude Haiku 4.5 using the inlined `evaluer-appel` skill
12//      as system prompt + the rendered grille. Parse the structured rapport
13//      back into evaluations[] + score_total/score_max.
14//   8. Insert the rapport row.
15//   9. Write Score + Rapport URL back to the originating Google Sheet (per
16//      sheet_mappings[*].tabs[*].writeback). Mode = 'fill-blanks' by default
17//      so human entries in those columns are never overwritten. Best-effort:
18//      sheet API failures are logged but don't fail the run.
19//   10. Flip appel.status → done.
20
21import { Composio } from '@composio/core';
22
23// ─── Composio ────────────────────────────────────────────────────────
24const composio = new Composio({
25  apiKey: 'ak_cpsw-4oewu9zim8vwS6K',
26  toolkitVersions: {
27    googlesheets: '20260506_00',
28    googledocs:   '20260506_00',
29    googledrive:  '20260506_00',
30  },
31});
32const COMPOSIO_USER = 'pg-test-2M6ZsbRqWTRFywqEAdRixc5ID9Ph7YaV';
33async function execComposio(slug, args) {
34  const r = await composio.tools.execute(slug, { userId: COMPOSIO_USER, arguments: args });
35  if (!r.successful) throw new Error(slug + ': ' + r.error);
36  return r.data;
37}
38
39// ─── Constants (override at deploy time if needed) ───────────────────
40const RAPPORT_URL_BASE = 'https://qualite.flexylead.com/appels/';
41// Default FILL-BLANKS: writes Score + Rapport URL only into empty cells,
42// never clobbers human entries or older URLs. Override per-invoke with
43// {"writeback_mode": "overwrite"} to refresh stale links, or "dry-run" to
44// simulate without touching sheets.
45const WRITEBACK_MODE_DEFAULT = 'fill-blanks';   // 'fill-blanks' | 'overwrite' | 'dry-run'
46
47// Per-grille Drive folder for Google Doc rapports. Mirrors the layout the
48// original lib/upload_rapport.js used — everything lives under ZC CALL's
49// drive, organized by criteres_grille_id. Grilles not in this map fall back
50// to the dashboard URL on the sheet (no Doc created).
51const GRILLE_TO_DRIVE_PATH = {
52  mint_energie_v1: '/ZC CALL - Mint Énergie - Enregistrements Ventes/Rapports/MINT ENERGIE',
53  ohm_v1:          '/ZC CALL - Mint Énergie - Enregistrements Ventes/Rapports/OHM',
54  free_v1:         '/ZC CALL - Mint Énergie - Enregistrements Ventes/Rapports/FREE',
55};
···
1214  }
1215
1216  // 2. Trailing score table: CODE \t name \t score \t bareme
1217  // Only accept rows whose CODE matches an actual critère — otherwise TOTAL
1218  // rows or stray tab-aligned text get summed into score_max (saw 200/100
1219  // in the wild before this filter, 2026-05-20).
1220  const tableRe = /^([A-Za-z0-9\-]+)\t([^\t]*)\t([0-9]+(?:[.,][0-9]+)?)\t([0-9]+(?:[.,][0-9]+)?)\s*$/gm;
1221  while ((m = tableRe.exec(text)) !== null) {
1222    const [, code, , scoreStr, baremeStr] = m;
1223    if (!byCode.has(code)) continue;
1224    const score = Number(scoreStr.replace(',', '.'));
1225    const bareme = Number(baremeStr.replace(',', '.'));
1226    if (evaluations[code]) {
1227      evaluations[code].score = score;
1228      evaluations[code].bareme = bareme;
1229    } else {
1230      const c = byCode.get(code);
1231      evaluations[code] = {
1232        critere_code: code, categorie: c.categorie ?? null, libelle: c.libelle ?? null,
1233        bareme, verdict: null, score, applicable: true, commentaire: '',
1234      };
1235    }
1236  }
1237
1238  // 3. Reconcile verdict ↔ score for every evaluation. Verdict-authoritative.
1239  for (const code of Object.keys(evaluations)) reconcileEvalScore(evaluations[code]);
1240
1241  // 4. Compute totals from reconciled evaluations + grille (single source of
1242  //    truth for score_max — avoids accidental double-counting from stray
1243  //    table rows like TOTAL).
1244  const score_total = Object.values(evaluations).reduce((s, e) => s + (Number(e.score) || 0), 0);
1245  const score_max = criteres.reduce((s, c) => s + (Number(c.bareme) || 0), 0);
1246  return { evaluations: Object.values(evaluations), score_total, score_max };
1247}
1248
1249// ─── writeBackToSheet ────────────────────────────────────────────────
1250// Pushes evaluation results back to the originating sheets. Idempotent and
1251// best-effort: never throws (caller wraps in try/catch and logs).
1252async function writeBackToSheet(appel, rapport, ventes, mode) {
1253  const result = {
1254    mode,
1255    groups: 0,
1256    candidates: 0,
1257    writes: 0,
1258    already_ok: 0,
1259    skipped_non_empty: 0,
1260    missing_column: 0,
1261    errors: 0,
1262    error_samples: [],
1263  };
1264
1265  // 1. Group target rows by (spreadsheet_id, tab). Only ventes that came
1266  //    from a sheet with a 'writeback' block are eligible.
1267  const groups = new Map();
1268  for (const v of ventes) {
1269    const src = v.source || {};
1270    if (!src.spreadsheet_id || !src.tab || !src.row_number) continue;
1271    const sheetCfg = SHEET_MAPPINGS[src.spreadsheet_id];
1272    if (!sheetCfg?.tabs) continue;
1273    const tabCfg = sheetCfg.tabs[src.tab];
1274    if (!tabCfg?.writeback) continue;
1275
1276    const cells = {};