Appearance
Flujo Completo de Facturación Electrónica
Módulo: Ventas Tipo: Process Estado: Implementado Fecha: 2026-03-05
Descripción
Diagrama de flujo del proceso de facturación electrónica tal como está implementado hoy en ComprobanteService, FacturaService y NotaCreditoService. Muestra todas las ramas reales: modo prueba, modo oficial, condición de venta, registro en ARCA, efectos laterales (stock, tesorería, cuenta corriente) y manejo de errores.
Flujo Completo
mermaid
flowchart TD
%% -----------------------------------------------
%% ENTRADA
%% -----------------------------------------------
START([Request HTTP<br/>comprobanteController.insert])
START --> MAKE_REQUEST["ComprobanteRequest::make(data)<br/>Validate (true)"]
MAKE_REQUEST --> MAKE_SERVICE["ComprobanteServiceFactory::make(codigo)<br/>Instancia FacturaService /<br/>NotaCreditoService / NotaDebitoService"]
MAKE_SERVICE --> SET_PRUEBA{request.prueba?}
SET_PRUEBA -->|SI| PRUEBA_ON["setPrueba()<br/>prueba = true"]
SET_PRUEBA -->|NO| FETCH_DATA
PRUEBA_ON --> FETCH_DATA
%% -----------------------------------------------
%% DATOS DE CONTEXTO
%% -----------------------------------------------
FETCH_DATA["Consulta contexto:<br/>• empresa (Empres)<br/>• cliente (Cliente)<br/>• tipoComprobante (TipoComprobante::getById)<br/>• turno (Turno::getTurno)"]
FETCH_DATA --> HAS_ASOC{"¿Tiene comprobante<br/>asociado?"}
HAS_ASOC -->|SI| ASOC_VALID["Valida que el comprobante<br/>asociado exista en BD"]
ASOC_VALID --> ASOC_ERR{"¿Existe?"}
ASOC_ERR -->|NO| ERR_ASOC["BadRequest:<br/>No se encontró el<br/>comprobante asociado"]
ASOC_ERR -->|SI| CALC_DOMAIN
HAS_ASOC -->|NO| CALC_DOMAIN
%% -----------------------------------------------
%% CÁLCULO DE DOMINIO
%% -----------------------------------------------
CALC_DOMAIN["ComprobanteDomain::calculate()<br/>• Procesa ítems (precio × cantidad)<br/>• Aplica bonificaciones por ítem<br/>• Calcula IVA / Impuesto Interno<br/>• Aplica recargo condición de venta<br/>• Totaliza: netoGravado, netoNoGravado,<br/> excento, ivaTotal, impInterno, total"]
CALC_DOMAIN --> MAP_COMP["mapComprobante()<br/>Construye ComprobanteDTO<br/>con datos calculados"]
%% -----------------------------------------------
%% VALIDACIÓN CTA CTE
%% -----------------------------------------------
MAP_COMP --> COND_VENTA{condicionVenta<br/>= CTA_CTE?}
COND_VENTA -->|SI| VAL_CTACTE["validateCtaCte()<br/>• Cliente tiene cuenta corriente habilitada<br/>• Total + saldo actual < margen de crédito<br/>• Facturas impagas < límite configurado"]
VAL_CTACTE --> CTACTE_OK{"¿Valida?"}
CTACTE_OK -->|NO| ERR_CTACTE["BadRequest:<br/>• Sin habilitación CtaCte, o<br/>• Límite de crédito excedido, o<br/>• Max. comprobantes impagos"]
CTACTE_OK -->|SI| ARCA_BRANCH
COND_VENTA -->|NO| ARCA_BRANCH
%% -----------------------------------------------
%% BRANCH ARCA / PRUEBA
%% -----------------------------------------------
ARCA_BRANCH{!prueba AND<br/>registraARCA?}
%% MODO OFICIAL → ARCA real
ARCA_BRANCH -->|"SI — modo oficial"| ARCA_LAST["ArcaClient::forCompany(cuit)<br/>wsfe::getLastVoucher(sucursal, codigo)<br/>nrocomp = último + 1"]
ARCA_LAST --> ARCA_FORMAT["formatToArcaRequest(comp)<br/>• PtoVta, CbteTipo, CbteDesde/Hasta<br/>• DocTipo, DocNro<br/>• Importes: ImpTotal, ImpNeto, ImpIVA, ImpTrib<br/>• Alícuotas IVA por ítem<br/>• Tributos internos por ítem<br/>• CondicionIVAReceptorId"]
ARCA_FORMAT --> ARCA_VALIDATE["arcaRequest.validate(true)"]
ARCA_VALIDATE --> ARCA_CREATE["wsfe::createVoucher(request)"]
ARCA_CREATE --> ARCA_RESP{"¿Respuesta<br/>de ARCA?"}
ARCA_RESP -->|"Error de conexión o timeout"| ERR_ARCA_CONN["ArcaException:<br/>Error inesperado al<br/>procesar con ARCA"]
ARCA_RESP -->|"Rechazado por ARCA"| ERR_ARCA_REJ["Re-lanza BadRequest<br/>o ArcaException<br/>con detalle del rechazo"]
ARCA_RESP -->|"Aprobado"| ARCA_OK["comp.cae = arcaRes.CAE<br/>comp.caeVto = arcaRes.CAEFchVto<br/>comp.nrocomp = nrocomp calculado"]
ARCA_OK --> BEGIN_TX
%% MODO PRUEBA → numerador local
ARCA_BRANCH -->|"NO — prueba o sin ARCA"| IS_MANUAL{"¿Es RegistroManualRequest?"}
IS_MANUAL -->|"SI — manual"| MANUAL["comp.cae = request.CAE<br/>comp.caeVto = request.fechaVtoCAE<br/>comp.nrocomp = request.numeroComprobante<br/>comp.manual = true"]
IS_MANUAL -->|"NO — prueba"| PRUEBA_NRO["getNewNumeradorComprobante(codigo)<br/>Número siguiente desde BD local"]
MANUAL --> BEGIN_TX
PRUEBA_NRO --> BEGIN_TX
%% -----------------------------------------------
%% TRANSACCIÓN PRINCIPAL
%% -----------------------------------------------
BEGIN_TX["beginTransaction('oficial', 'principal')"]
BEGIN_TX --> INSERT_COMP["insertComprobante(comp, request)<br/>model.insert(comp)<br/>→ Guarda en tabla factura /<br/>nota_credito / nota_debito"]
INSERT_COMP --> HAS_PEND{"¿Tiene pendientes?<br/>solo en Factura"}
HAS_PEND -->|SI| PROC_PEND["processPendientes(pendientes, idFactura)<br/>Por cada pendiente:<br/>• PendienteService::patch → vincula id_factura<br/>• Si Stock activo: deleteByIdPendiente<br/> (elimina movimiento stock previo)"]
HAS_PEND -->|NO| AUDIT
PROC_PEND --> AUDIT
AUDIT["registrarAuditoria('INSERT', tipoComprobante, tabla, id)"]
AUDIT --> HAS_ITEMS{"¿Tiene items?"}
%% -----------------------------------------------
%% INSERT ITEMS + STOCK
%% -----------------------------------------------
HAS_ITEMS -->|SI| INSERT_ITEMS["ItemFacturaService::insert(items)<br/>Guarda en tabla items_subdicom"]
HAS_ITEMS -->|NO| CTACTE_REG
INSERT_ITEMS --> STOCK_MOD{"¿Modulo Stock<br/>activo?"}
STOCK_MOD -->|NO| CTACTE_REG
STOCK_MOD -->|SI| BRIDGE["BridgeGlobalVentasService<br/>::generateOrGetGlobalId(id, codigo, schema)<br/>Genera UUID global para trazabilidad"]
BRIDGE --> STOCK_LOOP["Por cada ítem con maneja_stock = S"]
STOCK_LOOP --> TIPO_COMP_STOCK{Tipo de<br/>comprobante}
TIPO_COMP_STOCK -->|"Factura"| STOCK_EGR["MovimientoStock EGRESO<br/>(descuenta stock)<br/>marca = OFICIAL o PRUEBA<br/>según conexión BD"]
TIPO_COMP_STOCK -->|"Nota de Crédito"| STOCK_ING["MovimientoStock INGRESO<br/>(restituye stock)<br/>marca = OFICIAL o PRUEBA"]
TIPO_COMP_STOCK -->|"Nota de Débito"| CTACTE_REG
STOCK_EGR --> CTACTE_REG
STOCK_ING --> CTACTE_REG
%% -----------------------------------------------
%% CUENTA CORRIENTE
%% -----------------------------------------------
CTACTE_REG{condicionVenta<br/>= CTA_CTE?}
CTACTE_REG -->|NO| MOV_CAJA_CHECK
CTACTE_REG -->|SI| INSERT_CTACTE
subgraph INSERT_CTACTE["Registro en Cuenta Corriente"]
direction TB
CC1{Tipo de<br/>comprobante}
CC2["insertMovimientoCtaCte DEBE<br/>ordcta.debe = total"]
CC3["insertMovimientoCtaCte HABER<br/>ordcta.haber = total"]
CC4{"¿NC con comprobante<br/>asociado?"}
CC5["getMovimiento(nrocomp, tipo)<br/>Actualiza saldo y fecha pago<br/>de la factura original"]
CC1 -->|"Factura / ND"| CC2
CC1 -->|"Nota de Crédito"| CC3 --> CC4
CC4 -->|"SI"| CC5
CC4 -->|"NO"| CC_END((ok))
CC2 --> CC_END
CC5 --> CC_END
end
INSERT_CTACTE --> MOV_CAJA_CHECK
%% -----------------------------------------------
%% MOVIMIENTO DE CAJA (TESORERÍA)
%% -----------------------------------------------
MOV_CAJA_CHECK{registraMovimientoCaja<br/>= true?}
MOV_CAJA_CHECK -->|NO| COMMIT
MOV_CAJA_CHECK -->|SI| COND_CAJA{condicionVenta<br/>= CTA_CTE?}
COND_CAJA -->|SI — no registra caja| COMMIT
COND_CAJA -->|NO| TES_MOD{"¿Modulo Tesoreria<br/>activo?"}
TES_MOD -->|NO| COMMIT
TES_MOD -->|SI| CAJA_CUENTA["CuentaAsignadaService<br/>::getOne(VENTAS_CONTADO)<br/>Obtiene cuenta contable"]
CAJA_CUENTA --> INSERT_MOV_CAJA
subgraph INSERT_MOV_CAJA["Registro Movimiento de Caja"]
direction TB
MC1{Tipo de<br/>comprobante}
MC2["MovimientoCaja ENTRADA<br/>cobro contado de factura"]
MC3["MovimientoCaja SALIDA<br/>devolución NC o ND"]
MC4{condicionVenta<br/>= TARJETA?}
MC5["2do MovimientoCaja tipo opuesto<br/>movimiento bancario tarjeta<br/>cuenta = tarjeta.cuenta"]
MC1 -->|"Factura"| MC2
MC1 -->|"NC / ND"| MC3
MC2 --> MC4
MC3 --> MC4
MC4 -->|"SI"| MC5
MC4 -->|"NO"| MC_END((ok))
MC5 --> MC_END
end
INSERT_MOV_CAJA --> COMMIT
%% -----------------------------------------------
%% COMMIT / ROLLBACK
%% -----------------------------------------------
COMMIT["connections.commit()"]
COMMIT --> SUCCESS
SUCCESS["Retorna:<br/>{ nrocomp, codigo, reporte }"]
SUCCESS --> END_OK([Comprobante registrado])
%% -----------------------------------------------
%% MANEJO DE ERRORES EN TRANSACCIÓN
%% -----------------------------------------------
BEGIN_TX --> ON_EX["Exception en<br/>transacción"]
ON_EX --> ROLLBACK["connections.rollback()"]
ROLLBACK --> WAS_ARCA{"¿Modo oficial?<br/>ARCA ya registro"}
WAS_ARCA -->|"SI — ARCA aprobó<br/>pero BD falló"| ERR_ARCA_PARTIAL["ErrorHandler::logToDatabase(e)<br/>Retorna error descriptivo<br/>+ datos del comp (para recuperación manual)"]
WAS_ARCA -->|"NO — prueba<br/>o sin ARCA"| RETHROW["Re-lanza excepción"]
%% -----------------------------------------------
%% ESTILOS
%% -----------------------------------------------
style START fill:#e8f4fd,stroke:#2196f3
style END_OK fill:#e8f5e9,stroke:#4caf50
style SUCCESS fill:#e8f5e9,stroke:#4caf50
style ERR_ASOC fill:#fdecea,stroke:#f44336,color:#b71c1c
style ERR_CTACTE fill:#fdecea,stroke:#f44336,color:#b71c1c
style ERR_ARCA_CONN fill:#fdecea,stroke:#f44336,color:#b71c1c
style ERR_ARCA_REJ fill:#fdecea,stroke:#f44336,color:#b71c1c
style ERR_ARCA_PARTIAL fill:#fff3e0,stroke:#ff9800,color:#e65100
style RETHROW fill:#fdecea,stroke:#f44336,color:#b71c1c
style ARCA_OK fill:#e8f5e9,stroke:#4caf50
style ARCA_CREATE fill:#fff8e1,stroke:#ffc107
style ARCA_FORMAT fill:#fff8e1,stroke:#ffc107
style ARCA_LAST fill:#fff8e1,stroke:#ffc107
style PRUEBA_ON fill:#f3e5f5,stroke:#9c27b0
style PRUEBA_NRO fill:#f3e5f5,stroke:#9c27b0
style BEGIN_TX fill:#e3f2fd,stroke:#1565c0
style COMMIT fill:#e3f2fd,stroke:#1565c0
style ROLLBACK fill:#fdecea,stroke:#f44336Resumen de efectos laterales por tipo de comprobante
| Efecto | Factura | Nota de Crédito | Nota de Débito |
|---|---|---|---|
| Inserta comprobante | ✅ | ✅ | ✅ |
| Inserta ítems | ✅ (si tiene) | ✅ (si tiene) | ✅ (si tiene) |
| Stock (si módulo activo) | EGRESO por ítem | INGRESO por ítem | — |
| CtaCte (si condición = CTA_CTE) | DEBE | HABER + cancela factura | DEBE |
| Tesorería (si módulo activo y no CTA_CTE) | ENTRADA | SALIDA | SALIDA |
| 2do movimiento caja (TARJETA) | ✅ (débito bancario) | ✅ | — |
| Procesa pendientes | ✅ (si tiene) | — | — |
| Registro en ARCA | ✅ (modo oficial) | ✅ (modo oficial) | ✅ (modo oficial) |
| Auditoría | ✅ | ✅ | ✅ |
Comportamiento del error post-ARCA
Cuando ARCA aprueba el comprobante pero la transacción de BD falla, el sistema no puede revertir el registro en ARCA (es externo). El flujo real:
- Hace rollback de la BD
- Llama a
ErrorHandler::logToDatabase($e)para dejar trazabilidad - Retorna un objeto de error con los datos del comprobante (para recuperación manual)
- El comprobante queda aprobado en ARCA pero sin registro en BD → requiere intervención manual con registro manual (RegistroManualRequest)
Archivos de código relevantes
| Clase | Ruta | Rol |
|---|---|---|
ComprobanteService | service/Venta/Facturacion/ComprobanteService.php | Base del flujo de inserción |
FacturaService | service/Venta/Facturacion/FacturaService.php | Stock EGRESO, pendientes, mov caja ENTRADA |
NotaCreditoService | service/Venta/Facturacion/NotaCreditoService.php | Stock INGRESO, mov caja SALIDA, cancela CtaCte |
ComprobanteController | controller/modulo-venta/Facturacion/ComprobanteController.php | Entry point HTTP |
ArcaClient | Api/Arca/ArcaClient.php | Cliente WSFE de ARCA/AFIP |
MovimientoStockService | service/Stock/MovimientoStockService.php | Registro de stock |
MovimientoCajaServiceFactory | Factories/Tesoreria/MovimientoCajaServiceFactory.php | Registro en caja |
CtaCteControllerFactory | Factories/CtaCte/CtaCteControllerFactory.php | Registro en cuenta corriente |
Documentos relacionados
- Configuración flexible de tipos de comprobante — proceso de determinación de letra y código ARCA (planificado)
- ABM de tipos de comprobante
- Implementación técnica normalización de comprobantes
- Consolidación de informes de ventas