Appearance
Esquema Multimoneda de Satelites — Facturacion por Lotes
Modulo: Membresias Tipo: Process Estado: Implementado Fecha Creacion: 2026-05-28 Fecha Implementacion: 2026-05-28
Descripcion
Problema que resuelve
El sistema de facturacion por lotes genera comprobantes en moneda base ARS. Cuando una organizacion opera con precios en dolares (USD) para sus categorias de membresia, necesita registrar tanto el importe en ARS (para compliance fiscal con ARCA) como el importe en la moneda original para trazabilidad interna.
Sin un esquema multimoneda, el sistema no podia:
- Registrar el importe en dolares al momento de facturar sin re-derivarlo despues
- Mantener la cotizacion exacta usada en cada operacion
- Mostrar a los socios el desglose en moneda alternativa
Solucion implementada
Se implemento un patron de tablas satelite que preserva el importe en moneda alternativa junto al esquema transaccional ARS existente. Las tablas base permanecen intactas (ARS); las tablas satelite acumulan la informacion complementaria en moneda alternativa.
Arquitectura
Patron base + satelite
Las tablas base del esquema transaccional (factura, credito, debito, itefac, devoluci) siempre contienen importes en ARS. Cada tabla tiene una tabla satelite correspondiente con sufijo _moneda_alt que almacena los importes en moneda alternativa:
| Tabla base | Tabla satelite |
|---|---|
factura | factura_moneda_alt |
credito | credito_moneda_alt |
debito | debito_moneda_alt |
itefac | itefac_moneda_alt |
devoluci | devoluci_moneda_alt |
Las tablas satelite tienen clave foranea hacia la tabla base (factura_id, credito_id, etc.) y son opcionales: si no hay datos en moneda alternativa, simplemente no existe la fila satelite.
itefacydevolucieran tablas legacy sin PK. Las migraciones20260529000005y20260529000006agreganid BIGSERIAL PRIMARY KEYa ambas — prerequisito para que las FK de sus satelites existan.factura,creditoydebitoya tenianid.
Por que satelites y no columnas adicionales
- Rollback limpio: DROP las tablas satelite + desactivar el flag → la base ARS queda intacta, sin columnas residuales.
- Extension sin regresion: Flujos que no soportan multimoneda (ventas generales, factura electronica manual) no necesitan cambios.
- Auditoria independiente: La informacion en moneda alternativa tiene su propio ciclo de vida y puede truncarse/migrarse sin afectar los comprobantes fiscales.
Catalogo monedas
La tabla monedas vive en el schema public del tenant (nivel LEVEL_EMPRESA en la nomenclatura de migraciones) y contiene el catalogo de monedas habilitadas para la organizacion.
sql
CREATE TABLE monedas (
id SERIAL PRIMARY KEY,
codigo CHAR(3) NOT NULL UNIQUE, -- ISO 4217: ARS, USD
nombre VARCHAR(100) NOT NULL,
simbolo VARCHAR(10),
activa BOOLEAN NOT NULL DEFAULT true
);El seed inicial inserta ARS y USD al crear el schema del tenant. El codigo de moneda ARS es la moneda base del sistema y no puede eliminarse.
Flag empres.multimoneda
La columna multimoneda BOOLEAN en la tabla empres (dentro del schema del tenant) actua como feature flag que habilita el path de facturacion alternativa.
Comportamiento
| Flag | Comportamiento |
|---|---|
false (default) | Facturacion normal: solo se escriben tablas base ARS. Las tablas satelite no se tocan. |
true | Facturacion multimoneda: se escriben tablas base ARS Y tablas satelite cuando la categoria tiene moneda = 'USD' y se provee una cotizacion. |
Lectura del flag
El flag se lee via ConfiguracionEmpresaRepository::isMultimonedaMembresiaEnabled() en el dominio. No se lee directamente del modelo ORM en Application ni Presentation para mantener la regla de dependencias.
Visibilidad condicional en formularios (UI)
El mismo flag gobierna la visibilidad de los selectores de moneda en la UI (SDD multimoneda-condicional-flujos). El campo currency_code en los formularios de lista de precios:
ListaPrecioForm(React): leeconfig.empresa.multimonedaviauseConfig(). Cuandomultimoneda = false, el select NO se renderiza en el DOM y el payload envia siemprecurrency_code = 'ARS', incluso si elinitialDatatraia otra moneda. Cuandomultimoneda = true, el campo aparece y se puebla desdeuseMonedas().form-producto.js(legacy vanilla JS): el select#idSelectMonedaListase oculta cuandomultimonedaEnabled = false, yMonedaService.fetchMonedas()solo se invoca cuando el flag esta activo. El<option value="ARS">por defecto garantiza que el payload incluyamoneda: 'ARS'aun con el select oculto.
TIP
No se requieren cambios de schema ni de backend: currency_code ya es opcional con default 'ARS'. La condicionalidad es puramente de presentacion.
Calculo de importes en moneda alternativa
Regla fundamental
El importe en moneda alternativa se calcula en tiempo de escritura (write time) y se almacena persistido. Nunca se re-deriva a posteriori.
La conversion la centraliza DeudaMembresiaCalculator::calcularAltFields() y se aplica de forma uniforme en los tres procesadores de items: procesarCategoriaTitular(), procesarDisciplinas() y procesarProductos(). La formula depende del tipo_precio del item:
tipo_precio | Formula de altAmount |
|---|---|
'N' (precio neto) | ROUND((arsNeto * (1 + alicuota/100)) / cotizacion, 5) |
'F' (precio final, IVA incluido) | ROUND(arsFinal / cotizacion, 5) |
Donde cotizacion es el valor ARS/USD provisto por el usuario en el request de facturacion por lotes, y alicuota es el porcentaje de IVA del item.
Precision y formula corregida
La precision es de 5 decimales (ROUND(x, 5)), no 2. Ademas, para tipo_precio='N' el altAmount se calcula sobre el total con IVA incluido, NO sobre el neto solo.
La formula previa ROUND(importe_ars / cotizacion, 2) era incorrecta y fue corregida por el SDD lotes-cotizacion-conversion-moneda.
Invariante de descomposicion
Para todo item, independientemente del tipo_precio o el procesador usado, se cumple:
altNeto + altIva == altAmount (tolerancia < 1e-6)El IVA alternativo se calcula como residual (altIva = altAmount - altNeto) para que la invariante sea exacta.
Por que write time y no derivacion on-the-fly
- La cotizacion al momento de facturar es unica e irrepetible. Derivarla mas tarde requeriria almacenarla de todas formas.
- Evita discrepancias si la cotizacion usada difiere de la cotizacion vigente al consultar.
- Simplifica las queries de reporte: no requieren joins adicionales para obtener el tipo de cambio historico.
Exposicion en la API
Campos del item cuando multimoneda esta activo
Cuando el item se calculo con una cotizacion (multimoneda activo), InvoiceItem::toDetailedArray() expone los siguientes campos opcionales, que ComprobanteMapper::toDetailedItems() propaga sin modificar hacia la respuesta de la API:
| Campo (API, snake_case) | Descripcion |
|---|---|
alt_neto | Neto del item en moneda alternativa |
alt_iva | IVA del item en moneda alternativa |
alt_total | Total del item en moneda alternativa (alt_neto + alt_iva == alt_total) |
moneda | Codigo ISO de la moneda alternativa (ej: USD) |
cotizacion | Cotizacion ARS/moneda usada en el item |
Campos condicionales, no null-padded
Cuando multimoneda NO esta activo (isMultimonedaMembresiaEnabled() = false o sin cotizacion), estos campos quedan ausentes de la respuesta — no se incluyen como null.
En el frontend, el mapper del servicio convierte estos campos snake_case a camelCase en BatchInvoicingItem: alt_neto → netoAlt, alt_iva → ivaAlt, alt_total → totalAlt, moneda → monedaAlt, cotizacion → cotizacion. Cuando los campos estan ausentes, las propiedades quedan undefined.
Endpoint GET /monedas
El catalogo de monedas se expone via GET /monedas, que devuelve las monedas activas del tenant. Cada entrada incluye code, signo, nombre y decimales. La respuesta incluye al menos ARS y USD.
Los formularios del frontend (select de moneda en lista de precios) poblan sus opciones desde este endpoint en lugar de usar valores hardcodeados.
Par de auditoria membresia_facturacion
La tabla membresia_facturacion (registro de que socio fue facturado en que periodo) incorpora dos columnas de auditoria:
| Columna | Tipo | Descripcion |
|---|---|---|
cotizacion | DECIMAL(16,5) | Cotizacion ARS/USD usada al facturar |
currency_code | CHAR(3) | Codigo ISO de la moneda alternativa (ej: USD) |
Rename de campo
La columna se llamaba originalmente cotizacion_dolar y fue renombrada a cotizacion (tanto en la tabla como en los contratos de la API: BatchInvoicingRequest, MembresiaFacturacionRequest, BatchInvoicingValidator) por el SDD facturacion-lotes-moneda-principal. El nombre cotizacion_dolar ya no existe ni en DB ni en la API.
Por que ambas columnas
currency_code identifica que moneda se uso. cotizacion registra a que valor se convirtio. Ambas son necesarias para un audit trail completo: si en el futuro se soportan mas monedas (EUR, BRL), currency_code distingue entre ellas mientras que cotizacion preserva el tipo de cambio exacto.
Si la facturacion fue en ARS puro (flag OFF o categoria ARS), ambas columnas quedan NULL.
Patron de join header → item
La tabla itefac_moneda_alt (items de factura en moneda alternativa) no almacena la cotizacion. Esta se obtiene via join con factura_moneda_alt:
sql
SELECT
i.id_factura,
i.importe_alt,
f.cotizacion,
f.currency_code
FROM itefac_moneda_alt i
JOIN factura_moneda_alt f ON f.factura_id = i.factura_id
WHERE i.factura_id = :id;Razon del diseno
Almacenar la cotizacion en el header (factura_moneda_alt) y no en cada item evita redundancia: todos los items de una misma factura usan la misma cotizacion. El join es siempre 1:N entre header e items.
Rollback del esquema multimoneda
Si se necesita revertir la funcionalidad multimoneda:
- Desactivar el flag:
UPDATE empres SET multimoneda = falseen el schema del tenant. - Opcional: DROP de las tablas satelite (los datos ya no se escriben con el flag en false):
sql
DROP TABLE IF EXISTS itefac_moneda_alt;
DROP TABLE IF EXISTS factura_moneda_alt;
DROP TABLE IF EXISTS credito_moneda_alt;
DROP TABLE IF EXISTS debito_moneda_alt;
DROP TABLE IF EXISTS devoluci_moneda_alt;- Las tablas base (
factura,credito,debito,itefac,devoluci) permanecen intactas con todos sus datos ARS. - La columna
dolaroriginal encreditoydebitoya fue eliminada en la migracion20260529000007_drop_column_dolar. Si se necesita el rollback completo, restaurar desde backup.
Extension futura
Para agregar soporte multimoneda en flujos no-batch (ventas generales, factura electronica manual):
- El mismo patron satelite aplica: agregar una tabla
{tabla}_moneda_altcon FK a la tabla base. - El flag
empres.multimonedaya existe — reutilizarlo o crear flags granulares por flujo. - El catalogo
monedasya esta sembrado — no requiere cambios. - El campo
cotizaciondebe proveerse en el request del flujo correspondiente. - Los servicios de dominio pueden reutilizar
DeudaMembresiaCalculator::setAltFields()como patron de referencia.
Flujos pendientes de extension
- Ventas generales (modulo
mod-ventas) - Factura electronica manual
- Notas de credito/debito manuales
Archivos relevantes
Backend
| Archivo | Descripcion |
|---|---|
migrations/migrations/tenancy/20260529000001_new_table_monedas.php | Catalogo de monedas |
migrations/migrations/tenancy/20260529000002_new_table_factura_moneda_alt.php | Satelite de facturas |
migrations/migrations/tenancy/20260529000003_new_table_credito_moneda_alt.php | Satelite de creditos |
migrations/migrations/tenancy/20260529000004_new_table_debito_moneda_alt.php | Satelite de debitos |
migrations/migrations/tenancy/20260529000005_add_id_to_itefac.php | Agrega id BIGSERIAL PRIMARY KEY a itefac |
migrations/migrations/tenancy/20260529000006_add_id_to_devoluci.php | Agrega id BIGSERIAL PRIMARY KEY a devoluci |
migrations/migrations/tenancy/20260529000007_new_table_itefac_moneda_alt.php | Satelite de items |
migrations/migrations/tenancy/20260529000008_new_table_devoluci_moneda_alt.php | Satelite de devoluciones |
migrations/migrations/tenancy/20260529000009_drop_column_dolar.php | Limpieza de columnas legacy |
migrations/migrations/tenancy/20260529000010_add_column_currency_code_cotizacion_membresia_facturacion.php | Par de auditoria |
Modules/Membresia/Infrastructure/Persistence/Repositories/DoctrineConfiguracionEmpresaRepository.php | Lectura del flag multimoneda |
Modules/Membresia/Domain/Facturacion/Services/DeudaMembresiaCalculator.php | Calculo de importe alt (setAltFields) |
Modules/Membresia/Application/Services/Facturacion/BatchFacturaRegistrationService.php | Escritura de satelite factura |
Modules/Membresia/Application/Services/Facturacion/BatchNotaCreditoRegistrationService.php | Escritura de satelite credito |
Presentation/DTOs/Facturacion/BatchInvoicingRequest.php | cotizacion + currency_code en request |
Frontend
| Archivo | Descripcion |
|---|---|
ts/mod-membresias/FacturacionLotes/components/FacturacionLotesForm/index.tsx | Campo cotizacion + tabla referencia ambito |
ts/mod-membresias/FacturacionLotes/hooks/useCotizacionDolar.ts | Query cotizacion actual + ambito referencia |
ts/bases/Moneda/services/moneda.service.ts | Servicio shared de monedas |
ts/bases/Moneda/hooks/useMonedas.ts | Hook shared de monedas |