Skip to content

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:

  1. Facturación inicial: INSERT con (ordcon_id, anio, mes, factura_id=F1, nota_credito_id=NULL)
  2. Primera refacturación: UPDATE registro previo: nota_credito_id=NC1. INSERT nuevo: (ordcon_id, anio, mes, factura_id=F2, nota_credito_id=NULL)
  3. 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.

CampoTipoConstraintsDescripción
idSERIALPRIMARY KEYIdentificador único autogenerado
ordcon_idINTEGERNOT NULLID del socio/miembro (FK a ordcon)
anioSMALLINTNOT NULLAño del período facturado
mesSMALLINTNOT NULLMes del período facturado (1-12)
factura_idINTEGERNOT NULLID de la factura generada en el módulo de Ventas (registro inmutable)
nota_credito_idINTEGERNULLID de la NC que anuló esta factura. NULL = factura activa. NOT NULL = factura anulada
modo_pruebaSMALLINT o BOOLEANNOT NULLModo en que fue emitida la factura (0 = oficial, 1 = prueba)
fechaTIMESTAMPNOT 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:

NombreColumnasTipoPropósito
idx_membresia_facturacion_orcondordcon_idBTREEBúsquedas por socio
idx_membresia_facturacion_periodoanio, mesBTREEBúsquedas por período

Sin constraints UNIQUE: Esta es una decisión deliberada. Ver ADR arriba.

Foreign Keys:

ConstraintColumna LocalReferenciaON DELETEON UPDATE
(ninguna declarada explícitamente)ordcon_idordcon(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

CampoTipoConstraintsDescripción
idSERIALPRIMARY KEYIdentificador único
modo_pruebaSMALLINT o BOOLEANNOT NULLModo del lote ejecutado
Otros campos de auditoríaEstadí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_facturacion al 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_facturacion para obtener registros activos del período
  • Manejar cross-mode: separar registros en sameModeRows y crossModeRows
  • Actualizar nota_credito_id en 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_id en 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 = :mes
  • nota_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:

  1. Verificar socios ya facturados en el período: Antes de facturar, consultar membresia_facturacion filtrando nota_credito_id IS NULL para el período. Los socios encontrados se omiten en facturación normal (se usan como base en refacturación).

  2. Control de modo en cross-mode: El campo modo_prueba de cada registro determina en qué base de datos se genera la NC correspondiente, independientemente del modo del request actual.

  3. Validación de doc_nro en NCs: Para NCs de facturas tipo B o C (Consumidor Final/Monotributista), doc_nro no es obligatorio. Para facturas tipo A, es obligatorio cuando doc_tipo lo requiere.

  4. Deuda cero: Si el cálculo de items da deuda cero para un socio/grupo, no se inserta registro en membresia_facturacion y 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:

  1. Frontend: Axios interceptor inyecta X-Schema header
  2. Backend: Middleware valida X-Schema
  3. ConnectionManager: Setea search_path PostgreSQL
  4. 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:

  • ConnectionManager provee conexiones nombradas (oficial, prueba) para acceder a los comprobantes en cada base de datos según corresponda
  • La consulta de membresia_facturacion siempre se hace sobre la conexión principal (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_orcond sobre ordcon_id
  • Por período: idx_membresia_facturacion_periodo sobre (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 NULL

No 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.