refactor: update country references to use CountryCode in supplier and company modules
- Changed references from Country to CountryCode in supplier input mappers, audit services, and snapshot builders. - Updated tests to reflect the changes in country handling. - Refactored postal address to utilize CountryCode instead of Country. - Added new company address mappers and tests to accommodate the updated structure. - Ensured all related schemas and DTOs are aligned with the new country code implementation.
This commit is contained in:
parent
275fc0aca8
commit
a4398e163f
@ -45,11 +45,6 @@ Regla crítica vigente:
|
||||
|
||||
- [Índice SQL de `customer-invoices`](./sql/README.md)
|
||||
|
||||
## Histórico o transicional (`HISTORICAL`)
|
||||
|
||||
- [Split Proformas / Issued Invoices](./split-proformas-issued-invoices-migration.md)
|
||||
- [InvoiceSeries legacy](./invoice-series.md)
|
||||
|
||||
## Criterio de lectura
|
||||
|
||||
- Para semántica vigente de negocio, empieza por `proforma-series-contract.md`, `proforma-create-contract.md` y `proforma-tax-config.md`.
|
||||
|
||||
@ -1,173 +0,0 @@
|
||||
# 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`.
|
||||
> Las tablas `customer_invoices`, `customer_invoice_items` y `customer_invoice_taxes`
|
||||
> ya no forman parte del runtime actual.
|
||||
|
||||
## Decisión consolidada
|
||||
|
||||
Antes de la migración, el recurso backend para las series de facturación de cliente se llamaba:
|
||||
|
||||
```txt
|
||||
InvoiceSeries
|
||||
```
|
||||
|
||||
Su tabla Sequelize asociada era:
|
||||
|
||||
```txt
|
||||
customer_invoice_series
|
||||
```
|
||||
|
||||
Este contenido ya no describe el runtime activo del ERP.
|
||||
|
||||
No usar este documento para desarrollo nuevo. La referencia vigente es `docs/document-series/*`.
|
||||
|
||||
## Alcance funcional
|
||||
|
||||
`InvoiceSeries` representa la serie usada para numerar facturas emitidas de cliente.
|
||||
|
||||
La proforma:
|
||||
|
||||
```txt
|
||||
- guarda una referencia legacy en Proforma.series
|
||||
- ese valor corresponde a InvoiceSeries.code
|
||||
- no consume numeración de factura emitida
|
||||
```
|
||||
|
||||
## Modelo mínimo
|
||||
|
||||
Campos activos en V1:
|
||||
|
||||
```txt
|
||||
id
|
||||
company_id
|
||||
code
|
||||
next_number
|
||||
padding_length
|
||||
is_default
|
||||
is_active
|
||||
created_at
|
||||
updated_at
|
||||
```
|
||||
|
||||
No existen en V1:
|
||||
|
||||
```txt
|
||||
prefix
|
||||
name
|
||||
description
|
||||
document_type
|
||||
```
|
||||
|
||||
## Reglas de numeración
|
||||
|
||||
`next_number` es obligatorio y es la fuente de verdad para asignar el siguiente número de factura emitida.
|
||||
|
||||
El flujo de emisión desde proforma es:
|
||||
|
||||
```txt
|
||||
1. leer la proforma
|
||||
2. tomar Proforma.series como InvoiceSeries.code
|
||||
3. buscar InvoiceSeries por company_id + code
|
||||
4. validar que la serie existe y está activa
|
||||
5. bloquear la fila de customer_invoice_series dentro de la misma transacción
|
||||
6. leer next_number
|
||||
7. asignar ese número a la issued invoice
|
||||
8. incrementar next_number
|
||||
9. confirmar issued invoice + increment en una sola transacción
|
||||
```
|
||||
|
||||
No se permite:
|
||||
|
||||
```txt
|
||||
- MAX(invoice_number) + 1
|
||||
- consumir numeración al crear o actualizar proformas
|
||||
- separar el lock y el update en transacciones distintas
|
||||
```
|
||||
|
||||
## Formato visible
|
||||
|
||||
El formato visible deriva de:
|
||||
|
||||
```txt
|
||||
formatted_number = code + "-" + number.padStart(padding_length, "0")
|
||||
```
|
||||
|
||||
Ejemplo:
|
||||
|
||||
```txt
|
||||
code = A
|
||||
number = 23
|
||||
padding_length = 6
|
||||
|
||||
=> A-000023
|
||||
```
|
||||
|
||||
En V1 no existe `prefix`.
|
||||
`code` actúa a la vez como identificador funcional y prefijo visible.
|
||||
|
||||
## Integridad defensiva
|
||||
|
||||
En `customer_invoice_series` debe existir:
|
||||
|
||||
```sql
|
||||
UNIQUE (company_id, code)
|
||||
```
|
||||
|
||||
En `customer_invoices` la regla defensiva para numeración emitida es:
|
||||
|
||||
```txt
|
||||
company + series + invoice_number debe ser único
|
||||
```
|
||||
|
||||
Actualmente el modelo Sequelize usa:
|
||||
|
||||
```sql
|
||||
UNIQUE (company_id, series, invoice_number, is_proforma)
|
||||
```
|
||||
|
||||
Esto evita colisiones entre proformas y facturas emitidas mientras ambas comparten `customer_invoices`.
|
||||
|
||||
## Seeds e inicialización
|
||||
|
||||
No se ha añadido seed automático por empresa porque el repo no expone un sistema real de seeders versionados ni una estrategia fiable para inicializar todas las companies existentes.
|
||||
|
||||
Regla operativa:
|
||||
|
||||
```txt
|
||||
cada company necesita al menos una InvoiceSeries activa antes de emitir facturas
|
||||
```
|
||||
|
||||
Serie mínima recomendada:
|
||||
|
||||
```txt
|
||||
code = A
|
||||
next_number = 1
|
||||
padding_length = 6
|
||||
is_default = true
|
||||
is_active = true
|
||||
```
|
||||
|
||||
## Estado técnico del repo
|
||||
|
||||
En este repo no se localizó un framework de migraciones versionadas con `sequelize-cli`, `umzug` o equivalente.
|
||||
|
||||
El arranque del servidor registra modelos Sequelize y sincroniza base de datos mediante:
|
||||
|
||||
```txt
|
||||
database.sync({ alter: true })
|
||||
```
|
||||
|
||||
Por eso el SQL manual asociado a `customer_invoice_series` vive en:
|
||||
|
||||
[customer-invoice-series.sql](/Z:/docs/dev/customer-invoice-series.sql)
|
||||
|
||||
Ese archivo debe integrarse en el pipeline real de despliegue DB cuando exista.
|
||||
|
||||
## Sustitución vigente
|
||||
|
||||
- API canónica: `/document-series`
|
||||
- tabla canónica: `document_series`
|
||||
- proformas: `proforma_series_code` y `target_invoice_series_code`
|
||||
- SQL histórico de apoyo: `docs/dev/customer-invoice-series.sql`
|
||||
@ -1,919 +0,0 @@
|
||||
# Split Proformas / Issued Invoices
|
||||
|
||||
Estado: historico / transicional.
|
||||
No describe el runtime minimo vigente por si solo; la fuente operativa actual para proformas es la combinación de:
|
||||
|
||||
- `docs/document-series/README.md`
|
||||
- `docs/customer-invoices/proforma-series-contract.md`
|
||||
- `docs/customer-invoices/proforma-create-contract.md`
|
||||
- `docs/customer-invoices/proforma-tax-config.md`
|
||||
- `docs/catalogs/payment-terms.md`
|
||||
|
||||
Nota SSOT VeriFactu:
|
||||
|
||||
- cardinalidad vigente implementada: `IssuedInvoice 1 -> 0..1 VerifactuRecord`;
|
||||
- direccion futura deseada: `IssuedInvoice 1 -> 0..N VerifactuRecord`;
|
||||
- este documento no debe usarse para reintroducir `IssuedInvoice.verifactuPolicy`.
|
||||
|
||||
Estado: diseno tecnico / fases 1A-1D + 2A-2B
|
||||
Estado del backend: persistencia V2 activada; integracion con `document-series` activa para numeracion nueva; sin ejecucion confirmada de migracion DB
|
||||
|
||||
## Objetivo
|
||||
|
||||
Preparar el esquema fisico para separar:
|
||||
|
||||
- `customer_invoices` -> `proformas`
|
||||
- `customer_invoices` -> `issued_invoices`
|
||||
- `customer_invoice_items` -> `proforma_items`
|
||||
- `customer_invoice_items` -> `issued_invoice_items`
|
||||
- `customer_invoice_taxes` -> `proforma_taxes`
|
||||
- `customer_invoice_taxes` -> `issued_invoice_taxes`
|
||||
- `verifactu_records.invoice_id` -> `verifactu_records.issued_invoice_id`
|
||||
|
||||
## Decisiones de fase 1A
|
||||
|
||||
- `proforma_reference` es el identificador visible canonico de proforma.
|
||||
- `target_invoice_series_code` sustituye la semantica legacy de `series` en proformas.
|
||||
- `document_series_id` identifica la serie documental usada para numerar la proforma.
|
||||
- `proforma_number` guarda el numero propio asignado a la proforma.
|
||||
- `target_invoice_series_code` es opcional y solo puede apuntar a una serie de `issued_invoice`.
|
||||
- una serie `PF` o cualquier otra serie de `document_type = proforma` nunca debe persistirse en `target_invoice_series_code`.
|
||||
- si `target_invoice_series_code` queda `NULL`, la emision de la factura usara la serie default activa de `issued_invoice`.
|
||||
- `issued_invoices` usa `invoice_series_code`, `invoice_number`, `invoice_reference`, `invoice_date` y `source_proforma_id`.
|
||||
- `proformas` no guarda `issued_invoice_id`; la relacion se resuelve con `issued_invoices.source_proforma_id`.
|
||||
- Los impuestos del esquema actual son por documento, no por linea: `proforma_taxes` y `issued_invoice_taxes`.
|
||||
- `payment_method` y `payment_term` en `issued_invoices` son snapshot historico, no estado vivo de cobro.
|
||||
- No hay doble escritura.
|
||||
- Legacy queda congelado solo para validacion y rollback cuando se haga el cutover.
|
||||
|
||||
## Estado de fase 1B
|
||||
|
||||
- Existen mappers Sequelize V2 sobre `proformas`, `proforma_items`, `proforma_taxes`.
|
||||
- Existen mappers Sequelize V2 sobre `issued_invoices`, `issued_invoice_items`, `issued_invoice_taxes`.
|
||||
- Existen repositorios Sequelize V2 listos para convivir con los repositorios legacy.
|
||||
- El wiring activo legacy no se ha sustituido todavia.
|
||||
- El dominio/application sigue arrastrando naming legacy en algunos contratos (`series`, `invoiceNumber`, `linkedProformaId`); la traduccion queda encapsulada en Infrastructure V2.
|
||||
- Esta afirmación quedó obsoleta. El runtime actual ya soporta `payment_term` en Company, Customer, Proforma e Issued Invoice según `docs/catalogs/payment-terms.md`.
|
||||
|
||||
## Estado de fase 1C
|
||||
|
||||
- El wiring activo del modulo usa persistencia V2 en los composition roots principales.
|
||||
- `buildV2ProformaPersistence(...)` queda activo en:
|
||||
- `modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts`
|
||||
- `modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts`
|
||||
- `buildV2IssuedInvoicePersistence(...)` queda activo en:
|
||||
- `modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoices.di.ts`
|
||||
- `modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-public-services.ts`
|
||||
- La persistencia legacy no se borra, pero deja de ser la ruta operativa principal.
|
||||
- La emision `Proforma -> IssuedInvoice` ahora encadena sobre V2:
|
||||
- lee `proformas`
|
||||
- crea `issued_invoices`
|
||||
- crea `issued_invoice_items`
|
||||
- crea `issued_invoice_taxes`
|
||||
- marca la proforma como `issued`
|
||||
- usa `issued_invoices.source_proforma_id`
|
||||
- No hay doble escritura: cada composition root sigue instanciando una sola familia de repositorios.
|
||||
- `VerifactuRecord` queda preparado para la ruta V2 mediante `issued_invoice_id` como relacion principal.
|
||||
- La migracion/backfill real sigue pendiente. Si la BD real no tiene las tablas nuevas, el backend no podra arrancar correctamente con este wiring.
|
||||
|
||||
## Estado de fase 1D
|
||||
|
||||
- Existen scripts SQL separados para desarrollo:
|
||||
- `docs/customer-invoices/sql/dev-split-proformas-issued-invoices.sql`
|
||||
- `docs/customer-invoices/sql/validate-split-proformas-issued-invoices.sql`
|
||||
- El SQL de desarrollo es idempotente y conservador:
|
||||
- crea tablas V2 con `CREATE TABLE IF NOT EXISTS`
|
||||
- hace backfill con `NOT EXISTS`
|
||||
- no usa `DROP`, `TRUNCATE` ni `DELETE`
|
||||
- no sobrescribe filas ya migradas
|
||||
- no toca datos legacy salvo la columna transicional `verifactu_records.issued_invoice_id`
|
||||
- Se documenta el orden de ejecucion en `docs/customer-invoices/sql/README.md`.
|
||||
- En este workspace no se encontro una configuracion de BD de desarrollo inequivoca (`.env*` no presente en raiz del repo), por lo que no se ejecuto SQL real.
|
||||
- Tampoco se hizo validacion funcional contra runtime real porque falta una BD de desarrollo confirmada y migrada para esta fase.
|
||||
|
||||
## Dependencias compartidas y exclusivas
|
||||
|
||||
### Compartidas entre legacy y V2
|
||||
|
||||
- contratos de Application (`IProformaRepository`, `IIssuedInvoiceRepository`)
|
||||
- use cases, servicios de dominio y assemblers
|
||||
- `TransactionManager`
|
||||
- adaptadores legacy de `InvoiceSeriesNumberAssigner` para `IssuedInvoice`
|
||||
- resolvers de catalogos
|
||||
- servicios de documentos PDF/preview
|
||||
|
||||
### Exclusivas de legacy
|
||||
|
||||
- `CustomerInvoiceModel` como storage real de Proforma e IssuedInvoice
|
||||
- numerador de proformas sobre `customer_invoices.invoice_number` con `is_proforma = true`
|
||||
- repositorios Sequelize legacy de `customer_invoices`
|
||||
- `CustomerInvoiceSeriesModel` y `SequelizeInvoiceSeriesRepository` como wiring transicional no prioritario
|
||||
|
||||
### Exclusivas de V2
|
||||
|
||||
- tablas `proformas`, `proforma_items`, `proforma_taxes`
|
||||
- tablas `issued_invoices`, `issued_invoice_items`, `issued_invoice_taxes`
|
||||
- numerador V2 de proformas sobre `document-series`, con snapshot en `proformas.document_series_id`, `proforma_number` y `proforma_reference`
|
||||
- mappers y repositorios Sequelize V2
|
||||
|
||||
## Regla operativa actual de series en proformas
|
||||
|
||||
La semantica operativa vigente ya no se mantiene en este documento historico.
|
||||
|
||||
Fuente canonica actual:
|
||||
|
||||
- `docs/customer-invoices/proforma-series-contract.md`
|
||||
- `docs/customer-invoices/proforma-create-contract.md`
|
||||
- `docs/customer-invoices/proforma-tax-config.md`
|
||||
|
||||
Este documento conserva solo el contexto de migracion, fases y SQL historico.
|
||||
|
||||
## 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
|
||||
- la semantica detallada del alias legacy `series` se consolido despues en `proforma-series-contract.md`
|
||||
- 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`
|
||||
- las reglas operativas exactas se consolidaron despues en `proforma-series-contract.md`
|
||||
- la UI nueva de proformas ya envia `proforma_series_code` y `target_invoice_series_code`
|
||||
|
||||
## Estado Fase 2F
|
||||
|
||||
- create de proforma se consolida como creacion de cabecera/documento inicial
|
||||
- la semantica funcional exacta de create se consolido despues en `proforma-create-contract.md`
|
||||
|
||||
## Estado Fase 2G
|
||||
|
||||
- `DELETE /proformas/:proforma_id` vuelve a estar operativo sobre persistencia V2
|
||||
- el borrado es logico mediante `deleted_at`
|
||||
- el archivado es logico mediante `archived_at`
|
||||
- el listado V2 de proformas usa `Criteria filters[]` para `archived_at` y `status`
|
||||
- `GET /proformas/:id` permite abrir proformas archivadas
|
||||
- las reglas funcionales exactas de borrado y numeracion se consolidaron despues en `proforma-series-contract.md`
|
||||
- la validacion de borrado comprueba tambien `linked_invoice_id` y `issued_invoices.source_proforma_id`
|
||||
- la numeracion de `document-series` no se reutiliza ni se decrementa tras un borrado
|
||||
|
||||
## Rollback recomendado
|
||||
|
||||
- no aplicar nada en produccion en esta fase
|
||||
- si en desarrollo se ejecuta la DDL/migracion y hay que revertir, hacerlo con backup previo o clon de desarrollo, nunca con borrado destructivo improvisado
|
||||
- conservar `customer_invoice_series` solo como legado historico hasta ejecutar la limpieza operativa documentada
|
||||
|
||||
## Tablas nuevas previstas
|
||||
|
||||
### proformas
|
||||
|
||||
- `id`
|
||||
- `company_id`
|
||||
- `status`
|
||||
- `proforma_reference`
|
||||
- `proforma_date`
|
||||
- `operation_date`
|
||||
- `target_invoice_series_code`
|
||||
- `reference`
|
||||
- `description`
|
||||
- `notes`
|
||||
- `language_code`
|
||||
- `currency_code`
|
||||
- `customer_id`
|
||||
- snapshot cliente
|
||||
- `payment_method_id`
|
||||
- `payment_term_id`
|
||||
- `tax_regime_code`
|
||||
- totales monetarios actuales
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `deleted_at`
|
||||
|
||||
### issued_invoices
|
||||
|
||||
- `id`
|
||||
- `company_id`
|
||||
- `status`
|
||||
- `source_proforma_id`
|
||||
- `invoice_series_code`
|
||||
- `invoice_number`
|
||||
- `invoice_reference`
|
||||
- `invoice_date`
|
||||
- `operation_date`
|
||||
- `reference`
|
||||
- `description`
|
||||
- `notes`
|
||||
- `language_code`
|
||||
- `currency_code`
|
||||
- `customer_id`
|
||||
- snapshot cliente
|
||||
- `payment_method_id`
|
||||
- `payment_method_name`
|
||||
- `payment_method_description`
|
||||
- `payment_term_id`
|
||||
- `payment_term_name`
|
||||
- `payment_term_description`
|
||||
- `payment_term_snapshot_json`
|
||||
- `tax_regime_code`
|
||||
- `tax_regime_description`
|
||||
- totales monetarios actuales
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `deleted_at`
|
||||
|
||||
### Items y taxes
|
||||
|
||||
- `proforma_items.proforma_id`
|
||||
- `issued_invoice_items.issued_invoice_id`
|
||||
- `proforma_taxes.proforma_id`
|
||||
- `issued_invoice_taxes.issued_invoice_id`
|
||||
|
||||
### Verifactu
|
||||
|
||||
- `verifactu_records.issued_invoice_id` nuevo
|
||||
- `verifactu_records.invoice_id` legacy temporal durante la transicion de fase 1A
|
||||
|
||||
## SQL orientativo de backfill
|
||||
|
||||
### Proformas
|
||||
|
||||
```sql
|
||||
INSERT INTO proformas (
|
||||
id,
|
||||
company_id,
|
||||
status,
|
||||
proforma_reference,
|
||||
proforma_date,
|
||||
operation_date,
|
||||
target_invoice_series_code,
|
||||
reference,
|
||||
description,
|
||||
notes,
|
||||
language_code,
|
||||
currency_code,
|
||||
customer_id,
|
||||
payment_method_id,
|
||||
payment_term_id,
|
||||
tax_regime_code,
|
||||
subtotal_amount_value,
|
||||
subtotal_amount_scale,
|
||||
items_discount_amount_value,
|
||||
items_discount_amount_scale,
|
||||
global_discount_percentage_value,
|
||||
global_discount_percentage_scale,
|
||||
global_discount_amount_value,
|
||||
global_discount_amount_scale,
|
||||
total_discount_amount_value,
|
||||
total_discount_amount_scale,
|
||||
taxable_amount_value,
|
||||
taxable_amount_scale,
|
||||
iva_amount_value,
|
||||
iva_amount_scale,
|
||||
rec_amount_value,
|
||||
rec_amount_scale,
|
||||
retention_amount_value,
|
||||
retention_amount_scale,
|
||||
taxes_amount_value,
|
||||
taxes_amount_scale,
|
||||
total_amount_value,
|
||||
total_amount_scale,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
company_id,
|
||||
status,
|
||||
CASE
|
||||
WHEN series IS NULL OR series = '' THEN invoice_number
|
||||
ELSE CONCAT(series, '-', invoice_number)
|
||||
END AS proforma_reference,
|
||||
invoice_date,
|
||||
operation_date,
|
||||
series,
|
||||
reference,
|
||||
description,
|
||||
notes,
|
||||
language_code,
|
||||
currency_code,
|
||||
customer_id,
|
||||
payment_method_id,
|
||||
NULL AS payment_term_id,
|
||||
tax_regime_code,
|
||||
subtotal_amount_value,
|
||||
subtotal_amount_scale,
|
||||
items_discount_amount_value,
|
||||
items_discount_amount_scale,
|
||||
global_discount_percentage_value,
|
||||
global_discount_percentage_scale,
|
||||
global_discount_amount_value,
|
||||
global_discount_amount_scale,
|
||||
total_discount_amount_value,
|
||||
total_discount_amount_scale,
|
||||
taxable_amount_value,
|
||||
taxable_amount_scale,
|
||||
iva_amount_value,
|
||||
iva_amount_scale,
|
||||
rec_amount_value,
|
||||
rec_amount_scale,
|
||||
retention_amount_value,
|
||||
retention_amount_scale,
|
||||
taxes_amount_value,
|
||||
taxes_amount_scale,
|
||||
total_amount_value,
|
||||
total_amount_scale,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
FROM customer_invoices
|
||||
WHERE is_proforma = 1;
|
||||
```
|
||||
|
||||
### Issued invoices
|
||||
|
||||
```sql
|
||||
INSERT INTO issued_invoices (
|
||||
id,
|
||||
company_id,
|
||||
status,
|
||||
source_proforma_id,
|
||||
invoice_series_code,
|
||||
invoice_number,
|
||||
invoice_date,
|
||||
operation_date,
|
||||
reference,
|
||||
description,
|
||||
notes,
|
||||
language_code,
|
||||
currency_code,
|
||||
customer_id,
|
||||
customer_tin,
|
||||
customer_name,
|
||||
customer_street,
|
||||
customer_street2,
|
||||
customer_city,
|
||||
customer_province,
|
||||
customer_postal_code,
|
||||
customer_country,
|
||||
payment_method_id,
|
||||
payment_method_name,
|
||||
payment_method_description,
|
||||
payment_term_id,
|
||||
payment_term_name,
|
||||
payment_term_description,
|
||||
payment_term_snapshot_json,
|
||||
tax_regime_code,
|
||||
tax_regime_description,
|
||||
subtotal_amount_value,
|
||||
subtotal_amount_scale,
|
||||
items_discount_amount_value,
|
||||
items_discount_amount_scale,
|
||||
global_discount_percentage_value,
|
||||
global_discount_percentage_scale,
|
||||
global_discount_amount_value,
|
||||
global_discount_amount_scale,
|
||||
total_discount_amount_value,
|
||||
total_discount_amount_scale,
|
||||
taxable_amount_value,
|
||||
taxable_amount_scale,
|
||||
iva_amount_value,
|
||||
iva_amount_scale,
|
||||
rec_amount_value,
|
||||
rec_amount_scale,
|
||||
retention_amount_value,
|
||||
retention_amount_scale,
|
||||
taxes_amount_value,
|
||||
taxes_amount_scale,
|
||||
total_amount_value,
|
||||
total_amount_scale,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
company_id,
|
||||
status,
|
||||
proforma_id,
|
||||
series,
|
||||
invoice_number,
|
||||
invoice_date,
|
||||
operation_date,
|
||||
reference,
|
||||
description,
|
||||
notes,
|
||||
language_code,
|
||||
currency_code,
|
||||
customer_id,
|
||||
customer_tin,
|
||||
customer_name,
|
||||
customer_street,
|
||||
customer_street2,
|
||||
customer_city,
|
||||
customer_province,
|
||||
customer_postal_code,
|
||||
customer_country,
|
||||
payment_method_id,
|
||||
payment_method_description,
|
||||
payment_method_description,
|
||||
NULL AS payment_term_id,
|
||||
NULL AS payment_term_name,
|
||||
NULL AS payment_term_description,
|
||||
NULL AS payment_term_snapshot_json,
|
||||
COALESCE(tax_regime_code, "01"),
|
||||
COALESCE(tax_regime_description, "01: Operación de régimen general."),
|
||||
subtotal_amount_value,
|
||||
subtotal_amount_scale,
|
||||
items_discount_amount_value,
|
||||
items_discount_amount_scale,
|
||||
global_discount_percentage_value,
|
||||
global_discount_percentage_scale,
|
||||
global_discount_amount_value,
|
||||
global_discount_amount_scale,
|
||||
total_discount_amount_value,
|
||||
total_discount_amount_scale,
|
||||
taxable_amount_value,
|
||||
taxable_amount_scale,
|
||||
iva_amount_value,
|
||||
iva_amount_scale,
|
||||
rec_amount_value,
|
||||
rec_amount_scale,
|
||||
retention_amount_value,
|
||||
retention_amount_scale,
|
||||
taxes_amount_value,
|
||||
taxes_amount_scale,
|
||||
total_amount_value,
|
||||
total_amount_scale,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
FROM customer_invoices
|
||||
WHERE is_proforma = 0;
|
||||
```
|
||||
|
||||
### Items
|
||||
|
||||
```sql
|
||||
INSERT INTO proforma_items (
|
||||
item_id,
|
||||
proforma_id,
|
||||
position,
|
||||
description,
|
||||
quantity_value,
|
||||
quantity_scale,
|
||||
unit_amount_value,
|
||||
unit_amount_scale,
|
||||
subtotal_amount_value,
|
||||
subtotal_amount_scale,
|
||||
item_discount_percentage_value,
|
||||
item_discount_percentage_scale,
|
||||
item_discount_amount_value,
|
||||
item_discount_amount_scale,
|
||||
global_discount_percentage_value,
|
||||
global_discount_percentage_scale,
|
||||
global_discount_amount_value,
|
||||
global_discount_amount_scale,
|
||||
total_discount_amount_value,
|
||||
total_discount_amount_scale,
|
||||
taxable_amount_value,
|
||||
taxable_amount_scale,
|
||||
iva_code,
|
||||
iva_percentage_value,
|
||||
iva_percentage_scale,
|
||||
iva_amount_value,
|
||||
iva_amount_scale,
|
||||
rec_code,
|
||||
rec_percentage_value,
|
||||
rec_percentage_scale,
|
||||
rec_amount_value,
|
||||
rec_amount_scale,
|
||||
retention_code,
|
||||
retention_percentage_value,
|
||||
retention_percentage_scale,
|
||||
retention_amount_value,
|
||||
retention_amount_scale,
|
||||
taxes_amount_value,
|
||||
taxes_amount_scale,
|
||||
total_amount_value,
|
||||
total_amount_scale,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
i.item_id,
|
||||
i.invoice_id,
|
||||
i.position,
|
||||
i.description,
|
||||
i.quantity_value,
|
||||
i.quantity_scale,
|
||||
i.unit_amount_value,
|
||||
i.unit_amount_scale,
|
||||
i.subtotal_amount_value,
|
||||
i.subtotal_amount_scale,
|
||||
i.item_discount_percentage_value,
|
||||
i.item_discount_percentage_scale,
|
||||
i.item_discount_amount_value,
|
||||
i.item_discount_amount_scale,
|
||||
i.global_discount_percentage_value,
|
||||
i.global_discount_percentage_scale,
|
||||
i.global_discount_amount_value,
|
||||
i.global_discount_amount_scale,
|
||||
i.total_discount_amount_value,
|
||||
i.total_discount_amount_scale,
|
||||
i.taxable_amount_value,
|
||||
i.taxable_amount_scale,
|
||||
i.iva_code,
|
||||
i.iva_percentage_value,
|
||||
i.iva_percentage_scale,
|
||||
i.iva_amount_value,
|
||||
i.iva_amount_scale,
|
||||
i.rec_code,
|
||||
i.rec_percentage_value,
|
||||
i.rec_percentage_scale,
|
||||
i.rec_amount_value,
|
||||
i.rec_amount_scale,
|
||||
i.retention_code,
|
||||
i.retention_percentage_value,
|
||||
i.retention_percentage_scale,
|
||||
i.retention_amount_value,
|
||||
i.retention_amount_scale,
|
||||
i.taxes_amount_value,
|
||||
i.taxes_amount_scale,
|
||||
i.total_amount_value,
|
||||
i.total_amount_scale,
|
||||
now(),
|
||||
now()
|
||||
FROM customer_invoice_items i
|
||||
JOIN customer_invoices ci ON ci.id = i.invoice_id
|
||||
WHERE ci.is_proforma = 1;
|
||||
|
||||
|
||||
|
||||
|
||||
INSERT INTO issued_invoice_items (
|
||||
item_id,
|
||||
issued_invoice_id,
|
||||
position,
|
||||
description,
|
||||
quantity_value,
|
||||
quantity_scale,
|
||||
unit_amount_value,
|
||||
unit_amount_scale,
|
||||
subtotal_amount_value,
|
||||
subtotal_amount_scale,
|
||||
item_discount_percentage_value,
|
||||
item_discount_percentage_scale,
|
||||
item_discount_amount_value,
|
||||
item_discount_amount_scale,
|
||||
global_discount_percentage_value,
|
||||
global_discount_percentage_scale,
|
||||
global_discount_amount_value,
|
||||
global_discount_amount_scale,
|
||||
total_discount_amount_value,
|
||||
total_discount_amount_scale,
|
||||
taxable_amount_value,
|
||||
taxable_amount_scale,
|
||||
iva_code,
|
||||
iva_percentage_value,
|
||||
iva_percentage_scale,
|
||||
iva_amount_value,
|
||||
iva_amount_scale,
|
||||
rec_code,
|
||||
rec_percentage_value,
|
||||
rec_percentage_scale,
|
||||
rec_amount_value,
|
||||
rec_amount_scale,
|
||||
retention_code,
|
||||
retention_percentage_value,
|
||||
retention_percentage_scale,
|
||||
retention_amount_value,
|
||||
retention_amount_scale,
|
||||
taxes_amount_value,
|
||||
taxes_amount_scale,
|
||||
total_amount_value,
|
||||
total_amount_scale,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
i.item_id,
|
||||
i.invoice_id,
|
||||
i.position,
|
||||
i.description,
|
||||
i.quantity_value,
|
||||
i.quantity_scale,
|
||||
i.unit_amount_value,
|
||||
i.unit_amount_scale,
|
||||
i.subtotal_amount_value,
|
||||
i.subtotal_amount_scale,
|
||||
i.item_discount_percentage_value,
|
||||
i.item_discount_percentage_scale,
|
||||
i.item_discount_amount_value,
|
||||
i.item_discount_amount_scale,
|
||||
i.global_discount_percentage_value,
|
||||
i.global_discount_percentage_scale,
|
||||
i.global_discount_amount_value,
|
||||
i.global_discount_amount_scale,
|
||||
i.total_discount_amount_value,
|
||||
i.total_discount_amount_scale,
|
||||
i.taxable_amount_value,
|
||||
i.taxable_amount_scale,
|
||||
i.iva_code,
|
||||
i.iva_percentage_value,
|
||||
i.iva_percentage_scale,
|
||||
i.iva_amount_value,
|
||||
i.iva_amount_scale,
|
||||
i.rec_code,
|
||||
i.rec_percentage_value,
|
||||
i.rec_percentage_scale,
|
||||
i.rec_amount_value,
|
||||
i.rec_amount_scale,
|
||||
i.retention_code,
|
||||
i.retention_percentage_value,
|
||||
i.retention_percentage_scale,
|
||||
i.retention_amount_value,
|
||||
i.retention_amount_scale,
|
||||
i.taxes_amount_value,
|
||||
i.taxes_amount_scale,
|
||||
i.total_amount_value,
|
||||
i.total_amount_scale,
|
||||
now(),
|
||||
now()
|
||||
FROM customer_invoice_items i
|
||||
JOIN customer_invoices ci ON ci.id = i.invoice_id
|
||||
WHERE ci.is_proforma = 0;
|
||||
```
|
||||
|
||||
### Taxes
|
||||
|
||||
```sql
|
||||
INSERT INTO proforma_taxes (
|
||||
tax_id,
|
||||
proforma_id,
|
||||
taxable_amount_value,
|
||||
taxable_amount_scale,
|
||||
iva_code,
|
||||
iva_percentage_value,
|
||||
iva_percentage_scale,
|
||||
iva_amount_value,
|
||||
iva_amount_scale,
|
||||
rec_code,
|
||||
rec_percentage_value,
|
||||
rec_percentage_scale,
|
||||
rec_amount_value,
|
||||
rec_amount_scale,
|
||||
retention_code,
|
||||
retention_percentage_value,
|
||||
retention_percentage_scale,
|
||||
retention_amount_value,
|
||||
retention_amount_scale,
|
||||
taxes_amount_value,
|
||||
taxes_amount_scale,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
t.tax_id,
|
||||
t.invoice_id,
|
||||
t.taxable_amount_value,
|
||||
t.taxable_amount_scale,
|
||||
t.iva_code,
|
||||
t.iva_percentage_value,
|
||||
t.iva_percentage_scale,
|
||||
t.iva_amount_value,
|
||||
t.iva_amount_scale,
|
||||
t.rec_code,
|
||||
t.rec_percentage_value,
|
||||
t.rec_percentage_scale,
|
||||
t.rec_amount_value,
|
||||
t.rec_amount_scale,
|
||||
t.retention_code,
|
||||
t.retention_percentage_value,
|
||||
t.retention_percentage_scale,
|
||||
t.retention_amount_value,
|
||||
t.retention_amount_scale,
|
||||
t.taxes_amount_value,
|
||||
t.taxes_amount_scale,
|
||||
now(),
|
||||
now()
|
||||
FROM customer_invoice_taxes t
|
||||
JOIN customer_invoices ci ON ci.id = t.invoice_id
|
||||
WHERE ci.is_proforma = 1;
|
||||
|
||||
INSERT INTO issued_invoice_taxes (tax_id,
|
||||
issued_invoice_id,
|
||||
taxable_amount_value,
|
||||
taxable_amount_scale,
|
||||
iva_code,
|
||||
iva_percentage_value,
|
||||
iva_percentage_scale,
|
||||
iva_amount_value,
|
||||
iva_amount_scale,
|
||||
rec_code,
|
||||
rec_percentage_value,
|
||||
rec_percentage_scale,
|
||||
rec_amount_value,
|
||||
rec_amount_scale,
|
||||
retention_code,
|
||||
retention_percentage_value,
|
||||
retention_percentage_scale,
|
||||
retention_amount_value,
|
||||
retention_amount_scale,
|
||||
taxes_amount_value,
|
||||
taxes_amount_scale,
|
||||
created_at,
|
||||
updated_at)
|
||||
SELECT
|
||||
t.tax_id,
|
||||
t.invoice_id,
|
||||
t.taxable_amount_value,
|
||||
t.taxable_amount_scale,
|
||||
t.iva_code,
|
||||
t.iva_percentage_value,
|
||||
t.iva_percentage_scale,
|
||||
t.iva_amount_value,
|
||||
t.iva_amount_scale,
|
||||
t.rec_code,
|
||||
t.rec_percentage_value,
|
||||
t.rec_percentage_scale,
|
||||
t.rec_amount_value,
|
||||
t.rec_amount_scale,
|
||||
t.retention_code,
|
||||
t.retention_percentage_value,
|
||||
t.retention_percentage_scale,
|
||||
t.retention_amount_value,
|
||||
t.retention_amount_scale,
|
||||
t.taxes_amount_value,
|
||||
t.taxes_amount_scale,
|
||||
now(),
|
||||
now()
|
||||
FROM customer_invoice_taxes t
|
||||
JOIN customer_invoices ci ON ci.id = t.invoice_id
|
||||
WHERE ci.is_proforma = 0;
|
||||
```
|
||||
|
||||
### Verifactu
|
||||
|
||||
```sql
|
||||
UPDATE verifactu_records vr
|
||||
JOIN customer_invoices ci ON ci.id = vr.invoice_id
|
||||
SET vr.issued_invoice_id = vr.invoice_id
|
||||
WHERE ci.is_proforma = 0;
|
||||
```
|
||||
|
||||
## Validaciones SQL
|
||||
|
||||
### Conteos
|
||||
|
||||
```sql
|
||||
SELECT COUNT(*) FROM customer_invoices WHERE is_proforma = 1;
|
||||
SELECT COUNT(*) FROM proformas;
|
||||
|
||||
SELECT COUNT(*) FROM customer_invoices WHERE is_proforma = 0;
|
||||
SELECT COUNT(*) FROM issued_invoices;
|
||||
|
||||
SELECT COUNT(*)
|
||||
FROM customer_invoice_items i
|
||||
JOIN customer_invoices ci ON ci.id = i.invoice_id
|
||||
WHERE ci.is_proforma = 1;
|
||||
SELECT COUNT(*) FROM proforma_items;
|
||||
|
||||
SELECT COUNT(*)
|
||||
FROM customer_invoice_taxes t
|
||||
JOIN customer_invoices ci ON ci.id = t.invoice_id
|
||||
WHERE ci.is_proforma = 0;
|
||||
SELECT COUNT(*) FROM issued_invoice_taxes;
|
||||
```
|
||||
|
||||
### Duplicados fiscales
|
||||
|
||||
```sql
|
||||
SELECT company_id, invoice_series_code, invoice_number, COUNT(*)
|
||||
FROM issued_invoices
|
||||
GROUP BY company_id, invoice_series_code, invoice_number
|
||||
HAVING COUNT(*) > 1;
|
||||
```
|
||||
|
||||
### Huerfanos
|
||||
|
||||
```sql
|
||||
SELECT COUNT(*)
|
||||
FROM issued_invoices ii
|
||||
LEFT JOIN proformas p ON p.id = ii.source_proforma_id
|
||||
WHERE ii.source_proforma_id IS NOT NULL
|
||||
AND p.id IS NULL;
|
||||
|
||||
SELECT COUNT(*)
|
||||
FROM verifactu_records vr
|
||||
LEFT JOIN issued_invoices ii ON ii.id = vr.issued_invoice_id
|
||||
WHERE vr.issued_invoice_id IS NOT NULL
|
||||
AND ii.id IS NULL;
|
||||
```
|
||||
|
||||
### Totales
|
||||
|
||||
```sql
|
||||
SELECT SUM(total_amount_value) FROM customer_invoices WHERE is_proforma = 1;
|
||||
SELECT SUM(total_amount_value) FROM proformas;
|
||||
|
||||
SELECT SUM(total_amount_value) FROM customer_invoices WHERE is_proforma = 0;
|
||||
SELECT SUM(total_amount_value) FROM issued_invoices;
|
||||
```
|
||||
|
||||
### Proformas sin lineas con impuestos materializados
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
p.id,
|
||||
p.proforma_reference,
|
||||
COUNT(DISTINCT pi.item_id) AS item_rows,
|
||||
COUNT(DISTINCT pt.tax_id) AS tax_rows
|
||||
FROM proformas p
|
||||
LEFT JOIN proforma_items pi
|
||||
ON pi.proforma_id = p.id
|
||||
LEFT JOIN proforma_taxes pt
|
||||
ON pt.proforma_id = p.id
|
||||
GROUP BY p.id, p.proforma_reference
|
||||
HAVING COUNT(DISTINCT pi.item_id) = 0
|
||||
AND COUNT(DISTINCT pt.tax_id) > 0;
|
||||
```
|
||||
|
||||
Resultado esperado:
|
||||
|
||||
- `0` filas
|
||||
|
||||
## No incluido en fase 1A
|
||||
|
||||
- no cutover del backend
|
||||
- no borrado de tablas legacy
|
||||
- no doble escritura
|
||||
- no ejecucion de migraciones en una BD real
|
||||
- no integracion Verifactu externa
|
||||
- no dominio de cobros, vencimientos vivos o conciliacion
|
||||
|
||||
## No incluido en fase 1C
|
||||
|
||||
- no borrado de legacy
|
||||
- no backfill real
|
||||
- no ejecucion de migraciones
|
||||
- no doble escritura
|
||||
- no integracion Verifactu funcional nueva
|
||||
- no cambios en controllers, rutas o DTOs publicos
|
||||
|
||||
## No incluido en fase 1D
|
||||
|
||||
- no ejecucion en produccion
|
||||
- no borrado de tablas legacy
|
||||
- no eliminacion de modelos/repositorios legacy
|
||||
- no integracion Verifacti funcional
|
||||
- no dominio de cobros
|
||||
- no doble escritura
|
||||
|
||||
## Resultado de validacion tecnica fase 1D
|
||||
|
||||
- `tsc` modulo `customer-invoices`: esperado ejecutar en esta fase
|
||||
- `biome lint` sobre `src/api/infrastructure` y `docs/customer-invoices`: esperado ejecutar en esta fase
|
||||
- `tsc` raiz: esperado ejecutar en esta fase
|
||||
- Validacion SQL real: no ejecutada por falta de entorno de desarrollo inequivoco
|
||||
- Validacion runtime real contra BD migrada: no ejecutada por la misma razon
|
||||
|
||||
## Siguiente fase recomendada
|
||||
|
||||
- confirmar `.env`/credenciales de una BD de desarrollo segura
|
||||
- ejecutar solo los runbooks SQL que sigan siendo necesarios para el entorno real afectado
|
||||
- levantar el backend contra esa BD y validar `create`, `update`, `get`, `list` e `issue proforma`
|
||||
- validar numeracion de proformas V2 y asignacion de serie/numero de factura emitida
|
||||
- revisar la ruta `VerifactuRecord -> issued_invoice_id` ya con persistencia V2 activa
|
||||
|
||||
## Nota document-series
|
||||
|
||||
`customer-invoices` ya migró funcionalmente desde `CustomerInvoiceSeriesModel` hacia `document-series`. Como nota histórica de 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
|
||||
@ -107,7 +107,7 @@ Campos reales actuales:
|
||||
- `city`
|
||||
- `province`
|
||||
- `postalCode`
|
||||
- `country`
|
||||
- `countryCode`
|
||||
- `primaryEmail`
|
||||
- `primaryPhone`
|
||||
- `primaryMobile`
|
||||
@ -286,13 +286,13 @@ El schema público de formulario rápido actual (`QuickCustomerCreateFormSchema`
|
||||
|
||||
La feature reutiliza `buildCustomerCreateDefault()` del área interna de create y resuelve por defecto:
|
||||
|
||||
- `country = ES`
|
||||
- `countryCode = ES`
|
||||
- `languageCode = es`
|
||||
- `currencyCode = EUR`
|
||||
|
||||
Además, `useQuickCreateCustomerFlow` y `useQuickCreateCustomerDialogController` admiten contexto opcional:
|
||||
|
||||
- `defaultCountry?`
|
||||
- `defaultCountryCode?`
|
||||
- `defaultLanguageCode?`
|
||||
- `defaultCurrencyCode?`
|
||||
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { toPostalAddress } from "./company-address.mapper";
|
||||
|
||||
test("maps a complete request address to PostalAddress", () => {
|
||||
const result = toPostalAddress({
|
||||
street: "Calle Mayor 1",
|
||||
street_2: null,
|
||||
city: "Madrid",
|
||||
postal_code: "28001",
|
||||
province: "Madrid",
|
||||
country_code: "ES",
|
||||
});
|
||||
|
||||
assert.equal(result.isSuccess, true);
|
||||
assert.equal(result.data.street.unwrap().toPrimitive(), "Calle Mayor 1");
|
||||
assert.equal(result.data.street2.isNone(), true);
|
||||
assert.equal(result.data.city.unwrap().toPrimitive(), "Madrid");
|
||||
assert.equal(result.data.postalCode.unwrap().toPrimitive(), "28001");
|
||||
assert.equal(result.data.province.unwrap().toPrimitive(), "Madrid");
|
||||
assert.equal(result.data.countryCode.unwrap().toPrimitive(), "ES");
|
||||
});
|
||||
@ -0,0 +1,35 @@
|
||||
import {
|
||||
City,
|
||||
CountryCode,
|
||||
PostalAddress,
|
||||
PostalCode,
|
||||
Province,
|
||||
Street,
|
||||
maybeFromNullableResult,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CompanyAddressRequestDTO } from "../../../../common";
|
||||
|
||||
export function toPostalAddress(dto: CompanyAddressRequestDTO): Result<PostalAddress, Error> {
|
||||
const streetResult = Street.create(dto.street);
|
||||
if (streetResult.isFailure) return Result.fail(streetResult.error);
|
||||
const street2Result = maybeFromNullableResult(dto.street_2, (value) => Street.create(value));
|
||||
if (street2Result.isFailure) return Result.fail(street2Result.error);
|
||||
const cityResult = City.create(dto.city);
|
||||
if (cityResult.isFailure) return Result.fail(cityResult.error);
|
||||
const postalCodeResult = PostalCode.create(dto.postal_code);
|
||||
if (postalCodeResult.isFailure) return Result.fail(postalCodeResult.error);
|
||||
const provinceResult = Province.create(dto.province);
|
||||
if (provinceResult.isFailure) return Result.fail(provinceResult.error);
|
||||
const countryCodeResult = CountryCode.create(dto.country_code);
|
||||
if (countryCodeResult.isFailure) return Result.fail(countryCodeResult.error);
|
||||
return PostalAddress.create({
|
||||
street: Maybe.some(streetResult.data),
|
||||
street2: street2Result.data,
|
||||
city: Maybe.some(cityResult.data),
|
||||
postalCode: Maybe.some(postalCodeResult.data),
|
||||
province: Maybe.some(provinceResult.data),
|
||||
countryCode: Maybe.some(countryCodeResult.data),
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import type { CompanyPublicModel } from "../public";
|
||||
|
||||
import { toCompanyResponseDTO } from "./company-response.mapper";
|
||||
|
||||
const company: Omit<CompanyPublicModel, "address"> = {
|
||||
id: "company-id",
|
||||
legalName: "Rodax Labs",
|
||||
tradeName: "Rodax",
|
||||
tin: "B12345678",
|
||||
slug: "rodax-labs",
|
||||
email: "hello@rodax.test",
|
||||
phone: "+34911111111",
|
||||
website: "https://rodax.test",
|
||||
defaultPaymentTermId: "payment-term-id",
|
||||
status: "active",
|
||||
};
|
||||
|
||||
test("maps a company without address", () => {
|
||||
const response = toCompanyResponseDTO({ ...company, address: null });
|
||||
|
||||
assert.equal(response.address, null);
|
||||
assert.equal(response.legal_name, "Rodax Labs");
|
||||
assert.equal(response.default_payment_term_id, "payment-term-id");
|
||||
});
|
||||
|
||||
test("maps a company address to the response contract", () => {
|
||||
const response = toCompanyResponseDTO({
|
||||
...company,
|
||||
address: {
|
||||
street: "Calle Mayor 1",
|
||||
street2: null,
|
||||
city: "Madrid",
|
||||
postalCode: "28001",
|
||||
province: "Madrid",
|
||||
countryCode: "ES",
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(response.address, {
|
||||
street: "Calle Mayor 1",
|
||||
street_2: null,
|
||||
city: "Madrid",
|
||||
postal_code: "28001",
|
||||
province: "Madrid",
|
||||
country_code: "ES",
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,28 @@
|
||||
import type { CompanyResponseDTO } from "../../../../common";
|
||||
import type { CompanyPublicModel } from "../public";
|
||||
|
||||
export function toCompanyResponseDTO(model: CompanyPublicModel): CompanyResponseDTO {
|
||||
return {
|
||||
id: model.id,
|
||||
legal_name: model.legalName,
|
||||
trade_name: model.tradeName,
|
||||
tin: model.tin,
|
||||
slug: model.slug,
|
||||
email: model.email,
|
||||
phone: model.phone,
|
||||
website: model.website,
|
||||
address:
|
||||
model.address === null
|
||||
? null
|
||||
: {
|
||||
street: model.address.street,
|
||||
street_2: model.address.street2,
|
||||
city: model.address.city,
|
||||
postal_code: model.address.postalCode,
|
||||
province: model.address.province,
|
||||
country_code: model.address.countryCode,
|
||||
},
|
||||
default_payment_term_id: model.defaultPaymentTermId,
|
||||
status: model.status,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { City, CountryCode, Name, PostalAddress, PostalCode, Province, Street, UniqueID, UtcDate } from "@repo/rdx-ddd";
|
||||
import { Maybe } from "@repo/rdx-utils";
|
||||
|
||||
import { Company, CompanySlug, CompanyStatus } from "../../../../domain";
|
||||
import { CompanyPublicModelMapper } from "./company-public-model.mapper";
|
||||
|
||||
const unwrap = <T>(result: { data: T }): T => result.data;
|
||||
const buildAddress = (street2 = Maybe.some(unwrap(Street.create("Suite 2")))) => unwrap(PostalAddress.create({ street: Maybe.some(unwrap(Street.create("Street 1"))), street2, city: Maybe.some(unwrap(City.create("Madrid"))), postalCode: Maybe.some(unwrap(PostalCode.create("28001"))), province: Maybe.some(unwrap(Province.create("Madrid"))), countryCode: Maybe.some(unwrap(CountryCode.create("ES"))) }));
|
||||
const buildCompany = (address = Maybe.none<PostalAddress>()) => unwrap(Company.create({ legalName: unwrap(Name.create("Rodax")), tradeName: Maybe.none(), tin: Maybe.none(), slug: unwrap(CompanySlug.create("rodax")), email: Maybe.none(), phone: Maybe.none(), website: Maybe.none(), address, defaultPaymentTermId: Maybe.none(), status: CompanyStatus.createActive(), createdAt: unwrap(UtcDate.createFromISO("2026-01-01")), updatedAt: unwrap(UtcDate.createFromISO("2026-01-01")) }, UniqueID.generateNewID()));
|
||||
const mapper = new CompanyPublicModelMapper();
|
||||
|
||||
test("maps Company address to public primitives", () => {
|
||||
assert.equal(mapper.toPublicModel(buildCompany()).address, null);
|
||||
const full = mapper.toPublicModel(buildCompany(Maybe.some(buildAddress()))).address;
|
||||
assert.deepEqual(full, { street: "Street 1", street2: "Suite 2", city: "Madrid", postalCode: "28001", province: "Madrid", countryCode: "ES" });
|
||||
assert.equal("countryCode" in full!, true);
|
||||
assert.equal("country" in full!, false);
|
||||
assert.equal(mapper.toPublicModel(buildCompany(Maybe.some(buildAddress(Maybe.none())))).address?.street2, null);
|
||||
});
|
||||
@ -14,6 +14,17 @@ export class CompanyPublicModelMapper {
|
||||
email: maybeToNullable(company.email, (value) => value.toPrimitive()),
|
||||
phone: maybeToNullable(company.phone, (value) => value.toPrimitive()),
|
||||
website: maybeToNullable(company.website, (value) => value.toPrimitive()),
|
||||
address: company.address.match(
|
||||
(address) => ({
|
||||
street: address.street.unwrap().toPrimitive(),
|
||||
street2: maybeToNullable(address.street2, (value) => value.toPrimitive()),
|
||||
city: address.city.unwrap().toPrimitive(),
|
||||
postalCode: address.postalCode.unwrap().toPrimitive(),
|
||||
province: address.province.unwrap().toPrimitive(),
|
||||
countryCode: address.countryCode.unwrap().toPrimitive(),
|
||||
}),
|
||||
() => null
|
||||
),
|
||||
defaultPaymentTermId: maybeToNullable(company.defaultPaymentTermId, (value) =>
|
||||
value.toPrimitive()
|
||||
),
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
export interface CompanyPublicAddress {
|
||||
street: string;
|
||||
street2: string | null;
|
||||
city: string;
|
||||
postalCode: string;
|
||||
province: string;
|
||||
countryCode: string;
|
||||
}
|
||||
|
||||
export interface CompanyPublicModel {
|
||||
id: string;
|
||||
legalName: string;
|
||||
@ -7,6 +16,7 @@ export interface CompanyPublicModel {
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
website: string | null;
|
||||
address: CompanyPublicAddress | null;
|
||||
defaultPaymentTermId: string | null;
|
||||
status: "active" | "disabled";
|
||||
}
|
||||
|
||||
@ -47,6 +47,7 @@ const buildCompany = (status = CompanyStatus.createActive()) => {
|
||||
email: Maybe.some(email.data),
|
||||
phone: Maybe.some(phone.data),
|
||||
website: Maybe.some(website.data),
|
||||
address: Maybe.none(),
|
||||
defaultPaymentTermId: Maybe.none(),
|
||||
status,
|
||||
createdAt: now.data,
|
||||
|
||||
@ -3,9 +3,15 @@ import test from "node:test";
|
||||
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
import {
|
||||
City,
|
||||
CountryCode,
|
||||
EmailAddress,
|
||||
Name,
|
||||
PhoneNumber,
|
||||
PostalAddress,
|
||||
PostalCode,
|
||||
Province,
|
||||
Street,
|
||||
TextValue,
|
||||
URLAddress,
|
||||
UniqueID,
|
||||
@ -15,6 +21,7 @@ import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { Company, CompanySlug, CompanyStatus } from "../../../domain";
|
||||
import { CompanyPublicModelMapper } from "../public";
|
||||
|
||||
import { CreateCompanyUseCase } from "./create-company.use-case";
|
||||
|
||||
const buildTransactionManager = (): ITransactionManager => ({
|
||||
@ -43,19 +50,23 @@ const buildCompany = () => {
|
||||
assert.equal(website.isSuccess, true);
|
||||
assert.equal(tin.isSuccess, true);
|
||||
|
||||
const companyResult = Company.create({
|
||||
legalName: legalName.data,
|
||||
tradeName: Maybe.none(),
|
||||
tin: Maybe.some(tin.data),
|
||||
slug: slug.data,
|
||||
email: Maybe.some(email.data),
|
||||
phone: Maybe.some(phone.data),
|
||||
website: Maybe.some(website.data),
|
||||
defaultPaymentTermId: Maybe.none(),
|
||||
status: CompanyStatus.createActive(),
|
||||
createdAt: now.data,
|
||||
updatedAt: now.data,
|
||||
}, id);
|
||||
const companyResult = Company.create(
|
||||
{
|
||||
legalName: legalName.data,
|
||||
tradeName: Maybe.none(),
|
||||
tin: Maybe.some(tin.data),
|
||||
slug: slug.data,
|
||||
email: Maybe.some(email.data),
|
||||
phone: Maybe.some(phone.data),
|
||||
website: Maybe.some(website.data),
|
||||
address: Maybe.none(),
|
||||
defaultPaymentTermId: Maybe.none(),
|
||||
status: CompanyStatus.createActive(),
|
||||
createdAt: now.data,
|
||||
updatedAt: now.data,
|
||||
},
|
||||
id
|
||||
);
|
||||
|
||||
assert.equal(companyResult.isSuccess, true);
|
||||
return companyResult.data;
|
||||
@ -88,3 +99,86 @@ test("create devuelve la company sin default payment term", async () => {
|
||||
assert.equal(result.isSuccess, true);
|
||||
assert.equal(result.data.default_payment_term_id, null);
|
||||
});
|
||||
|
||||
test("create serializes the company public address", async () => {
|
||||
const company = buildCompany();
|
||||
const address = PostalAddress.create({
|
||||
street: Maybe.some(Street.create("Calle Mayor 1").data),
|
||||
street2: Maybe.none(),
|
||||
city: Maybe.some(City.create("Madrid").data),
|
||||
postalCode: Maybe.some(PostalCode.create("28001").data),
|
||||
province: Maybe.some(Province.create("Madrid").data),
|
||||
countryCode: Maybe.some(CountryCode.create("ES").data),
|
||||
});
|
||||
assert.equal(address.isSuccess, true);
|
||||
assert.equal(
|
||||
company.update({ address: Maybe.some(address.data), updatedAt: company.updatedAt }).data,
|
||||
true
|
||||
);
|
||||
const useCase = new CreateCompanyUseCase({
|
||||
creator: { create: async () => Result.ok(company) },
|
||||
responseMapper: new CompanyPublicModelMapper(),
|
||||
transactionManager: buildTransactionManager(),
|
||||
});
|
||||
const result = await useCase.execute({
|
||||
dto: {
|
||||
id: company.id.toPrimitive(),
|
||||
legal_name: company.legalName.toPrimitive(),
|
||||
trade_name: null,
|
||||
tin: company.tin.unwrap().toPrimitive(),
|
||||
slug: company.slug.toPrimitive(),
|
||||
email: company.email.unwrap().toPrimitive(),
|
||||
phone: company.phone.unwrap().toPrimitive(),
|
||||
website: company.website.unwrap().toPrimitive(),
|
||||
},
|
||||
});
|
||||
assert.equal(result.isSuccess, true);
|
||||
assert.deepEqual(result.data.address, {
|
||||
street: "Calle Mayor 1",
|
||||
street_2: null,
|
||||
city: "Madrid",
|
||||
postal_code: "28001",
|
||||
province: "Madrid",
|
||||
country_code: "ES",
|
||||
});
|
||||
});
|
||||
|
||||
test("create maps a complete request address to Company props", async () => {
|
||||
const company = buildCompany();
|
||||
let capturedAddress: typeof company.address | undefined;
|
||||
const useCase = new CreateCompanyUseCase({
|
||||
creator: {
|
||||
create: async ({ props }) => {
|
||||
capturedAddress = props.address;
|
||||
return Result.ok(company);
|
||||
},
|
||||
},
|
||||
responseMapper: new CompanyPublicModelMapper(),
|
||||
transactionManager: buildTransactionManager(),
|
||||
});
|
||||
const result = await useCase.execute({
|
||||
dto: {
|
||||
id: company.id.toPrimitive(),
|
||||
legal_name: company.legalName.toPrimitive(),
|
||||
trade_name: null,
|
||||
tin: company.tin.unwrap().toPrimitive(),
|
||||
slug: company.slug.toPrimitive(),
|
||||
email: company.email.unwrap().toPrimitive(),
|
||||
phone: company.phone.unwrap().toPrimitive(),
|
||||
website: company.website.unwrap().toPrimitive(),
|
||||
address: {
|
||||
street: "Calle Mayor 1",
|
||||
street_2: null,
|
||||
city: "Madrid",
|
||||
postal_code: "28001",
|
||||
province: "Madrid",
|
||||
country_code: "ES",
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(result.isSuccess, true);
|
||||
assert.equal(capturedAddress?.isSome(), true);
|
||||
assert.equal(capturedAddress?.unwrap().street.unwrap().toPrimitive(), "Calle Mayor 1");
|
||||
assert.equal(capturedAddress?.unwrap().street2.isNone(), true);
|
||||
assert.equal(capturedAddress?.unwrap().countryCode.unwrap().toPrimitive(), "ES");
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
import type { PostalAddress } from "@repo/rdx-ddd";
|
||||
import {
|
||||
EmailAddress,
|
||||
Name,
|
||||
@ -11,8 +12,10 @@ import {
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CreateCompanyRequestDTO, CompanyResponseDTO } from "../../../../common";
|
||||
import type { CompanyResponseDTO, CreateCompanyRequestDTO } from "../../../../common";
|
||||
import { CompanySlug, CompanyStatus, type ICompanyCreateProps } from "../../../domain";
|
||||
import { toPostalAddress } from "../mappers/company-address.mapper";
|
||||
import { toCompanyResponseDTO } from "../mappers/company-response.mapper";
|
||||
import type { CompanyPublicModelMapper } from "../public";
|
||||
import type { ICompanyCreator } from "../services";
|
||||
|
||||
@ -25,7 +28,9 @@ export class CreateCompanyUseCase {
|
||||
}
|
||||
) {}
|
||||
|
||||
public execute(params: { dto: CreateCompanyRequestDTO }): Promise<Result<CompanyResponseDTO, Error>> {
|
||||
public execute(params: {
|
||||
dto: CreateCompanyRequestDTO;
|
||||
}): Promise<Result<CompanyResponseDTO, Error>> {
|
||||
const mappedResult = mapCreateCompanyProps(params.dto);
|
||||
if (mappedResult.isFailure) {
|
||||
return Promise.resolve(Result.fail(mappedResult.error));
|
||||
@ -40,7 +45,9 @@ export class CreateCompanyUseCase {
|
||||
return Result.fail(createResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(toCompanyResponseDTO(this.deps.responseMapper.toPublicModel(createResult.data)));
|
||||
return Result.ok(
|
||||
toCompanyResponseDTO(this.deps.responseMapper.toPublicModel(createResult.data))
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
@ -59,7 +66,9 @@ function mapCreateCompanyProps(dto: CreateCompanyRequestDTO) {
|
||||
return Result.fail(legalNameResult.error);
|
||||
}
|
||||
|
||||
const tradeNameResult = maybeFromNullableResult(dto.trade_name ?? null, (value) => Name.create(value));
|
||||
const tradeNameResult = maybeFromNullableResult(dto.trade_name ?? null, (value) =>
|
||||
Name.create(value)
|
||||
);
|
||||
if (tradeNameResult.isFailure) {
|
||||
return Result.fail(tradeNameResult.error);
|
||||
}
|
||||
@ -74,21 +83,36 @@ function mapCreateCompanyProps(dto: CreateCompanyRequestDTO) {
|
||||
return Result.fail(slugResult.error);
|
||||
}
|
||||
|
||||
const emailResult = maybeFromNullableResult(dto.email ?? null, (value) => EmailAddress.create(value));
|
||||
const emailResult = maybeFromNullableResult(dto.email ?? null, (value) =>
|
||||
EmailAddress.create(value)
|
||||
);
|
||||
if (emailResult.isFailure) {
|
||||
return Result.fail(emailResult.error);
|
||||
}
|
||||
|
||||
const phoneResult = maybeFromNullableResult(dto.phone ?? null, (value) => PhoneNumber.create(value));
|
||||
const phoneResult = maybeFromNullableResult(dto.phone ?? null, (value) =>
|
||||
PhoneNumber.create(value)
|
||||
);
|
||||
if (phoneResult.isFailure) {
|
||||
return Result.fail(phoneResult.error);
|
||||
}
|
||||
|
||||
const websiteResult = maybeFromNullableResult(dto.website ?? null, (value) => URLAddress.create(value));
|
||||
const websiteResult = maybeFromNullableResult(dto.website ?? null, (value) =>
|
||||
URLAddress.create(value)
|
||||
);
|
||||
if (websiteResult.isFailure) {
|
||||
return Result.fail(websiteResult.error);
|
||||
}
|
||||
|
||||
let address: Maybe<PostalAddress> = Maybe.none();
|
||||
if (dto.address !== undefined && dto.address !== null) {
|
||||
const addressResult = toPostalAddress(dto.address);
|
||||
if (addressResult.isFailure) {
|
||||
return Result.fail(addressResult.error);
|
||||
}
|
||||
address = Maybe.some(addressResult.data);
|
||||
}
|
||||
|
||||
const now = UtcDateTime.now();
|
||||
|
||||
const props: ICompanyCreateProps = {
|
||||
@ -99,6 +123,7 @@ function mapCreateCompanyProps(dto: CreateCompanyRequestDTO) {
|
||||
email: emailResult.data,
|
||||
phone: phoneResult.data,
|
||||
website: websiteResult.data,
|
||||
address,
|
||||
defaultPaymentTermId: Maybe.none(),
|
||||
status: CompanyStatus.createActive(),
|
||||
createdAt: now,
|
||||
@ -110,29 +135,3 @@ function mapCreateCompanyProps(dto: CreateCompanyRequestDTO) {
|
||||
props,
|
||||
});
|
||||
}
|
||||
|
||||
function toCompanyResponseDTO(model: {
|
||||
id: string;
|
||||
legalName: string;
|
||||
tradeName: string | null;
|
||||
tin: string | null;
|
||||
slug: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
website: string | null;
|
||||
defaultPaymentTermId: string | null;
|
||||
status: "active" | "disabled";
|
||||
}): CompanyResponseDTO {
|
||||
return {
|
||||
id: model.id,
|
||||
legal_name: model.legalName,
|
||||
trade_name: model.tradeName,
|
||||
tin: model.tin,
|
||||
slug: model.slug,
|
||||
email: model.email,
|
||||
phone: model.phone,
|
||||
website: model.website,
|
||||
default_payment_term_id: model.defaultPaymentTermId,
|
||||
status: model.status,
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { UniqueID, UtcDateTime } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CompanyResponseDTO } from "../../../../common";
|
||||
import { toCompanyResponseDTO } from "../mappers/company-response.mapper";
|
||||
import type { CompanyPublicModelMapper } from "../public";
|
||||
import type { ICompanyFinder, ICompanyStatusChanger } from "../services";
|
||||
|
||||
@ -46,29 +47,3 @@ export class DisableCompanyUseCase {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toCompanyResponseDTO(model: {
|
||||
id: string;
|
||||
legalName: string;
|
||||
tradeName: string | null;
|
||||
tin: string | null;
|
||||
slug: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
website: string | null;
|
||||
defaultPaymentTermId: string | null;
|
||||
status: "active" | "disabled";
|
||||
}): CompanyResponseDTO {
|
||||
return {
|
||||
id: model.id,
|
||||
legal_name: model.legalName,
|
||||
trade_name: model.tradeName,
|
||||
tin: model.tin,
|
||||
slug: model.slug,
|
||||
email: model.email,
|
||||
phone: model.phone,
|
||||
website: model.website,
|
||||
default_payment_term_id: model.defaultPaymentTermId,
|
||||
status: model.status,
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { UniqueID, UtcDateTime } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CompanyResponseDTO } from "../../../../common";
|
||||
import { toCompanyResponseDTO } from "../mappers/company-response.mapper";
|
||||
import type { CompanyPublicModelMapper } from "../public";
|
||||
import type { ICompanyFinder, ICompanyStatusChanger } from "../services";
|
||||
|
||||
@ -46,29 +47,3 @@ export class EnableCompanyUseCase {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toCompanyResponseDTO(model: {
|
||||
id: string;
|
||||
legalName: string;
|
||||
tradeName: string | null;
|
||||
tin: string | null;
|
||||
slug: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
website: string | null;
|
||||
defaultPaymentTermId: string | null;
|
||||
status: "active" | "disabled";
|
||||
}): CompanyResponseDTO {
|
||||
return {
|
||||
id: model.id,
|
||||
legal_name: model.legalName,
|
||||
trade_name: model.tradeName,
|
||||
tin: model.tin,
|
||||
slug: model.slug,
|
||||
email: model.email,
|
||||
phone: model.phone,
|
||||
website: model.website,
|
||||
default_payment_term_id: model.defaultPaymentTermId,
|
||||
status: model.status,
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CompanyResponseDTO } from "../../../../common";
|
||||
import { toCompanyResponseDTO } from "../mappers/company-response.mapper";
|
||||
import type { CompanyPublicModelMapper } from "../public";
|
||||
import type { ICompanyFinder } from "../services";
|
||||
|
||||
@ -35,29 +36,3 @@ export class GetCompanyByIdUseCase {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toCompanyResponseDTO(model: {
|
||||
id: string;
|
||||
legalName: string;
|
||||
tradeName: string | null;
|
||||
tin: string | null;
|
||||
slug: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
website: string | null;
|
||||
defaultPaymentTermId: string | null;
|
||||
status: "active" | "disabled";
|
||||
}): CompanyResponseDTO {
|
||||
return {
|
||||
id: model.id,
|
||||
legal_name: model.legalName,
|
||||
trade_name: model.tradeName,
|
||||
tin: model.tin,
|
||||
slug: model.slug,
|
||||
email: model.email,
|
||||
phone: model.phone,
|
||||
website: model.website,
|
||||
default_payment_term_id: model.defaultPaymentTermId,
|
||||
status: model.status,
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CompanyResponseDTO } from "../../../../common";
|
||||
import { toCompanyResponseDTO } from "../mappers/company-response.mapper";
|
||||
import type { CompanyPublicModelMapper } from "../public";
|
||||
import type { ICompanyFinder } from "../services";
|
||||
|
||||
@ -35,29 +36,3 @@ export class GetCurrentCompanyUseCase {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toCompanyResponseDTO(model: {
|
||||
id: string;
|
||||
legalName: string;
|
||||
tradeName: string | null;
|
||||
tin: string | null;
|
||||
slug: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
website: string | null;
|
||||
defaultPaymentTermId: string | null;
|
||||
status: "active" | "disabled";
|
||||
}): CompanyResponseDTO {
|
||||
return {
|
||||
id: model.id,
|
||||
legal_name: model.legalName,
|
||||
trade_name: model.tradeName,
|
||||
tin: model.tin,
|
||||
slug: model.slug,
|
||||
email: model.email,
|
||||
phone: model.phone,
|
||||
website: model.website,
|
||||
default_payment_term_id: model.defaultPaymentTermId,
|
||||
status: model.status,
|
||||
};
|
||||
}
|
||||
|
||||
@ -13,8 +13,9 @@ import {
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { Company, CompanySlug, CompanyStatus, type CompanyPatchProps } from "../../../domain";
|
||||
import { Company, type CompanyPatchProps, CompanySlug, CompanyStatus } from "../../../domain";
|
||||
import { CompanyPublicModelMapper } from "../public";
|
||||
|
||||
import { UpdateCompanyUseCase } from "./update-company.use-case";
|
||||
|
||||
const buildTransactionManager = (): ITransactionManager => ({
|
||||
@ -43,19 +44,23 @@ const buildCompany = () => {
|
||||
assert.equal(website.isSuccess, true);
|
||||
assert.equal(tin.isSuccess, true);
|
||||
|
||||
const companyResult = Company.create({
|
||||
legalName: legalName.data,
|
||||
tradeName: Maybe.none(),
|
||||
tin: Maybe.some(tin.data),
|
||||
slug: slug.data,
|
||||
email: Maybe.some(email.data),
|
||||
phone: Maybe.some(phone.data),
|
||||
website: Maybe.some(website.data),
|
||||
defaultPaymentTermId: Maybe.some(UniqueID.generateNewID()),
|
||||
status: CompanyStatus.createActive(),
|
||||
createdAt: now.data,
|
||||
updatedAt: now.data,
|
||||
}, id);
|
||||
const companyResult = Company.create(
|
||||
{
|
||||
legalName: legalName.data,
|
||||
tradeName: Maybe.none(),
|
||||
tin: Maybe.some(tin.data),
|
||||
slug: slug.data,
|
||||
email: Maybe.some(email.data),
|
||||
phone: Maybe.some(phone.data),
|
||||
website: Maybe.some(website.data),
|
||||
address: Maybe.none(),
|
||||
defaultPaymentTermId: Maybe.some(UniqueID.generateNewID()),
|
||||
status: CompanyStatus.createActive(),
|
||||
createdAt: now.data,
|
||||
updatedAt: now.data,
|
||||
},
|
||||
id
|
||||
);
|
||||
|
||||
assert.equal(companyResult.isSuccess, true);
|
||||
return companyResult.data;
|
||||
@ -99,3 +104,49 @@ test("update permite limpiar el default payment term con null", async () => {
|
||||
assert.equal(receivedId?.isNone(), true);
|
||||
assert.equal(result.data.default_payment_term_id, null);
|
||||
});
|
||||
|
||||
test("update company maps the address tri-state", async () => {
|
||||
const company = buildCompany();
|
||||
const captured: Array<unknown> = [];
|
||||
const useCase = new UpdateCompanyUseCase({
|
||||
updater: {
|
||||
update: async ({ patchProps }) => {
|
||||
captured.push(patchProps.address);
|
||||
return Result.ok(company);
|
||||
},
|
||||
},
|
||||
paymentTermResolver: { ensureAssignable: async () => Result.ok(undefined) },
|
||||
responseMapper: new CompanyPublicModelMapper(),
|
||||
transactionManager: buildTransactionManager(),
|
||||
});
|
||||
const base = { company_id: company.id.toPrimitive() };
|
||||
assert.equal((await useCase.execute({ ...base, dto: {} })).isSuccess, true);
|
||||
assert.equal((await useCase.execute({ ...base, dto: { address: null } })).isSuccess, true);
|
||||
assert.equal(
|
||||
(
|
||||
await useCase.execute({
|
||||
...base,
|
||||
dto: {
|
||||
address: {
|
||||
street: "Calle Mayor 1",
|
||||
street_2: null,
|
||||
city: "Madrid",
|
||||
postal_code: "28001",
|
||||
province: "Madrid",
|
||||
country_code: "ES",
|
||||
},
|
||||
},
|
||||
})
|
||||
).isSuccess,
|
||||
true
|
||||
);
|
||||
assert.equal(captured[0], undefined);
|
||||
assert.equal((captured[1] as Maybe<unknown>).isNone(), true);
|
||||
assert.equal(
|
||||
(captured[2] as Maybe<{ countryCode: Maybe<{ toPrimitive(): string }> }>)
|
||||
.unwrap()
|
||||
.countryCode.unwrap()
|
||||
.toPrimitive(),
|
||||
"ES"
|
||||
);
|
||||
});
|
||||
|
||||
@ -12,7 +12,9 @@ import {
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CompanyResponseDTO, UpdateCompanyRequestDTO } from "../../../../common";
|
||||
import { CompanySlug, type CompanyPatchProps } from "../../../domain";
|
||||
import { type CompanyPatchProps, CompanySlug } from "../../../domain";
|
||||
import { toPostalAddress } from "../mappers/company-address.mapper";
|
||||
import { toCompanyResponseDTO } from "../mappers/company-response.mapper";
|
||||
import type { CompanyPublicModelMapper } from "../public";
|
||||
import type { ICompanyPaymentTermResolver, ICompanyUpdater } from "../services";
|
||||
|
||||
@ -69,7 +71,9 @@ export class UpdateCompanyUseCase {
|
||||
return Result.fail(updateResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(toCompanyResponseDTO(this.deps.responseMapper.toPublicModel(updateResult.data)));
|
||||
return Result.ok(
|
||||
toCompanyResponseDTO(this.deps.responseMapper.toPublicModel(updateResult.data))
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
@ -138,6 +142,18 @@ function mapUpdateCompanyProps(dto: UpdateCompanyRequestDTO): Result<CompanyPatc
|
||||
patchProps.website = websiteResult.data;
|
||||
}
|
||||
|
||||
if (dto.address !== undefined) {
|
||||
if (dto.address === null) {
|
||||
patchProps.address = Maybe.none();
|
||||
} else {
|
||||
const addressResult = toPostalAddress(dto.address);
|
||||
if (addressResult.isFailure) {
|
||||
return Result.fail(addressResult.error);
|
||||
}
|
||||
patchProps.address = Maybe.some(addressResult.data);
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.default_payment_term_id !== undefined) {
|
||||
const defaultPaymentTermIdResult = maybeFromNullableResult(
|
||||
dto.default_payment_term_id,
|
||||
@ -170,29 +186,3 @@ function mapDefaultPaymentTermId(
|
||||
|
||||
return Result.ok(Maybe.some(defaultPaymentTermIdResult.data));
|
||||
}
|
||||
|
||||
function toCompanyResponseDTO(model: {
|
||||
id: string;
|
||||
legalName: string;
|
||||
tradeName: string | null;
|
||||
tin: string | null;
|
||||
slug: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
website: string | null;
|
||||
defaultPaymentTermId: string | null;
|
||||
status: "active" | "disabled";
|
||||
}): CompanyResponseDTO {
|
||||
return {
|
||||
id: model.id,
|
||||
legal_name: model.legalName,
|
||||
trade_name: model.tradeName,
|
||||
tin: model.tin,
|
||||
slug: model.slug,
|
||||
email: model.email,
|
||||
phone: model.phone,
|
||||
website: model.website,
|
||||
default_payment_term_id: model.defaultPaymentTermId,
|
||||
status: model.status,
|
||||
};
|
||||
}
|
||||
|
||||
@ -55,6 +55,7 @@ const buildCompany = () => {
|
||||
email: Maybe.some(email.data),
|
||||
phone: Maybe.some(phone.data),
|
||||
website: Maybe.some(website.data),
|
||||
address: Maybe.none(),
|
||||
defaultPaymentTermId: Maybe.some(UniqueID.generateNewID()),
|
||||
status: CompanyStatus.createActive(),
|
||||
createdAt: now.data,
|
||||
@ -139,3 +140,49 @@ test("update current company no permite cambiar el slug", async () => {
|
||||
assert.equal(result.isSuccess, true);
|
||||
assert.equal(result.data.slug, company.slug.toPrimitive());
|
||||
});
|
||||
|
||||
test("update current company preserves omitted address and maps null or a replacement", async () => {
|
||||
const company = buildCompany();
|
||||
const captured: Array<unknown> = [];
|
||||
const useCase = new UpdateCurrentCompanyUseCase({
|
||||
updater: {
|
||||
update: async ({ patchProps }) => {
|
||||
captured.push(patchProps.address);
|
||||
return Result.ok(company);
|
||||
},
|
||||
},
|
||||
paymentTermResolver: { ensureAssignable: async () => Result.ok(undefined) },
|
||||
responseMapper: new CompanyPublicModelMapper(),
|
||||
transactionManager: buildTransactionManager(),
|
||||
});
|
||||
const base = { companyId: company.id.toPrimitive() };
|
||||
assert.equal((await useCase.execute({ ...base, dto: {} })).isSuccess, true);
|
||||
assert.equal((await useCase.execute({ ...base, dto: { address: null } })).isSuccess, true);
|
||||
assert.equal(
|
||||
(
|
||||
await useCase.execute({
|
||||
...base,
|
||||
dto: {
|
||||
address: {
|
||||
street: "Calle Mayor 1",
|
||||
street_2: null,
|
||||
city: "Madrid",
|
||||
postal_code: "28001",
|
||||
province: "Madrid",
|
||||
country_code: "ES",
|
||||
},
|
||||
},
|
||||
})
|
||||
).isSuccess,
|
||||
true
|
||||
);
|
||||
assert.equal(captured[0], undefined);
|
||||
assert.equal((captured[1] as Maybe<unknown>).isNone(), true);
|
||||
assert.equal(
|
||||
(captured[2] as Maybe<{ countryCode: Maybe<{ toPrimitive(): string }> }>)
|
||||
.unwrap()
|
||||
.countryCode.unwrap()
|
||||
.toPrimitive(),
|
||||
"ES"
|
||||
);
|
||||
});
|
||||
|
||||
@ -13,6 +13,8 @@ import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CompanyResponseDTO, UpdateCurrentCompanyRequestDTO } from "../../../../common";
|
||||
import type { CompanyPatchProps } from "../../../domain";
|
||||
import { toPostalAddress } from "../mappers/company-address.mapper";
|
||||
import { toCompanyResponseDTO } from "../mappers/company-response.mapper";
|
||||
import type { CompanyPublicModelMapper } from "../public";
|
||||
import type { ICompanyPaymentTermResolver, ICompanyUpdater } from "../services";
|
||||
|
||||
@ -92,6 +94,7 @@ function mapUpdateCurrentCompanyProps(
|
||||
assignPhonePatch(patchProps, dto),
|
||||
assignWebsitePatch(patchProps, dto),
|
||||
assignDefaultPaymentTermPatch(patchProps, dto),
|
||||
assignAddressPatch(patchProps, dto),
|
||||
];
|
||||
|
||||
for (const assignment of assignments) {
|
||||
@ -205,6 +208,25 @@ function assignWebsitePatch(
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
function assignAddressPatch(
|
||||
patchProps: CompanyPatchProps,
|
||||
dto: UpdateCurrentCompanyRequestDTO
|
||||
): Result<void, Error> {
|
||||
if (dto.address === undefined) {
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
if (dto.address === null) {
|
||||
patchProps.address = Maybe.none();
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
const addressResult = toPostalAddress(dto.address);
|
||||
if (addressResult.isFailure) {
|
||||
return Result.fail(addressResult.error);
|
||||
}
|
||||
patchProps.address = Maybe.some(addressResult.data);
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
function assignDefaultPaymentTermPatch(
|
||||
patchProps: CompanyPatchProps,
|
||||
dto: UpdateCurrentCompanyRequestDTO
|
||||
@ -242,29 +264,3 @@ function mapDefaultPaymentTermId(
|
||||
|
||||
return Result.ok(Maybe.some(defaultPaymentTermIdResult.data));
|
||||
}
|
||||
|
||||
function toCompanyResponseDTO(model: {
|
||||
id: string;
|
||||
legalName: string;
|
||||
tradeName: string | null;
|
||||
tin: string | null;
|
||||
slug: string;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
website: string | null;
|
||||
defaultPaymentTermId: string | null;
|
||||
status: "active" | "disabled";
|
||||
}): CompanyResponseDTO {
|
||||
return {
|
||||
id: model.id,
|
||||
legal_name: model.legalName,
|
||||
trade_name: model.tradeName,
|
||||
tin: model.tin,
|
||||
slug: model.slug,
|
||||
email: model.email,
|
||||
phone: model.phone,
|
||||
website: model.website,
|
||||
default_payment_term_id: model.defaultPaymentTermId,
|
||||
status: model.status,
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
City,
|
||||
CountryCode,
|
||||
Name,
|
||||
PostalAddress,
|
||||
PostalCode,
|
||||
Province,
|
||||
Street,
|
||||
UniqueID,
|
||||
UtcDate,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Maybe } from "@repo/rdx-utils";
|
||||
|
||||
import { Company } from "./company.aggregate";
|
||||
import { CompanySlug } from "./company-slug.vo";
|
||||
import { CompanyStatus } from "./company-status.vo";
|
||||
|
||||
const unwrap = <T>(result: { data: T }): T => result.data;
|
||||
const address = (street = "Street 1") =>
|
||||
unwrap(
|
||||
PostalAddress.create({
|
||||
street: Maybe.some(unwrap(Street.create(street))),
|
||||
street2: Maybe.none(),
|
||||
city: Maybe.some(unwrap(City.create("Madrid"))),
|
||||
postalCode: Maybe.some(unwrap(PostalCode.create("28001"))),
|
||||
province: Maybe.some(unwrap(Province.create("Madrid"))),
|
||||
countryCode: Maybe.some(unwrap(CountryCode.create("ES"))),
|
||||
})
|
||||
);
|
||||
const props = (value = Maybe.none<PostalAddress>()) => ({
|
||||
legalName: unwrap(Name.create("Rodax")),
|
||||
tradeName: Maybe.none(),
|
||||
tin: Maybe.none(),
|
||||
slug: unwrap(CompanySlug.create("rodax")),
|
||||
email: Maybe.none(),
|
||||
phone: Maybe.none(),
|
||||
website: Maybe.none(),
|
||||
address: value,
|
||||
defaultPaymentTermId: Maybe.none(),
|
||||
status: CompanyStatus.createActive(),
|
||||
createdAt: unwrap(UtcDate.createFromISO("2026-01-01")),
|
||||
updatedAt: unwrap(UtcDate.createFromISO("2026-01-01")),
|
||||
});
|
||||
const company = (value = Maybe.none<PostalAddress>()) =>
|
||||
unwrap(Company.create(props(value), UniqueID.generateNewID()));
|
||||
|
||||
test("Company address supports create, getters and rehydrate", () => {
|
||||
assert.equal(company().address.isNone(), true);
|
||||
const postalAddress = address();
|
||||
assert.equal(
|
||||
company(Maybe.some(postalAddress)).address.unwrap().countryCode.unwrap().toPrimitive(),
|
||||
"ES"
|
||||
);
|
||||
assert.equal(Company.rehydrate(props(), UniqueID.generateNewID()).address.isNone(), true);
|
||||
assert.equal(
|
||||
Company.rehydrate(props(Maybe.some(postalAddress)), UniqueID.generateNewID()).address.isSome(),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("Company address update preserves, sets, replaces, clears and compares primitives", () => {
|
||||
const initial = address("Street A");
|
||||
const target = company(Maybe.some(initial));
|
||||
assert.equal(
|
||||
target.update({ updatedAt: unwrap(UtcDate.createFromISO("2026-01-02")) }).data,
|
||||
false
|
||||
);
|
||||
const equivalent = address("Street A");
|
||||
assert.notEqual(initial, equivalent);
|
||||
assert.equal(
|
||||
target.update({
|
||||
address: Maybe.some(equivalent),
|
||||
updatedAt: unwrap(UtcDate.createFromISO("2026-01-02")),
|
||||
}).data,
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
target.update({
|
||||
address: Maybe.some(address("Street B")),
|
||||
updatedAt: unwrap(UtcDate.createFromISO("2026-01-02")),
|
||||
}).data,
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
target.update({ address: Maybe.none(), updatedAt: unwrap(UtcDate.createFromISO("2026-01-02")) })
|
||||
.data,
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
target.update({
|
||||
address: Maybe.some(address()),
|
||||
updatedAt: unwrap(UtcDate.createFromISO("2026-01-02")),
|
||||
}).data,
|
||||
true
|
||||
);
|
||||
});
|
||||
@ -3,6 +3,7 @@ import {
|
||||
type EmailAddress,
|
||||
type Name,
|
||||
type PhoneNumber,
|
||||
type PostalAddress,
|
||||
type TextValue,
|
||||
type URLAddress,
|
||||
type UniqueID,
|
||||
@ -22,6 +23,7 @@ export interface ICompanyCreateProps {
|
||||
email: Maybe<EmailAddress>;
|
||||
phone: Maybe<PhoneNumber>;
|
||||
website: Maybe<URLAddress>;
|
||||
address: Maybe<PostalAddress>;
|
||||
defaultPaymentTermId: Maybe<UniqueID>;
|
||||
status: CompanyStatus;
|
||||
createdAt: UtcDate;
|
||||
@ -36,6 +38,7 @@ export type CompanyPatchProps = Partial<{
|
||||
email: Maybe<EmailAddress>;
|
||||
phone: Maybe<PhoneNumber>;
|
||||
website: Maybe<URLAddress>;
|
||||
address: Maybe<PostalAddress>;
|
||||
defaultPaymentTermId: Maybe<UniqueID>;
|
||||
}> & {
|
||||
updatedAt: UtcDate;
|
||||
@ -109,6 +112,10 @@ export class Company extends AggregateRoot<CompanyInternalProps> {
|
||||
return this.props.website;
|
||||
}
|
||||
|
||||
public get address(): Maybe<PostalAddress> {
|
||||
return this.props.address;
|
||||
}
|
||||
|
||||
public get defaultPaymentTermId(): Maybe<UniqueID> {
|
||||
return this.props.defaultPaymentTermId;
|
||||
}
|
||||
@ -173,6 +180,14 @@ export class Company extends AggregateRoot<CompanyInternalProps> {
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (
|
||||
patch.address !== undefined &&
|
||||
!Company.sameMaybePostalAddress(this.props.address, patch.address)
|
||||
) {
|
||||
this.props.address = patch.address;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (
|
||||
patch.defaultPaymentTermId !== undefined &&
|
||||
!Company.sameMaybeValueObject(this.props.defaultPaymentTermId, patch.defaultPaymentTermId)
|
||||
@ -213,6 +228,32 @@ export class Company extends AggregateRoot<CompanyInternalProps> {
|
||||
return this.status.isActive();
|
||||
}
|
||||
|
||||
private static sameMaybePostalAddress(
|
||||
current: Maybe<PostalAddress>,
|
||||
next: Maybe<PostalAddress>
|
||||
): boolean {
|
||||
return current.match(
|
||||
(currentAddress) =>
|
||||
next.match(
|
||||
(nextAddress) =>
|
||||
currentAddress.street.unwrap().toPrimitive() ===
|
||||
nextAddress.street.unwrap().toPrimitive() &&
|
||||
currentAddress.street2.getOrUndefined()?.toPrimitive() ===
|
||||
nextAddress.street2.getOrUndefined()?.toPrimitive() &&
|
||||
currentAddress.city.unwrap().toPrimitive() ===
|
||||
nextAddress.city.unwrap().toPrimitive() &&
|
||||
currentAddress.postalCode.unwrap().toPrimitive() ===
|
||||
nextAddress.postalCode.unwrap().toPrimitive() &&
|
||||
currentAddress.province.unwrap().toPrimitive() ===
|
||||
nextAddress.province.unwrap().toPrimitive() &&
|
||||
currentAddress.countryCode.unwrap().toPrimitive() ===
|
||||
nextAddress.countryCode.unwrap().toPrimitive(),
|
||||
() => false
|
||||
),
|
||||
() => next.isNone()
|
||||
);
|
||||
}
|
||||
|
||||
private static sameMaybeValueObject<T extends { equals(other: T): boolean }>(
|
||||
current: Maybe<T>,
|
||||
next: Maybe<T>
|
||||
|
||||
@ -0,0 +1,144 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
City,
|
||||
CountryCode,
|
||||
Name,
|
||||
PostalAddress,
|
||||
PostalCode,
|
||||
Province,
|
||||
Street,
|
||||
UniqueID,
|
||||
UtcDate,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Maybe } from "@repo/rdx-utils";
|
||||
|
||||
import { Company } from "../../../../domain";
|
||||
import { CompanySlug } from "../../../../domain/companies/company-slug.vo";
|
||||
import { CompanyStatus } from "../../../../domain/companies/company-status.vo";
|
||||
import type { CompanyModel } from "../models";
|
||||
|
||||
import { SequelizeCompanyDomainMapper } from "./sequelize-company-domain.mapper";
|
||||
|
||||
const unwrap = <T>(result: { data: T }): T => result.data;
|
||||
const postalAddress = (street2 = "Suite 2") =>
|
||||
unwrap(
|
||||
PostalAddress.create({
|
||||
street: Maybe.some(unwrap(Street.create("Street 1"))),
|
||||
street2: Maybe.some(unwrap(Street.create(street2))),
|
||||
city: Maybe.some(unwrap(City.create("Madrid"))),
|
||||
postalCode: Maybe.some(unwrap(PostalCode.create("28001"))),
|
||||
province: Maybe.some(unwrap(Province.create("Madrid"))),
|
||||
countryCode: Maybe.some(unwrap(CountryCode.create("ES"))),
|
||||
})
|
||||
);
|
||||
const company = (address = Maybe.none<PostalAddress>()) =>
|
||||
unwrap(
|
||||
Company.create(
|
||||
{
|
||||
legalName: unwrap(Name.create("Rodax")),
|
||||
tradeName: Maybe.none(),
|
||||
tin: Maybe.none(),
|
||||
slug: unwrap(CompanySlug.create("rodax")),
|
||||
email: Maybe.none(),
|
||||
phone: Maybe.none(),
|
||||
website: Maybe.none(),
|
||||
address,
|
||||
defaultPaymentTermId: Maybe.none(),
|
||||
status: CompanyStatus.createActive(),
|
||||
createdAt: unwrap(UtcDate.createFromISO("2026-01-01")),
|
||||
updatedAt: unwrap(UtcDate.createFromISO("2026-01-01")),
|
||||
},
|
||||
UniqueID.generateNewID()
|
||||
)
|
||||
);
|
||||
const row = (patch: Record<string, string | null> = {}) =>
|
||||
({
|
||||
id: UniqueID.generateNewID().toPrimitive(),
|
||||
legal_name: "Rodax",
|
||||
trade_name: null,
|
||||
tin: null,
|
||||
slug: "rodax",
|
||||
email: null,
|
||||
phone: null,
|
||||
website: null,
|
||||
default_payment_term_id: null,
|
||||
status: "active",
|
||||
created_at: "2026-01-01T00:00:00.000Z",
|
||||
updated_at: "2026-01-01T00:00:00.000Z",
|
||||
address_street: null,
|
||||
address_street_2: null,
|
||||
address_city: null,
|
||||
address_postal_code: null,
|
||||
address_province: null,
|
||||
address_country_code: null,
|
||||
...patch,
|
||||
}) as CompanyModel;
|
||||
const mapper = new SequelizeCompanyDomainMapper();
|
||||
|
||||
test("mapper persists none, complete and missing street2", () => {
|
||||
const none = unwrap(mapper.mapToPersistence(company()));
|
||||
assert.deepEqual(
|
||||
[
|
||||
none.address_street,
|
||||
none.address_street_2,
|
||||
none.address_city,
|
||||
none.address_postal_code,
|
||||
none.address_province,
|
||||
none.address_country_code,
|
||||
],
|
||||
[null, null, null, null, null, null]
|
||||
);
|
||||
const full = unwrap(mapper.mapToPersistence(company(Maybe.some(postalAddress()))));
|
||||
assert.deepEqual(
|
||||
[
|
||||
full.address_street,
|
||||
full.address_street_2,
|
||||
full.address_city,
|
||||
full.address_postal_code,
|
||||
full.address_province,
|
||||
full.address_country_code,
|
||||
],
|
||||
["Street 1", "Suite 2", "Madrid", "28001", "Madrid", "ES"]
|
||||
);
|
||||
assert.equal(
|
||||
unwrap(
|
||||
mapper.mapToPersistence(
|
||||
company(
|
||||
Maybe.some(
|
||||
unwrap(PostalAddress.create({ ...postalAddress().getProps(), street2: Maybe.none() }))
|
||||
)
|
||||
)
|
||||
)
|
||||
).address_street_2,
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test("mapper rehydrates valid states and rejects partial states", () => {
|
||||
assert.equal(unwrap(mapper.mapToDomain(row())).address.isNone(), true);
|
||||
const complete = row({
|
||||
address_street: "Street 1",
|
||||
address_street_2: "Suite 2",
|
||||
address_city: "Madrid",
|
||||
address_postal_code: "28001",
|
||||
address_province: "Madrid",
|
||||
address_country_code: "ES",
|
||||
});
|
||||
assert.equal(
|
||||
unwrap(mapper.mapToDomain(complete)).address.unwrap().countryCode.unwrap().toPrimitive(),
|
||||
"ES"
|
||||
);
|
||||
assert.equal(
|
||||
unwrap(mapper.mapToDomain(row({ ...complete, address_street_2: null })))
|
||||
.address.unwrap()
|
||||
.street2.isNone(),
|
||||
true
|
||||
);
|
||||
for (const key of ["address_street", "address_postal_code", "address_country_code"]) {
|
||||
const partial = row(complete as unknown as Record<string, string | null>);
|
||||
partial[key as keyof CompanyModel] = null as never;
|
||||
assert.equal(mapper.mapToDomain(partial).isFailure, true);
|
||||
}
|
||||
});
|
||||
@ -1,8 +1,14 @@
|
||||
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||
import {
|
||||
City,
|
||||
CountryCode,
|
||||
EmailAddress,
|
||||
Name,
|
||||
PhoneNumber,
|
||||
PostalAddress,
|
||||
PostalCode,
|
||||
Province,
|
||||
Street,
|
||||
TextValue,
|
||||
URLAddress,
|
||||
UniqueID,
|
||||
@ -13,15 +19,46 @@ import {
|
||||
maybeFromNullableResult,
|
||||
maybeToNullable,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { Company, CompanySlug, CompanyStatus, type CompanyInternalProps } from "../../../../domain";
|
||||
import { Company, type CompanyInternalProps, CompanySlug, CompanyStatus } from "../../../../domain";
|
||||
import type { CompanyCreationAttributes, CompanyModel } from "../models";
|
||||
|
||||
function toUtcIsoString(value: Date | string): string {
|
||||
return value instanceof Date ? value.toISOString() : value;
|
||||
}
|
||||
|
||||
function mapAddress(source: CompanyModel): Result<Maybe<PostalAddress>, Error> {
|
||||
const values = [
|
||||
source.address_street,
|
||||
source.address_city,
|
||||
source.address_postal_code,
|
||||
source.address_province,
|
||||
source.address_country_code,
|
||||
];
|
||||
if (values.every((value) => value === null) && source.address_street_2 === null)
|
||||
return Result.ok(Maybe.none());
|
||||
if (values.some((value) => value === null))
|
||||
return Result.fail(new Error("Company address persistence is partial."));
|
||||
const street = Street.create(source.address_street as string);
|
||||
const city = City.create(source.address_city as string);
|
||||
const postalCode = PostalCode.create(source.address_postal_code as string);
|
||||
const province = Province.create(source.address_province as string);
|
||||
const countryCode = CountryCode.create(source.address_country_code as string);
|
||||
const street2 = maybeFromNullableResult(source.address_street_2, (value) => Street.create(value));
|
||||
for (const result of [street, city, postalCode, province, countryCode, street2])
|
||||
if (result.isFailure) return Result.fail(result.error);
|
||||
const address = PostalAddress.create({
|
||||
street: Maybe.some(street.data),
|
||||
street2: street2.data,
|
||||
city: Maybe.some(city.data),
|
||||
postalCode: Maybe.some(postalCode.data),
|
||||
province: Maybe.some(province.data),
|
||||
countryCode: Maybe.some(countryCode.data),
|
||||
});
|
||||
return address.isFailure ? Result.fail(address.error) : Result.ok(Maybe.some(address.data));
|
||||
}
|
||||
|
||||
export class SequelizeCompanyDomainMapper extends SequelizeDomainMapper<
|
||||
CompanyModel,
|
||||
CompanyCreationAttributes,
|
||||
@ -64,6 +101,8 @@ export class SequelizeCompanyDomainMapper extends SequelizeDomainMapper<
|
||||
"default_payment_term_id",
|
||||
errors
|
||||
);
|
||||
const address = mapAddress(source);
|
||||
if (address.isFailure) return Result.fail(address.error);
|
||||
const status = extractOrPushError(CompanyStatus.create(source.status), "status", errors);
|
||||
const createdAt = extractOrPushError(
|
||||
UtcDateTime.createFromISO(toUtcIsoString(source.created_at)),
|
||||
@ -88,6 +127,7 @@ export class SequelizeCompanyDomainMapper extends SequelizeDomainMapper<
|
||||
email: email!,
|
||||
phone: phone!,
|
||||
website: website!,
|
||||
address: address.data,
|
||||
defaultPaymentTermId: defaultPaymentTermId!,
|
||||
status: status!,
|
||||
createdAt: createdAt!,
|
||||
@ -113,6 +153,22 @@ export class SequelizeCompanyDomainMapper extends SequelizeDomainMapper<
|
||||
email: maybeToNullable(source.email, (value: EmailAddress) => value.toPrimitive()),
|
||||
phone: maybeToNullable(source.phone, (value: PhoneNumber) => value.toPrimitive()),
|
||||
website: maybeToNullable(source.website, (value: URLAddress) => value.toPrimitive()),
|
||||
address_street: maybeToNullable(source.address, (value) =>
|
||||
value.street.unwrap().toPrimitive()
|
||||
),
|
||||
address_street_2: maybeToNullable(source.address, (value) =>
|
||||
maybeToNullable(value.street2, (part) => part.toPrimitive())
|
||||
),
|
||||
address_city: maybeToNullable(source.address, (value) => value.city.unwrap().toPrimitive()),
|
||||
address_postal_code: maybeToNullable(source.address, (value) =>
|
||||
value.postalCode.unwrap().toPrimitive()
|
||||
),
|
||||
address_province: maybeToNullable(source.address, (value) =>
|
||||
value.province.unwrap().toPrimitive()
|
||||
),
|
||||
address_country_code: maybeToNullable(source.address, (value) =>
|
||||
value.countryCode.unwrap().toPrimitive()
|
||||
),
|
||||
default_payment_term_id: maybeToNullable(source.defaultPaymentTermId, (value) =>
|
||||
value.toPrimitive()
|
||||
),
|
||||
|
||||
@ -20,6 +20,12 @@ export class CompanyModel extends Model<
|
||||
declare email: string | null;
|
||||
declare phone: string | null;
|
||||
declare website: string | null;
|
||||
declare address_street: string | null;
|
||||
declare address_street_2: string | null;
|
||||
declare address_city: string | null;
|
||||
declare address_postal_code: string | null;
|
||||
declare address_province: string | null;
|
||||
declare address_country_code: string | null;
|
||||
declare default_payment_term_id: string | null;
|
||||
declare status: string;
|
||||
declare created_at: string;
|
||||
@ -72,6 +78,12 @@ export default (database: Sequelize) => {
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
address_street: { type: DataTypes.STRING, allowNull: true, defaultValue: null },
|
||||
address_street_2: { type: DataTypes.STRING, allowNull: true, defaultValue: null },
|
||||
address_city: { type: DataTypes.STRING, allowNull: true, defaultValue: null },
|
||||
address_postal_code: { type: DataTypes.STRING, allowNull: true, defaultValue: null },
|
||||
address_province: { type: DataTypes.STRING, allowNull: true, defaultValue: null },
|
||||
address_country_code: { type: DataTypes.CHAR(2), allowNull: true, defaultValue: null },
|
||||
default_payment_term_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./request";
|
||||
export * from "./response";
|
||||
export * from "./shared";
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { EmailSchema, TinSchema, WebsiteSchema } from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { CompanyAddressRequestSchema } from "../shared";
|
||||
|
||||
export const CreateCompanyRequestSchema = z.object({
|
||||
id: z.uuid(),
|
||||
legal_name: z.string().min(1),
|
||||
@ -10,6 +12,7 @@ export const CreateCompanyRequestSchema = z.object({
|
||||
email: EmailSchema.nullable().optional(),
|
||||
phone: z.string().nullable().optional(),
|
||||
website: WebsiteSchema.nullable().optional(),
|
||||
address: CompanyAddressRequestSchema.nullable().optional(),
|
||||
});
|
||||
|
||||
export type CreateCompanyRequestDTO = z.infer<typeof CreateCompanyRequestSchema>;
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import { EmailSchema, TinSchema, WebsiteSchema } from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { CompanyAddressRequestSchema } from "../shared";
|
||||
|
||||
export const UpdateCompanyByIdParamsRequestSchema = z.object({
|
||||
company_id: z.uuid(),
|
||||
});
|
||||
|
||||
export type UpdateCompanyByIdParamsRequestDTO = z.infer<typeof UpdateCompanyByIdParamsRequestSchema>;
|
||||
export type UpdateCompanyByIdParamsRequestDTO = z.infer<
|
||||
typeof UpdateCompanyByIdParamsRequestSchema
|
||||
>;
|
||||
|
||||
export const UpdateCompanyRequestSchema = z.object({
|
||||
legal_name: z.string().min(1).optional(),
|
||||
@ -16,6 +20,7 @@ export const UpdateCompanyRequestSchema = z.object({
|
||||
phone: z.string().nullable().optional(),
|
||||
website: WebsiteSchema.nullable().optional(),
|
||||
default_payment_term_id: z.uuid().nullable().optional(),
|
||||
address: CompanyAddressRequestSchema.nullable().optional(),
|
||||
});
|
||||
|
||||
export type UpdateCompanyRequestDTO = z.infer<typeof UpdateCompanyRequestSchema>;
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { EmailSchema, TinSchema, WebsiteSchema } from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { CompanyAddressRequestSchema } from "../shared";
|
||||
|
||||
export const UpdateCurrentCompanyRequestSchema = z.object({
|
||||
legal_name: z.string().min(1).optional(),
|
||||
trade_name: z.string().nullable().optional(),
|
||||
@ -9,6 +11,7 @@ export const UpdateCurrentCompanyRequestSchema = z.object({
|
||||
phone: z.string().nullable().optional(),
|
||||
website: WebsiteSchema.nullable().optional(),
|
||||
default_payment_term_id: z.uuid().nullable().optional(),
|
||||
address: CompanyAddressRequestSchema.nullable().optional(),
|
||||
});
|
||||
|
||||
export type UpdateCurrentCompanyRequestDTO = z.infer<typeof UpdateCurrentCompanyRequestSchema>;
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { CompanyResponseSchema } from "./company.response.dto";
|
||||
|
||||
const response = (address: unknown) => ({ id: "550e8400-e29b-41d4-a716-446655440000", legal_name: "Rodax", trade_name: null, tin: null, slug: "rodax", email: null, phone: null, website: null, address, default_payment_term_id: null, status: "active" });
|
||||
const complete = { street: "Calle Mayor 1", street_2: null, city: "Madrid", postal_code: "28001", province: "Madrid", country_code: "ES" };
|
||||
|
||||
test("CompanyResponseSchema requires a complete nullable address", () => {
|
||||
assert.equal(CompanyResponseSchema.safeParse(response(null)).success, true);
|
||||
assert.equal(CompanyResponseSchema.safeParse(response(complete)).success, true);
|
||||
const omitted = response(null); delete (omitted as { address?: unknown }).address;
|
||||
assert.equal(CompanyResponseSchema.safeParse(omitted).success, false);
|
||||
const partial = { ...complete }; delete (partial as { country_code?: unknown }).country_code;
|
||||
assert.equal(CompanyResponseSchema.safeParse(response(partial)).success, false);
|
||||
});
|
||||
@ -1,6 +1,15 @@
|
||||
import { EmailSchema, TinSchema, WebsiteSchema } from "@erp/core";
|
||||
import { CountryCodeSchema, EmailSchema, PostalCodeSchema, TinSchema, WebsiteSchema } from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const CompanyAddressResponseSchema = z.object({
|
||||
street: z.string(),
|
||||
street_2: z.string().nullable(),
|
||||
city: z.string(),
|
||||
postal_code: PostalCodeSchema,
|
||||
province: z.string(),
|
||||
country_code: CountryCodeSchema,
|
||||
});
|
||||
|
||||
export const CompanyStatusResponseSchema = z.enum(["active", "disabled"]);
|
||||
|
||||
export const CompanyResponseSchema = z.object({
|
||||
@ -12,6 +21,7 @@ export const CompanyResponseSchema = z.object({
|
||||
email: EmailSchema.nullable(),
|
||||
phone: z.string().nullable(),
|
||||
website: WebsiteSchema.nullable(),
|
||||
address: CompanyAddressResponseSchema.nullable(),
|
||||
default_payment_term_id: z.uuid().nullable(),
|
||||
status: CompanyStatusResponseSchema,
|
||||
});
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { CreateCompanyRequestSchema } from "../request/create-company.request.dto";
|
||||
import { UpdateCompanyRequestSchema } from "../request/update-company.request.dto";
|
||||
import { UpdateCurrentCompanyRequestSchema } from "../request/update-current-company.request.dto";
|
||||
|
||||
const address = {
|
||||
street: "Calle Mayor 1",
|
||||
street_2: null,
|
||||
city: "Madrid",
|
||||
postal_code: "28001",
|
||||
province: "Madrid",
|
||||
country_code: "ES",
|
||||
};
|
||||
|
||||
const create = {
|
||||
id: "550e8400-e29b-41d4-a716-446655440000",
|
||||
legal_name: "Rodax Labs",
|
||||
slug: "rodax-labs",
|
||||
};
|
||||
|
||||
test("create address accepts omitted, null and complete values", () => {
|
||||
assert.equal(CreateCompanyRequestSchema.safeParse(create).success, true);
|
||||
assert.equal(CreateCompanyRequestSchema.safeParse({ ...create, address: null }).success, true);
|
||||
assert.equal(CreateCompanyRequestSchema.safeParse({ ...create, address }).success, true);
|
||||
});
|
||||
|
||||
test("company address rejects partial and invalid values", () => {
|
||||
assert.equal(
|
||||
CreateCompanyRequestSchema.safeParse({ ...create, address: { city: "Madrid" } }).success,
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
CreateCompanyRequestSchema.safeParse({
|
||||
...create,
|
||||
address: { ...address, country_code: "ESP" },
|
||||
}).success,
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
CreateCompanyRequestSchema.safeParse({
|
||||
...create,
|
||||
address: { ...address, postal_code: "invalid" },
|
||||
}).success,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("patch schemas accept the address tri-state and reject partial values", () => {
|
||||
for (const schema of [UpdateCurrentCompanyRequestSchema, UpdateCompanyRequestSchema]) {
|
||||
assert.equal(schema.safeParse({}).success, true);
|
||||
assert.equal(schema.safeParse({ address: null }).success, true);
|
||||
assert.equal(schema.safeParse({ address }).success, true);
|
||||
assert.equal(schema.safeParse({ address: { city: "Madrid" } }).success, false);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
import { CountryCodeSchema, PostalCodeSchema } from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const CompanyAddressRequestSchema = z.object({
|
||||
street: z.string().trim().min(1),
|
||||
street_2: z.string().trim().nullable(),
|
||||
city: z.string().trim().min(1),
|
||||
postal_code: PostalCodeSchema,
|
||||
province: z.string().trim().min(1),
|
||||
country_code: CountryCodeSchema,
|
||||
});
|
||||
|
||||
export type CompanyAddressRequestDTO = z.infer<typeof CompanyAddressRequestSchema>;
|
||||
1
modules/companies/src/common/dto/shared/index.ts
Normal file
1
modules/companies/src/common/dto/shared/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./company-address.request.dto";
|
||||
@ -72,6 +72,14 @@ export const useCompanyDetailsSettingsController = () => {
|
||||
email: "",
|
||||
phone: "",
|
||||
website: "",
|
||||
address: {
|
||||
street: "",
|
||||
street2: "",
|
||||
city: "",
|
||||
postalCode: "",
|
||||
province: "",
|
||||
countryCode: "",
|
||||
},
|
||||
defaultPaymentTermId: "",
|
||||
};
|
||||
}
|
||||
@ -102,7 +110,8 @@ export const useCompanyDetailsSettingsController = () => {
|
||||
paymentTermsQuery.error ??
|
||||
(needsCurrentPaymentTerm ? currentPaymentTermQuery.error : null);
|
||||
|
||||
const hasRequiredData = Boolean(company) &&
|
||||
const hasRequiredData =
|
||||
Boolean(company) &&
|
||||
Boolean(paymentTermsQuery.data) &&
|
||||
(!needsCurrentPaymentTerm || Boolean(currentPaymentTermQuery.data));
|
||||
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { CompanySettingsFormSchema } from "./company-settings-form.schema";
|
||||
|
||||
const base = {
|
||||
legalName: "Acme",
|
||||
tradeName: "",
|
||||
tin: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
website: "",
|
||||
defaultPaymentTermId: "",
|
||||
};
|
||||
const empty = { street: "", street2: "", city: "", postalCode: "", province: "", countryCode: "" };
|
||||
const complete = {
|
||||
street: "Calle Mayor 1",
|
||||
street2: "",
|
||||
city: "Madrid",
|
||||
postalCode: "28001",
|
||||
province: "Madrid",
|
||||
countryCode: "ES",
|
||||
};
|
||||
|
||||
test("validates empty and complete addresses", () => {
|
||||
assert.equal(CompanySettingsFormSchema.safeParse({ ...base, address: empty }).success, true);
|
||||
assert.equal(CompanySettingsFormSchema.safeParse({ ...base, address: complete }).success, true);
|
||||
});
|
||||
|
||||
test("rejects partial, invalid postal code and invalid country code", () => {
|
||||
assert.equal(
|
||||
CompanySettingsFormSchema.safeParse({ ...base, address: { ...empty, street: "Calle" } })
|
||||
.success,
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
CompanySettingsFormSchema.safeParse({
|
||||
...base,
|
||||
address: { ...complete, postalCode: "invalid" },
|
||||
}).success,
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
CompanySettingsFormSchema.safeParse({ ...base, address: { ...complete, countryCode: "ESP" } })
|
||||
.success,
|
||||
false
|
||||
);
|
||||
});
|
||||
@ -1,6 +1,46 @@
|
||||
import { EmailSchema, TinSchema, WebsiteSchema } from "@erp/core";
|
||||
import {
|
||||
CountryCodeSchema,
|
||||
EmailSchema,
|
||||
PostalCodeSchema,
|
||||
TinSchema,
|
||||
WebsiteSchema,
|
||||
} from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const AddressFormSchema = z
|
||||
.object({
|
||||
street: z.string(),
|
||||
street2: z.string(),
|
||||
city: z.string(),
|
||||
postalCode: z.string(),
|
||||
province: z.string(),
|
||||
countryCode: z.string(),
|
||||
})
|
||||
.superRefine((address, ctx) => {
|
||||
const required = [
|
||||
address.street,
|
||||
address.city,
|
||||
address.postalCode,
|
||||
address.province,
|
||||
address.countryCode,
|
||||
];
|
||||
const empty = required.every((value) => value.trim() === "");
|
||||
if (empty) return;
|
||||
if (address.street.trim() === "")
|
||||
ctx.addIssue({ code: "custom", path: ["street"], message: "Required" });
|
||||
if (address.city.trim() === "")
|
||||
ctx.addIssue({ code: "custom", path: ["city"], message: "Required" });
|
||||
if (address.postalCode.trim() === "" || !PostalCodeSchema.safeParse(address.postalCode).success)
|
||||
ctx.addIssue({ code: "custom", path: ["postalCode"], message: "Invalid postal code" });
|
||||
if (address.province.trim() === "")
|
||||
ctx.addIssue({ code: "custom", path: ["province"], message: "Required" });
|
||||
if (
|
||||
address.countryCode.trim() === "" ||
|
||||
!CountryCodeSchema.safeParse(address.countryCode).success
|
||||
)
|
||||
ctx.addIssue({ code: "custom", path: ["countryCode"], message: "Invalid country code" });
|
||||
});
|
||||
|
||||
export const CompanySettingsFormSchema = z.object({
|
||||
legalName: z.string().min(1),
|
||||
tradeName: z.string(),
|
||||
@ -9,6 +49,6 @@ export const CompanySettingsFormSchema = z.object({
|
||||
phone: z.string(),
|
||||
website: WebsiteSchema.or(z.literal("")),
|
||||
defaultPaymentTermId: z.string(),
|
||||
address: AddressFormSchema,
|
||||
});
|
||||
|
||||
export type CompanySettingsForm = z.infer<typeof CompanySettingsFormSchema>;
|
||||
|
||||
@ -15,6 +15,7 @@ test("preserva null en los campos nullable del DTO", () => {
|
||||
email: null,
|
||||
phone: null,
|
||||
website: null,
|
||||
address: null,
|
||||
default_payment_term_id: null,
|
||||
status: "active",
|
||||
};
|
||||
@ -27,6 +28,38 @@ test("preserva null en los campos nullable del DTO", () => {
|
||||
email: null,
|
||||
phone: null,
|
||||
website: null,
|
||||
address: null,
|
||||
defaultPaymentTermId: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("maps API address to company settings camelCase", () => {
|
||||
const result = CompanySettingsAdapter.fromDto({
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
legal_name: "Acme",
|
||||
trade_name: null,
|
||||
tin: null,
|
||||
slug: "acme",
|
||||
email: null,
|
||||
phone: null,
|
||||
website: null,
|
||||
address: {
|
||||
street: "Calle Mayor 1",
|
||||
street_2: "2º B",
|
||||
city: "Madrid",
|
||||
postal_code: "28001",
|
||||
province: "Madrid",
|
||||
country_code: "ES",
|
||||
},
|
||||
default_payment_term_id: null,
|
||||
status: "active",
|
||||
});
|
||||
assert.deepEqual(result.address, {
|
||||
street: "Calle Mayor 1",
|
||||
street2: "2º B",
|
||||
city: "Madrid",
|
||||
postalCode: "28001",
|
||||
province: "Madrid",
|
||||
countryCode: "ES",
|
||||
});
|
||||
});
|
||||
|
||||
@ -11,6 +11,17 @@ export const CompanySettingsAdapter = {
|
||||
email: dto.email,
|
||||
phone: dto.phone,
|
||||
website: dto.website,
|
||||
address:
|
||||
dto.address == null
|
||||
? null
|
||||
: {
|
||||
street: dto.address.street,
|
||||
street2: dto.address.street_2,
|
||||
city: dto.address.city,
|
||||
postalCode: dto.address.postal_code,
|
||||
province: dto.address.province,
|
||||
countryCode: dto.address.country_code,
|
||||
},
|
||||
defaultPaymentTermId: dto.default_payment_term_id,
|
||||
};
|
||||
},
|
||||
|
||||
@ -6,5 +6,13 @@ export interface CompanySettings {
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
website: string | null;
|
||||
address: {
|
||||
street: string;
|
||||
street2: string | null;
|
||||
city: string;
|
||||
postalCode: string;
|
||||
province: string;
|
||||
countryCode: string;
|
||||
} | null;
|
||||
defaultPaymentTermId: string | null;
|
||||
}
|
||||
|
||||
@ -166,7 +166,6 @@ export const CompanyDetailsSettingsPage = () => {
|
||||
)}
|
||||
typePreset="email"
|
||||
/>
|
||||
|
||||
<TextField<CompanySettingsForm>
|
||||
description={t(
|
||||
"settings.companyDetails.sections.contact.fields.phone.description"
|
||||
@ -197,6 +196,45 @@ export const CompanyDetailsSettingsPage = () => {
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxPanel>
|
||||
<RdxPanel
|
||||
description="Datos fiscales de la empresa"
|
||||
icon={<ContactIcon className="size-5" />}
|
||||
title="Dirección fiscal"
|
||||
variant="plain"
|
||||
>
|
||||
<RdxFormGrid className="md:grid-cols-2">
|
||||
<TextField<CompanySettingsForm>
|
||||
disabled={ctrl.isSaving}
|
||||
label="Dirección"
|
||||
name="address.street"
|
||||
/>
|
||||
<TextField<CompanySettingsForm>
|
||||
disabled={ctrl.isSaving}
|
||||
label="Dirección 2"
|
||||
name="address.street2"
|
||||
/>
|
||||
<TextField<CompanySettingsForm>
|
||||
disabled={ctrl.isSaving}
|
||||
label="Código postal"
|
||||
name="address.postalCode"
|
||||
/>
|
||||
<TextField<CompanySettingsForm>
|
||||
disabled={ctrl.isSaving}
|
||||
label="Localidad"
|
||||
name="address.city"
|
||||
/>
|
||||
<TextField<CompanySettingsForm>
|
||||
disabled={ctrl.isSaving}
|
||||
label="Provincia"
|
||||
name="address.province"
|
||||
/>
|
||||
<TextField<CompanySettingsForm>
|
||||
disabled={ctrl.isSaving}
|
||||
label="País"
|
||||
name="address.countryCode"
|
||||
/>
|
||||
</RdxFormGrid>
|
||||
</RdxPanel>
|
||||
|
||||
<RdxPanel
|
||||
description={t("settings.companyDetails.sections.paymentTerm.description")}
|
||||
|
||||
@ -14,6 +14,7 @@ test('convierte null del Entity a "" en el Form para campos nullable', () => {
|
||||
email: null,
|
||||
phone: null,
|
||||
website: null,
|
||||
address: null,
|
||||
defaultPaymentTermId: null,
|
||||
};
|
||||
|
||||
@ -24,6 +25,7 @@ test('convierte null del Entity a "" en el Form para campos nullable', () => {
|
||||
email: "",
|
||||
phone: "",
|
||||
website: "",
|
||||
address: { street: "", street2: "", city: "", postalCode: "", province: "", countryCode: "" },
|
||||
defaultPaymentTermId: "",
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,5 +8,16 @@ export const buildCompanySettingsForm = (company: CompanySettings): CompanySetti
|
||||
email: company.email ?? "",
|
||||
phone: company.phone ?? "",
|
||||
website: company.website ?? "",
|
||||
address:
|
||||
company.address == null
|
||||
? { street: "", street2: "", city: "", postalCode: "", province: "", countryCode: "" }
|
||||
: {
|
||||
street: company.address.street,
|
||||
street2: company.address.street2 ?? "",
|
||||
city: company.address.city,
|
||||
postalCode: company.address.postalCode,
|
||||
province: company.address.province,
|
||||
countryCode: company.address.countryCode,
|
||||
},
|
||||
defaultPaymentTermId: company.defaultPaymentTermId ?? "",
|
||||
});
|
||||
|
||||
@ -10,6 +10,7 @@ const buildForm = () => ({
|
||||
email: "info@acme.test",
|
||||
phone: "+34 900 000 000",
|
||||
website: "https://acme.test",
|
||||
address: { street: "", street2: "", city: "", postalCode: "", province: "", countryCode: "" },
|
||||
defaultPaymentTermId: "22222222-2222-4222-8222-222222222222",
|
||||
});
|
||||
|
||||
@ -150,6 +151,7 @@ test("convierte defaultPaymentTermId vacío dirty a null", () => {
|
||||
const result = buildUpdateCompanySettingsParams({
|
||||
form: {
|
||||
...buildForm(),
|
||||
address: { street: "", street2: "", city: "", postalCode: "", province: "", countryCode: "" },
|
||||
defaultPaymentTermId: "",
|
||||
},
|
||||
dirtyFields: {
|
||||
@ -187,3 +189,40 @@ test("envía varios campos dirty y omite el resto", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("maps dirty empty address to null", () => {
|
||||
const result = buildUpdateCompanySettingsParams({
|
||||
form: buildForm(),
|
||||
dirtyFields: { address: { street: true } },
|
||||
});
|
||||
assert.deepEqual(result, { data: { address: null } });
|
||||
});
|
||||
|
||||
test("maps dirty address to the request contract", () => {
|
||||
const result = buildUpdateCompanySettingsParams({
|
||||
form: {
|
||||
...buildForm(),
|
||||
address: {
|
||||
street: " Calle Mayor 1 ",
|
||||
street2: "",
|
||||
city: " Madrid ",
|
||||
postalCode: "28001",
|
||||
province: " Madrid ",
|
||||
countryCode: "ES",
|
||||
},
|
||||
},
|
||||
dirtyFields: { address: { street: true } },
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
data: {
|
||||
address: {
|
||||
street: "Calle Mayor 1",
|
||||
street_2: null,
|
||||
city: "Madrid",
|
||||
postal_code: "28001",
|
||||
province: "Madrid",
|
||||
country_code: "ES",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { FieldNamesMarkedBoolean } from "react-hook-form";
|
||||
|
||||
import type { UpdateCurrentCompanyRequestDTO } from "../../../../common";
|
||||
import type { CompanySettingsForm } from "../entities";
|
||||
import type { FieldNamesMarkedBoolean } from "react-hook-form";
|
||||
|
||||
export const buildUpdateCompanySettingsParams = (params: {
|
||||
form: CompanySettingsForm;
|
||||
@ -38,9 +39,28 @@ export const buildUpdateCompanySettingsParams = (params: {
|
||||
data.default_payment_term_id = toNullableText(params.form.defaultPaymentTermId);
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
};
|
||||
if (params.dirtyFields.address) {
|
||||
const address = params.form.address;
|
||||
const required = [
|
||||
address.street,
|
||||
address.city,
|
||||
address.postalCode,
|
||||
address.province,
|
||||
address.countryCode,
|
||||
];
|
||||
data.address = required.every((value) => value.trim() === "")
|
||||
? null
|
||||
: {
|
||||
street: address.street.trim(),
|
||||
street_2: toNullableText(address.street2),
|
||||
city: address.city.trim(),
|
||||
postal_code: address.postalCode.trim(),
|
||||
province: address.province.trim(),
|
||||
country_code: address.countryCode.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
return { data };
|
||||
};
|
||||
|
||||
const toRequiredText = (value: string): string => {
|
||||
|
||||
@ -34,7 +34,7 @@ export class SequelizeProformaV2RecipientDomainMapper {
|
||||
const _city = source.current_customer.city;
|
||||
const _postal_code = source.current_customer.postal_code;
|
||||
const _province = source.current_customer.province;
|
||||
const _country = source.current_customer.country;
|
||||
const _country_code = source.current_customer.country_code;
|
||||
|
||||
const customerName = extractOrPushError(Name.create(_name), "current_customer.name", errors);
|
||||
const customerTin = extractOrPushError(
|
||||
@ -68,8 +68,8 @@ export class SequelizeProformaV2RecipientDomainMapper {
|
||||
errors
|
||||
);
|
||||
const customerCountry = extractOrPushError(
|
||||
maybeFromNullableResult(_country, (value) => Country.create(value)),
|
||||
"current_customer.country",
|
||||
maybeFromNullableResult(_country_code, (value) => Country.create(value)),
|
||||
"current_customer.country_code",
|
||||
errors
|
||||
);
|
||||
|
||||
|
||||
@ -458,7 +458,7 @@ export class SequelizeProformaRepositoryV2
|
||||
"city",
|
||||
"postal_code",
|
||||
"province",
|
||||
"country",
|
||||
"country_code",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -62,9 +62,7 @@ export const ProformaUpdateTaxEditor = ({
|
||||
)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
<Separator className="w-full col-start-1 col-span-full" />
|
||||
|
||||
<SwitchField
|
||||
className="md:col-span-12 md:col-start-1 not-disabled:cursor-pointer"
|
||||
disabled={disabled}
|
||||
@ -75,7 +73,6 @@ export const ProformaUpdateTaxEditor = ({
|
||||
}}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
className="md:col-span-12 md:col-start-1 min-[480px]:flex-row min-[480px]:items-center min-[480px]:*:w-auto min-[480px]:[&>[data-slot=field-label]]:min-w-0 min-[480px]:[&>[data-slot=field-label]]:flex-auto min-[480px]:[&>[button]]:ml-auto min-[480px]:[&>[button]]:min-w-[11rem] min-[480px]:[&>[button]]:flex-none [&>[data-slot=field-description]]:basis-full [&>[data-slot=field-description]]:w-full [&>[data-slot=field-description]]:pl-0 [&>[data-slot=field-error]]:basis-full [&>[data-slot=field-error]]:w-full [&>[data-slot=field-error]]:pl-0"
|
||||
description={
|
||||
@ -105,7 +102,6 @@ export const ProformaUpdateTaxEditor = ({
|
||||
readOnly={readOnly}
|
||||
serialize={(value) => (typeof value === "number" ? String(value) : "")}
|
||||
/>
|
||||
|
||||
<Separator className="w-full col-start-1 col-span-full" />
|
||||
|
||||
<SwitchField
|
||||
@ -129,7 +125,6 @@ export const ProformaUpdateTaxEditor = ({
|
||||
onCheckedChange={taxCtrl.updateRecPercentage}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
<SwitchField
|
||||
className="md:col-span-12 md:col-start-1"
|
||||
disabled={disabled}
|
||||
@ -137,7 +132,6 @@ export const ProformaUpdateTaxEditor = ({
|
||||
name="hasRetentionPercentage"
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
className="md:col-span-12 md:col-start-1 min-[480px]:flex-row min-[480px]:items-center min-[480px]:*:w-auto min-[480px]:[&>[data-slot=field-label]]:min-w-0 min-[480px]:[&>[data-slot=field-label]]:flex-auto min-[480px]:[&>[button]]:ml-auto min-[480px]:[&>[button]]:min-w-[11rem] min-[480px]:[&>[button]]:flex-none [&>[data-slot=field-description]]:basis-full [&>[data-slot=field-description]]:w-full [&>[data-slot=field-description]]:pl-0 [&>[data-slot=field-error]]:basis-full [&>[data-slot=field-error]]:w-full [&>[data-slot=field-error]]:pl-0"
|
||||
deserialize={(value) => (value === null || value === "" ? null : Number(value))}
|
||||
|
||||
@ -185,7 +185,7 @@ export const ProformaUpdatePage = () => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RdxPageContainer padding="default" width="full">
|
||||
<RdxPageContainer width="full">
|
||||
{hasPageAlerts ? (
|
||||
<RdxPageContainer className="pb-0" width="full">
|
||||
<RdxPageStack spacing="compact">
|
||||
|
||||
@ -3,7 +3,7 @@ import test from "node:test";
|
||||
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CountryCode,
|
||||
CurrencyCode,
|
||||
EmailAddress,
|
||||
LanguageCode,
|
||||
@ -44,7 +44,7 @@ const buildCustomer = () =>
|
||||
city: Maybe.some(unwrap(City.create("Madrid"))),
|
||||
province: Maybe.some(unwrap(Province.create("Madrid"))),
|
||||
postalCode: Maybe.some(unwrap(PostalCode.create("28001"))),
|
||||
country: Maybe.some(unwrap(Country.create("ES"))),
|
||||
countryCode: Maybe.some(unwrap(CountryCode.create("ES"))),
|
||||
},
|
||||
emailPrimary: Maybe.some(unwrap(EmailAddress.create("primary@example.test"))),
|
||||
emailSecondary: Maybe.none(),
|
||||
|
||||
@ -64,7 +64,7 @@ export const toCustomerAuditState = (customer: Customer): CustomerAuditState =>
|
||||
city: toNullablePrimitive(customer.address.getProps().city),
|
||||
province: toNullablePrimitive(customer.address.getProps().province),
|
||||
postalCode: toNullablePrimitive(customer.address.getProps().postalCode),
|
||||
country: toNullablePrimitive(customer.address.getProps().country),
|
||||
country: toNullablePrimitive(customer.address.getProps().countryCode),
|
||||
},
|
||||
emailPrimary: toNullablePrimitive(customer.emailPrimary),
|
||||
emailSecondary: toNullablePrimitive(customer.emailSecondary),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CountryCode,
|
||||
CurrencyCode,
|
||||
DomainError,
|
||||
EmailAddress,
|
||||
@ -97,7 +97,7 @@ export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
||||
);
|
||||
|
||||
const country = extractOrPushError(
|
||||
maybeFromNullableResult(dto.address.country, (value) => Country.create(value)),
|
||||
maybeFromNullableResult(dto.address.countryCode, (value) => CountryCode.create(value)),
|
||||
"country",
|
||||
errors
|
||||
);
|
||||
@ -203,7 +203,7 @@ export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
|
||||
city: city!,
|
||||
postalCode: postalCode!,
|
||||
province: province!,
|
||||
country: country!,
|
||||
countryCode: country!,
|
||||
};
|
||||
|
||||
const customerProps: ICustomerCreateProps = {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CountryCode,
|
||||
CurrencyCode,
|
||||
DomainError,
|
||||
EmailAddress,
|
||||
@ -219,9 +219,9 @@ export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper {
|
||||
});
|
||||
|
||||
toPatchField(dto.country).ifSetOrNull((country) => {
|
||||
postalAddressPatchProps.country = extractOrPushError(
|
||||
maybeFromNullableResult(country, (value) => Country.create(value)),
|
||||
"address.country",
|
||||
postalAddressPatchProps.countryCode = extractOrPushError(
|
||||
maybeFromNullableResult(country, (value) => CountryCode.create(value)),
|
||||
"address.countryCode",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
@ -31,7 +31,7 @@ export class CustomerFullSnapshotBuilder implements ICustomerFullSnapshotBuilder
|
||||
city: toNullable(address.city, (value) => value.toPrimitive()),
|
||||
province: toNullable(address.province, (value) => value.toPrimitive()),
|
||||
postal_code: toNullable(address.postalCode, (value) => value.toPrimitive()),
|
||||
country: toNullable(address.country, (value) => value.toPrimitive()),
|
||||
country: toNullable(address.countryCode, (value) => value.toPrimitive()),
|
||||
},
|
||||
|
||||
contact: {
|
||||
|
||||
@ -27,7 +27,7 @@ export class CustomerSummarySnapshotBuilder implements ICustomerSummarySnapshotB
|
||||
city: toNullable(address.city, (value) => value.toString()),
|
||||
postal_code: toNullable(address.postalCode, (value) => value.toString()),
|
||||
province: toNullable(address.province, (value) => value.toString()),
|
||||
country: toNullable(address.country, (value) => value.toString()),
|
||||
country: toNullable(address.countryCode, (value) => value.toString()),
|
||||
},
|
||||
|
||||
contact: {
|
||||
|
||||
@ -3,7 +3,7 @@ import test from "node:test";
|
||||
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
import {
|
||||
Country,
|
||||
CountryCode,
|
||||
CurrencyCode,
|
||||
LanguageCode,
|
||||
Name,
|
||||
@ -26,7 +26,7 @@ const buildCustomer = (paymentTermId?: UniqueID) => {
|
||||
const companyId = UniqueID.generateNewID();
|
||||
const customerId = UniqueID.generateNewID();
|
||||
const nameResult = Name.create("Acme");
|
||||
const countryResult = Country.create("ES");
|
||||
const countryResult = CountryCode.create("ES");
|
||||
const languageCodeResult = LanguageCode.create("es");
|
||||
const currencyCodeResult = CurrencyCode.create("EUR");
|
||||
|
||||
@ -50,7 +50,7 @@ const buildCustomer = (paymentTermId?: UniqueID) => {
|
||||
city: Maybe.none(),
|
||||
province: Maybe.none(),
|
||||
postalCode: Maybe.none(),
|
||||
country: Maybe.some(countryResult.data),
|
||||
countryCode: Maybe.some(countryResult.data),
|
||||
},
|
||||
emailPrimary: Maybe.none(),
|
||||
emailSecondary: Maybe.none(),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CountryCode,
|
||||
CurrencyCode,
|
||||
EmailAddress,
|
||||
LanguageCode,
|
||||
@ -94,9 +94,9 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
|
||||
errors
|
||||
);
|
||||
|
||||
const country = extractOrPushError(
|
||||
maybeFromNullableResult(source.country, (value) => Country.create(value)),
|
||||
"country",
|
||||
const countryCode = extractOrPushError(
|
||||
maybeFromNullableResult(source.country_code, (value) => CountryCode.create(value)),
|
||||
"country_code",
|
||||
errors
|
||||
);
|
||||
|
||||
@ -197,7 +197,7 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
|
||||
city: city!,
|
||||
postalCode: postalCode!,
|
||||
province: province!,
|
||||
country: country!,
|
||||
countryCode: countryCode!,
|
||||
};
|
||||
|
||||
const postalAddress = extractOrPushError(
|
||||
@ -294,7 +294,7 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
|
||||
postal_code: maybeToNullable(source.address.postalCode, (postalCode) =>
|
||||
postalCode.toPrimitive()
|
||||
),
|
||||
country: maybeToNullable(source.address.country, (country) => country.toPrimitive()),
|
||||
country: maybeToNullable(source.address.countryCode, (country) => country.toPrimitive()),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api";
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CountryCode,
|
||||
CurrencyCode,
|
||||
EmailAddress,
|
||||
LanguageCode,
|
||||
@ -92,9 +92,9 @@ export class SequelizeCustomerSummaryMapper extends SequelizeQueryMapper<
|
||||
errors
|
||||
);
|
||||
|
||||
const country = extractOrPushError(
|
||||
maybeFromNullableResult(raw.country, (value) => Country.create(value)),
|
||||
"country",
|
||||
const countryCode = extractOrPushError(
|
||||
maybeFromNullableResult(raw.country_code, (value) => CountryCode.create(value)),
|
||||
"country_code",
|
||||
errors
|
||||
);
|
||||
|
||||
@ -171,7 +171,7 @@ export class SequelizeCustomerSummaryMapper extends SequelizeQueryMapper<
|
||||
city: city!,
|
||||
postalCode: postalCode!,
|
||||
province: province!,
|
||||
country: country!,
|
||||
countryCode: countryCode!,
|
||||
};
|
||||
|
||||
const postalAddress = extractOrPushError(
|
||||
|
||||
@ -32,7 +32,7 @@ export class CustomerModel extends Model<
|
||||
declare city: CreationOptional<string | null>;
|
||||
declare province: CreationOptional<string | null>;
|
||||
declare postal_code: CreationOptional<string | null>;
|
||||
declare country: CreationOptional<string | null>;
|
||||
declare country_code: CreationOptional<string | null>;
|
||||
|
||||
// Correos electrónicos
|
||||
declare email_primary: CreationOptional<string | null>;
|
||||
@ -137,8 +137,8 @@ export default (database: Sequelize) => {
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
country: {
|
||||
type: DataTypes.STRING,
|
||||
country_code: {
|
||||
type: DataTypes.STRING(2),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
@ -33,7 +33,7 @@ export const CustomerUpdateEditorForm = ({
|
||||
}: CustomerUpdateEditorFormProps) => {
|
||||
return (
|
||||
<form
|
||||
className={cn("flex min-w-0 flex-col gap-6", className)}
|
||||
className={cn("space-y-6", className)}
|
||||
id={formId}
|
||||
noValidate
|
||||
onKeyDown={preventEnterKeySubmitForm}
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import { ErrorAlert, PageErrorState } from "@erp/core/components";
|
||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||
import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
RdxEditorLayout,
|
||||
RdxPageContainer,
|
||||
RdxPageFrame,
|
||||
RdxPageStack,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { FormProvider } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
@ -66,6 +71,8 @@ export const CustomerUpdatePage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const hasPageAlerts = updateCtrl.isUpdateError;
|
||||
|
||||
return (
|
||||
<FormProvider {...updateCtrl.form}>
|
||||
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
||||
@ -96,6 +103,28 @@ export const CustomerUpdatePage = () => {
|
||||
}
|
||||
>
|
||||
<RdxPageContainer width="content">
|
||||
{hasPageAlerts ? (
|
||||
<RdxPageContainer className="pb-0" width="full">
|
||||
<RdxPageStack spacing="compact">
|
||||
{updateCtrl.isUpdateError && (
|
||||
<ErrorAlert
|
||||
message={
|
||||
(updateCtrl.updateError as Error)?.message ??
|
||||
t(
|
||||
"pages.customers.update.error_msg",
|
||||
"Revisa los datos e inténtalo de nuevo."
|
||||
)
|
||||
}
|
||||
title={t(
|
||||
"pages.customers.update.error_title",
|
||||
"No se pudo guardar los cambios"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</RdxPageStack>
|
||||
</RdxPageContainer>
|
||||
) : null}
|
||||
|
||||
<RdxEditorLayout>
|
||||
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
||||
<div className="flex min-w-0 flex-col gap-6">
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
} from "@erp/customer-invoices/api/domain";
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CountryCode,
|
||||
CurrencyCode,
|
||||
DomainError,
|
||||
EmailAddress,
|
||||
@ -396,7 +396,7 @@ export class CreateProformaFromFactugesInputMapper
|
||||
);
|
||||
|
||||
const country = extractOrPushError(
|
||||
maybeFromNullableResult(customer.country, (value) => Country.create(value)),
|
||||
maybeFromNullableResult(customer.country, (value) => CountryCode.create(value)),
|
||||
"country",
|
||||
errors
|
||||
);
|
||||
@ -457,7 +457,7 @@ export class CreateProformaFromFactugesInputMapper
|
||||
city: city!,
|
||||
postalCode: postalCode!,
|
||||
province: province!,
|
||||
country: country!,
|
||||
countryCode: country!,
|
||||
};
|
||||
|
||||
const customerProps: ProformaCustomerDraft = {
|
||||
|
||||
@ -61,7 +61,7 @@ export const toSupplierAuditState = (supplier: Supplier): SupplierAuditState =>
|
||||
city: toNullablePrimitive(supplier.address.getProps().city),
|
||||
province: toNullablePrimitive(supplier.address.getProps().province),
|
||||
postalCode: toNullablePrimitive(supplier.address.getProps().postalCode),
|
||||
country: toNullablePrimitive(supplier.address.getProps().country),
|
||||
country: toNullablePrimitive(supplier.address.getProps().countryCode),
|
||||
},
|
||||
emailPrimary: toNullablePrimitive(supplier.emailPrimary),
|
||||
emailSecondary: toNullablePrimitive(supplier.emailSecondary),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CountryCode,
|
||||
CurrencyCode,
|
||||
DomainError,
|
||||
EmailAddress,
|
||||
@ -96,7 +96,7 @@ export class CreateSupplierInputMapper implements ICreateSupplierInputMapper {
|
||||
);
|
||||
|
||||
const country = extractOrPushError(
|
||||
maybeFromNullableResult(command.country, (value) => Country.create(value)),
|
||||
maybeFromNullableResult(command.country, (value) => CountryCode.create(value)),
|
||||
"country",
|
||||
errors
|
||||
);
|
||||
@ -171,7 +171,7 @@ export class CreateSupplierInputMapper implements ICreateSupplierInputMapper {
|
||||
city: city!,
|
||||
postalCode: postalCode!,
|
||||
province: province!,
|
||||
country: country!,
|
||||
countryCode: country!,
|
||||
};
|
||||
|
||||
const supplierProps: ISupplierCreateProps = {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CountryCode,
|
||||
CurrencyCode,
|
||||
DomainError,
|
||||
EmailAddress,
|
||||
@ -240,8 +240,8 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
||||
});
|
||||
|
||||
toPatchField(dto.country).ifSet((country) => {
|
||||
postalAddressPatchProps.country = extractOrPushError(
|
||||
maybeFromNullableResult(country, (value) => Country.create(value)),
|
||||
postalAddressPatchProps.countryCode = extractOrPushError(
|
||||
maybeFromNullableResult(country, (value) => CountryCode.create(value)),
|
||||
"country",
|
||||
errors
|
||||
);
|
||||
|
||||
@ -4,7 +4,7 @@ import test from "node:test";
|
||||
import type { IAudit, RecordAuditMutationShortcutInput } from "@repo/rdx-audit";
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CountryCode,
|
||||
CurrencyCode,
|
||||
EmailAddress,
|
||||
LanguageCode,
|
||||
@ -47,7 +47,7 @@ const buildSupplier = (status = SupplierStatus.active()) =>
|
||||
city: Maybe.some(unwrap(City.create("Madrid"))),
|
||||
province: Maybe.some(unwrap(Province.create("Madrid"))),
|
||||
postalCode: Maybe.some(unwrap(PostalCode.create("28001"))),
|
||||
country: Maybe.some(unwrap(Country.create("ES"))),
|
||||
countryCode: Maybe.some(unwrap(CountryCode.create("ES"))),
|
||||
},
|
||||
emailPrimary: Maybe.some(unwrap(EmailAddress.create("supplier@example.test"))),
|
||||
emailSecondary: Maybe.none(),
|
||||
|
||||
@ -25,7 +25,7 @@ test("el snapshot de detalle satisface el contrato de respuesta", () => {
|
||||
city: Maybe.none(),
|
||||
province: Maybe.none(),
|
||||
postalCode: Maybe.none(),
|
||||
country: Maybe.none(),
|
||||
countryCode: Maybe.none(),
|
||||
}),
|
||||
},
|
||||
emailPrimary: Maybe.none(),
|
||||
|
||||
@ -27,7 +27,7 @@ export class SupplierFullSnapshotBuilder implements ISupplierFullSnapshotBuilder
|
||||
city: maybeToEmptyString(address.city, (value) => value.toPrimitive()),
|
||||
province: maybeToEmptyString(address.province, (value) => value.toPrimitive()),
|
||||
postal_code: maybeToEmptyString(address.postalCode, (value) => value.toPrimitive()),
|
||||
country: maybeToEmptyString(address.country, (value) => value.toPrimitive()),
|
||||
country: maybeToEmptyString(address.countryCode, (value) => value.toPrimitive()),
|
||||
|
||||
email_primary: maybeToEmptyString(supplier.emailPrimary, (value) => value.toPrimitive()),
|
||||
email_secondary: maybeToEmptyString(supplier.emailSecondary, (value) => value.toPrimitive()),
|
||||
|
||||
@ -27,7 +27,7 @@ export class SupplierSummarySnapshotBuilder implements ISupplierSummarySnapshotB
|
||||
city: maybeToEmptyString(address.city, (value) => value.toString()),
|
||||
postal_code: maybeToEmptyString(address.postalCode, (value) => value.toString()),
|
||||
province: maybeToEmptyString(address.province, (value) => value.toString()),
|
||||
country: maybeToEmptyString(address.country, (value) => value.toString()),
|
||||
country: maybeToEmptyString(address.countryCode, (value) => value.toString()),
|
||||
|
||||
email_primary: maybeToEmptyString(supplier.emailPrimary, (value) => value.toString()),
|
||||
email_secondary: maybeToEmptyString(supplier.emailSecondary, (value) => value.toString()),
|
||||
|
||||
@ -43,7 +43,7 @@ const buildSupplier = (companyId: UniqueID) => {
|
||||
city: Maybe.none(),
|
||||
province: Maybe.none(),
|
||||
postalCode: Maybe.none(),
|
||||
country: Maybe.none(),
|
||||
countryCode: Maybe.none(),
|
||||
});
|
||||
const languageCode = LanguageCode.create("es");
|
||||
const currencyCode = CurrencyCode.create("EUR");
|
||||
|
||||
@ -28,7 +28,7 @@ test("crea Supplier activo por defecto y actualiza su dirección", () => {
|
||||
city: Maybe.none(),
|
||||
postalCode: Maybe.none(),
|
||||
province: Maybe.none(),
|
||||
country: Maybe.none(),
|
||||
countryCode: Maybe.none(),
|
||||
},
|
||||
emailPrimary: Maybe.none(),
|
||||
emailSecondary: Maybe.none(),
|
||||
@ -70,7 +70,7 @@ test("activa y desactiva Supplier sin persistir transiciones repetidas", () => {
|
||||
city: Maybe.none(),
|
||||
postalCode: Maybe.none(),
|
||||
province: Maybe.none(),
|
||||
country: Maybe.none(),
|
||||
countryCode: Maybe.none(),
|
||||
},
|
||||
emailPrimary: Maybe.none(),
|
||||
emailSecondary: Maybe.none(),
|
||||
|
||||
@ -45,7 +45,7 @@ test("rehidrata identidad, estado, booleanos y opcionales desde persistencia", (
|
||||
assert.equal(supplier.reference.isNone(), true);
|
||||
assert.equal(supplier.tradeName.isNone(), true);
|
||||
assert.equal(supplier.mobileSecondary.isNone(), true);
|
||||
assert.equal(supplier.address.country.isNone(), true);
|
||||
assert.equal(supplier.address.countryCode.isNone(), true);
|
||||
});
|
||||
|
||||
test("el round-trip conserva los campos persistibles relevantes", () => {
|
||||
@ -62,7 +62,7 @@ test("el round-trip conserva los campos persistibles relevantes", () => {
|
||||
assert.equal(result.data.status, "inactive");
|
||||
assert.equal(result.data.is_company, false);
|
||||
assert.equal(result.data.mobile_secondary, null);
|
||||
assert.equal(result.data.country, null);
|
||||
assert.equal(result.data.country_code, null);
|
||||
});
|
||||
|
||||
test("rehidrata status activo e is_company true sin conversiones legacy", () => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { SequelizeDomainMapper } from "@erp/core/api";
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CountryCode,
|
||||
CurrencyCode,
|
||||
EmailAddress,
|
||||
LanguageCode,
|
||||
@ -23,10 +23,10 @@ import {
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type SupplierInternalProps,
|
||||
Supplier,
|
||||
SupplierStatus,
|
||||
SUPPLIER_STATUS,
|
||||
Supplier,
|
||||
type SupplierInternalProps,
|
||||
SupplierStatus,
|
||||
} from "../../../../../domain";
|
||||
import type { SupplierCreationAttributes, SupplierModel } from "../../../sequelize";
|
||||
|
||||
@ -108,8 +108,8 @@ export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
|
||||
);
|
||||
|
||||
const country = extractOrPushError(
|
||||
maybeFromNullableResult(source.country, (value) => Country.create(value)),
|
||||
"country",
|
||||
maybeFromNullableResult(source.country_code, (value) => CountryCode.create(value)),
|
||||
"country_code",
|
||||
errors
|
||||
);
|
||||
|
||||
@ -180,7 +180,7 @@ export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
|
||||
city: city!,
|
||||
postalCode: postalCode!,
|
||||
province: province!,
|
||||
country: country!,
|
||||
countryCode: country!,
|
||||
};
|
||||
|
||||
const postalAddress = extractOrPushError(
|
||||
@ -259,7 +259,7 @@ export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
|
||||
postal_code: maybeToNullable(source.address.postalCode, (postalCode) =>
|
||||
postalCode.toPrimitive()
|
||||
),
|
||||
country: maybeToNullable(source.address.country, (country) => country.toPrimitive()),
|
||||
country: maybeToNullable(source.address.countryCode, (country) => country.toPrimitive()),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { SequelizeQueryMapper } from "@erp/core/api";
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CountryCode,
|
||||
CurrencyCode,
|
||||
EmailAddress,
|
||||
LanguageCode,
|
||||
@ -22,7 +22,7 @@ import {
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { SupplierSummary } from "../../../../../application";
|
||||
import { SupplierStatus, SUPPLIER_STATUS } from "../../../../../domain";
|
||||
import { SUPPLIER_STATUS, SupplierStatus } from "../../../../../domain";
|
||||
import type { SupplierModel } from "../../models";
|
||||
|
||||
const PERSISTED_STATUS_TO_DOMAIN: Readonly<Record<string, SUPPLIER_STATUS>> = {
|
||||
@ -98,8 +98,8 @@ export class SequelizeSupplierSummaryMapper extends SequelizeQueryMapper<
|
||||
);
|
||||
|
||||
const country = extractOrPushError(
|
||||
maybeFromNullableResult(raw.country, (value) => Country.create(value)),
|
||||
"country",
|
||||
maybeFromNullableResult(raw.country_code, (value) => CountryCode.create(value)),
|
||||
"country_code",
|
||||
errors
|
||||
);
|
||||
|
||||
@ -170,7 +170,7 @@ export class SequelizeSupplierSummaryMapper extends SequelizeQueryMapper<
|
||||
city: city!,
|
||||
postalCode: postalCode!,
|
||||
province: province!,
|
||||
country: country!,
|
||||
countryCode: country!,
|
||||
};
|
||||
|
||||
const postalAddress = extractOrPushError(
|
||||
|
||||
@ -32,7 +32,7 @@ export class SupplierModel extends Model<
|
||||
declare city: CreationOptional<string | null>;
|
||||
declare province: CreationOptional<string | null>;
|
||||
declare postal_code: CreationOptional<string | null>;
|
||||
declare country: CreationOptional<string | null>;
|
||||
declare country_code: CreationOptional<string | null>;
|
||||
|
||||
// Correos electrónicos
|
||||
declare email_primary: CreationOptional<string | null>;
|
||||
@ -125,8 +125,8 @@ export default (database: Sequelize) => {
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
country: {
|
||||
type: DataTypes.STRING,
|
||||
country_code: {
|
||||
type: DataTypes.STRING(2),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
@ -1,43 +1,37 @@
|
||||
import { Maybe } from "@repo/rdx-utils";
|
||||
|
||||
import { City } from "../city";
|
||||
import { CountryCode } from "../country-code";
|
||||
import { PostalAddress } from "../postal-address";
|
||||
import { PostalCode } from "../postal-code";
|
||||
import { Province } from "../province";
|
||||
import { Street } from "../street";
|
||||
|
||||
describe("PostalAddress Value Object", () => {
|
||||
const validAddress = {
|
||||
street: "123 Main St",
|
||||
city: "Springfield",
|
||||
postalCode: "12345",
|
||||
state: "IL",
|
||||
country: "USA",
|
||||
};
|
||||
const unwrap = <T>(result: { data: T }): T => result.data;
|
||||
|
||||
test("✅ Debería crear un PostalAddress con valores válidos", () => {
|
||||
const result = PostalAddress.create(validAddress);
|
||||
describe("PostalAddress", () => {
|
||||
const address = () =>
|
||||
PostalAddress.create({
|
||||
street: Maybe.some(unwrap(Street.create("Main street 1"))),
|
||||
street2: Maybe.none(),
|
||||
city: Maybe.some(unwrap(City.create("Madrid"))),
|
||||
postalCode: Maybe.some(unwrap(PostalCode.create("28001"))),
|
||||
province: Maybe.some(unwrap(Province.create("Madrid"))),
|
||||
countryCode: Maybe.some(unwrap(CountryCode.create("es"))),
|
||||
});
|
||||
|
||||
test("uses CountryCode and serializes its normalized primitive", () => {
|
||||
const result = address();
|
||||
|
||||
expect(result.isSuccess).toBe(true);
|
||||
expect(result.data).toBeInstanceOf(PostalAddress);
|
||||
});
|
||||
|
||||
test("❌ Debería fallar al crear un PostalAddress con código postal inválido", () => {
|
||||
const invalidAddress = { ...validAddress, postalCode: "abc" }; // Código postal inválido
|
||||
const result = PostalAddress.create(invalidAddress);
|
||||
|
||||
expect(result.isFailure).toBe(true);
|
||||
expect(result.error?.message).toBe("Invalid postal code format");
|
||||
});
|
||||
|
||||
test("✅ Métodos getters deberían devolver valores esperados", () => {
|
||||
const address = PostalAddress.create(validAddress).data;
|
||||
|
||||
expect(address.street).toBe(validAddress.street);
|
||||
expect(address.city).toBe(validAddress.city);
|
||||
expect(address.postalCode).toBe(validAddress.postalCode);
|
||||
expect(address.state).toBe(validAddress.state);
|
||||
expect(address.country).toBe(validAddress.country);
|
||||
});
|
||||
|
||||
test("✅ `toString()` debería devolver la representación correcta", () => {
|
||||
const address = PostalAddress.create(validAddress).data;
|
||||
const expectedString = `${validAddress.street}, ${validAddress.city}, ${validAddress.postalCode}, ${validAddress.state}, ${validAddress.country}`;
|
||||
|
||||
expect(address.toString()).toBe(expectedString);
|
||||
expect(result.data.countryCode.unwrap().toPrimitive()).toBe("ES");
|
||||
expect(result.data.toString()).toEqual({
|
||||
street: "Main street 1",
|
||||
street2: "",
|
||||
city: "Madrid",
|
||||
postal_code: "28001",
|
||||
province: "Madrid",
|
||||
country_code: "ES",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,7 +3,7 @@ import { type Maybe, Result } from "@repo/rdx-utils";
|
||||
import { maybeToEmptyString } from "../helpers";
|
||||
|
||||
import type { City } from "./city";
|
||||
import type { Country } from "./country";
|
||||
import type { CountryCode } from "./country-code";
|
||||
import type { PostalCode } from "./postal-code";
|
||||
import type { Province } from "./province";
|
||||
import type { Street } from "./street";
|
||||
@ -15,7 +15,7 @@ export interface PostalAddressProps {
|
||||
city: Maybe<City>;
|
||||
postalCode: Maybe<PostalCode>;
|
||||
province: Maybe<Province>;
|
||||
country: Maybe<Country>;
|
||||
countryCode: Maybe<CountryCode>;
|
||||
}
|
||||
|
||||
export type PostalAddressPatchProps = Partial<PostalAddressProps>;
|
||||
@ -27,48 +27,32 @@ export class PostalAddress extends ValueObject<PostalAddressProps> {
|
||||
|
||||
static create(values: PostalAddressProps): Result<PostalAddress, Error> {
|
||||
const valueIsValid = PostalAddress.validate(values);
|
||||
|
||||
if (valueIsValid.isFailure) {
|
||||
return Result.fail(valueIsValid.error);
|
||||
}
|
||||
if (valueIsValid.isFailure) return Result.fail(valueIsValid.error);
|
||||
return Result.ok(new PostalAddress(values));
|
||||
}
|
||||
|
||||
get street(): Maybe<Street> {
|
||||
return this.props.street;
|
||||
}
|
||||
|
||||
get street2(): Maybe<Street> {
|
||||
return this.props.street2;
|
||||
}
|
||||
|
||||
get city(): Maybe<City> {
|
||||
return this.props.city;
|
||||
}
|
||||
|
||||
get postalCode(): Maybe<PostalCode> {
|
||||
return this.props.postalCode;
|
||||
}
|
||||
|
||||
get province(): Maybe<Province> {
|
||||
return this.props.province;
|
||||
}
|
||||
|
||||
get country(): Maybe<Country> {
|
||||
return this.props.country;
|
||||
get countryCode(): Maybe<CountryCode> {
|
||||
return this.props.countryCode;
|
||||
}
|
||||
|
||||
getProps(): PostalAddressProps {
|
||||
return {
|
||||
street: this.street,
|
||||
street2: this.street2,
|
||||
city: this.city,
|
||||
postalCode: this.postalCode,
|
||||
province: this.province,
|
||||
country: this.country,
|
||||
};
|
||||
return { ...this.props };
|
||||
}
|
||||
|
||||
toPrimitive() {
|
||||
return this.getProps();
|
||||
}
|
||||
@ -80,11 +64,18 @@ export class PostalAddress extends ValueObject<PostalAddressProps> {
|
||||
city: maybeToEmptyString(this.city, (value) => value.toString()),
|
||||
postal_code: maybeToEmptyString(this.postalCode, (value) => value.toString()),
|
||||
province: maybeToEmptyString(this.province, (value) => value.toString()),
|
||||
country: maybeToEmptyString(this.country, (value) => value.toString()),
|
||||
country_code: maybeToEmptyString(this.countryCode, (value) => value.toString()),
|
||||
};
|
||||
}
|
||||
|
||||
toFormat(): string {
|
||||
return `${this.props.street}, ${this.props.street2}, ${this.props.city}, ${this.props.postalCode}, ${this.props.province}, ${this.props.country}`;
|
||||
return [this.street, this.street2, this.city, this.postalCode, this.province, this.countryCode]
|
||||
.map((value) =>
|
||||
value.match(
|
||||
(item) => item.toString(),
|
||||
() => ""
|
||||
)
|
||||
)
|
||||
.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user