# Impresión administrativa de boletos

## Flujo de dos etapas

`POST /api/v1/admin/promotion-tickets/print` usa el permiso base `promotion_tickets.print`. Si la selección contiene al menos un boleto previamente impreso, también exige `promotion_tickets.reprint` antes de devolver cualquier formulario o desafío de autorización.

La política de rango mixto es única: si cualquier boleto ya se imprimió, todo el lote se trata como reimpresión. Laravel conserva los registros de primera impresión de cada boleto y registra un lote de reimpresión separado.

## 1. Inspección inicial

La primera solicitud envía únicamente un criterio de selección. No incluir `confirm_reprint_authorization`, `reprint_access_code`, `reprint_reason` ni `reprint_reason_other`.

### Por rango numérico

```json
{
  "from_ticket_number": 1001,
  "to_ticket_number": 1050
}
```

### Por fecha de generación

```json
{
  "generated_from": "2026-07-01",
  "generated_to": "2026-07-24"
}
```

Si ningún boleto fue impreso, Laravel prepara la impresión inicial y devuelve la vista imprimible. Si detecta una reimpresión, responde exitosamente; no usa un 422 para activar la interfaz.

```json
{
  "success": true,
  "message": "Reimpresión requerida.",
  "data": {
    "requires_reprint_authorization": true,
    "selection": {
      "total_tickets": 25,
      "previously_printed_tickets": 10,
      "unprinted_tickets": 15
    },
    "reprint": {
      "code_is_configured": true,
      "reason_required": true,
      "reason_options": [
        { "value": "damaged_print", "label": "Impresión dañada" },
        { "value": "paper_jam", "label": "Papel atascado" },
        { "value": "lost_document", "label": "Documento perdido" },
        { "value": "printer_error", "label": "Error de impresora" },
        { "value": "other", "label": "Otro" }
      ]
    }
  }
}
```

React debe interpretar `requires_reprint_authorization: true`, conservar exactamente la selección y cambiar al estado `reprint_authorization_required`. Debe mostrar el código de reimpresión como campo password y el selector de motivo. No debe esperar `TICKET_REPRINT_ACCESS_CODE_REQUIRED` ni tratar la detección como error.

Si el API devuelve `403` con `error_code: "TICKET_REPRINT_FORBIDDEN"`, React debe conservar el rango, ocultar código, motivo y botón de autorización, y mostrar: `No tienes permisos para reimprimir boletos.` Puede complementar con: `Este rango contiene boletos impresos anteriormente y requiere autorización de un administrador.` El código válido nunca sustituye este permiso.

Si `code_is_configured` es `false`, debe bloquear la confirmación y mostrar que un administrador debe configurar el código desde Ajustes.

## Datos de farmacia en la vista imprimible

- `city` puede ser `null`.
- Cuando falte ciudad, React debe mostrar `No especificada` o un guion.
- La ausencia de ciudad o provincia no debe bloquear la preparación ni la impresión física.
- `pharmacy.branch` contiene UUID público y nombre de sucursal.
- Mostrar la sucursal en una línea propia y no renderizar `Sucursal: null`.

## 2. Confirmación de reimpresión

React reenvía exactamente el mismo rango o fechas, y agrega la confirmación, el código y el motivo.

### Motivo estándar

```json
{
  "from_ticket_number": 1001,
  "to_ticket_number": 1050,
  "confirm_reprint_authorization": true,
  "reprint_access_code": "codigo",
  "reprint_reason": "printer_error"
}
```

### Motivo `other`

```json
{
  "generated_from": "2026-07-01",
  "generated_to": "2026-07-24",
  "confirm_reprint_authorization": true,
  "reprint_access_code": "codigo",
  "reprint_reason": "other",
  "reprint_reason_other": "Descripción del motivo"
}
```

Laravel vuelve a consultar y bloquear los boletos al confirmar. Si su estado cambió y ahora se requiere autorización, devuelve nuevamente la respuesta de inspección; así evita registrar erróneamente una impresión inicial.

Una confirmación correcta devuelve los boletos listos para imprimir:

```json
{
  "success": true,
  "data": {
    "filter": { "type": "ticket_number_range", "from_ticket_number": 1001, "to_ticket_number": 1050 },
    "range": { "from_ticket_number": 1001, "to_ticket_number": 1050 },
    "is_reprint": true,
    "tickets": []
  }
}
```

## Errores de confirmación

Estos errores no se usan para detectar normalmente una reimpresión durante la inspección inicial:

- `422 TICKET_REPRINT_ACCESS_CODE_REQUIRED`: se confirmó una reimpresión sin código.
- `422 TICKET_REPRINT_REASON_REQUIRED`: falta el motivo.
- `422 TICKET_REPRINT_REASON_OTHER_REQUIRED`: se eligió `other` sin descripción.
- `422 TICKET_REPRINT_ACCESS_CODE_INVALID`: el código es incorrecto.
- `409 TICKET_REPRINT_ACCESS_CODE_NOT_CONFIGURED`: no hay código configurado.
- `429 TICKET_REPRINT_RATE_LIMITED`: se excedieron los intentos permitidos.
- `403 FORBIDDEN`: falta el permiso necesario.

También pueden ocurrir `404 TICKET_RANGE_NOT_FOUND` o `TICKET_GENERATED_DATE_RANGE_NOT_FOUND`, y `409 TICKET_RANGE_INCOMPLETE` o `TICKET_RANGE_CONTAINS_NON_PRINTABLE_TICKETS` al validar la selección.

## Reglas de integración React

- Ocultar el código y el motivo durante una impresión inicial.
- Al recibir inspección de reimpresión, mostrar conteos, advertencia, código y motivo; el botón pasa a `Autorizar reimpresión`. Esto solo aplica cuando el usuario tiene `promotion_tickets.reprint`.
- Ante `TICKET_REPRINT_FORBIDDEN`, ocultar código, motivo y autorización; conservar la selección y permitir preparar otro rango.
- Mantener los filtros y no persistir el código en URL, almacenamiento o logs.
- Mostrar errores de código, motivo y descripción junto a su campo.
- Renderizar e imprimir solo los boletos devueltos por Laravel después de una preparación o autorización correcta.
