# 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 un `customer_id`
  queda 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.

1. **El dinero siempre es una cadena decimal.** GTQ con 2 decimales, USDC con
   6 decimales, precio con 6 decimales. Nunca un número JSON.
2. **Nexa calcula todos los montos derivados.** Tesser repite `usdc_amount` y
   el GTQ acreditado; nunca divide por su cuenta.
3. **Reportar un depósito no es solicitar nada.** Un `2xx` significa
   "registrado". La decisión de Nexa de acreditar, pedir más fondos o
   rechazar llega después por webhook.
4. **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]`.

<Mermaid
  chart={`sequenceDiagram
    autonumber
    participant T as Tesser (orquestador)
    participant N as Banco Nexa

    Note over T,N: Fase 1 · Cotización
    T->>N: POST /v1/quotes [N]
    N-->>T: 201 CREATED (precio, quoted_gtq, usdc_amount, expires_at)
    T->>N: POST /v1/quotes/{tesser_quote_id}/accept [N]
    N-->>T: 200 ACCEPTED (deposit_deadline)

    Note over T,N: Fase 2 · Depósito de USDC (Felix → BitGo, observado por Tesser)
    T->>N: POST /v1/quotes/{tesser_quote_id}/deposits [N]<br/>(usdc_amount, evaluación de riesgo, hashes de transacción)
    N-->>T: 200 DEPOSIT_REPORTED (solo acuse de recibo)

    Note over T,N: Fase 3 · Decisión de acreditación de Nexa (asincrónica)
    alt Nexa acredita
        N->>T: webhook quote.credit.sent [T]
        N->>T: webhook quote.credit.confirmed (gtq_amount autoritativo) [T]
    else Nexa pide más fondos
        N->>T: webhook quote.additional_funds_requested [T]
        T->>N: POST .../deposits (otro reporte) [N]
    else Nexa rechaza
        N->>T: webhook quote.rejected [T]
    end

    Note over T,N: Fase 4 · Desembolso en GTQ al beneficiario fijo (sincrónico)
    T->>N: POST /v1/payments (verificación cruzada de gtq_amount) [N]
    N-->>T: 201 con el resultado: pago COMPLETED, o FAILED si se rechaza de forma sincrónica
    opt Falla que aparece después de una respuesta COMPLETED
        N->>T: webhook payment.status.changed (FAILED / RETURNED) [T]
    end

    Note over T,N: Fase 5 · Tesorería de Nexa (API principal existente de Tesser)
    N->>T: GET /v1/accounts/{id} [T]
    N->>T: POST /v1/treasury/withdrawals [T]
    T->>N: Webhooks de Tesser: estado del retiro → receptor de Nexa [N]`}
/>

## Ciclo de vida de la cotización (vocabulario de estados compartido)

<Mermaid
  chart={`stateDiagram-v2
    direction LR
    [*] --> CREATED
    CREATED --> ACCEPTED: aceptación (op 2)
    CREATED --> EXPIRED: pasa expires_at
    ACCEPTED --> DEPOSIT_REPORTED: reporte de depósito (op 3)
    ACCEPTED --> EXPIRED: pasa deposit_deadline sin reportes
    DEPOSIT_REPORTED --> DEPOSIT_REPORTED: quote.additional_funds_requested (sin cambio de estado)
    DEPOSIT_REPORTED --> CREDIT_SENT: quote.credit.sent
    DEPOSIT_REPORTED --> REJECTED: quote.rejected
    CREDIT_SENT --> CREDIT_CONFIRMED: quote.credit.confirmed
    CREDIT_CONFIRMED --> PAYMENT_COMPLETED: creación del pago (op 5), sincrónica
    CREDIT_CONFIRMED --> PAYMENT_FAILED: creación del pago (op 5), rechazo sincrónico
    PAYMENT_COMPLETED --> PAYMENT_FAILED: payment.status.changed FAILED
    PAYMENT_COMPLETED --> PAYMENT_RETURNED: payment.status.changed RETURNED
    EXPIRED --> [*]
    REJECTED --> [*]
    PAYMENT_COMPLETED --> [*]
    PAYMENT_FAILED --> [*]
    PAYMENT_RETURNED --> [*]`}
/>

## Tabla de estados

| `quote_status` | Lo fija | ¿Terminal? | Significado |
| --- | --- | --- | --- |
| `CREATED` | [Crear cotización](/api-nexa/quotes#create-quote-op-1) <N /> | no | La cotización existe con un precio y un `expires_at`. Todavía no está bloqueada. |
| `ACCEPTED` | [Aceptar cotización](/api-nexa/quotes#accept-quote-op-2) <N /> | no | Tipo de cambio bloqueado. Nexa lo mantiene hasta `deposit_deadline`. |
| `DEPOSIT_REPORTED` | [Reportar depósito de USDC](/api-nexa/deposits#report-usdc-deposit-op-3) <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](/api-nexa/payments#create-payment-op-5) <N />, **en la respuesta** | sí, salvo que después llegue un evento de falla | Desembolso realizado al beneficiario fijo. |
| `PAYMENT_FAILED` | [Crear pago](/api-nexa/payments#create-payment-op-5) <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 `EXPIRED` solo se llega desde `CREATED` (se perdió el plazo de aceptación)
  y desde `ACCEPTED` (no se reportó ningún depósito antes de
  `deposit_deadline`). **En cuanto se reporta cualquier depósito a tiempo, la
  cotización ya no expira sola**, y Nexa debe una decisión.
- `REJECTED` es la decisión de Nexa posterior a un reporte de depósito (a
  través del webhook `quote.rejected`), nunca un error sincrónico de la
  llamada de depósito. Después del rechazo,
  [`GET /v1/quotes/{tesser_quote_id}`](/api-nexa/quotes#get-quote-optional-op-8)
  devuelve `quote_status: "REJECTED"`.
- **Una solicitud de fondos adicionales no cambia el estado**: la cotización
  permanece en `DEPOSIT_REPORTED` y 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_FAILED` y `PAYMENT_RETURNED`. La recuperación desde
  `PAYMENT_FAILED`/`PAYMENT_RETURNED`, y el destino del USDC después de
  `REJECTED`, se manejan de forma manual y fuera de banda en el POC
  ([preguntas abiertas 4 y 7](/es/open-questions)).
- **El desembolso es sincrónico.** La *respuesta* de la creación del pago
  lleva el resultado: `PAYMENT_COMPLETED`, o `PAYMENT_FAILED` cuando 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 respuesta `COMPLETED` — por eso
  `PAYMENT_COMPLETED` es el único estado terminal del que todavía se puede
  salir, hacia `PAYMENT_FAILED` o `PAYMENT_RETURNED`. Cuánto tiempo después
  de una respuesta `COMPLETED` puede aparecer todavía esa falla es la
  [pregunta abierta 5](/es/open-questions).

## 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`.
