Preuve · code, extrait

backend/flexylead-app-commercial-bootstrap-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 1734–1803, sur 2 400 (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 : 1764–1773

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

1734  }), { aiCalls: 0, appointments: 0, callsWithAppointment: 0, convertedAppointments: 0, sales: 0, callsWithSale: 0, revenue: 0 });
1735  Object.assign(summary, {
1736    pendingOutcomes: appointments.filter((row) => ['', 'pending'].includes(row.commercialOutcome)).length,
1737    cancelled: appointments.filter((row) => row.status === 'cancelled').length,
1738    noShows: appointments.filter((row) => row.status === 'no_show').length,
1739    routingPending: 0, ownershipMismatches: 0,
1740    callToAppointmentRate: summary.aiCalls ? rate(summary.callsWithAppointment, summary.aiCalls) : null,
1741    appointmentToSaleRate: summary.appointments ? rate(summary.convertedAppointments, summary.appointments) : null,
1742    finalConversionRate: summary.aiCalls ? rate(summary.callsWithSale, summary.aiCalls) : null,
1743    outcomes: Object.fromEntries(callRows.map((row) => clean(row.ai_outcome || 'unknown')).reduce((map, key) => map.set(key, (map.get(key) || 0) + 1), new Map())),
1744  });
1745  return {
1746    aiOperations: {
1747      summary, history, rows, appointments, commercialPerformance: [], assignedCommercials: teamRows.map((row) => ({ id: clean(row.id), name: clean(row.name) })),
1748      agents: Array.from(new Set(callRows.map((row) => clean(row.ai_agent_id)).filter(Boolean))),
1749      centres: centreRows.map((row) => ({ id: clean(row.id), name: clean(row.name) || clean(row.id) })),
1750    },
1751    version: `${range.from}:${range.toExclusive}:${callRows.length}:${appointmentRows.length}`,
1752  };
1753}
1754
1755async function resolveActor(event, context) {
1756  const userId = Number(requireTrustedPrincipal(event, context).subject);
1757  if (!userId) throw httpError(401, "Authenticated user subject is missing.");
1758  const authContext = await context.call("auth", "auth_users_get_context", { organization_id: AUTH_ORGANIZATION_ID, id: userId });
1759  if (clean(authContext?.user?.status).toLowerCase() !== "active") throw httpError(403, "User is inactive.");
1760  const permissions = new Set(arrayOf(authContext?.authorization?.permissions).map(clean));
1761  return { id: String(userId), email: clean(authContext?.user?.email).toLowerCase(), permissions };
1762}
1763
1764async function resolveCommercial(context, actor, requestedId) {
1765  const own = await findCommercial(context, [['auth_user_id', actor.id], ['email', actor.email]]);
1766  if (!own) throw httpError(403, 'Commercial profile not found.');
1767  const ownAliases = new Set(commercialAliasList(own));
1768  if (!requestedId || ownAliases.has(requestedId)) return own;
1769  const requested = await findCommercial(context, [['local_id', requestedId], ['id', requestedId], ['auth_user_id', requestedId]]);
1770  if (!requested) throw httpError(404, 'Requested commercial not found.');
1771  const canSeeAll = actor.permissions.has('commercial:all_centres');
1772  const canSupervise = actor.permissions.has('commercial:team:supervise') && clean(own.centre_appel_id) === clean(requested.centre_appel_id);
1773  if (!canSeeAll && !canSupervise) throw httpError(403, 'Commercial is outside the authenticated scope.');
1774  return requested;
1775}
1776
1777async function findCommercial(context, candidates) {
1778  const supported = new Set(['id', 'auth_user_id', 'email']);
1779  const usable = candidates.filter(([column, value]) => supported.has(column) && clean(value));
1780  if (!usable.length) return null;
1781  const predicates = usable.map(([column]) => column === 'email'
1782    ? 'lower(CAST(email AS TEXT)) = lower(?)'
1783    : `CAST(${column} AS TEXT) = CAST(? AS TEXT)`);
1784  const result = await context.call('tables', 'tables_query', {
1785    sql: `SELECT * FROM {commerciaux}
1786          WHERE COALESCE(active, 1) = 1 AND (${predicates.join(' OR ')})
1787          ORDER BY id DESC LIMIT 10`,
1788    params: usable.map(([, value]) => clean(value)),
1789  });
1790  const rows = Array.isArray(result) ? result : result?.rows || [];
1791  return rows.find((row) => usable.some(([column, value]) => clean(row[column]).toLowerCase() === clean(value).toLowerCase())) || null;
1792}
1793
1794async function searchPage(context, table, where, request, select, fallback, max) {
1795  const limit = bounded(request.limit, 1, max, fallback);
1796  const beforeId = positiveInteger(request.cursor);
1797  const filtered = beforeId ? [...where, { col: 'id', op: 'lt', value: beforeId }] : where;
1798  const result = await searchRows(context, table, filtered, limit, select);
1799  return pageContract(result, limit);
1800}
1801
1802async function searchHumanCallsPage(context, where, request) {
1803  const limit = bounded(request.limit, 1, 50, 15);