# Errores conocidos y soluciones

## Índice

1. [Firma TED — llave RSASK incompatible con OpenSSL](#1-firma-ted--llave-rsask-incompatible-con-openssl)
2. [SII rechaza XML por canonicalización incorrecta](#2-sii-rechaza-xml-por-canonicalización-incorrecta)
3. [PFX no se puede desencriptar](#3-pfx-no-se-puede-desencriptar)
4. [CAF agotado o sin folios disponibles](#4-caf-agotado-o-sin-folios-disponibles)
5. [SII no devuelve TOKEN en autenticación](#5-sii-no-devuelve-token-en-autenticación)
6. [TRACKID ausente en respuesta de envío](#6-trackid-ausente-en-respuesta-de-envío)
7. [DTE rechazado por RUT inválido](#7-dte-rechazado-por-rut-inválido)
8. [Folio duplicado en emisión concurrente](#8-folio-duplicado-en-emisión-concurrente)
9. [Namespace incorrecto en modelos DTE](#9-namespace-incorrecto-en-modelos-dte)
10. [PHP 8.5 deprecations en Laravel 9](#10-php-85-deprecations-en-laravel-9)
11. [composer require phpseclib falla por advisories](#11-composer-require-phpseclib-falla-por-advisories)
12. [Enviar libro a DTEUpload retorna error de formato](#12-enviar-libro-a-dteupload-retorna-error-de-formato)
13. [Token SII expira en 90 segundos, no 5 minutos](#13-token-sii-expira-en-90-segundos-no-5-minutos)
14. [Tablas dte_envios y dte_logs vacías sin columnas](#14-tablas-dte_envios-y-dte_logs-vacías-sin-columnas)
15. [SSL Windows con WSDL remoto del SII](#15-ssl-windows-con-wsdl-remoto-del-sii)
16. [password_pfx guardado en plano](#16-password_pfx-guardado-en-plano)
17. [Boletas (39/41) enviadas a maullin.sii.cl en vez de rahue.sii.cl](#17-boletas-3941-enviadas-a-maullinsiicl-en-vez-de-rahuesiicl)

---

## 1. Firma TED — llave RSASK incompatible con OpenSSL

**Síntoma:** `openssl_sign()` retorna `false` o error al firmar el nodo `DD` del TED.

**Causa:** Los CAF del SII contienen llaves RSA antiguas (generalmente 1024 bits) que OpenSSL moderno rechaza por considerarlas inseguras.

**Solución:** Usar `phpseclib3\Crypt\RSA` en lugar de `openssl_sign` directamente:

```php
use phpseclib3\Crypt\RSA;

$key = RSA::load($rsask);
$key = $key->withPadding(RSA::SIGNATURE_PKCS1)->withHash('sha1');
$firma = base64_encode($key->sign($ddCanonical));
```

**Archivo:** `Services/TedBuilder.php`

---

## 2. SII rechaza XML por canonicalización incorrecta

**Síntoma:** SII retorna `ESTADO=RCH` o el XML no pasa validación de firma en el sobre.

**Causas posibles:**
- Whitespace extra en el XML antes de canonicalizar
- Encoding UTF-8 en lugar de ISO-8859-1
- Nodo `Documento` con ID dinámico que no coincide con la referencia de la firma

**Diagnóstico:**
1. Comparar XML generado contra fixture válido de `DTEngine`
2. Verificar que `DOMDocument` use `ISO-8859-1` al cargar el XML
3. Verificar que el atributo `ID` del nodo `Documento` coincida con la referencia `URI="#DTE-33F1001"` de la firma

**Solución:**
```php
$doc = new \DOMDocument('1.0', 'ISO-8859-1');
$doc->preserveWhiteSpace = false;
$doc->loadXML($xmlDte);
```

**Fallback manual si xmlseclibs no produce canonicalización correcta:**
```php
$canonical = $doc->C14N(false, false);
openssl_sign($canonical, $signature, $privateKey, OPENSSL_ALGO_SHA1);
$sigB64 = base64_encode($signature);
```

---

## 3. PFX no se puede desencriptar

**Síntoma:** `openssl_pkcs12_read` retorna `false`.

**Causas posibles:**
- Password incorrecto
- Archivo PFX corrupto o incompleto
- PFX creado con OpenSSL legacy que PHP 8+ no reconoce

**Diagnóstico:**
```bash
openssl pkcs12 -in certificado.pfx -noout -legacy
```

**Solución para PFX legacy:**
```bash
# Convertir a formato compatible con OpenSSL 3.x
openssl pkcs12 -in legacy.pfx -legacy -passin pass:CLAVE -out cert.pem -passout pass:CLAVE
openssl pkcs12 -in cert.pem -export -out modern.pfx -passout pass:CLAVE
```

**Nota:** No guardar passwords en logs. El `CertificateService` ya maneja esto.

---

## 4. CAF agotado o sin folios disponibles

**Síntoma:** `CafException: CAF agotado para empresa X tipo DTE Y`.

**Causa:** `dte_folio_usages.folio` llegó al límite `dte_cafs.folio_hasta`.

**Solución:**
1. Solicitar nuevo CAF al SII en ambiente productivo
2. Subir el XML del CAF desde el panel de administración
3. El nuevo CAF queda activo automáticamente si tiene `activo=1`

**Verificación:**
```sql
SELECT folio_desde, folio_hasta, usado_hasta, activo
FROM dte_cafs
WHERE par_empresa_id = X AND tipo_dte = 39
ORDER BY id DESC;
```

---

## 5. SII no devuelve TOKEN en autenticación

**Síntoma:** `SiiException: SII no devolvió TOKEN válido`.

**Causas posibles:**
- Semilla expirada (válida solo por ~1 minuto)
- Firma de semilla incorrecta (canonicalización)
- Certificado vencido o RUT no habilitado en SII

**Diagnóstico:**
- Revisar la respuesta raw en el log: `SiiAuthService: respuesta sin TOKEN`
- Verificar si el certificado está vigente en `dte_certificados.fecha_vencimiento`
- Probar en Maullin (certificación) antes que en Palena (producción)

**Solución temporal:** Limpiar la caché del token:
```php
Cache::forget('sii_token_' . md5($rutEmisor));
```

---

## 6. TRACKID ausente en respuesta de envío

**Síntoma:** `SiiDteClient: SII no devolvió TRACKID`.

**Causas posibles:**
- Token expirado o inválido
- XML malformado (SII rechazó sin dar track_id)
- RUT de empresa sin habilitación para enviar en ambiente de producción

**Diagnóstico:**
- Revisar el `rawResponse` en el log: `SiiDteClient: respuesta envio`
- Si la respuesta contiene `<STATUS>-11</STATUS>` → token inválido
- Si contiene `<DETAIL>` con errores → XML tiene problemas estructurales

**Solución:** El DTE queda en estado `firmado`. Usar `POST /api/dte/{id}/reenviar` para reintentar cuando el problema se resuelva.

---

## 7. DTE rechazado por RUT inválido

**Síntoma:** `Factura requiere RUT válido del cliente` o SII rechaza con `ESTADO=RCH`.

**Causas:**
- Cliente en POS tiene `rut = null` o `rut = 66666666-6` y se está emitiendo una factura (tipo 33/34/52)
- RUT con formato incorrecto (sin DV, sin guión)

**Solución en POS:** Antes de emitir una factura, el frontend debe validar que el receptor tenga RUT completo. Esta validación ya existe en `PosVentaController`:

```php
if (in_array($tipoDte, [33, 34, 52])) {
    if (!$cliente->rut || $cliente->rut === '66666666-6') {
        throw new \Exception('Factura requiere RUT válido del cliente');
    }
}
```

Para boletas (39/41) el RUT `66666666-6` es válido (receptor genérico).

---

## 8. Folio duplicado en emisión concurrente

**Síntoma:** `SQLSTATE[23000]: Integrity constraint violation` en `dte_folio_usages`.

**Causa:** Dos peticiones simultáneas intentaron reservar el mismo folio.

**Solución:** El `CafService::reservarFolio` ya usa `DB::transaction` + `lockForUpdate`. Si aun ocurre, verificar que el motor de base de datos soporte transacciones (InnoDB, no MyISAM).

**Verificación:**
```sql
SHOW TABLE STATUS WHERE Name = 'dte_cafs';
-- Engine debe ser InnoDB
```

---

## 9. Namespace incorrecto en modelos DTE

**Síntoma:** `Class 'App\Models\DteLog' not found` o similar.

**Causa:** Los modelos `DteLog`, `DteEnvio` y `DteDetalle` estaban originalmente con `namespace App\Models` aunque el archivo está en `app/Models/Dte/`.

**Estado:** Corregido en el commit inicial de la migración. Los tres modelos ahora tienen `namespace App\Models\Dte`.

**Si aparece en otro modelo DTE:** buscar con:
```bash
grep -r "^namespace App\\\\Models;" app/Models/Dte/
```

---

## 10. PHP 8.5 deprecations en Laravel 9

**Síntoma:** Warnings `PHP Deprecated: Implicitly marking parameter ... as nullable is deprecated` al correr artisan o composer.

**Causa:** PHP 8.5 introduce nuevas deprecations para firmas de métodos que Laravel 9 no cumple.

**Impacto:** Solo son warnings, no errores. La aplicación funciona correctamente.

**Solución a largo plazo:** Actualizar a Laravel 10 o 11 (fuera del alcance de esta migración según el plan).

**Solución temporal para ocultar en logs:**
```php
// En bootstrap/app.php o AppServiceProvider::boot()
error_reporting(E_ALL & ~E_DEPRECATED);
```

---

## 11. composer require phpseclib falla por advisories

**Síntoma:**
```
found phpseclib/phpseclib[3.0.0] but these were not loaded,
because they are affected by security advisories
```

**Causa:** Composer 2.x bloquea paquetes con advisories de seguridad. La versión exacta `3.0` se interpreta como `3.0.0` exacto, que tiene advisories.

**Solución aplicada:**
```json
"config": {
    "audit": {
        "block-insecure": false
    }
}
```
Y usar el constraint `^3.0.0` con `--no-security-blocking`:
```bash
composer update phpseclib/phpseclib --no-security-blocking --ignore-platform-reqs
```

**Versión instalada:** `3.0.52` (sin vulnerabilidades activas conocidas para el caso de uso de firma RSA).

---

## 12. Enviar libro: IECVUpload vs DTEUpload

**Síntoma:** Al enviar un libro al endpoint equivocado, el SII puede rechazar por estructura inválida o devolver respuesta no-XML.

**Endpoints reales:**
- DTEs (33, 34, 39, 41, 52, 56, 61): `DTEUpload` (host `maullin/palena` para facturas/notas/guías, `rahue/pangal` para boletas)
- Libros (IECV: VENTA, COMPRA, GUIA): `IECVUpload` (host `maullin/palena`)

**Estado:** `SiiLibroClient` implementado con endpoint **configurable** — comando `dte:test-libro` permite probar ambos:

```bash
# Endpoint oficial
php artisan dte:test-libro 1 path/libro.xml --endpoint=iecv --tipo=VENTA

# Endpoint experimental (mismo URL que DTEs)
php artisan dte:test-libro 1 path/libro.xml --endpoint=dte --tipo=VENTA
```

Esto permite diagnosticar si IECVUpload no responde validando el resto del pipeline (token, multipart, cert) contra el endpoint DTE conocido.

---

## 13. Token SII expira en 90 segundos, no 5 minutos

**Síntoma:** Envíos fallan con `STATUS=-11` (token inválido) cuando el flujo tarda más de ~1.5 minutos entre obtener el token y enviar el DTE.

**Causa:** El TTL real del token SII es **90 segundos**, no 5 minutos como se asumía en la documentación inicial.

**Solución:**

En `SiiAuthService`, ajustar el cache TTL:

```php
Cache::remember("sii_token_{$rutEmisor}", 80, function() {
    // 80s para tener margen antes de los 90s reales
    return $this->solicitarTokenNuevo();
});
```

**Verificación:**
Si después de varias emisiones consecutivas aparecen errores `STATUS=-11`, revisar el log de `SiiAuthService` para ver cuánto tiempo pasó entre token y envío.

---

## 14. Tablas dte_envios y dte_logs vacías sin columnas

**Síntoma:** Los modelos `DteEnvio` y `DteLog` se pueden instanciar pero al hacer `create([...])` con campos como `track_id` o `evento`, MySQL rechaza con `Unknown column`.

**Causa:** Las migraciones que crearon estas tablas (`2025_12_23_171848_create_dte_envios_table.php` y `2025_12_23_171904_create_dte_logs_table.php`) son **stubs** — solo crean `id + timestamps` sin columnas reales.

**Estado:** Gap conocido, pendiente cerrar. Ver `TRAZABILIDAD.md` §2 para diseño completo.

**Solución (pendiente):**

Crear migraciones nuevas que agreguen columnas a las tablas existentes:

```php
// 2026_05_19_000001_complete_dte_envios_table.php
Schema::table('dte_envios', function (Blueprint $table) {
    $table->foreignId('dte_documento_id')->after('id')->constrained();
    $table->string('track_id', 50)->nullable()->index();
    $table->string('endpoint', 100);
    $table->string('ambiente', 20);
    $table->unsignedSmallInteger('http_status')->nullable();
    $table->longText('xml_enviado')->nullable();
    $table->longText('respuesta_raw')->nullable();
    $table->json('respuesta_parseada')->nullable();
    $table->string('error_mensaje', 500)->nullable();
    $table->timestamp('enviado_en');
    $table->enum('resultado', ['exito', 'error_http', 'sin_trackid', 'rechazado_estructura'])->index();
});
```

**Por ahora:** El flujo de emisión guarda en `dte_documentos.respuesta_sii` directamente, sin histórico de intentos. Para reintentos no hay trazabilidad granular todavía.

---

## 15. SSL Windows con WSDL remoto del SII

**Síntoma:** `SoapFault: Could not connect to host` o errores de certificado SSL al consumir un servicio SOAP del SII desde Windows.

**Causa:** PHP en Windows puede tener problemas validando el certificado SSL del SII desde el WSDL remoto.

**Solución aplicada en DTEngine:** Cachear el WSDL local en `storage/wsdl/`:

```text
storage/wsdl/
├── CrSeed.wsdl
├── GetTokenFromSeed.wsdl
└── QueryEstUp.wsdl
```

Configurar `SoapClient` para usar la URL local:

```php
$client = new SoapClient(storage_path('wsdl/CrSeed.wsdl'), [
    'cache_wsdl' => WSDL_CACHE_BOTH,
    'trace' => true,
]);
```

**Alternativa:** Usar Guzzle/Http con SOAP manual (es el approach actual del `SiiDteClient::consultarEstado`), evitando `SoapClient`. Funciona bien y no depende del WSDL.

---

## 16. password_pfx guardado en plano

**Síntoma:** Inspeccionando `dte_certificados.password_pfx` se ve el password en texto claro.

**Causa:** El campo está como `varchar(255)` sin cifrado aplicado.

**Riesgo:** Si la BD se filtra (backup expuesto, dump robado), los passwords de los certificados quedan accesibles.

**Solución (pendiente):**

1. Agregar mutador al modelo `DteCertificado`:

```php
public function setPasswordPfxAttribute(string $value): void
{
    $this->attributes['password_pfx'] = Crypt::encryptString($value);
}

public function getPasswordPfxAttribute(string $value): string
{
    return Crypt::decryptString($value);
}
```

2. Migrar passwords existentes (si los hay):

```php
// Comando one-shot
\App\Models\Dte\DteCertificado::all()->each(function ($cert) {
    $plain = DB::table('dte_certificados')->where('id', $cert->id)->value('password_pfx');
    $cert->password_pfx = $plain; // pasa por el mutador
    $cert->save();
});
```

**Por ahora:** mientras no se aplique, no compartir la BD `aconta_fact_db` y mantener el password fuera de logs.

---

## 17. Boletas (39/41) enviadas a maullin.sii.cl en vez de rahue.sii.cl

**Síntoma:** Boletas electrónicas rechazadas por el SII o sin TRACKID al enviarlas al endpoint general de Maullín.

**Causa:** El SII tiene servidores separados para boletas. Confirmado en `DTEngine\app\Services\SII\SiiTransportService.php` línea 27-36:

```text
certificacion:
    dte    => maullin.sii.cl   (facturas, notas, guías)
    boleta => rahue.sii.cl     (boletas 39, 41)

produccion:
    dte    => palena.sii.cl
    boleta => pangal.sii.cl    (boletas 39, 41)
```

**Solución aplicada:** `SiiDteClient::urlEnvio(int $tipoDte)` selecciona host correcto según tipo. Implementado en commit nuevo. El `DteEmissionService` pasa el tipo al llamar `enviar()`.

**Verificación:** Si una boleta sale a `maullin.sii.cl`, hay que revisar que el caller esté pasando `$dte->tipo_dte` al método `enviar()`.

---

## Errores pendientes de confirmar en pruebas

Estos errores son potenciales pero no han ocurrido aún porque el motor interno todavía no se ha probado contra el SII real:

| Error potencial | Riesgo | Acción |
|---|---|---|
| TED no pasa validación SII | Alto | Comparar con fixture de DTEngine |
| Canonicalización del Documento distinta a la esperada | Alto | Probar con xmlseclibs vs C14N manual |
| Encoding ISO-8859-1 vs UTF-8 al enviar | Medio | Verificar Content-Type del envío multipart |
| Token SII expira durante emisión larga | Bajo | Cache de 5 min cubre el flujo normal |
| CAF de otro empresa usado por error | Bajo | CafService valida par_empresa_id |
