# FacturaPY — Backend (Laravel API)

API REST construida con Laravel 11. Gestiona emisores, documentos tributarios electrónicos, comunicación SOAP con SIFEN y generación del KuDE.

---

## Estructura de carpetas

```
app/
├── app/
│   ├── Console/
│   │   └── Commands/
│   │       └── SifenHomologar.php          # Comando para enviar set de pruebas a DNIT
│   ├── Exceptions/
│   │   └── SifenException.php
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── Admin/
│   │   │   │   └── AdminTenantController.php   # Panel super admin (tenants, planes, stats)
│   │   │   ├── Auth/
│   │   │   │   └── AuthController.php
│   │   │   ├── ConfiguracionController.php # 5 secciones: emisor, timbrado, cert, sifen, email
│   │   │   ├── EmisorController.php        # GET/PUT /api/emisor (legacy)
│   │   │   ├── FacturaController.php       # CRUD + emisión de facturas
│   │   │   ├── ClienteController.php       # Receptores de facturas
│   │   │   ├── ProductoController.php      # Catálogo de ítems
│   │   │   ├── DashboardController.php     # Estadísticas del dashboard
│   │   │   ├── KudeController.php          # Generación y descarga de PDF
│   │   │   ├── SifenController.php         # Consultas directas al SIFEN
│   │   │   └── TenantRegistrationController.php  # POST /api/register (onboarding)
│   │   ├── Middleware/
│   │   │   ├── SetTenant.php               # Resuelve y guarda el tenant activo
│   │   │   ├── CheckPlan.php               # Valida límite de DEs por suscripción
│   │   │   ├── SuperAdmin.php              # Solo usuarios is_super_admin
│   │   │   └── EmisorActivo.php
│   │   └── Requests/
│   │       ├── FacturaRequest.php
│   │       └── EmisorRequest.php
│   ├── Jobs/
│   │   ├── ProcesarDTE.php                 # Job principal: firma + envío SIFEN
│   │   ├── ConsultarEstadoDTE.php          # Polling estado en SIFEN
│   │   └── EnviarKudeEmail.php             # Envío de KuDE al receptor
│   ├── Mail/
│   │   └── TestConnectionMail.php          # Mailable de prueba de conexión SMTP
│   ├── Models/
│   │   ├── User.php
│   │   ├── Tenant.php                      # tbl_tenants
│   │   ├── Suscripcion.php                 # tbl_suscripciones
│   │   ├── ConfiguracionEmail.php          # tbl_configuracion_email — SMTP por tenant
│   │   ├── Emisor.php                      # tbl_emisor — RUC, timbrado, CCFE, logo
│   │   ├── Factura.php                     # tbl_facturas — DTE principal
│   │   ├── FacturaItem.php                 # tbl_factura_items — ítems (grupo E8)
│   │   ├── Cliente.php                     # tbl_clientes — Receptores
│   │   ├── Producto.php                    # tbl_productos
│   │   ├── Evento.php                      # tbl_eventos — Cancelaciones, inutilizaciones
│   │   └── TimbradoLog.php
│   ├── Services/
│   │   ├── Sifen/
│   │   │   ├── SifenService.php            # Orquestador principal
│   │   │   ├── XmlGeneratorService.php     # Genera XML DTE grupos AA→J ✅
│   │   │   ├── FirmaService.php            # Firma XMLDSig con CCFE ⏳ pendiente
│   │   │   ├── SoapClientService.php       # Cliente SOAP para los WS ⏳ pendiente
│   │   │   ├── CdcService.php              # Cálculo del CDC 44 chars + dígito mod11 ✅
│   │   │   └── ValidatorService.php        # Valida XML contra schemas XSD locales ✅
│   │   ├── IvaCalculatorService.php        # IVA 10%, 5%, exento ✅
│   │   ├── KudeService.php                 # Genera PDF del KuDE (DomPDF) ✅
│   │   └── QrService.php                   # Genera QR SVG (bacon/bacon-qr-code) ✅
│   ├── Traits/
│   │   ├── BelongsToTenant.php             # Global scope tenant_id automático
│   │   └── HasCdc.php
│   └── helpers.php                         # currentTenant() helper global
├── config/
│   └── sifen.php                           # Config centralizada SIFEN + endpoints
├── database/
│   ├── migrations/
│   │   ├── ..._create_tbl_emisor_table.php
│   │   ├── ..._create_tbl_facturas_table.php
│   │   ├── ..._create_tbl_factura_items_table.php
│   │   ├── ..._create_tbl_clientes_table.php
│   │   ├── ..._create_tbl_productos_table.php
│   │   ├── ..._create_tbl_eventos_table.php
│   │   ├── 000010_create_tbl_tenants_table.php
│   │   ├── 000011_create_tbl_suscripciones_table.php
│   │   └── 000012_add_tenant_id_to_all_tables.php
│   └── seeders/
│       ├── DatabaseSeeder.php
│       └── EmisorSeeder.php
├── resources/
│   └── views/
│       └── kude/
│           └── factura.blade.php           # Template PDF del KuDE
├── routes/
│   └── api.php
├── storage/
│   ├── app/
│   │   ├── public/
│   │   │   └── logos/{tenant_id}/          # Logos de emisores (accesible vía URL)
│   │   └── certs/{tenant_id}/              # CCFE .p12 (NUNCA en public)
└── tests/
    ├── Feature/
    │   ├── FacturaTest.php
    │   └── SifenTest.php
    └── Unit/
        ├── XmlGeneratorTest.php            # ✅ PASSED
        ├── CdcTest.php                     # ✅ PASSED
        ├── IvaCalculatorTest.php           # ✅ PASSED
        ├── FirmaTest.php
        └── ExampleTest.php                 # ✅ PASSED
```

---

## Multi-tenant

- **Modelo:** Opción A — una sola DB, `tenant_id` en todas las tablas
- **Tablas principales:** `tbl_tenants`, `tbl_suscripciones`
- **Trait `BelongsToTenant`:** añade global scope que filtra por `tenant_id` automáticamente en todos los modelos
- **Middlewares:**
  - `SetTenant` — resuelve el tenant del usuario autenticado y lo inyecta en el contenedor (`app('current_tenant')`)
  - `CheckPlan` — valida que el tenant no haya superado el límite de DEs de su suscripción
  - `SuperAdmin` — bloquea acceso si `user.is_super_admin !== true`
- **Helper global:** `currentTenant()` — retorna `app('current_tenant')` o `null`

---

## Dependencias principales

```json
{
  "require": {
    "php": "^8.3",
    "laravel/framework": "^11.0",
    "laravel/sanctum": "^4.0",
    "ext-soap": "*",
    "ext-openssl": "*",
    "ext-dom": "*",
    "robrichards/xmlseclibs": "^3.1",
    "barryvdh/laravel-dompdf": "^2.2",
    "bacon/bacon-qr-code": "^3.0",
    "spatie/laravel-permission": "^6.0"
  }
}
```

---

## Rutas API

```
POST   /api/auth/login
POST   /api/auth/logout
GET    /api/auth/me

POST   /api/register                        # Onboarding público: crea tenant+user+emisor

# Super admin (middleware: auth:sanctum, super.admin)
GET    /api/admin/tenants
POST   /api/admin/tenants
GET    /api/admin/tenants/{id}
PUT    /api/admin/tenants/{id}
PUT    /api/admin/tenants/{id}/suspender
PUT    /api/admin/tenants/{id}/plan
GET    /api/admin/stats

# Tenant (middleware: auth:sanctum, tenant)
GET    /api/emisor
PUT    /api/emisor

GET    /api/clientes
POST   /api/clientes
GET    /api/clientes/{id}
PUT    /api/clientes/{id}
DELETE /api/clientes/{id}

GET    /api/sifen/ruc/{ruc}

GET    /api/productos
POST   /api/productos
PUT    /api/productos/{id}
DELETE /api/productos/{id}

GET    /api/facturas
POST   /api/facturas                        # middleware check.plan:emitir
GET    /api/facturas/{id}
DELETE /api/facturas/{id}
GET    /api/facturas/{id}/kude
GET    /api/facturas/{id}/xml

GET    /api/dashboard/stats

# Configuración SIFEN
GET    /api/configuracion/emisor
PUT    /api/configuracion/emisor
POST   /api/configuracion/emisor/logo
GET    /api/configuracion/timbrado
PUT    /api/configuracion/timbrado
GET    /api/configuracion/certificado
POST   /api/configuracion/certificado
DELETE /api/configuracion/certificado
GET    /api/configuracion/sifen
PUT    /api/configuracion/sifen
POST   /api/configuracion/sifen/probar-conexion

GET    /api/configuracion/email
PUT    /api/configuracion/email
POST   /api/configuracion/email/probar
DELETE /api/configuracion/email
```

---

## Modelos y base de datos

Todas las tablas usan el prefijo `tbl_`.

### `tbl_emisor`
```
id, tenant_id, ruc, razon_social, nombre_fantasia,
tipo_contribuyente, establecimiento, punto_expedicion,
timbrado, timbrado_fecha_inicio, timbrado_fecha_fin, numero_actual,
actividad_economica_codigo, actividad_economica_desc, tipo_regimen,
direccion, departamento_codigo, departamento_desc,
distrito_codigo, distrito_desc, ciudad_codigo, ciudad_desc,
telefono, email,
cert_path, cert_password (encrypted con encrypt()), cert_vencimiento,
logo_path, sifen_ambiente (test|production), activo,
created_at, updated_at
```

### `tbl_facturas`
```
id, tenant_id, emisor_id, cliente_id,
cdc (44 chars, unique), numero_doc, serie, tipo_doc,
fecha_emision, condicion_operacion,
xml_generado (text), xml_firmado (text),
estado (borrador|pendiente|aprobado|rechazado|cancelado),
numero_protocolo, fecha_procesamiento,
total_gravado_10, total_gravado_5, total_exento,
iva_10, iva_5, total,
created_at, updated_at
```

### `tbl_factura_items`
```
id, tenant_id, factura_id, producto_id,
descripcion, cantidad, precio_unitario,
descuento, total_item,
afectacion_iva, tasa_iva, iva_item
```

### `tbl_tenants`
```
id, nombre, slug, ruc, email_admin, estado (activo|suspendido|cancelado),
created_at, updated_at
```

### `tbl_suscripciones`
```
id, tenant_id, plan (basico|estandar), limite_de_anual,
de_emitidos_anio, plan_vence, monto,
created_at, updated_at
```

### `tbl_configuracion_email`
```
id, tenant_id (unique — un registro por tenant),
mail_host, mail_port,
mail_username, mail_password (encrypted con encrypt()),
mail_encryption (tls|ssl|none),
mail_from_address, mail_from_name,
activo (boolean — false = usa SMTP global de FacturaPY),
created_at, updated_at
```

- `mail_password` siempre encriptado con `encrypt()` — el accessor devuelve `null` siempre; usar `getRawOriginal('mail_password')` antes de `decrypt()` para leer el valor real
- La API incluye `has_password: bool` en el GET para que el frontend sepa si ya hay password guardado sin exponerlo
- Si `activo = false` o no existe registro, el job `EnviarKudeEmail` usa el SMTP global del `.env`

---

## Servicios SIFEN

### Estado de implementación

| Servicio | Estado |
|---|---|
| `CdcService` | ✅ Funcionando — CDC 44 chars + dígito verificador módulo 11 |
| `XmlGeneratorService` | ✅ Funcionando — genera XML DTE grupos AA→J |
| `ValidatorService` | ✅ Funcionando — valida XML contra schemas XSD locales |
| `IvaCalculatorService` | ✅ Funcionando — IVA 10%, 5%, exento |
| `KudeService` | ✅ Funcionando — PDF con DomPDF, diseño oficial paraguayo aprobado |
| `QrService` | ✅ Funcionando — SVG con `bacon/bacon-qr-code` SvgImageBackEnd, 80px |
| `FirmaService` | ⏳ Pendiente — requiere CCFE de la DNIT |
| `SoapClientService` | ⏳ Pendiente — requiere CCFE y firma |

### `XmlGeneratorService` — grupos XML

```
Grupo AA  — versión del formato (AA001-AA009)
Grupo A   — campos firmados, contenedor del DE (A001-A099)
Grupo B   — operación del DE: tipo doc, fecha, moneda (B001-B099)
Grupo C   — timbrado: número, establecimiento, punto (C001-C099)
Grupo D   — datos generales:
  D1      — operación comercial
  D2      — emisor + actividad económica
  D3      — receptor
Grupo E   — específico por tipo de documento:
  E1      — Factura Electrónica (FE)
  E7      — condición: contado / crédito / forma de pago
  E8      — ítems: descripción, precio, IVA por ítem
Grupo F   — subtotales, totales, liquidación IVA
Grupo G   — campos complementarios generales
Grupo H   — documentos asociados (si aplica)
Grupo I   — información de la firma digital
```

### `CdcService` — estructura CDC 44 chars

```
Posición  1     → Tipo de DTE (1=FE, 4=AFE, 5=NCE, 6=NDE, 7=NRE)
Posición  2-9   → Fecha de emisión (AAAAMMDD)
Posición 10-18  → RUC del emisor (sin dígito verificador)
Posición 19     → Dígito verificador del RUC
Posición 20-22  → Establecimiento (001)
Posición 23-25  → Punto de expedición (001)
Posición 26-32  → Número del documento (0000001)
Posición 33     → Tipo de contribuyente (1=física, 2=jurídica)
Posición 34     → Ambiente (1=producción, 2=test)
Posición 35-43  → Código de seguridad (9 dígitos aleatorios)
Posición 44     → Dígito verificador del CDC (módulo 11)
```

### `KudeService` — estructura del PDF

Diseño oficial paraguayo aprobado. Blanco y negro. DomPDF con rutas absolutas filesystem (nunca URLs HTTP).

```
1. Header — 2 bloques: logo+datos emisor (izq) / tipo doc+números+CDC (der)
2. Receptor — datos del comprador
3. Tabla de ítems — con filas vacías hasta completar página
4. Subtotales — fondo gris
5. EN LETRAS — monto total en palabras
6. LIQUIDACIÓN IVA — tabla 10% / 5% / exento
7. Footer — QR (80px SVG) + CDC completo + nota 72hs
```

Logo del emisor: `storage/app/public/logos/{tenant_id}/` — se pasa como path absoluto a DomPDF.

### `QrService`

Usa `bacon/bacon-qr-code` con `SvgImageBackEnd`. **No usa imagick** (no disponible en XAMPP).

### `ConfiguracionController`

5 secciones independientes:
- **Emisor:** RUC, razón social, actividad económica, dirección, departamento, logo (`storage/app/public/logos/{tenant_id}/`)
- **Timbrado:** número, vigencia, establecimiento, punto expedición
- **Certificado CCFE:** subida .p12/.pfx → `storage/app/certs/{tenant_id}/` (local, **nunca public**), password encriptado con `encrypt()`; estados: `no_cargado | cargado | vigente | por_vencer | vencido`
- **SIFEN:** ambiente `test | production`, endpoints informativos, prueba de conexión (simulada hasta tener `FirmaService`)
- **Correo (SMTP):** config almacenada en `tbl_configuracion_email`; permite que los KuDE salgan desde el dominio propio del cliente; si `activo = false` o no hay registro, usa SMTP global del `.env`

#### Lógica de mailer dinámico en `EnviarKudeEmail` job
```php
$config = ConfiguracionEmail::where('tenant_id', $factura->tenant_id)
    ->where('activo', true)->first();

if ($config) {
    config(['mail.mailers.tenant_smtp' => [
        'transport'  => 'smtp',
        'host'       => $config->mail_host,
        'port'       => $config->mail_port,
        'username'   => $config->mail_username,
        'password'   => decrypt($config->getRawOriginal('mail_password')),
        'encryption' => $config->mail_encryption === 'none' ? null : $config->mail_encryption,
    ],
    'mail.from.address' => $config->mail_from_address,
    'mail.from.name'    => $config->mail_from_name,
    ]);
    Mail::mailer('tenant_smtp')->to($receptor)->send(new KudeMail($factura));
} else {
    Mail::to($receptor)->send(new KudeMail($factura)); // fallback SMTP global
}
```

---

## Configuración SIFEN (`config/sifen.php`)

```php
return [
    'env'       => env('SIFEN_ENV', 'test'),
    'namespace' => env('SIFEN_NAMESPACE', 'http://efacturapy.set.gov.py/sifen/xsd'),
    'endpoints' => [
        'test' => [
            'recibe'        => 'https://sifen-test.set.gov.py/de/ws/sync/recibe.wsdl',
            'recibe_lote'   => 'https://sifen-test.set.gov.py/de/ws/async/recibe-lote.wsdl',
            'consulta_lote' => 'https://sifen-test.set.gov.py/de/ws/consultas/consulta-lote.wsdl',
            'consulta'      => 'https://sifen-test.set.gov.py/de/ws/consultas/consulta.wsdl',
            'evento'        => 'https://sifen-test.set.gov.py/de/ws/eventos/evento.wsdl',
            'consulta_ruc'  => 'https://sifen-test.set.gov.py/de/ws/consultas/consulta-ruc.wsdl',
        ],
        'production' => [
            'recibe'        => 'https://sifen.set.gov.py/de/ws/sync/recibe.wsdl',
            'recibe_lote'   => 'https://sifen.set.gov.py/de/ws/async/recibe-lote.wsdl',
            'consulta_lote' => 'https://sifen.set.gov.py/de/ws/consultas/consulta-lote.wsdl',
            'consulta'      => 'https://sifen.set.gov.py/de/ws/consultas/consulta.wsdl',
            'evento'        => 'https://sifen.set.gov.py/de/ws/eventos/evento.wsdl',
            'consulta_ruc'  => 'https://sifen.set.gov.py/de/ws/consultas/consulta-ruc.wsdl',
        ],
    ],
    'cert_path'     => env('SIFEN_CERT_PATH', storage_path('certs/cert.p12')),
    'cert_password' => env('SIFEN_CERT_PASSWORD', ''),
    'qr_base_url'   => 'https://efacturapy.set.gov.py/consultas/qr',
];
```

---

## Queue y jobs

Los jobs se procesan con Redis. En desarrollo correr:

```bash
php artisan queue:work redis --queue=sifen
```

### Estados del DTE

```
borrador   → factura creada, sin CCFE, no enviada
pendiente  → encolada, esperando procesamiento
enviada    → XML enviado al SIFEN, esperando respuesta asíncrona
aprobado   → DNIT aprobó el DTE (tiene número de protocolo)
rechazado  → DNIT rechazó (con códigos de error)
cancelado  → evento de cancelación aprobado
```

---

## Comandos útiles

```bash
# Validar XML contra schemas locales (sin firma, sin SIFEN)
php artisan sifen:validar-xml {factura_id}

# Enviar set de pruebas para homologación
php artisan sifen:homologar

# Consultar estado de un DTE en SIFEN
php artisan sifen:consultar {cdc}

# Regenerar KuDE de una factura
php artisan kude:generar {factura_id}
```

---

## Testing

```bash
# Unit: generador XML, CDC, IVA, ejemplo (17/17 PASSED)
php artisan test --testsuite=Unit

# Feature: endpoints API
php artisan test --testsuite=Feature

# Con SIFEN test (requiere CCFE)
SIFEN_ENV=test php artisan test --group=sifen
```

### Suites Unit actuales

| Test | Tests | Estado |
|---|---|---|
| `CdcTest` | 5 | ✅ PASSED |
| `IvaCalculatorTest` | 6 | ✅ PASSED |
| `XmlGeneratorTest` | 5 | ✅ PASSED |
| `ExampleTest` | 1 | ✅ PASSED |

---

## Estado actual (2026-07-03)

**Próximo paso:** `FirmaService` (firma XMLDSig con CCFE usando `robrichards/xmlseclibs`) + `SoapClientService` (envío SOAP al SIFEN). Ambos bloqueados hasta recibir el CCFE de la DNIT.
