464 const callRefs = uniqueReferences(rows.flatMap((row) => [row.appel_id, row.appel_source_id])); 465 const reservationRefs = uniqueReferences(rows.map((row) => row.reservation_id)); 466 const finalized = { prospects: 0, calls: 0, reservations: 0, assignments: 0 }; 467 const ownedProspects = []; 468 const prospectReferences = new Set(); 469 const callReferences = new Set(callRefs); 470 const callsByReference = new Map(); 471 const updatedCallIds = new Set(); 472 const lockUntil = saleLockUntil(rows, now); 473 474 for (const ref of prospectRefs) { 475 const prospect = await findLogicalRow(context, "prospects", ref); 476 if (!prospect) throw new Error(`Prospect lié introuvable (${ref}).`); 477 if (!commercial.aliases.has(clean(prospect.assigned_commercial_id))) { 478 // A contact can legitimately be shared with an agent through a historical 479 // call. Never rewrite another agent's prospect from that shared view. 480 continue; 481 } 482 await context.call("tables", "rows_update", { 483 table: "prospects", 484 id: Number(prospect.id), 485 fields: { 486 statut: "converti", 487 lead_hidden_until: null, 488 next_callback_at: null, 489 last_qualification_event: "vente", 490 last_qualification_at: now, 491 qualification_status_reason: "sale_created", 492 centre_lock_id: commercial.centreId, 493 centre_lock_until: lockUntil, 494 centre_lock_source: "sale", 495 updated_at_iso: now, 496 }, 497 }); 498 ownedProspects.push(prospect); 499 for (const prospectRef of uniqueReferences([prospect.id, prospect.local_id])) prospectReferences.add(prospectRef); 500 finalized.prospects += 1; 501 } 502 503 for (const ref of callRefs) { 504 const call = await findLogicalRow(context, "appels", ref); 505 if (!call) throw new Error(`Appel lié introuvable (${ref}).`); 506 const owner = clean(call.commercial_id); 507 if (owner && !commercial.aliases.has(owner)) throw new Error("L’appel lié appartient à un autre commercial."); 508 await context.call("tables", "rows_update", { 509 table: "appels", 510 id: Number(call.id), 511 fields: { 512 status: "termine", 513 issue: "vente", 514 motif: "vente", 515 next_callback_at: null, 516 followup_status: "done", 517 callback_status: "done", 518 missed_pool_status: "completed", 519 missed_claim_expires_at: null, 520 status_at: now, 521 }, 522 }); 523 updatedCallIds.add(Number(call.id)); 524 rememberCall(callsByReference, callReferences, call); ··· 703export function creationPayloadSignature(rows, body = {}) { 704 // These are submission timestamps emitted anew by the browser on retry, 705 // not business dates. Keep every sale/contract/amount/person field binding. 706 const semanticRows = rows.map(input => { 707 const { commercial_status_at: _submittedAt, source, ...sale } = input; 708 const { created_at: _createdAt, ...metadata } = jsonObject(source); 709 return { ...sale, source: metadata }; 710 }); 711 return JSON.stringify(stablePayload({ rows: semanticRows, 712 new_prospect: body.new_prospect || null, origin_prospect_id: body.origin_prospect_id || null })); 713} 714 715function saleCreationId(input, index, requestId, commercial) { 716 const localId = clean(input?.local_id) || `vente:${commercial.id}:${requestId}:${index + 1}:${clean(input?.vente_role) || "principale"}`; 717 if (!localId.startsWith(`vente:${commercial.id}:${requestId}:`)) throw new Error("Identifiant de vente incohérent."); 718 return localId; 719} 720 721function buildSale({ input, index, requestId, commercial, offers, rules, remunerationRules, now }) { 722 if (!input || typeof input !== "object") throw new Error("Vente invalide."); 723 const offerId = clean(input.offre_id); 724 const offer = offers.get(offerId); 725 if (!offer) throw new Error(`Offre introuvable (${offerId || index + 1}).`); 726 const saleType = clean(input.vente_type || "standard") || "standard"; 727 const saleDate = effectiveDate(input.date_vente || now); 728 const revenue = resolveRevenueSnapshot({ rules, offer, offerId, saleType, centreId: commercial.centreId, saleDate }); 729 const remuneration = resolveRemunerationSnapshot({ 730 rules: remunerationRules, 731 offerId, 732 saleType, 733 centreId: commercial.centreId, 734 saleDate, 735 revenueCents: revenue.revenue_cents_snapshot, 736 }); 737 const localId = saleCreationId(input, index, requestId, commercial); 738 const source = jsonObject(input.source); 739 return { 740 ...input, 741 local_id: localId, 742 centre_appel_id: commercial.centreId, 743 commercial: commercial.name, 744 commercial_id: commercial.id, 745 offre_id: offerId, 746 vente_type: saleType, 747 etat: clean(input.etat || "signee") || "signee", 748 economic_role: revenue.economic_role, 749 revenue_cents_snapshot: revenue.revenue_cents_snapshot, 750 revenue_currency: revenue.revenue_currency, 751 revenue_rule_id: revenue.revenue_rule_id, 752 validated_at: countableStatus(input.etat || "signee") ? now : null, 753 prime_montant: remuneration?.prime_montant ?? null, 754 prime_devise: remuneration?.prime_devise || null, 755 prime_rule_id: remuneration?.prime_rule_id || null, 756 source: { 757 ...source, 758 app: clean(source.app) || (commercial.actorId === commercial.authUserId ? "commercial_app" : "admin_manual"), 759 request_id: requestId, 760 actor_auth_user_id: commercial.actorId, 761 target_commercial_id: commercial.id, 762 }, 763 }; 764} 765 766export function resolveRevenueSnapshot({ rules, offer, offerId, saleType, centreId, saleDate }) { 767 const candidates = array(rules) 768 .filter((row) => clean(row.offre_id) === offerId) 769 .filter((row) => clean(row.vente_type || "standard") === saleType) 770 .filter((row) => clean(row.calculation_type || "fixed").toLowerCase() === "fixed") 771 .filter((row) => active(row.active) && effectiveOn(row, saleDate)) 772 .filter((row) => clean(row.scope_type || "global") === "global" 773 || (clean(row.scope_type) === "centre" && clean(row.scope_id) === centreId)) 774 .sort(compareRules); 775 const selected = candidates[0] || null; 776 const role = economicRole(selected?.economic_role, inferEconomicRole(offer)); 777 return { 778 economic_role: role, 779 revenue_cents_snapshot: selected && Number.isFinite(Number(selected.revenue_cents)) 780 ? Math.max(0, Number(selected.revenue_cents)) 781 : null, 782 revenue_currency: selected ? clean(selected.currency || "EUR") || "EUR" : null, 783 revenue_rule_id: selected ? clean(selected.local_id || selected.id) || null : null, 784 }; 785} 786 787function resolveRemunerationSnapshot({ rules, offerId, saleType, centreId, saleDate, revenueCents }) { 788 const applicable = array(rules) 789 .filter((row) => clean(row.offre_id) === offerId) 790 .filter((row) => active(row.active) && effectiveOn(row, saleDate)) 791 .filter((row) => clean(row.scope_type || "global") === "global" 792 || (clean(row.scope_type) === "centre" && clean(row.scope_id) === centreId))