@cemscale-voip/voip-sdk 2.0.11 → 2.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,78 +43,475 @@ await voip.transfer(callUuid, { targetExtension: '1002', type: 'blind' });
43
43
  await voip.hangup(callUuid);
44
44
  ```
45
45
 
46
- ### Three-Way Calling (Conferencia Tripartita)
46
+ ### Three-Way Calling (Conferencia Tripartita) — GUÍA COMPLETA
47
47
 
48
- Three-way calling lets an agent merge an active call with a third party into a
49
- conference room. The SDK provides two flows:
48
+ > **⚠️ LEE ESTO ANTES DE IMPLEMENTAR.** La tripartita tiene un flujo de varios pasos
49
+ > y varios estados de UI. No es una sola llamada API. Si muestras el timer antes
50
+ > de que la persona conteste, o llamas merge antes de que el participante esté listo,
51
+ > TODO el flujo se rompe.
50
52
 
51
- **Flow 1 — `addCallParticipant` + `mergeCalls` (WebRTC softphone)**
53
+ ---
54
+
55
+ #### RESUMEN del flujo
56
+
57
+ ```
58
+ PASO 0: El agente está en llamada con Persona A. currentCall.state === 'established'.
59
+ PASO 1: El agente presiona "Agregar" → addParticipant('numero de Persona B').
60
+ La API pone a Persona A en hold y origina llamada a Persona B.
61
+ threeWaySession.state → 'adding' ← MOSTRAR "Llamando a Persona B..."
62
+ PASO 2: Persona B contesta su teléfono.
63
+ threeWaySession.state → 'adding' ← se mantiene igual, NO cambia solo
64
+ PASO 3: El agente presiona "Fusionar" → mergeThreeWay().
65
+ Las 3 personas (Agente, A, B) entran a conferencia.
66
+ threeWaySession.state → 'active' ← AHORA sí inicia el timer de conferencia
67
+ ```
68
+
69
+ ---
52
70
 
53
- The SDK's `useVoIP` hook handles UUID resolution automatically. After a call
54
- connects, `currentCall.fsChannelUuid` is populated and can be used directly.
71
+ #### FLOW 1 — `addParticipant` + `mergeThreeWay` (WebRTC softphone, recomendado)
72
+
73
+ Este es el flujo con el hook `useVoIP`. **El hook maneja TODO automáticamente:**
74
+ resolución de UUID de FreeSWITCH, estados de la tripartita, y limpieza al colgar.
75
+
76
+ ##### 1.1 — Configuración inicial
55
77
 
56
78
  ```typescript
57
- const { currentCall, addParticipant, mergeThreeWay, swapParticipant, kickParticipant } = useVoIP({
79
+ import { useVoIP } from '@cemscale-voip/voip-sdk';
80
+
81
+ // Dentro de tu componente React:
82
+ const {
83
+ // Estado del teléfono
84
+ isRegistered, // boolean — SIP registrado?
85
+ currentCall, // WebRTCCallInfo | null — datos de la llamada activa
86
+ error, // string | null — último error (resetear con clearError)
87
+
88
+ // Acciones de llamada básica
89
+ startPhone, // (ext, password, displayName?) => Promise<void>
90
+ call, // (number) => Promise<WebRTCCallInfo>
91
+ answer, // () => Promise<void>
92
+ hangup, // () => Promise<void>
93
+ toggleHold, // () => Promise<boolean>
94
+ toggleMute, // () => boolean
95
+
96
+ // Acciones de tripartita
97
+ addParticipant, // (target: string) => Promise<void>
98
+ mergeThreeWay, // () => Promise<void>
99
+ swapParticipant, // (keepUuid: string, holdUuid: string) => Promise<void>
100
+ dropParticipant, // (memberId: string) => Promise<void>
101
+
102
+ // Estado de la tripartita
103
+ threeWaySession, // ThreeWaySession | null
104
+ } = useVoIP({
58
105
  apiUrl: 'https://voip-api.cemscale.com',
59
- apiKey: 'csk_live_...',
106
+ apiKey: 'csk_live_...', // API key de tu tenant
60
107
  });
108
+ ```
61
109
 
62
- // Step 1 — Call Person A (normal call)
63
- await call('+15551234567');
64
- // Wait for state: 'established'
110
+ ##### 1.2`ThreeWaySession` (tipo completo)
65
111
 
66
- // Step 2 Add Person B (Person A goes on hold automatically)
67
- await addParticipant('+15559999999');
68
- // Person B is now ringing. When they answer:
112
+ Este objeto describe TODO el estado de la tripartita. Tu UI debe leerlo para decidir qué mostrar.
69
113
 
70
- // Step 3 — Merge all three into conference
71
- await mergeThreeWay();
72
- // All three parties in conference. currentCall is now the conference room.
114
+ ```typescript
115
+ interface ThreeWaySession {
116
+ conferenceName: string; // Nombre de la sala de conferencia (UUID, ej: "page_abc_123")
117
+ state: 'adding' | 'active' | 'swapping';
118
+ // 'adding' = Persona B está siendo llamada. MOSTRAR "Llamando..."
119
+ // 'active' = Conferencia activa. AHORA iniciar timer.
120
+ // 'swapping' = Intercambiando participantes (no mostrar timer propio)
121
+ participants: ThreeWayParticipant[]; // Solo poblado en state='active'
122
+ pendingUuid: string | undefined; // UUID de Persona B. Existe en state='adding'
123
+ originalBridgedLeg: string | undefined; // UUID de la pata PSTN de Persona A
124
+ }
125
+
126
+ interface ThreeWayParticipant {
127
+ uuid: string; // UUID del canal FreeSWITCH
128
+ callerIdNumber: string; // Número de teléfono
129
+ callerIdName: string; // Nombre (o número si no hay nombre)
130
+ status: 'active' | 'held'; // Estado en la conferencia
131
+ memberId: string; // ID de miembro en la conferencia
132
+ }
133
+ ```
73
134
 
74
- // BonusSwap: go private with one participant
75
- await swapParticipant(keepUuid, holdUuid);
135
+ ##### 1.3`WebRTCCallInfo` (campos relevantes para tripartita)
76
136
 
77
- // Bonus — Kick one participant
78
- await kickParticipant(memberId);
137
+ ```typescript
138
+ interface WebRTCCallInfo {
139
+ id: string; // ID local de SIP.js — ** NO USAR para API calls **
140
+ fsChannelUuid: string | null; // UUID de FreeSWITCH — ** USAR ESTE para API calls **
141
+ // null mientras la llamada está conectando
142
+ // poblado automáticamente cuando state === 'established'
143
+ state: string; // 'ringing' | 'connecting' | 'established' | 'held' | 'terminated' | ...
144
+ direction: 'inbound' | 'outbound';
145
+ remoteIdentity: string; // Número remoto (ej: '+17866302522')
146
+ remoteDisplayName: string; // Nombre mostrado
147
+ held: boolean;
148
+ muted: boolean;
149
+ }
79
150
  ```
80
151
 
81
- **Flow 2`mergeDirectCalls` (desk phone / manual)**
152
+ ##### 1.4Flujo COMPLETO paso a paso
82
153
 
83
- For physical SIP phones where the user manually holds call A and dials call B.
84
- Does NOT require `addCallParticipant()` first.
154
+ ```typescript
155
+ // ═══════════════════════════════════════════════════════════
156
+ // PASO 0: Agente en llamada con Persona A
157
+ // ═══════════════════════════════════════════════════════════
158
+
159
+ // currentCall.state === 'established' ← CONDICIÓN para mostrar botón "Agregar"
160
+ // currentCall.fsChannelUuid !== null ← resuelto automáticamente
161
+ // threeWaySession === null ← no hay tripartita activa
162
+
163
+ // ═══════════════════════════════════════════════════════════
164
+ // PASO 1: Agregar Persona B
165
+ // ═══════════════════════════════════════════════════════════
166
+
167
+ // UI: Botón "Agregar participante" → input para número → llama a addParticipant
168
+
169
+ try {
170
+ await addParticipant('+17866302522');
171
+ // └─ Internamente el SDK:
172
+ // 1. Toma currentCall.fsChannelUuid
173
+ // 2. Llama POST /api/calls/{fsChannelUuid}/add-participant
174
+ // 3. API pone Persona A en hold (escucha música)
175
+ // 4. API origina llamada a Persona B vía trunk
176
+ // 5. Retorna { newCallUuid, originalBridgedLeg }
177
+ // 6. SDK setea threeWaySession = { state: 'adding', pendingUuid, ... }
178
+
179
+ // ⚠️ DESPUÉS DE ESTA LLAMADA:
180
+ // threeWaySession.state === 'adding' ← MOSTRAR "Llamando a Persona B..."
181
+ // threeWaySession.pendingUuid !== undefined ← Persona B está sonando
182
+ // currentCall aún existe (Persona A en hold)
183
+ // ❌ NO iniciar timer — Persona B NO ha contestado todavía
184
+ // ❌ NO llamar a mergeThreeWay() todavía — Persona B no ha contestado
185
+
186
+ } catch (err) {
187
+ // addParticipant falló. Persona A vuelve de hold automáticamente.
188
+ console.error('Error al agregar participante:', err);
189
+ }
190
+
191
+ // ═══════════════════════════════════════════════════════════
192
+ // PASO 2: Persona B contesta (el agente lo escucha o ve en UI)
193
+ // ═══════════════════════════════════════════════════════════
194
+
195
+ // El SDK NO notifica automáticamente cuando Persona B contesta.
196
+ // La UI debe mostrar un botón "Fusionar" inmediatamente después de
197
+ // addParticipant. El agente presiona "Fusionar" cuando escucha
198
+ // que Persona B contestó, o cuando ve que pasaron ~10 segundos.
199
+
200
+ // ═══════════════════════════════════════════════════════════
201
+ // PASO 3: Fusionar en conferencia
202
+ // ═══════════════════════════════════════════════════════════
203
+
204
+ // UI: Botón "Fusionar" — visible cuando threeWaySession.state === 'adding'
205
+
206
+ try {
207
+ await mergeThreeWay();
208
+ // └─ Internamente el SDK:
209
+ // 1. Toma currentCall.fsChannelUuid (callUuidA)
210
+ // 2. Toma threeWaySession.pendingUuid (callUuidB)
211
+ // 3. Toma threeWaySession.originalBridgedLeg
212
+ // 4. Llama POST /api/calls/three-way/merge
213
+ // 5. API mueve todos a conferencia
214
+ // 6. SDK setea threeWaySession = { state: 'active', participants: [...] }
215
+
216
+ // ✅ DESPUÉS DE ESTA LLAMADA:
217
+ // threeWaySession.state === 'active' ← AHORA sí iniciar timer de conferencia
218
+ // threeWaySession.participants.length >= 2 ← Persona A + Persona B (más el agente)
219
+ // currentCall sigue activo (el agente está en la conferencia)
220
+
221
+ } catch (err) {
222
+ console.error('Error al fusionar:', err);
223
+ }
224
+ ```
225
+
226
+ ##### 1.5 — Qué mostrar en la UI en CADA estado
85
227
 
86
228
  ```typescript
87
- // Detect 2+ active calls from the same extension
88
- const { activeCalls } = await voip.getActiveCalls();
89
- const userCalls = activeCalls.filter(
90
- c => c.caller_id_number === '1001' || c.destination === '1001'
91
- );
229
+ // ── SIN LLAMADA ──
230
+ if (!currentCall) {
231
+ return <Dialpad onCall={call} />;
232
+ }
92
233
 
93
- if (userCalls.length >= 2) {
94
- await voip.mergeDirectCalls({
95
- callUuidA: userCalls[0].uuid, // FreeSWITCH channel UUID
96
- callUuidB: userCalls[1].uuid,
97
- });
98
- // Both calls merged into conference. User stays on call A's path.
234
+ // ── LLAMADA ACTIVA, SIN TRIPARTITA ──
235
+ if (currentCall && !threeWaySession) {
236
+ return (
237
+ <InCallScreen>
238
+ <CallTimer startTime={currentCall.answerTime} />
239
+ <RemoteParty name={currentCall.remoteDisplayName} number={currentCall.remoteIdentity} />
240
+ <MuteButton active={currentCall.muted} onPress={toggleMute} />
241
+ <HoldButton active={currentCall.held} onPress={toggleHold} />
242
+ <HangupButton onPress={hangup} />
243
+ <AddParticipantButton onPress={() => showAddParticipantInput()} />
244
+ {/* ↑ Solo mostrar si currentCall.state === 'established' */}
245
+ </InCallScreen>
246
+ );
247
+ }
248
+
249
+ // ── AGREGANDO PARTICIPANTE (Persona B está sonando) ──
250
+ if (threeWaySession?.state === 'adding') {
251
+ return (
252
+ <InCallScreen>
253
+ {/* Persona A sigue en hold — mostrar timer de Persona A */}
254
+ <CallTimer startTime={currentCall.answerTime} />
255
+ <RemoteParty name={currentCall.remoteDisplayName} number={currentCall.remoteIdentity} />
256
+ <HoldIndicator /> {/* Persona A está en hold */}
257
+
258
+ {/* Persona B */}
259
+ <CallingBanner>
260
+ <Spinner />
261
+ <Text>Llamando a Persona B...</Text>
262
+ {/* ❌ NO mostrar timer para Persona B — no ha contestado */}
263
+ </CallingBanner>
264
+
265
+ <MergeButton onPress={mergeThreeWay} />
266
+ {/* ↑ El agente presiona esto cuando escucha que Persona B contestó */}
267
+
268
+ <CancelButton onPress={hangup} />
269
+ {/* ↑ Cancela todo: cuelga Persona A, Persona B, y al agente */}
270
+ </InCallScreen>
271
+ );
272
+ }
273
+
274
+ // ── CONFERENCIA ACTIVA ──
275
+ if (threeWaySession?.state === 'active') {
276
+ return (
277
+ <ConferenceScreen>
278
+ <ConferenceHeader>
279
+ <ConferenceIcon />
280
+ <Text>Conferencia — 3 participantes</Text>
281
+ <ConferenceTimer startTime={conferenceStartTime} />
282
+ {/* ↑ AHORA sí iniciar timer */}
283
+ </ConferenceHeader>
284
+
285
+ {threeWaySession.participants.map(p => (
286
+ <ParticipantRow key={p.uuid}>
287
+ <ParticipantName name={p.callerIdName} number={p.callerIdNumber} />
288
+ <ParticipantStatus status={p.status} />
289
+ {/* p.status === 'held' → mostrar "En espera" */}
290
+ <SwapButton onPress={() => swapParticipant(p.uuid, getOtherActiveUuid(p))} />
291
+ {/* ↑ Solo mostrar si hay >1 participante active */}
292
+ <DropButton onPress={() => dropParticipant(p.memberId)} />
293
+ </ParticipantRow>
294
+ ))}
295
+
296
+ <MuteButton active={currentCall.muted} onPress={toggleMute} />
297
+ <EndConferenceButton onPress={hangup} />
298
+ {/* ↑ Cuelga a TODOS y destruye la conferencia */}
299
+ </ConferenceScreen>
300
+ );
99
301
  }
100
302
  ```
101
303
 
102
- **CRITICALUUID types:**
304
+ ##### 1.6 Errores comunes y cómo manejarlos
103
305
 
104
- | ID | Source | Used for |
105
- |---|---|---|
106
- | `call.id` | SIP.js local session ID | NOT valid for API calls |
107
- | `call.fsChannelUuid` | Resolved from FreeSWITCH | `addCallParticipant`, `mergeCalls` |
108
- | `call.uuid` (from ActiveCall) | `getActiveCalls()` response | `mergeDirectCalls` |
109
- | `result.newCallUuid` | Response from `addCallParticipant` | Pass to `mergeCalls` as `callUuidB` |
306
+ | Error | Causa | Solución |
307
+ |-------|-------|----------|
308
+ | `"FreeSWITCH UUID not resolved yet"` | Llamaste `addParticipant` antes de que la llamada conecte | Espera a `currentCall.state === 'established'` y `currentCall.fsChannelUuid !== null` |
309
+ | `"No active call"` | `currentCall` es null | Verifica que haya una llamada activa antes de mostrar el botón |
310
+ | `"No pending participant to merge"` | `threeWaySession.pendingUuid` es undefined | Solo mostrar botón "Fusionar" cuando `threeWaySession?.state === 'adding'` y `threeWaySession?.pendingUuid` existe |
311
+ | Timer se inicia antes de contestar | Tu UI inicia el timer en state='adding' | Solo iniciar timer cuando `threeWaySession.state === 'active'` |
312
+ | La llamada se cuelga al hacer merge | Llamaste merge antes de que Persona B conteste | Asegúrate de que Persona B ya contestó antes de fusionar |
313
+ | `addParticipant` lanza error 500 | El backend no pudo originar a Persona B | Mostrar el mensaje de error al agente. La llamada con Persona A se reanuda sola. |
110
314
 
111
- **Manual UUID resolution (for custom UIs):**
315
+ ---
316
+
317
+ #### FLOW 2 — `mergeDirectCalls` (teléfono físico / desk phone)
318
+
319
+ Para cuando el agente usa un teléfono físico (Yealink, Grandstream) y manualmente:
320
+ 1. Llama a Persona A
321
+ 2. Pone en hold
322
+ 3. Marca a Persona B
323
+ 4. Quiere fusionar ambas
324
+
325
+ **NO usa `addParticipant`.** Solo detecta las 2 llamadas activas y las fusiona.
112
326
 
113
327
  ```typescript
114
- const fsUuid = await voip.resolveCallFsUuid('1001', '+15551234567', 'outbound');
115
- if (fsUuid) {
116
- const result = await voip.addCallParticipant(fsUuid, { toNumber: '+15559999999' });
328
+ import { VoIPClient } from '@cemscale-voip/voip-sdk';
329
+
330
+ const voip = new VoIPClient({
331
+ apiUrl: 'https://voip-api.cemscale.com',
332
+ apiKey: 'csk_live_...',
333
+ });
334
+
335
+ // ═══════════════════════════════════════════════════════
336
+ // PASO 1: Detectar 2+ llamadas activas de la misma extensión
337
+ // ═══════════════════════════════════════════════════════
338
+
339
+ // Hacer polling cada 5 segundos
340
+ const pollInterval = setInterval(async () => {
341
+ const { activeCalls } = await voip.getActiveCalls();
342
+
343
+ // Filtrar llamadas de la extensión del agente
344
+ const agentExt = '1001';
345
+ const myCalls = activeCalls.filter(c =>
346
+ c.caller_id_number === agentExt || c.destination === agentExt
347
+ );
348
+
349
+ // ¿Hay 2 o más llamadas activas?
350
+ if (myCalls.length >= 2) {
351
+ // Mostrar botón "Fusionar" con opción de seleccionar cuáles 2
352
+ showMergeButton(myCalls);
353
+ } else {
354
+ hideMergeButton();
355
+ }
356
+ }, 5000);
357
+
358
+ // ═══════════════════════════════════════════════════════
359
+ // PASO 2: Fusionar las 2 llamadas seleccionadas
360
+ // ═══════════════════════════════════════════════════════
361
+
362
+ async function handleMerge(callA: ActiveCall, callB: ActiveCall) {
363
+ try {
364
+ const result = await voip.mergeDirectCalls({
365
+ callUuidA: callA.uuid, // ⚠️ uuid de FreeSWITCH, NO call_uuid
366
+ callUuidB: callB.uuid,
367
+ });
368
+
369
+ console.log('Conferencia creada:', result.conferenceName);
370
+ // result.participants → Array con los 3 participantes
371
+ // La conferencia se destruye sola cuando el agente cuelga
372
+
373
+ } catch (err) {
374
+ console.error('Error al fusionar:', err);
375
+ }
376
+ }
377
+
378
+ // ═══════════════════════════════════════════════════════
379
+ // LIMPIEZA
380
+ // ═══════════════════════════════════════════════════════
381
+
382
+ // Cuando el componente se desmonta:
383
+ clearInterval(pollInterval);
384
+ ```
385
+
386
+ ##### 2.1 — Formato de `ActiveCall` (de `getActiveCalls()`)
387
+
388
+ ```typescript
389
+ interface ActiveCall {
390
+ uuid: string; // UUID del canal FreeSWITCH — USAR ESTE
391
+ call_uuid: string; // UUID de la sesión de llamada (compartido entre A-leg y B-leg)
392
+ caller_id_number: string; // Quién llama (extensión o número externo)
393
+ caller_id_name: string; // Nombre del caller
394
+ destination: string; // Número marcado
395
+ direction: string; // 'inbound' | 'outbound'
396
+ callstate: string; // 'ACTIVE' | 'EARLY' | 'RINGING' | 'HANGUP'
397
+ answered: boolean;
398
+ on_hold: boolean;
399
+ }
400
+ ```
401
+
402
+ ##### 2.2 — IMPORTANTE: No confundir los UUIDs
403
+
404
+ ```
405
+ activeCall.uuid → "a1b2c3d4-..." ← USAR ESTE para mergeDirectCalls
406
+ activeCall.call_uuid → "a1b2c3d4-..." ← NO USAR (es el mismo que el de la otra pata)
407
+
408
+ Una llamada tiene 2 canales (A-leg y B-leg) que comparten el MISMO call_uuid
409
+ pero tienen DIFERENTE uuid. mergeDirectCalls necesita los uuid de canal.
410
+ ```
411
+
412
+ ---
413
+
414
+ #### FLOW 3 — `addCallParticipant` + `mergeCalls` sin hook (VoIPClient directo)
415
+
416
+ Si NO estás usando `useVoIP` (estás usando `VoIPClient` + `WebRTCPhone` manualmente):
417
+
418
+ ```typescript
419
+ import { VoIPClient, WebRTCPhone } from '@cemscale-voip/voip-sdk';
420
+
421
+ const voip = new VoIPClient({ apiUrl: '...', apiKey: '...' });
422
+ const phone = new WebRTCPhone({ ... });
423
+
424
+ // ════════ PASO 1: Hacer llamada a Persona A ════════
425
+ const callInfo = await phone.call('+17863402240');
426
+ // Esperar: callInfo.state === 'established'
427
+
428
+ // ════════ PASO 2: Resolver el UUID de FreeSWITCH ════════
429
+ const fsUuid = await voip.resolveCallFsUuid(
430
+ callInfo.remoteIdentity.includes('100') ? callInfo.remoteIdentity : '1001', // extensión
431
+ callInfo.remoteIdentity, // número remoto
432
+ callInfo.direction // 'outbound' | 'inbound'
433
+ );
434
+
435
+ if (!fsUuid) {
436
+ throw new Error('No se pudo resolver el UUID de FreeSWITCH. ¿La llamada sigue activa?');
117
437
  }
438
+ // fsUuid → "53c4b1ef-487c-438a-8254-c99ef17a3a26" (UUID real de FreeSWITCH)
439
+
440
+ // ════════ PASO 3: Agregar Persona B ════════
441
+ const { newCallUuid, originalBridgedLeg } = await voip.addCallParticipant(fsUuid, {
442
+ toNumber: '+17866302522', // Número externo (E.164)
443
+ // toExtension: '1004', // O extensión interna
444
+ });
445
+
446
+ // ⚠️ Persona B está SONANDO (no ha contestado)
447
+ // newCallUuid → UUID del canal de Persona B
448
+ // originalBridgedLeg → UUID de la pata PSTN de Persona A
449
+
450
+ // ════════ PASO 4: Esperar a que Persona B conteste ════════
451
+ // El agente escucha que Persona B contestó, o la UI muestra el botón "Fusionar"
452
+
453
+ // ════════ PASO 5: Fusionar ════════
454
+ const result = await voip.mergeCalls({
455
+ callUuidA: fsUuid, // UUID de FreeSWITCH de la llamada original
456
+ callUuidB: newCallUuid, // UUID de Persona B (de addCallParticipant)
457
+ bridgedLegA: originalBridgedLeg, // UUID de la pata PSTN de Persona A
458
+ conferenceName: undefined, // Auto-generado si no se especifica
459
+ });
460
+
461
+ // result.conferenceName → "page_15d0851a_..."
462
+ // result.participants → [{ callerIdNumber, callerIdName, uuid, status, memberId }, ...]
463
+ // result.state → 'active'
464
+ ```
465
+
466
+ ---
467
+
468
+ #### RESUMEN de estados y UUIDs
469
+
470
+ | Contexto | Qué UUID usar | De dónde viene |
471
+ |----------|--------------|----------------|
472
+ | `addParticipant` | `currentCall.fsChannelUuid` | Resuelto automático por useVoIP al conectar |
473
+ | `mergeThreeWay` (callUuidA) | `currentCall.fsChannelUuid` | Mismo que arriba |
474
+ | `mergeThreeWay` (callUuidB) | `threeWaySession.pendingUuid` | Retornado por `addParticipant` |
475
+ | `mergeThreeWay` (bridgedLegA) | `threeWaySession.originalBridgedLeg` | Retornado por `addParticipant` |
476
+ | `mergeDirectCalls` (callUuidA/B) | `activeCall.uuid` | De `getActiveCalls()` |
477
+ | `resolveCallFsUuid` (manual) | Retorna `string \| null` | Lo usas para `addCallParticipant` |
478
+ | `swapParticipant` (keepUuid/holdUuid) | `participant.uuid` | De `threeWaySession.participants[i].uuid` |
479
+ | `dropParticipant` (memberId) | `participant.memberId` | De `threeWaySession.participants[i].memberId` |
480
+
481
+ | ❌ NO USAR NUNCA | Por qué |
482
+ |------------------|---------|
483
+ | `currentCall.id` | Es el ID local de SIP.js, no el UUID de FreeSWITCH |
484
+ | `activeCall.call_uuid` | Es compartido entre A-leg y B-leg, no identifica un canal único |
485
+
486
+ ---
487
+
488
+ #### MÁQUINA DE ESTADOS de threeWaySession
489
+
490
+ ```
491
+ null ──addParticipant()──> { state: 'adding', pendingUuid: '...' }
492
+
493
+ mergeThreeWay()
494
+
495
+
496
+ { state: 'active', participants: [...] }
497
+
498
+ swapParticipant()
499
+
500
+
501
+ { state: 'swapping', ... }
502
+ │ (automático al completar)
503
+
504
+ { state: 'active', participants: [...] }
505
+
506
+ dropParticipant() (si quedan ≤1)
507
+
508
+
509
+ null
510
+
511
+ currentCall === null
512
+
513
+
514
+ null (limpieza automática)
118
515
  ```
119
516
 
120
517
  ### Extensions