feat(document-series): implement document series management API
- Add UpdateDocumentSeriesController for updating document series. - Create documentSeriesApiErrorMapper for error handling. - Define document series routes including CRUD operations. - Implement Sequelize-based persistence layer for document series. - Add DTOs for request and response schemas for document series operations. - Establish common structures for document series data handling. - Configure TypeScript settings for the document series module.
This commit is contained in:
parent
65015c8c4f
commit
5ed6556036
@ -40,6 +40,7 @@
|
|||||||
"@erp/catalogs": "workspace:*",
|
"@erp/catalogs": "workspace:*",
|
||||||
"@erp/customer-invoices": "workspace:*",
|
"@erp/customer-invoices": "workspace:*",
|
||||||
"@erp/customers": "workspace:*",
|
"@erp/customers": "workspace:*",
|
||||||
|
"@erp/document-series": "workspace:*",
|
||||||
"@erp/identity": "workspace:*",
|
"@erp/identity": "workspace:*",
|
||||||
"@erp/factuges": "workspace:*",
|
"@erp/factuges": "workspace:*",
|
||||||
"@repo/rdx-logger": "workspace:*",
|
"@repo/rdx-logger": "workspace:*",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import catalogsAPIModule from "@erp/catalogs/api";
|
|||||||
import companiesAPIModule from "@erp/companies/api";
|
import companiesAPIModule from "@erp/companies/api";
|
||||||
import customerInvoicesAPIModule from "@erp/customer-invoices/api";
|
import customerInvoicesAPIModule from "@erp/customer-invoices/api";
|
||||||
import customersAPIModule from "@erp/customers/api";
|
import customersAPIModule from "@erp/customers/api";
|
||||||
|
import documentSeriesAPIModule from "@erp/document-series/api";
|
||||||
import factuGESAPIModule from "@erp/factuges/api";
|
import factuGESAPIModule from "@erp/factuges/api";
|
||||||
import identityAPIModule from "@erp/identity/api";
|
import identityAPIModule from "@erp/identity/api";
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ export const registerModules = () => {
|
|||||||
registerModule(companiesAPIModule);
|
registerModule(companiesAPIModule);
|
||||||
registerModule(catalogsAPIModule);
|
registerModule(catalogsAPIModule);
|
||||||
registerModule(customersAPIModule);
|
registerModule(customersAPIModule);
|
||||||
|
registerModule(documentSeriesAPIModule);
|
||||||
registerModule(customerInvoicesAPIModule);
|
registerModule(customerInvoicesAPIModule);
|
||||||
registerModule(factuGESAPIModule);
|
registerModule(factuGESAPIModule);
|
||||||
//registerModule(suppliersAPIModule);
|
//registerModule(suppliersAPIModule);
|
||||||
|
|||||||
@ -20,8 +20,9 @@ docs/
|
|||||||
- [Estado de migración a identity](./architecture/identity-migration-status.md)
|
- [Estado de migración a identity](./architecture/identity-migration-status.md)
|
||||||
- [Flujo frontend de auth y selección de empresa](./frontend/auth-and-company-selection.md)
|
- [Flujo frontend de auth y selección de empresa](./frontend/auth-and-company-selection.md)
|
||||||
- [Seed local de admin](./dev/seed-local-admin.sql)
|
- [Seed local de admin](./dev/seed-local-admin.sql)
|
||||||
- [SQL manual de `customer_invoice_series`](./dev/customer-invoice-series.sql)
|
- [SQL histórico de `customer_invoice_series`](./dev/customer-invoice-series.sql)
|
||||||
- [Decisión backend de `InvoiceSeries`](./customer-invoices/invoice-series.md)
|
- [Migración desde `customer_invoice_series` a `document_series`](./document-series/migration-from-customer-invoice-series.md)
|
||||||
|
- [Contrato de series de proformas](./customer-invoices/proforma-series-contract.md)
|
||||||
- [README de `modules/auth`](../modules/auth/README.md)
|
- [README de `modules/auth`](../modules/auth/README.md)
|
||||||
- [README de `modules/identity`](../modules/identity/README.md)
|
- [README de `modules/identity`](../modules/identity/README.md)
|
||||||
- [README de `modules/companies`](../modules/companies/README.md)
|
- [README de `modules/companies`](../modules/companies/README.md)
|
||||||
|
|||||||
@ -1,20 +1,23 @@
|
|||||||
# InvoiceSeries
|
# InvoiceSeries (Histórico / Obsoleto)
|
||||||
|
|
||||||
|
> Documento mantenido solo como referencia histórica previa a la retirada de `/catalogs/invoice-series`.
|
||||||
|
> Estado actual: la API canónica es `/document-series` y la tabla canónica es `document_series`.
|
||||||
|
|
||||||
## Decisión consolidada
|
## Decisión consolidada
|
||||||
|
|
||||||
El recurso backend para las series de facturación de cliente se llama:
|
Antes de la migración, el recurso backend para las series de facturación de cliente se llamaba:
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
InvoiceSeries
|
InvoiceSeries
|
||||||
```
|
```
|
||||||
|
|
||||||
Su tabla Sequelize asociada es:
|
Su tabla Sequelize asociada era:
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
customer_invoice_series
|
customer_invoice_series
|
||||||
```
|
```
|
||||||
|
|
||||||
No se usa ya un recurso backend genérico `series`, ni `document-series`, ni `issued-invoice-series`.
|
Este contenido ya no describe el runtime activo del ERP.
|
||||||
|
|
||||||
## Alcance funcional
|
## Alcance funcional
|
||||||
|
|
||||||
|
|||||||
81
docs/customer-invoices/proforma-series-contract.md
Normal file
81
docs/customer-invoices/proforma-series-contract.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# Proforma Series Contract
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Normalizar el contrato publico de proformas para separar definitivamente la serie propia de la proforma de la serie futura de la factura emitida.
|
||||||
|
|
||||||
|
## Semantica correcta
|
||||||
|
|
||||||
|
En proformas:
|
||||||
|
|
||||||
|
- `document_series_id` identifica la serie documental propia de la proforma
|
||||||
|
- `proforma_number` guarda el numero propio de la proforma
|
||||||
|
- `proforma_reference` guarda la referencia visible de la proforma, por ejemplo `PF-0139`
|
||||||
|
- `proforma_series_code` es el codigo funcional que se usa solo en create para elegir una serie de `document_type = proforma`
|
||||||
|
- `target_invoice_series_code` identifica la serie futura que se usara al emitir una `issued_invoice`, por ejemplo `F26`
|
||||||
|
|
||||||
|
`target_invoice_series_code` nunca debe apuntar implicitamente a la serie documental de proforma. Un valor como `PF` solo seria valido si existiera tambien una serie activa `PF` de `document_type = issued_invoice`, lo cual no debe asumirse.
|
||||||
|
|
||||||
|
## Reglas funcionales
|
||||||
|
|
||||||
|
### Crear proforma
|
||||||
|
|
||||||
|
- se pide fecha
|
||||||
|
- se pide cliente
|
||||||
|
- `proforma_series_code` es opcional; si falta, se usa la serie default activa de `document_type = proforma`
|
||||||
|
- `target_invoice_series_code` es opcional; si falta, se persiste `NULL`
|
||||||
|
- la UI obtiene ambas listas desde `GET /document-series`, filtrando por `document_type`
|
||||||
|
|
||||||
|
### Editar draft
|
||||||
|
|
||||||
|
- se permiten cambios comerciales
|
||||||
|
- se permite cambiar `target_invoice_series_code`
|
||||||
|
- no se permite cambiar `proforma_series_code`, `document_series_id`, `proforma_number` ni `proforma_reference`
|
||||||
|
|
||||||
|
### Editar approved
|
||||||
|
|
||||||
|
- la edicion queda restringida
|
||||||
|
- solo se permite ajustar `target_invoice_series_code` antes de emitir
|
||||||
|
|
||||||
|
### Editar issued
|
||||||
|
|
||||||
|
- no se permite edicion funcional
|
||||||
|
|
||||||
|
## Contrato actual
|
||||||
|
|
||||||
|
### Requests
|
||||||
|
|
||||||
|
- create acepta `proforma_series_code`
|
||||||
|
- create acepta `target_invoice_series_code`
|
||||||
|
- update acepta `target_invoice_series_code`
|
||||||
|
- el flujo nuevo ya no necesita `series`
|
||||||
|
|
||||||
|
### Responses
|
||||||
|
|
||||||
|
- el backend emite `target_invoice_series_code` como nombre preferente
|
||||||
|
- `series` puede mantenerse solo como alias legacy de salida mientras existan consumidores antiguos
|
||||||
|
|
||||||
|
## Regla de emision
|
||||||
|
|
||||||
|
- si `target_invoice_series_code` tiene valor, la emision usa ese `seriesCode` para `document_type = issued_invoice`
|
||||||
|
- si `target_invoice_series_code` es `NULL`, `document-series` resuelve la serie default activa de `issued_invoice`
|
||||||
|
- la UI nunca debe consumir numeracion directa desde `POST /document-series/assign-next`
|
||||||
|
|
||||||
|
## Validacion defensiva
|
||||||
|
|
||||||
|
Si el request informa `target_invoice_series_code`:
|
||||||
|
|
||||||
|
- debe corresponder a una serie activa de `document_type = issued_invoice`
|
||||||
|
- no se permite volver a persistir series de `document_type = proforma` como target de factura
|
||||||
|
|
||||||
|
Si el request informa `proforma_series_code`:
|
||||||
|
|
||||||
|
- debe corresponder a una serie activa de `document_type = proforma`
|
||||||
|
- solo se usa para numerar la proforma en create
|
||||||
|
- despues queda congelado en `document_series_id`, `proforma_number` y `proforma_reference`
|
||||||
|
|
||||||
|
## Retirada futura de `series`
|
||||||
|
|
||||||
|
- `series` era ambiguo porque mezclaba dos conceptos distintos
|
||||||
|
- el contrato correcto usa `proforma_series_code` y `target_invoice_series_code`
|
||||||
|
- `series` debe retirarse por completo cuando ya no queden consumidores legacy de responses
|
||||||
@ -1,7 +1,7 @@
|
|||||||
# Split Proformas / Issued Invoices
|
# Split Proformas / Issued Invoices
|
||||||
|
|
||||||
Estado: diseno tecnico / fases 1A-1D
|
Estado: diseno tecnico / fases 1A-1D + 2A-2B
|
||||||
Estado del backend: persistencia V2 activada; SQL de desarrollo preparado; sin ejecucion confirmada de migracion DB
|
Estado del backend: persistencia V2 activada; integracion con `document-series` activa para numeracion nueva; sin ejecucion confirmada de migracion DB
|
||||||
|
|
||||||
## Objetivo
|
## Objetivo
|
||||||
|
|
||||||
@ -19,6 +19,11 @@ Preparar el esquema fisico para separar:
|
|||||||
|
|
||||||
- `proforma_reference` es el identificador visible canonico de proforma.
|
- `proforma_reference` es el identificador visible canonico de proforma.
|
||||||
- `target_invoice_series_code` sustituye la semantica legacy de `series` en proformas.
|
- `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`.
|
- `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`.
|
- `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`.
|
- Los impuestos del esquema actual son por documento, no por linea: `proforma_taxes` y `issued_invoice_taxes`.
|
||||||
@ -78,7 +83,7 @@ Preparar el esquema fisico para separar:
|
|||||||
- contratos de Application (`IProformaRepository`, `IIssuedInvoiceRepository`)
|
- contratos de Application (`IProformaRepository`, `IIssuedInvoiceRepository`)
|
||||||
- use cases, servicios de dominio y assemblers
|
- use cases, servicios de dominio y assemblers
|
||||||
- `TransactionManager`
|
- `TransactionManager`
|
||||||
- `InvoiceSeriesNumberAssigner` para `IssuedInvoice`
|
- adaptadores legacy de `InvoiceSeriesNumberAssigner` para `IssuedInvoice`
|
||||||
- resolvers de catalogos
|
- resolvers de catalogos
|
||||||
- servicios de documentos PDF/preview
|
- servicios de documentos PDF/preview
|
||||||
|
|
||||||
@ -87,14 +92,66 @@ Preparar el esquema fisico para separar:
|
|||||||
- `CustomerInvoiceModel` como storage real de Proforma e IssuedInvoice
|
- `CustomerInvoiceModel` como storage real de Proforma e IssuedInvoice
|
||||||
- numerador de proformas sobre `customer_invoices.invoice_number` con `is_proforma = true`
|
- numerador de proformas sobre `customer_invoices.invoice_number` con `is_proforma = true`
|
||||||
- repositorios Sequelize legacy de `customer_invoices`
|
- repositorios Sequelize legacy de `customer_invoices`
|
||||||
|
- `CustomerInvoiceSeriesModel` y `SequelizeInvoiceSeriesRepository` como wiring transicional no prioritario
|
||||||
|
|
||||||
### Exclusivas de V2
|
### Exclusivas de V2
|
||||||
|
|
||||||
- tablas `proformas`, `proforma_items`, `proforma_taxes`
|
- tablas `proformas`, `proforma_items`, `proforma_taxes`
|
||||||
- tablas `issued_invoices`, `issued_invoice_items`, `issued_invoice_taxes`
|
- tablas `issued_invoices`, `issued_invoice_items`, `issued_invoice_taxes`
|
||||||
- numerador V2 de proformas sobre `proformas.proforma_reference`
|
- numerador V2 de proformas sobre `document-series`, con snapshot en `proformas.document_series_id`, `proforma_number` y `proforma_reference`
|
||||||
- mappers y repositorios Sequelize V2
|
- 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`
|
||||||
|
|
||||||
|
## 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
|
## Tablas nuevas previstas
|
||||||
|
|
||||||
### proformas
|
### proformas
|
||||||
@ -795,3 +852,20 @@ SELECT SUM(total_amount_value) FROM issued_invoices;
|
|||||||
- levantar el backend contra esa BD y validar `create`, `update`, `get`, `list` e `issue proforma`
|
- 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
|
- 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
|
- 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
|
||||||
|
|||||||
@ -0,0 +1,103 @@
|
|||||||
|
-- DDL conservador e idempotente para preparar proformas con document-series.
|
||||||
|
-- Compatible con MariaDB/MySQL sin depender de ALTER ... IF NOT EXISTS.
|
||||||
|
-- Ejecutar solo en una BD de desarrollo segura.
|
||||||
|
-- No ejecutar automaticamente en produccion.
|
||||||
|
|
||||||
|
SET @target_schema = DATABASE();
|
||||||
|
|
||||||
|
SET
|
||||||
|
@ddl = (
|
||||||
|
SELECT IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE
|
||||||
|
table_schema = @target_schema
|
||||||
|
AND table_name = 'proformas'
|
||||||
|
AND column_name = 'document_series_id'
|
||||||
|
), 'SELECT ''document_series_id already exists''', 'ALTER TABLE proformas ADD COLUMN document_series_id CHAR(36) NULL AFTER company_id'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
|
||||||
|
EXECUTE stmt;
|
||||||
|
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET
|
||||||
|
@ddl = (
|
||||||
|
SELECT IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE
|
||||||
|
table_schema = @target_schema
|
||||||
|
AND table_name = 'proformas'
|
||||||
|
AND column_name = 'proforma_number'
|
||||||
|
), 'SELECT ''proforma_number already exists''', 'ALTER TABLE proformas ADD COLUMN proforma_number VARCHAR(64) NULL AFTER proforma_reference'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
|
||||||
|
EXECUTE stmt;
|
||||||
|
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET
|
||||||
|
@ddl = (
|
||||||
|
SELECT IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.statistics
|
||||||
|
WHERE
|
||||||
|
table_schema = @target_schema
|
||||||
|
AND table_name = 'proformas'
|
||||||
|
AND index_name = 'idx_proformas_company_document_series'
|
||||||
|
), 'SELECT ''idx_proformas_company_document_series already exists''', 'ALTER TABLE proformas ADD INDEX idx_proformas_company_document_series (company_id, document_series_id)'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
|
||||||
|
EXECUTE stmt;
|
||||||
|
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET
|
||||||
|
@ddl = (
|
||||||
|
SELECT IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.statistics
|
||||||
|
WHERE
|
||||||
|
table_schema = @target_schema
|
||||||
|
AND table_name = 'proformas'
|
||||||
|
AND index_name = 'idx_proformas_company_proforma_reference'
|
||||||
|
), 'SELECT ''idx_proformas_company_proforma_reference already exists''', 'ALTER TABLE proformas ADD INDEX idx_proformas_company_proforma_reference (company_id, proforma_reference)'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
|
||||||
|
EXECUTE stmt;
|
||||||
|
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Validacion minima posterior a la DDL.
|
||||||
|
-- SELECT column_name
|
||||||
|
-- FROM information_schema.columns
|
||||||
|
-- WHERE table_schema = DATABASE()
|
||||||
|
-- AND table_name = 'proformas'
|
||||||
|
-- AND column_name IN ('document_series_id', 'proforma_number');
|
||||||
|
--
|
||||||
|
-- SELECT index_name, GROUP_CONCAT(column_name ORDER BY seq_in_index) AS indexed_columns
|
||||||
|
-- FROM information_schema.statistics
|
||||||
|
-- WHERE table_schema = DATABASE()
|
||||||
|
-- AND table_name = 'proformas'
|
||||||
|
-- AND index_name IN (
|
||||||
|
-- 'idx_proformas_company_document_series',
|
||||||
|
-- 'idx_proformas_company_proforma_reference'
|
||||||
|
-- )
|
||||||
|
-- GROUP BY index_name;
|
||||||
15
docs/customer-invoices/sql/fix-proforma-series-semantics.sql
Normal file
15
docs/customer-invoices/sql/fix-proforma-series-semantics.sql
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
UPDATE proformas AS p
|
||||||
|
JOIN document_series AS ds
|
||||||
|
ON ds.company_id = p.company_id
|
||||||
|
AND ds.code = p.target_invoice_series_code
|
||||||
|
SET
|
||||||
|
p.target_invoice_series_code = NULL,
|
||||||
|
p.updated_at = NOW()
|
||||||
|
WHERE ds.document_type = 'proforma';
|
||||||
|
|
||||||
|
SELECT p.id, p.proforma_reference, p.target_invoice_series_code
|
||||||
|
FROM proformas AS p
|
||||||
|
JOIN document_series AS ds
|
||||||
|
ON ds.company_id = p.company_id
|
||||||
|
AND ds.code = p.target_invoice_series_code
|
||||||
|
WHERE ds.document_type = 'proforma';
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
UPDATE proformas AS p
|
||||||
|
JOIN document_series AS ds
|
||||||
|
ON ds.code = p.target_invoice_series_code
|
||||||
|
AND ds.document_type = 'proforma'
|
||||||
|
SET
|
||||||
|
p.target_invoice_series_code = NULL,
|
||||||
|
p.updated_at = NOW()
|
||||||
|
WHERE p.target_invoice_series_code IS NOT NULL;
|
||||||
|
|
||||||
|
SELECT p.id, p.proforma_reference, p.target_invoice_series_code
|
||||||
|
FROM proformas AS p
|
||||||
|
JOIN document_series AS ds
|
||||||
|
ON ds.company_id = p.company_id
|
||||||
|
AND ds.code = p.target_invoice_series_code
|
||||||
|
WHERE ds.document_type = 'proforma';
|
||||||
74
docs/document-series/README.md
Normal file
74
docs/document-series/README.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# document-series
|
||||||
|
|
||||||
|
Centraliza la gestion de series documentales y su numeracion transaccional para documentos ERP. A fecha de cierre V1, es la API canonica y la tabla canonica para series de `customer-invoices` en `issued_invoice` y `proforma`.
|
||||||
|
|
||||||
|
## Estructura
|
||||||
|
|
||||||
|
- `modules/document-series/src/api/domain`: agregado `DocumentSeries` y VOs
|
||||||
|
- `modules/document-series/src/api/application`: use cases, servicios y repositorio
|
||||||
|
- `modules/document-series/src/api/infrastructure`: Sequelize, Express, DI y servicios públicos
|
||||||
|
- `modules/document-series/src/common/dto`: contratos HTTP y de transporte con Zod
|
||||||
|
|
||||||
|
## Servicios disponibles
|
||||||
|
|
||||||
|
- `GET /document-series`
|
||||||
|
- `GET /document-series/:id`
|
||||||
|
- `POST /document-series`
|
||||||
|
- `PUT /document-series/:id`
|
||||||
|
- `PATCH /document-series/:id/disable`
|
||||||
|
- `POST /document-series/assign-next`
|
||||||
|
|
||||||
|
## Consumo desde otros módulos
|
||||||
|
|
||||||
|
`document-series:general` expone:
|
||||||
|
|
||||||
|
- `assignNextNumber(params)`
|
||||||
|
- `listActiveSeries(params)`
|
||||||
|
|
||||||
|
## Estado de integración
|
||||||
|
|
||||||
|
- `issued-invoices` ya asigna numeracion nueva mediante `document-series`
|
||||||
|
- `proformas` ya asigna referencia nueva mediante `document-series`
|
||||||
|
- las proformas V2 persisten snapshot explicito de serie con `document_series_id`, `proforma_number` y `proforma_reference`
|
||||||
|
- en create de proformas, `proforma_series_code` permite seleccionar la serie de `document_type = proforma`
|
||||||
|
- en emision de proformas, `target_invoice_series_code` apunta solo a `document_type = issued_invoice`
|
||||||
|
- el endpoint transicional `/catalogs/invoice-series` queda eliminado antes de produccion
|
||||||
|
- la UI debe consultar `GET /document-series` filtrando por `document_type`
|
||||||
|
- la UI no debe consumir `POST /document-series/assign-next` para previsualizar numeros
|
||||||
|
- `customer_invoice_series` queda como legado historico a retirar mediante limpieza operativa controlada, no como runtime activo
|
||||||
|
|
||||||
|
## SQL de soporte V1
|
||||||
|
|
||||||
|
- `docs/document-series/sql/migrate-customer-invoice-series-to-document-series.sql`
|
||||||
|
- `docs/document-series/sql/validate-document-series-migration.sql`
|
||||||
|
- `docs/document-series/sql/drop-customer-invoice-series-legacy.sql`
|
||||||
|
- `docs/customer-invoices/sql/add-proforma-document-series-columns.sql`
|
||||||
|
|
||||||
|
Estos scripts son conservadores e idempotentes. No se han ejecutado automaticamente en este workspace ni sustituyen una migracion productiva formal.
|
||||||
|
|
||||||
|
## Estado Fase 2C
|
||||||
|
|
||||||
|
- el entorno detectado para desarrollo es `apps/server/.env.development`
|
||||||
|
- el acceso apunta a `localhost` con `NODE_ENV=development`
|
||||||
|
- en esta terminal no se pudo ejecutar SQL real porque:
|
||||||
|
- no existe cliente `mysql` disponible
|
||||||
|
- la dependencia `mysql2` del workspace no resuelve una dependencia transitiva (`sql-escaper`), por lo que tampoco fue posible abrir conexion desde Node
|
||||||
|
- por tanto, la Fase 2C queda preparada y documentada, pero no ejecutada desde este workspace
|
||||||
|
|
||||||
|
## Pendientes conocidos
|
||||||
|
|
||||||
|
- propagacion real de `branchId` desde el contexto de negocio hasta `assignNextNumber(...)`
|
||||||
|
- validacion funcional contra una BD de desarrollo migrada
|
||||||
|
|
||||||
|
Ejemplo conceptual:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const documentSeries = getService<DocumentSeriesPublicServicesType>("document-series:general");
|
||||||
|
|
||||||
|
const result = await documentSeries.assignNextNumber({
|
||||||
|
companyId,
|
||||||
|
documentType: "issued_invoice",
|
||||||
|
seriesCode: "F",
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
```
|
||||||
91
docs/document-series/document-series-design.md
Normal file
91
docs/document-series/document-series-design.md
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# Document Series Design
|
||||||
|
|
||||||
|
## Objetivo del módulo
|
||||||
|
|
||||||
|
`document-series` es un módulo propio porque la serie documental no es un catálogo estático: mantiene reglas de vigencia, flags de activación, defaults y estado transaccional de numeración.
|
||||||
|
|
||||||
|
## Por qué no vive en catalogs
|
||||||
|
|
||||||
|
Los catálogos del ERP resuelven valores de referencia. Aquí hay concurrencia, bloqueo de fila y mutación transaccional de `next_number`, así que el comportamiento es operativo y no meramente descriptivo.
|
||||||
|
|
||||||
|
## Por qué numeración y serie viven juntas
|
||||||
|
|
||||||
|
La numeración depende del estado de la serie:
|
||||||
|
|
||||||
|
- si está activa
|
||||||
|
- si está vigente
|
||||||
|
- si es la serie por defecto
|
||||||
|
- cuál es su prefijo, sufijo y padding
|
||||||
|
|
||||||
|
Separarlo introduciría doble escritura o lecturas no atómicas.
|
||||||
|
|
||||||
|
## Modelo conceptual
|
||||||
|
|
||||||
|
- `DocumentType`: tipo documental cerrado en V1
|
||||||
|
- `DocumentSeries`: agregado con configuración y contador
|
||||||
|
- `AssignedDocumentNumber`: resultado de asignación atómica
|
||||||
|
|
||||||
|
## Campos principales
|
||||||
|
|
||||||
|
- `companyId`, `branchId`
|
||||||
|
- `documentType`
|
||||||
|
- `code`, `name`, `description`
|
||||||
|
- `prefix`, `suffix`
|
||||||
|
- `nextNumber`, `padding`
|
||||||
|
- `validFrom`, `validTo`
|
||||||
|
- `isDefault`, `isActive`
|
||||||
|
|
||||||
|
## Reglas V1
|
||||||
|
|
||||||
|
- tipos soportados: `proforma`, `issued_invoice`
|
||||||
|
- no asigna si la serie está inactiva
|
||||||
|
- no asigna si la serie está fuera de vigencia
|
||||||
|
- default por empresa + tipo, con prioridad de `branch_id` cuando aplica
|
||||||
|
- formato de referencia: `prefix + padded(next_number) + suffix`
|
||||||
|
|
||||||
|
## Concurrencia
|
||||||
|
|
||||||
|
La asignación usa transacción obligatoria y bloqueo de fila con `LOCK.UPDATE` sobre `document_series`. El incremento de `next_number` se persiste dentro de la misma transacción y con comprobación optimista del valor previo para evitar dobles consumos.
|
||||||
|
|
||||||
|
## Integracion con customer-invoices
|
||||||
|
|
||||||
|
En V1:
|
||||||
|
|
||||||
|
- `issued-invoices` delega la asignacion de numero en `document-series`
|
||||||
|
- `proformas` delega la asignacion de referencia en `document-series`
|
||||||
|
- `proformas` persiste `document_series_id` y `proforma_number` como snapshot tecnico adicional a `proforma_reference`
|
||||||
|
- `proformas.document_series_id` identifica la serie documental usada para numerar la proforma
|
||||||
|
- `proformas.proforma_number` guarda el numero propio de la proforma
|
||||||
|
- `proformas.proforma_reference` guarda la referencia visible de la proforma
|
||||||
|
- `proforma_series_code` es el input funcional de create para elegir la serie de `document_type = proforma`
|
||||||
|
- `proformas.target_invoice_series_code` es opcional y, cuando existe, apunta a una serie de `issued_invoice`, nunca a una serie de `proforma`
|
||||||
|
- si `proformas.target_invoice_series_code` es `NULL`, la emision resuelve la serie default activa de `issued_invoice`
|
||||||
|
- en Fase 2E, `series` deja de ser el contrato nuevo para create/update de proformas
|
||||||
|
- la UI consulta `GET /document-series` con filtro por `document_type` e idealmente `is_active = true`
|
||||||
|
- `/catalogs/invoice-series` deja de existir como endpoint runtime
|
||||||
|
- `customer_invoice_series` no participa ya en flujos activos de lectura o numeracion
|
||||||
|
|
||||||
|
## Migracion conservadora
|
||||||
|
|
||||||
|
- la migracion historica de `issued_invoice` parte de `customer_invoice_series`
|
||||||
|
- la creacion de series default de `proforma` usa `companies` como fuente
|
||||||
|
- las validaciones SQL se mantienen separadas del script de migracion
|
||||||
|
- no hay ejecucion automatica ni cutover productivo en esta fase
|
||||||
|
- la DDL de `proformas` usa `information_schema` + SQL dinamico para mantener compatibilidad MariaDB/MySQL sin depender de `IF NOT EXISTS` en `ALTER TABLE`
|
||||||
|
|
||||||
|
## Branch scope pendiente
|
||||||
|
|
||||||
|
El modelo ya contempla `branchId`, y la resolucion default da prioridad a `branch_id` cuando existe. Aun asi, `customer-invoices` no propaga un contexto real de sucursal hasta `assignNextNumber(...)`, asi que en esta fase toda la integracion sigue operando con alcance de empresa.
|
||||||
|
|
||||||
|
## Límites V1
|
||||||
|
|
||||||
|
- sin `format_pattern`
|
||||||
|
- sin reseteos anuales automáticos
|
||||||
|
- sin reservas de numeración
|
||||||
|
- sin huecos justificados
|
||||||
|
- sin migración productiva automática de legacy
|
||||||
|
|
||||||
|
## Extensiones futuras
|
||||||
|
|
||||||
|
- nuevos `DocumentType`
|
||||||
|
- seeds/migraciones controladas de series heredadas
|
||||||
87
docs/document-series/manual-concurrency-validation.md
Normal file
87
docs/document-series/manual-concurrency-validation.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# Manual concurrency validation
|
||||||
|
|
||||||
|
Validacion manual de concurrencia para `document-series` sin introducir framework nuevo.
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Comprobar que varias asignaciones simultaneas sobre la misma serie:
|
||||||
|
|
||||||
|
- no generan duplicados
|
||||||
|
- consumen numeros consecutivos
|
||||||
|
- avanzan `next_number` exactamente `N` posiciones
|
||||||
|
|
||||||
|
## Precondiciones
|
||||||
|
|
||||||
|
- BD de desarrollo segura
|
||||||
|
- serie de `proforma` activa y conocida
|
||||||
|
- backend arrancado contra esa BD
|
||||||
|
- capacidad de lanzar varias peticiones HTTP casi simultaneas
|
||||||
|
|
||||||
|
## Caso base recomendado
|
||||||
|
|
||||||
|
Serie inicial:
|
||||||
|
|
||||||
|
- `document_type = 'proforma'`
|
||||||
|
- `code = 'PF'`
|
||||||
|
- `next_number = 100`
|
||||||
|
|
||||||
|
Numero de asignaciones concurrentes:
|
||||||
|
|
||||||
|
- `N = 10`
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
- referencias unicas `PF-000100` a `PF-000109`
|
||||||
|
- `document_series.next_number = 110`
|
||||||
|
- cero duplicados
|
||||||
|
|
||||||
|
## Preparacion SQL
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT id, company_id, document_type, code, prefix, suffix, next_number, padding
|
||||||
|
FROM document_series
|
||||||
|
WHERE document_type = 'proforma'
|
||||||
|
AND code = 'PF';
|
||||||
|
```
|
||||||
|
|
||||||
|
Si hace falta preparar la serie manualmente en desarrollo, hacerlo de forma conservadora y reversible.
|
||||||
|
|
||||||
|
## Ejecucion HTTP manual
|
||||||
|
|
||||||
|
1. Preparar 10 peticiones de creacion de proforma para la misma empresa y misma serie destino.
|
||||||
|
2. Lanzarlas en paralelo desde el cliente HTTP disponible.
|
||||||
|
3. Registrar `proforma_reference`, `proforma_number` y `document_series_id` devueltos.
|
||||||
|
|
||||||
|
## Verificaciones SQL
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT proforma_reference, proforma_number, document_series_id
|
||||||
|
FROM proformas
|
||||||
|
WHERE document_series_id = '<document_series_id>'
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 20;
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT proforma_reference, COUNT(*) AS duplicates
|
||||||
|
FROM proformas
|
||||||
|
WHERE document_series_id = '<document_series_id>'
|
||||||
|
GROUP BY proforma_reference
|
||||||
|
HAVING COUNT(*) > 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT id, next_number
|
||||||
|
FROM document_series
|
||||||
|
WHERE id = '<document_series_id>';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Criterio de aceptacion
|
||||||
|
|
||||||
|
- no hay filas en la query de duplicados
|
||||||
|
- aparecen `N` nuevas referencias distintas
|
||||||
|
- `next_number_final = next_number_inicial + N`
|
||||||
|
|
||||||
|
## Bloqueo actual en este workspace
|
||||||
|
|
||||||
|
En este workspace no se pudo ejecutar la prueba porque la terminal no pudo abrir conexion operativa a la BD de desarrollo. La configuracion detectada es de desarrollo, pero falta cliente `mysql` y el conector `mysql2` local no resuelve todas sus dependencias transitivas.
|
||||||
57
docs/document-series/manual-validation.md
Normal file
57
docs/document-series/manual-validation.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Manual validation
|
||||||
|
|
||||||
|
Guia minima de validacion manual para el cierre V1 de `document-series`. No requiere nueva infraestructura de tests automatizados.
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Confirmar que `document-series` es la fuente operativa unica para series y numeracion nueva de `customer-invoices` y que las restricciones funcionales V1 siguen activas.
|
||||||
|
|
||||||
|
## Precondiciones
|
||||||
|
|
||||||
|
- BD de desarrollo con tablas `document_series`, `proformas` e `issued_invoices`
|
||||||
|
- DDL de `docs/customer-invoices/sql/add-proforma-document-series-columns.sql` aplicada si `proformas` aun no tiene las columnas nuevas
|
||||||
|
- seed o migracion conservadora de `docs/document-series/sql/migrate-customer-invoice-series-to-document-series.sql` aplicada de forma manual si procede
|
||||||
|
- backend arrancado con `apps/server/.env.development` o equivalente seguro de desarrollo
|
||||||
|
|
||||||
|
## Casos recomendados
|
||||||
|
|
||||||
|
1. Asignacion de `issued_invoice` desde `document-series`
|
||||||
|
- crear o dejar activa una serie `issued_invoice`
|
||||||
|
- emitir una factura desde el flujo normal
|
||||||
|
- comprobar que la referencia/numero resultante coincide con `prefix + padded(next_number) + suffix`
|
||||||
|
- comprobar que `document_series.next_number` se incrementa exactamente en 1
|
||||||
|
|
||||||
|
2. Asignacion de `proforma` desde `document-series`
|
||||||
|
- crear o dejar activa una serie `proforma`
|
||||||
|
- crear una proforma desde el flujo normal
|
||||||
|
- comprobar que `proformas.document_series_id`, `proformas.proforma_number` y `proformas.proforma_reference` quedan informados
|
||||||
|
- comprobar que `proforma_reference` usa el formato de la serie asignada
|
||||||
|
|
||||||
|
3. Serie inactiva
|
||||||
|
- desactivar una serie
|
||||||
|
- intentar asignar numeracion con esa serie
|
||||||
|
- comprobar que el caso falla y no incrementa `next_number`
|
||||||
|
|
||||||
|
4. Vigencia invalida
|
||||||
|
- configurar una serie con ventana fuera de fecha
|
||||||
|
- intentar asignar numeracion
|
||||||
|
- comprobar que el caso falla y no incrementa `next_number`
|
||||||
|
|
||||||
|
5. API canonica de series documentales
|
||||||
|
- llamar a `/document-series` filtrando `document_type = proforma`
|
||||||
|
- llamar a `/document-series` filtrando `document_type = issued_invoice`
|
||||||
|
- comprobar que la respuesta refleja filas activas de `document_series`
|
||||||
|
|
||||||
|
6. Endpoint legacy retirado
|
||||||
|
- llamar a `/catalogs/invoice-series`
|
||||||
|
- comprobar `404` o endpoint no registrado
|
||||||
|
|
||||||
|
## Pendiente conocido
|
||||||
|
|
||||||
|
`branchId` aun no se propaga desde `customer-invoices` hasta `assignNextNumber(...)`. Toda la validacion de esta fase debe asumirse en alcance de empresa.
|
||||||
|
|
||||||
|
## Estado Fase 2C en este workspace
|
||||||
|
|
||||||
|
- configuracion de desarrollo detectada: si
|
||||||
|
- ejecucion real de SQL desde Codex: no
|
||||||
|
- motivo: falta cliente `mysql` en la terminal y la dependencia `mysql2` del workspace no pudo abrirse por una dependencia transitiva ausente
|
||||||
105
docs/document-series/migration-from-customer-invoice-series.md
Normal file
105
docs/document-series/migration-from-customer-invoice-series.md
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# Migration from customer_invoice_series
|
||||||
|
|
||||||
|
## Equivalencia conceptual
|
||||||
|
|
||||||
|
- `customer_invoice_series.code` -> `document_series.code`
|
||||||
|
- `customer_invoice_series.company_id` -> `document_series.company_id`
|
||||||
|
- `customer_invoice_series.next_number` -> `document_series.next_number`
|
||||||
|
- `customer_invoice_series.padding_length` -> `document_series.padding`
|
||||||
|
- `customer_invoice_series.is_default` -> `document_series.is_default`
|
||||||
|
- `customer_invoice_series.is_active` -> `document_series.is_active`
|
||||||
|
- `document_series.document_type` para legacy de facturas emitidas será `issued_invoice`
|
||||||
|
|
||||||
|
## Migración conservadora de issued invoices
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO document_series (
|
||||||
|
id,
|
||||||
|
company_id,
|
||||||
|
branch_id,
|
||||||
|
document_type,
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
prefix,
|
||||||
|
suffix,
|
||||||
|
next_number,
|
||||||
|
padding,
|
||||||
|
valid_from,
|
||||||
|
valid_to,
|
||||||
|
is_default,
|
||||||
|
is_active,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
cis.id,
|
||||||
|
cis.company_id,
|
||||||
|
NULL,
|
||||||
|
'issued_invoice',
|
||||||
|
cis.code,
|
||||||
|
cis.code,
|
||||||
|
NULL,
|
||||||
|
CONCAT(cis.code, '-'),
|
||||||
|
NULL,
|
||||||
|
cis.next_number,
|
||||||
|
cis.padding_length,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
cis.is_default,
|
||||||
|
cis.is_active,
|
||||||
|
cis.created_at,
|
||||||
|
cis.updated_at
|
||||||
|
FROM customer_invoice_series cis
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM document_series ds
|
||||||
|
WHERE ds.company_id = cis.company_id
|
||||||
|
AND ds.document_type = 'issued_invoice'
|
||||||
|
AND ds.code = cis.code
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Series de proformas
|
||||||
|
|
||||||
|
Crear una serie por defecto por empresa con:
|
||||||
|
|
||||||
|
- `document_type = 'proforma'`
|
||||||
|
- `code = 'PF'`
|
||||||
|
- `prefix = 'PF-'`
|
||||||
|
- `next_number = 1`
|
||||||
|
- `padding = 6`
|
||||||
|
|
||||||
|
En Fase 2B la fuente preferida para este seed pasa a ser `companies`, no `customer_invoice_series`, para cubrir empresas activas sin series legacy previas.
|
||||||
|
|
||||||
|
## Cambio de assigner
|
||||||
|
|
||||||
|
- antes: `InvoiceSeriesNumberAssigner`
|
||||||
|
- después: `DocumentSeriesNumberAssigner` o `document-series:general.assignNextNumber`
|
||||||
|
|
||||||
|
## Estado actual
|
||||||
|
|
||||||
|
- `document_series` es la tabla canonica
|
||||||
|
- `GET /document-series` es la API canonica
|
||||||
|
- `/catalogs/invoice-series` ha sido retirado del runtime
|
||||||
|
- `CustomerInvoiceSeriesModel` y su repositorio legacy ya no participan en runtime
|
||||||
|
|
||||||
|
## Pendiente
|
||||||
|
|
||||||
|
- migración controlada de otros tipos documentales
|
||||||
|
- ejecucion manual y controlada del SQL conservador de migracion
|
||||||
|
- DDL previa de `proformas.document_series_id` y `proformas.proforma_number`
|
||||||
|
- propagacion real de `branchId` desde `customer-invoices`
|
||||||
|
|
||||||
|
## Validacion recomendada
|
||||||
|
|
||||||
|
- ejecutar `docs/document-series/sql/validate-document-series-migration.sql`
|
||||||
|
- revisar que no existan duplicados logicos por `company_id + document_type + code`
|
||||||
|
- revisar que cada empresa activa tenga una default de `proforma`
|
||||||
|
- revisar que ninguna proforma quede sin `proforma_reference`
|
||||||
|
|
||||||
|
## Estado de ejecucion Fase 2C
|
||||||
|
|
||||||
|
- se detecto una configuracion de desarrollo en `apps/server/.env.development`
|
||||||
|
- no se ejecuto SQL real desde esta terminal por bloqueo operativo del cliente/conector local
|
||||||
|
- produccion no se considero objetivo de ejecucion
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
-- Limpieza operativa V1.
|
||||||
|
-- NO ejecutar automaticamente desde codigo ni pipelines.
|
||||||
|
-- Ejecutar solo cuando:
|
||||||
|
-- 1) document_series contiene las series migradas necesarias
|
||||||
|
-- 2) /document-series funciona como API canonica
|
||||||
|
-- 3) no queda codigo runtime leyendo customer_invoice_series
|
||||||
|
-- 4) se ha validado create proforma e issue proforma
|
||||||
|
|
||||||
|
DROP TABLE customer_invoice_series;
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
-- Migracion conservadora e idempotente de customer_invoice_series a document_series.
|
||||||
|
-- No ejecutar automaticamente en produccion.
|
||||||
|
-- Revisar primero nombres de columnas y existencia de tabla document_series en el entorno destino.
|
||||||
|
|
||||||
|
-- 1. Migrar series legacy de facturas emitidas si no existen ya en document_series.
|
||||||
|
INSERT INTO document_series (
|
||||||
|
id,
|
||||||
|
company_id,
|
||||||
|
branch_id,
|
||||||
|
document_type,
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
prefix,
|
||||||
|
suffix,
|
||||||
|
next_number,
|
||||||
|
padding,
|
||||||
|
valid_from,
|
||||||
|
valid_to,
|
||||||
|
is_default,
|
||||||
|
is_active,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
deleted_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
cis.id,
|
||||||
|
cis.company_id,
|
||||||
|
NULL,
|
||||||
|
'issued_invoice',
|
||||||
|
cis.code,
|
||||||
|
cis.code,
|
||||||
|
NULL,
|
||||||
|
CONCAT(cis.code, '-'),
|
||||||
|
NULL,
|
||||||
|
cis.next_number,
|
||||||
|
cis.padding_length,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
cis.is_default,
|
||||||
|
cis.is_active,
|
||||||
|
cis.created_at,
|
||||||
|
cis.updated_at,
|
||||||
|
NULL
|
||||||
|
FROM customer_invoice_series cis
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM document_series ds
|
||||||
|
WHERE ds.company_id = cis.company_id
|
||||||
|
AND ds.document_type = 'issued_invoice'
|
||||||
|
AND ds.code = cis.code
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. Crear una serie por defecto de proformas por empresa solo si no existe ninguna.
|
||||||
|
-- Fuente preferida: companies. Esto evita depender de que exista customer_invoice_series
|
||||||
|
-- para empresas nuevas o para empresas sin series legacy de facturas emitidas.
|
||||||
|
INSERT INTO document_series (
|
||||||
|
id,
|
||||||
|
company_id,
|
||||||
|
branch_id,
|
||||||
|
document_type,
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
prefix,
|
||||||
|
suffix,
|
||||||
|
next_number,
|
||||||
|
padding,
|
||||||
|
valid_from,
|
||||||
|
valid_to,
|
||||||
|
is_default,
|
||||||
|
is_active,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
deleted_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
UUID(),
|
||||||
|
source.id,
|
||||||
|
NULL,
|
||||||
|
'proforma',
|
||||||
|
'PF',
|
||||||
|
'Proformas',
|
||||||
|
NULL,
|
||||||
|
'PF-',
|
||||||
|
NULL,
|
||||||
|
1,
|
||||||
|
6,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
TRUE,
|
||||||
|
TRUE,
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
NULL
|
||||||
|
FROM (
|
||||||
|
SELECT c.id
|
||||||
|
FROM companies c
|
||||||
|
WHERE c.status = 'active'
|
||||||
|
) AS source
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM document_series ds
|
||||||
|
WHERE ds.company_id = source.id
|
||||||
|
AND ds.document_type = 'proforma'
|
||||||
|
AND ds.code = 'PF'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. Validaciones manuales recomendadas.
|
||||||
|
-- SELECT company_id, document_type, code, next_number, padding, is_default, is_active
|
||||||
|
-- FROM document_series
|
||||||
|
-- ORDER BY company_id, document_type, code;
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
-- Validaciones manuales para la migracion conservadora a document_series.
|
||||||
|
-- No modifica datos.
|
||||||
|
|
||||||
|
-- 1. Series duplicadas de issued_invoice tras la migracion legacy.
|
||||||
|
SELECT
|
||||||
|
company_id,
|
||||||
|
document_type,
|
||||||
|
code,
|
||||||
|
COUNT(*) AS duplicated_rows
|
||||||
|
FROM document_series
|
||||||
|
WHERE document_type = 'issued_invoice'
|
||||||
|
GROUP BY company_id, document_type, code
|
||||||
|
HAVING COUNT(*) > 1;
|
||||||
|
|
||||||
|
-- 2. Empresas activas sin serie default de proforma.
|
||||||
|
SELECT c.id AS company_id
|
||||||
|
FROM companies c
|
||||||
|
LEFT JOIN document_series ds
|
||||||
|
ON ds.company_id = c.id
|
||||||
|
AND ds.document_type = 'proforma'
|
||||||
|
AND ds.is_default = TRUE
|
||||||
|
AND ds.deleted_at IS NULL
|
||||||
|
WHERE c.status = 'active'
|
||||||
|
GROUP BY c.id
|
||||||
|
HAVING COUNT(ds.id) = 0;
|
||||||
|
|
||||||
|
-- 3. Duplicados logicos por company/type/code.
|
||||||
|
SELECT
|
||||||
|
company_id,
|
||||||
|
document_type,
|
||||||
|
code,
|
||||||
|
COUNT(*) AS duplicated_rows
|
||||||
|
FROM document_series
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
GROUP BY company_id, document_type, code
|
||||||
|
HAVING COUNT(*) > 1;
|
||||||
|
|
||||||
|
-- 4. Contadores invalidos.
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
company_id,
|
||||||
|
document_type,
|
||||||
|
code,
|
||||||
|
next_number,
|
||||||
|
padding
|
||||||
|
FROM document_series
|
||||||
|
WHERE next_number < 1
|
||||||
|
OR padding < 1;
|
||||||
|
|
||||||
|
-- 5. Ventanas de vigencia incoherentes.
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
company_id,
|
||||||
|
document_type,
|
||||||
|
code,
|
||||||
|
valid_from,
|
||||||
|
valid_to
|
||||||
|
FROM document_series
|
||||||
|
WHERE valid_from IS NOT NULL
|
||||||
|
AND valid_to IS NOT NULL
|
||||||
|
AND valid_from > valid_to;
|
||||||
|
|
||||||
|
-- 6. Proformas sin referencia visible.
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
company_id,
|
||||||
|
document_series_id,
|
||||||
|
proforma_number,
|
||||||
|
proforma_reference
|
||||||
|
FROM proformas
|
||||||
|
WHERE proforma_reference IS NULL
|
||||||
|
OR TRIM(proforma_reference) = '';
|
||||||
@ -43,6 +43,7 @@
|
|||||||
"@erp/core": "workspace:*",
|
"@erp/core": "workspace:*",
|
||||||
"@erp/catalogs": "workspace:*",
|
"@erp/catalogs": "workspace:*",
|
||||||
"@erp/customers": "workspace:*",
|
"@erp/customers": "workspace:*",
|
||||||
|
"@erp/document-series": "workspace:*",
|
||||||
"@erp/identity": "workspace:*",
|
"@erp/identity": "workspace:*",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@lglab/react-qr-code": "^1.4.10",
|
"@lglab/react-qr-code": "^1.4.10",
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
export * from "./invoice-series-finder.di";
|
|
||||||
export * from "./invoice-series-number-assigner.di";
|
|
||||||
export * from "./invoice-series-snapshot-builders.di";
|
|
||||||
export * from "./invoice-series-use-cases.di";
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import type { IInvoiceSeriesRepository } from "../repositories";
|
|
||||||
import { type IInvoiceSeriesFinder, InvoiceSeriesFinder } from "../services";
|
|
||||||
|
|
||||||
export function buildInvoiceSeriesFinder(
|
|
||||||
repository: IInvoiceSeriesRepository
|
|
||||||
): IInvoiceSeriesFinder {
|
|
||||||
return new InvoiceSeriesFinder(repository);
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import type { IInvoiceSeriesRepository } from "../repositories";
|
|
||||||
import {
|
|
||||||
type IInvoiceSeriesNumberAssigner,
|
|
||||||
InvoiceSeriesNumberAssigner,
|
|
||||||
} from "../services";
|
|
||||||
|
|
||||||
export const buildInvoiceSeriesNumberAssigner = (
|
|
||||||
repository: IInvoiceSeriesRepository
|
|
||||||
): IInvoiceSeriesNumberAssigner => new InvoiceSeriesNumberAssigner(repository);
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { InvoiceSeriesSummarySnapshotBuilder } from "../snapshot-builders";
|
|
||||||
|
|
||||||
export function buildInvoiceSeriesSnapshotBuilders() {
|
|
||||||
return {
|
|
||||||
summary: new InvoiceSeriesSummarySnapshotBuilder(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import type { ITransactionManager } from "@erp/core/api";
|
|
||||||
|
|
||||||
import type { IInvoiceSeriesFinder } from "../services";
|
|
||||||
import type { IInvoiceSeriesSummarySnapshotBuilder } from "../snapshot-builders";
|
|
||||||
import { ListInvoiceSeriesUseCase } from "../use-cases";
|
|
||||||
|
|
||||||
export function buildListInvoiceSeriesUseCase(deps: {
|
|
||||||
finder: IInvoiceSeriesFinder;
|
|
||||||
summarySnapshotBuilder: IInvoiceSeriesSummarySnapshotBuilder;
|
|
||||||
transactionManager: ITransactionManager;
|
|
||||||
}) {
|
|
||||||
return new ListInvoiceSeriesUseCase(
|
|
||||||
deps.finder,
|
|
||||||
deps.summarySnapshotBuilder,
|
|
||||||
deps.transactionManager
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,6 +1,2 @@
|
|||||||
export * from "./di";
|
|
||||||
export * from "./errors";
|
export * from "./errors";
|
||||||
export * from "./repositories";
|
|
||||||
export * from "./services";
|
export * from "./services";
|
||||||
export * from "./snapshot-builders";
|
|
||||||
export * from "./use-cases";
|
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./invoice-series-repository.interface";
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
|
||||||
import type { Collection, Maybe, Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
import type { InvoiceSeries, InvoiceSeriesCode } from "../../../domain";
|
|
||||||
|
|
||||||
export interface IInvoiceSeriesRepository {
|
|
||||||
findActiveByCompany(params: {
|
|
||||||
companyId: UniqueID;
|
|
||||||
transaction?: unknown;
|
|
||||||
}): Promise<Result<Collection<InvoiceSeries>, Error>>;
|
|
||||||
|
|
||||||
findByCodeInCompany(params: {
|
|
||||||
companyId: UniqueID;
|
|
||||||
invoiceSeriesCode: InvoiceSeriesCode;
|
|
||||||
transaction?: unknown;
|
|
||||||
}): Promise<Result<Maybe<InvoiceSeries>, Error>>;
|
|
||||||
|
|
||||||
findByCodeInCompanyForUpdate(params: {
|
|
||||||
companyId: UniqueID;
|
|
||||||
invoiceSeriesCode: InvoiceSeriesCode;
|
|
||||||
transaction: unknown;
|
|
||||||
}): Promise<Result<Maybe<InvoiceSeries>, Error>>;
|
|
||||||
|
|
||||||
update(invoiceSeries: InvoiceSeries, transaction?: unknown): Promise<Result<void, Error>>;
|
|
||||||
}
|
|
||||||
@ -1,2 +1 @@
|
|||||||
export * from "./invoice-series-finder";
|
|
||||||
export * from "./invoice-series-number-assigner";
|
export * from "./invoice-series-number-assigner";
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
|
||||||
import type { Collection, Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
import type { InvoiceSeries } from "../../../domain";
|
|
||||||
import type { IInvoiceSeriesRepository } from "../repositories";
|
|
||||||
|
|
||||||
export interface IInvoiceSeriesFinder {
|
|
||||||
findActiveByCompany(
|
|
||||||
companyId: UniqueID,
|
|
||||||
transaction?: unknown
|
|
||||||
): Promise<Result<Collection<InvoiceSeries>, Error>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class InvoiceSeriesFinder implements IInvoiceSeriesFinder {
|
|
||||||
public constructor(private readonly repository: IInvoiceSeriesRepository) {}
|
|
||||||
|
|
||||||
public findActiveByCompany(companyId: UniqueID, transaction?: unknown) {
|
|
||||||
return this.repository.findActiveByCompany({
|
|
||||||
companyId,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +1,18 @@
|
|||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import { type InvoiceNumber, type InvoiceSeriesCode } from "../../../domain";
|
import type { InvoiceNumber, InvoiceSerie } from "../../../domain";
|
||||||
import {
|
|
||||||
InvoiceSeriesInactiveError,
|
|
||||||
InvoiceSeriesNotFoundError,
|
|
||||||
InvoiceSeriesTransactionRequiredError,
|
|
||||||
} from "../errors";
|
|
||||||
import type { IInvoiceSeriesRepository } from "../repositories";
|
|
||||||
|
|
||||||
export interface InvoiceSeriesAssignment {
|
export interface InvoiceSeriesAssignment {
|
||||||
invoiceNumber: InvoiceNumber;
|
invoiceNumber: InvoiceNumber;
|
||||||
formattedNumber: string;
|
formattedNumber: string;
|
||||||
invoiceSeriesId: string;
|
invoiceSeriesId: string;
|
||||||
invoiceSeriesCode: string;
|
invoiceSeriesCode: InvoiceSerie;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InvoiceSeriesNumberAssignerParams {
|
export interface InvoiceSeriesNumberAssignerParams {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
invoiceSeriesCode: InvoiceSeriesCode;
|
invoiceSeriesCode: Maybe<InvoiceSerie>;
|
||||||
transaction: unknown;
|
transaction: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,50 +21,3 @@ export interface IInvoiceSeriesNumberAssigner {
|
|||||||
params: InvoiceSeriesNumberAssignerParams
|
params: InvoiceSeriesNumberAssignerParams
|
||||||
): Promise<Result<InvoiceSeriesAssignment, Error>>;
|
): Promise<Result<InvoiceSeriesAssignment, Error>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InvoiceSeriesNumberAssigner implements IInvoiceSeriesNumberAssigner {
|
|
||||||
public constructor(private readonly repository: IInvoiceSeriesRepository) {}
|
|
||||||
|
|
||||||
public async assignNextNumber(
|
|
||||||
params: InvoiceSeriesNumberAssignerParams
|
|
||||||
): Promise<Result<InvoiceSeriesAssignment, Error>> {
|
|
||||||
if (!params.transaction) {
|
|
||||||
return Result.fail(new InvoiceSeriesTransactionRequiredError());
|
|
||||||
}
|
|
||||||
|
|
||||||
const invoiceSeriesResult = await this.repository.findByCodeInCompanyForUpdate(params);
|
|
||||||
|
|
||||||
if (invoiceSeriesResult.isFailure) {
|
|
||||||
return Result.fail(invoiceSeriesResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invoiceSeriesResult.data.isNone()) {
|
|
||||||
return Result.fail(new InvoiceSeriesNotFoundError());
|
|
||||||
}
|
|
||||||
|
|
||||||
const invoiceSeries = invoiceSeriesResult.data.unwrap();
|
|
||||||
|
|
||||||
if (!invoiceSeries.isActive) {
|
|
||||||
return Result.fail(new InvoiceSeriesInactiveError(invoiceSeries.code.toPrimitive()));
|
|
||||||
}
|
|
||||||
|
|
||||||
const invoiceNumberResult = invoiceSeries.assignNextInvoiceNumber();
|
|
||||||
|
|
||||||
if (invoiceNumberResult.isFailure) {
|
|
||||||
return Result.fail(invoiceNumberResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateResult = await this.repository.update(invoiceSeries, params.transaction);
|
|
||||||
|
|
||||||
if (updateResult.isFailure) {
|
|
||||||
return Result.fail(updateResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok({
|
|
||||||
invoiceNumber: invoiceNumberResult.data,
|
|
||||||
formattedNumber: invoiceSeries.formatInvoiceNumber(invoiceNumberResult.data),
|
|
||||||
invoiceSeriesId: invoiceSeries.id.toString(),
|
|
||||||
invoiceSeriesCode: invoiceSeries.code.toPrimitive(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./summary";
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./invoice-series-summary.snapshot-builder";
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
|
||||||
|
|
||||||
import type { InvoiceSeriesSummaryDTO } from "../../../../../common";
|
|
||||||
import type { InvoiceSeries } from "../../../../domain";
|
|
||||||
|
|
||||||
export interface IInvoiceSeriesSummarySnapshotBuilder
|
|
||||||
extends ISnapshotBuilder<InvoiceSeries, InvoiceSeriesSummaryDTO> {}
|
|
||||||
|
|
||||||
export class InvoiceSeriesSummarySnapshotBuilder
|
|
||||||
implements IInvoiceSeriesSummarySnapshotBuilder
|
|
||||||
{
|
|
||||||
public toOutput(invoiceSeries: InvoiceSeries): InvoiceSeriesSummaryDTO {
|
|
||||||
return {
|
|
||||||
id: invoiceSeries.id.toString(),
|
|
||||||
code: invoiceSeries.code.toPrimitive(),
|
|
||||||
next_number: invoiceSeries.nextNumber.toPrimitive(),
|
|
||||||
padding_length: invoiceSeries.paddingLength.toPrimitive(),
|
|
||||||
is_default: invoiceSeries.isDefault,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./list-invoice-series.use-case";
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import type { ITransactionManager } from "@erp/core/api";
|
|
||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
import type { IInvoiceSeriesFinder } from "../services";
|
|
||||||
import type { IInvoiceSeriesSummarySnapshotBuilder } from "../snapshot-builders";
|
|
||||||
|
|
||||||
type ListInvoiceSeriesUseCaseInput = {
|
|
||||||
companyId: UniqueID;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ListInvoiceSeriesUseCase {
|
|
||||||
public constructor(
|
|
||||||
private readonly finder: IInvoiceSeriesFinder,
|
|
||||||
private readonly summarySnapshotBuilder: IInvoiceSeriesSummarySnapshotBuilder,
|
|
||||||
private readonly transactionManager: ITransactionManager
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public execute(params: ListInvoiceSeriesUseCaseInput) {
|
|
||||||
const { companyId } = params;
|
|
||||||
|
|
||||||
return this.transactionManager.complete(async (transaction: unknown) => {
|
|
||||||
try {
|
|
||||||
const result = await this.finder.findActiveByCompany(companyId, transaction);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Result.fail(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = result.data.map((invoiceSeries) =>
|
|
||||||
this.summarySnapshotBuilder.toOutput(invoiceSeries)
|
|
||||||
);
|
|
||||||
|
|
||||||
return Result.ok({ items });
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,17 +2,16 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type IIssuedInvoiceCreateProps,
|
|
||||||
InvoiceSeriesCode,
|
|
||||||
IssuedInvoice,
|
IssuedInvoice,
|
||||||
} from "../../../domain";
|
} from "../../../domain";
|
||||||
import type { IInvoiceSeriesNumberAssigner } from "../../invoice-series";
|
import type { IInvoiceSeriesNumberAssigner } from "../../invoice-series";
|
||||||
import type { IIssuedInvoiceRepository } from "../repositories";
|
import type { IIssuedInvoiceRepository } from "../repositories";
|
||||||
|
import type { ProformaToIssuedInvoiceCreateProps } from "./proforma-to-issued-invoice-props-converter";
|
||||||
|
|
||||||
export interface IIssuedInvoiceCreatorParams {
|
export interface IIssuedInvoiceCreatorParams {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
id: UniqueID;
|
id: UniqueID;
|
||||||
props: Omit<IIssuedInvoiceCreateProps, "invoiceNumber">;
|
props: ProformaToIssuedInvoiceCreateProps;
|
||||||
transaction: unknown;
|
transaction: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,15 +36,9 @@ export class IssuedInvoiceCreator implements IIssuedInvoiceCreator {
|
|||||||
async create(params: IIssuedInvoiceCreatorParams): Promise<Result<IssuedInvoice, Error>> {
|
async create(params: IIssuedInvoiceCreatorParams): Promise<Result<IssuedInvoice, Error>> {
|
||||||
const { companyId, id, props, transaction } = params;
|
const { companyId, id, props, transaction } = params;
|
||||||
|
|
||||||
const invoiceSeriesCodeResult = InvoiceSeriesCode.create(props.series.toPrimitive());
|
|
||||||
|
|
||||||
if (invoiceSeriesCodeResult.isFailure) {
|
|
||||||
return Result.fail(invoiceSeriesCodeResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const numberResult = await this.invoiceSeriesNumberAssigner.assignNextNumber({
|
const numberResult = await this.invoiceSeriesNumberAssigner.assignNextNumber({
|
||||||
companyId,
|
companyId,
|
||||||
invoiceSeriesCode: invoiceSeriesCodeResult.data,
|
invoiceSeriesCode: props.series,
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -55,7 +48,15 @@ export class IssuedInvoiceCreator implements IIssuedInvoiceCreator {
|
|||||||
|
|
||||||
const invoiceNumber = numberResult.data.invoiceNumber;
|
const invoiceNumber = numberResult.data.invoiceNumber;
|
||||||
|
|
||||||
const invoiceResult = IssuedInvoice.create({ ...props, invoiceNumber, companyId }, id);
|
const invoiceResult = IssuedInvoice.create(
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
series: numberResult.data.invoiceSeriesCode,
|
||||||
|
invoiceNumber,
|
||||||
|
companyId,
|
||||||
|
},
|
||||||
|
id
|
||||||
|
);
|
||||||
|
|
||||||
if (invoiceResult.isFailure) {
|
if (invoiceResult.isFailure) {
|
||||||
return Result.fail(invoiceResult.error);
|
return Result.fail(invoiceResult.error);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { Maybe, Result } from "@repo/rdx-utils";
|
|||||||
import {
|
import {
|
||||||
type IIssuedInvoiceCreateProps,
|
type IIssuedInvoiceCreateProps,
|
||||||
InvoicePaymentMethod,
|
InvoicePaymentMethod,
|
||||||
|
type InvoiceSerie,
|
||||||
InvoiceStatus,
|
InvoiceStatus,
|
||||||
InvoiceTaxRegime,
|
InvoiceTaxRegime,
|
||||||
IssuedInvoiceItem,
|
IssuedInvoiceItem,
|
||||||
@ -16,7 +17,12 @@ import {
|
|||||||
} from "../../../domain";
|
} from "../../../domain";
|
||||||
import type { ProformaIssueReadModel } from "../models";
|
import type { ProformaIssueReadModel } from "../models";
|
||||||
|
|
||||||
export type ProformaToIssuedInvoiceCreateProps = Omit<IIssuedInvoiceCreateProps, "invoiceNumber">;
|
export type ProformaToIssuedInvoiceCreateProps = Omit<
|
||||||
|
IIssuedInvoiceCreateProps,
|
||||||
|
"invoiceNumber" | "series"
|
||||||
|
> & {
|
||||||
|
series: Maybe<InvoiceSerie>;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IProformaToIssuedInvoiceConverter {
|
export interface IProformaToIssuedInvoiceConverter {
|
||||||
toCreateProps(source: ProformaIssueReadModel): Result<ProformaToIssuedInvoiceCreateProps, Error>;
|
toCreateProps(source: ProformaIssueReadModel): Result<ProformaToIssuedInvoiceCreateProps, Error>;
|
||||||
@ -70,7 +76,7 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
|
|||||||
companyId: proforma.companyId,
|
companyId: proforma.companyId,
|
||||||
status: InvoiceStatus.issued(),
|
status: InvoiceStatus.issued(),
|
||||||
|
|
||||||
series: proforma.series.getOrUndefined()!,
|
series: proforma.series,
|
||||||
linkedProformaId: proforma.id,
|
linkedProformaId: proforma.id,
|
||||||
|
|
||||||
// La fecha de factura debe reflejar la emisión, no la fecha original de la proforma.
|
// La fecha de factura debe reflejar la emisión, no la fecha original de la proforma.
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import {
|
|||||||
type IProformaCreator,
|
type IProformaCreator,
|
||||||
type IProformaNumberGenerator,
|
type IProformaNumberGenerator,
|
||||||
ProformaCreator,
|
ProformaCreator,
|
||||||
|
type ProformaSeriesValidator,
|
||||||
|
type ProformaTargetInvoiceSeriesValidator,
|
||||||
type ProformaTaxResolver,
|
type ProformaTaxResolver,
|
||||||
} from "../services";
|
} from "../services";
|
||||||
|
|
||||||
@ -12,6 +14,8 @@ export const buildProformaCreator = (params: {
|
|||||||
paymentResolver: ProformaPaymentResolver;
|
paymentResolver: ProformaPaymentResolver;
|
||||||
numberService: IProformaNumberGenerator;
|
numberService: IProformaNumberGenerator;
|
||||||
repository: IProformaRepository;
|
repository: IProformaRepository;
|
||||||
|
proformaSeriesValidator: ProformaSeriesValidator;
|
||||||
|
targetInvoiceSeriesValidator: ProformaTargetInvoiceSeriesValidator;
|
||||||
}): IProformaCreator => {
|
}): IProformaCreator => {
|
||||||
return new ProformaCreator(params);
|
return new ProformaCreator(params);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,12 +1,18 @@
|
|||||||
// modules/customer-invoices/src/api/application/proformas/di/proforma-updater.di.ts
|
// modules/customer-invoices/src/api/application/proformas/di/proforma-updater.di.ts
|
||||||
import type { IProformaRepository } from "../repositories";
|
import type { IProformaRepository } from "../repositories";
|
||||||
import type { ProformaPaymentResolver } from "../services";
|
import type { ProformaPaymentResolver } from "../services";
|
||||||
import { type IProformaUpdater, type ProformaTaxResolver, ProformaUpdater } from "../services";
|
import {
|
||||||
|
type IProformaUpdater,
|
||||||
|
type ProformaTargetInvoiceSeriesValidator,
|
||||||
|
type ProformaTaxResolver,
|
||||||
|
ProformaUpdater,
|
||||||
|
} from "../services";
|
||||||
|
|
||||||
export const buildProformaUpdater = (params: {
|
export const buildProformaUpdater = (params: {
|
||||||
repository: IProformaRepository;
|
repository: IProformaRepository;
|
||||||
taxResolver: ProformaTaxResolver;
|
taxResolver: ProformaTaxResolver;
|
||||||
paymentResolver: ProformaPaymentResolver;
|
paymentResolver: ProformaPaymentResolver;
|
||||||
|
targetInvoiceSeriesValidator: ProformaTargetInvoiceSeriesValidator;
|
||||||
}): IProformaUpdater => {
|
}): IProformaUpdater => {
|
||||||
return new ProformaUpdater(params);
|
return new ProformaUpdater(params);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -58,9 +58,15 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
|||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const series = extractOrPushError(
|
const proformaSeriesCode = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.series, (value) => InvoiceSerie.create(value)),
|
maybeFromNullableResult(dto.proforma_series_code, (value) => InvoiceSerie.create(value)),
|
||||||
"series",
|
"proforma_series_code",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const targetInvoiceSeriesCode = extractOrPushError(
|
||||||
|
maybeFromNullableResult(dto.target_invoice_series_code, (value) => InvoiceSerie.create(value)),
|
||||||
|
"target_invoice_series_code",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -126,7 +132,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
|||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const items = this.mapItemsProps(dto.items, {
|
const items = this.mapItemsProps(dto.items ?? [], {
|
||||||
languageCode: languageCode!,
|
languageCode: languageCode!,
|
||||||
currencyCode: currencyCode!,
|
currencyCode: currencyCode!,
|
||||||
globalDiscountPercentage: globalDiscountPercentage!,
|
globalDiscountPercentage: globalDiscountPercentage!,
|
||||||
@ -139,8 +145,8 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
|||||||
companyId: params.companyId,
|
companyId: params.companyId,
|
||||||
status: InvoiceStatus.draft(),
|
status: InvoiceStatus.draft(),
|
||||||
|
|
||||||
//invoiceNumber: invoiceNumber!,
|
proformaSeriesCode: proformaSeriesCode!,
|
||||||
series: series!,
|
targetInvoiceSeriesCode: targetInvoiceSeriesCode!,
|
||||||
|
|
||||||
proformaDate: proformaDate!,
|
proformaDate: proformaDate!,
|
||||||
operationDate: operationDate!,
|
operationDate: operationDate!,
|
||||||
@ -283,4 +289,5 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
|||||||
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
|
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,10 +55,10 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
|
|||||||
const errors: ValidationErrorDetail[] = [];
|
const errors: ValidationErrorDetail[] = [];
|
||||||
const proformaPatchProps: ProformaPatchInputProps = {};
|
const proformaPatchProps: ProformaPatchInputProps = {};
|
||||||
|
|
||||||
toPatchField(dto.series).ifSet((series) => {
|
toPatchField(dto.target_invoice_series_code).ifSet((targetInvoiceSeriesCode) => {
|
||||||
proformaPatchProps.series = extractOrPushError(
|
proformaPatchProps.targetInvoiceSeriesCode = extractOrPushError(
|
||||||
maybeFromNullableResult(series, (value) => InvoiceSerie.create(value)),
|
maybeFromNullableResult(targetInvoiceSeriesCode, (value) => InvoiceSerie.create(value)),
|
||||||
"series",
|
"target_invoice_series_code",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -262,4 +262,5 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
|
|||||||
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
|
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import type { IProformaCreateProps, IProformaItemCreateProps } from "../../../domain";
|
import type { IProformaCreateProps, IProformaItemCreateProps } from "../../../domain";
|
||||||
|
import type { Maybe } from "@repo/rdx-utils";
|
||||||
|
import type { InvoiceSerie } from "../../../domain";
|
||||||
|
|
||||||
import type { ProformaItemTaxCodesInput } from "./proforma-item-tax-codes-input.model";
|
import type { ProformaItemTaxCodesInput } from "./proforma-item-tax-codes-input.model";
|
||||||
|
|
||||||
@ -12,7 +14,12 @@ import type { ProformaItemTaxCodesInput } from "./proforma-item-tax-codes-input.
|
|||||||
* - Se encargan de validar y convertir los datos recibidos en formatos adecuados para el dominio.
|
* - Se encargan de validar y convertir los datos recibidos en formatos adecuados para el dominio.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ProformaCreateInputProps = Omit<IProformaCreateProps, "items" | "proformaReference"> & {
|
export type ProformaCreateInputProps = Omit<
|
||||||
|
IProformaCreateProps,
|
||||||
|
"items" | "proformaReference" | "proformaNumber" | "documentSeriesId" | "series"
|
||||||
|
> & {
|
||||||
|
proformaSeriesCode: Maybe<InvoiceSerie>;
|
||||||
|
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
|
||||||
items: ProformaItemCreateInputProps[];
|
items: ProformaItemCreateInputProps[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import type { ProformaItemPatchProps, ProformaPatchProps } from "../../../domain";
|
import type { ProformaItemPatchProps, ProformaPatchProps } from "../../../domain";
|
||||||
|
import type { Maybe } from "@repo/rdx-utils";
|
||||||
|
import type { InvoiceSerie } from "../../../domain";
|
||||||
|
|
||||||
import type { ProformaItemTaxCodesInput } from "./proforma-item-tax-codes-input.model";
|
import type { ProformaItemTaxCodesInput } from "./proforma-item-tax-codes-input.model";
|
||||||
|
|
||||||
@ -12,7 +14,8 @@ import type { ProformaItemTaxCodesInput } from "./proforma-item-tax-codes-input.
|
|||||||
* - Se encargan de validar y convertir los datos recibidos en formatos adecuados para el dominio.
|
* - Se encargan de validar y convertir los datos recibidos en formatos adecuados para el dominio.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ProformaPatchInputProps = Omit<ProformaPatchProps, "items"> & {
|
export type ProformaPatchInputProps = Omit<ProformaPatchProps, "items" | "series"> & {
|
||||||
|
targetInvoiceSeriesCode?: Maybe<InvoiceSerie>;
|
||||||
items?: ProformaItemPatchInputProps[];
|
items?: ProformaItemPatchInputProps[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -8,5 +8,7 @@ export * from "./proforma-finder";
|
|||||||
export * from "./proforma-issuer";
|
export * from "./proforma-issuer";
|
||||||
export * from "./proforma-number-generator.interface";
|
export * from "./proforma-number-generator.interface";
|
||||||
export * from "./proforma-public-services.interface";
|
export * from "./proforma-public-services.interface";
|
||||||
|
export * from "./proforma-series-validator.interface";
|
||||||
export * from "./proforma-status-changer";
|
export * from "./proforma-status-changer";
|
||||||
|
export * from "./proforma-target-invoice-series-validator.interface";
|
||||||
export * from "./proforma-updater";
|
export * from "./proforma-updater";
|
||||||
|
|||||||
@ -4,12 +4,15 @@ import { Maybe, Result } from "@repo/rdx-utils";
|
|||||||
import {
|
import {
|
||||||
type IProformaCreateProps,
|
type IProformaCreateProps,
|
||||||
type IProformaItemCreateProps,
|
type IProformaItemCreateProps,
|
||||||
|
InvoiceNumber,
|
||||||
Proforma,
|
Proforma,
|
||||||
} from "../../../domain";
|
} from "../../../domain";
|
||||||
import type { ProformaCreateInputProps } from "../models";
|
import type { ProformaCreateInputProps } from "../models";
|
||||||
import type { IProformaRepository } from "../repositories";
|
import type { IProformaRepository } from "../repositories";
|
||||||
|
|
||||||
import type { ProformaPaymentResolver } from "./catalog-resolver/proforma-payment-resolver";
|
import type { ProformaPaymentResolver } from "./catalog-resolver/proforma-payment-resolver";
|
||||||
|
import type { ProformaSeriesValidator } from "./proforma-series-validator.interface";
|
||||||
|
import type { ProformaTargetInvoiceSeriesValidator } from "./proforma-target-invoice-series-validator.interface";
|
||||||
import type { ProformaTaxResolver } from "./catalog-resolver/proforma-tax-resolver";
|
import type { ProformaTaxResolver } from "./catalog-resolver/proforma-tax-resolver";
|
||||||
import type { IProformaNumberGenerator } from "./proforma-number-generator.interface";
|
import type { IProformaNumberGenerator } from "./proforma-number-generator.interface";
|
||||||
|
|
||||||
@ -31,6 +34,8 @@ export class ProformaCreator implements IProformaCreator {
|
|||||||
numberService: IProformaNumberGenerator;
|
numberService: IProformaNumberGenerator;
|
||||||
taxResolver: ProformaTaxResolver;
|
taxResolver: ProformaTaxResolver;
|
||||||
paymentResolver: ProformaPaymentResolver;
|
paymentResolver: ProformaPaymentResolver;
|
||||||
|
proformaSeriesValidator: ProformaSeriesValidator;
|
||||||
|
targetInvoiceSeriesValidator: ProformaTargetInvoiceSeriesValidator;
|
||||||
}
|
}
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -43,11 +48,21 @@ export class ProformaCreator implements IProformaCreator {
|
|||||||
return Result.fail(resolvedProps.error);
|
return Result.fail(resolvedProps.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const proformaSeriesValidationResult =
|
||||||
|
await this.deps.proformaSeriesValidator.ensureValidProformaSeries({
|
||||||
|
companyId,
|
||||||
|
proformaSeriesCode: resolvedProps.data.proformaSeriesCode,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (proformaSeriesValidationResult.isFailure) {
|
||||||
|
return Result.fail(proformaSeriesValidationResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Obtener siguiente número
|
// 1. Obtener siguiente número
|
||||||
const { series } = props;
|
|
||||||
const numberResult = await this.deps.numberService.getNextForCompany(
|
const numberResult = await this.deps.numberService.getNextForCompany(
|
||||||
companyId,
|
companyId,
|
||||||
series,
|
resolvedProps.data.proformaSeriesCode,
|
||||||
transaction
|
transaction
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -55,11 +70,41 @@ export class ProformaCreator implements IProformaCreator {
|
|||||||
return Result.fail(numberResult.error);
|
return Result.fail(numberResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const proformaReference = numberResult.data;
|
const targetInvoiceSeriesValidationResult =
|
||||||
|
await this.deps.targetInvoiceSeriesValidator.ensureValidIssuedInvoiceSeries({
|
||||||
|
companyId,
|
||||||
|
targetInvoiceSeriesCode: resolvedProps.data.targetInvoiceSeriesCode,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (targetInvoiceSeriesValidationResult.isFailure) {
|
||||||
|
return Result.fail(targetInvoiceSeriesValidationResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proformaReferenceResult = InvoiceNumber.create(numberResult.data.reference);
|
||||||
|
|
||||||
|
if (proformaReferenceResult.isFailure) {
|
||||||
|
return Result.fail(proformaReferenceResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proformaNumberResult = InvoiceNumber.create(numberResult.data.number);
|
||||||
|
|
||||||
|
if (proformaNumberResult.isFailure) {
|
||||||
|
return Result.fail(proformaNumberResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proformaReference = proformaReferenceResult.data;
|
||||||
|
|
||||||
// 2. Crear agregado
|
// 2. Crear agregado
|
||||||
const proformaResult = Proforma.create(
|
const proformaResult = Proforma.create(
|
||||||
{ ...resolvedProps.data, proformaReference, companyId },
|
{
|
||||||
|
...resolvedProps.data,
|
||||||
|
companyId,
|
||||||
|
documentSeriesId: Maybe.some(numberResult.data.documentSeriesId),
|
||||||
|
proformaNumber: Maybe.some(proformaNumberResult.data),
|
||||||
|
proformaReference,
|
||||||
|
series: resolvedProps.data.targetInvoiceSeriesCode,
|
||||||
|
},
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -88,7 +133,9 @@ export class ProformaCreator implements IProformaCreator {
|
|||||||
|
|
||||||
private async resolveCreateProps(
|
private async resolveCreateProps(
|
||||||
props: ProformaCreateInputProps
|
props: ProformaCreateInputProps
|
||||||
): Promise<Result<Omit<IProformaCreateProps, "proformaReference">, Error>> {
|
): Promise<
|
||||||
|
Result<Omit<IProformaCreateProps, "proformaReference" | "proformaNumber" | "documentSeriesId">, Error>
|
||||||
|
> {
|
||||||
// TODO: Esto hay que cambiarlo en el futuro para buscar valores por defecto, desde la empresa o desde el cliente
|
// TODO: Esto hay que cambiarlo en el futuro para buscar valores por defecto, desde la empresa o desde el cliente
|
||||||
const _newProps = {
|
const _newProps = {
|
||||||
...props,
|
...props,
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
import type { InvoiceNumber, InvoiceSerie } from "@erp/customer-invoices/api/domain";
|
import type { InvoiceSerie } from "@erp/customer-invoices/api/domain";
|
||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import type { Maybe, Result } from "@repo/rdx-utils";
|
import type { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
export type ProformaNumberAssignment = {
|
||||||
|
documentSeriesId: string;
|
||||||
|
documentSeriesCode: string;
|
||||||
|
number: string;
|
||||||
|
reference: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Servicio de dominio que define cómo se genera el siguiente número de factura.
|
* Servicio de dominio que define cómo se genera el siguiente número de factura.
|
||||||
*/
|
*/
|
||||||
@ -15,7 +22,7 @@ export interface IProformaNumberGenerator {
|
|||||||
*/
|
*/
|
||||||
getNextForCompany(
|
getNextForCompany(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
series: Maybe<InvoiceSerie>,
|
proformaSeriesCode: Maybe<InvoiceSerie>,
|
||||||
transaction: any
|
transaction: any
|
||||||
): Promise<Result<InvoiceNumber, Error>>;
|
): Promise<Result<ProformaNumberAssignment, Error>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import type { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { InvoiceSerie } from "../../../domain";
|
||||||
|
|
||||||
|
export interface EnsureValidProformaSeriesParams {
|
||||||
|
companyId: UniqueID;
|
||||||
|
proformaSeriesCode: Maybe<InvoiceSerie>;
|
||||||
|
transaction?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProformaSeriesValidator {
|
||||||
|
ensureValidProformaSeries(params: EnsureValidProformaSeriesParams): Promise<Result<void, Error>>;
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import type { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { InvoiceSerie } from "../../../domain";
|
||||||
|
|
||||||
|
export interface EnsureValidIssuedInvoiceSeriesParams {
|
||||||
|
companyId: UniqueID;
|
||||||
|
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
|
||||||
|
transaction?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProformaTargetInvoiceSeriesValidator {
|
||||||
|
ensureValidIssuedInvoiceSeries(
|
||||||
|
params: EnsureValidIssuedInvoiceSeriesParams
|
||||||
|
): Promise<Result<void, Error>>;
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type { UniqueID, UtcDate } from "@repo/rdx-ddd";
|
import { DomainValidationError, type UniqueID, type UtcDate } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { Proforma, ProformaItemPatchProps, ProformaPatchProps } from "../../../domain";
|
import type { Proforma, ProformaItemPatchProps, ProformaPatchProps } from "../../../domain";
|
||||||
@ -7,6 +7,7 @@ import type { IProformaRepository } from "../repositories";
|
|||||||
|
|
||||||
import type { ProformaPaymentResolver } from "./catalog-resolver/proforma-payment-resolver";
|
import type { ProformaPaymentResolver } from "./catalog-resolver/proforma-payment-resolver";
|
||||||
import type { ProformaTaxResolver } from "./catalog-resolver/proforma-tax-resolver";
|
import type { ProformaTaxResolver } from "./catalog-resolver/proforma-tax-resolver";
|
||||||
|
import type { ProformaTargetInvoiceSeriesValidator } from "./proforma-target-invoice-series-validator.interface";
|
||||||
|
|
||||||
export interface IProformaUpdater {
|
export interface IProformaUpdater {
|
||||||
update(params: {
|
update(params: {
|
||||||
@ -23,6 +24,7 @@ export class ProformaUpdater implements IProformaUpdater {
|
|||||||
repository: IProformaRepository;
|
repository: IProformaRepository;
|
||||||
taxResolver: ProformaTaxResolver;
|
taxResolver: ProformaTaxResolver;
|
||||||
paymentResolver: ProformaPaymentResolver;
|
paymentResolver: ProformaPaymentResolver;
|
||||||
|
targetInvoiceSeriesValidator: ProformaTargetInvoiceSeriesValidator;
|
||||||
}
|
}
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -43,10 +45,35 @@ export class ProformaUpdater implements IProformaUpdater {
|
|||||||
|
|
||||||
const proforma = existingResult.data;
|
const proforma = existingResult.data;
|
||||||
|
|
||||||
|
if (proforma.status.isIssued()) {
|
||||||
|
return Result.fail(
|
||||||
|
new DomainValidationError(
|
||||||
|
"PROFORMA_ALREADY_ISSUED",
|
||||||
|
"status",
|
||||||
|
"Issued proformas cannot be edited"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proforma.status.isApproved()) {
|
||||||
|
const allowedApprovedPatch = this.isApprovedPatchAllowed(patchProps);
|
||||||
|
|
||||||
|
if (!allowedApprovedPatch) {
|
||||||
|
return Result.fail(
|
||||||
|
new DomainValidationError(
|
||||||
|
"APPROVED_PROFORMA_RESTRICTED_UPDATE",
|
||||||
|
"status",
|
||||||
|
"Approved proformas only allow target invoice series changes before issuing"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resolvedPatch = await this.resolvePatchProps({
|
const resolvedPatch = await this.resolvePatchProps({
|
||||||
companyId,
|
companyId,
|
||||||
currentInvoiceDate: proforma.invoiceDate,
|
currentInvoiceDate: proforma.invoiceDate,
|
||||||
patch: patchProps,
|
patch: patchProps,
|
||||||
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resolvedPatch.isFailure) {
|
if (resolvedPatch.isFailure) {
|
||||||
@ -81,8 +108,9 @@ export class ProformaUpdater implements IProformaUpdater {
|
|||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
currentInvoiceDate: UtcDate;
|
currentInvoiceDate: UtcDate;
|
||||||
patch: ProformaPatchInputProps;
|
patch: ProformaPatchInputProps;
|
||||||
|
transaction: unknown;
|
||||||
}): Promise<Result<ProformaPatchProps, Error>> {
|
}): Promise<Result<ProformaPatchProps, Error>> {
|
||||||
const { patch, companyId, currentInvoiceDate } = params;
|
const { patch, companyId, currentInvoiceDate, transaction } = params;
|
||||||
|
|
||||||
if (patch.taxRegimeCode !== undefined) {
|
if (patch.taxRegimeCode !== undefined) {
|
||||||
const taxRegimeResult = await this.deps.taxResolver.ensureTaxRegimeByCode({
|
const taxRegimeResult = await this.deps.taxResolver.ensureTaxRegimeByCode({
|
||||||
@ -106,8 +134,24 @@ export class ProformaUpdater implements IProformaUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (patch.targetInvoiceSeriesCode !== undefined) {
|
||||||
|
const targetInvoiceSeriesValidationResult =
|
||||||
|
await this.deps.targetInvoiceSeriesValidator.ensureValidIssuedInvoiceSeries({
|
||||||
|
companyId,
|
||||||
|
targetInvoiceSeriesCode: patch.targetInvoiceSeriesCode,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (targetInvoiceSeriesValidationResult.isFailure) {
|
||||||
|
return Result.fail(targetInvoiceSeriesValidationResult.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (patch.items === undefined) {
|
if (patch.items === undefined) {
|
||||||
return Result.ok(patch as ProformaPatchProps);
|
return Result.ok({
|
||||||
|
...patch,
|
||||||
|
series: patch.targetInvoiceSeriesCode,
|
||||||
|
} as ProformaPatchProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveInvoiceDate = patch.proformaDate ?? currentInvoiceDate;
|
const effectiveInvoiceDate = patch.proformaDate ?? currentInvoiceDate;
|
||||||
@ -132,7 +176,14 @@ export class ProformaUpdater implements IProformaUpdater {
|
|||||||
|
|
||||||
return Result.ok({
|
return Result.ok({
|
||||||
...patch,
|
...patch,
|
||||||
|
series: patch.targetInvoiceSeriesCode,
|
||||||
items: resolvedItems,
|
items: resolvedItems,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isApprovedPatchAllowed(patch: ProformaPatchInputProps): boolean {
|
||||||
|
const allowedKeys = new Set(["targetInvoiceSeriesCode"]);
|
||||||
|
|
||||||
|
return Object.keys(patch).every((key) => allowedKeys.has(key));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,11 +36,13 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
|
|||||||
const allTotals = proforma.totals();
|
const allTotals = proforma.totals();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// `series` se mantiene como alias legacy temporal del contrato publico.
|
||||||
id: proforma.id.toString(),
|
id: proforma.id.toString(),
|
||||||
company_id: proforma.companyId.toString(),
|
company_id: proforma.companyId.toString(),
|
||||||
|
|
||||||
proforma_reference: proforma.proformaReference.toString(),
|
proforma_reference: proforma.proformaReference.toString(),
|
||||||
status: proforma.status.toPrimitive() as ProformaFullSnapshot["status"],
|
status: proforma.status.toPrimitive() as ProformaFullSnapshot["status"],
|
||||||
|
target_invoice_series_code: maybeToNullable(proforma.series, (value) => value.toString()),
|
||||||
series: maybeToNullable(proforma.series, (value) => value.toString()),
|
series: maybeToNullable(proforma.series, (value) => value.toString()),
|
||||||
|
|
||||||
proforma_date: proforma.invoiceDate.toDateString(),
|
proforma_date: proforma.invoiceDate.toDateString(),
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotB
|
|||||||
|
|
||||||
proforma_reference: proforma.proformaReference.toString(),
|
proforma_reference: proforma.proformaReference.toString(),
|
||||||
status: proforma.status.toPrimitive() as ProformaSummaryDTO["status"],
|
status: proforma.status.toPrimitive() as ProformaSummaryDTO["status"],
|
||||||
|
target_invoice_series_code: maybeToNullable(proforma.series, (value) => value.toString()),
|
||||||
series: maybeToNullable(proforma.series, (value) => value.toString()),
|
series: maybeToNullable(proforma.series, (value) => value.toString()),
|
||||||
|
|
||||||
proforma_date: proforma.proformaDate.toDateString(),
|
proforma_date: proforma.proformaDate.toDateString(),
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import {
|
|||||||
InvoiceStatus,
|
InvoiceStatus,
|
||||||
type ItemAmount,
|
type ItemAmount,
|
||||||
} from "../../common/value-objects";
|
} from "../../common/value-objects";
|
||||||
import { InvalidInvoiceSeriesCodeError } from "../../invoice-series";
|
|
||||||
import {
|
import {
|
||||||
type IProformaItemCreateProps,
|
type IProformaItemCreateProps,
|
||||||
type IProformaItems,
|
type IProformaItems,
|
||||||
@ -36,6 +35,8 @@ export interface IProformaCreateProps {
|
|||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
status: InvoiceStatus;
|
status: InvoiceStatus;
|
||||||
|
|
||||||
|
documentSeriesId: Maybe<string>;
|
||||||
|
proformaNumber: Maybe<InvoiceNumber>;
|
||||||
proformaReference: InvoiceNumber;
|
proformaReference: InvoiceNumber;
|
||||||
series: Maybe<InvoiceSerie>;
|
series: Maybe<InvoiceSerie>;
|
||||||
|
|
||||||
@ -86,6 +87,8 @@ export interface IProforma {
|
|||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
status: InvoiceStatus;
|
status: InvoiceStatus;
|
||||||
|
|
||||||
|
documentSeriesId: Maybe<string>;
|
||||||
|
proformaNumber: Maybe<InvoiceNumber>;
|
||||||
series: Maybe<InvoiceSerie>;
|
series: Maybe<InvoiceSerie>;
|
||||||
proformaReference: InvoiceNumber;
|
proformaReference: InvoiceNumber;
|
||||||
|
|
||||||
@ -234,6 +237,14 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
|||||||
return this.props.series;
|
return this.props.series;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get documentSeriesId(): Maybe<string> {
|
||||||
|
return this.props.documentSeriesId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get proformaNumber(): Maybe<InvoiceNumber> {
|
||||||
|
return this.props.proformaNumber;
|
||||||
|
}
|
||||||
|
|
||||||
public get proformaReference() {
|
public get proformaReference() {
|
||||||
return this.props.proformaReference;
|
return this.props.proformaReference;
|
||||||
}
|
}
|
||||||
@ -363,10 +374,6 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
|||||||
}
|
}
|
||||||
|
|
||||||
private validateCanBeIssued(): Result<void, Error> {
|
private validateCanBeIssued(): Result<void, Error> {
|
||||||
if (this.series.isNone()) {
|
|
||||||
return Result.fail(InvalidInvoiceSeriesCodeError.required());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.description.isNone()) {
|
if (this.description.isNone()) {
|
||||||
return Result.fail(
|
return Result.fail(
|
||||||
new DomainValidationError(
|
new DomainValidationError(
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
import type { IModuleServer } from "@erp/core/api";
|
import type { IModuleServer } from "@erp/core/api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildInvoiceSeriesDependencies,
|
|
||||||
buildIssuedInvoicePublicServices,
|
buildIssuedInvoicePublicServices,
|
||||||
buildIssuedInvoicesDependencies,
|
buildIssuedInvoicesDependencies,
|
||||||
buildProformaPublicServices,
|
buildProformaPublicServices,
|
||||||
buildProformasDependencies,
|
buildProformasDependencies,
|
||||||
invoiceSeriesRouter,
|
|
||||||
issuedInvoicesRouter,
|
issuedInvoicesRouter,
|
||||||
models,
|
models,
|
||||||
proformasRouter,
|
proformasRouter,
|
||||||
@ -17,7 +15,7 @@ export type { IProformaPublicServices } from "./application";
|
|||||||
export const customerInvoicesAPIModule: IModuleServer = {
|
export const customerInvoicesAPIModule: IModuleServer = {
|
||||||
name: "customer-invoices",
|
name: "customer-invoices",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
dependencies: ["catalogs", "customers", "identity", "companies"],
|
dependencies: ["catalogs", "customers", "identity", "companies", "document-series"],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fase de SETUP
|
* Fase de SETUP
|
||||||
@ -27,10 +25,9 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
|||||||
* - NO conecta infraestructura
|
* - NO conecta infraestructura
|
||||||
*/
|
*/
|
||||||
async setup(params) {
|
async setup(params) {
|
||||||
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
const { logger } = params;
|
||||||
|
|
||||||
// 1) Dominio interno
|
// 1) Dominio interno
|
||||||
const invoiceSeriesInternal = buildInvoiceSeriesDependencies(params);
|
|
||||||
const issuedInvoicesInternal = buildIssuedInvoicesDependencies(params);
|
const issuedInvoicesInternal = buildIssuedInvoicesDependencies(params);
|
||||||
const proformasInternal = buildProformasDependencies(params);
|
const proformasInternal = buildProformasDependencies(params);
|
||||||
|
|
||||||
@ -52,7 +49,6 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
|||||||
|
|
||||||
// Implementación privada del módulo
|
// Implementación privada del módulo
|
||||||
internal: {
|
internal: {
|
||||||
invoiceSeries: invoiceSeriesInternal,
|
|
||||||
issuedInvoices: issuedInvoicesInternal,
|
issuedInvoices: issuedInvoicesInternal,
|
||||||
proformas: proformasInternal,
|
proformas: proformasInternal,
|
||||||
},
|
},
|
||||||
@ -67,10 +63,9 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
|||||||
* - NO construye dominio
|
* - NO construye dominio
|
||||||
*/
|
*/
|
||||||
async start(params) {
|
async start(params) {
|
||||||
const { app, baseRoutePath, logger, getInternal, getService } = params;
|
const { logger } = params;
|
||||||
|
|
||||||
// Registro de rutas HTTP
|
// Registro de rutas HTTP
|
||||||
invoiceSeriesRouter(params);
|
|
||||||
issuedInvoicesRouter(params);
|
issuedInvoicesRouter(params);
|
||||||
proformasRouter(params);
|
proformasRouter(params);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import customerInvoiceModelInit from "./models/customer-invoice.model";
|
import customerInvoiceModelInit from "./models/customer-invoice.model";
|
||||||
import customerInvoiceItemModelInit from "./models/customer-invoice-item.model";
|
import customerInvoiceItemModelInit from "./models/customer-invoice-item.model";
|
||||||
import customerInvoiceSeriesModelInit from "./models/customer-invoice-series.model";
|
|
||||||
import customerInvoiceTaxesModelInit from "./models/customer-invoice-tax.model";
|
import customerInvoiceTaxesModelInit from "./models/customer-invoice-tax.model";
|
||||||
import issuedInvoiceModelInit from "./models/sequelize-issued-invoice.model";
|
import issuedInvoiceModelInit from "./models/sequelize-issued-invoice.model";
|
||||||
import issuedInvoiceItemModelInit from "./models/sequelize-issued-invoice-item.model";
|
import issuedInvoiceItemModelInit from "./models/sequelize-issued-invoice-item.model";
|
||||||
@ -12,12 +11,10 @@ import verifactuRecordModelInit from "./models/verifactu-record.model";
|
|||||||
|
|
||||||
export * from "./models";
|
export * from "./models";
|
||||||
|
|
||||||
// Array de inicializadores para que registerModels() lo use
|
// Array de inicializadores para que registerModels() lo use.
|
||||||
export const models = [
|
export const models = [
|
||||||
customerInvoiceModelInit,
|
customerInvoiceModelInit,
|
||||||
customerInvoiceItemModelInit,
|
customerInvoiceItemModelInit,
|
||||||
customerInvoiceSeriesModelInit,
|
|
||||||
|
|
||||||
customerInvoiceTaxesModelInit,
|
customerInvoiceTaxesModelInit,
|
||||||
proformaModelInit,
|
proformaModelInit,
|
||||||
proformaItemModelInit,
|
proformaItemModelInit,
|
||||||
|
|||||||
@ -1,112 +0,0 @@
|
|||||||
import {
|
|
||||||
type CreationOptional,
|
|
||||||
DataTypes,
|
|
||||||
type InferAttributes,
|
|
||||||
type InferCreationAttributes,
|
|
||||||
Model,
|
|
||||||
type Sequelize,
|
|
||||||
} from "sequelize";
|
|
||||||
|
|
||||||
export class CustomerInvoiceSeriesModel extends Model<
|
|
||||||
InferAttributes<CustomerInvoiceSeriesModel>,
|
|
||||||
InferCreationAttributes<CustomerInvoiceSeriesModel>
|
|
||||||
> {
|
|
||||||
declare id: string;
|
|
||||||
declare company_id: string;
|
|
||||||
declare code: string;
|
|
||||||
declare next_number: number;
|
|
||||||
declare padding_length: number;
|
|
||||||
declare is_default: CreationOptional<boolean>;
|
|
||||||
declare is_active: CreationOptional<boolean>;
|
|
||||||
declare created_at: CreationOptional<Date>;
|
|
||||||
declare updated_at: CreationOptional<Date>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (database: Sequelize) => {
|
|
||||||
CustomerInvoiceSeriesModel.init(
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
charset: "utf8mb4",
|
|
||||||
collate: "utf8mb4_bin",
|
|
||||||
} as any,
|
|
||||||
|
|
||||||
company_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
charset: "utf8mb4",
|
|
||||||
collate: "utf8mb4_bin",
|
|
||||||
} as any,
|
|
||||||
|
|
||||||
code: {
|
|
||||||
type: DataTypes.STRING(10),
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
next_number: {
|
|
||||||
type: DataTypes.INTEGER.UNSIGNED,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
padding_length: {
|
|
||||||
type: DataTypes.SMALLINT.UNSIGNED,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
is_default: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
is_active: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
},
|
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize: database,
|
|
||||||
modelName: "CustomerInvoiceSeriesModel",
|
|
||||||
tableName: "customer_invoice_series",
|
|
||||||
|
|
||||||
charset: "utf8mb4",
|
|
||||||
collate: "utf8mb4_unicode_ci",
|
|
||||||
underscored: true,
|
|
||||||
paranoid: false,
|
|
||||||
timestamps: true,
|
|
||||||
|
|
||||||
createdAt: "created_at",
|
|
||||||
updatedAt: "updated_at",
|
|
||||||
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
name: "idx_customer_invoice_series_company_id",
|
|
||||||
fields: ["company_id"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "idx_customer_invoice_series_company_active",
|
|
||||||
fields: ["company_id", "is_active"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "uq_customer_invoice_series_company_code",
|
|
||||||
fields: ["company_id", "code"],
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
|
||||||
|
|
||||||
defaultScope: {},
|
|
||||||
|
|
||||||
scopes: {},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return CustomerInvoiceSeriesModel;
|
|
||||||
};
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
export * from "./customer-invoice.model";
|
export * from "./customer-invoice.model";
|
||||||
export * from "./customer-invoice-item.model";
|
export * from "./customer-invoice-item.model";
|
||||||
export * from "./customer-invoice-series.model";
|
|
||||||
export * from "./customer-invoice-tax.model";
|
export * from "./customer-invoice-tax.model";
|
||||||
export * from "./sequelize-proforma.model";
|
export * from "./sequelize-proforma.model";
|
||||||
export * from "./sequelize-proforma-item.model";
|
export * from "./sequelize-proforma-item.model";
|
||||||
|
|||||||
@ -35,6 +35,8 @@ export class ProformaModel extends Model<
|
|||||||
declare company_id: string;
|
declare company_id: string;
|
||||||
declare status: string;
|
declare status: string;
|
||||||
|
|
||||||
|
declare document_series_id: CreationOptional<string | null>;
|
||||||
|
declare proforma_number: CreationOptional<string | null>;
|
||||||
declare proforma_reference: string;
|
declare proforma_reference: string;
|
||||||
declare proforma_date: string;
|
declare proforma_date: string;
|
||||||
declare operation_date: CreationOptional<string | null>;
|
declare operation_date: CreationOptional<string | null>;
|
||||||
@ -171,6 +173,18 @@ export default (database: Sequelize) => {
|
|||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: "draft",
|
defaultValue: "draft",
|
||||||
},
|
},
|
||||||
|
document_series_id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
charset: "utf8mb4",
|
||||||
|
collate: "utf8mb4_bin",
|
||||||
|
} as any,
|
||||||
|
proforma_number: {
|
||||||
|
type: new DataTypes.STRING(32),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
proforma_reference: {
|
proforma_reference: {
|
||||||
type: new DataTypes.STRING(32),
|
type: new DataTypes.STRING(32),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
@ -414,6 +428,10 @@ export default (database: Sequelize) => {
|
|||||||
name: "idx_proformas_company",
|
name: "idx_proformas_company",
|
||||||
fields: ["company_id"],
|
fields: ["company_id"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "idx_proformas_company_document_series",
|
||||||
|
fields: ["company_id", "document_series_id"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "uq_proformas_company_reference",
|
name: "uq_proformas_company_reference",
|
||||||
fields: ["company_id", "proforma_reference"],
|
fields: ["company_id", "proforma_reference"],
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
export * from "./common";
|
export * from "./common";
|
||||||
export * from "./invoice-series";
|
|
||||||
export * from "./issued-invoices";
|
export * from "./issued-invoices";
|
||||||
export * from "./proformas";
|
export * from "./proformas";
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./invoice-series.di";
|
|
||||||
export * from "./invoice-series-repository.di";
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import type { Sequelize } from "sequelize";
|
|
||||||
|
|
||||||
import { SequelizeInvoiceSeriesRepository } from "../persistence";
|
|
||||||
import { InvoiceSeriesDomainMapper } from "../persistence/sequelize/mappers";
|
|
||||||
|
|
||||||
export const buildInvoiceSeriesRepository = (database: Sequelize) =>
|
|
||||||
new SequelizeInvoiceSeriesRepository(new InvoiceSeriesDomainMapper(), database);
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
|
||||||
|
|
||||||
import {
|
|
||||||
type ListInvoiceSeriesUseCase,
|
|
||||||
buildInvoiceSeriesFinder,
|
|
||||||
buildInvoiceSeriesSnapshotBuilders,
|
|
||||||
buildListInvoiceSeriesUseCase,
|
|
||||||
} from "../../../application";
|
|
||||||
|
|
||||||
import { buildInvoiceSeriesRepository } from "./invoice-series-repository.di";
|
|
||||||
|
|
||||||
export type InvoiceSeriesInternalDeps = {
|
|
||||||
useCases: {
|
|
||||||
listInvoiceSeries: () => ListInvoiceSeriesUseCase;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function buildInvoiceSeriesDependencies(params: ModuleParams): InvoiceSeriesInternalDeps {
|
|
||||||
const { database } = params;
|
|
||||||
|
|
||||||
const transactionManager = buildTransactionManager(database);
|
|
||||||
const repository = buildInvoiceSeriesRepository(database);
|
|
||||||
const finder = buildInvoiceSeriesFinder(repository);
|
|
||||||
const snapshotBuilders = buildInvoiceSeriesSnapshotBuilders();
|
|
||||||
|
|
||||||
return {
|
|
||||||
useCases: {
|
|
||||||
listInvoiceSeries: () =>
|
|
||||||
buildListInvoiceSeriesUseCase({
|
|
||||||
finder,
|
|
||||||
summarySnapshotBuilder: snapshotBuilders.summary,
|
|
||||||
transactionManager,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./list-invoice-series.controller";
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import {
|
|
||||||
ExpressController,
|
|
||||||
forbidQueryFieldGuard,
|
|
||||||
requireAuthenticatedGuard,
|
|
||||||
requireCompanyContextGuard,
|
|
||||||
} from "@erp/core/api";
|
|
||||||
|
|
||||||
import { ListInvoiceSeriesResponseSchema } from "../../../../../common";
|
|
||||||
import type { ListInvoiceSeriesUseCase } from "../../../../application";
|
|
||||||
import { invoiceSeriesApiErrorMapper } from "../invoice-series-api-error-mapper";
|
|
||||||
|
|
||||||
export class ListInvoiceSeriesController extends ExpressController {
|
|
||||||
public constructor(private readonly useCase: ListInvoiceSeriesUseCase) {
|
|
||||||
super();
|
|
||||||
this.errorMapper = invoiceSeriesApiErrorMapper;
|
|
||||||
|
|
||||||
this.registerGuards(
|
|
||||||
requireAuthenticatedGuard(),
|
|
||||||
requireCompanyContextGuard(),
|
|
||||||
forbidQueryFieldGuard("companyId"),
|
|
||||||
forbidQueryFieldGuard("company_id")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async executeImpl() {
|
|
||||||
const companyId = this.getTenantId();
|
|
||||||
if (!companyId) {
|
|
||||||
return this.forbiddenError("Tenant ID not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.useCase.execute({ companyId });
|
|
||||||
|
|
||||||
return result.match(
|
|
||||||
(data) => {
|
|
||||||
const dto = ListInvoiceSeriesResponseSchema.parse(data);
|
|
||||||
return this.ok(dto);
|
|
||||||
},
|
|
||||||
(error) => this.handleError(error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from "./controllers";
|
|
||||||
export * from "./invoice-series-api-error-mapper";
|
|
||||||
export * from "./invoice-series.routes";
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
import {
|
|
||||||
ApiErrorMapper,
|
|
||||||
type ErrorToApiRule,
|
|
||||||
InternalApiError,
|
|
||||||
ValidationApiError,
|
|
||||||
} from "@erp/core/api";
|
|
||||||
|
|
||||||
import {
|
|
||||||
InvoiceSeriesConcurrencyError,
|
|
||||||
InvoiceSeriesLockRequiredError,
|
|
||||||
InvoiceSeriesTransactionRequiredError,
|
|
||||||
} from "../../../application";
|
|
||||||
import {
|
|
||||||
type InvalidInvoiceSeriesCodeError,
|
|
||||||
type InvalidInvoiceSeriesNextNumberError,
|
|
||||||
type InvalidInvoiceSeriesPaddingLengthError,
|
|
||||||
isInvalidInvoiceSeriesCodeError,
|
|
||||||
isInvalidInvoiceSeriesNextNumberError,
|
|
||||||
isInvalidInvoiceSeriesPaddingLengthError,
|
|
||||||
} from "../../../domain";
|
|
||||||
|
|
||||||
const invalidInvoiceSeriesCodeRule: ErrorToApiRule = {
|
|
||||||
priority: 130,
|
|
||||||
matches: (error) => isInvalidInvoiceSeriesCodeError(error),
|
|
||||||
build: (error) =>
|
|
||||||
new ValidationApiError(
|
|
||||||
(error as InvalidInvoiceSeriesCodeError).message || "Invoice series code is invalid."
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const invalidInvoiceSeriesNextNumberRule: ErrorToApiRule = {
|
|
||||||
priority: 130,
|
|
||||||
matches: (error) => isInvalidInvoiceSeriesNextNumberError(error),
|
|
||||||
build: (error) =>
|
|
||||||
new ValidationApiError(
|
|
||||||
(error as InvalidInvoiceSeriesNextNumberError).message ||
|
|
||||||
"Invoice series next number is invalid."
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const invalidInvoiceSeriesPaddingLengthRule: ErrorToApiRule = {
|
|
||||||
priority: 130,
|
|
||||||
matches: (error) => isInvalidInvoiceSeriesPaddingLengthError(error),
|
|
||||||
build: (error) =>
|
|
||||||
new ValidationApiError(
|
|
||||||
(error as InvalidInvoiceSeriesPaddingLengthError).message ||
|
|
||||||
"Invoice series padding length is invalid."
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const invoiceSeriesTransactionRequiredRule: ErrorToApiRule = {
|
|
||||||
priority: 130,
|
|
||||||
matches: (error) => error instanceof InvoiceSeriesTransactionRequiredError,
|
|
||||||
build: (error) =>
|
|
||||||
new InternalApiError(
|
|
||||||
(error as InvoiceSeriesTransactionRequiredError).message ||
|
|
||||||
"Invoice series operations require an active transaction."
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const invoiceSeriesLockRequiredRule: ErrorToApiRule = {
|
|
||||||
priority: 130,
|
|
||||||
matches: (error) => error instanceof InvoiceSeriesLockRequiredError,
|
|
||||||
build: (error) =>
|
|
||||||
new InternalApiError(
|
|
||||||
(error as InvoiceSeriesLockRequiredError).message ||
|
|
||||||
"Invoice series row lock requires an active transaction."
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const invoiceSeriesConcurrencyRule: ErrorToApiRule = {
|
|
||||||
priority: 130,
|
|
||||||
matches: (error) => error instanceof InvoiceSeriesConcurrencyError,
|
|
||||||
build: (error) =>
|
|
||||||
new InternalApiError(
|
|
||||||
(error as InvoiceSeriesConcurrencyError).message ||
|
|
||||||
"Invoice series persistence ended in an unexpected concurrency state."
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const invoiceSeriesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
|
||||||
.register(invoiceSeriesConcurrencyRule)
|
|
||||||
.register(invoiceSeriesLockRequiredRule)
|
|
||||||
.register(invoiceSeriesTransactionRequiredRule)
|
|
||||||
.register(invalidInvoiceSeriesPaddingLengthRule)
|
|
||||||
.register(invalidInvoiceSeriesNextNumberRule)
|
|
||||||
.register(invalidInvoiceSeriesCodeRule);
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import type { StartParams } from "@erp/core/api";
|
|
||||||
import { requireIdentityTenant } from "@erp/identity/api";
|
|
||||||
import { type NextFunction, type Request, type Response, Router } from "express";
|
|
||||||
|
|
||||||
import type { InvoiceSeriesInternalDeps } from "../di/invoice-series.di";
|
|
||||||
|
|
||||||
import { ListInvoiceSeriesController } from "./controllers";
|
|
||||||
|
|
||||||
export const invoiceSeriesRouter = (params: StartParams) => {
|
|
||||||
const { app, config, getInternal } = params;
|
|
||||||
|
|
||||||
const deps = getInternal<InvoiceSeriesInternalDeps>("customer-invoices", "invoiceSeries");
|
|
||||||
|
|
||||||
const router: Router = Router({ mergeParams: true });
|
|
||||||
|
|
||||||
router.use(...requireIdentityTenant(params));
|
|
||||||
|
|
||||||
router.get("/", (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const useCase = deps.useCases.listInvoiceSeries();
|
|
||||||
const controller = new ListInvoiceSeriesController(useCase);
|
|
||||||
return controller.execute(req, res, next);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(`${config.server.apiBasePath}/catalogs/invoice-series`, router);
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./invoice-series-domain.mapper";
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import {
|
|
||||||
UniqueID,
|
|
||||||
ValidationErrorCollection,
|
|
||||||
type ValidationErrorDetail,
|
|
||||||
extractOrPushError,
|
|
||||||
} from "@repo/rdx-ddd";
|
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
import {
|
|
||||||
type InvalidInvoiceSeriesCodeError,
|
|
||||||
type InvalidInvoiceSeriesNextNumberError,
|
|
||||||
type InvalidInvoiceSeriesPaddingLengthError,
|
|
||||||
InvoiceSeries,
|
|
||||||
InvoiceSeriesCode,
|
|
||||||
type InvoiceSeriesInternalProps,
|
|
||||||
InvoiceSeriesNextNumber,
|
|
||||||
InvoiceSeriesPaddingLength,
|
|
||||||
} from "../../../../../domain";
|
|
||||||
import type { CustomerInvoiceSeriesModel } from "../../../../common";
|
|
||||||
|
|
||||||
export class InvoiceSeriesDomainMapper {
|
|
||||||
public mapToDomain(raw: CustomerInvoiceSeriesModel): Result<InvoiceSeries, Error> {
|
|
||||||
const errors: ValidationErrorDetail[] = [];
|
|
||||||
|
|
||||||
const id = extractOrPushError(UniqueID.create(raw.id), "id", errors);
|
|
||||||
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
|
|
||||||
|
|
||||||
const codeResult = InvoiceSeriesCode.create(raw.code);
|
|
||||||
if (codeResult.isFailure) {
|
|
||||||
return Result.fail(codeResult.error as InvalidInvoiceSeriesCodeError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextNumberResult = InvoiceSeriesNextNumber.create(raw.next_number);
|
|
||||||
if (nextNumberResult.isFailure) {
|
|
||||||
return Result.fail(nextNumberResult.error as InvalidInvoiceSeriesNextNumberError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const paddingLengthResult = InvoiceSeriesPaddingLength.create(raw.padding_length);
|
|
||||||
if (paddingLengthResult.isFailure) {
|
|
||||||
return Result.fail(paddingLengthResult.error as InvalidInvoiceSeriesPaddingLengthError);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
return Result.fail(
|
|
||||||
new ValidationErrorCollection("Invoice series mapping failed [mapToDomain]", errors)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const props: InvoiceSeriesInternalProps = {
|
|
||||||
companyId: companyId!,
|
|
||||||
code: codeResult.data,
|
|
||||||
nextNumber: nextNumberResult.data,
|
|
||||||
paddingLength: paddingLengthResult.data,
|
|
||||||
isDefault: raw.is_default,
|
|
||||||
isActive: raw.is_active,
|
|
||||||
};
|
|
||||||
|
|
||||||
return Result.ok(InvoiceSeries.rehydrate(props, id!));
|
|
||||||
}
|
|
||||||
|
|
||||||
public mapToPersistence(source: InvoiceSeries) {
|
|
||||||
return Result.ok({
|
|
||||||
id: source.id.toString(),
|
|
||||||
company_id: source.companyId.toString(),
|
|
||||||
code: source.code.toPrimitive(),
|
|
||||||
next_number: source.nextNumber.toPrimitive(),
|
|
||||||
padding_length: source.paddingLength.toPrimitive(),
|
|
||||||
is_default: source.isDefault,
|
|
||||||
is_active: source.isActive,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public mapOptionalToDomain(
|
|
||||||
raw: CustomerInvoiceSeriesModel | null
|
|
||||||
): Result<Maybe<InvoiceSeries>, Error> {
|
|
||||||
if (!raw) {
|
|
||||||
return Result.ok(Maybe.none<InvoiceSeries>());
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.mapToDomain(raw).map((invoiceSeries) => Maybe.some(invoiceSeries));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./invoice-series.repository";
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
import { SequelizeRepository, translateSequelizeError } from "@erp/core/api";
|
|
||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
|
||||||
import type { Sequelize, Transaction } from "sequelize";
|
|
||||||
|
|
||||||
import type { IInvoiceSeriesRepository } from "../../../../../application";
|
|
||||||
import {
|
|
||||||
InvoiceSeriesConcurrencyError,
|
|
||||||
InvoiceSeriesLockRequiredError,
|
|
||||||
InvoiceSeriesTransactionRequiredError,
|
|
||||||
} from "../../../../../application";
|
|
||||||
import type { InvoiceSeries, InvoiceSeriesCode } from "../../../../../domain";
|
|
||||||
import { CustomerInvoiceSeriesModel } from "../../../../common";
|
|
||||||
import type { InvoiceSeriesDomainMapper } from "../mappers";
|
|
||||||
|
|
||||||
export class SequelizeInvoiceSeriesRepository
|
|
||||||
extends SequelizeRepository<InvoiceSeries>
|
|
||||||
implements IInvoiceSeriesRepository
|
|
||||||
{
|
|
||||||
public constructor(
|
|
||||||
private readonly domainMapper: InvoiceSeriesDomainMapper,
|
|
||||||
database: Sequelize
|
|
||||||
) {
|
|
||||||
super({ database });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async findActiveByCompany(params: { companyId: UniqueID; transaction?: unknown }) {
|
|
||||||
try {
|
|
||||||
const rows = await CustomerInvoiceSeriesModel.findAll({
|
|
||||||
where: {
|
|
||||||
company_id: params.companyId.toString(),
|
|
||||||
is_active: true,
|
|
||||||
},
|
|
||||||
order: [
|
|
||||||
["is_default", "DESC"],
|
|
||||||
["code", "ASC"],
|
|
||||||
],
|
|
||||||
transaction: params.transaction as Transaction | undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const invoiceSeriesRows: InvoiceSeries[] = [];
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
const invoiceSeriesResult = this.domainMapper.mapToDomain(row);
|
|
||||||
|
|
||||||
if (invoiceSeriesResult.isFailure) {
|
|
||||||
return Result.fail(invoiceSeriesResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
invoiceSeriesRows.push(invoiceSeriesResult.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(new Collection(invoiceSeriesRows));
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return Result.fail(translateSequelizeError(error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async findByCodeInCompany(params: {
|
|
||||||
companyId: UniqueID;
|
|
||||||
invoiceSeriesCode: InvoiceSeriesCode;
|
|
||||||
transaction?: unknown;
|
|
||||||
}) {
|
|
||||||
try {
|
|
||||||
const row = await CustomerInvoiceSeriesModel.findOne({
|
|
||||||
where: {
|
|
||||||
company_id: params.companyId.toString(),
|
|
||||||
code: params.invoiceSeriesCode.toPrimitive(),
|
|
||||||
},
|
|
||||||
transaction: params.transaction as Transaction | undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.domainMapper.mapOptionalToDomain(row);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return Result.fail(translateSequelizeError(error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async findByCodeInCompanyForUpdate(params: {
|
|
||||||
companyId: UniqueID;
|
|
||||||
invoiceSeriesCode: InvoiceSeriesCode;
|
|
||||||
transaction: unknown;
|
|
||||||
}) {
|
|
||||||
try {
|
|
||||||
const transaction = params.transaction as Transaction;
|
|
||||||
|
|
||||||
if (!transaction) {
|
|
||||||
return Result.fail(new InvoiceSeriesLockRequiredError());
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = await CustomerInvoiceSeriesModel.findOne({
|
|
||||||
where: {
|
|
||||||
company_id: params.companyId.toString(),
|
|
||||||
code: params.invoiceSeriesCode.toPrimitive(),
|
|
||||||
},
|
|
||||||
transaction,
|
|
||||||
lock: transaction.LOCK.UPDATE,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.domainMapper.mapOptionalToDomain(row);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return Result.fail(translateSequelizeError(error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async update(
|
|
||||||
invoiceSeries: InvoiceSeries,
|
|
||||||
transaction?: unknown
|
|
||||||
): Promise<Result<void, Error>> {
|
|
||||||
try {
|
|
||||||
if (!transaction) {
|
|
||||||
return Result.fail(new InvoiceSeriesTransactionRequiredError());
|
|
||||||
}
|
|
||||||
|
|
||||||
const dtoResult = this.domainMapper.mapToPersistence(invoiceSeries);
|
|
||||||
|
|
||||||
if (dtoResult.isFailure) {
|
|
||||||
return Result.fail(dtoResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id, ...payload } = dtoResult.data;
|
|
||||||
const [affectedRows] = await CustomerInvoiceSeriesModel.update(payload, {
|
|
||||||
where: { id },
|
|
||||||
transaction: transaction as Transaction | undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (affectedRows !== 1) {
|
|
||||||
return Result.fail(new InvoiceSeriesConcurrencyError());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok();
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return Result.fail(translateSequelizeError(error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import type { SetupParams } from "@erp/core/api";
|
import type { SetupParams } from "@erp/core/api";
|
||||||
import { buildCatalogs } from "@erp/core/api";
|
import { buildCatalogs } from "@erp/core/api";
|
||||||
|
import type { DocumentSeriesPublicServicesType } from "@erp/document-series/api";
|
||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
@ -9,9 +10,9 @@ import {
|
|||||||
type IIssuedInvoiceServicesContext,
|
type IIssuedInvoiceServicesContext,
|
||||||
buildIssuedInvoiceCreator,
|
buildIssuedInvoiceCreator,
|
||||||
} from "../../../application/issued-invoices";
|
} from "../../../application/issued-invoices";
|
||||||
import { buildInvoiceSeriesNumberAssigner } from "../../../application/invoice-series";
|
import type { IInvoiceSeriesNumberAssigner } from "../../../application/invoice-series";
|
||||||
|
import { InvoiceNumber, InvoiceSerie } from "../../../domain";
|
||||||
|
|
||||||
import { buildInvoiceSeriesRepository } from "../../invoice-series";
|
|
||||||
import { buildV2IssuedInvoicePersistence } from "./issued-invoice-persistence.di";
|
import { buildV2IssuedInvoicePersistence } from "./issued-invoice-persistence.di";
|
||||||
import type { IssuedInvoicesInternalDeps } from "./issued-invoices.di";
|
import type { IssuedInvoicesInternalDeps } from "./issued-invoices.di";
|
||||||
|
|
||||||
@ -23,11 +24,46 @@ export function buildIssuedInvoicePublicServices(
|
|||||||
|
|
||||||
// Infrastructure
|
// Infrastructure
|
||||||
const catalogs = buildCatalogs();
|
const catalogs = buildCatalogs();
|
||||||
|
const documentSeriesServices =
|
||||||
|
params.getService<DocumentSeriesPublicServicesType>("document-series:general");
|
||||||
// Fase 1C: la emision ya persiste en `issued_invoices` y relaciona Verifactu por `issued_invoice_id`.
|
// Fase 1C: la emision ya persiste en `issued_invoices` y relaciona Verifactu por `issued_invoice_id`.
|
||||||
const persistence = buildV2IssuedInvoicePersistence({ catalogs, database });
|
const persistence = buildV2IssuedInvoicePersistence({ catalogs, database });
|
||||||
const { repository } = persistence;
|
const { repository } = persistence;
|
||||||
const invoiceSeriesRepository = buildInvoiceSeriesRepository(database);
|
const invoiceSeriesNumberAssigner: IInvoiceSeriesNumberAssigner = {
|
||||||
const invoiceSeriesNumberAssigner = buildInvoiceSeriesNumberAssigner(invoiceSeriesRepository);
|
assignNextNumber: async ({ companyId, invoiceSeriesCode, transaction }) => {
|
||||||
|
const requestedSeriesCode = invoiceSeriesCode
|
||||||
|
.map((series) => series.toPrimitive())
|
||||||
|
.unwrapOr(null);
|
||||||
|
|
||||||
|
const assignResult = await documentSeriesServices.assignNextNumber({
|
||||||
|
companyId: companyId.toString(),
|
||||||
|
documentType: "issued_invoice",
|
||||||
|
seriesCode: requestedSeriesCode,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (assignResult.isFailure) {
|
||||||
|
return Result.fail(assignResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoiceNumberResult = InvoiceNumber.create(assignResult.data.number);
|
||||||
|
if (invoiceNumberResult.isFailure) {
|
||||||
|
return Result.fail(invoiceNumberResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignedSeriesResult = InvoiceSerie.create(assignResult.data.series_code);
|
||||||
|
if (assignedSeriesResult.isFailure) {
|
||||||
|
return Result.fail(assignedSeriesResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok({
|
||||||
|
invoiceNumber: invoiceNumberResult.data,
|
||||||
|
formattedNumber: assignResult.data.reference,
|
||||||
|
invoiceSeriesId: assignResult.data.series_id,
|
||||||
|
invoiceSeriesCode: assignedSeriesResult.data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Application helpers
|
// Application helpers
|
||||||
const creator = buildIssuedInvoiceCreator({ invoiceSeriesNumberAssigner, repository });
|
const creator = buildIssuedInvoiceCreator({ invoiceSeriesNumberAssigner, repository });
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import type { DocumentSeriesPublicServicesType } from "@erp/document-series/api";
|
||||||
|
|
||||||
import type { IProformaNumberGenerator } from "../../../application";
|
import type { IProformaNumberGenerator } from "../../../application";
|
||||||
import { SequelizeProformaV2NumberGenerator } from "../persistence";
|
import { SequelizeProformaV2NumberGenerator } from "../persistence";
|
||||||
|
|
||||||
export const buildV2ProformaNumberGenerator = (): IProformaNumberGenerator =>
|
export const buildV2ProformaNumberGenerator = (
|
||||||
new SequelizeProformaV2NumberGenerator();
|
documentSeriesServices: DocumentSeriesPublicServicesType
|
||||||
|
): IProformaNumberGenerator => new SequelizeProformaV2NumberGenerator(documentSeriesServices);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { DocumentSeriesPublicServicesType } from "@erp/document-series/api";
|
||||||
import type { Sequelize } from "sequelize";
|
import type { Sequelize } from "sequelize";
|
||||||
|
|
||||||
import { buildV2ProformaNumberGenerator } from "./proforma-number-generator-v2.di";
|
import { buildV2ProformaNumberGenerator } from "./proforma-number-generator-v2.di";
|
||||||
@ -9,6 +10,7 @@ import { buildProformaRepositoryV2 } from "./proforma-repositories-v2.di";
|
|||||||
|
|
||||||
type BuildProformaPersistenceParams = {
|
type BuildProformaPersistenceParams = {
|
||||||
database: Sequelize;
|
database: Sequelize;
|
||||||
|
documentSeriesServices: DocumentSeriesPublicServicesType;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*export type LegacyProformaPersistence = {
|
/*export type LegacyProformaPersistence = {
|
||||||
@ -53,6 +55,6 @@ export const buildV2ProformaPersistence = (
|
|||||||
database: params.database,
|
database: params.database,
|
||||||
mappers,
|
mappers,
|
||||||
}),
|
}),
|
||||||
numberGenerator: buildV2ProformaNumberGenerator(),
|
numberGenerator: buildV2ProformaNumberGenerator(params.documentSeriesServices),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { SetupParams } from "@erp/core/api";
|
import type { SetupParams } from "@erp/core/api";
|
||||||
|
import type { DocumentSeriesPublicServicesType } from "@erp/document-series/api";
|
||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import type { Transaction } from "sequelize";
|
import type { Transaction } from "sequelize";
|
||||||
@ -17,6 +18,8 @@ import type { Proforma } from "../../../domain";
|
|||||||
import { buildV2ProformaPersistence } from "./proforma-persistence.di";
|
import { buildV2ProformaPersistence } from "./proforma-persistence.di";
|
||||||
import type { ProformasInternalDeps } from "./proformas.di";
|
import type { ProformasInternalDeps } from "./proformas.di";
|
||||||
import { resolveProformaCatalogsDeps } from "./proforrma-catalog-deps.di";
|
import { resolveProformaCatalogsDeps } from "./proforrma-catalog-deps.di";
|
||||||
|
import { DocumentSeriesProformaSeriesValidator } from "../services/document-series-proforma-series-validator";
|
||||||
|
import { DocumentSeriesProformaTargetInvoiceSeriesValidator } from "../services/document-series-proforma-target-invoice-series-validator";
|
||||||
|
|
||||||
type ProformaServicesContext = {
|
type ProformaServicesContext = {
|
||||||
transaction: Transaction;
|
transaction: Transaction;
|
||||||
@ -48,6 +51,8 @@ export function buildProformaPublicServices(
|
|||||||
const { database } = params;
|
const { database } = params;
|
||||||
|
|
||||||
const catalogs = resolveProformaCatalogsDeps(params);
|
const catalogs = resolveProformaCatalogsDeps(params);
|
||||||
|
const documentSeriesServices =
|
||||||
|
params.getService<DocumentSeriesPublicServicesType>("document-series:general");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fase 1C: los servicios publicos ya consumen Proforma V2.
|
* Fase 1C: los servicios publicos ya consumen Proforma V2.
|
||||||
@ -55,6 +60,7 @@ export function buildProformaPublicServices(
|
|||||||
*/
|
*/
|
||||||
const persistence = buildV2ProformaPersistence({
|
const persistence = buildV2ProformaPersistence({
|
||||||
database,
|
database,
|
||||||
|
documentSeriesServices,
|
||||||
});
|
});
|
||||||
const { repository, numberGenerator: numberService } = persistence;
|
const { repository, numberGenerator: numberService } = persistence;
|
||||||
const finder = buildProformaFinder(repository);
|
const finder = buildProformaFinder(repository);
|
||||||
@ -72,12 +78,20 @@ export function buildProformaPublicServices(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const snapshotBuilders = buildProformaSnapshotBuilders();
|
const snapshotBuilders = buildProformaSnapshotBuilders();
|
||||||
|
const proformaSeriesValidator = new DocumentSeriesProformaSeriesValidator(
|
||||||
|
documentSeriesServices
|
||||||
|
);
|
||||||
|
const targetInvoiceSeriesValidator = new DocumentSeriesProformaTargetInvoiceSeriesValidator(
|
||||||
|
documentSeriesServices
|
||||||
|
);
|
||||||
|
|
||||||
const creator = buildProformaCreator({
|
const creator = buildProformaCreator({
|
||||||
repository,
|
repository,
|
||||||
numberService,
|
numberService,
|
||||||
taxResolver: catalogResolvers.taxResolver,
|
taxResolver: catalogResolvers.taxResolver,
|
||||||
paymentResolver: catalogResolvers.paymentResolver,
|
paymentResolver: catalogResolvers.paymentResolver,
|
||||||
|
proformaSeriesValidator,
|
||||||
|
targetInvoiceSeriesValidator,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
||||||
|
import type { DocumentSeriesPublicServicesType } from "@erp/document-series/api";
|
||||||
|
|
||||||
import type { ICompanyPublicServices } from "../../../../../../companies/src/api";
|
import type { ICompanyPublicServices } from "../../../../../../companies/src/api";
|
||||||
import {
|
import {
|
||||||
@ -32,6 +33,8 @@ import {
|
|||||||
buildUpdateProformaUseCase,
|
buildUpdateProformaUseCase,
|
||||||
} from "../../../application";
|
} from "../../../application";
|
||||||
import { CompanyReportProfileFinder } from "../adapters/company-report-profile-finder";
|
import { CompanyReportProfileFinder } from "../adapters/company-report-profile-finder";
|
||||||
|
import { DocumentSeriesProformaSeriesValidator } from "../services/document-series-proforma-series-validator";
|
||||||
|
import { DocumentSeriesProformaTargetInvoiceSeriesValidator } from "../services/document-series-proforma-target-invoice-series-validator";
|
||||||
|
|
||||||
import { buildProformaDocumentServices } from "./proforma-documents.di";
|
import { buildProformaDocumentServices } from "./proforma-documents.di";
|
||||||
import { buildV2ProformaPersistence } from "./proforma-persistence.di";
|
import { buildV2ProformaPersistence } from "./proforma-persistence.di";
|
||||||
@ -62,6 +65,8 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
|||||||
|
|
||||||
const catalogs = resolveProformaCatalogsDeps(params);
|
const catalogs = resolveProformaCatalogsDeps(params);
|
||||||
const companiesServices = params.getService<ICompanyPublicServices>("companies:general");
|
const companiesServices = params.getService<ICompanyPublicServices>("companies:general");
|
||||||
|
const documentSeriesServices =
|
||||||
|
params.getService<DocumentSeriesPublicServicesType>("document-series:general");
|
||||||
|
|
||||||
const transactionManager = buildTransactionManager(database);
|
const transactionManager = buildTransactionManager(database);
|
||||||
/**
|
/**
|
||||||
@ -70,6 +75,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
|||||||
*/
|
*/
|
||||||
const persistence = buildV2ProformaPersistence({
|
const persistence = buildV2ProformaPersistence({
|
||||||
database,
|
database,
|
||||||
|
documentSeriesServices,
|
||||||
});
|
});
|
||||||
const { repository, numberGenerator: proformaNumberService } = persistence;
|
const { repository, numberGenerator: proformaNumberService } = persistence;
|
||||||
|
|
||||||
@ -89,18 +95,27 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
|||||||
});
|
});
|
||||||
|
|
||||||
const finder = buildProformaFinder(repository);
|
const finder = buildProformaFinder(repository);
|
||||||
|
const proformaSeriesValidator = new DocumentSeriesProformaSeriesValidator(
|
||||||
|
documentSeriesServices
|
||||||
|
);
|
||||||
|
const targetInvoiceSeriesValidator = new DocumentSeriesProformaTargetInvoiceSeriesValidator(
|
||||||
|
documentSeriesServices
|
||||||
|
);
|
||||||
|
|
||||||
const creator = buildProformaCreator({
|
const creator = buildProformaCreator({
|
||||||
taxResolver: catalogResolvers.taxResolver,
|
taxResolver: catalogResolvers.taxResolver,
|
||||||
paymentResolver: catalogResolvers.paymentResolver,
|
paymentResolver: catalogResolvers.paymentResolver,
|
||||||
numberService: proformaNumberService,
|
numberService: proformaNumberService,
|
||||||
repository,
|
repository,
|
||||||
|
proformaSeriesValidator,
|
||||||
|
targetInvoiceSeriesValidator,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updater = buildProformaUpdater({
|
const updater = buildProformaUpdater({
|
||||||
repository,
|
repository,
|
||||||
taxResolver: catalogResolvers.taxResolver,
|
taxResolver: catalogResolvers.taxResolver,
|
||||||
paymentResolver: catalogResolvers.paymentResolver,
|
paymentResolver: catalogResolvers.paymentResolver,
|
||||||
|
targetInvoiceSeriesValidator,
|
||||||
});
|
});
|
||||||
|
|
||||||
const statusChanger = buildProformaStatusChanger({
|
const statusChanger = buildProformaStatusChanger({
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
maybeFromNullableResult,
|
maybeFromNullableResult,
|
||||||
maybeToNullable,
|
maybeToNullable,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
InvoiceNumber,
|
InvoiceNumber,
|
||||||
@ -244,6 +244,8 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
|||||||
companyId: attributes.companyId!,
|
companyId: attributes.companyId!,
|
||||||
|
|
||||||
status: attributes.status!,
|
status: attributes.status!,
|
||||||
|
documentSeriesId: Maybe.none(),
|
||||||
|
proformaNumber: Maybe.some(attributes.invoiceNumber!),
|
||||||
series: attributes.series!,
|
series: attributes.series!,
|
||||||
proformaReference: attributes.invoiceNumber!,
|
proformaReference: attributes.invoiceNumber!,
|
||||||
proformaDate: attributes.invoiceDate!,
|
proformaDate: attributes.invoiceDate!,
|
||||||
|
|||||||
@ -50,6 +50,11 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
|
|||||||
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
|
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
|
||||||
const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors);
|
const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors);
|
||||||
const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors);
|
const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors);
|
||||||
|
const documentSeriesId = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.document_series_id, (value) => Result.ok(String(value))),
|
||||||
|
"document_series_id",
|
||||||
|
errors
|
||||||
|
);
|
||||||
const series = extractOrPushError(
|
const series = extractOrPushError(
|
||||||
maybeFromNullableResult(raw.target_invoice_series_code, (value) =>
|
maybeFromNullableResult(raw.target_invoice_series_code, (value) =>
|
||||||
InvoiceSerie.create(value)
|
InvoiceSerie.create(value)
|
||||||
@ -57,6 +62,11 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
|
|||||||
"target_invoice_series_code",
|
"target_invoice_series_code",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
const proformaNumber = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.proforma_number, (value) => InvoiceNumber.create(value)),
|
||||||
|
"proforma_number",
|
||||||
|
errors
|
||||||
|
);
|
||||||
const invoiceNumber = extractOrPushError(
|
const invoiceNumber = extractOrPushError(
|
||||||
InvoiceNumber.create(raw.proforma_reference),
|
InvoiceNumber.create(raw.proforma_reference),
|
||||||
"proforma_reference",
|
"proforma_reference",
|
||||||
@ -125,6 +135,8 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
|
|||||||
companyId,
|
companyId,
|
||||||
customerId,
|
customerId,
|
||||||
status,
|
status,
|
||||||
|
documentSeriesId,
|
||||||
|
proformaNumber,
|
||||||
series,
|
series,
|
||||||
invoiceNumber,
|
invoiceNumber,
|
||||||
invoiceDate,
|
invoiceDate,
|
||||||
@ -173,6 +185,8 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
|
|||||||
const invoiceProps: ProformaInternalProps = {
|
const invoiceProps: ProformaInternalProps = {
|
||||||
companyId: attributes.companyId!,
|
companyId: attributes.companyId!,
|
||||||
status: attributes.status!,
|
status: attributes.status!,
|
||||||
|
documentSeriesId: attributes.documentSeriesId!,
|
||||||
|
proformaNumber: attributes.proformaNumber!,
|
||||||
series: attributes.series!,
|
series: attributes.series!,
|
||||||
proformaReference: attributes.invoiceNumber!,
|
proformaReference: attributes.invoiceNumber!,
|
||||||
proformaDate: attributes.invoiceDate!,
|
proformaDate: attributes.invoiceDate!,
|
||||||
@ -235,6 +249,8 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
|
|||||||
|
|
||||||
// Flags / estado / serie / número
|
// Flags / estado / serie / número
|
||||||
status: source.status.toPrimitive(),
|
status: source.status.toPrimitive(),
|
||||||
|
document_series_id: maybeToNullable(source.documentSeriesId, (value) => value),
|
||||||
|
proforma_number: maybeToNullable(source.proformaNumber, (value) => value.toPrimitive()),
|
||||||
proforma_reference: source.proformaReference.toPrimitive(),
|
proforma_reference: source.proformaReference.toPrimitive(),
|
||||||
proforma_date: source.invoiceDate.toPrimitive(),
|
proforma_date: source.invoiceDate.toPrimitive(),
|
||||||
operation_date: maybeToNullable(source.operationDate, (v) => v.toPrimitive()),
|
operation_date: maybeToNullable(source.operationDate, (v) => v.toPrimitive()),
|
||||||
|
|||||||
@ -1,74 +1,40 @@
|
|||||||
|
import type { DocumentSeriesPublicServicesType } from "@erp/document-series/api";
|
||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { type Maybe, Result } from "@repo/rdx-utils";
|
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||||
import type { Transaction, WhereOptions } from "sequelize";
|
import type { Transaction } from "sequelize";
|
||||||
|
|
||||||
import type { IProformaNumberGenerator } from "../../../../../application/proformas";
|
import type { IProformaNumberGenerator } from "../../../../../application/proformas";
|
||||||
import { InvoiceNumber, type InvoiceSerie } from "../../../../../domain";
|
import type { InvoiceSerie } from "../../../../../domain";
|
||||||
import { ProformaModel } from "../../../../common/persistence";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Numeracion V2 basada en la tabla fisica `proformas`.
|
* Adapter V2 para delegar la numeracion de proformas en document-series.
|
||||||
* Queda preparada para el cutover, pero no debe activarse mientras legacy siga en produccion.
|
|
||||||
*/
|
*/
|
||||||
export class SequelizeProformaV2NumberGenerator implements IProformaNumberGenerator {
|
export class SequelizeProformaV2NumberGenerator implements IProformaNumberGenerator {
|
||||||
private static readonly PREFIX = "PF-";
|
public constructor(
|
||||||
private static readonly DIGITS = 4;
|
private readonly documentSeriesServices: DocumentSeriesPublicServicesType
|
||||||
|
) {}
|
||||||
|
|
||||||
public async getNextForCompany(
|
public async getNextForCompany(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
_series: Maybe<InvoiceSerie>,
|
proformaSeriesCode: Maybe<InvoiceSerie>,
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
): Promise<Result<InvoiceNumber, Error>> {
|
) {
|
||||||
const where: WhereOptions = {
|
return this.documentSeriesServices.assignNextNumber({
|
||||||
company_id: companyId.toString(),
|
companyId: companyId.toString(),
|
||||||
};
|
documentType: "proforma",
|
||||||
|
seriesCode: proformaSeriesCode.getOrUndefined()?.toPrimitive() ?? null,
|
||||||
try {
|
transaction,
|
||||||
const existingInvoices = await ProformaModel.findAll({
|
}).then((assignmentResult) => {
|
||||||
attributes: ["proforma_reference"],
|
if (assignmentResult.isFailure) {
|
||||||
where,
|
return Result.fail(assignmentResult.error);
|
||||||
transaction,
|
|
||||||
raw: true,
|
|
||||||
lock: transaction.LOCK.UPDATE,
|
|
||||||
});
|
|
||||||
|
|
||||||
const maxSequence = existingInvoices.reduce((max, currentInvoice) => {
|
|
||||||
const currentSequence = this.extractSequence(currentInvoice.proforma_reference);
|
|
||||||
return Math.max(max, currentSequence);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const nextValue = this.formatSequence(maxSequence + 1);
|
|
||||||
|
|
||||||
const numberResult = InvoiceNumber.create(nextValue);
|
|
||||||
if (numberResult.isFailure) {
|
|
||||||
return Result.fail(numberResult.error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.ok(numberResult.data);
|
return Result.ok({
|
||||||
} catch (error) {
|
documentSeriesId: assignmentResult.data.series_id,
|
||||||
return Result.fail(
|
documentSeriesCode: assignmentResult.data.series_code,
|
||||||
new Error(
|
number: assignmentResult.data.number,
|
||||||
`Error generating proforma number for company ${companyId}: ${(error as Error).message}`
|
reference: assignmentResult.data.reference,
|
||||||
)
|
});
|
||||||
);
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractSequence(invoiceNumber: string): number {
|
|
||||||
const normalized = invoiceNumber.trim().toUpperCase();
|
|
||||||
|
|
||||||
if (normalized.startsWith(SequelizeProformaV2NumberGenerator.PREFIX)) {
|
|
||||||
const suffix = normalized.slice(SequelizeProformaV2NumberGenerator.PREFIX.length);
|
|
||||||
const parsed = Number(suffix);
|
|
||||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = Number(normalized);
|
|
||||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatSequence(sequence: number): string {
|
|
||||||
const padded = String(sequence).padStart(SequelizeProformaV2NumberGenerator.DIGITS, "0");
|
|
||||||
return `${SequelizeProformaV2NumberGenerator.PREFIX}${padded}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,51 @@
|
|||||||
|
import type { DocumentSeriesPublicServicesType } from "@erp/document-series/api";
|
||||||
|
import { DomainValidationError } from "@repo/rdx-ddd";
|
||||||
|
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
EnsureValidProformaSeriesParams,
|
||||||
|
ProformaSeriesValidator,
|
||||||
|
} from "../../../application";
|
||||||
|
import type { InvoiceSerie } from "../../../domain";
|
||||||
|
|
||||||
|
export class DocumentSeriesProformaSeriesValidator implements ProformaSeriesValidator {
|
||||||
|
public constructor(private readonly documentSeriesServices: DocumentSeriesPublicServicesType) {}
|
||||||
|
|
||||||
|
public async ensureValidProformaSeries(
|
||||||
|
params: EnsureValidProformaSeriesParams
|
||||||
|
): Promise<Result<void, Error>> {
|
||||||
|
const proformaSeriesCode = this.normalize(params.proformaSeriesCode);
|
||||||
|
|
||||||
|
if (proformaSeriesCode === null) {
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
const listResult = await this.documentSeriesServices.listActiveSeries({
|
||||||
|
companyId: params.companyId.toString(),
|
||||||
|
documentType: "proforma",
|
||||||
|
transaction: params.transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (listResult.isFailure) {
|
||||||
|
return Result.fail(listResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = listResult.data.some((series) => series.code === proformaSeriesCode);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
return Result.fail(
|
||||||
|
new DomainValidationError(
|
||||||
|
"INVALID_PROFORMA_SERIES_CODE",
|
||||||
|
"proforma_series_code",
|
||||||
|
"Proforma series code must reference an active proforma series"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalize(proformaSeriesCode: Maybe<InvoiceSerie>): string | null {
|
||||||
|
return proformaSeriesCode.isSome() ? proformaSeriesCode.unwrap().toPrimitive() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import type { DocumentSeriesPublicServicesType } from "@erp/document-series/api";
|
||||||
|
import { DomainValidationError } from "@repo/rdx-ddd";
|
||||||
|
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
EnsureValidIssuedInvoiceSeriesParams,
|
||||||
|
ProformaTargetInvoiceSeriesValidator,
|
||||||
|
} from "../../../application";
|
||||||
|
import type { InvoiceSerie } from "../../../domain";
|
||||||
|
|
||||||
|
export class DocumentSeriesProformaTargetInvoiceSeriesValidator
|
||||||
|
implements ProformaTargetInvoiceSeriesValidator
|
||||||
|
{
|
||||||
|
public constructor(private readonly documentSeriesServices: DocumentSeriesPublicServicesType) {}
|
||||||
|
|
||||||
|
public async ensureValidIssuedInvoiceSeries(
|
||||||
|
params: EnsureValidIssuedInvoiceSeriesParams
|
||||||
|
): Promise<Result<void, Error>> {
|
||||||
|
const targetInvoiceSeriesCode = this.normalize(params.targetInvoiceSeriesCode);
|
||||||
|
|
||||||
|
if (targetInvoiceSeriesCode === null) {
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
const listResult = await this.documentSeriesServices.listActiveSeries({
|
||||||
|
companyId: params.companyId.toString(),
|
||||||
|
documentType: "issued_invoice",
|
||||||
|
transaction: params.transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (listResult.isFailure) {
|
||||||
|
return Result.fail(listResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = listResult.data.some((series) => series.code === targetInvoiceSeriesCode);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
return Result.fail(
|
||||||
|
new DomainValidationError(
|
||||||
|
"INVALID_TARGET_INVOICE_SERIES_CODE",
|
||||||
|
"target_invoice_series_code",
|
||||||
|
"Target invoice series code must reference an active issued_invoice series"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalize(targetInvoiceSeriesCode: Maybe<InvoiceSerie>): string | null {
|
||||||
|
return targetInvoiceSeriesCode.isSome() ? targetInvoiceSeriesCode.unwrap().toPrimitive() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,7 +29,8 @@ export const CreateProformaRequestSchema = z.object({
|
|||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
|
|
||||||
proforma_reference: z.string().nullable().optional(),
|
proforma_reference: z.string().nullable().optional(),
|
||||||
series: z.string().nullable(),
|
proforma_series_code: z.string().nullable().optional(),
|
||||||
|
target_invoice_series_code: z.string().nullable().optional(),
|
||||||
|
|
||||||
proforma_date: IsoDateSchema,
|
proforma_date: IsoDateSchema,
|
||||||
operation_date: IsoDateSchema.nullable().optional(),
|
operation_date: IsoDateSchema.nullable().optional(),
|
||||||
@ -49,7 +50,7 @@ export const CreateProformaRequestSchema = z.object({
|
|||||||
payment_term_id: z.uuid().nullable().optional(),
|
payment_term_id: z.uuid().nullable().optional(),
|
||||||
tax_regime_code: z.string().nullable().optional(),
|
tax_regime_code: z.string().nullable().optional(),
|
||||||
|
|
||||||
items: z.array(CreateProformaItemRequestSchema),
|
items: z.array(CreateProformaItemRequestSchema).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateProformaRequestDTO = z.infer<typeof CreateProformaRequestSchema>;
|
export type CreateProformaRequestDTO = z.infer<typeof CreateProformaRequestSchema>;
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export const UpdateProformaByIdParamsRequestSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateProformaByIdRequestSchema = z.object({
|
export const UpdateProformaByIdRequestSchema = z.object({
|
||||||
series: z.string().nullable().optional(),
|
target_invoice_series_code: z.string().nullable().optional(),
|
||||||
|
|
||||||
proforma_date: IsoDateSchema.optional(),
|
proforma_date: IsoDateSchema.optional(),
|
||||||
operation_date: IsoDateSchema.nullable().optional(),
|
operation_date: IsoDateSchema.nullable().optional(),
|
||||||
|
|||||||
@ -1,3 +1,2 @@
|
|||||||
export * from "./invoice-series";
|
|
||||||
export * from "./issued-invoices";
|
export * from "./issued-invoices";
|
||||||
export * from "./proformas";
|
export * from "./proformas";
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./list-invoice-series.response.dto";
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { z } from "zod/v4";
|
|
||||||
|
|
||||||
import { InvoiceSeriesSummarySchema } from "../../shared/invoice-series";
|
|
||||||
|
|
||||||
export const ListInvoiceSeriesResponseSchema = z.object({
|
|
||||||
items: z.array(InvoiceSeriesSummarySchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ListInvoiceSeriesResponseDTO = z.infer<typeof ListInvoiceSeriesResponseSchema>;
|
|
||||||
@ -26,6 +26,7 @@ export const GetProformaByIdResponseSchema = z.object({
|
|||||||
|
|
||||||
proforma_reference: z.string(),
|
proforma_reference: z.string(),
|
||||||
status: ProformaStatusSchema,
|
status: ProformaStatusSchema,
|
||||||
|
target_invoice_series_code: z.string().nullable(),
|
||||||
series: z.string().nullable(),
|
series: z.string().nullable(),
|
||||||
|
|
||||||
proforma_date: IsoDateSchema,
|
proforma_date: IsoDateSchema,
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
export * from "./issued-invoices";
|
export * from "./issued-invoices";
|
||||||
export * from "./invoice-series";
|
|
||||||
export * from "./item-position.dto";
|
export * from "./item-position.dto";
|
||||||
export * from "./payment-method-ref.dto";
|
export * from "./payment-method-ref.dto";
|
||||||
export * from "./payment-term-ref.dto";
|
export * from "./payment-term-ref.dto";
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./invoice-series-summary.dto";
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { z } from "zod/v4";
|
|
||||||
|
|
||||||
export const InvoiceSeriesSummarySchema = z.object({
|
|
||||||
id: z.uuid(),
|
|
||||||
code: z.string(),
|
|
||||||
next_number: z.number().int().nonnegative(),
|
|
||||||
padding_length: z.number().int().nonnegative(),
|
|
||||||
is_default: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type InvoiceSeriesSummaryDTO = z.infer<typeof InvoiceSeriesSummarySchema>;
|
|
||||||
@ -10,6 +10,7 @@ export const ProformaSummarySchema = z.object({
|
|||||||
|
|
||||||
proforma_reference: z.string(),
|
proforma_reference: z.string(),
|
||||||
status: ProformaStatusSchema,
|
status: ProformaStatusSchema,
|
||||||
|
target_invoice_series_code: z.string().nullable(),
|
||||||
series: z.string().nullable(),
|
series: z.string().nullable(),
|
||||||
|
|
||||||
proforma_date: IsoDateSchema,
|
proforma_date: IsoDateSchema,
|
||||||
|
|||||||
@ -267,7 +267,7 @@
|
|||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
"invoice_date": {
|
"proforma_date": {
|
||||||
"label": "Proforma date",
|
"label": "Proforma date",
|
||||||
"placeholder": "Select a date",
|
"placeholder": "Select a date",
|
||||||
"description": "Proforma date"
|
"description": "Proforma date"
|
||||||
|
|||||||
@ -259,8 +259,8 @@
|
|||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
"invoice_date": {
|
"proforma_date": {
|
||||||
"label": "Fecha",
|
"label": "Fecha de la proforma",
|
||||||
"placeholder": "Selecciona una fecha",
|
"placeholder": "Selecciona una fecha",
|
||||||
"description": "Fecha de emisión de la proforma"
|
"description": "Fecha de emisión de la proforma"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,7 +12,8 @@ export const buildCreateProformaParams = (
|
|||||||
data: {
|
data: {
|
||||||
id: proformaId,
|
id: proformaId,
|
||||||
proforma_reference: "",
|
proforma_reference: "",
|
||||||
series: formData.series.trim() || null,
|
proforma_series_code: formData.proformaSeriesCode.trim() || null,
|
||||||
|
target_invoice_series_code: formData.targetInvoiceSeriesCode.trim() || null,
|
||||||
proforma_date: formData.proformaDate,
|
proforma_date: formData.proformaDate,
|
||||||
customer_id: formData.customerId,
|
customer_id: formData.customerId,
|
||||||
language_code: formData.languageCode,
|
language_code: formData.languageCode,
|
||||||
@ -21,7 +22,6 @@ export const buildCreateProformaParams = (
|
|||||||
payment_method_id: null,
|
payment_method_id: null,
|
||||||
payment_term_id: null,
|
payment_term_id: null,
|
||||||
tax_regime_code: null,
|
tax_regime_code: null,
|
||||||
items: [],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -19,8 +19,8 @@ import type { FieldErrors } from "react-hook-form";
|
|||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import type { Proforma } from "../../shared";
|
import type { Proforma } from "../../shared";
|
||||||
import {
|
import {
|
||||||
buildInvoiceSeriesSelectItems,
|
buildDocumentSeriesSelectItems,
|
||||||
useInvoiceSeriesQuery,
|
useDocumentSeriesQuery,
|
||||||
useProformaCreateMutation,
|
useProformaCreateMutation,
|
||||||
} from "../../shared";
|
} from "../../shared";
|
||||||
import { buildCreateProformaParams } from "../adapters";
|
import { buildCreateProformaParams } from "../adapters";
|
||||||
@ -55,7 +55,8 @@ export const useCreateProformaController = (options?: UseCreateProformaControlle
|
|||||||
error: createError,
|
error: createError,
|
||||||
} = useProformaCreateMutation();
|
} = useProformaCreateMutation();
|
||||||
|
|
||||||
const seriesQuery = useInvoiceSeriesQuery();
|
const proformaSeriesQuery = useDocumentSeriesQuery("proforma");
|
||||||
|
const targetInvoiceSeriesQuery = useDocumentSeriesQuery("issued_invoice");
|
||||||
|
|
||||||
const initialValues = useMemo<ProformaCreateForm>(() => buildProformaCreateDefault(), []);
|
const initialValues = useMemo<ProformaCreateForm>(() => buildProformaCreateDefault(), []);
|
||||||
|
|
||||||
@ -65,27 +66,35 @@ export const useCreateProformaController = (options?: UseCreateProformaControlle
|
|||||||
disabled: isCreating,
|
disabled: isCreating,
|
||||||
});
|
});
|
||||||
|
|
||||||
const seriesOptions = useMemo(
|
const proformaSeriesOptions = useMemo(
|
||||||
() => buildInvoiceSeriesSelectItems(seriesQuery.data ?? []),
|
() => buildDocumentSeriesSelectItems(proformaSeriesQuery.data ?? []),
|
||||||
[seriesQuery.data]
|
[proformaSeriesQuery.data]
|
||||||
);
|
);
|
||||||
|
|
||||||
const defaultSeries = useMemo(() => {
|
const targetInvoiceSeriesOptions = useMemo(
|
||||||
const invoiceSeries = seriesQuery.data ?? [];
|
() => [
|
||||||
|
{ value: "", label: "Automática" },
|
||||||
|
...buildDocumentSeriesSelectItems(targetInvoiceSeriesQuery.data ?? []),
|
||||||
|
],
|
||||||
|
[targetInvoiceSeriesQuery.data]
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultProformaSeries = useMemo(() => {
|
||||||
|
const invoiceSeries = proformaSeriesQuery.data ?? [];
|
||||||
|
|
||||||
return invoiceSeries.find((series) => series.isDefault)?.code ?? invoiceSeries[0]?.code ?? "";
|
return invoiceSeries.find((series) => series.isDefault)?.code ?? invoiceSeries[0]?.code ?? "";
|
||||||
}, [seriesQuery.data]);
|
}, [proformaSeriesQuery.data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (form.getValues("series")) return;
|
if (form.getValues("proformaSeriesCode")) return;
|
||||||
if (!defaultSeries) return;
|
if (!defaultProformaSeries) return;
|
||||||
|
|
||||||
form.setValue("series", defaultSeries, {
|
form.setValue("proformaSeriesCode", defaultProformaSeries, {
|
||||||
shouldDirty: false,
|
shouldDirty: false,
|
||||||
shouldTouch: false,
|
shouldTouch: false,
|
||||||
shouldValidate: true,
|
shouldValidate: true,
|
||||||
});
|
});
|
||||||
}, [defaultSeries, form]);
|
}, [defaultProformaSeries, form]);
|
||||||
|
|
||||||
const setCustomer = (customer: CustomerSelectionOption) => {
|
const setCustomer = (customer: CustomerSelectionOption) => {
|
||||||
setSelectedCustomer(customer);
|
setSelectedCustomer(customer);
|
||||||
@ -128,7 +137,7 @@ export const useCreateProformaController = (options?: UseCreateProformaControlle
|
|||||||
form.reset(
|
form.reset(
|
||||||
{
|
{
|
||||||
...buildProformaCreateDefault(),
|
...buildProformaCreateDefault(),
|
||||||
series: formData.series,
|
proformaSeriesCode: formData.proformaSeriesCode,
|
||||||
},
|
},
|
||||||
{ keepDirty: false }
|
{ keepDirty: false }
|
||||||
);
|
);
|
||||||
@ -179,9 +188,11 @@ export const useCreateProformaController = (options?: UseCreateProformaControlle
|
|||||||
selectedCustomer,
|
selectedCustomer,
|
||||||
setCustomer,
|
setCustomer,
|
||||||
clearCustomer,
|
clearCustomer,
|
||||||
seriesOptions,
|
proformaSeriesOptions,
|
||||||
isSeriesLoading: seriesQuery.isLoading,
|
targetInvoiceSeriesOptions,
|
||||||
isSeriesLoadError: seriesQuery.isError,
|
isProformaSeriesLoading: proformaSeriesQuery.isLoading,
|
||||||
seriesLoadError: seriesQuery.error,
|
isTargetInvoiceSeriesLoading: targetInvoiceSeriesQuery.isLoading,
|
||||||
|
isSeriesLoadError: proformaSeriesQuery.isError || targetInvoiceSeriesQuery.isError,
|
||||||
|
seriesLoadError: proformaSeriesQuery.error ?? targetInvoiceSeriesQuery.error,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
export interface ProformaCreateForm {
|
export interface ProformaCreateForm {
|
||||||
customerId: string;
|
customerId: string;
|
||||||
proformaDate: string;
|
proformaDate: string;
|
||||||
series: string;
|
proformaSeriesCode: string;
|
||||||
|
targetInvoiceSeriesCode: string;
|
||||||
languageCode: string;
|
languageCode: string;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,8 @@ export const ProformaCreateFormSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.min(1, "Introduce una fecha válida.")
|
.min(1, "Introduce una fecha válida.")
|
||||||
.regex(ISO_DATE_PATTERN, "Introduce una fecha válida."),
|
.regex(ISO_DATE_PATTERN, "Introduce una fecha válida."),
|
||||||
series: z.string(),
|
proformaSeriesCode: z.string(),
|
||||||
|
targetInvoiceSeriesCode: z.string(),
|
||||||
languageCode: z.string().min(1),
|
languageCode: z.string().min(1),
|
||||||
currencyCode: z.string().min(1),
|
currencyCode: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,10 +7,10 @@ export const ProformaCreateAutoAppliedInfo = () => {
|
|||||||
<AlertCircleIcon className="mt-0.5 size-5 text-blue-600" />
|
<AlertCircleIcon className="mt-0.5 size-5 text-blue-600" />
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold">Se aplicará automáticamente</h2>
|
<h2 className="text-lg font-semibold">Se preparará automáticamente</h2>
|
||||||
<p className="text-sm text-slate-600">
|
<p className="text-sm text-slate-600">
|
||||||
Los siguientes datos se tomarán del cliente o de la configuración de empresa y
|
Estos valores iniciales se tomarán del cliente o de la configuración de empresa.
|
||||||
podrás modificarlos después en la edición.
|
Podrás revisarlos y modificarlos después en la edición.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -45,8 +45,8 @@ export const ProformaCreateAutoAppliedInfo = () => {
|
|||||||
<div>
|
<div>
|
||||||
<p className="font-medium">Fiscalidad inicial</p>
|
<p className="font-medium">Fiscalidad inicial</p>
|
||||||
<p className="text-sm text-slate-600">
|
<p className="text-sm text-slate-600">
|
||||||
La fiscalidad se tomará del cliente o de la configuración de empresa y podrás
|
Se tomará del cliente o de la configuración de empresa. Los impuestos no se
|
||||||
modificarla después en la edición.
|
calcularán hasta que añadas líneas a la proforma.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type { ProformaCreateForm } from "../../entities";
|
|||||||
|
|
||||||
interface ProformaCreateCustomerFieldProps {
|
interface ProformaCreateCustomerFieldProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
selectedCustomer?: CustomerSelectionOption | null;
|
selectedCustomer?: CustomerSelectionOption | null;
|
||||||
onSelectCustomer: () => void;
|
onSelectCustomer: () => void;
|
||||||
onClearCustomer: () => void;
|
onClearCustomer: () => void;
|
||||||
@ -15,6 +16,7 @@ interface ProformaCreateCustomerFieldProps {
|
|||||||
|
|
||||||
export const ProformaCreateCustomerField = ({
|
export const ProformaCreateCustomerField = ({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
className,
|
||||||
selectedCustomer,
|
selectedCustomer,
|
||||||
onSelectCustomer,
|
onSelectCustomer,
|
||||||
onClearCustomer,
|
onClearCustomer,
|
||||||
@ -28,7 +30,7 @@ export const ProformaCreateCustomerField = ({
|
|||||||
control={control}
|
control={control}
|
||||||
name="customerId"
|
name="customerId"
|
||||||
render={({ fieldState }) => (
|
render={({ fieldState }) => (
|
||||||
<Field className="gap-1" data-invalid={fieldState.invalid}>
|
<Field className={cn("gap-1", className)} data-invalid={fieldState.invalid}>
|
||||||
<label className="text-sm font-medium leading-none" htmlFor={triggerId}>
|
<label className="text-sm font-medium leading-none" htmlFor={triggerId}>
|
||||||
Cliente <span className="text-destructive">*</span>
|
Cliente <span className="text-destructive">*</span>
|
||||||
</label>
|
</label>
|
||||||
@ -46,7 +48,9 @@ export const ProformaCreateCustomerField = ({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{selectedCustomer?.name || selectedCustomer?.tradeName || "Buscar o seleccionar cliente..."}
|
{selectedCustomer?.name ||
|
||||||
|
selectedCustomer?.tradeName ||
|
||||||
|
"Buscar o seleccionar cliente..."}
|
||||||
</span>
|
</span>
|
||||||
<ChevronsUpDownIcon className="ml-2 size-4 shrink-0 opacity-60" />
|
<ChevronsUpDownIcon className="ml-2 size-4 shrink-0 opacity-60" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -9,24 +9,28 @@ import { ProformaCreateInitialInfoCard } from "./proforma-create-initial-info-ca
|
|||||||
interface ProformaCreateFormProps {
|
interface ProformaCreateFormProps {
|
||||||
formId: string;
|
formId: string;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
isSeriesLoading?: boolean;
|
isProformaSeriesLoading?: boolean;
|
||||||
|
isTargetInvoiceSeriesLoading?: boolean;
|
||||||
onSubmit: React.SubmitEventHandler<HTMLFormElement>;
|
onSubmit: React.SubmitEventHandler<HTMLFormElement>;
|
||||||
selectedCustomer?: CustomerSelectionOption | null;
|
selectedCustomer?: CustomerSelectionOption | null;
|
||||||
onSelectCustomer: () => void;
|
onSelectCustomer: () => void;
|
||||||
onClearCustomer: () => void;
|
onClearCustomer: () => void;
|
||||||
seriesOptions: { value: string; label: string }[];
|
proformaSeriesOptions: { value: string; label: string }[];
|
||||||
|
targetInvoiceSeriesOptions: { value: string; label: string }[];
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProformaCreateForm = ({
|
export const ProformaCreateForm = ({
|
||||||
formId,
|
formId,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
isSeriesLoading = false,
|
isProformaSeriesLoading = false,
|
||||||
|
isTargetInvoiceSeriesLoading = false,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
selectedCustomer,
|
selectedCustomer,
|
||||||
onSelectCustomer,
|
onSelectCustomer,
|
||||||
onClearCustomer,
|
onClearCustomer,
|
||||||
seriesOptions,
|
proformaSeriesOptions,
|
||||||
|
targetInvoiceSeriesOptions,
|
||||||
className,
|
className,
|
||||||
}: ProformaCreateFormProps) => {
|
}: ProformaCreateFormProps) => {
|
||||||
return (
|
return (
|
||||||
@ -35,11 +39,13 @@ export const ProformaCreateForm = ({
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<ProformaCreateInitialInfoCard
|
<ProformaCreateInitialInfoCard
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
isSeriesLoading={isSeriesLoading}
|
isProformaSeriesLoading={isProformaSeriesLoading}
|
||||||
|
isTargetInvoiceSeriesLoading={isTargetInvoiceSeriesLoading}
|
||||||
onClearCustomer={onClearCustomer}
|
onClearCustomer={onClearCustomer}
|
||||||
onSelectCustomer={onSelectCustomer}
|
onSelectCustomer={onSelectCustomer}
|
||||||
|
proformaSeriesOptions={proformaSeriesOptions}
|
||||||
selectedCustomer={selectedCustomer}
|
selectedCustomer={selectedCustomer}
|
||||||
seriesOptions={seriesOptions}
|
targetInvoiceSeriesOptions={targetInvoiceSeriesOptions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ProformaCreateAutoAppliedInfo />
|
<ProformaCreateAutoAppliedInfo />
|
||||||
|
|||||||
@ -14,35 +14,38 @@ import { ProformaCreateCustomerField } from "./proforma-create-customer-field";
|
|||||||
|
|
||||||
interface ProformaCreateInitialInfoCardProps {
|
interface ProformaCreateInitialInfoCardProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isSeriesLoading?: boolean;
|
isProformaSeriesLoading?: boolean;
|
||||||
|
isTargetInvoiceSeriesLoading?: boolean;
|
||||||
selectedCustomer?: CustomerSelectionOption | null;
|
selectedCustomer?: CustomerSelectionOption | null;
|
||||||
onSelectCustomer: () => void;
|
onSelectCustomer: () => void;
|
||||||
onClearCustomer: () => void;
|
onClearCustomer: () => void;
|
||||||
seriesOptions: SelectFieldItem[];
|
proformaSeriesOptions: SelectFieldItem[];
|
||||||
|
targetInvoiceSeriesOptions: SelectFieldItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProformaCreateInitialInfoCard = ({
|
export const ProformaCreateInitialInfoCard = ({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
isSeriesLoading = false,
|
isProformaSeriesLoading = false,
|
||||||
|
isTargetInvoiceSeriesLoading = false,
|
||||||
selectedCustomer,
|
selectedCustomer,
|
||||||
onSelectCustomer,
|
onSelectCustomer,
|
||||||
onClearCustomer,
|
onClearCustomer,
|
||||||
seriesOptions,
|
proformaSeriesOptions,
|
||||||
|
targetInvoiceSeriesOptions,
|
||||||
}: ProformaCreateInitialInfoCardProps) => {
|
}: ProformaCreateInitialInfoCardProps) => {
|
||||||
return (
|
return (
|
||||||
<FormSectionCard icon={<FileTextIcon className="size-5" />} title="Información inicial">
|
<FormSectionCard icon={<FileTextIcon className="size-5" />} title="Información inicial">
|
||||||
<FormSectionGrid>
|
<FormSectionGrid className="md:grid-cols-6 2xl:grid-cols-12">
|
||||||
<div className="md:col-span-5">
|
<ProformaCreateCustomerField
|
||||||
<ProformaCreateCustomerField
|
className="md:col-span-4"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClearCustomer={onClearCustomer}
|
onClearCustomer={onClearCustomer}
|
||||||
onSelectCustomer={onSelectCustomer}
|
onSelectCustomer={onSelectCustomer}
|
||||||
selectedCustomer={selectedCustomer}
|
selectedCustomer={selectedCustomer}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<DatePickerField<ProformaCreateForm>
|
<DatePickerField<ProformaCreateForm>
|
||||||
className="md:col-span-3"
|
className="md:col-span-2"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label="Fecha"
|
label="Fecha"
|
||||||
name="proformaDate"
|
name="proformaDate"
|
||||||
@ -51,19 +54,28 @@ export const ProformaCreateInitialInfoCard = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectField<ProformaCreateForm>
|
<SelectField<ProformaCreateForm>
|
||||||
className="md:col-span-4"
|
className="md:col-span-2"
|
||||||
disabled={disabled || isSeriesLoading}
|
disabled={disabled || isProformaSeriesLoading}
|
||||||
items={seriesOptions}
|
items={proformaSeriesOptions}
|
||||||
label="Serie"
|
label="Serie de esta proforma"
|
||||||
name="series"
|
name="proformaSeriesCode"
|
||||||
placeholder={
|
placeholder={
|
||||||
isSeriesLoading
|
isProformaSeriesLoading
|
||||||
? "Cargando series..."
|
? "Cargando series..."
|
||||||
: seriesOptions.length > 0
|
: proformaSeriesOptions.length > 0
|
||||||
? "Selecciona una serie"
|
? "Selecciona una serie"
|
||||||
: "Sin serie"
|
: "Sin serie"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectField<ProformaCreateForm>
|
||||||
|
className="md:col-span-2"
|
||||||
|
disabled={disabled || isTargetInvoiceSeriesLoading}
|
||||||
|
items={targetInvoiceSeriesOptions}
|
||||||
|
label="Serie de la factura"
|
||||||
|
name="targetInvoiceSeriesCode"
|
||||||
|
placeholder={isTargetInvoiceSeriesLoading ? "Cargando series..." : "Automática"}
|
||||||
|
/>
|
||||||
</FormSectionGrid>
|
</FormSectionGrid>
|
||||||
</FormSectionCard>
|
</FormSectionCard>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -54,13 +54,15 @@ export const ProformaCreatePage = () => {
|
|||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<ProformaCreateForm
|
<ProformaCreateForm
|
||||||
formId={createCtrl.formId}
|
formId={createCtrl.formId}
|
||||||
isSeriesLoading={createCtrl.isSeriesLoading}
|
isProformaSeriesLoading={createCtrl.isProformaSeriesLoading}
|
||||||
isSubmitting={createCtrl.isCreating}
|
isSubmitting={createCtrl.isCreating}
|
||||||
|
isTargetInvoiceSeriesLoading={createCtrl.isTargetInvoiceSeriesLoading}
|
||||||
onClearCustomer={createCtrl.clearCustomer}
|
onClearCustomer={createCtrl.clearCustomer}
|
||||||
onSelectCustomer={selectCustomerCtrl.selectCtrl.openDialog}
|
onSelectCustomer={selectCustomerCtrl.selectCtrl.openDialog}
|
||||||
onSubmit={createCtrl.onSubmit}
|
onSubmit={createCtrl.onSubmit}
|
||||||
|
proformaSeriesOptions={createCtrl.proformaSeriesOptions}
|
||||||
selectedCustomer={createCtrl.selectedCustomer}
|
selectedCustomer={createCtrl.selectedCustomer}
|
||||||
seriesOptions={createCtrl.seriesOptions}
|
targetInvoiceSeriesOptions={createCtrl.targetInvoiceSeriesOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user