Appearance
Flujo Completo de Facturación en Lote (Membresías)
Módulo: Membresías Tipo: Process Estado: Implementado Fecha: 2026-03-09
Descripción
Diagrama de flujo del proceso de facturación masiva de membresías tal como está implementado en BatchInvoicingOrchestrator y sus cuatro servicios especializados. Muestra todas las ramas reales: modo simulación, modo prueba, modo oficial, refacturación, agrupación familiar, enriquecimiento, validación de deuda, envío a ARCA en lote, registro en BD y manejo del error GAP-C (ARCA aprobó pero BD falló).
Flujo Completo
mermaid
flowchart TD
%% -----------------------------------------------
%% ENTRADA
%% -----------------------------------------------
START([Request HTTP<br/>BatchInvoicingController])
START --> VALIDATOR["BatchInvoicingValidator<br/>• rangoSocios: array de 2 enteros<br/>• periodo: formato Y-m<br/>• refacturacion: bool opcional<br/>• simulacion: bool opcional"]
VALIDATOR --> AUDIT_START["BatchInvoicingAuditService<br/>::registrarInicio(request)<br/>→ auditoriaId"]
%% -----------------------------------------------
%% PREPARACIÓN DE CONTEXTO
%% -----------------------------------------------
AUDIT_START --> REFACT{request<br/>refacturacion?}
REFACT -->|SI| REFACT_PROC["RefacturacionService<br/>::procesarRefacturacionYObtenerSocios()<br/>• MiembrosService::getAll()<br/>• getFacturadosEnPeriodoPorModo()<br/>• obtenerSociosConFacturasSinNC()"]
REFACT_PROC --> REFACT_PAGOS["validarPagosAntesDeRefacturar()<br/>• MembresiaFacturacionConFacturaQuery<br/>• CuentaCorriente::validarPagosBulk()"]
REFACT_PAGOS --> REFACT_SIM{simulacion?}
REFACT_SIM -->|SI| REFACT_OK["Solo valida AFIP<br/>no anula ni refactura"]
REFACT_SIM -->|NO| ANULAR["anularYRefacturar()<br/>• enrichFacturasWithItems()<br/> Factura + ItemFactura bulk<br/>• agruparPorCodigoComprobante()"]
ANULAR --> REG_NC["BatchNotaCreditoRegistrationService<br/>::registrarLote()<br/>Abre su propia transacción"]
REG_NC --> NC_PRUEBA{isPruebaConnection?}
NC_PRUEBA -->|SI| NC_LOCAL["getNewNumeradorComprobante()<br/>Nota Crédito sintética (CAE=null)"]
NC_PRUEBA -->|NO| NC_ARCA["BatchNotaCreditoBuilder<br/>WSFEService::createBatchVoucher()<br/>→ NC en ARCA"]
NC_LOCAL --> NC_BULK
NC_ARCA --> NC_BULK
NC_BULK["bulkInsert: NotaCredito<br/>bulkInsert: ItemNotaCredito<br/>bulkInsert: MovimientoCuentaCorriente<br/>actualizarRegistrosConNC()"]
NC_BULK --> REFACT_OK
REFACT -->|NO| EXCLUIDOS["MembresiaFacturacion<br/>::getAllFacturadosEnPeriodo()<br/>Obtiene excluidos del periodo"]
EXCLUIDOS --> REFACT_OK
%% Carga de contexto compartido
REFACT_OK --> LOAD_CTX["Carga contexto:<br/>• Empres::getAllFields()<br/>• Turno::getTurno()<br/>• CategoriaIva::getAll()<br/>• CondicionIva::getAll()<br/>→ FacturacionContexto"]
%% -----------------------------------------------
%% OBTENCIÓN DE MIEMBROS
%% -----------------------------------------------
LOAD_CTX --> GET_MEMBERS{refacturacion?}
GET_MEMBERS -->|SI| MEMBERS_IDS["MiembrosService::getByIds()<br/>Solo socios a refacturar"]
GET_MEMBERS -->|NO| MEMBERS_ACTIVE["obtenerMiembrosActivosConValidacion()<br/>• MembresiaFacturacionConFacturaQuery<br/>• CuentaCorriente::validarPagosBulk()<br/>• MiembrosService::getAll()"]
MEMBERS_IDS --> EMPTY_CHECK
MEMBERS_ACTIVE --> EMPTY_CHECK
EMPTY_CHECK{"¿Sin miembros?"}
EMPTY_CHECK -->|SI| EMPTY_RESP["buildEmptyResponse()<br/>registrarFinalizacion()"]
EMPTY_RESP --> END_EMPTY([Lote vacío — sin comprobantes])
EMPTY_CHECK -->|NO| BUILD_GROUPS
%% -----------------------------------------------
%% CONSTRUCCIÓN Y ENRIQUECIMIENTO DE GRUPOS
%% -----------------------------------------------
BUILD_GROUPS["GrupoFacturableService<br/>::buildGruposFacturables()<br/>• TipoRelacionService::getAll()<br/>• GrupoFamiliarService::agruparMiembrosPorGrupo()<br/>• RealGroupProvider: grupos con titular<br/>• SymbolicGroupProvider: socios sin grupo"]
BUILD_GROUPS --> ENRICH["GrupoFacturableService<br/>::enrichGruposConRelaciones()"]
subgraph ENRICH_DETAIL["Enriquecimiento en lote — 6 queries totales"]
direction LR
E1["CategoriaMembresiaEnricher<br/>• RelOrdconCategoria<br/> getBatchByOrdcons() — 1 query<br/>• CategoriaMembresiaService<br/> getByIds() — 1 query"]
E2["DisciplinaEnricher<br/>• Empres.disciplina flag<br/>• RelOrdconDisciplina<br/> getByOrdcons() — 1 query<br/>• DisciplinaService<br/> getByIds() — 1 query"]
E3["ProductoEnricher<br/>• RelOrdconProducto<br/> getByOrdcons() — 1 query<br/>• ProductoService<br/> getByIds() — 1 query"]
end
ENRICH --> ENRICH_DETAIL
%% -----------------------------------------------
%% VALIDACIÓN
%% -----------------------------------------------
ENRICH_DETAIL --> VAL_PRECIOS["BatchInvoicingValidationService<br/>::validarPreciosYCondiciones()"]
VAL_PRECIOS --> VAL_PROD["DeudaMembresiaCalculator<br/>::validarPreciosProductos()<br/>por cada grupo"]
VAL_PROD --> VAL_PROD_OK{"¿Faltan precios?"}
VAL_PROD_OK -->|SI| ERR_PRECIOS["ProductosSinPrecioException<br/>Lista todos los productos<br/>sin precio configurado"]
VAL_PROD_OK -->|NO| VAL_COND
VAL_COND["FamilyGroup::validarParaFacturacion()<br/>por cada grupo"]
VAL_COND --> VAL_COND_OK{"¿Errores de<br/>condicion?"}
VAL_COND_OK -->|SI| ERR_COND["Exception:<br/>Grupos con condiciones<br/>no válidas para facturar"]
VAL_COND_OK -->|NO| CALC_DEUDA
%% -----------------------------------------------
%% CÁLCULO DE DEUDA
%% -----------------------------------------------
CALC_DEUDA["BatchInvoicingProcessingService<br/>::calcularDeuda(grupos, contexto)"]
CALC_DEUDA --> CALC_LOOP["Por cada FamilyGroup:<br/>DeudaMembresiaCalculator::calcularDeuda()<br/>• PrecioProductoRepository::findPreciosBatch()<br/> 1 query bulk por grupo<br/>• ConfiguracionEmpresaRepository<br/> getCondicionIvaEmisor()<br/>• procesarCategoriaTitular() → InvoiceItem<br/>• procesarDisciplinas() → InvoiceItem[]<br/>• procesarProductos() → InvoiceItem[]<br/>→ BatchInvoice"]
CALC_LOOP --> PRUEBA_SET{request.prueba?}
PRUEBA_SET -->|SI| SET_RESOLVER["setPruebaResolver(<br/>PruebaTipoComprobanteResolver)"]
PRUEBA_SET -->|NO| BEGIN_TX
SET_RESOLVER --> BEGIN_TX
%% -----------------------------------------------
%% TRANSACCIÓN PRINCIPAL
%% -----------------------------------------------
BEGIN_TX["ConnectionManager<br/>::beginTransaction('oficial', 'principal')"]
BEGIN_TX --> PROC_COMP["BatchInvoicingProcessingService<br/>::procesarComprobantes(invoices, simulacion, contexto)"]
PROC_COMP --> AGRUPAR["agruparPorTipoComprobante()<br/>TipoComprobanteResolver o<br/>PruebaTipoComprobanteResolver<br/>→ grupos por código ARCA"]
AGRUPAR --> SIM_BRANCH{simulacion?}
%% RAMA SIMULACIÓN
SIM_BRANCH -->|SI| PROC_SIM["procesarSimulacion()<br/>ComprobanteMapper: construye datos<br/>Sin escrituras en BD<br/>→ Detalles simulados"]
PROC_SIM --> COMMIT
%% RAMA REAL
SIM_BRANCH -->|NO| PRUEBA_BRANCH{isPrueba<br/>connection?}
%% MODO PRUEBA
PRUEBA_BRANCH -->|SI| PROC_PRUEBA["procesarPruebaYRegistro()<br/>TipoComprobanteModel<br/>::reservarRangoNumeradores()<br/>2 queries SELECT FOR UPDATE + UPDATE<br/>BatchVoucherResponse sintética<br/>CAE = null, status = 'approved'"]
PROC_PRUEBA --> REG_LOTE
%% MODO OFICIAL → ARCA real
PRUEBA_BRANCH -->|NO| ARCA_BUILD["BatchVoucherBuilder<br/>ComprobanteMapper + config<br/>Construye request ARCA en lote"]
ARCA_BUILD --> ARCA_CALL["WSFEService<br/>::createBatchVoucher(batchRequest)<br/>→ BatchVoucherResponse<br/>con CAE por comprobante"]
ARCA_CALL --> ARCA_RESP{"¿Respuesta<br/>ARCA?"}
ARCA_RESP -->|Error o timeout| ERR_ARCA_CALL["ArcaException"]
ARCA_RESP -->|Aprobado| REG_LOTE
%% -----------------------------------------------
%% REGISTRO EN BD
%% -----------------------------------------------
REG_LOTE["BatchFacturaRegistrationService<br/>::registrarLote()"]
subgraph REGISTRAR["Registro en lote — dentro de la transacción del Orchestrator"]
direction TB
R1["buildComprobantesDTOs()<br/>Separa aprobados / rechazados por ARCA<br/>→ FacturaDTO[] para aprobados<br/>condicionVenta = CTA_CTE siempre"]
R2["Factura::bulkInsert()<br/>→ comprobantesIds[]"]
R3["BridgeGlobalVentasService<br/>::generateOrGetGlobalId()<br/>por cada ID — trazabilidad cross-schema"]
R4["buildItemsDTOs() → ItemFacturaDTO[]<br/>ItemFactura::bulkInsert()"]
R5["buildPeriodosRequests()<br/>→ MembresiaFacturacionRequest[]<br/>MembresiaFacturacion::bulkInsert()"]
R6["buildMovimientosCtaCte()<br/>→ MovimientoCuentaCorriente[]<br/>CuentaCorriente::bulkInsert()"]
R7["EventDispatcher<br/>::dispatch(FacturacionLoteProcesadaEvent)"]
R1 --> R2 --> R3 --> R4 --> R5 --> R6 --> R7
end
REG_LOTE --> REGISTRAR
%% -----------------------------------------------
%% GAP-C: ARCA aprobó pero BD falló
%% -----------------------------------------------
REGISTRAR --> GAPC_CHECK{"¿Result contiene<br/>clave 'error'?"}
GAPC_CHECK -->|SI — GAP-C| ERR_GAPC["ConnectionManager::rollBack()<br/>ErrorHandler::logToDatabase()<br/>Retorna payload con datos CAE<br/>para recuperación manual"]
ERR_GAPC --> AUDIT_ERR_GAPC["BatchInvoicingAuditService<br/>::registrarError(auditoriaId)"]
AUDIT_ERR_GAPC --> END_GAPC([ARCA aprobó — BD falló<br/>Recuperación manual requerida])
GAPC_CHECK -->|NO| COMMIT
%% -----------------------------------------------
%% COMMIT Y RESPUESTA
%% -----------------------------------------------
COMMIT["ConnectionManager::commit()"]
COMMIT --> BUILD_RESP["buildResponse()<br/>Merge de todos los detalles<br/>→ BatchInvoicingResponse"]
BUILD_RESP --> AUDIT_FIN["BatchInvoicingAuditService<br/>::registrarFinalizacion()<br/>auditoriaId + response + duracion"]
AUDIT_FIN --> SUCCESS["Retorna BatchInvoicingResponse<br/>{ procesados, rechazados,<br/> errores, detalles[] }"]
SUCCESS --> END_OK([Lote procesado])
%% -----------------------------------------------
%% MANEJO DE ERRORES — THROWABLE GENERAL
%% -----------------------------------------------
BEGIN_TX --> ON_EX["Throwable en transacción"]
ON_EX --> ROLLBACK["ConnectionManager::rollBack()"]
ROLLBACK --> AUDIT_ERR["BatchInvoicingAuditService<br/>::registrarError(auditoriaId, e)"]
AUDIT_ERR --> RETHROW["Re-lanza excepción"]
%% -----------------------------------------------
%% ESTILOS
%% -----------------------------------------------
style START fill:#e8f4fd,stroke:#2196f3
style END_OK fill:#e8f5e9,stroke:#4caf50
style END_EMPTY fill:#f5f5f5,stroke:#9e9e9e
style END_GAPC fill:#fff3e0,stroke:#ff9800
style SUCCESS fill:#e8f5e9,stroke:#4caf50
style ERR_PRECIOS fill:#fdecea,stroke:#f44336,color:#b71c1c
style ERR_COND fill:#fdecea,stroke:#f44336,color:#b71c1c
style ERR_ARCA_CALL fill:#fdecea,stroke:#f44336,color:#b71c1c
style RETHROW fill:#fdecea,stroke:#f44336,color:#b71c1c
style ERR_GAPC fill:#fff3e0,stroke:#ff9800,color:#e65100
style AUDIT_ERR_GAPC fill:#fff3e0,stroke:#ff9800,color:#e65100
style ARCA_CALL fill:#fff8e1,stroke:#ffc107
style ARCA_BUILD fill:#fff8e1,stroke:#ffc107
style NC_ARCA fill:#fff8e1,stroke:#ffc107
style PRUEBA_SET fill:#f3e5f5,stroke:#9c27b0
style SET_RESOLVER fill:#f3e5f5,stroke:#9c27b0
style PROC_PRUEBA fill:#f3e5f5,stroke:#9c27b0
style NC_LOCAL fill:#f3e5f5,stroke:#9c27b0
style PROC_SIM fill:#e8eaf6,stroke:#3f51b5
style BEGIN_TX fill:#e3f2fd,stroke:#1565c0
style COMMIT fill:#e3f2fd,stroke:#1565c0
style ROLLBACK fill:#fdecea,stroke:#f44336
style ENRICH_DETAIL fill:#f9fbe7,stroke:#afb42b
style REGISTRAR fill:#e8f5e9,stroke:#388e3cDiferencias clave con facturación individual (ComprobanteService)
| Aspecto | Facturación individual | Facturación en lote |
|---|---|---|
| Entrada | Un comprobante | Rango de socios + periodo |
| Agrupación | Sin agrupación | Grupos familiares (titular + adherentes) |
| Envío ARCA | Un comprobante por request | Batch de comprobantes por tipo |
| Condición de venta | Configurable | Siempre CTA_CTE |
| Registro stock | Sí (por ítem con stock) | No aplica |
| Registro caja | Sí (si módulo activo) | No aplica |
| Refacturación | No | Sí (anula con NC + refactura) |
| Simulación | No | Sí (calcula sin escribir) |
| Auditoria de lote | No | Sí (MembresiaFacturacionLoteAuditoria) |
| Enriquecimiento | No | Sí (categoría, disciplina, producto en batch) |
Comportamiento del error GAP-C
El mismo patrón que en la facturación individual pero en contexto de lote:
- ARCA aprueba el lote (CAE asignado por comprobante)
- Falla alguna escritura en BD durante
registrarLote() procesarEnvioArcaYRegistro()captura la excepciónConnectionManager::rollBack()— deshace toda la transacción del loteErrorHandler::logToDatabase(e)— trazabilidad- Retorna
['error' => ..., 'data' => $caePayload]— los CAE están en el payload - El Orchestrator detecta la clave
'error', registra auditoría y retorna el payload - Los comprobantes están aprobados en ARCA sin registro en BD → recuperación manual requerida
Archivos de código relevantes
| Clase | Ruta | Rol |
|---|---|---|
BatchInvoicingOrchestrator | Modules/Membresia/Application/Services/Facturacion/BatchInvoicingOrchestrator.php | Entry point, coordinación de fases |
BatchInvoicingPreparationService | Modules/Membresia/Application/Services/Facturacion/BatchInvoicingPreparationService.php | Contexto, miembros, grupos |
BatchInvoicingValidationService | Modules/Membresia/Application/Services/Facturacion/BatchInvoicingValidationService.php | Precios y condiciones |
BatchInvoicingProcessingService | Modules/Membresia/Application/Services/Facturacion/BatchInvoicingProcessingService.php | Deuda, envío ARCA, registro |
BatchInvoicingAuditService | Modules/Membresia/Application/Services/Facturacion/BatchInvoicingAuditService.php | Auditoría de lote |
BatchFacturaRegistrationService | Modules/Membresia/Application/Services/Facturacion/BatchFacturaRegistrationService.php | Registro en BD (facturas) |
BatchComprobanteRegistrationService | Modules/Membresia/Application/Services/Facturacion/BatchComprobanteRegistrationService.php | Base abstracta de registro |
BatchNotaCreditoRegistrationService | Modules/Membresia/Application/Services/Facturacion/BatchNotaCreditoRegistrationService.php | Registro NC (refacturación) |
RefacturacionService | Modules/Membresia/Application/Services/Facturacion/RefacturacionService.php | Anulación + refacturación |
GrupoFacturableService | Modules/Membresia/Application/Services/Facturacion/GrupoFacturableService.php | Construcción de grupos familiares |
DeudaMembresiaCalculator | Modules/Membresia/Domain/Facturacion/Services/DeudaMembresiaCalculator.php | Cálculo de deuda por grupo |
WSFEService | (compartido) | Cliente AFIP/ARCA en lote |