Appearance
membresia_facturacion — Esquema Técnico Backend
Módulo: Membresias Feature: Tabla de registro de facturación por período Fecha: 2026-03-05
IMPORTANTE - Política de Código en Documentación Técnica:
Este documento contiene DECISIONES TÉCNICAS sobre el esquema de base de datos, NO código de implementación detallado.
Enfoque: Documentar QUÉ construir y POR QUÉ (decisiones arquitectónicas).
Documento de Negocio Relacionado
Requisitos de Negocio: Facturación por Lotes — Process
Índice de Facturación: Facturación por Lotes — Índice (ver RN-009, RN-010, RN-011)
Decisión Arquitectónica Central
ADR: Eliminación de constraints UNIQUE en membresia_facturacion
Decisión: La tabla membresia_facturacion no tiene constraints UNIQUE sobre ninguna combinación de columnas.
Fecha de migración: 20260305000002_drop_membresia_facturacion_unique_constraints
Contexto: La tabla originalmente tenía un UNIQUE sobre (ordcon_id, anio, mes) (creado en 20251110105836), luego modificado a (ordcon_id, anio, mes, modo_prueba) (20260305000001), y finalmente eliminado por completo (20260305000002).
Razón: Un socio puede ser refacturado múltiples veces en el mismo período:
- Facturación inicial en modo prueba + refacturación en modo oficial (cross-mode)
- Múltiples refacturaciones sucesivas por corrección de errores
- Un UNIQUE constraint en la capa de DB impedía estas operaciones válidas de negocio
Consecuencia: La deduplicación y determinación del estado activo de una factura es responsabilidad exclusiva de la lógica de negocio, no de la base de datos.
Flujo de Datos
Flujo de registro en membresia_facturacion
La tabla actúa como un log inmutable de cada operación de facturación:
- Facturación inicial: INSERT con
(ordcon_id, anio, mes, factura_id=F1, nota_credito_id=NULL) - Primera refacturación: UPDATE registro previo:
nota_credito_id=NC1. INSERT nuevo:(ordcon_id, anio, mes, factura_id=F2, nota_credito_id=NULL) - Segunda refacturación: UPDATE registro previo:
nota_credito_id=NC2. INSERT nuevo:(ordcon_id, anio, mes, factura_id=F3, nota_credito_id=NULL)
Invariante del modelo: El registro activo (factura vigente) para un período dado es siempre el que tiene nota_credito_id IS NULL. Solo puede existir un registro activo por (ordcon_id, anio, mes, modo_prueba) en condiciones normales; la garantía la provee la lógica de negocio, no la base de datos.
Esquema de Base de Datos
Tabla: membresia_facturacion
Nivel Multi-tenant: EMPRESA y SUCURSAL
Propagación de schema: X-Schema header → ConnectionManager → search_path PostgreSQL
Importante sobre multi-modo: La tabla reside siempre en la base de datos oficial (sin sufijo _p). El campo modo_prueba es la columna que indica el origen de cada registro (modo prueba vs oficial). Esto permite consultar el historial cross-mode desde una sola fuente de verdad.
| Campo | Tipo | Constraints | Descripción |
|---|---|---|---|
id | SERIAL | PRIMARY KEY | Identificador único autogenerado |
ordcon_id | INTEGER | NOT NULL | ID del socio/miembro (FK a ordcon) |
anio | SMALLINT | NOT NULL | Año del período facturado |
mes | SMALLINT | NOT NULL | Mes del período facturado (1-12) |
factura_id | INTEGER | NOT NULL | ID de la factura generada en el módulo de Ventas (registro inmutable) |
nota_credito_id | INTEGER | NULL | ID de la NC que anuló esta factura. NULL = factura activa. NOT NULL = factura anulada |
modo_prueba | SMALLINT o BOOLEAN | NOT NULL | Modo en que fue emitida la factura (0 = oficial, 1 = prueba) |
fecha | TIMESTAMP | NOT NULL DEFAULT NOW() | Fecha de registro (auto-generada) |
Nota sobre factura_id: El campo es inmutable una vez insertado. Representa el comprobante emitido en ese momento de facturación. No se actualiza en refacturaciones; en cambio, se inserta un nuevo registro.
Nota sobre nota_credito_id: Es el único campo que se actualiza post-inserción. El UPDATE ocurre durante la Fase 1 del proceso de refacturación para marcar la factura anterior como anulada.
Indexes:
| Nombre | Columnas | Tipo | Propósito |
|---|---|---|---|
idx_membresia_facturacion_orcond | ordcon_id | BTREE | Búsquedas por socio |
idx_membresia_facturacion_periodo | anio, mes | BTREE | Búsquedas por período |
Sin constraints UNIQUE: Esta es una decisión deliberada. Ver ADR arriba.
Foreign Keys:
| Constraint | Columna Local | Referencia | ON DELETE | ON UPDATE |
|---|---|---|---|---|
| (ninguna declarada explícitamente) | ordcon_id | ordcon(id) | — | — |
Nota: No hay FK formal declarada a nivel de DB para factura_id ni nota_credito_id porque estos IDs pertenecen a tablas del módulo de Ventas que pueden estar en esquemas distintos según el nivel multi-tenant.
Tabla relacionada: membresia_facturacion_lote_auditoria
Nivel Multi-tenant: EMPRESA y SUCURSAL
| Campo | Tipo | Constraints | Descripción |
|---|---|---|---|
id | SERIAL | PRIMARY KEY | Identificador único |
modo_prueba | SMALLINT o BOOLEAN | NOT NULL | Modo del lote ejecutado |
| Otros campos de auditoría | — | — | Estadísticas del lote, timestamps, estado |
Relación con tabla principal: Es el registro de auditoría de cada lote de facturación. Los registros en membresia_facturacion se crean como consecuencia de un lote registrado aquí.
Nota: Para la documentación técnica del proceso de reenvío de emails ver: reenvio-emails-technical.md
Modelo de Dominio
Servicios involucrados
BatchInvoicingOrchestrator
Archivo: Modules/Membresia/Application/Services/Facturacion/BatchInvoicingOrchestrator.php
Responsabilidades:
- Orquestar el proceso completo de facturación por lotes
- Coordinar entre el servicio de registro de comprobantes y
RefacturacionService - Gestionar el flujo principal y de refacturación
- Insertar en
membresia_facturacional completar el registro de cada comprobante
RefacturacionService
Archivo: Modules/Membresia/Application/Services/Facturacion/RefacturacionService.php
Responsabilidades:
- Implementar la Fase 1 (generar NCs para facturas previas)
- Consultar
membresia_facturacionpara obtener registros activos del período - Manejar cross-mode: separar registros en
sameModeRowsycrossModeRows - Actualizar
nota_credito_iden registros anulados (Fase 1)
Método clave: getFacturadosEnPeriodoPorModo(int $anio, int $mes) — retorna registros de membresia_facturacion donde nota_credito_id IS NULL, agrupados por modo_prueba. Este método implementa la invariante de "registro activo".
BatchComprobanteRegistrationService
Archivo: Modules/Membresia/Application/Services/Facturacion/BatchComprobanteRegistrationService.php
Responsabilidades:
- Registrar el resultado de cada factura en
membresia_facturacion - Insertar nuevos registros (Fase 2 de refacturación y facturación inicial)
Repositorio
MembresiaFacturacionRepository
Archivo: Modules/Membresia/Infrastructure/Persistence/Repositories/MembresiaFacturacionRepository.php
Responsabilidades:
- Encapsular acceso a datos de
membresia_facturacion - Implementar queries de consulta por período y modo
- Actualizar
nota_credito_iden registros anulados
Query especializada
MembresiaFacturacionConFacturaQuery
Archivo: Modules/Membresia/Infrastructure/Persistence/Queries/MembresiaFacturacionConFacturaQuery.php
Propósito: Query de lectura que obtiene registros de membresia_facturacion junto con datos del comprobante del módulo de Ventas. Implementa la invariante nota_credito_id IS NULL para filtrar solo facturas activas.
Capa de Datos
Queries principales sobre membresia_facturacion
Obtener registros activos del período (invariante fundamental)
Descripción: Retorna las facturas vigentes para un período dado. Es la query base para determinar qué socios ya están facturados y cuáles serán anulados en una refacturación.
Columnas críticas del filtro:
anio = :anio AND mes = :mesnota_credito_id IS NULL— solo facturas activas (no anuladas)modo_prueba = :modo_prueba— para filtrar por modo
Separación cross-mode: En el flujo de refacturación, se consultan todos los registros del período (sin filtro de modo_prueba) y se separan en código según su valor de modo_prueba para procesamiento diferenciado.
INSERT de nuevo registro de facturación
Descripción: Inserta un registro por cada factura emitida exitosamente.
Campos obligatorios en INSERT: ordcon_id, anio, mes, factura_id, modo_prueba, fecha
Campos omitidos en INSERT: nota_credito_id — se inserta como NULL (factura activa)
UPDATE para marcar factura como anulada (Fase 1 de refacturación)
Descripción: Actualiza nota_credito_id en el registro previo cuando se genera una NC.
Filtro del UPDATE: Por id específico del registro (obtenido en la consulta de registros activos).
Invariante post-UPDATE: El registro pasa de nota_credito_id IS NULL a nota_credito_id = NC_ID.
Validaciones
Nivel 1: Validaciones Estructurales
Las validaciones de estructura de request (año válido, mes 1-12, rango de IDs) se aplican como middleware antes del controller del endpoint de facturación por lotes.
Nivel 2: Validaciones de Negocio (Service)
La deduplicación y control de estado de membresia_facturacion se implementa en la capa de Service:
Verificar socios ya facturados en el período: Antes de facturar, consultar
membresia_facturacionfiltrandonota_credito_id IS NULLpara el período. Los socios encontrados se omiten en facturación normal (se usan como base en refacturación).Control de modo en cross-mode: El campo
modo_pruebade cada registro determina en qué base de datos se genera la NC correspondiente, independientemente del modo del request actual.Validación de
doc_nroen NCs: Para NCs de facturas tipo B o C (Consumidor Final/Monotributista),doc_nrono es obligatorio. Para facturas tipo A, es obligatorio cuandodoc_tipolo requiere.Deuda cero: Si el cálculo de items da deuda cero para un socio/grupo, no se inserta registro en
membresia_facturaciony se marca como "Omitido" en estadísticas.
Puntos de Integración
Módulo de Ventas
Qué consume: factura_id y nota_credito_id referencia comprobantes en las tablas del módulo de Ventas.
Dirección: membresia_facturacion.factura_id → tabla de comprobantes del módulo de Ventas (en schema correspondiente al nivel multi-tenant activo).
Nota cross-schema: Los comprobantes de facturas en modo oficial viven en la BD oficial; los de modo prueba en la BD prueba (sufijo _p). La tabla membresia_facturacion siempre está en BD oficial, por lo que el factura_id puede referenciar comprobantes en BDs distintas. No hay FK declarada a nivel de DB; la integridad la garantiza el flujo de negocio.
ARCA (Organismo fiscal)
Las NCs de facturas emitidas en modo oficial requieren llamada a ARCA. Las NCs de facturas emitidas en modo prueba son sintéticas (sin ARCA), independientemente del modo del request de refacturación.
Multi-Tenancy
Propagación de Schema
Flow completo:
- Frontend: Axios interceptor inyecta
X-Schemaheader - Backend: Middleware valida
X-Schema - ConnectionManager: Setea
search_pathPostgreSQL - Queries sobre
membresia_facturacion: Ejecutan en schema correcto automáticamente
Nivel de datos: EMPRESA + SUCURSAL (la tabla se crea en ambos niveles según configuración de la empresa)
Multi-modo (prueba vs oficial)
La tabla membresia_facturacion existe solo en la base de datos oficial (sin sufijo _p). El campo modo_prueba registra el modo de emisión de cada factura.
En el flujo cross-mode de refacturación:
ConnectionManagerprovee conexiones nombradas (oficial,prueba) para acceder a los comprobantes en cada base de datos según corresponda- La consulta de
membresia_facturacionsiempre se hace sobre la conexiónprincipal(que apunta a la BD oficial)
Consideraciones de Performance
Indexes existentes
Los indexes actuales cubren las búsquedas más frecuentes:
- Por socio:
idx_membresia_facturacion_orcondsobreordcon_id - Por período:
idx_membresia_facturacion_periodosobre(anio, mes)
Query de "registro activo"
La condición nota_credito_id IS NULL se ejecuta frecuentemente. Si el volumen de registros históricos crece, puede convenir un índice parcial sobre registros activos:
INDEX PARTIEL: WHERE nota_credito_id IS NULLNo implementado actualmente — a evaluar según crecimiento de la tabla.
Volumen esperado
La tabla crece con cada facturación mensual: aproximadamente N registros por lote (1 por socio facturado exitosamente) + M registros por refacturación. El historial no se purga para preservar trazabilidad.
Consideraciones Adicionales
Trazabilidad del historial
La ausencia de UNIQUE constraints es una decisión de diseño que habilita conservar el historial completo de facturaciones y refacturaciones para un socio en un período. No es posible deducir el estado actual solo contando registros: se requiere evaluar nota_credito_id IS NULL.
Rollback de migración
La migración 20260305000002_drop_membresia_facturacion_unique_constraints tiene down() que restaura el UNIQUE sobre (ordcon_id, anio, mes, modo_prueba). Sin embargo, este rollback solo es seguro si no existen aún múltiples registros para el mismo (ordcon_id, anio, mes, modo_prueba), de lo contrario fallará por violación de constraint.
Restricción de módulo
Las migraciones de membresia_facturacion incluyen isMembresiasEnabled() en shouldExecute(). La tabla solo se crea en empresas con el módulo de membresías habilitado.