Ciclo de vida
Una máquina de estados por cada cotización. Toda respuesta de la API y todo
webhook, en cualquiera de las dos direcciones, lleva quote_status tomado de
este enum, de modo que los registros de Tesser y los de Nexa coinciden en el
mismo vocabulario cuando hay que investigar algo.
Alcance del MVP
- USDC es el único activo cripto; GTQ, la única moneda fiat.
- Un solo cliente:
FELIX(enum cerrado). Generalizar hacia uncustomer_idqueda pendiente para después. - Cada cliente tiene exactamente una billetera y una cuenta bancaria en Nexa.
- Un solo beneficiario fijo. Nexa siempre paga al mismo destino, acordado fuera de banda y que nunca aparece en el cuerpo de una llamada de API. Ese beneficiario es el socio de desembolso, que ejecuta el desembolso de última milla hacia los receptores de remesas individuales.
- Nexa no participa en la última milla. No hay datos de beneficiario por destinatario, ni objetos de destino, ni campos de cumplimiento del beneficiario. Que Nexa asuma los desembolsos de remesas de última milla es una etapa posterior del programa y queda fuera de alcance aquí.
- Un pago por cotización. Los depósitos sí se pueden repetir dentro de una misma cotización (lo exige el flujo de fondos adicionales), pero el caso normal es uno solo.
- Sin paginación y sin endpoints de listado. Un solo entorno.
Reglas del contrato
Cuatro reglas resuelven la mayoría de los casos límite de esta integración.
- El dinero siempre es una cadena decimal. GTQ con 2 decimales, USDC con 6 decimales, precio con 6 decimales. Nunca un número JSON.
- Nexa calcula todos los montos derivados. Tesser repite
usdc_amounty el GTQ acreditado; nunca divide por su cuenta. - Reportar un depósito no es solicitar nada. Un
2xxsignifica "registrado". La decisión de Nexa de acreditar, pedir más fondos o rechazar llega después por webhook. - Todo POST es idempotente sobre un ID generado por quien llama. Un
reintento devuelve el objeto actual; un cuerpo que entra en conflicto
devuelve
409. Los reintentos de red nunca pueden fallar en falso.
El flujo completo en un solo diagrama
Cada una de las cinco páginas de fases que siguen muestra su propia porción de
este diagrama. [N] = alojado en Nexa (Tesser lo llama), [T] = alojado en
Tesser (Nexa lo llama); dentro del diagrama esas mismas etiquetas aparecen
como [N] y [T].
Ciclo de vida de la cotización (vocabulario de estados compartido)
Tabla de estados
quote_status | Lo fija | ¿Terminal? | Significado |
|---|---|---|---|
CREATED | Crear cotización [N] | no | La cotización existe con un precio y un expires_at. Todavía no está bloqueada. |
ACCEPTED | Aceptar cotización [N] | no | Tipo de cambio bloqueado. Nexa lo mantiene hasta deposit_deadline. |
DEPOSIT_REPORTED | Reportar depósito de USDC [N] | no | Tesser dio fe de al menos una entrega de USDC. Nexa debe una decisión. |
CREDIT_SENT | webhook quote.credit.sent [T] | no | Nexa decidió acreditar; la instrucción ya se envió al core bancario. |
CREDIT_CONFIRMED | webhook quote.credit.confirmed [T] | no | El GTQ está disponible en la cuenta de Felix en Nexa. El gtq_amount de aquí es el autoritativo. |
PAYMENT_COMPLETED | Crear pago [N], en la respuesta | sí, salvo que después llegue un evento de falla | Desembolso realizado al beneficiario fijo. |
PAYMENT_FAILED | Crear pago [N] en la respuesta (rechazo sincrónico), o el webhook payment.status.changed [T] (falla posterior a una respuesta COMPLETED) | sí | El desembolso fue rechazado o falló. En el POC la recuperación es manual. |
PAYMENT_RETURNED | webhook payment.status.changed [T] | sí | La red de pagos devolvió el desembolso. En el POC la recuperación es manual. |
EXPIRED | el tiempo | sí | Venció el plazo de aceptación o el plazo de depósito sin que se reportara nada. |
REJECTED | webhook quote.rejected [T] | sí | Nexa se niega a acreditar. El USDC ya se movió; ver más abajo. |
Reglas que codifica el diagrama
- A
EXPIREDsolo se llega desdeCREATED(se perdió el plazo de aceptación) y desdeACCEPTED(no se reportó ningún depósito antes dedeposit_deadline). En cuanto se reporta cualquier depósito a tiempo, la cotización ya no expira sola, y Nexa debe una decisión. REJECTEDes la decisión de Nexa posterior a un reporte de depósito (a través del webhookquote.rejected), nunca un error sincrónico de la llamada de depósito. Después del rechazo,GET /v1/quotes/{tesser_quote_id}devuelvequote_status: "REJECTED".- Una solicitud de fondos adicionales no cambia el estado: la cotización
permanece en
DEPOSIT_REPORTEDy va acumulando reportes de depósito mientras la decisión de Nexa sigue pendiente. Los reportes tardíos, posteriores a una decisión, también se aceptan. - Los estados terminales son
EXPIRED,REJECTED,PAYMENT_COMPLETED,PAYMENT_FAILEDyPAYMENT_RETURNED. La recuperación desdePAYMENT_FAILED/PAYMENT_RETURNED, y el destino del USDC después deREJECTED, se manejan de forma manual y fuera de banda en el POC (preguntas abiertas 4 y 7). - El desembolso es sincrónico. La respuesta de la creación del pago
lleva el resultado:
PAYMENT_COMPLETED, oPAYMENT_FAILEDcuando Nexa rechaza el desembolso o no puede ejecutarlo durante la llamada. No existe un webhook de éxito, y el único evento del desembolso es una falla que aparece en la red de pagos después de una respuestaCOMPLETED— por esoPAYMENT_COMPLETEDes el único estado terminal del que todavía se puede salir, haciaPAYMENT_FAILEDoPAYMENT_RETURNED. Cuánto tiempo después de una respuestaCOMPLETEDpuede aparecer todavía esa falla es la pregunta abierta 5.
El rechazo no deshace nada
El USDC ya se movió en la cadena antes de que siquiera se llame al endpoint de
depósito. Una decisión quote.rejected significa "Nexa no va a acreditar
GTQ", no "el depósito no ocurrió". Qué pasa con el USDC recibido es un asunto
operativo y fuera de banda para el POC. Se señala aquí porque es el caso
límite con mayor probabilidad de provocar un incidente entre ambas empresas.
Glosario
| Término | Significado |
|---|---|
tesser_quote_id | UUIDv4 generado por Tesser antes de la llamada de creación de la cotización. Funciona además como clave de idempotencia y es la referencia cruzada de toda llamada y todo webhook posterior. |
nexa_quote_id | La referencia propia de Nexa para la cotización, devuelta para soporte y conciliación. |
tesser_deposit_id | UUIDv4 generado por Tesser para cada reporte de depósito; es la clave de idempotencia de ese reporte. |
nexa_payment_id | El identificador de Nexa para el desembolso en GTQ. |
quote_status | El valor de la máquina de estados compartida que viaja en cada respuesta y cada webhook. |
payment_status | El resultado del desembolso. COMPLETED o FAILED pueden volver ambos de forma sincrónica en la creación del pago; RETURNED, y un FAILED que solo aparece después de una respuesta COMPLETED, llegan por el webhook de falla. |
usdc_gtq_price | GTQ por 1 USDC, en cadena de texto, con un máximo de 6 decimales. |
quoted_gtq | El GTQ que Nexa realmente va a entregar. Puede ser menor que desired_gtq (una ejecución parcial). |
usdc_amount | El USDC que Felix debe entregar: quoted_gtq / usdc_gtq_price, redondeado hacia arriba a 6 decimales. Lo calcula Nexa. |
| Custodia | La billetera BitGo de Nexa, cuyas llaves tiene Tesser. Ahí llega el USDC de Felix. |
Ejemplo trabajado, usado en todos los ejemplos y en todas las respuestas
de los stubs de este portal: Felix quiere GTQ 250000.00; Nexa cotiza
7.745100 GTQ por USDC; entonces usdc_amount es 32278.472841 (redondeado
hacia arriba a 6 decimales). Comprobación del redondeo:
32278.472841 × 7.745100 = 250000.0000008…, que cubre quoted_gtq.