# SISTEMA DE FACTURACIÓN ELECTRÓNICA OFFLINE — DGII República Dominicana

> **Versión:** 1.0  
> **Fecha:** Julio 2026  
> **Proyecto:** NUBEPREST / SantanaSoft POS (SSB3)  
> **Proveedor e-CF:** GAE Digital (`fe.gaedigital.com`)

---

## 1. ARQUITECTURA GENERAL

```
┌──────────────┐     ┌──────────────┐     ┌──────────────┐     ┌──────┐
│   grabar.php │────▶│ecf_offline.php│────▶│  BD (ventas) │     │Ticket│
│  (COMMIT)    │     │ Genera XML    │     │ecf_respuesta │────▶│ QR ✓ │
│              │     │ + código L-*  │     │codigo_seg   │     │local │
│              │     │ + QR local    │     │estado=NO_ENV│     └──────┘
└──────────────┘     └──────┬───────┘     └──────┬───────┘
                            │                    │
                   ┌────────▼───────┐    ┌───────▼────────┐
                   │ enviar_a_dgii? │    │  ecf_cola (BD) │
                   │  Si ────▶ encola   │  payload=XML    │
                   │  No ────▶ solo local│  estado=PEND    │
                   └────────────────┘    └───────┬────────┘
                                                 │
                    ┌────────────────────────────▼──────────────┐
                    │  ecf_procesar_cola.php (CRON cada 1 min)  │
                    │  Lee cola → ecf_offline_enviar_a_gae()    │
                    │  Construye JSON → envía a GAE Digital     │
                    │  Recibe código real + QR real + XML firm. │
                    │  Actualiza ventas.ecf_respuesta           │
                    └───────────────────────────────────────────┘
```

**Principio**: La factura se cierra **instantáneamente** sin esperar a GAE.  
El envío real a DGII ocurre en **segundo plano** vía CRON.

---

## 2. BASE DE DATOS — NUEVAS COLUMNAS

### 2.1 Tabla `ventas`

| Columna | Tipo | Default | Descripción |
|---------|------|---------|-------------|
| `estado_dgii` | `VARCHAR(30)` | `'NO_ENVIADO'` | NO_ENVIADO, PENDIENTE, ENVIADO, ACEPTADO, RECHAZADO |
| `codigo_seguridad_dgii` | `VARCHAR(100)` | `''` | Código de seguridad (L-* = local, real tras GAE) |
| `track_id_dgii` | `VARCHAR(100)` | `''` | Track ID asignado por GAE/DGII |
| `ecf_enviado_at` | `DATETIME` | `NULL` | Fecha/hora de generación offline o envío a GAE |
| `ecf_respuesta` | `TEXT` | — | JSON completo con XML, código, QR URL, fecha firma |
| `enviar_a_dgii` | `ENUM('Si','No')` | `'Si'` | Controla si se encola para envío a GAE |

### 2.2 Tabla `parametros`

| Columna | Tipo | Default | Descripción |
|---------|------|---------|-------------|
| `metodo_envio_ecf` | `ENUM('Todo','Parcial')` | `'Todo'` | Todo=envía todos; Parcial=rota 3 Si / 3 No en E32 Consumo |

### 2.3 Tabla `ecf_cola` y `ecf_log` (existentes)

- `ecf_cola`: cola de envíos pendientes con reintentos
- `ecf_log`: historial de acciones por venta (log en BD)

### 2.4 Migración SQL

```sql
ALTER TABLE ventas ADD COLUMN IF NOT EXISTS estado_dgii VARCHAR(30) DEFAULT 'NO_ENVIADO';
ALTER TABLE ventas ADD COLUMN IF NOT EXISTS codigo_seguridad_dgii VARCHAR(100) DEFAULT '';
ALTER TABLE ventas ADD COLUMN IF NOT EXISTS track_id_dgii VARCHAR(100) DEFAULT '';
ALTER TABLE ventas ADD COLUMN IF NOT EXISTS ecf_enviado_at DATETIME NULL;
ALTER TABLE ventas ADD COLUMN IF NOT EXISTS ecf_respuesta TEXT;
ALTER TABLE ventas ADD COLUMN IF NOT EXISTS enviar_a_dgii ENUM('Si','No') DEFAULT 'Si';
ALTER TABLE parametros ADD COLUMN IF NOT EXISTS metodo_envio_ecf ENUM('Todo','Parcial') DEFAULT 'Todo';
```

---

## 3. ARCHIVOS CREADOS

### 3.1 `ecf/ecf_xml_builder.php`
Genera XML e-CF según schema DGII (ECF v1.0).

**Función principal:** `ecf_generar_xml($id_venta)`

**Estructura XML generada:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<ECF xmlns="http://dgii.gov.do/ecf">
  <Encabezado>
    <IdDoc>
      <TipoECF>31|32|44|46</TipoECF>
      <eNCF>E3100000001001</eNCF>
      <FechaVencimientoSecuencia>2026-12-31</FechaVencimientoSecuencia>
      <IndicadorMontoGravado>0</IndicadorMontoGravado>
      <TipoIngresos>01|02</TipoIngresos>
    </IdDoc>
    <Emisor>
      <RncEmisor>133432897</RncEmisor>
      <RazonSocialEmisor>...</RazonSocialEmisor>
      <NombreComercial>...</NombreComercial>
      <Sucursal>001</Sucursal>
      <DireccionEmisor>...</DireccionEmisor>
      <Municipio>...</Municipio>
      <Provincia>...</Provincia>
    </Emisor>
    <Comprador>...</Comprador>
    <InformacionPago>
      <FormaDePago>1-6</FormaDePago>
      <CondicionDePago>1|2</CondicionDePago>
      <FechaPago>YYYY-MM-DD</FechaPago>
    </InformacionPago>
    <Totales>
      <MontoGravadoI1>...</MontoGravadoI1>
      <TotalITBIS1>...</TotalITBIS1>
      <MontoGravadoI2>...</MontoGravadoI2>
      <TotalITBIS2>...</TotalITBIS2>
      <MontoGravadoI3>...</MontoGravadoI3>
      <TotalITBIS3>0.00</TotalITBIS3>
      <TotalMontoGravado>...</TotalMontoGravado>
      <TotalITBIS>...</TotalITBIS>
      <TotalMontoFactura>...</TotalMontoFactura>
      <TotalAvancePago>0.00</TotalAvancePago>
    </Totales>
  </Encabezado>
  <DetalleItems>
    <Item>
      <NumeroLinea>1</NumeroLinea>
      <IndicadorFacturacion>1</IndicadorFacturacion>
      <NombreItem>...</NombreItem>
      <IndicadorBienoServicio>1</IndicadorBienoServicio>
      <CantidadItem>1.00</CantidadItem>
      <UnidadMedida>43</UnidadMedida>
      <PrecioUnitarioItem>100.00</PrecioUnitarioItem>
      <MontoItem>118.00</MontoItem>
      <SubCantidad>0.00</SubCantidad>
      <MontoGravado>100.00</MontoGravado>
      <ImpuestoAdicional>
        <TipoImpuesto>1|2</TipoImpuesto>
        <MontoImpuesto>18.00</MontoImpuesto>
        <TasaImpuesto>18.00</TasaImpuesto>
      </ImpuestoAdicional>
    </Item>
  </DetalleItems>
  <FirmaDigital></FirmaDigital>  <!-- vacío, GAE lo completa -->
</ECF>
```

**TaxTypes:**
- `1` = ITBIS 18%
- `2` = ITBIS 16%  
- `3` = Exento
- `4` = No facturable (E44)

**FormasDePago:**
- `1` = Efectivo
- `2` = Cheque
- `3` = Tarjeta
- `4` = Transferencia
- `5` = Crédito
- `6` = Mixto

### 3.2 `ecf/ecf_seguridad_local.php`
Genera código de seguridad temporal offline.

**Formato:** `L-XXXXXXXX-XXXXXXXX-XXXX` (24 caracteres)
- Prefijo `L-` (Local)
- Hash SHA-256 del seed (RNC|eNCF|RNC comprador|monto|fecha|timestamp)
- Truncado a 20 caracteres + separadores

**Funciones:**
- `ecf_generar_codigo_seguridad_local($rnc_emisor, $encf, $rnc_comprador, $monto, $fecha)`
- `ecf_es_codigo_local($codigo)` → true si empieza con `L-`

### 3.3 `ecf/ecf_qr_local.php`
Construye URL del QR apuntando al portal DGII.

**URL base:** `https://ecf.dgii.gov.do/ecf/ConsultaTimbre`

**Parámetros:** `RncEmisor`, `eNCF`, `RncComprador`, `Valor`, `CodigoSeguridad`, `FechaEmision`

**Funciones:**
- `ecf_generar_qr_url(...)` — construye URL con parámetros
- `ecf_generar_qr_url_venta($id_venta, $codigo)` — wrapper para venta
- `ecf_obtener_qr_url_venta($id_venta)` — prioriza URL real de GAE, fallback a local

### 3.4 `ecf/ecf_offline.php`
**Orquestador principal** del flujo offline.

**Función principal:** `ecf_offline_procesar_venta($id_venta, $ncf, $tipo_ncf)`

**Flujo:**
1. Verifica que sea comprobante electrónico
2. Genera XML local (`ecf_generar_xml`)
3. Genera código seguridad local (`ecf_generar_codigo_seguridad_local`)
4. Genera QR URL local (`ecf_generar_qr_url_venta`)
5. Guarda todo en `ventas.ecf_respuesta` como JSON con `_offline: true`
6. Verifica `enviar_a_dgii` en la venta:
   - `'Si'` → encola en `ecf_cola` para envío posterior
   - `'No'` → solo guarda datos locales, NO encola

**Otras funciones:**
- `ecf_cola_agregar_offline(...)` — guarda XML en cola con metadatos
- `ecf_offline_enviar_a_gae($item_cola)` — envía item de cola a GAE, actualiza BD con datos reales
- `ecf_offline_procesar_cola($limite)` — procesa lote de cola (para ejecución manual)

### 3.5 `ecf/get_ecf_logs.php`
Endpoint JSON para la consola de logs.

**Parámetros:**
- `lineas=N` — últimas N líneas (default 200)
- `venta=ID` — consulta adicional BD `ecf_log` para la venta

**Respuesta:** JSON con array de logs (archivos + BD), cada uno con líneas clasificadas por tipo (`error`, `success`, `warning`, `info`).

### 3.6 `parametros/set_enviar_dgii.php`
Endpoint para cambiar `enviar_a_dgii` de una venta individual.

**Parámetros:** `id_venta`, `valor` (Si|No)

### 3.7 `parametros/get_ventas_ecf_hoy.php`
Endpoint para el modal de monitoreo en `caja.php`.  
Devuelve ventas e-CF del día con `enviar_a_dgii`, estadísticas Si/No.

---

## 4. ARCHIVOS MODIFICADOS

### 4.1 `pos/grabar.php`
**Después del COMMIT** (líneas 347-401):

1. **Determina `enviar_a_dgii`** según `parametros.metodo_envio_ecf`:
   - `Todo` → siempre `'Si'`
   - `Parcial`:
     - E32 Consumo + Tarjeta/Transferencia → `'Si'` (obligatorio)
     - E32 Consumo + resto → rotación **3 Si / 3 No** basada en módulo 6 del total de E32 del día
     - Todos los demás tipos (E31, E33, E34, E41...) → `'Si'`

2. **Llama a `ecf_offline_procesar_venta()`** en lugar de `ecf_enviar_venta()`:
   - Genera todo localmente sin esperar GAE
   - La factura se cierra instantáneamente

### 4.2 `ecf/ecf_enviar.php`
- Detecta si la venta tiene datos offline (`_offline: true` en `ecf_respuesta`)
- Sobrescribe datos locales con los reales de GAE al enviar exitosamente
- Logs diferenciados: `ENVIO_EXITOSO` vs `ENVIO_EXITOSO_DESDE_OFFLINE`

### 4.3 `ecf/ecf_procesar_cola.php`
- Detecta payloads con `xml_local` (offline) y los envía vía `ecf_offline_enviar_a_gae()`
- Filtro de seguridad: salta items donde `enviar_a_dgii = 'No'` (marca como `SALTADO`)
- Payloads normales: flujo JSON estándar a GAE
- Auto-verificación portal DGII tras envío exitoso

### 4.4 `parametros/caja.php`
- **Nuevo select** `metodo_envio_ecf` en panel Facturación (Todo/Parcial)
- Carga/guarda vía `get_puntos_config.php` y `update.php`
- **Botón "Ver ventas e-CF de hoy"** → abre modal con tabla de ventas del día
- **Columna "Cambiar"** en modal: select Si/No para editar `enviar_a_dgii` individual
- Filas coloreadas: verde=Si, rojo=No

### 4.5 `parametros/get_puntos_config.php`
- Agregado campo `metodo_envio_ecf` a la consulta SELECT

### 4.6 `pos/ecf_cola_admin.php`
- **Columna Error** → enlace `[logs]` que abre consola de logs
- **Modal `#modalLogsEcf`** — consola estilo terminal (fondo negro, texto verde)
  - Muestra logs de archivos (`ecf_cron.log`, `ecf_portal.log`, etc.) + `ecf_log` de BD
  - Selector de archivos, auto-refresh cada 10s, auto-scroll
  - Colores por tipo: rojo=error, verde=éxito, ámbar=pendiente, azul=info

### 4.7 Tickets (8 archivos)
Todos actualizados para mostrar QR y código de seguridad en estados `NO_ENVIADO`, `PENDIENTE`, `ENVIADO`, `ACEPTADO`:

| Archivo | Tipo |
|---------|------|
| `pos/ticket_factura.php` | Térmico 80mm |
| `pos/ticket_factura_carta.php` | Carta |
| `pos/ticket_pdf.php` | PDF térmico |
| `pos/ticket_nota_debito.php` | Nota de débito |
| `pos/ticket_preview.php` | Preview AJAX |
| `pos/recibo_egreso.php` | Recibo egreso |
| `compras/ticket_compra.php` | Compra carta |
| `compras/ticket_compra_80mm.php` | Compra térmico |

**Bloque e-CF en tickets:**
- QR generado con `qrcode.min.js` (cliente) o `api.qrserver.com` (PDF)
- Muestra: "e-CF Validado por DGII", código de seguridad, fecha firma digital
- Sección visible siempre que exista `codigo_seguridad_dgii`

---

## 5. CONFIGURACIÓN GAE DIGITAL

**Archivo:** `ecf/config_gae.php`

```php
define('GAE_API_URL', 'https://fe.gaedigital.com:8081/SignatureServices/api/Invoice');  // Precertificación
// define('GAE_API_URL', 'https://fe.gaedigital.com/SignatureServices/api/Invoice');    // Producción
define('GAE_API_KEY', '...');
define('GAE_WEBHOOK_SECRET', 'SANTANASOFT_WEBHOOK_2026$');
define('ECF_RNC_EMISOR', '133432897');
define('ECF_SELLER_CODE', '001');
define('ECF_MAX_REINTENTOS', 5);
define('ECF_REINTENTOS_INTERVALOS', '5,10,20,30,40');  // minutos
```

---

## 6. CRON — ENVÍO EN SEGUNDO PLANO

```bash
* * * * * php /var/www/html/SSB3/ecf/ecf_procesar_cola.php >> /var/www/html/SSB3/logs/ecf_cron.log 2>&1
```

**Frecuencia:** cada 1 minuto  
**Lote:** hasta 10 items por ejecución  
**Reintentos:** 5 intentos, intervalos: 5, 10, 20, 30, 40 minutos  
**Timeout total:** ~106 minutos antes de marcar como FALLIDO

---

## 7. TABLA DE ESTADOS

| Estado | Significado |
|--------|-------------|
| `NO_ENVIADO` | Datos locales generados, pendiente de encolar o `enviar_a_dgii=No` |
| `PENDIENTE` | Encolado, esperando reintento |
| `ENVIADO` | GAE aceptó el envío, pendiente confirmación DGII |
| `ACEPTADO` | DGII confirmó (verificado vía portal) |
| `RECHAZADO` | DGII rechazó (error de validación) |
| `FALLIDO` | Máximo de reintentos alcanzado sin éxito |

---

## 8. LOGS DEL SISTEMA

| Archivo | Contenido |
|---------|-----------|
| `logs/ecf_cron.log` | Salida del procesador de cola |
| `logs/ecf_portal.log` | Verificaciones de estado vía portal DGII |
| `logs/ecf_estado.log` | Consultas de estado a GAE |
| `logs/ecf_generator.log` | Generación de NCF/ECF |
| `logs/ecf_offline_cron.log` | Procesamiento de cola offline |

**Tabla BD:** `ecf_log` — historial por venta (acciones, estados, detalles)

---

## 9. CHECKLIST DE MIGRACIÓN PARA OTROS PROYECTOS

### Requisitos previos
- [ ] PHP con extensión `mysql` o `mysqli`
- [ ] MySQL/MariaDB
- [ ] cURL habilitado en PHP
- [ ] jQuery + Bootstrap 3
- [ ] `qrcode.min.js` en `/js/`
- [ ] Credenciales GAE Digital (`GAE_API_KEY`, `ECF_RNC_EMISOR`)
- [ ] Tablas `ventas`, `parametros`, `ncf`, `ecf_secuencias`, `ecf_emitidos`

### Paso a paso
1. **Ejecutar migración SQL** (sección 2.4)
2. **Copiar 4 archivos nuevos** a `ecf/`:
   - `ecf_xml_builder.php`
   - `ecf_seguridad_local.php`
   - `ecf_qr_local.php`
   - `ecf_offline.php`
3. **Copiar archivos de soporte** a `ecf/` y `parametros/`:
   - `ecf/get_ecf_logs.php`
   - `parametros/set_enviar_dgii.php`
   - `parametros/get_ventas_ecf_hoy.php`
4. **Modificar `pos/grabar.php`**:
   - Agregar lógica `enviar_a_dgii` después del COMMIT
   - Reemplazar `ecf_enviar_venta()` por `ecf_offline_procesar_venta()`
5. **Modificar `ecf/ecf_enviar.php`**:
   - Agregar detección de datos offline (`_offline: true`)
6. **Modificar `ecf/ecf_procesar_cola.php`**:
   - Agregar rama offline + filtro `enviar_a_dgii`
7. **Modificar `parametros/caja.php`**:
   - Agregar select `metodo_envio_ecf`
   - Agregar botón + modal "Ver ventas e-CF de hoy"
8. **Modificar `pos/ecf_cola_admin.php`**:
   - Agregar modal consola de logs
   - Agregar enlace `[logs]` en columna Error
   - Agregar delegación de eventos jQuery
9. **Actualizar tickets** (todos los que muestren QR e-CF):
   - Cambiar condición de `array('ACEPTADO','ENVIADO')` a `array('NO_ENVIADO','PENDIENTE','ENVIADO','ACEPTADO')`
10. **Configurar CRON**
11. **Verificar**:
    - `parametros.metodo_envio_ecf` = `'Todo'` o `'Parcial'` según necesidad
    - `ventas.enviar_a_dgii` se setea correctamente
    - Los tickets muestran QR y código de seguridad
    - El CRON procesa la cola y actualiza `ecf_respuesta`

---

## 10. SOLUCIÓN DE PROBLEMAS COMUNES

| Problema | Causa probable | Solución |
|----------|---------------|----------|
| Todas las ventas E32 marcadas como `No` | `metodo_envio_ecf` roto por conteo histórico | Verificar `pos/grabar.php` usa módulo 6, no conteo Si/No |
| Modal logs muestra "Sin registros" | `venta=ID` filtra todo en archivos `.log` | Verificar `get_ecf_logs.php` no filtra archivos por venta |
| El select `metodo_envio_ecf` no se autoselecciona | `$.get()` + `JSON.parse()` en jQuery 1.x | Usar `$.getJSON()` directamente |
| QR no aparece en ticket | `estado_dgii` no está en el array de estados visibles | Verificar incluye `NO_ENVIADO` y `PENDIENTE` |
| CRON no procesa | No configurado o ruta incorrecta | `crontab -l` para verificar |
