Uecko_ERP/docs/customer-invoices/split-proformas-issued-invoices-migration.md

26 KiB

Split Proformas / Issued Invoices

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

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.
  • 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.
  • 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
  • adaptadores legacy de 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
  • CustomerInvoiceSeriesModel y SequelizeInvoiceSeriesRepository como wiring transicional no prioritario

Exclusivas de V2

  • tablas proformas, proforma_items, proforma_taxes
  • tablas issued_invoices, issued_invoice_items, issued_invoice_taxes
  • numerador V2 de proformas sobre document-series, con snapshot en proformas.document_series_id, proforma_number y proforma_reference
  • mappers y repositorios Sequelize V2

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

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
  • tax_config queda persistido en proformas como configuracion fiscal de 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

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

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;

Proformas sin lineas con impuestos materializados

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

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

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