# 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 - `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 ```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, 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 ```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, 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 ```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, 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 ```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; ``` ### 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 ## 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