# API de facturas generales

## 1. Definición general

El backend registra una factura física una sola vez como `Invoice`. A partir de esa factura, el sistema crea una o varias `InvoicePromotionApplication` cuando existen promociones activas, elegibles y con términos aceptados al momento del registro.

## 2. Diferencias entre entidades

- `Invoice`: factura física general.
- `InvoiceRevision`: snapshot histórico de la factura para correcciones.
- `InvoicePromotionApplication`: aplicación promocional de una revisión de factura a una promoción.
- `PromotionTicket`: boleto generado por una aplicación promocional.

Una factura puede generar múltiples aplicaciones. Una participación del dependiente es una `InvoicePromotionApplication` vigente.

## 3. Permisos `invoices.*`

- `invoices.view_any`
- `invoices.view`
- `invoices.create`
- `invoices.correct`
- `invoices.view_revisions`
- `invoices.view_eligible_dependents`

Permisos obsoletos:

- `promotion_invoices.view_any`
- `promotion_invoices.view`
- `promotion_invoices.create`
- `promotion_invoices.view_eligible_dependents`

## 4. Endpoints

| Método | Endpoint | Permiso |
|---|---|---|
| GET | `/api/v1/admin/invoices/eligible-dependents` | `invoices.view_eligible_dependents` |
| GET | `/api/v1/admin/invoices/product-options` | `invoices.view_eligible_dependents` |
| POST | `/api/v1/admin/invoices/preview` | `invoices.create` |
| POST | `/api/v1/admin/invoices` | `invoices.create` |
| GET | `/api/v1/admin/invoices` | `invoices.view_any` |
| GET | `/api/v1/admin/invoices/{invoice}` | `invoices.view` |
| POST | `/api/v1/admin/invoices/{invoice}/revisions/preview` | `invoices.correct` |
| POST | `/api/v1/admin/invoices/{invoice}/revisions` | `invoices.correct` |
| GET | `/api/v1/admin/invoices/{invoice}/revisions` | `invoices.view_revisions` |
| GET | `/api/v1/admin/invoices/{invoice}/revisions/{revision}` | `invoices.view_revisions` |

## 5. Selector general de dependientes

`GET /api/v1/admin/invoices/eligible-dependents`

Filtros:

- `page`
- `per_page`
- `search`
- `pharmacy_id`
- `sort`
- `direction`

## 5.1 Regla de `can_register_invoice`

`can_register_invoice` es `true` para cualquier dependiente activo con farmacia activa. Una factura puede contener productos activos aunque no participen en una promoción.

Los términos solo se exigen cuando uno de los productos sí coincide con una promoción vigente que los requiere.

## 5.2 Resumen de términos por dependiente

Cada dependiente devuelve:

```json
{
  "id": "uuid-dependiente",
  "can_register_invoice": true,
  "terms": {
    "accepted": true,
    "accepted_at": "2026-07-16T19:01:22.000000Z",
    "version": null,
    "status": "partial",
    "status_label": "Aceptación parcial",
    "message": "1 promoción aceptada · 1 pendiente",
    "accepted_promotions_count": 1,
    "pending_promotions_count": 1,
    "accepted_promotions": [
      {
        "id": "uuid-promocion-a",
        "name": "Promoción A",
        "version": "1.0",
        "accepted_at": "2026-07-16T19:01:22.000000Z",
        "status": "accepted",
        "status_label": "Términos aceptados",
        "message": "Términos aceptados"
      }
    ],
    "pending_promotions": [
      {
        "id": "uuid-promocion-b",
        "name": "Promoción B",
        "version": "2.0",
        "accepted_at": null,
        "status": "new_version_pending",
        "status_label": "Nueva versión pendiente",
        "message": "Existe una nueva versión de los términos que debe ser aceptada."
      }
    ]
  },
  "reason_code": null,
  "reason": "1 promoción aceptada · 1 pendiente"
}
```

Estados agregados posibles:

- `accepted`
- `partial`
- `pending`
- `unavailable`

## 6. Opciones de productos

`GET /api/v1/admin/invoices/product-options`

Filtros:

- `page`
- `per_page`
- `search`
- `sort`
- `direction`

## 7. Preview autoritativo de boletos estimados

`POST /api/v1/admin/invoices/preview`

Payload:

```json
{
  "dependent_id": "uuid-del-dependiente",
  "invoice_date": "2026-07-15",
  "items": [
    { "product_id": "uuid-producto-1", "quantity": 10 },
    { "product_id": "uuid-producto-2", "quantity": 3 }
  ]
}
```

Reglas importantes:

- usa `invoice_date` para evaluar vigencia de promociones
- suma todas las promociones activas y elegibles que coincidan con la factura
- omite promociones coincidentes con términos pendientes y las devuelve en `omitted_promotions`
- los productos sin regla promocional se devuelven con `promotion_estimates: []`; no bloquean el registro
- para productos con regla, calcula sobre el acumulado vigente del mismo dependiente, promoción y producto
- el frontend no debe recalcular boletos por su cuenta

Respuesta resumida:

```json
{
  "success": true,
  "message": "Estimación de factura obtenida correctamente.",
  "data": {
    "items": [
      {
        "product_id": "uuid-producto-1",
        "product_code": "PROD-001",
        "product_name": "Producto A",
        "product_presentation": "Caja",
        "quantity": 10,
        "estimated_tickets": 4,
        "promotion_estimates": [
          {
            "promotion_id": "uuid-promocion-a",
            "promotion_name": "Promo A",
            "required_quantity": 5,
            "tickets_awarded": 2,
            "calculation_mode": "per_complete_block",
            "complete_blocks": 2,
            "estimated_tickets": 4,
            "previous_quantity": 2,
            "accumulated_quantity": 12,
            "remaining_quantity": 3,
            "description": "2 boletos por cada 5 unidades"
          }
        ]
      }
    ],
    "summary": {
      "products_count": 1,
      "units_count": 10,
      "estimated_tickets": 4
    },
    "omitted_promotions": [
      {
        "promotion": {
          "id": "uuid-promocion-b",
          "name": "Promo B"
        },
        "reason_code": "PROMOTION_TERMS_ACCEPTANCE_REQUIRED",
        "reason": "La promoción requiere aceptación de términos vigente."
      }
    ]
  }
}
```

Para `per_complete_block`, el backend calcula los boletos nuevos con:

- `floor((acumulado_previo + cantidad_actual) / required_quantity) * tickets_awarded - boletos_previamente_emitidos`

Por ejemplo, con una regla de 5 unidades por boleto: una factura de 2 unidades queda acumulada y una siguiente de 3 unidades genera el boleto.

## 8. Registro sin promoción manual

Toda factura con productos activos se registra aunque no genere boletos ni tenga aplicaciones promocionales. Los productos sin reglas promocionales no acumulan unidades ni generan boletos.

`POST /api/v1/admin/invoices`

Header obligatorio:

```http
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
```

Payload:

```json
{
  "dependent_id": "uuid-del-dependiente",
  "invoice_number": "FAC-00125",
  "invoice_date": "2026-07-15",
  "items": [
    { "product_id": "uuid-producto-1", "quantity": 3 },
    { "product_id": "uuid-producto-2", "quantity": 1 }
  ]
}
```

## 9. Campos que frontend no debe enviar

- `promotion_id`
- `pharmacy_id`
- `registered_by`
- `source`
- `status`
- `revision_number`
- `terms_acceptance_id`
- snapshots
- ids internos
- hashes internos

## 10. Idempotency-Key

La creación y la corrección usan `Idempotency-Key` en header.

Mismo key + mismo payload:

- puede responder `200`
- puede devolver `already_processed = true`

Mismo key + payload diferente:

- `409 IDEMPOTENCY_KEY_REUSED`

## 11. Unicidad del número por farmacia

La unicidad se protege por:

- validación
- reserva histórica
- base de datos

Regla:

- `pharmacy_id + invoice_number_normalized`

Error:

- `409 INVOICE_NUMBER_ALREADY_EXISTS`

## 12. Normalización del número

El backend conserva `invoice_number` como valor visible y calcula `invoice_number_normalized` de forma determinista y estable.

## 13. Promociones aplicadas y omitidas

El cliente no selecciona promoción. El backend:

- detecta promociones activas y elegibles
- congela contexto y reglas
- crea múltiples aplicaciones cuando corresponde
- omite promociones sin términos aceptados vigentes

Cuando existe al menos una promoción coincidente con términos aceptados y otra pendiente, la factura se crea y la respuesta incluye `omitted_promotions`.

Errores importantes:

- `422 NO_APPLICABLE_PROMOTIONS`
- `409 PROMOTION_TERMS_ACCEPTANCE_REQUIRED`

## 13.1 Éxito parcial

Respuesta de creación con promociones omitidas:

```json
{
  "success": true,
  "message": "Factura registrada correctamente.",
  "data": {
    "id": "uuid-factura",
    "already_processed": false,
    "omitted_promotions": [
      {
        "promotion": {
          "id": "uuid-promocion-b",
          "name": "Promoción B"
        },
        "reason_code": "PROMOTION_TERMS_ACCEPTANCE_REQUIRED",
        "reason": "La promoción requiere aceptación de términos vigente."
      }
    ]
  }
}
```

Si todas las promociones coincidentes están pendientes, la factura se rechaza con `PROMOTION_TERMS_ACCEPTANCE_REQUIRED` y no se crean factura, revisión, aplicaciones ni boletos.

## 13. Factura con múltiples promociones

Una misma factura puede generar varias `InvoicePromotionApplication`.

Los boletos nuevos usan:

- una secuencia global única
- un formato visible configurable
- continuidad sin reutilización aunque cambie el prefijo o el padding

## 14. Listado administrativo

`GET /api/v1/admin/invoices`

Filtros:

- `page`
- `per_page`
- `search`
- `promotion_id`
- `dependent_id`
- `pharmacy_id`
- `status`
- `invoice_number`
- `registered_by`
- `invoice_date_from`
- `invoice_date_to`
- `registered_from`
- `registered_to`
- `sort`
- `direction`

## 15. Detalle administrativo

`GET /api/v1/admin/invoices/{invoice}`

Devuelve factura, items vigentes, revisión activa y aplicaciones vigentes. No expone IDs internos, claves de idempotencia ni hashes.

## 16. Preview de corrección

`POST /api/v1/admin/invoices/{invoice}/revisions/preview`

No modifica datos. Devuelve cambios propuestos, promociones afectadas y estimación de boletos.

## 17. Aplicación de corrección

`POST /api/v1/admin/invoices/{invoice}/revisions`

Campos extra:

- `expected_revision`
- `reason`

## 18. expected_revision

Protege contra escritura concurrente. Si no coincide con la revisión actual:

- `409 INVOICE_REVISION_CONFLICT`

## 19. Boletos anulados

Cuando una corrección sustituye aplicaciones anteriores:

- los boletos anteriores se marcan `voided`
- se conserva historial
- no se reutilizan números

## 20. Boletos nuevos

Los nuevos boletos continúan una secuencia global. No se calcula `MAX(ticket_number) + 1`.

## 21. Historial de revisiones

- `GET /api/v1/admin/invoices/{invoice}/revisions`
- `GET /api/v1/admin/invoices/{invoice}/revisions/{revision}`

## 22. Estados y etiquetas

Factura:

- `confirmed` → `Confirmada`
- `voided` → `Anulada`

Revisión:

- `active` → `Vigente`
- `superseded` → `Sustituida`

Aplicación:

- `applied` → `Aplicada`
- `superseded` → `Sustituida`
- `voided` → `Anulada`

Boleto:

- `valid` → `Válido`
- `voided` → `Anulado`

## 23. Filtros y paginación

Todas las colecciones responden con `meta.current_page`, `meta.per_page`, `meta.total` y `meta.last_page`.

## 24. Códigos de error

- `INVOICE_NUMBER_ALREADY_EXISTS`
- `INVOICE_NOT_FOUND`
- `INVOICE_REVISION_NOT_FOUND`
- `INVOICE_REVISION_CONFLICT`
- `INVOICE_REVISION_NO_CHANGES`
- `INVOICE_CORRECTION_INVALID`
- `NO_APPLICABLE_PROMOTIONS`
- `PROMOTION_TERMS_ACCEPTANCE_REQUIRED`
- `IDEMPOTENCY_KEY_REUSED`
- `VALIDATION_ERROR`
- `UNAUTHENTICATED`
- `FORBIDDEN`
- `RESOURCE_NOT_FOUND`
- `TOO_MANY_REQUESTS`
- `PROMOTION_MANUAL_SELECTION_DEPRECATED`

## 25. Ejemplos Axios

```ts
import axios from 'axios';

export const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  withCredentials: true,
  withXSRFToken: true,
  headers: { Accept: 'application/json' },
});
```

```ts
await api.post('/api/v1/admin/invoices', payload, {
  headers: { 'Idempotency-Key': crypto.randomUUID() },
});
```

```ts
await api.post(`/api/v1/admin/invoices/${invoiceId}/revisions/preview`, payload);
```

```ts
await api.post(`/api/v1/admin/invoices/${invoiceId}/revisions`, payload, {
  headers: { 'Idempotency-Key': crypto.randomUUID() },
});
```

## 26. UUID públicos

Todos los IDs expuestos para frontend son UUID públicos.

## 27. Campos que frontend no debe mostrar

- IDs internos
- request hashes
- context hashes
- idempotency keys
- detalles administrativos no documentados

## 28. Funcionalidades pendientes

- dashboard
- reportes
- puntos
- ledger de puntos
- OCR
- integraciones/importaciones
- anulación general de facturas
## Sucursal de farmacia

Las opciones de dependientes y los recursos de factura identifican la farmacia con código, nombre y sucursal. Las facturas nuevas guardan `pharmacy_branch_snapshot`; el campo `pharmacy.branch_name` del recurso se obtiene del snapshot y no se recalcula al editar posteriormente la farmacia.
