# Cobros

Cobra una factura con el usuario delante, con su tarjeta guardada o con una nueva, completa la autenticación del banco y consulta el historial de intentos.

El servicio de cobros cobra una factura ya emitida mientras el usuario está en tu aplicación: con una tarjeta que guardó antes o con una nueva que introduce en ese momento. Es la pieza entre [Facturas](https://os.utilia.ai/docs/developers/sdk/invoices), que emite, y [Métodos de pago](https://os.utilia.ai/docs/developers/sdk/payment-methods), que guarda tarjetas. Para cobrar **sin** el usuario delante se usa otra vía: `invoices.charge`, contra una [autorización de cobro](https://os.utilia.ai/docs/developers/sdk/payment-authorizations). Funciona con la clave de tu aplicación externa, siempre desde tu servidor.

## Antes de empezar

| JavaScript | Python | Para qué | Scope |
| - | - | - | - |
| `payments.payInvoice(facturaId, userId, pmId?, { saveCard? })` | `payments.pay_invoice(invoice_id, user_id, payment_method_id, save_card=...)` | Iniciar el cobro | `billing:charge` |
| `payments.getPaymentIntents(facturaId, userId)` | `payments.get_payment_intents(invoice_id, user_id)` | Historial de intentos de cobro | `crm:invoices:read` |

El cobro exige los aplicativos **Finanzas** y **Pagos con tarjeta (Stripe)** en el plan: sin ellos, la respuesta es un 403 `MODULE_NOT_ENABLED`, que no arregla ningún permiso. Si la pasarela está apagada en UTILIA OS, la respuesta es un 503 con `errorCode` `STRIPE_NOT_CONFIGURED`. Para no llegar a ese punto, pregunta antes con `paymentMethods.getReadiness()` al abrir la pantalla de pago, como se explica en [Comprobar si se puede cobrar](https://os.utilia.ai/docs/developers/sdk/payment-methods#comprobar-si-se-puede-cobrar).

## Cobrar una factura

Hay dos modos, según pases o no una tarjeta guardada:

- **Con tarjeta guardada** (`paymentMethodId`): UTILIA OS confirma el cobro en la misma petición. Si el banco no pide autenticación, la respuesta llega con `alreadySucceeded` en verdadero.
- **Con tarjeta nueva** (sin `paymentMethodId`): UTILIA OS prepara el cobro y te devuelve los datos para que tu web monte Stripe Elements y el usuario introduzca la tarjeta. Con `saveCard` quedará guardada después del cobro; guardarla no autoriza cobros futuros, y el usuario debe saber que se guarda.

La respuesta trae `clientSecret` (para confirmar en el navegador; no lo guardes), `paymentIntentId` (identificador de UTILIA OS, no el `pi_` de Stripe), `status`, `alreadySucceeded`, `publishableKey` y `stripeAccountId`. Los estados posibles son `REQUIRES_PAYMENT_METHOD`, `REQUIRES_CONFIRMATION`, `REQUIRES_ACTION`, `PROCESSING`, `SUCCEEDED`, `CANCELED` y `FAILED`.

### JavaScript

```typescript
// Con la tarjeta predeterminada
const cards = await sdk.paymentMethods.list('user_01HXYZ');
const card = cards.find((c) => c.isDefault);

const result = await sdk.payments.payInvoice(invoiceId, 'user_01HXYZ', card?.id);

if (result.alreadySucceeded) {
  // Cobrada. Espera el aviso INVOICE_PAID para darla por pagada.
} else if (result.status === 'REQUIRES_ACTION') {
  // El banco pide autenticación: tu web la completa con clientSecret.
  enviarALaWeb(result.clientSecret, result.publishableKey, result.stripeAccountId);
}

// Con una tarjeta nueva, que se guardará tras el cobro
const nueva = await sdk.payments.payInvoice(invoiceId, 'user_01HXYZ', undefined, { saveCard: true });
```

### Python

```python
# Con la tarjeta predeterminada
cards = await sdk.payment_methods.list("user_01HXYZ")
card = next((c for c in cards if c.is_default), None)

result = await sdk.payments.pay_invoice(invoice_id, "user_01HXYZ", card.id if card else None)

if result.already_succeeded:
    pass  # Cobrada. Espera el aviso INVOICE_PAID para darla por pagada.
elif result.status == "REQUIRES_ACTION":
    # El banco pide autenticación: tu web la completa con client_secret.
    enviar_a_la_web(result.client_secret, result.publishable_key, result.stripe_account_id)

# Con una tarjeta nueva, que se guardará tras el cobro
nueva = await sdk.payments.pay_invoice(invoice_id, "user_01HXYZ", save_card=True)
```

Cuando el banco pide autenticación (3D Secure), el cobro termina en tu web y la confirmación te llega por aviso:

**Cobro con autenticación del banco**

Participantes: Tu servidor, Tu web, UTILIA OS y Stripe.

1. Tu servidor → UTILIA OS: `POST /api/external/v1/invoices/:id/pay`. payInvoice con la factura, el usuario y su tarjeta guardada.
2. UTILIA OS → Tu servidor (respuesta): status REQUIRES_ACTION y clientSecret
3. Tu servidor → Tu web: clientSecret, publishableKey y stripeAccountId
4. Tu web → Stripe: confirmCardPayment(clientSecret). El banco autentica al usuario.
5. Stripe → UTILIA OS: Aviso de cobro confirmado. UTILIA OS marca la factura como pagada.
6. UTILIA OS → Tu servidor: Aviso INVOICE_PAID. Aquí das la factura por pagada.

_La respuesta de payInvoice no es la confirmación: lo es el aviso INVOICE_PAID._

En tu web, la confirmación con Stripe es igual sea cual sea el lenguaje de tu servidor:

```typescript
import { loadStripe } from '@stripe/stripe-js';

const stripe = await loadStripe(publishableKey, { stripeAccount: stripeAccountId });
const { error } = await stripe!.confirmCardPayment(clientSecret);
if (error) console.error(error.message);
// Sin error, el cobro está hecho: espera el aviso INVOICE_PAID.
```

La respuesta de `payInvoice` puede quedarse en `PROCESSING` o completarse después en el navegador: la fuente de verdad del cobro es el aviso `INVOICE_PAID` en tu servidor, también cuando no hay autenticación del banco.

## Cuando la tarjeta se rechaza

Hay que distinguir dos situaciones:

**El banco rechaza la tarjeta.** No es una excepción: `payInvoice` devuelve el resultado con `alreadySucceeded` en falso y estado `FAILED` o `REQUIRES_PAYMENT_METHOD`. El motivo está en el historial, en el último intento: `errorCode` trae el código de Stripe (por ejemplo, `card_declined`) y `errorMessage`, el mensaje ya traducido al castellano. Enséñalo y pide otra tarjeta.

**El cobro no se puede ni iniciar.** Entonces sí se lanza un `UtiliaSDKError`, y el motivo viaja en `errorCode` (`error_code` en Python), no en `code`:

| `errorCode` | Qué pasa |
| - | - |
| `INVOICE_ALREADY_PAID`, `INVOICE_ALREADY_CANCELLED`, `INVOICE_STATE_INVALID` | La factura no se puede cobrar en su estado actual. |
| `PAYMENT_METHOD_NOT_FOUND`, `PAYMENT_METHOD_INACTIVE` | La tarjeta no existe o ya no está activa. |
| `PAYMENT_METHOD_ACTIVE_INTENT` | Ya hay un cobro en curso con esa tarjeta. |
| `BILLING_NOT_ENABLED` | La facturación de la aplicación está apagada. La enciende un Super Administrador con **Activar la facturación**, en **Dinero › Facturación** de la ficha de la aplicación. |
| `STRIPE_ACCOUNT_NOT_READY` | La cuenta de cobro de la organización no está lista. |
| `VALIDATION_ERROR` | Otros fallos al iniciar el cobro; el mensaje de Stripe viene en `message`. |

No existen códigos del tipo `STRIPE_CARD_DECLINED`: un rechazo nunca llega como excepción. La lista completa de códigos de facturación está en [Códigos de error de facturación](https://os.utilia.ai/docs/guides/finance/invoicing-error-codes).

### JavaScript

```typescript
import { UtiliaSDKError } from '@utilia-os/sdk-js';

try {
  const result = await sdk.payments.payInvoice(invoiceId, 'user_01HXYZ', pmId);
  if (!result.alreadySucceeded && result.status !== 'REQUIRES_ACTION') {
    const [last] = await sdk.payments.getPaymentIntents(invoiceId, 'user_01HXYZ');
    mostrarRechazo(last?.errorMessage); // por ejemplo, «Tu banco rechazó el cobro.»
  }
} catch (error) {
  if (error instanceof UtiliaSDKError && error.errorCode === 'INVOICE_ALREADY_PAID') {
    // Nada que cobrar
  } else {
    throw error;
  }
}
```

### Python

```python
from utilia_sdk import UtiliaSDKError

try:
    result = await sdk.payments.pay_invoice(invoice_id, "user_01HXYZ", pm_id)
    if not result.already_succeeded and result.status != "REQUIRES_ACTION":
        intents = await sdk.payments.get_payment_intents(invoice_id, "user_01HXYZ")
        mostrar_rechazo(intents[0].error_message if intents else None)
except UtiliaSDKError as error:
    if error.error_code == "INVOICE_ALREADY_PAID":
        pass  # Nada que cobrar
    else:
        raise
```

## Historial de intentos

`getPaymentIntents` devuelve los intentos de cobro de una factura, del más reciente al más antiguo. Cada uno trae `id`, `amount`, `currency`, `status`, `initiatedBy` (`CUSTOMER`, `STAFF` o `SYSTEM_RECURRING`), las fechas `createdAt`, `succeededAt` y `failedAt` (cadenas ISO 8601), y, si falló, `errorCode` y `errorMessage`.

## Siguiente paso

- [Avisos a tu servidor](https://os.utilia.ai/docs/developers/webhooks/events): INVOICE_PAID y los demás eventos que confirman un cobro.
- [Autorizaciones de cobro](https://os.utilia.ai/docs/developers/sdk/payment-authorizations): Para cobrar las siguientes veces sin el usuario delante.
