Uecko_ERP/docs/customer-invoices/split-proformas-issued-invoices-migration.md
2026-07-15 14:13:13 +02:00

20 KiB

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

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

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,
  COALESCE(tax_regime_code, "01"),
  COALESCE(tax_regime_description, "01: Operación de régimen general."),
  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

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,
  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()
FROM customer_invoice_items i
JOIN customer_invoices ci ON ci.id = i.invoice_id
WHERE ci.is_proforma = 1;




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()
FROM customer_invoice_items i
JOIN customer_invoices ci ON ci.id = i.invoice_id
WHERE ci.is_proforma = 0;

Taxes

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,
  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()
FROM customer_invoice_taxes t
JOIN customer_invoices ci ON ci.id = t.invoice_id
WHERE ci.is_proforma = 1;

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()
FROM customer_invoice_taxes t
JOIN customer_invoices ci ON ci.id = t.invoice_id
WHERE ci.is_proforma = 0;

Verifactu

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

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

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

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

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