# Arquitectura de la integración Lucode/APISUNAT

## 1. Objetivo y alcance

Este documento describe cómo el backend de `app_transportes` integra la emisión
electrónica mediante Lucode/APISUNAT.

La implementación actual permite:

- Crear, listar y consultar borradores de comprobantes electrónicos.
- Emitir boletas electrónicas simples.
- Emitir facturas electrónicas simples.
- Consultar el estado del comprobante después de emitirlo.
- Guardar la respuesta, estado, XML, CDR y PDF retornados por Lucode.
- Separar los comprobantes y la configuración por empresa.
- Trabajar con los ambientes `beta` y `production`.

No forman parte de la implementación actual:

- Notas de crédito.
- Notas de débito.
- Comunicación de baja.
- Resumen diario de boletas.
- Facturas a crédito con cronograma de cuotas.
- Detracciones, percepciones o retenciones.

## 2. Estado del soporte

| Comprobante | Tipo interno | Tipo enviado a Lucode | Serie | Estado |
|---|---|---|---|---|
| Boleta | `receipt` | `boleta` | `Bxxx` | Implementado |
| Factura | `invoice` | `factura` | `Fxxx` | Implementado |

### Boleta

Acepta clientes con DNI, RUC, carné de extranjería o pasaporte. Si el cliente no
tiene documento, el adaptador envía los valores de consumidor final:

```json
{
  "cliente_tipo_de_documento": "1",
  "cliente_numero_de_documento": "99999999",
  "cliente_denominacion": "CLIENTE VARIOS",
  "cliente_direccion": "-"
}
```

### Factura

El cliente debe tener un RUC válido de 11 dígitos. Si no cumple esta condición,
el borrador no puede crearse y el backend devuelve un error de validación.

## 3. Vista general de la arquitectura

```mermaid
flowchart LR
    A[Aplicación cliente] -->|Bearer token + X-Company-Id| B[Rutas /api/v1/sunat]
    B --> C[Controladores SUNAT]
    C --> D[CompanyContextService]
    C --> E[SunatBillingService]
    E --> F[ElectronicBillingConfig]
    E --> G[ElectronicDocument]
    E --> H[LucodeSunatService]
    H -->|Authorization: Bearer token| I[API Lucode/APISUNAT]
    I -->|estado, XML, CDR, PDF| H
    H --> E
    E --> G
    G --> J[(PostgreSQL)]
```

El backend usa una arquitectura por capas:

1. **Rutas:** exponen la API REST bajo `/api/v1/sunat`.
2. **Autenticación:** Laravel Sanctum valida el token del usuario.
3. **Contexto de empresa:** determina sobre qué empresa trabaja la solicitud.
4. **Controladores:** reciben la petición y delegan la lógica.
5. **Servicio de facturación:** crea borradores, correlativos y estados.
6. **Adaptador Lucode:** transforma el modelo interno al contrato APISUNAT.
7. **Persistencia:** guarda configuración, solicitud, respuesta y archivos.

## 4. Seguridad y contexto de empresa

Todos los endpoints de Lucode están protegidos por `auth:sanctum`.

Cabeceras que debe enviar el frontend:

```http
Authorization: Bearer <TOKEN_DEL_USUARIO>
Accept: application/json
Content-Type: application/json
X-Company-Id: 2
```

Los dos tokens cumplen funciones diferentes:

| Token | Uso |
|---|---|
| Token Sanctum | Autentica al usuario frente a este backend. |
| Token Lucode | Autentica al backend frente a APISUNAT. |

`X-Company-Id` selecciona la empresa activa. El backend comprueba que esa empresa
pertenezca al usuario autenticado. Si no se envía la cabecera, utiliza la primera
empresa activa asociada al usuario.

Las consultas de documentos siempre incluyen `company_id`, evitando que una
empresa consulte o emita documentos pertenecientes a otra.

## 5. Componentes principales

### `routes/api.php`

Declara los endpoints protegidos:

| Método | Endpoint | Función |
|---|---|---|
| `GET` | `/api/v1/sunat/config` | Consultar configuración de la empresa |
| `PUT` | `/api/v1/sunat/config` | Crear o actualizar configuración |
| `GET` | `/api/v1/sunat/documents` | Listar documentos, 20 por página |
| `POST` | `/api/v1/sunat/documents` | Crear un borrador |
| `GET` | `/api/v1/sunat/documents/{id}` | Consultar un documento |
| `POST` | `/api/v1/sunat/documents/{id}/send` | Emitir un borrador |

### `ElectronicDocumentController`

Es la entrada HTTP de los comprobantes. Sus responsabilidades son:

- Resolver la empresa activa.
- Validar que el documento pertenezca a esa empresa.
- Delegar creación y emisión al servicio de facturación.
- Devolver `ElectronicDocumentResource`.

### `SunatBillingService`

Orquesta el flujo de negocio:

- Guarda la configuración Lucode por empresa.
- Valida que cliente, servicio y cotización pertenezcan a la empresa.
- Selecciona la serie correspondiente.
- Crea el documento con estado `draft`.
- Asigna el siguiente correlativo al emitir.
- Invoca `LucodeSunatService`.
- Guarda el resultado y cambia el estado a `sent` o `accepted`.

### `LucodeSunatService`

Es el adaptador entre el dominio local y la API de Lucode. Se encarga de:

- Resolver URL, ambiente y token.
- Convertir `receipt` en `boleta`.
- Convertir `invoice` en `factura`.
- Formar los datos del cliente y los ítems.
- Enviar el comprobante con autenticación Bearer.
- Consultar el estado posterior a la emisión.
- Normalizar las URLs de XML, CDR y PDF.
- Convertir errores de conexión o de Lucode en errores de validación del backend.

### Modelos

`ElectronicBillingConfig` guarda una configuración única por empresa.

`ElectronicDocument` guarda el ciclo de vida del comprobante y puede estar
relacionado con:

- Empresa.
- Cliente.
- Servicio de transporte.
- Cotización.

## 6. Configuración de Lucode

### Variables de entorno

```dotenv
SUNAT_ENVIRONMENT=production
LUCODE_API_BASE_URL=https://app.apisunat.pe
LUCODE_SANDBOX_BASE_URL=https://sandbox.apisunat.pe
LUCODE_PRODUCTION_BASE_URL=https://app.apisunat.pe
LUCODE_SANDBOX_DOCUMENTS_PATH=/api/v3/documents
LUCODE_PRODUCTION_DOCUMENTS_PATH=/api/v2/documents
LUCODE_SANDBOX_STATUS_PATH=/api/v3/status
LUCODE_PRODUCTION_STATUS_PATH=/api/v2/status
LUCODE_API_TOKEN=<TOKEN_LUCODE>
```

El token Lucode nunca debe enviarse al frontend ni incluirse en el repositorio.
La respuesta de configuración solo expone `has_api_token: true|false`.

### Prioridad de configuración

La URL base se resuelve en este orden:

1. `extra_settings.api_base_url` de la empresa.
2. `LUCODE_API_BASE_URL`.
3. URL predeterminada del ambiente seleccionado.

El token se resuelve en este orden:

1. `extra_settings.api_token` de la empresa.
2. `LUCODE_API_TOKEN`.

### Configuración mediante la API

```http
PUT /api/v1/sunat/config
```

Ejemplo para producción:

```json
{
  "environment": "production",
  "office_ubigeo": "210101",
  "office_address": "Dirección fiscal de la empresa",
  "office_district": "Puno",
  "office_province": "Puno",
  "office_department": "Puno",
  "office_country_code": "PE",
  "invoice_series": "F001",
  "receipt_series": "B001",
  "is_active": true,
  "extra_settings": {
    "provider": "lucode",
    "api_base_url": "https://app.apisunat.pe",
    "initial_receipt_correlative": 0,
    "initial_invoice_correlative": 0
  }
}
```

Si el token está en `.env`, no es necesario enviarlo en esta petición.

## 7. Flujo de emisión

La emisión se realiza en dos llamadas. Crear un borrador no envía nada a SUNAT.

```mermaid
sequenceDiagram
    participant F as Frontend
    participant B as Backend
    participant DB as PostgreSQL
    participant L as Lucode/APISUNAT

    F->>B: POST /sunat/documents
    B->>B: Validar empresa, cliente y montos
    B->>DB: Guardar documento status=draft
    DB-->>B: ID del borrador
    B-->>F: 201 Documento creado

    F->>B: POST /sunat/documents/{id}/send
    B->>DB: Obtener configuración y correlativo
    B->>L: POST documents con Bearer token
    L-->>B: Resultado de emisión
    B->>L: POST status
    L-->>B: Estado, XML, CDR y PDF
    B->>DB: Guardar resultado y status
    B-->>F: Documento emitido
```

### Estados locales

```mermaid
stateDiagram-v2
    [*] --> draft: Crear borrador
    draft --> accepted: Lucode retorna ACEPTADO
    draft --> sent: Lucode retorna otro estado exitoso
```

Solo los documentos con estado `draft` pueden enviarse. Un segundo intento sobre
el mismo documento es rechazado por el backend.

## 8. Crear y emitir una boleta

### Paso 1: crear borrador

```http
POST /api/v1/sunat/documents
```

```json
{
  "client_id": 15,
  "transport_service_id": 120,
  "quotation_id": null,
  "document_type": "receipt",
  "issue_date": "2026-07-29",
  "currency_code": "PEN",
  "subtotal_amount": 100.00,
  "tax_amount": 18.00,
  "total_amount": 118.00,
  "payload": {
    "lines": [
      {
        "code": "SERV-001",
        "description": "Servicio de transporte",
        "quantity": 1,
        "unit_code": "ZZ",
        "unit_value": 100.00,
        "tax_amount": 18.00
      }
    ],
    "meta": {}
  }
}
```

El backend asigna la serie configurada para boletas, por ejemplo `B001`.

### Paso 2: emitir

```http
POST /api/v1/sunat/documents/{ID}/send
```

Contrato generado para Lucode:

```json
{
  "documento": "boleta",
  "serie": "B001",
  "numero": 1,
  "fecha_de_emision": "2026-07-29",
  "hora_de_emision": "10:30:00",
  "moneda": "PEN",
  "tipo_operacion": "0101",
  "cliente_tipo_de_documento": "1",
  "cliente_numero_de_documento": "12345678",
  "cliente_denominacion": "Nombre del cliente",
  "cliente_direccion": "Dirección del cliente",
  "items": [
    {
      "unidad_de_medida": "ZZ",
      "descripcion": "Servicio de transporte",
      "cantidad": "1.00",
      "valor_unitario": "100.000000",
      "porcentaje_igv": "18",
      "codigo_tipo_afectacion_igv": "10",
      "nombre_tributo": "IGV"
    }
  ],
  "total": "118.00"
}
```

## 9. Crear y emitir una factura

El flujo usa los mismos endpoints. Las diferencias son:

- `document_type` debe ser `invoice`.
- El cliente debe tener RUC de 11 dígitos.
- La serie debe comenzar con `F`.
- Lucode recibe `documento: "factura"`.
- Se agrega `fecha_de_vencimiento`.
- Para el PDF se prioriza el formato A4.

Ejemplo de borrador:

```json
{
  "client_id": 20,
  "transport_service_id": 121,
  "quotation_id": 45,
  "document_type": "invoice",
  "issue_date": "2026-07-29",
  "currency_code": "PEN",
  "subtotal_amount": 500.00,
  "tax_amount": 90.00,
  "total_amount": 590.00,
  "payload": {
    "lines": [
      {
        "code": "SERV-002",
        "description": "Transporte de carga",
        "quantity": 1,
        "unit_code": "ZZ",
        "unit_value": 500.00,
        "tax_amount": 90.00
      }
    ],
    "meta": {
      "due_date": "2026-07-29"
    }
  }
}
```

Contrato generado para Lucode:

```json
{
  "documento": "factura",
  "serie": "F001",
  "numero": 1,
  "fecha_de_emision": "2026-07-29",
  "fecha_de_vencimiento": "2026-07-29",
  "hora_de_emision": "10:30:00",
  "moneda": "PEN",
  "tipo_operacion": "0101",
  "cliente_tipo_de_documento": "6",
  "cliente_numero_de_documento": "20123456789",
  "cliente_denominacion": "EMPRESA CLIENTE S.A.C.",
  "cliente_direccion": "Dirección fiscal del cliente",
  "items": [
    {
      "unidad_de_medida": "ZZ",
      "descripcion": "Transporte de carga",
      "cantidad": "1.00",
      "valor_unitario": "500.000000",
      "porcentaje_igv": "18",
      "codigo_tipo_afectacion_igv": "10",
      "nombre_tributo": "IGV"
    }
  ],
  "total": "590.00"
}
```

## 10. Reglas de transformación

### Cliente

| Tipo local | Código Lucode |
|---|---|
| `DNI` o `1` | `1` |
| `CE` o `4` | `4` |
| `RUC` o `6` | `6` |
| `PASSPORT`, `PASAPORTE` o `7` | `7` |

### Ítems

| Campo local | Campo Lucode | Regla |
|---|---|---|
| `unit_code` | `unidad_de_medida` | Por defecto `NIU` |
| `description` | `descripcion` | Por defecto `Servicio de transporte` |
| `quantity` | `cantidad` | Dos decimales |
| `unit_value` | `valor_unitario` | Seis decimales |
| `tax_amount > 0` | Afectación IGV | `18`, código `10`, tributo `IGV` |
| `tax_amount = 0` | Exonerado | `0`, código `20`, tributo `EXO` |

El campo `total` se toma de `total_amount`. El backend no recalcula ni compara el
total general contra la suma de los ítems.

### Fecha

- Se usa la zona horaria `America/Lima`.
- Una fecha futura se normaliza a la fecha actual.
- Se aceptan como máximo tres días de antigüedad.
- Una fecha más antigua produce un error antes de llamar a Lucode.

## 11. Correlativos

El correlativo se asigna al emitir, no al crear el borrador.

Para cada serie se calcula:

```text
max(último correlativo local, correlativo inicial configurado) + 1
```

Los correlativos iniciales pueden establecerse con:

- `extra_settings.initial_receipt_correlative`
- `extra_settings.initial_invoice_correlative`

La base de datos tiene una restricción única por:

```text
company_id + series + correlative
```

Esto evita repetir un número dentro de la misma empresa y serie.

## 12. Persistencia

### `electronic_billing_configs`

Campos relevantes:

- `company_id`: una configuración única por empresa.
- `environment`: `beta` o `production`.
- `invoice_series`: serie de factura.
- `receipt_series`: serie de boleta.
- `is_active`: habilita la emisión.
- `extra_settings`: proveedor, URL, token y correlativos iniciales.

### `electronic_documents`

Campos relevantes:

- Identificadores de empresa, cliente, servicio y cotización.
- Tipo, serie, correlativo y estado.
- Fecha, moneda, subtotal, impuesto y total.
- `payload`: datos originales y trazabilidad de Lucode.
- `xml_path`, `cdr_path`, `pdf_path`.
- Código y mensaje de respuesta.
- Fechas de envío y aceptación.

Después de emitir, `payload` conserva:

```json
{
  "provider": "lucode",
  "provider_request": {},
  "provider_response": {
    "emission": {},
    "status": {}
  }
}
```

Esto facilita auditoría y diagnóstico, pero requiere restringir el acceso a las
respuestas porque pueden contener datos tributarios del cliente.

## 13. Respuesta del backend

Ejemplo resumido de un documento aceptado:

```json
{
  "message": "Documento enviado a SUNAT correctamente.",
  "data": {
    "id": 100,
    "document_type": "invoice",
    "series": "F001",
    "correlative": 1,
    "full_number": "F001-00000001",
    "status": "accepted",
    "sunat_response_code": "ACEPTADO",
    "sunat_response_message": "El comprobante fue enviado y aceptado por SUNAT.",
    "xml_path": "https://...",
    "cdr_path": "https://...",
    "pdf_path": "https://...",
    "sent_at": "2026-07-29T10:30:00-05:00",
    "accepted_at": "2026-07-29T10:30:00-05:00"
  }
}
```

Para facturas se prioriza `pdf.a4`. Para boletas se prioriza `pdf.ticket`.

## 14. Errores controlados

El backend devuelve errores de validación cuando:

- La configuración no existe o no está activa.
- Falta la URL base.
- Falta el token Lucode.
- El cliente no pertenece a la empresa.
- La factura no tiene un cliente con RUC válido.
- No existen ítems.
- La fecha es demasiado antigua.
- El documento ya no está en estado `draft`.
- Lucode responde con `success: false`.
- Lucode devuelve un código HTTP no exitoso.
- No se puede establecer conexión con Lucode.

No se debe reintentar automáticamente un error incierto de emisión sin consultar
primero si el número ya fue registrado, porque podría generar una respuesta de
documento duplicado.

## 15. Endpoints externos

| Ambiente | Operación | Endpoint |
|---|---|---|
| Pruebas | Emitir | `https://sandbox.apisunat.pe/api/v3/documents` |
| Pruebas | Consultar estado | `https://sandbox.apisunat.pe/api/v3/status` |
| Producción | Emitir | `https://app.apisunat.pe/api/v2/documents` |
| Producción | Consultar estado | `https://app.apisunat.pe/api/v2/status` |

Todas las llamadas usan:

```http
Accept: application/json
Authorization: Bearer <TOKEN_LUCODE>
Content-Type: application/json
```

Referencia de configuración:
<https://docs.apisunat.pe/integracion/facturacion-electronica/configuracion-api>

## 16. Ubicación del código

| Responsabilidad | Archivo |
|---|---|
| Rutas REST | `routes/api.php` |
| Controlador de documentos | `app/Http/Controllers/Api/Sunat/ElectronicDocumentController.php` |
| Controlador de configuración | `app/Http/Controllers/Api/Sunat/SunatConfigController.php` |
| Orquestación de emisión | `app/Services/Sunat/SunatBillingService.php` |
| Adaptador Lucode | `app/Services/Sunat/LucodeSunatService.php` |
| Selección de empresa | `app/Services/Tenant/CompanyContextService.php` |
| Configuración de endpoints | `config/sunat.php` |
| Modelo del documento | `app/Models/ElectronicDocument.php` |
| Modelo de configuración | `app/Models/ElectronicBillingConfig.php` |
| Validación del documento | `app/Http/Requests/Sunat/StoreElectronicDocumentRequest.php` |
| Pruebas del adaptador | `tests/Unit/LucodeSunatServiceTest.php` |

## 17. Pruebas existentes

La suite de `LucodeSunatServiceTest` comprueba:

- Contrato oficial de boleta.
- Construcción del payload de factura.
- Uso del token Bearer.
- Selección de endpoints de pruebas.
- Selección de endpoints productivos `/api/v2`.
- Consulta posterior del estado.
- Boleta para cliente sin documento.
- Rechazo cuando falta el token.

Ejecutar:

```bash
php artisan test --filter=LucodeSunatServiceTest
```
