Preuve · code, extrait

backend/flexylead-app-commercial-missed-calls-query.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 307–367, sur 2 488 (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 : 337

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

307      WHERE CAST(p.id AS TEXT) IN (${missing.map(() => '?').join(', ')})`, missing);
308    for (const row of fetched) prospectById.set(clean(row.id), row);
309  }
310  const rows = [];
311  for (const pid of orderedIds) {
312    if (rows.length >= limit) break;
313    const row = prospectById.get(pid);
314    if (row) rows.push(row);
315  }
316  const aliasSet = new Set(aliases.map((value) => clean(value)));
317  for (const row of matchedProspects) {
318    if (rows.length >= limit) break;
319    const pid = clean(row.id);
320    if (seen.has(pid)) continue;
321    if (!aliasSet.has(clean(row.assigned_commercial_id))) continue;
322    seen.add(pid);
323    rows.push(row);
324  }
325  return { prospectRows: rows, callRows, version: latestVersion([...rows, ...callRows]) };
326}
327
328export async function missedCallsResource(context, profile, request) {
329  const limit = bounded(request.limit, 1, 15, 10);
330  const centreId = clean(profile.centre_appel_id);
331  // The assignment destination is the authorization boundary. A routed call
332  // deliberately keeps its source centre on `appels`, so filtering calls by
333  // `centre_appel_id` made the ZC counter increase while the card disappeared.
334  // Load the small assignment window first, then resolve only those exact call
335  // IDs regardless of their source centre.
336  const aliases = commercialAliasList(profile);
337  if (!centreId || !aliases.length) throw new Error('Commercial queue scope missing');
338  // Match the UI's ownership rule before pagination. Loading every colleague's
339  // assignments made a 3,000-row centre exhaust the browser's 100-page guard
340  // even when this commercial had only one follow-up. Claimed owner wins over
341  // the original assignee; unowned pool/group items remain visible for the
342  // existing group-membership check.
343  const owner = "COALESCE(NULLIF(TRIM(claimed_by), ''), NULLIF(TRIM(commercial_id), ''), '')";
344  const cursor = positiveInteger(request.cursor);
345  const assignmentRows = await queryRows(context, `
346    SELECT ${SELECTS.assignments.join(', ')} FROM {work_routing_assignments}
347    WHERE resource_type = 'missed_call'
348      AND status IN ('pooled', 'assigned', 'claimed', 'in_progress')
349      AND destination_centre_id = ?
350      AND (${owner} = '' OR ${owner} IN (${aliases.map(() => '?').join(', ')}))
351      ${cursor ? 'AND id < ?' : ''}
352    ORDER BY id DESC LIMIT ?`, [centreId, ...aliases, ...(cursor ? [cursor] : []), limit]);
353  const assignments = { rows: assignmentRows };
354  const calls = await routedMissedCallRows(context, assignments.rows, limit);
355  const prospectIds = [...new Set(calls.map((row) => positiveInteger(row.prospect_id)).filter(Boolean))];
356  const linkedProspects = prospectIds.length
357    ? await searchRows(context, 'prospects', [{ col: 'id', op: 'in', value: prospectIds }], prospectIds.length, SELECTS.prospects)
358    : { rows: [] };
359  const safeCalls = suppressClosedProspectWork(calls, linkedProspects.rows, centreId)
360    .filter((row) => !closedProspectWorkReason(linkedProspectForCall(row, linkedProspects.rows), centreId));
361  const safeCallRefs = new Set(safeCalls.flatMap((row) => [clean(row.id), clean(row.local_id)]).filter(Boolean));
362  const safeAssignments = assignments.rows.filter((row) => safeCallRefs.has(clean(row.resource_id)));
363  return {
364    // Continue on assignment IDs, even when every call in this page was
365    // suppressed. Call IDs are not ordered like the routing assignments.
366    has_more: assignments.rows.length === limit,
367    next_cursor: assignments.rows.length === limit ? clean(assignments.rows.at(-1)?.id) : null,