47 const error = new Error(message); error.statusCode = statusCode; return error; 48} 49 50const DEFAULT_POOL_DELAY_MS = 2 * 60 * 1000; 51const DEFAULT_CLAIM_TTL_MS = 10 * 60 * 1000; 52const MISSED_CLAIM_EXPIRY_ENABLED = true; 53const CLAIM_ARBITRATION_TTL_MS = 30 * 1000; 54const CENTRE_LOCK_TTL_MS = 90 * 24 * 60 * 60 * 1000; 55const CLAIM_TABLE = "commercial_missed_call_claims"; 56let claimTableReady = false; 57 58export default async function handler(event, context) { 59 context = { ...context, [__authParityEvent]: event }; 60 61 const body = event?.body && typeof event.body === "object" ? event.body : event; 62 assertNotSupervisionMutation(body); 63 const requestedCommercialId = clean(body?.commercial_id); 64 const callId = clean(body?.call_id); 65 const callRowId = Number(body?.call_row_id || 0) || null; 66 const nowMs = Date.now(); 67 const now = new Date(nowMs).toISOString(); 68 69 const actor = await resolveActorCommercial(context, body, requestedCommercialId); 70 const commercialId = actor.id; 71 72 let call = await getParentCall(context, callRowId, callId); 73 if (!call) throw new Error("Appel manqué introuvable."); 74 75 // Authorization is checked before any recovery mutation. 76 if (call.followup_appel_id) await assertCommercialCanClaim(context, actor.aliases, call); 77 call = await recoverFailedCallbackPointer(context, call, actor.aliases, nowMs); 78 await assertInteractionStillMissed(context, call); 79 assertMissedCallClaimable(call, actor.aliases, nowMs); 80 const claimRoute = await assertCommercialCanClaim(context, actor.aliases, call); 81 const claimGroupId = claimRoute.groupId; 82 83 const rowId = Number(call.id || callRowId || 0); 84 if (!rowId) throw new Error("Identifiant technique de l'appel introuvable."); 85 86 const linkedProspect = await getLinkedProspect(context, call); 87 const prospectCentreLock = linkedProspect 88 ? await resolveProspectCentreLock(context, linkedProspect, new Date(nowMs)) 89 : null; 90 assertClaimantMatchesCentreLock(actor, prospectCentreLock); 91 92 const expiresAt = MISSED_CLAIM_EXPIRY_ENABLED 93 ? new Date(nowMs + DEFAULT_CLAIM_TTL_MS).toISOString() 94 : null; 95 // The claim row below is the durable 10-minute reservation. This lock only arbitrates 96 // concurrent requests and must never keep an agent blocked for the whole claim TTL. 97 const arbitrationExpiresAt = new Date(nowMs + CLAIM_ARBITRATION_TTL_MS).toISOString(); 98 const currentOwner = clean(call.missed_claimed_by); 99 const currentExpiry = claimExpiryMs(call); 100 if (currentOwner && actor.aliases.has(currentOwner) && (!currentExpiry || currentExpiry > nowMs)) { 101 return claimResponse(call, commercialId, rowId); 102 } 103 104 await ensureClaimTable(context); 105 const commercialLock = await acquireClaimLock(context, { 106 callRef: `commercial:${commercialId}`, 107 rowId,