Skip to content

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:#388e3c

Diferencias clave con facturación individual (ComprobanteService)

AspectoFacturación individualFacturación en lote
EntradaUn comprobanteRango de socios + periodo
AgrupaciónSin agrupaciónGrupos familiares (titular + adherentes)
Envío ARCAUn comprobante por requestBatch de comprobantes por tipo
Condición de ventaConfigurableSiempre CTA_CTE
Registro stockSí (por ítem con stock)No aplica
Registro cajaSí (si módulo activo)No aplica
RefacturaciónNoSí (anula con NC + refactura)
SimulaciónNoSí (calcula sin escribir)
Auditoria de loteNoSí (MembresiaFacturacionLoteAuditoria)
EnriquecimientoNoSí (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:

  1. ARCA aprueba el lote (CAE asignado por comprobante)
  2. Falla alguna escritura en BD durante registrarLote()
  3. procesarEnvioArcaYRegistro() captura la excepción
  4. ConnectionManager::rollBack() — deshace toda la transacción del lote
  5. ErrorHandler::logToDatabase(e) — trazabilidad
  6. Retorna ['error' => ..., 'data' => $caePayload] — los CAE están en el payload
  7. El Orchestrator detecta la clave 'error', registra auditoría y retorna el payload
  8. Los comprobantes están aprobados en ARCA sin registro en BD → recuperación manual requerida

Archivos de código relevantes

ClaseRutaRol
BatchInvoicingOrchestratorModules/Membresia/Application/Services/Facturacion/BatchInvoicingOrchestrator.phpEntry point, coordinación de fases
BatchInvoicingPreparationServiceModules/Membresia/Application/Services/Facturacion/BatchInvoicingPreparationService.phpContexto, miembros, grupos
BatchInvoicingValidationServiceModules/Membresia/Application/Services/Facturacion/BatchInvoicingValidationService.phpPrecios y condiciones
BatchInvoicingProcessingServiceModules/Membresia/Application/Services/Facturacion/BatchInvoicingProcessingService.phpDeuda, envío ARCA, registro
BatchInvoicingAuditServiceModules/Membresia/Application/Services/Facturacion/BatchInvoicingAuditService.phpAuditoría de lote
BatchFacturaRegistrationServiceModules/Membresia/Application/Services/Facturacion/BatchFacturaRegistrationService.phpRegistro en BD (facturas)
BatchComprobanteRegistrationServiceModules/Membresia/Application/Services/Facturacion/BatchComprobanteRegistrationService.phpBase abstracta de registro
BatchNotaCreditoRegistrationServiceModules/Membresia/Application/Services/Facturacion/BatchNotaCreditoRegistrationService.phpRegistro NC (refacturación)
RefacturacionServiceModules/Membresia/Application/Services/Facturacion/RefacturacionService.phpAnulación + refacturación
GrupoFacturableServiceModules/Membresia/Application/Services/Facturacion/GrupoFacturableService.phpConstrucción de grupos familiares
DeudaMembresiaCalculatorModules/Membresia/Domain/Facturacion/Services/DeudaMembresiaCalculator.phpCálculo de deuda por grupo
WSFEService(compartido)Cliente AFIP/ARCA en lote

Documentos relacionados