2026-07-10 11:59:44 +00:00
# Split Proformas / Issued Invoices
2026-07-28 16:04:57 +00:00
Estado: diseno tecnico / fases 1A-1D + 2A-2B
Estado del backend: persistencia V2 activada; integracion con `document-series` activa para numeracion nueva; sin ejecucion confirmada de migracion DB
2026-07-10 11:59:44 +00:00
## 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.
2026-07-28 16:04:57 +00:00
- `document_series_id` identifica la serie documental usada para numerar la proforma.
- `proforma_number` guarda el numero propio asignado a la proforma.
- `target_invoice_series_code` es opcional y solo puede apuntar a una serie de `issued_invoice` .
- una serie `PF` o cualquier otra serie de `document_type = proforma` nunca debe persistirse en `target_invoice_series_code` .
- si `target_invoice_series_code` queda `NULL` , la emision de la factura usara la serie default activa de `issued_invoice` .
2026-07-10 11:59:44 +00:00
- `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`
2026-07-28 16:04:57 +00:00
- adaptadores legacy de `InvoiceSeriesNumberAssigner` para `IssuedInvoice`
2026-07-10 11:59:44 +00:00
- 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`
2026-07-28 16:04:57 +00:00
- `CustomerInvoiceSeriesModel` y `SequelizeInvoiceSeriesRepository` como wiring transicional no prioritario
2026-07-10 11:59:44 +00:00
### Exclusivas de V2
- tablas `proformas` , `proforma_items` , `proforma_taxes`
- tablas `issued_invoices` , `issued_invoice_items` , `issued_invoice_taxes`
2026-07-28 16:04:57 +00:00
- numerador V2 de proformas sobre `document-series` , con snapshot en `proformas.document_series_id` , `proforma_number` y `proforma_reference`
2026-07-10 11:59:44 +00:00
- mappers y repositorios Sequelize V2
2026-07-28 16:04:57 +00:00
## Regla operativa actual de series en proformas
- al crear una proforma, `document-series` se invoca con `document_type = proforma` para resolver la numeracion visible de la proforma
- ese resultado solo alimenta `document_series_id` , `proforma_number` y `proforma_reference`
- `target_invoice_series_code` no se rellena automaticamente con la serie documental de proforma
- si el usuario no selecciona serie futura de factura, `target_invoice_series_code` se persiste en `NULL`
- si el usuario informa `target_invoice_series_code` , el backend lo conserva solo si corresponde a una serie activa de `issued_invoice`
- al emitir una proforma, la factura definitiva se numera con `document_type = issued_invoice`
- si la proforma trae `target_invoice_series_code` , se usa como `seriesCode`
- si `target_invoice_series_code` es `NULL` , `document-series` resuelve la serie default activa de `issued_invoice`
- `proforma_series_code` es un input de create para `document_type = proforma` y no vuelve a ser editable despues
## Estado Fase 2B
- `customer-invoices` ya no usa `customer_invoice_series` como fuente primaria para listar series activas ni para asignar numeracion nueva en `issued_invoice` y `proforma`
- `/catalogs/invoice-series` fue transicional y queda eliminado antes de produccion
- `document-series` queda como API canonica para listar series activas y asignar numeracion
- la DDL de `proformas` y la migracion/validacion SQL quedan documentadas, no ejecutadas
- la propagacion de `branchId` sigue pendiente porque el contexto de sucursal no llega todavia a los casos de uso activos
## Estado Fase 2C
- se detecto un entorno de desarrollo en `apps/server/.env.development`
- produccion quedo descartada para ejecucion en esta fase
- no se ejecuto SQL real desde esta terminal por bloqueo operativo del cliente de BD local
- la DDL de proformas se endurecio para compatibilidad MariaDB/MySQL mediante `information_schema` y SQL dinamico
- la validacion funcional y la prueba de concurrencia quedaron documentadas para ejecucion manual sobre la BD de desarrollo segura
## Estado Fase 2D
- el contrato publico de proformas ya soporta `target_invoice_series_code` en create y update
- `series` se mantiene como alias legacy temporal en requests
- si `series` y `target_invoice_series_code` llegan a la vez con distinto valor, el request falla por validacion
- las responses de proforma prefieren `target_invoice_series_code`
- `series` se mantiene temporalmente en responses como alias legacy con el mismo valor
- el cliente interno del modulo ya emite `target_invoice_series_code` en requests nuevos
## Estado Fase 2E
- create separa `proforma_series_code` de `target_invoice_series_code`
- `proforma_series_code` solo se usa para numerar la proforma
- update ya no usa `series` en el flujo nuevo ni permite cambiar la serie propia de proforma
- `target_invoice_series_code` sigue siendo el unico concepto de serie futura de factura emitida
- la UI nueva de proformas ya envia `proforma_series_code` y `target_invoice_series_code`
2026-07-29 08:16:34 +00:00
## Estado Fase 2F
- create de proforma se consolida como creacion de cabecera/documento inicial
- una proforma sin lineas es valida y debe persistirse con `items = []`
- `tax_regime_code` queda como configuracion fiscal inicial de la cabecera
- `proforma_taxes` solo representa impuestos calculados, no defaults fiscales ni placeholders sobre base `0`
- si una proforma nace sin lineas, la response debe devolver `taxes = []` y totales monetarios a `0`
- update sigue siendo el punto donde se anaden lineas y se materializan impuestos/totales reales
- la UI deja de prometer que la fiscalidad queda "aplicada" en create y pasa a hablar de valores iniciales
2026-07-28 16:04:57 +00:00
## Rollback recomendado
- no aplicar nada en produccion en esta fase
- si en desarrollo se ejecuta la DDL/migracion y hay que revertir, hacerlo con backup previo o clon de desarrollo, nunca con borrado destructivo improvisado
- conservar `customer_invoice_series` solo como legado historico hasta ejecutar la limpieza operativa documentada
2026-07-10 11:59:44 +00:00
## 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;
```
2026-07-29 08:16:34 +00:00
### Proformas sin lineas con impuestos materializados
```sql
SELECT
p.id,
p.proforma_reference,
COUNT(DISTINCT pi.item_id) AS item_rows,
COUNT(DISTINCT pt.tax_id) AS tax_rows
FROM proformas p
LEFT JOIN proforma_items pi
ON pi.proforma_id = p.id
LEFT JOIN proforma_taxes pt
ON pt.proforma_id = p.id
GROUP BY p.id, p.proforma_reference
HAVING COUNT(DISTINCT pi.item_id) = 0
AND COUNT(DISTINCT pt.tax_id) > 0;
```
Resultado esperado:
- `0` filas
2026-07-10 11:59:44 +00:00
## 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
2026-07-28 16:04:57 +00:00
## Nota document-series
`customer-invoices` debe migrar progresivamente desde `CustomerInvoiceSeriesModel` hacia `document-series` . Desde la Fase 2A:
- `issued_invoice` ya asigna numeración nueva mediante `document-series`
- `proforma` ya asigna referencia nueva mediante `document-series`
- la UI debe consumir `GET /document-series` filtrando por `document_type`
- `CustomerInvoiceSeriesModel` deja de existir como modelo runtime activo
## Fase 2B no incluida
- no ejecucion de SQL en una BD real
- no borrado de tablas/modelos/repositorios legacy
- no propagacion completa de `branchId`
- no cambios de frontend salvo compatibilidad ya existente
- no nueva infraestructura de tests automatizados para estos modulos