2026-07-10 11:59:44 +00:00
# Split Proformas / Issued Invoices
Estado: diseno tecnico / fases 1A-1D
Estado del backend: persistencia V2 activada; SQL de desarrollo preparado; sin ejecucion confirmada de migracion DB
## Objetivo
Preparar el esquema fisico para separar:
- `customer_invoices` -> `proformas`
- `customer_invoices` -> `issued_invoices`
- `customer_invoice_items` -> `proforma_items`
- `customer_invoice_items` -> `issued_invoice_items`
- `customer_invoice_taxes` -> `proforma_taxes`
- `customer_invoice_taxes` -> `issued_invoice_taxes`
- `verifactu_records.invoice_id` -> `verifactu_records.issued_invoice_id`
## Decisiones de fase 1A
- `proforma_reference` es el identificador visible canonico de proforma.
- `target_invoice_series_code` sustituye la semantica legacy de `series` en proformas.
- `issued_invoices` usa `invoice_series_code` , `invoice_number` , `invoice_date` y `source_proforma_id` .
- `proformas` no guarda `issued_invoice_id` ; la relacion se resuelve con `issued_invoices.source_proforma_id` .
- Los impuestos del esquema actual son por documento, no por linea: `proforma_taxes` y `issued_invoice_taxes` .
- `payment_method` y `payment_term` en `issued_invoices` son snapshot historico, no estado vivo de cobro.
- No hay doble escritura.
- Legacy queda congelado solo para validacion y rollback cuando se haga el cutover.
## Estado de fase 1B
- Existen mappers Sequelize V2 sobre `proformas` , `proforma_items` , `proforma_taxes` .
- Existen mappers Sequelize V2 sobre `issued_invoices` , `issued_invoice_items` , `issued_invoice_taxes` .
- Existen repositorios Sequelize V2 listos para convivir con los repositorios legacy.
- El wiring activo legacy no se ha sustituido todavia.
- El dominio/application sigue arrastrando naming legacy en algunos contratos (`series`, `invoiceNumber` , `linkedProformaId` ); la traduccion queda encapsulada en Infrastructure V2.
- `payment_term` sigue sin soporte real en el dominio actual de customer invoices; en persistencia V2 queda preparado pero no conectado funcionalmente.
## Estado de fase 1C
- El wiring activo del modulo usa persistencia V2 en los composition roots principales.
- `buildV2ProformaPersistence(...)` queda activo en:
- `modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts`
- `modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts`
- `buildV2IssuedInvoicePersistence(...)` queda activo en:
- `modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoices.di.ts`
- `modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-public-services.ts`
- La persistencia legacy no se borra, pero deja de ser la ruta operativa principal.
- La emision `Proforma -> IssuedInvoice` ahora encadena sobre V2:
- lee `proformas`
- crea `issued_invoices`
- crea `issued_invoice_items`
- crea `issued_invoice_taxes`
- marca la proforma como `issued`
- usa `issued_invoices.source_proforma_id`
- No hay doble escritura: cada composition root sigue instanciando una sola familia de repositorios.
- `VerifactuRecord` queda preparado para la ruta V2 mediante `issued_invoice_id` como relacion principal.
- La migracion/backfill real sigue pendiente. Si la BD real no tiene las tablas nuevas, el backend no podra arrancar correctamente con este wiring.
## Estado de fase 1D
- Existen scripts SQL separados para desarrollo:
- `docs/customer-invoices/sql/dev-split-proformas-issued-invoices.sql`
- `docs/customer-invoices/sql/validate-split-proformas-issued-invoices.sql`
- El SQL de desarrollo es idempotente y conservador:
- crea tablas V2 con `CREATE TABLE IF NOT EXISTS`
- hace backfill con `NOT EXISTS`
- no usa `DROP` , `TRUNCATE` ni `DELETE`
- no sobrescribe filas ya migradas
- no toca datos legacy salvo la columna transicional `verifactu_records.issued_invoice_id`
- Se documenta el orden de ejecucion en `docs/customer-invoices/sql/README.md` .
- En este workspace no se encontro una configuracion de BD de desarrollo inequivoca (`.env*` no presente en raiz del repo), por lo que no se ejecuto SQL real.
- Tampoco se hizo validacion funcional contra runtime real porque falta una BD de desarrollo confirmada y migrada para esta fase.
## Dependencias compartidas y exclusivas
### Compartidas entre legacy y V2
- contratos de Application (`IProformaRepository`, `IIssuedInvoiceRepository` )
- use cases, servicios de dominio y assemblers
- `TransactionManager`
- `InvoiceSeriesNumberAssigner` para `IssuedInvoice`
- resolvers de catalogos
- servicios de documentos PDF/preview
### Exclusivas de legacy
- `CustomerInvoiceModel` como storage real de Proforma e IssuedInvoice
- numerador de proformas sobre `customer_invoices.invoice_number` con `is_proforma = true`
- repositorios Sequelize legacy de `customer_invoices`
### Exclusivas de V2
- tablas `proformas` , `proforma_items` , `proforma_taxes`
- tablas `issued_invoices` , `issued_invoice_items` , `issued_invoice_taxes`
- numerador V2 de proformas sobre `proformas.proforma_reference`
- mappers y repositorios Sequelize V2
## Tablas nuevas previstas
### proformas
- `id`
- `company_id`
- `status`
- `proforma_reference`
- `proforma_date`
- `operation_date`
- `target_invoice_series_code`
- `reference`
- `description`
- `notes`
- `language_code`
- `currency_code`
- `customer_id`
- snapshot cliente
- `payment_method_id`
- `payment_term_id`
- `tax_regime_code`
- totales monetarios actuales
- `created_at`
- `updated_at`
- `deleted_at`
### issued_invoices
- `id`
- `company_id`
- `status`
- `source_proforma_id`
- `invoice_series_code`
- `invoice_number`
- `invoice_date`
- `operation_date`
- `reference`
- `description`
- `notes`
- `language_code`
- `currency_code`
- `customer_id`
- snapshot cliente
- `payment_method_id`
- `payment_method_name`
- `payment_method_description`
- `payment_term_id`
- `payment_term_name`
- `payment_term_description`
- `payment_term_snapshot_json`
- `tax_regime_code`
- `tax_regime_description`
- totales monetarios actuales
- `created_at`
- `updated_at`
- `deleted_at`
### Items y taxes
- `proforma_items.proforma_id`
- `issued_invoice_items.issued_invoice_id`
- `proforma_taxes.proforma_id`
- `issued_invoice_taxes.issued_invoice_id`
### Verifactu
- `verifactu_records.issued_invoice_id` nuevo
- `verifactu_records.invoice_id` legacy temporal durante la transicion de fase 1A
## SQL orientativo de backfill
### Proformas
```sql
INSERT INTO proformas (
id,
company_id,
status,
proforma_reference,
proforma_date,
operation_date,
target_invoice_series_code,
reference,
description,
notes,
language_code,
currency_code,
customer_id,
payment_method_id,
payment_term_id,
tax_regime_code,
subtotal_amount_value,
subtotal_amount_scale,
items_discount_amount_value,
items_discount_amount_scale,
global_discount_percentage_value,
global_discount_percentage_scale,
global_discount_amount_value,
global_discount_amount_scale,
total_discount_amount_value,
total_discount_amount_scale,
taxable_amount_value,
taxable_amount_scale,
iva_amount_value,
iva_amount_scale,
rec_amount_value,
rec_amount_scale,
retention_amount_value,
retention_amount_scale,
taxes_amount_value,
taxes_amount_scale,
total_amount_value,
total_amount_scale,
created_at,
updated_at,
deleted_at
)
SELECT
id,
company_id,
status,
CASE
WHEN series IS NULL OR series = '' THEN invoice_number
ELSE CONCAT(series, '-', invoice_number)
END AS proforma_reference,
invoice_date,
operation_date,
series,
reference,
description,
notes,
language_code,
currency_code,
customer_id,
payment_method_id,
NULL AS payment_term_id,
tax_regime_code,
subtotal_amount_value,
subtotal_amount_scale,
items_discount_amount_value,
items_discount_amount_scale,
global_discount_percentage_value,
global_discount_percentage_scale,
global_discount_amount_value,
global_discount_amount_scale,
total_discount_amount_value,
total_discount_amount_scale,
taxable_amount_value,
taxable_amount_scale,
iva_amount_value,
iva_amount_scale,
rec_amount_value,
rec_amount_scale,
retention_amount_value,
retention_amount_scale,
taxes_amount_value,
taxes_amount_scale,
total_amount_value,
total_amount_scale,
created_at,
updated_at,
deleted_at
FROM customer_invoices
WHERE is_proforma = 1;
```
### Issued invoices
```sql
INSERT INTO issued_invoices (
id,
company_id,
status,
source_proforma_id,
invoice_series_code,
invoice_number,
invoice_date,
operation_date,
reference,
description,
notes,
language_code,
currency_code,
customer_id,
customer_tin,
customer_name,
customer_street,
customer_street2,
customer_city,
customer_province,
customer_postal_code,
customer_country,
payment_method_id,
payment_method_name,
payment_method_description,
payment_term_id,
payment_term_name,
payment_term_description,
payment_term_snapshot_json,
tax_regime_code,
tax_regime_description,
subtotal_amount_value,
subtotal_amount_scale,
items_discount_amount_value,
items_discount_amount_scale,
global_discount_percentage_value,
global_discount_percentage_scale,
global_discount_amount_value,
global_discount_amount_scale,
total_discount_amount_value,
total_discount_amount_scale,
taxable_amount_value,
taxable_amount_scale,
iva_amount_value,
iva_amount_scale,
rec_amount_value,
rec_amount_scale,
retention_amount_value,
retention_amount_scale,
taxes_amount_value,
taxes_amount_scale,
total_amount_value,
total_amount_scale,
created_at,
updated_at,
deleted_at
)
SELECT
id,
company_id,
status,
proforma_id,
series,
invoice_number,
invoice_date,
operation_date,
reference,
description,
notes,
language_code,
currency_code,
customer_id,
customer_tin,
customer_name,
customer_street,
customer_street2,
customer_city,
customer_province,
customer_postal_code,
customer_country,
payment_method_id,
payment_method_description,
payment_method_description,
NULL AS payment_term_id,
NULL AS payment_term_name,
NULL AS payment_term_description,
NULL AS payment_term_snapshot_json,
2026-07-15 12:13:13 +00:00
COALESCE(tax_regime_code, "01"),
COALESCE(tax_regime_description, "01: Operación de régimen general."),
2026-07-10 11:59:44 +00:00
subtotal_amount_value,
subtotal_amount_scale,
items_discount_amount_value,
items_discount_amount_scale,
global_discount_percentage_value,
global_discount_percentage_scale,
global_discount_amount_value,
global_discount_amount_scale,
total_discount_amount_value,
total_discount_amount_scale,
taxable_amount_value,
taxable_amount_scale,
iva_amount_value,
iva_amount_scale,
rec_amount_value,
rec_amount_scale,
retention_amount_value,
retention_amount_scale,
taxes_amount_value,
taxes_amount_scale,
total_amount_value,
total_amount_scale,
created_at,
updated_at,
deleted_at
FROM customer_invoices
WHERE is_proforma = 0;
```
### Items
```sql
INSERT INTO proforma_items (
item_id,
proforma_id,
position,
description,
quantity_value,
quantity_scale,
unit_amount_value,
unit_amount_scale,
subtotal_amount_value,
subtotal_amount_scale,
item_discount_percentage_value,
item_discount_percentage_scale,
item_discount_amount_value,
item_discount_amount_scale,
global_discount_percentage_value,
global_discount_percentage_scale,
global_discount_amount_value,
global_discount_amount_scale,
total_discount_amount_value,
total_discount_amount_scale,
taxable_amount_value,
taxable_amount_scale,
iva_code,
iva_percentage_value,
iva_percentage_scale,
iva_amount_value,
iva_amount_scale,
rec_code,
rec_percentage_value,
rec_percentage_scale,
rec_amount_value,
rec_amount_scale,
retention_code,
retention_percentage_value,
retention_percentage_scale,
retention_amount_value,
retention_amount_scale,
taxes_amount_value,
taxes_amount_scale,
total_amount_value,
2026-07-15 12:13:13 +00:00
total_amount_scale,
created_at,
updated_at
2026-07-10 11:59:44 +00:00
)
SELECT
i.item_id,
i.invoice_id,
i.position,
i.description,
i.quantity_value,
i.quantity_scale,
i.unit_amount_value,
i.unit_amount_scale,
i.subtotal_amount_value,
i.subtotal_amount_scale,
i.item_discount_percentage_value,
i.item_discount_percentage_scale,
i.item_discount_amount_value,
i.item_discount_amount_scale,
i.global_discount_percentage_value,
i.global_discount_percentage_scale,
i.global_discount_amount_value,
i.global_discount_amount_scale,
i.total_discount_amount_value,
i.total_discount_amount_scale,
i.taxable_amount_value,
i.taxable_amount_scale,
i.iva_code,
i.iva_percentage_value,
i.iva_percentage_scale,
i.iva_amount_value,
i.iva_amount_scale,
i.rec_code,
i.rec_percentage_value,
i.rec_percentage_scale,
i.rec_amount_value,
i.rec_amount_scale,
i.retention_code,
i.retention_percentage_value,
i.retention_percentage_scale,
i.retention_amount_value,
i.retention_amount_scale,
i.taxes_amount_value,
i.taxes_amount_scale,
i.total_amount_value,
2026-07-15 12:13:13 +00:00
i.total_amount_scale,
now(),
now()
2026-07-10 11:59:44 +00:00
FROM customer_invoice_items i
JOIN customer_invoices ci ON ci.id = i.invoice_id
WHERE ci.is_proforma = 1;
2026-07-15 12:13:13 +00:00
INSERT INTO issued_invoice_items (
item_id,
issued_invoice_id,
position,
description,
quantity_value,
quantity_scale,
unit_amount_value,
unit_amount_scale,
subtotal_amount_value,
subtotal_amount_scale,
item_discount_percentage_value,
item_discount_percentage_scale,
item_discount_amount_value,
item_discount_amount_scale,
global_discount_percentage_value,
global_discount_percentage_scale,
global_discount_amount_value,
global_discount_amount_scale,
total_discount_amount_value,
total_discount_amount_scale,
taxable_amount_value,
taxable_amount_scale,
iva_code,
iva_percentage_value,
iva_percentage_scale,
iva_amount_value,
iva_amount_scale,
rec_code,
rec_percentage_value,
rec_percentage_scale,
rec_amount_value,
rec_amount_scale,
retention_code,
retention_percentage_value,
retention_percentage_scale,
retention_amount_value,
retention_amount_scale,
taxes_amount_value,
taxes_amount_scale,
total_amount_value,
total_amount_scale,
created_at,
updated_at
)
SELECT
i.item_id,
i.invoice_id,
i.position,
i.description,
i.quantity_value,
i.quantity_scale,
i.unit_amount_value,
i.unit_amount_scale,
i.subtotal_amount_value,
i.subtotal_amount_scale,
i.item_discount_percentage_value,
i.item_discount_percentage_scale,
i.item_discount_amount_value,
i.item_discount_amount_scale,
i.global_discount_percentage_value,
i.global_discount_percentage_scale,
i.global_discount_amount_value,
i.global_discount_amount_scale,
i.total_discount_amount_value,
i.total_discount_amount_scale,
i.taxable_amount_value,
i.taxable_amount_scale,
i.iva_code,
i.iva_percentage_value,
i.iva_percentage_scale,
i.iva_amount_value,
i.iva_amount_scale,
i.rec_code,
i.rec_percentage_value,
i.rec_percentage_scale,
i.rec_amount_value,
i.rec_amount_scale,
i.retention_code,
i.retention_percentage_value,
i.retention_percentage_scale,
i.retention_amount_value,
i.retention_amount_scale,
i.taxes_amount_value,
i.taxes_amount_scale,
i.total_amount_value,
i.total_amount_scale,
now(),
now()
2026-07-10 11:59:44 +00:00
FROM customer_invoice_items i
JOIN customer_invoices ci ON ci.id = i.invoice_id
WHERE ci.is_proforma = 0;
```
### Taxes
```sql
INSERT INTO proforma_taxes (
tax_id,
proforma_id,
taxable_amount_value,
taxable_amount_scale,
iva_code,
iva_percentage_value,
iva_percentage_scale,
iva_amount_value,
iva_amount_scale,
rec_code,
rec_percentage_value,
rec_percentage_scale,
rec_amount_value,
rec_amount_scale,
retention_code,
retention_percentage_value,
retention_percentage_scale,
retention_amount_value,
retention_amount_scale,
taxes_amount_value,
2026-07-15 12:13:13 +00:00
taxes_amount_scale,
created_at,
updated_at
2026-07-10 11:59:44 +00:00
)
SELECT
t.tax_id,
t.invoice_id,
t.taxable_amount_value,
t.taxable_amount_scale,
t.iva_code,
t.iva_percentage_value,
t.iva_percentage_scale,
t.iva_amount_value,
t.iva_amount_scale,
t.rec_code,
t.rec_percentage_value,
t.rec_percentage_scale,
t.rec_amount_value,
t.rec_amount_scale,
t.retention_code,
t.retention_percentage_value,
t.retention_percentage_scale,
t.retention_amount_value,
t.retention_amount_scale,
t.taxes_amount_value,
2026-07-15 12:13:13 +00:00
t.taxes_amount_scale,
now(),
now()
2026-07-10 11:59:44 +00:00
FROM customer_invoice_taxes t
JOIN customer_invoices ci ON ci.id = t.invoice_id
WHERE ci.is_proforma = 1;
2026-07-15 12:13:13 +00:00
INSERT INTO issued_invoice_taxes (tax_id,
issued_invoice_id,
taxable_amount_value,
taxable_amount_scale,
iva_code,
iva_percentage_value,
iva_percentage_scale,
iva_amount_value,
iva_amount_scale,
rec_code,
rec_percentage_value,
rec_percentage_scale,
rec_amount_value,
rec_amount_scale,
retention_code,
retention_percentage_value,
retention_percentage_scale,
retention_amount_value,
retention_amount_scale,
taxes_amount_value,
taxes_amount_scale,
created_at,
updated_at)
SELECT
t.tax_id,
t.invoice_id,
t.taxable_amount_value,
t.taxable_amount_scale,
t.iva_code,
t.iva_percentage_value,
t.iva_percentage_scale,
t.iva_amount_value,
t.iva_amount_scale,
t.rec_code,
t.rec_percentage_value,
t.rec_percentage_scale,
t.rec_amount_value,
t.rec_amount_scale,
t.retention_code,
t.retention_percentage_value,
t.retention_percentage_scale,
t.retention_amount_value,
t.retention_amount_scale,
t.taxes_amount_value,
t.taxes_amount_scale,
now(),
now()
2026-07-10 11:59:44 +00:00
FROM customer_invoice_taxes t
JOIN customer_invoices ci ON ci.id = t.invoice_id
WHERE ci.is_proforma = 0;
```
### Verifactu
```sql
UPDATE verifactu_records vr
JOIN customer_invoices ci ON ci.id = vr.invoice_id
SET vr.issued_invoice_id = vr.invoice_id
WHERE ci.is_proforma = 0;
```
## Validaciones SQL
### Conteos
```sql
SELECT COUNT(*) FROM customer_invoices WHERE is_proforma = 1;
SELECT COUNT(*) FROM proformas;
SELECT COUNT(*) FROM customer_invoices WHERE is_proforma = 0;
SELECT COUNT(*) FROM issued_invoices;
SELECT COUNT(*)
FROM customer_invoice_items i
JOIN customer_invoices ci ON ci.id = i.invoice_id
WHERE ci.is_proforma = 1;
SELECT COUNT(*) FROM proforma_items;
SELECT COUNT(*)
FROM customer_invoice_taxes t
JOIN customer_invoices ci ON ci.id = t.invoice_id
WHERE ci.is_proforma = 0;
SELECT COUNT(*) FROM issued_invoice_taxes;
```
### Duplicados fiscales
```sql
SELECT company_id, invoice_series_code, invoice_number, COUNT(*)
FROM issued_invoices
GROUP BY company_id, invoice_series_code, invoice_number
HAVING COUNT(*) > 1;
```
### Huerfanos
```sql
SELECT COUNT(*)
FROM issued_invoices ii
LEFT JOIN proformas p ON p.id = ii.source_proforma_id
WHERE ii.source_proforma_id IS NOT NULL
AND p.id IS NULL;
SELECT COUNT(*)
FROM verifactu_records vr
LEFT JOIN issued_invoices ii ON ii.id = vr.issued_invoice_id
WHERE vr.issued_invoice_id IS NOT NULL
AND ii.id IS NULL;
```
### Totales
```sql
SELECT SUM(total_amount_value) FROM customer_invoices WHERE is_proforma = 1;
SELECT SUM(total_amount_value) FROM proformas;
SELECT SUM(total_amount_value) FROM customer_invoices WHERE is_proforma = 0;
SELECT SUM(total_amount_value) FROM issued_invoices;
```
## No incluido en fase 1A
- no cutover del backend
- no borrado de tablas legacy
- no doble escritura
- no ejecucion de migraciones en una BD real
- no integracion Verifactu externa
- no dominio de cobros, vencimientos vivos o conciliacion
## No incluido en fase 1C
- no borrado de legacy
- no backfill real
- no ejecucion de migraciones
- no doble escritura
- no integracion Verifactu funcional nueva
- no cambios en controllers, rutas o DTOs publicos
## No incluido en fase 1D
- no ejecucion en produccion
- no borrado de tablas legacy
- no eliminacion de modelos/repositorios legacy
- no integracion Verifacti funcional
- no dominio de cobros
- no doble escritura
## Resultado de validacion tecnica fase 1D
- `tsc` modulo `customer-invoices` : esperado ejecutar en esta fase
- `biome lint` sobre `src/api/infrastructure` y `docs/customer-invoices` : esperado ejecutar en esta fase
- `tsc` raiz: esperado ejecutar en esta fase
- Validacion SQL real: no ejecutada por falta de entorno de desarrollo inequivoco
- Validacion runtime real contra BD migrada: no ejecutada por la misma razon
## Siguiente fase recomendada
- confirmar `.env` /credenciales de una BD de desarrollo segura
- ejecutar `dev-split-proformas-issued-invoices.sql`
- ejecutar `validate-split-proformas-issued-invoices.sql`
- levantar el backend contra esa BD y validar `create` , `update` , `get` , `list` e `issue proforma`
- validar numeracion de proformas V2 y asignacion de serie/numero de factura emitida
- revisar la ruta `VerifactuRecord -> issued_invoice_id` ya con persistencia V2 activa