Muchos cambios
This commit is contained in:
parent
1ce77814b5
commit
53eb33376c
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@ -7,6 +7,7 @@
|
|||||||
"formulahendry.auto-rename-tag",
|
"formulahendry.auto-rename-tag",
|
||||||
"cweijan.dbclient-jdbc",
|
"cweijan.dbclient-jdbc",
|
||||||
"pkief.material-icon-theme",
|
"pkief.material-icon-theme",
|
||||||
"fralle.copy-code-context"
|
"fralle.copy-code-context",
|
||||||
|
"imgildev.vscode-auto-barrel"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,8 +27,8 @@ export const ICreateAccountRequestSchema = z.object({
|
|||||||
|
|
||||||
default_tax: z.number(),
|
default_tax: z.number(),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
language_code: z.string(),
|
language_code: LanguageCodeSchema,
|
||||||
currency_code: z.string(),
|
currency_code: CurrencyCodeSchema,
|
||||||
logo: z.string(),
|
logo: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -55,8 +55,8 @@ export const IUpdateAccountRequestSchema = z.object({
|
|||||||
|
|
||||||
default_tax: z.number(),
|
default_tax: z.number(),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
language_code: z.string(),
|
language_code: LanguageCodeSchema,
|
||||||
currency_code: z.string(),
|
currency_code: CurrencyCodeSchema,
|
||||||
logo: z.string(),
|
logo: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,150 +0,0 @@
|
|||||||
# Guía de estilo — DTOs y organización de carpetas
|
|
||||||
|
|
||||||
> **Objetivo:** asegurar que **todos** los equipos usen un vocabulario y una estructura de ficheros idéntica al modelar, versionar y serializar los *Data Transfer Objects* (DTO).
|
|
||||||
> Esta guía aplica a cualquier API HTTP/JSON implementada en Express + TypeScript que siga DDD, SOLID y CQRS.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Estructura de carpetas obligatoria
|
|
||||||
|
|
||||||
```text
|
|
||||||
src/
|
|
||||||
└─ <bounded-context>/ (ej. billing/)
|
|
||||||
└─ api/
|
|
||||||
└─ dto/
|
|
||||||
├─ common/ ← Tipos reutilizables (MoneyDTO, AddressDTO…)
|
|
||||||
├─ request/ ← Sólo comandos y queries
|
|
||||||
│ ├─ *.command.dto.ts
|
|
||||||
│ └─ *.query.dto.ts
|
|
||||||
│
|
|
||||||
└─ response/ ← Sólo resultados/vistas
|
|
||||||
├─ *.result.dto.ts
|
|
||||||
└─ *.view.dto.ts
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
*Alias TS recomendado*: `@<context>/dto/*` → `src/<context>/api/dto/*`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Convención de nombres
|
|
||||||
|
|
||||||
| Categoría | Sufijo **obligatorio** | Descripción & ejemplos |
|
|
||||||
|-----------|------------------------|------------------------|
|
|
||||||
| **Comandos** (mutaciones) | `…CommandDTO` | `CreateInvoiceCommandDTO`, `UpdateInvoiceCommandDTO`, `DeleteInvoiceCommandDTO`, `ChangeInvoiceStatusCommandDTO` |
|
|
||||||
| **Queries** (lecturas con filtros) | `…QueryDTO` | `ListInvoicesQueryDTO`, `GetInvoiceByIdQueryDTO` |
|
|
||||||
| **Resultados** (respuesta de comandos) | `…ResultDTO` | `InvoiceCreationResultDTO`, `InvoiceDeletionResultDTO` |
|
|
||||||
| **Vistas** (respuesta de queries) | `…ViewDTO` | `InvoiceViewDTO`, `InvoiceSummaryViewDTO` |
|
|
||||||
| **Tipos comunes** | `…DTO` | `MoneyDTO`, `PaginationMetaDTO`, `AddressDTO` |
|
|
||||||
|
|
||||||
*Regla de oro:* **No existe ningún DTO sin sufijo, salvo los tipos comunes dentro de `common/`.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Reglas de contenido
|
|
||||||
|
|
||||||
1. **Solo datos planos**: números, cadenas, literales, arrays; nada de lógica.
|
|
||||||
2. **Fechas** en ISO-8601 UTC (`yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`).
|
|
||||||
3. **Enums** expuestos como _string literal_ en `snake_case` o `UPPER_SNAKE_CASE`; evita números mágicos.
|
|
||||||
4. **Moneda**
|
|
||||||
- **Siempre** con la estructura común:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export interface MoneyDTO {
|
|
||||||
amount: number | null; // unidades mínimas (ej. céntimos)
|
|
||||||
scale: number; // nº de decimales (2 = céntimos)
|
|
||||||
currency_code: string; // ISO-4217 (“EUR”)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- Se importa desde `dto/common/money.dto.ts`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Guía de mapeo
|
|
||||||
|
|
||||||
| Dirección | Componente responsable | Ubicación |
|
|
||||||
|-----------|------------------------|-----------|
|
|
||||||
| **DTO → Dominio** | `…CommandMapper` / `…QueryMapper` | `src/<context>/application/mappers/` |
|
|
||||||
| **Dominio → DTO** | `…ResultMapper` / `…ViewMapper` | mismo directorio |
|
|
||||||
|
|
||||||
Cada mapper implementa **una** función pública:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
interface InvoiceCreationResultMapper {
|
|
||||||
toResult(entity: Invoice): InvoiceCreationResultDTO;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Validación y versiones
|
|
||||||
|
|
||||||
1. **Validación de entrada**
|
|
||||||
- Usa `class-validator` o Zod en el *controller*; nunca en el dominio.
|
|
||||||
- Convierte a Value Objects una vez que el DTO pasó la validación.
|
|
||||||
|
|
||||||
2. **Versionado**
|
|
||||||
- Añade sufijos de versión en el **archivo**, no en el nombre de la interfaz:
|
|
||||||
`invoice.view.v2.dto.ts` → `export interface InvoiceViewV2DTO { … }`
|
|
||||||
- Mantén las versiones anteriores durante **≥1 release** o hasta que los consumidores migren.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Ejemplo completo (creación de factura)
|
|
||||||
|
|
||||||
```text
|
|
||||||
billing/
|
|
||||||
├─ api/
|
|
||||||
│ └─ dto/
|
|
||||||
│ ├─ input/create-invoice.command.dto.ts
|
|
||||||
│ ├─ output/invoice-creation.result.dto.ts
|
|
||||||
│ └─ common/money.dto.ts
|
|
||||||
└─ application/
|
|
||||||
└─ mappers/
|
|
||||||
└─ invoice-creation.result.mapper.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// create-invoice.command.dto.ts
|
|
||||||
export interface CreateInvoiceCommandDTO {
|
|
||||||
customerId: string;
|
|
||||||
issueDate: string;
|
|
||||||
lines: ReadonlyArray<{
|
|
||||||
description: string;
|
|
||||||
quantity: number;
|
|
||||||
unitPrice: MoneyDTO;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// invoice-creation.result.dto.ts
|
|
||||||
export interface InvoiceCreationResultDTO {
|
|
||||||
invoiceId: string;
|
|
||||||
number: string;
|
|
||||||
totalAmount: MoneyDTO;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Checklist antes de hacer *merge*
|
|
||||||
|
|
||||||
- [ ] Archivo ubicado en la carpeta correcta.
|
|
||||||
- [ ] Sufijo conforme (**CommandDTO**, **QueryDTO**, **ResultDTO**, **ViewDTO**).
|
|
||||||
- [ ] Todos los importes usan `MoneyDTO`.
|
|
||||||
- [ ] Tipos opcionales marcados con `?` y comentados.
|
|
||||||
- [ ] **Sin** lógica, constructores ni métodos.
|
|
||||||
- [ ] PR incluye al menos un test de mapper (input ⇄ dominio ⇄ output).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Tabla resumen
|
|
||||||
|
|
||||||
| Carpeta | Sufijo | Ejemplo clásico |
|
|
||||||
|---------|--------|-----------------|
|
|
||||||
| `dto/input/` | `CommandDTO` | `DeleteInvoiceCommandDTO` |
|
|
||||||
| `dto/input/` | `QueryDTO` | `ListInvoicesQueryDTO` |
|
|
||||||
| `dto/output/` | `ResultDTO` | `InvoiceDeletionResultDTO` |
|
|
||||||
| `dto/output/` | `ViewDTO` | `InvoiceSummaryViewDTO` |
|
|
||||||
| `dto/common/` | `DTO` | `MoneyDTO` |
|
|
||||||
@ -1,49 +1,20 @@
|
|||||||
## Guía de diseño de DTOs para el ERP
|
# Guía de diseño de DTOs para el ERP
|
||||||
|
|
||||||
---
|
## 1. Principios generales
|
||||||
|
|
||||||
* Un DTO debe representar **únicamente** el **contrato de transporte** entre procesos.
|
- Un DTO representa únicamente el contrato de transporte entre cliente y servidor.
|
||||||
* DTOs request/response separados por endpoint, aunque internamente reutilice piezas.
|
- Cada endpoint debe tener sus DTOs de `request` y `response`, aunque internamente reutilice subesquemas.
|
||||||
* No reutilizar el mismo DTO para varios endpoints salvo que el contrato sea realmente idéntico y estable.
|
- No reutilizar el mismo DTO entre endpoints salvo que el contrato sea realmente idéntico y estable.
|
||||||
* Sin `defaults` en requests de negocio. Deben evitarse salvo casos muy concretos y no ambiguos.
|
- Siempre que sea posible, definir DTOs a partir de esquemas Zod v4 e inferir el tipo desde el esquema.
|
||||||
* En la medida de lo posible, usar esquemas Zod v4 para luego inferir el tipo del DTO:
|
|
||||||
|
|
||||||
Ejemplo:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export type ListIssuedInvoicesRequestDTO = z.infer<typeof ListIssuedInvoicesRequestSchema>;
|
export type ListIssuedInvoicesRequestDTO = z.infer<typeof ListIssuedInvoicesRequestSchema>;
|
||||||
```
|
```
|
||||||
## 1. Esquemas Zod y DTOs
|
|
||||||
|
|
||||||
* **El esquema valida, no “arregla” entradas**. Evitar `.default()`, `.transform()` y similares salvo excepción muy justificada.
|
- El esquema valida; no debe “arreglar” entradas. Evitar `.default()`, `.transform()` y similares salvo excepción justificada.
|
||||||
* Las operaciones `list`, `get`, `create`, `update` y `report` no deben compartir el mismo esquema completo pero sí deben compartir `naming`, reglas de tipos, convenciones y reutilizar subesquemas comunes.
|
- No mezclar DTOs con lógica de negocio, mapping ni estado de formulario.
|
||||||
* No usar `z.string()` si se puede precisar más el tipo semántico. El schema debe expresar lo que **es** el dato, no solo cómo serializarlo.
|
|
||||||
|
|
||||||
Ejemplos:
|
|
||||||
- booleano -> `z.boolean()`
|
|
||||||
- UUID -> `z.uuid()`
|
|
||||||
- estado cerrado -> `z.enum([...])`
|
|
||||||
- fecha ISO -> schema específico
|
|
||||||
- código ISO -> schema específico o restricción mínima consistente
|
|
||||||
|
|
||||||
* Conservar el mismo tipo para el mismo campo en todos los esquemas. Si `customer_id` es UUID, debe ser UUID siempre.
|
|
||||||
* Conservar el mismo nombre para campos con la misma semántica a no ser que esté justificado.
|
|
||||||
|
|
||||||
Ejeemplo. No alternar:
|
|
||||||
- `recipient_id`
|
|
||||||
- `customer_id`
|
|
||||||
- `recipient_customer_id`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Estructura recomendada de DTOs
|
|
||||||
|
|
||||||
## 2.1. Organización por módulo
|
|
||||||
|
|
||||||
* Agrupar endpoints en `request/` y `response/`
|
|
||||||
* `shared/` para fragmentos reutilizables de shape
|
|
||||||
* no meter lógica de mapping en `dto/`
|
|
||||||
|
|
||||||
|
## 2. Organización y naming
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
common/dto/
|
common/dto/
|
||||||
@ -67,138 +38,62 @@ common/dto/
|
|||||||
recipient.dto.ts
|
recipient.dto.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
* Estructurar DTOs por módulo en `request/`, `response/` y `shared/`.
|
||||||
|
* `shared/` solo para subesquemas reutilizables y estables.
|
||||||
## 3. Convenciones de naming
|
* Nombres de fichero en `kebab-case`.
|
||||||
|
* Patrón de nombres:
|
||||||
## 3.1. Ficheros
|
|
||||||
|
|
||||||
* Siempre en `kebab-case`.
|
|
||||||
* Uso de singular/plural: colección -> plural (`proformas`), elemento -> singular (`proforma`)
|
|
||||||
* Patrón:
|
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
<action>-<resource>[...].request.dto.ts
|
<action>-<resource>[...].request.dto.ts
|
||||||
<action>-<resource>[...].response.dto.ts
|
<action>-<resource>[...].response.dto.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
Ejemplos:
|
* Usar singular/plural de forma consistente:
|
||||||
|
|
||||||
* `list-proformas.request.dto.ts`
|
* colección -> plural
|
||||||
* `get-proforma-by-id.response.dto.ts`
|
* elemento -> singular
|
||||||
* `update-proforma-by-id.request.dto.ts`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Reglas de tipos serializados
|
|
||||||
|
|
||||||
## 4.1. Cuándo usar string
|
|
||||||
|
|
||||||
Usar `string` cuando el dato de transporte sea realmente textual o cuando convenga preservar exactitud/neutralidad:
|
|
||||||
|
|
||||||
* ids externos textuales
|
|
||||||
* códigos
|
|
||||||
* fechas ISO serializadas
|
|
||||||
* importes
|
|
||||||
* cantidades
|
|
||||||
* porcentajes
|
|
||||||
* referencias documentales
|
|
||||||
* descripciones
|
|
||||||
* notas
|
|
||||||
|
|
||||||
## 4.2. Cuándo no usar string
|
|
||||||
|
|
||||||
No serializar como string si el tipo semántico natural debe conservarse y no hay ventaja en degradarlo.
|
|
||||||
|
|
||||||
Ejemplos:
|
Ejemplos:
|
||||||
|
|
||||||
* booleanos -> `z.boolean()`
|
```plaintext
|
||||||
* enteros de paginación -> `z.number().int()`
|
list-proformas.request.dto.ts
|
||||||
* arrays -> `z.array(...)`
|
get-proforma-by-id.response.dto.ts
|
||||||
* objetos -> `z.object(...)`
|
update-proforma-by-id.request.dto.ts
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Reglas para primitivas de negocio
|
|
||||||
|
|
||||||
* Un VO monetario/cuantitativo siempre debe tener una sola forma de transporte en todo el ERP.
|
|
||||||
|
|
||||||
## 5.1. Identificadores
|
|
||||||
|
|
||||||
* Si el identificador es UUID, usar `z.uuid()`.
|
|
||||||
* Si existe un id no UUID, definir un schema específico con nombre explícito.
|
|
||||||
* No mezclar `z.string()` y `z.uuid()` para el mismo campo según endpoint.
|
|
||||||
* Un mismo identificador debe tener el mismo schema en todos los DTOs.
|
|
||||||
|
|
||||||
Ejemplo:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
proforma_id: z.uuid()
|
|
||||||
customer_id: z.uuid()
|
|
||||||
company_id: z.uuid()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
* `<resource>-summary.dto.ts` para vistas de resumen reutilizables de list.
|
||||||
|
* `<resource>-detail.dto.ts` para vistas de detalle reutilizables de get.
|
||||||
|
* `<action>-<resource>.response.dto.ts` para la response completa del endpoint.
|
||||||
|
* `<resource>-summary.snapshot-builder.ts` para builders que materializan un item/resumen.
|
||||||
|
* Evitar nombres como `I<Resource>Snapshot` si representan el mismo contrato que un DTO ya definido por schema.
|
||||||
|
|
||||||
## 5.2. Fechas y timestamps
|
## 3. Reglas de tipado
|
||||||
|
|
||||||
Distinguir entre:
|
* No usar `z.string()` si el tipo semántico del campo puede concretarse mejor.
|
||||||
|
* El mismo campo debe conservar siempre el mismo nombre y el mismo tipo en todos los DTOs.
|
||||||
|
* Ejemplos de tipado semántico:
|
||||||
|
|
||||||
* fecha de calendario sin hora -> "2020-01-01"
|
* booleano -> `z.boolean()`
|
||||||
* timestamp con fecha y hora ->
|
* UUID -> `z.uuid()`
|
||||||
|
* conjunto cerrado -> `z.enum([...])`
|
||||||
|
* fecha/timestamp ISO -> schema específico
|
||||||
|
* arrays -> `z.array(...)`
|
||||||
|
* objetos -> `z.object(...)`
|
||||||
|
|
||||||
```ts
|
## 4. Reglas para primitivas de negocio
|
||||||
export const IsoDateSchema = z.iso.date(); // "2020-01-01"
|
|
||||||
export const IsoTimeSchema = z.iso.time(); // "03:15:00", "03:15"
|
|
||||||
export const OffsetDateTimeSchema = z.iso.datetime({ offset: true }); // "2020-01-01T06:15:00+02:00"
|
|
||||||
export const LocalDateTimeSchema = z.iso.datetime({ local: true }); // "2020-01-01T06:15:01"
|
|
||||||
export const DateTimeSchema1 = z.iso.datetime({ precision: -1 }); // minute precision (no seconds) -> "2020-01-01T06:15Z"
|
|
||||||
|
|
||||||
```
|
* Si un identificador es UUID, usar siempre `z.uuid()`.
|
||||||
|
* Si un identificador no es UUID, definir un schema específico con nombre explícito.
|
||||||
|
* Todo estado de negocio cerrado debe modelarse con `z.enum(...)`.
|
||||||
|
* No usar `z.string()` para booleanos ni para estados cerrados.
|
||||||
|
* Distinguir explícitamente entre fecha, hora y timestamp mediante schemas específicos.
|
||||||
|
|
||||||
Si `z.iso.date()` no está disponible o no os convence, crear schema propio.
|
## 5. Fechas, porcentajes, importes, cantidades
|
||||||
|
|
||||||
---
|
### 5.1. Amount, Money, Percentage y Quantity
|
||||||
|
|
||||||
## 5.3. Enums y estados
|
* Los valores exactos no deben viajar como `number`.
|
||||||
|
* Deben viajar siempre con una única forma de transporte para toda la API.
|
||||||
* Todo campo de conjunto cerrado debe ser `z.enum(...)`. Ejemplos:
|
|
||||||
|
|
||||||
```
|
|
||||||
* `status`
|
|
||||||
* `language_code` si el conjunto es acotado
|
|
||||||
* `currency_code` si estáis acotando monedas soportadas
|
|
||||||
```
|
|
||||||
|
|
||||||
* No usar `z.string()` para estados de negocio cerrados.
|
|
||||||
* Si el conjunto no está cerrado aún, al menos documentarlo y preparar schema específico.
|
|
||||||
|
|
||||||
Ejemplo:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export const ProformaStatusSchema = z.enum([
|
|
||||||
"draft",
|
|
||||||
"pending",
|
|
||||||
"issued",
|
|
||||||
"cancelled",
|
|
||||||
]);
|
|
||||||
```
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5.4. Booleanos
|
|
||||||
|
|
||||||
Ejemplo:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
z.boolean()
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5.5. Amount, Money, Percentage y Quantity
|
|
||||||
|
|
||||||
* Estos valores deben viajar como objetos explícitos:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
type MoneyDTO = {
|
type MoneyDTO = {
|
||||||
@ -223,96 +118,81 @@ type QuantityDTO = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5.6. Listas
|
### 5.2. Fechas y timestamps
|
||||||
|
|
||||||
|
Distinguir entre:
|
||||||
|
|
||||||
|
* fecha de calendario sin hora -> "2020-01-01"
|
||||||
|
* timestamp con fecha y hora ->
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
type ListIssuedInvoicesResponseDTO = {
|
export const IsoDateSchema = z.iso.date(); // "2020-01-01"
|
||||||
page: number,
|
export const IsoTimeSchema = z.iso.time(); // "03:15:00", "03:15"
|
||||||
per_page: number,
|
export const OffsetDateTimeSchema = z.iso.datetime({ offset: true }); // "2020-01-01T06:15:00+02:00"
|
||||||
total_pages: number,
|
export const LocalDateTimeSchema = z.iso.datetime({ local: true }); // "2020-01-01T06:15:01"
|
||||||
total_items: number,
|
export const DateTimeSchema1 = z.iso.datetime({ precision: -1 }); // minute precision (no seconds) -> "2020-01-01T06:15Z"
|
||||||
items: {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Si se prefiere no usar `z.iso.date()`, crear schema propio.
|
||||||
|
|
||||||
## 6. Nullabilidad, opcionalidad y semántica de ausencia
|
|
||||||
|
|
||||||
Significado:
|
## 6. Opcionalidad, nullabilidad y ausencia
|
||||||
|
|
||||||
- `undefined` / campo omitido: no enviado
|
Significados:
|
||||||
- `null`: enviado explícitamente sin valor
|
|
||||||
- `""`: cadena vacía, no ausencia
|
|
||||||
- `[]`: colección vacía, no ausencia
|
|
||||||
|
|
||||||
**No cambiar estos significados salvo decisión explícita de contrato.**
|
* `undefined` / campo omitido -> no enviado
|
||||||
|
* `null` -> enviado explícitamente sin valor
|
||||||
|
* `""` -> cadena vacía, no indica ausencia de ese campo
|
||||||
|
* `[]` -> colección vacía, no indica ausencia de esa colección
|
||||||
|
|
||||||
### 6.1. Para `requests`
|
No cambiar estos significados salvo decisión explícita de contrato.
|
||||||
|
|
||||||
* Preferir `optional()` para campos no obligatorios.
|
### 6.1. Para requests
|
||||||
* Usar `nullable()` solo cuando tenga significado de negocio “borrar/desvincular”.
|
|
||||||
|
* Usar `optional()` para campos no obligatorios.
|
||||||
|
* Usar `nullable()` solo cuando tenga semántica de negocio, por ejemplo “borrar” o “desvincular”.
|
||||||
|
|
||||||
Ejemplo:
|
Ejemplo:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
payment_method_id: z.uuid().nullable().optional()
|
payment_method_id: z.uuid().nullable().optional()
|
||||||
|
```
|
||||||
|
|
||||||
Interpretación:
|
Interpretación:
|
||||||
|
|
||||||
* omitido -> no tocar
|
* omitido -> no tocar
|
||||||
* `null` -> quitar método de pago
|
* `null` -> quitar relación
|
||||||
* UUID -> asignar id de método de pago
|
* UUID -> asignar relación
|
||||||
|
|
||||||
|
### 6.2. Para responses
|
||||||
|
|
||||||
|
* Decidir de forma explícita si una ausencia semántica se representa con `null` o con omisión.
|
||||||
|
* Para relaciones opcionales, suele ser más claro usar `null` que omitir la relación.
|
||||||
|
|
||||||
|
## 7. Reglas para CREATE
|
||||||
|
|
||||||
|
* El request de `create` debe contener solo los datos necesarios para crear el recurso. El resto, como opcionales => `optional()`
|
||||||
|
* No reutilizar el DTO de detalle como DTO de creación.
|
||||||
|
* No usar defaults en requests de negocio.
|
||||||
|
* Si hace falta normalización, hacerla fuera del DTO:
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
validación DTO => normalización input => mapeo a command/props
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.3. Para `responses`
|
## 8. Reglas para UPDATE
|
||||||
|
|
||||||
* si el campo puede no existir semánticamente, decidir si se usa `null` o ausencia `""`
|
* `update` debe seguir semántica tipo `PATCH`:
|
||||||
* para relaciones opcionales, suele ser más claro usar `null` que omitir la relación.
|
|
||||||
|
|
||||||
---
|
* solo viajan los campos a modificar
|
||||||
|
* los campos omitidos no se tocan
|
||||||
## 7. Reglas para requests de CREATE
|
* La opcionalidad debe declararse en el esquema con `optional()`.
|
||||||
|
* No usar `Partial<>` sobre tipos inferidos si el esquema ya expresa opcionalidad.
|
||||||
* Definir como obligatorios en el esquema los campos necesarios para crear el recurso. El resto, como opcionales => `optional()`
|
* No usar defaults en `update`, especialmente en colecciones.
|
||||||
* No reutilizar el DTO de detalle como DTO de creación.
|
|
||||||
* No usar defaults
|
|
||||||
|
|
||||||
Incorrecto:
|
Incorrecto:
|
||||||
|
|
||||||
```ts
|
|
||||||
description: z.string().default("")
|
|
||||||
```
|
|
||||||
|
|
||||||
Correcto:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
description: z.string().optional()
|
|
||||||
```
|
|
||||||
|
|
||||||
* Si el backend necesita normalización, hacerla fuera del DTO. Secuencia: `validación DTO` => `normalización input` => ' '
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Reglas para requests de UPDATE
|
|
||||||
|
|
||||||
* Usar semántica tipo **PATCH**:
|
|
||||||
- solo viajan los campos a modificar
|
|
||||||
- los campos omitidos no se tocan
|
|
||||||
|
|
||||||
* Si un campo es opcional, usar `optional()` en el esquema:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
series: z.string().optional()
|
|
||||||
```
|
|
||||||
|
|
||||||
* No usar `defaults` en update:
|
|
||||||
|
|
||||||
Esto es incorrecto porque `[]` puede significar “vaciar items”, no “campo omitido”.
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
items: z.array(...).default([])
|
items: z.array(...).default([])
|
||||||
```
|
```
|
||||||
@ -323,239 +203,210 @@ Correcto:
|
|||||||
items: z.array(...).optional()
|
items: z.array(...).optional()
|
||||||
```
|
```
|
||||||
|
|
||||||
* no usar `Partial<>`:
|
### 8.1. UPDATE de colecciones
|
||||||
|
|
||||||
Incorrecto:
|
* Por defecto, mantener reemplazo completo de la colección.
|
||||||
|
* Si se necesita granularidad real, usar una estructura explícita:
|
||||||
```ts
|
|
||||||
type UpdateProformaByIdRequestDTO = Partial<z.infer<typeof UpdateProformaByIdRequestSchema>>;
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## 8.1. Reglas para request de UPDATE en **colecciones**
|
|
||||||
|
|
||||||
* En `update` de colecciones, mantener reemplazo completo de la colección
|
|
||||||
* Si se necesitara granularidad real en el `update` de items, valorar usar en la API una estructura explícita:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
items: {
|
items: {
|
||||||
create: [...]
|
create: [...],
|
||||||
update: [...]
|
update: [...],
|
||||||
delete: [...]
|
delete: [...],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Responses de LIST y DETAIL
|
||||||
|
|
||||||
|
* `list` y `get` no deben compartir el mismo esquema completo.
|
||||||
|
* Separar vistas de resumen y detalle:
|
||||||
|
|
||||||
|
* `List` -> summary
|
||||||
|
* `Get` -> detail
|
||||||
|
* `list` debe contener solo lo necesario para el listado.
|
||||||
|
* Puede existir un esquema base compartido solo si mejora claridad y estabilidad.
|
||||||
|
|
||||||
|
## 10. Subesquemas reutilizables
|
||||||
|
|
||||||
|
* Extraer a `shared/` toda estructura no trivial que se repita y sea estable.
|
||||||
|
* No extraer abstracciones prematuramente.
|
||||||
|
|
||||||
|
Ejemplos típicos:
|
||||||
|
|
||||||
|
* `recipient`
|
||||||
|
* `item`
|
||||||
|
* `tax-breakdown`
|
||||||
|
* `payment-method-ref`
|
||||||
|
|
||||||
|
## 10.bis. Snapshot builders y materialización de DTOs
|
||||||
|
|
||||||
|
* Los snapshot builders de API existen para materializar objetos de salida serializables a partir de modelos de aplicación, agregados o read models.
|
||||||
|
* Un snapshot builder **no define el contrato HTTP**; el contrato lo define el schema DTO.
|
||||||
|
* El schema Zod es la **única fuente de verdad (SSOT)** del contrato de salida.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Regla principal
|
||||||
|
|
||||||
|
> Si un snapshot builder genera un DTO de API, su tipo de salida debe ser el `z.infer` del schema exacto que representa.
|
||||||
|
|
||||||
|
Ejemplo:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const CustomerSummarySchema = z.object({ ... });
|
||||||
|
export type CustomerSummaryDTO = z.infer<typeof CustomerSummarySchema>;
|
||||||
|
|
||||||
|
export class CustomerSummarySnapshotBuilder
|
||||||
|
implements ISnapshotBuilder<CustomerSummary, CustomerSummaryDTO> {
|
||||||
|
public toOutput(model: CustomerSummary): CustomerSummaryDTO {
|
||||||
|
...
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Reglas para responses de **LIST** y **DETAIL**
|
### Reglas obligatorias
|
||||||
|
|
||||||
* Separar summary y detail:
|
* No duplicar el contrato mediante interfaces manuales paralelas si ya existe un schema Zod equivalente.
|
||||||
- List => summary => `ProformaSummaryDTO`
|
* Eliminar tipos como `ICustomerSummarySnapshot` si representan el mismo contrato que un DTO.
|
||||||
- Get <=> detail => `ProformaDetailDTO`
|
* Si el builder construye una subvista reutilizable, debe tiparse contra el subschema compartido correspondiente (`shared/`).
|
||||||
|
* Si el builder construye la response completa, debe tiparse contra el response DTO completo.
|
||||||
|
* El builder debe devolver exactamente la forma del schema (no “aproximada”).
|
||||||
|
|
||||||
* `list` debe contener solo lo imprescindible para la tabla/listado
|
---
|
||||||
* Puede definirse un esquema base compartido **si aporta claridad**. Ejemplo:
|
|
||||||
|
### Responsabilidades del snapshot builder
|
||||||
|
|
||||||
|
Puede:
|
||||||
|
|
||||||
|
* Mapear `Maybe` → `null`
|
||||||
|
* Convertir Value Objects → primitives o DTOs (`string`, `{ value, scale }`, etc.)
|
||||||
|
* Adaptar enums internos → valores de contrato
|
||||||
|
* Construir vistas (`summary`, `detail`, etc.)
|
||||||
|
|
||||||
|
No puede:
|
||||||
|
|
||||||
|
* Definir contratos
|
||||||
|
* Validar reglas de negocio
|
||||||
|
* Acceder a repositorios
|
||||||
|
* Ejecutar lógica de dominio
|
||||||
|
* “corregir” datos fuera del contrato definido
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Separación por niveles
|
||||||
|
|
||||||
|
Si el endpoint es:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const ProformaBaseDTOSchema = z.object({
|
ListCustomersResponseSchema
|
||||||
id: z.uuid(),
|
|
||||||
company_id: z.uuid(),
|
|
||||||
customer_id: z.uuid(),
|
|
||||||
invoice_number: z.string(),
|
|
||||||
status: ProformaStatusSchema,
|
|
||||||
series: z.string(),
|
|
||||||
invoice_date: IsoDateSchema,
|
|
||||||
operation_date: IsoDateSchema,
|
|
||||||
language_code: z.string(),
|
|
||||||
currency_code: z.string(),
|
|
||||||
reference: z.string().nullable(),
|
|
||||||
description: z.string().nullable(),
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Despues:
|
Entonces:
|
||||||
|
|
||||||
* `summary` extiende añadiendo algún campos más
|
* Subschema:
|
||||||
* `detail` extiende añadiendo taxes, items, notes, etc.
|
|
||||||
|
```ts
|
||||||
|
CustomerSummarySchema
|
||||||
|
```
|
||||||
|
|
||||||
|
* Builder:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
CustomerSummarySnapshotBuilder -> CustomerSummaryDTO
|
||||||
|
```
|
||||||
|
|
||||||
|
No:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
Builder -> ListCustomersResponseDTO ❌
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Reglas para subesquemas reutilizables
|
### Naming obligatorio
|
||||||
|
|
||||||
* Estructura no trivial que se repite => definir subesquema en `shared/`.
|
DTO:
|
||||||
* Deben ser estructuras estables no sujetas a cambios. En caso contrario, mejor no reutilizar
|
|
||||||
|
|
||||||
Ejemplos claros en tu caso:
|
```plaintext
|
||||||
|
<resource>-summary.dto.ts
|
||||||
|
<resource>-detail.dto.ts
|
||||||
|
<action>-<resource>.response.dto.ts
|
||||||
|
```
|
||||||
|
|
||||||
* `recipient`
|
Builders:
|
||||||
* `proforma-item`
|
|
||||||
* `proforma-tax-breakdown`
|
```plaintext
|
||||||
* `payment-method-ref`
|
<resource>-summary.snapshot-builder.ts
|
||||||
|
<resource>-detail.snapshot-builder.ts
|
||||||
|
<action>-<resource>.response.snapshot-builder.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Evitar:
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
I<Resource>Snapshot ❌
|
||||||
|
<Resource>Output ❌
|
||||||
|
<Resource>Interface ❌
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. Reglas específicas para errores
|
### Regla de consistencia
|
||||||
|
|
||||||
|
> Si cambia el schema DTO, el builder debe romper en compile-time.
|
||||||
|
|
||||||
|
Esto garantiza:
|
||||||
|
|
||||||
|
* contrato consistente
|
||||||
|
* ausencia de drift
|
||||||
|
* eliminación de duplicación
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Errores
|
||||||
|
|
||||||
* El DTO de error debe ser estable, tipado y predecible.
|
* El DTO de error debe ser estable, tipado y predecible.
|
||||||
|
* No usar shapes excesivamente genéricos si ya se conoce la estructura mínima útil.
|
||||||
|
* El cliente debe poder representar errores sin heurísticas frágiles.
|
||||||
|
|
||||||
## 13.1. Estructura base recomendada
|
## 12. Metadata
|
||||||
|
|
||||||
```ts
|
* `metadata` solo debe contener datos auxiliares, trazabilidad no crítica, versionado o hints no funcionales.
|
||||||
export const ErrorDetailDTOSchema = z.object({
|
* Si un dato es necesario para que el cliente funcione, no debe ir en `metadata`, sino en el DTO principal.
|
||||||
code: z.string(),
|
|
||||||
path: z.string().optional(),
|
|
||||||
message: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ErrorResponseDTOSchema = z.object({
|
## 13. Evolución de la API
|
||||||
type: z.string().optional(),
|
|
||||||
title: z.string(),
|
|
||||||
status: z.number().int(),
|
|
||||||
detail: z.string().optional(),
|
|
||||||
instance: z.string().optional(),
|
|
||||||
context: z
|
|
||||||
.object({
|
|
||||||
params: z.record(z.string(), z.unknown()).optional(),
|
|
||||||
query: z.record(z.string(), z.unknown()).optional(),
|
|
||||||
body: z.record(z.string(), z.unknown()).optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
extra: z
|
|
||||||
.object({
|
|
||||||
errors: z.array(ErrorDetailDTOSchema).default([]),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## 13.2. Regla
|
* Es mejor añadir campos que reinterpretar campos existentes.
|
||||||
|
* Cambiar el significado o la estructura de un campo implica cambio de contrato.
|
||||||
|
|
||||||
No dejar `errors: Record<string, unknown>[]` si ya conoces la forma mínima útil.
|
## 14. Criterio para detectar overengineering
|
||||||
|
|
||||||
El cliente necesita poder renderizar errores sin heurísticas frágiles.
|
Un diseño de DTO está sobreingenierizado si:
|
||||||
|
|
||||||
---
|
* para entender un endpoint simple hay que abrir demasiados ficheros
|
||||||
|
* el esquema valida, normaliza y transforma a la vez
|
||||||
## 14. Reglas de metadata
|
* se extraen subesquemas que apenas se reutilizan
|
||||||
|
|
||||||
* Si un consumidor necesita ese dato para funcionar, no va en `metadata`: va en el DTO principal.
|
|
||||||
|
|
||||||
## 14.1. Cuándo usarla
|
|
||||||
|
|
||||||
Solo para:
|
|
||||||
|
|
||||||
* datos auxiliares
|
|
||||||
* trazabilidad no crítica
|
|
||||||
* versionado
|
|
||||||
* hints no funcionales
|
|
||||||
|
|
||||||
## 14.2. Cuándo no usarla
|
|
||||||
|
|
||||||
No usar `metadata` para:
|
|
||||||
|
|
||||||
* reglas de negocio
|
|
||||||
* campos que condicionan UI principal
|
|
||||||
* datos que el cliente necesita interpretar de forma contractual
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 15. Evolución de la API
|
|
||||||
|
|
||||||
* Es mejor añadir un campo nuevo que cambiar el significado de uno existente.
|
|
||||||
* Si un campo cambia de semántica o estructura, debe tratarse como cambio de contrato.
|
|
||||||
|
|
||||||
|
|
||||||
## 16. Criterio para detectar `overengineering`
|
|
||||||
|
|
||||||
Considera que un diseño de DTO está `sobreingenierizado` si ocurre alguna de estas:
|
|
||||||
|
|
||||||
* necesitas abrir 5 ficheros para entender un endpoint simple
|
|
||||||
* el schema hace validación, normalización, defaults y transformación a la vez
|
|
||||||
* hacer abstracciones compartidas en `shared` que lueo solo se utilizan en un caso
|
|
||||||
* el DTO intenta reflejar demasiado fielmente el dominio interno
|
* el DTO intenta reflejar demasiado fielmente el dominio interno
|
||||||
|
|
||||||
|
Regla práctica:
|
||||||
|
|
||||||
* preferir duplicación pequeña y clara frente a abstracción prematura
|
* preferir duplicación pequeña y clara frente a abstracción prematura
|
||||||
|
|
||||||
---
|
## 15. Checklist de revisión
|
||||||
|
|
||||||
## 21. Plantilla base recomendada
|
Antes de cerrar un DTO, comprobar:
|
||||||
|
|
||||||
## 21.1. Request simple por id
|
1. ¿Representa solo contrato de transporte?
|
||||||
|
2. ¿Cada campo usa el tipo semántico más preciso posible?
|
||||||
```ts
|
3. ¿Se distingue bien entre omitido, `null`, vacío y colección vacía?
|
||||||
import { z } from "zod/v4";
|
4. ¿Hay defaults ocultando intención?
|
||||||
|
5. ¿El naming es consistente?
|
||||||
export const GetProformaByIdRequestSchema = z.object({
|
6. ¿Las estructuras repetidas merecen extraerse?
|
||||||
proforma_id: z.uuid(),
|
7. ¿Hay abstracciones prematuras?
|
||||||
});
|
|
||||||
|
|
||||||
export type GetProformaByIdRequestDTO = z.infer<typeof GetProformaByIdRequestSchema>;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 21.2. Fragmento shared
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { z } from "zod/v4";
|
|
||||||
import { MoneyDTOSchema, PercentageDTOSchema } from "@erp/core";
|
|
||||||
|
|
||||||
export const ProformaTaxDTOSchema = z.object({
|
|
||||||
taxable_amount: MoneyDTOSchema,
|
|
||||||
|
|
||||||
iva_code: z.string(),
|
|
||||||
iva_percentage: PercentageDTOSchema,
|
|
||||||
iva_amount: MoneyDTOSchema,
|
|
||||||
|
|
||||||
rec_code: z.string(),
|
|
||||||
rec_percentage: PercentageDTOSchema,
|
|
||||||
rec_amount: MoneyDTOSchema,
|
|
||||||
|
|
||||||
retention_code: z.string(),
|
|
||||||
retention_percentage: PercentageDTOSchema,
|
|
||||||
retention_amount: MoneyDTOSchema,
|
|
||||||
|
|
||||||
taxes_amount: MoneyDTOSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ProformaTaxDTO = z.infer<typeof ProformaTaxDTOSchema>;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 21.3. Update parcial sin defaults
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { z } from "zod/v4";
|
|
||||||
|
|
||||||
export const UpdateProformaByIdRequestSchema = z.object({
|
|
||||||
series: z.string().optional(),
|
|
||||||
invoice_date: z.iso.date().optional(),
|
|
||||||
operation_date: z.iso.date().optional(),
|
|
||||||
customer_id: z.uuid().optional(),
|
|
||||||
reference: z.string().optional(),
|
|
||||||
description: z.string().optional(),
|
|
||||||
notes: z.string().optional(),
|
|
||||||
language_code: z.string().optional(),
|
|
||||||
currency_code: z.string().optional(),
|
|
||||||
items: z.array(UpdateProformaItemRequestSchema).optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type UpdateProformaByIdRequestDTO = z.infer<typeof UpdateProformaByIdRequestSchema>;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 22. Checklist de revisión de un DTO
|
|
||||||
|
|
||||||
Antes de dar un DTO por bueno, comprobar:
|
|
||||||
|
|
||||||
1. ¿Representa contrato de transporte y nada más?
|
|
||||||
2. ¿Cada campo tiene el tipo semántico más preciso posible?
|
|
||||||
3. ¿Se distingue bien entre omitido, null, vacío y colección vacía?
|
|
||||||
4. ¿Hay defaults que estén ocultando intención?
|
|
||||||
5. ¿El naming es consistente con el resto?
|
|
||||||
6. ¿Hay subestructuras repetidas que merezcan extraerse?
|
|
||||||
7. ¿Hay abstracciones extraídas demasiado pronto?
|
|
||||||
8. ¿El cliente puede consumirlo sin heurísticas frágiles?
|
8. ¿El cliente puede consumirlo sin heurísticas frágiles?
|
||||||
9. ¿El mismo campo mantiene mismo tipo en todo el módulo?
|
9. ¿El mismo campo mantiene el mismo tipo en todo el módulo?
|
||||||
10. ¿El DTO refleja una vista del endpoint y no una mezcla de varias?
|
10. ¿El DTO refleja una vista concreta del endpoint?
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
---
|
||||||
@ -17,6 +17,7 @@ import {
|
|||||||
isDomainValidationError,
|
isDomainValidationError,
|
||||||
isValidationErrorCollection,
|
isValidationErrorCollection,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
|
import type { ZodError } from "zod";
|
||||||
|
|
||||||
import { isSchemaError } from "../../../common/schemas";
|
import { isSchemaError } from "../../../common/schemas";
|
||||||
import { type DocumentGenerationError, isDocumentGenerationError } from "../../application";
|
import { type DocumentGenerationError, isDocumentGenerationError } from "../../application";
|
||||||
@ -33,7 +34,6 @@ import {
|
|||||||
isInfrastructureRepositoryError,
|
isInfrastructureRepositoryError,
|
||||||
isInfrastructureUnavailableError,
|
isInfrastructureUnavailableError,
|
||||||
} from "../errors";
|
} from "../errors";
|
||||||
import type { InfrastructureAPIContractError } from "../errors/infrastructure-api-contract-error";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ApiError,
|
ApiError,
|
||||||
@ -214,8 +214,10 @@ const defaultRules: ReadonlyArray<ErrorToApiRule> = [
|
|||||||
{
|
{
|
||||||
priority: 30,
|
priority: 30,
|
||||||
matches: (e) => isSchemaError(e),
|
matches: (e) => isSchemaError(e),
|
||||||
build: (e) =>
|
build: (e) => {
|
||||||
new InternalApiError((e as InfrastructureAPIContractError).message, "API contract violation"),
|
const schemaError = e as ZodError;
|
||||||
|
return new ValidationApiError("Schema validation failed", schemaError.issues);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod/v4";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import { AmountBaseSchema } from "./base.schemas";
|
import { AmountBaseSchema } from "./base.schemas";
|
||||||
|
import { CurrencyCodeSchema } from "./currency-code.dto";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Esquema del DTO para valores monetarios con/sin código de moneda.
|
Esquema del DTO para valores monetarios con/sin código de moneda.
|
||||||
@ -13,7 +14,7 @@ import { AmountBaseSchema } from "./base.schemas";
|
|||||||
|
|
||||||
// 🔹 Con moneda
|
// 🔹 Con moneda
|
||||||
export const MoneySchema = AmountBaseSchema.extend({
|
export const MoneySchema = AmountBaseSchema.extend({
|
||||||
currency_code: z.string(),
|
currency_code: CurrencyCodeSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🔹 Sin moneda
|
// 🔹 Sin moneda
|
||||||
|
|||||||
13
modules/core/src/common/dto/country-code.dto.ts
Normal file
13
modules/core/src/common/dto/country-code.dto.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Código de país ISO 3166-1 alpha-2 en mayúsculas.
|
||||||
|
*
|
||||||
|
* Valida exactamente 2 letras ASCII mayúsculas.
|
||||||
|
* Ejemplos válidos: "ES", "FR", "PT".
|
||||||
|
*/
|
||||||
|
export const CountryCodeSchema = z
|
||||||
|
.string()
|
||||||
|
.regex(/^[A-Z]{2}$/, "Country code must be a valid ISO 3166-1 alpha-2 uppercase code.");
|
||||||
|
|
||||||
|
export type CountryCodeDTO = z.infer<typeof CountryCodeSchema>;
|
||||||
13
modules/core/src/common/dto/currency-code.dto.ts
Normal file
13
modules/core/src/common/dto/currency-code.dto.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Código de moneda ISO 4217 (alpha-3).
|
||||||
|
*
|
||||||
|
* Valida exactamente 3 letras ASCII mayúsculas.
|
||||||
|
* Ejemplos válidos: "EUR", "USD", "GBP".
|
||||||
|
*/
|
||||||
|
export const CurrencyCodeSchema = z
|
||||||
|
.string()
|
||||||
|
.regex(/^[A-Z]{3}$/, "Currency code must be a valid ISO 4217 alpha-3 uppercase code.");
|
||||||
|
|
||||||
|
export type CurrencyCodeDTO = z.infer<typeof CurrencyCodeSchema>;
|
||||||
5
modules/core/src/common/dto/email.dto.ts
Normal file
5
modules/core/src/common/dto/email.dto.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const EmailSchema = z.string().email();
|
||||||
|
|
||||||
|
export type EmailDTO = z.infer<typeof EmailSchema>;
|
||||||
@ -1,8 +1,18 @@
|
|||||||
export * from "./amount-money.dto";
|
export * from "./amount-money.dto";
|
||||||
export * from "./base.schemas";
|
export * from "./base.schemas";
|
||||||
|
export * from "./country-code.dto";
|
||||||
export * from "./critera.dto";
|
export * from "./critera.dto";
|
||||||
|
export * from "./currency-code.dto";
|
||||||
|
export * from "./email.dto";
|
||||||
export * from "./error.dto";
|
export * from "./error.dto";
|
||||||
|
export * from "./iso-date-dto";
|
||||||
|
export * from "./land-phone.dto";
|
||||||
|
export * from "./language-code.dto";
|
||||||
export * from "./list-view.response.dto";
|
export * from "./list-view.response.dto";
|
||||||
export * from "./metadata.dto";
|
export * from "./metadata.dto";
|
||||||
|
export * from "./mobile-phone.dto";
|
||||||
export * from "./percentage.dto";
|
export * from "./percentage.dto";
|
||||||
|
export * from "./postal-code.dto";
|
||||||
export * from "./quantity.dto";
|
export * from "./quantity.dto";
|
||||||
|
export * from "./tin.dto";
|
||||||
|
export * from "./url.dto";
|
||||||
|
|||||||
51
modules/core/src/common/dto/iso-date-dto.ts
Normal file
51
modules/core/src/common/dto/iso-date-dto.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Esquema para validar una fecha en formato ISO 8601 (solo fecha).
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* Formato esperado: `YYYY-MM-DD`
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* "2020-01-01"
|
||||||
|
*/
|
||||||
|
export const IsoDateSchema = z.iso.date();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Esquema para validar una hora en formato ISO 8601.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* Acepta horas con o sin segundos.
|
||||||
|
* Formatos válidos:
|
||||||
|
* - `HH:mm`
|
||||||
|
* - `HH:mm:ss`
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* "03:15"
|
||||||
|
* "03:15:00"
|
||||||
|
*/
|
||||||
|
export const IsoTimeSchema = z.iso.time();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Esquema para validar una fecha y hora con offset de zona horaria.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* Requiere un desplazamiento explícito respecto a UTC.
|
||||||
|
* Formato: `YYYY-MM-DDTHH:mm:ss±HH:mm`
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* "2020-01-01T06:15:00+02:00"
|
||||||
|
*/
|
||||||
|
export const OffsetDateTimeSchema = z.iso.datetime({ offset: true });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Esquema para validar una fecha y hora local (sin información de zona horaria).
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* No incluye offset ni información de zona.
|
||||||
|
* Formato: `YYYY-MM-DDTHH:mm:ss`
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* "2020-01-01T06:15:01"
|
||||||
|
*/
|
||||||
|
export const LocalDateTimeSchema = z.iso.datetime({ local: true });
|
||||||
5
modules/core/src/common/dto/land-phone.dto.ts
Normal file
5
modules/core/src/common/dto/land-phone.dto.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const LandPhoneSchema = z.string().regex(/^\d{3}-\d{3}-\d{4}$/);
|
||||||
|
|
||||||
|
export type LandPhoneDTO = z.infer<typeof LandPhoneSchema>;
|
||||||
13
modules/core/src/common/dto/language-code.dto.ts
Normal file
13
modules/core/src/common/dto/language-code.dto.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Código de idioma ISO 639-1.
|
||||||
|
*
|
||||||
|
* Valida exactamente 2 letras ASCII minúsculas.
|
||||||
|
* Ejemplos válidos: "es", "en", "fr".
|
||||||
|
*/
|
||||||
|
export const LanguageCodeSchema = z
|
||||||
|
.string()
|
||||||
|
.regex(/^[a-z]{2}$/, "Language code must be a valid ISO 639-1 lowercase code.");
|
||||||
|
|
||||||
|
export type LanguageCodeDTO = z.infer<typeof LanguageCodeSchema>;
|
||||||
5
modules/core/src/common/dto/mobile-phone.dto.ts
Normal file
5
modules/core/src/common/dto/mobile-phone.dto.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const MobilePhoneSchema = z.string().regex(/^\d{3}-\d{3}-\d{4}$/);
|
||||||
|
|
||||||
|
export type MobilePhoneDTO = z.infer<typeof MobilePhoneSchema>;
|
||||||
21
modules/core/src/common/dto/postal-code.dto.ts
Normal file
21
modules/core/src/common/dto/postal-code.dto.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Código postal genérico (global).
|
||||||
|
*
|
||||||
|
* Permite letras, números, espacios y guiones.
|
||||||
|
* No valida formato específico por país.
|
||||||
|
*
|
||||||
|
* Ejemplos válidos:
|
||||||
|
* - "28013" (ES)
|
||||||
|
* - "SW1A 1AA" (UK)
|
||||||
|
* - "75008" (FR)
|
||||||
|
* - "10115" (DE)
|
||||||
|
*/
|
||||||
|
export const PostalCodeSchema = z
|
||||||
|
.string()
|
||||||
|
.min(1, "Postal code cannot be empty")
|
||||||
|
.max(16, "Postal code too long")
|
||||||
|
.regex(/^[A-Za-z0-9\- ]+$/, "Invalid postal code format");
|
||||||
|
|
||||||
|
export type PostalCodeDTO = z.infer<typeof PostalCodeSchema>;
|
||||||
23
modules/core/src/common/dto/tin.dto.ts
Normal file
23
modules/core/src/common/dto/tin.dto.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tax Identification Number (TIN) genérico internacional.
|
||||||
|
*
|
||||||
|
* Permite letras y números ASCII, además de separadores frecuentes.
|
||||||
|
* No valida reglas específicas por país.
|
||||||
|
*
|
||||||
|
* Ejemplos válidos:
|
||||||
|
* - "B83999441"
|
||||||
|
* - "ESB83999441"
|
||||||
|
* - "48086V"
|
||||||
|
* - "FR40303265045"
|
||||||
|
* - "DE123456789"
|
||||||
|
*/
|
||||||
|
export const TinSchema = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "TIN cannot be empty.")
|
||||||
|
.max(32, "TIN is too long.")
|
||||||
|
.regex(/^(?=.*[A-Za-z0-9])[A-Za-z0-9.\-/ ]+$/, "TIN has an invalid format.");
|
||||||
|
|
||||||
|
export type TinDTO = z.infer<typeof TinSchema>;
|
||||||
7
modules/core/src/common/dto/url.dto.ts
Normal file
7
modules/core/src/common/dto/url.dto.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const URLSchema = z.url({
|
||||||
|
message: "Invalid URL format",
|
||||||
|
});
|
||||||
|
|
||||||
|
export type URLDTO = z.infer<typeof URLSchema>;
|
||||||
@ -38,4 +38,6 @@ export type IssuedInvoiceSummary = {
|
|||||||
totalAmount: InvoiceAmount;
|
totalAmount: InvoiceAmount;
|
||||||
|
|
||||||
verifactu: Maybe<VerifactuRecord>;
|
verifactu: Maybe<VerifactuRecord>;
|
||||||
|
|
||||||
|
linkedProformaId: Maybe<UniqueID>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
import { maybeToNullable } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
import type { IssuedInvoice } from "../../../../domain";
|
import type { IssuedInvoice } from "../../../../domain";
|
||||||
|
|
||||||
@ -26,31 +26,26 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
|
|||||||
const verifactu = this.verifactuBuilder.toOutput(invoice);
|
const verifactu = this.verifactuBuilder.toOutput(invoice);
|
||||||
const taxes = this.taxesBuilder.toOutput(invoice.taxes);
|
const taxes = this.taxesBuilder.toOutput(invoice.taxes);
|
||||||
|
|
||||||
const payment = invoice.paymentMethod.match(
|
const payment_method = maybeToNullable(invoice.paymentMethod, (p) => ({
|
||||||
(payment) => {
|
payment_id: p.id.toString(),
|
||||||
const { id, payment_description } = payment.toObjectString();
|
payment_description: p.paymentDescription.toString(),
|
||||||
return {
|
}));
|
||||||
payment_id: id,
|
|
||||||
payment_description,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
() => undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: invoice.id.toString(),
|
id: invoice.id.toString(),
|
||||||
company_id: invoice.companyId.toString(),
|
company_id: invoice.companyId.toString(),
|
||||||
|
is_proforma: false,
|
||||||
|
|
||||||
invoice_number: invoice.invoiceNumber.toString(),
|
invoice_number: invoice.invoiceNumber.toString(),
|
||||||
status: invoice.status.toPrimitive(),
|
status: invoice.status.toPrimitive(),
|
||||||
series: maybeToEmptyString(invoice.series, (value) => value.toString()),
|
series: invoice.series.toString(),
|
||||||
|
|
||||||
invoice_date: invoice.invoiceDate.toDateString(),
|
invoice_date: invoice.invoiceDate.toDateString(),
|
||||||
operation_date: maybeToEmptyString(invoice.operationDate, (value) => value.toDateString()),
|
operation_date: maybeToNullable(invoice.operationDate, (value) => value.toDateString()),
|
||||||
|
|
||||||
reference: maybeToEmptyString(invoice.reference, (value) => value.toString()),
|
reference: maybeToNullable(invoice.reference, (value) => value.toString()),
|
||||||
description: maybeToEmptyString(invoice.description, (value) => value.toString()),
|
description: invoice.description.toString(),
|
||||||
notes: maybeToEmptyString(invoice.notes, (value) => value.toString()),
|
notes: maybeToNullable(invoice.notes, (value) => value.toString()),
|
||||||
|
|
||||||
language_code: invoice.languageCode.toString(),
|
language_code: invoice.languageCode.toString(),
|
||||||
currency_code: invoice.currencyCode.toString(),
|
currency_code: invoice.currencyCode.toString(),
|
||||||
@ -58,14 +53,19 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
|
|||||||
customer_id: invoice.customerId.toString(),
|
customer_id: invoice.customerId.toString(),
|
||||||
recipient,
|
recipient,
|
||||||
|
|
||||||
payment_method: payment,
|
linked_proforma_id: maybeToNullable(invoice.linkedProformaId, (linkedId) =>
|
||||||
|
linkedId.toString()
|
||||||
|
),
|
||||||
|
|
||||||
|
taxes,
|
||||||
|
|
||||||
|
payment_method,
|
||||||
|
|
||||||
subtotal_amount: invoice.subtotalAmount.toObjectString(),
|
subtotal_amount: invoice.subtotalAmount.toObjectString(),
|
||||||
items_discount_amount: invoice.itemsDiscountAmount.toObjectString(),
|
|
||||||
|
|
||||||
|
items_discount_amount: invoice.itemsDiscountAmount.toObjectString(),
|
||||||
global_discount_percentage: invoice.globalDiscountPercentage.toObjectString(),
|
global_discount_percentage: invoice.globalDiscountPercentage.toObjectString(),
|
||||||
global_discount_amount: invoice.globalDiscountAmount.toObjectString(),
|
global_discount_amount: invoice.globalDiscountAmount.toObjectString(),
|
||||||
|
|
||||||
total_discount_amount: invoice.totalDiscountAmount.toObjectString(),
|
total_discount_amount: invoice.totalDiscountAmount.toObjectString(),
|
||||||
|
|
||||||
taxable_amount: invoice.taxableAmount.toObjectString(),
|
taxable_amount: invoice.taxableAmount.toObjectString(),
|
||||||
@ -77,8 +77,6 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
|
|||||||
taxes_amount: invoice.taxesAmount.toObjectString(),
|
taxes_amount: invoice.taxesAmount.toObjectString(),
|
||||||
total_amount: invoice.totalAmount.toObjectString(),
|
total_amount: invoice.totalAmount.toObjectString(),
|
||||||
|
|
||||||
taxes,
|
|
||||||
|
|
||||||
verifactu,
|
verifactu,
|
||||||
items,
|
items,
|
||||||
|
|
||||||
|
|||||||
@ -3,20 +3,32 @@ import type { IIssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recip
|
|||||||
import type { IIssuedInvoiceTaxFullSnapshot } from "./issued-invoice-tax-full-snapshot-interface";
|
import type { IIssuedInvoiceTaxFullSnapshot } from "./issued-invoice-tax-full-snapshot-interface";
|
||||||
import type { IIssuedInvoiceVerifactuFullSnapshot } from "./issued-invoice-verifactu-full-snapshot.interface";
|
import type { IIssuedInvoiceVerifactuFullSnapshot } from "./issued-invoice-verifactu-full-snapshot.interface";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilizar GetIssuedInvoiceByIdResponseDTO o
|
||||||
|
* definir un tipo específico para el snapshot de factura emitida,
|
||||||
|
* ya que GetIssuedInvoiceByIdResponseDTO tiene campos que no se
|
||||||
|
* corresponden exactamente con el snapshot
|
||||||
|
* (por ejemplo, tiene un campo "linked_invoice" que en el snapshot se representa
|
||||||
|
* como "linked_invoice_id").
|
||||||
|
*/
|
||||||
|
|
||||||
|
//export type IIssuedInvoiceFullSnapshot = GetIssuedInvoiceByIdResponseDTO;
|
||||||
|
|
||||||
export interface IIssuedInvoiceFullSnapshot {
|
export interface IIssuedInvoiceFullSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
company_id: string;
|
company_id: string;
|
||||||
|
is_proforma: boolean;
|
||||||
|
|
||||||
invoice_number: string;
|
invoice_number: string;
|
||||||
status: string;
|
status: string;
|
||||||
series: string;
|
series: string;
|
||||||
|
|
||||||
invoice_date: string;
|
invoice_date: string;
|
||||||
operation_date: string;
|
operation_date: string | null;
|
||||||
|
|
||||||
reference: string;
|
reference: string | null;
|
||||||
description: string;
|
description: string;
|
||||||
notes: string;
|
notes: string | null;
|
||||||
|
|
||||||
language_code: string;
|
language_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
@ -24,18 +36,20 @@ export interface IIssuedInvoiceFullSnapshot {
|
|||||||
customer_id: string;
|
customer_id: string;
|
||||||
recipient: IIssuedInvoiceRecipientFullSnapshot;
|
recipient: IIssuedInvoiceRecipientFullSnapshot;
|
||||||
|
|
||||||
payment_method?: {
|
linked_proforma_id: string | null;
|
||||||
|
|
||||||
|
taxes: IIssuedInvoiceTaxFullSnapshot[];
|
||||||
|
|
||||||
|
payment_method: {
|
||||||
payment_id: string;
|
payment_id: string;
|
||||||
payment_description: string;
|
payment_description: string;
|
||||||
};
|
} | null;
|
||||||
|
|
||||||
subtotal_amount: { value: string; scale: string; currency_code: string };
|
subtotal_amount: { value: string; scale: string; currency_code: string };
|
||||||
|
|
||||||
items_discount_amount: { value: string; scale: string; currency_code: string };
|
items_discount_amount: { value: string; scale: string; currency_code: string };
|
||||||
|
|
||||||
global_discount_percentage: { value: string; scale: string };
|
global_discount_percentage: { value: string; scale: string };
|
||||||
global_discount_amount: { value: string; scale: string; currency_code: string };
|
global_discount_amount: { value: string; scale: string; currency_code: string };
|
||||||
|
|
||||||
total_discount_amount: { value: string; scale: string; currency_code: string };
|
total_discount_amount: { value: string; scale: string; currency_code: string };
|
||||||
|
|
||||||
taxable_amount: { value: string; scale: string; currency_code: string };
|
taxable_amount: { value: string; scale: string; currency_code: string };
|
||||||
@ -47,8 +61,6 @@ export interface IIssuedInvoiceFullSnapshot {
|
|||||||
taxes_amount: { value: string; scale: string; currency_code: string };
|
taxes_amount: { value: string; scale: string; currency_code: string };
|
||||||
total_amount: { value: string; scale: string; currency_code: string };
|
total_amount: { value: string; scale: string; currency_code: string };
|
||||||
|
|
||||||
taxes: IIssuedInvoiceTaxFullSnapshot[];
|
|
||||||
|
|
||||||
verifactu: IIssuedInvoiceVerifactuFullSnapshot;
|
verifactu: IIssuedInvoiceVerifactuFullSnapshot;
|
||||||
items: IIssuedInvoiceItemFullSnapshot[];
|
items: IIssuedInvoiceItemFullSnapshot[];
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
export interface IIssuedInvoiceItemFullSnapshot {
|
export interface IIssuedInvoiceItemFullSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
is_valued: string;
|
is_valued: boolean;
|
||||||
position: string;
|
position: number;
|
||||||
description: string;
|
description: string | null;
|
||||||
|
|
||||||
quantity: { value: string; scale: string };
|
quantity: { value: string; scale: string };
|
||||||
unit_amount: { value: string; scale: string; currency_code: string };
|
unit_amount: { value: string; scale: string; currency_code: string };
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
maybeToEmptyPercentageObjectString,
|
maybeToEmptyPercentageObjectString,
|
||||||
maybeToEmptyQuantityObjectString,
|
maybeToEmptyQuantityObjectString,
|
||||||
maybeToEmptyString,
|
maybeToEmptyString,
|
||||||
|
maybeToNullable,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
|
|
||||||
import { type IssuedInvoiceItem, type IssuedInvoiceItems, ItemAmount } from "../../../../domain";
|
import { type IssuedInvoiceItem, type IssuedInvoiceItems, ItemAmount } from "../../../../domain";
|
||||||
@ -21,10 +22,10 @@ export class IssuedInvoiceItemsFullSnapshotBuilder
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: invoiceItem.id.toPrimitive(),
|
id: invoiceItem.id.toPrimitive(),
|
||||||
is_valued: String(isValued),
|
is_valued: isValued,
|
||||||
position: String(index),
|
position: index,
|
||||||
|
|
||||||
description: maybeToEmptyString(invoiceItem.description, (value) => value.toString()),
|
description: maybeToNullable(invoiceItem.description, (value) => value.toString()),
|
||||||
|
|
||||||
quantity: maybeToEmptyQuantityObjectString(invoiceItem.quantity),
|
quantity: maybeToEmptyQuantityObjectString(invoiceItem.quantity),
|
||||||
unit_amount: maybeToEmptyMoneyObjectString(invoiceItem.unitAmount),
|
unit_amount: maybeToEmptyMoneyObjectString(invoiceItem.unitAmount),
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
import { DomainValidationError, maybeToEmptyString } from "@repo/rdx-ddd";
|
import { DomainValidationError, maybeToNullable } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
import type { InvoiceRecipient, IssuedInvoice } from "../../../../domain";
|
import type { IssuedInvoice } from "../../../../domain";
|
||||||
|
|
||||||
import type { IIssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot.interface";
|
import type { IIssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot.interface";
|
||||||
|
|
||||||
@ -17,30 +17,18 @@ export class IssuedInvoiceRecipientFullSnapshotBuilder
|
|||||||
cause: invoice,
|
cause: invoice,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const recipient = invoice.recipient;
|
||||||
|
|
||||||
return invoice.recipient.match(
|
return {
|
||||||
(recipient: InvoiceRecipient) => ({
|
id: invoice.customerId.toString(),
|
||||||
id: invoice.customerId.toString(),
|
name: recipient.name.toString(),
|
||||||
name: recipient.name.toString(),
|
tin: recipient.tin.toString(),
|
||||||
tin: recipient.tin.toString(),
|
street: maybeToNullable(recipient.street, (v) => v.toString()),
|
||||||
street: maybeToEmptyString(recipient.street, (v) => v.toString()),
|
street2: maybeToNullable(recipient.street2, (v) => v.toString()),
|
||||||
street2: maybeToEmptyString(recipient.street2, (v) => v.toString()),
|
city: maybeToNullable(recipient.city, (v) => v.toString()),
|
||||||
city: maybeToEmptyString(recipient.city, (v) => v.toString()),
|
province: maybeToNullable(recipient.province, (v) => v.toString()),
|
||||||
province: maybeToEmptyString(recipient.province, (v) => v.toString()),
|
postal_code: maybeToNullable(recipient.postalCode, (v) => v.toString()),
|
||||||
postal_code: maybeToEmptyString(recipient.postalCode, (v) => v.toString()),
|
country: maybeToNullable(recipient.country, (v) => v.toString()),
|
||||||
country: maybeToEmptyString(recipient.country, (v) => v.toString()),
|
};
|
||||||
}),
|
|
||||||
() => ({
|
|
||||||
id: "",
|
|
||||||
name: "",
|
|
||||||
tin: "",
|
|
||||||
street: "",
|
|
||||||
street2: "",
|
|
||||||
city: "",
|
|
||||||
province: "",
|
|
||||||
postal_code: "",
|
|
||||||
country: "",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,19 @@
|
|||||||
export interface IIssuedInvoiceRecipientFullSnapshot {
|
/**
|
||||||
id: string;
|
* Fijarse en IssuedInvoiceRecipientSummarySchema
|
||||||
name: string;
|
*/
|
||||||
tin: string;
|
|
||||||
street: string;
|
import type { IssuedInvoiceRecipientSummaryDTO } from "../../../../../common/dto";
|
||||||
street2: string;
|
|
||||||
city: string;
|
export type IIssuedInvoiceRecipientFullSnapshot = IssuedInvoiceRecipientSummaryDTO;
|
||||||
province: string;
|
|
||||||
postal_code: string;
|
interface IIssuedInvoiceRecipientFullSnapshot2 {
|
||||||
country: string;
|
id: string | null;
|
||||||
|
name: string | null;
|
||||||
|
tin: string | null;
|
||||||
|
street: string | null;
|
||||||
|
street2: string | null;
|
||||||
|
city: string | null;
|
||||||
|
province: string | null;
|
||||||
|
postal_code: string | null;
|
||||||
|
country: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,11 @@ export interface IIssuedInvoiceTaxFullSnapshot {
|
|||||||
iva_percentage: { value: string; scale: string };
|
iva_percentage: { value: string; scale: string };
|
||||||
iva_amount: { value: string; scale: string; currency_code: string };
|
iva_amount: { value: string; scale: string; currency_code: string };
|
||||||
|
|
||||||
rec_code: string;
|
rec_code: string | null;
|
||||||
rec_percentage: { value: string; scale: string };
|
rec_percentage: { value: string; scale: string };
|
||||||
rec_amount: { value: string; scale: string; currency_code: string };
|
rec_amount: { value: string; scale: string; currency_code: string };
|
||||||
|
|
||||||
retention_code: string;
|
retention_code: string | null;
|
||||||
retention_percentage: { value: string; scale: string };
|
retention_percentage: { value: string; scale: string };
|
||||||
retention_amount: { value: string; scale: string; currency_code: string };
|
retention_amount: { value: string; scale: string; currency_code: string };
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
import { maybeToEmptyPercentageObjectString, maybeToEmptyString } from "@repo/rdx-ddd";
|
import { maybeToEmptyPercentageObjectString, maybeToNullable } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
import type { IssuedInvoiceTax, IssuedInvoiceTaxes } from "../../../../domain";
|
import type { IssuedInvoiceTax, IssuedInvoiceTaxes } from "../../../../domain";
|
||||||
|
|
||||||
@ -19,11 +19,11 @@ export class IssuedInvoiceTaxesFullSnapshotBuilder
|
|||||||
iva_percentage: invoiceTax.ivaPercentage.toObjectString(),
|
iva_percentage: invoiceTax.ivaPercentage.toObjectString(),
|
||||||
iva_amount: invoiceTax.ivaAmount.toObjectString(),
|
iva_amount: invoiceTax.ivaAmount.toObjectString(),
|
||||||
|
|
||||||
rec_code: maybeToEmptyString(invoiceTax.recCode),
|
rec_code: maybeToNullable(invoiceTax.recCode, (v) => v.toString()),
|
||||||
rec_percentage: maybeToEmptyPercentageObjectString(invoiceTax.recPercentage),
|
rec_percentage: maybeToEmptyPercentageObjectString(invoiceTax.recPercentage),
|
||||||
rec_amount: invoiceTax.recAmount.toObjectString(),
|
rec_amount: invoiceTax.recAmount.toObjectString(),
|
||||||
|
|
||||||
retention_code: maybeToEmptyString(invoiceTax.retentionCode),
|
retention_code: maybeToNullable(invoiceTax.retentionCode, (v) => v.toString()),
|
||||||
retention_percentage: maybeToEmptyPercentageObjectString(invoiceTax.retentionPercentage),
|
retention_percentage: maybeToEmptyPercentageObjectString(invoiceTax.retentionPercentage),
|
||||||
retention_amount: invoiceTax.retentionAmount.toObjectString(),
|
retention_amount: invoiceTax.retentionAmount.toObjectString(),
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Fijarse en VerifactuRecordSchema
|
||||||
|
*/
|
||||||
|
|
||||||
export interface IIssuedInvoiceVerifactuFullSnapshot {
|
export interface IIssuedInvoiceVerifactuFullSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
status: string;
|
status: string;
|
||||||
url: string;
|
url: string | null;
|
||||||
qr_code: string;
|
qr_code: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
import { VERIFACTU_RECORD_STATUS } from "@erp/customer-invoices/api/domain";
|
||||||
|
import { maybeToEmptyString, maybeToNullable } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
import type { IssuedInvoiceSummary } from "../../models";
|
import type { IssuedInvoiceSummary } from "../../models";
|
||||||
|
|
||||||
@ -13,19 +14,24 @@ export class IssuedInvoiceSummarySnapshotBuilder implements IIssuedInvoiceSummar
|
|||||||
const recipient = invoice.recipient.toObjectString();
|
const recipient = invoice.recipient.toObjectString();
|
||||||
|
|
||||||
const verifactu = invoice.verifactu.match(
|
const verifactu = invoice.verifactu.match(
|
||||||
(v) => v.toObjectString(),
|
(v) => {
|
||||||
|
return {
|
||||||
|
status: v.estado.toString(),
|
||||||
|
url: maybeToNullable(v.url, (u) => u.toString()),
|
||||||
|
qr_code: maybeToNullable(v.qrCode, (q) => q.toString()),
|
||||||
|
};
|
||||||
|
},
|
||||||
() => ({
|
() => ({
|
||||||
status: "",
|
status: VERIFACTU_RECORD_STATUS.PENDIENTE,
|
||||||
url: "",
|
url: null,
|
||||||
qr_code: "",
|
qr_code: null,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: invoice.id.toString(),
|
id: invoice.id.toString(),
|
||||||
company_id: invoice.companyId.toString(),
|
company_id: invoice.companyId.toString(),
|
||||||
|
is_proforma: invoice.isProforma,
|
||||||
customer_id: invoice.customerId.toString(),
|
|
||||||
|
|
||||||
invoice_number: invoice.invoiceNumber.toString(),
|
invoice_number: invoice.invoiceNumber.toString(),
|
||||||
status: invoice.status.toPrimitive(),
|
status: invoice.status.toPrimitive(),
|
||||||
@ -36,7 +42,12 @@ export class IssuedInvoiceSummarySnapshotBuilder implements IIssuedInvoiceSummar
|
|||||||
reference: maybeToEmptyString(invoice.reference, (v) => v.toString()),
|
reference: maybeToEmptyString(invoice.reference, (v) => v.toString()),
|
||||||
description: maybeToEmptyString(invoice.description, (v) => v.toString()),
|
description: maybeToEmptyString(invoice.description, (v) => v.toString()),
|
||||||
|
|
||||||
recipient,
|
customer_id: invoice.customerId.toString(),
|
||||||
|
|
||||||
|
recipient: {
|
||||||
|
id: invoice.customerId.toString(),
|
||||||
|
...recipient,
|
||||||
|
},
|
||||||
|
|
||||||
language_code: invoice.languageCode.code,
|
language_code: invoice.languageCode.code,
|
||||||
currency_code: invoice.currencyCode.code,
|
currency_code: invoice.currencyCode.code,
|
||||||
@ -49,9 +60,7 @@ export class IssuedInvoiceSummarySnapshotBuilder implements IIssuedInvoiceSummar
|
|||||||
|
|
||||||
verifactu,
|
verifactu,
|
||||||
|
|
||||||
metadata: {
|
linked_proforma_id: maybeToNullable(invoice.linkedProformaId, (v) => v.toString()),
|
||||||
entity: "issued-invoice",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Fijarse en ListIssuedInvoicesResponseDTO["items"]
|
||||||
|
*/
|
||||||
export interface IIssuedInvoiceSummarySnapshot {
|
export interface IIssuedInvoiceSummarySnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
company_id: string;
|
company_id: string;
|
||||||
|
is_proforma: boolean;
|
||||||
customer_id: string;
|
|
||||||
|
|
||||||
invoice_number: string;
|
invoice_number: string;
|
||||||
status: string;
|
status: string;
|
||||||
@ -14,18 +16,20 @@ export interface IIssuedInvoiceSummarySnapshot {
|
|||||||
language_code: string;
|
language_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
|
||||||
reference: string;
|
reference: string | null;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
|
customer_id: string;
|
||||||
recipient: {
|
recipient: {
|
||||||
|
id: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
name: string;
|
name: string;
|
||||||
street: string;
|
street: string | null;
|
||||||
street2: string;
|
street2: string | null;
|
||||||
city: string;
|
city: string | null;
|
||||||
postal_code: string;
|
postal_code: string | null;
|
||||||
province: string;
|
province: string | null;
|
||||||
country: string;
|
country: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
subtotal_amount: { value: string; scale: string; currency_code: string };
|
subtotal_amount: { value: string; scale: string; currency_code: string };
|
||||||
@ -36,9 +40,9 @@ export interface IIssuedInvoiceSummarySnapshot {
|
|||||||
|
|
||||||
verifactu: {
|
verifactu: {
|
||||||
status: string;
|
status: string;
|
||||||
url: string;
|
url: string | null;
|
||||||
qr_code: string;
|
qr_code: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
metadata?: Record<string, string>;
|
linked_proforma_id: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { JsonTaxCatalogProvider } from "@erp/core";
|
import { type JsonTaxCatalogProvider, NumberHelper } from "@erp/core";
|
||||||
import { DiscountPercentage, Tax } from "@erp/core/api";
|
import { DiscountPercentage } from "@erp/core/api";
|
||||||
import {
|
import {
|
||||||
CurrencyCode,
|
CurrencyCode,
|
||||||
DomainError,
|
DomainError,
|
||||||
@ -15,7 +15,7 @@ import {
|
|||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../common";
|
import type { CreateProformaRequestDTO } from "../../../../common";
|
||||||
import {
|
import {
|
||||||
type IProformaCreateProps,
|
type IProformaCreateProps,
|
||||||
type IProformaItemCreateProps,
|
type IProformaItemCreateProps,
|
||||||
@ -27,6 +27,7 @@ import {
|
|||||||
ItemAmount,
|
ItemAmount,
|
||||||
ItemDescription,
|
ItemDescription,
|
||||||
ItemQuantity,
|
ItemQuantity,
|
||||||
|
ProformaItemTaxes,
|
||||||
type ProformaItemTaxesProps,
|
type ProformaItemTaxesProps,
|
||||||
} from "../../../domain";
|
} from "../../../domain";
|
||||||
|
|
||||||
@ -221,19 +222,25 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
|
|||||||
);
|
);
|
||||||
|
|
||||||
const quantity = extractOrPushError(
|
const quantity = extractOrPushError(
|
||||||
maybeFromNullableResult(item.quantity, (v) => ItemQuantity.create(v)),
|
maybeFromNullableResult(item.quantity, (v) =>
|
||||||
|
ItemQuantity.create({ value: NumberHelper.toSafeNumber(v) })
|
||||||
|
),
|
||||||
`items[${index}].quantity`,
|
`items[${index}].quantity`,
|
||||||
params.errors
|
params.errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const unitAmount = extractOrPushError(
|
const unitAmount = extractOrPushError(
|
||||||
maybeFromNullableResult(item.unit_amount, (v) => ItemAmount.create(v)),
|
maybeFromNullableResult(item.unit_amount, (v) =>
|
||||||
|
ItemAmount.create({ value: NumberHelper.toSafeNumber(v) })
|
||||||
|
),
|
||||||
`items[${index}].unit_amount`,
|
`items[${index}].unit_amount`,
|
||||||
params.errors
|
params.errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const discountPercentage = extractOrPushError(
|
const discountPercentage = extractOrPushError(
|
||||||
maybeFromNullableResult(item.item_discount_percentage, (v) => DiscountPercentage.create(v)),
|
maybeFromNullableResult(item.item_discount_percentage, (v) =>
|
||||||
|
DiscountPercentage.create({ value: NumberHelper.toSafeNumber(v.value) })
|
||||||
|
),
|
||||||
`items[${index}].discount_percentage`,
|
`items[${index}].discount_percentage`,
|
||||||
params.errors
|
params.errors
|
||||||
);
|
);
|
||||||
@ -264,10 +271,14 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
|
|||||||
/* Devuelve las propiedades de los impustos de una línea de detalle */
|
/* Devuelve las propiedades de los impustos de una línea de detalle */
|
||||||
|
|
||||||
private mapTaxesProps(
|
private mapTaxesProps(
|
||||||
taxesDTO: Pick<CreateProformaItemRequestDTO, "taxes">["taxes"],
|
taxesDTO: NonNullable<CreateProformaRequestDTO["items"]>[number]["taxes"],
|
||||||
params: { itemIndex: number; errors: ValidationErrorDetail[] }
|
params: { itemIndex: number; errors: ValidationErrorDetail[] }
|
||||||
): ProformaItemTaxesProps {
|
): ProformaItemTaxesProps {
|
||||||
const { itemIndex, errors } = params;
|
// TODO: POR AHORA SE QUEDA ASÍ
|
||||||
|
|
||||||
|
return ProformaItemTaxes.empty().getProps();
|
||||||
|
|
||||||
|
/*const { itemIndex, errors } = params;
|
||||||
|
|
||||||
const taxesProps: ProformaItemTaxesProps = {
|
const taxesProps: ProformaItemTaxesProps = {
|
||||||
iva: Maybe.none(),
|
iva: Maybe.none(),
|
||||||
@ -275,7 +286,6 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
|
|||||||
rec: Maybe.none(),
|
rec: Maybe.none(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Normaliza: "" -> []
|
|
||||||
const taxStrCodes = taxesDTO
|
const taxStrCodes = taxesDTO
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
@ -328,5 +338,6 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
|
|||||||
this.throwIfValidationErrors(errors);
|
this.throwIfValidationErrors(errors);
|
||||||
|
|
||||||
return taxesProps;
|
return taxesProps;
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,8 @@ import {
|
|||||||
ItemDescription,
|
ItemDescription,
|
||||||
ItemQuantity,
|
ItemQuantity,
|
||||||
type ProformaItemPatchProps,
|
type ProformaItemPatchProps,
|
||||||
|
ProformaItemTaxes,
|
||||||
|
type ProformaItemTaxesProps,
|
||||||
type ProformaPatchProps,
|
type ProformaPatchProps,
|
||||||
} from "../../../domain";
|
} from "../../../domain";
|
||||||
|
|
||||||
@ -198,10 +200,10 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
|
|||||||
params.errors
|
params.errors
|
||||||
);
|
);
|
||||||
|
|
||||||
/*const taxes = this.mapTaxesProps(item.taxes, {
|
const taxes = this.mapTaxesProps(item.taxes, {
|
||||||
itemIndex: index,
|
itemIndex: index,
|
||||||
errors: params.errors,
|
errors: params.errors,
|
||||||
});*/
|
});
|
||||||
|
|
||||||
this.throwIfValidationErrors(params.errors);
|
this.throwIfValidationErrors(params.errors);
|
||||||
|
|
||||||
@ -210,20 +212,24 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
|
|||||||
quantity: quantity!,
|
quantity: quantity!,
|
||||||
unitAmount: unitAmount!,
|
unitAmount: unitAmount!,
|
||||||
itemDiscountPercentage: discountPercentage!,
|
itemDiscountPercentage: discountPercentage!,
|
||||||
//taxes,
|
taxes,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return itemsProps;
|
return itemsProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Devuelve las propiedades de los impustos de una línea de detalle */
|
/* Devuelve las propiedades de los impuestos de una línea de detalle */
|
||||||
|
|
||||||
/*private mapTaxesProps(
|
private mapTaxesProps(
|
||||||
taxesDTO: Pick<ProformaItemRequestDTO, "taxes">["taxes"],
|
taxesDTO: NonNullable<UpdateProformaByIdRequestDTO["items"]>[number]["taxes"],
|
||||||
params: { itemIndex: number; errors: ValidationErrorDetail[] }
|
params: { itemIndex: number; errors: ValidationErrorDetail[] }
|
||||||
): ProformaItemTaxesProps {
|
): ProformaItemTaxesProps {
|
||||||
const { itemIndex, errors } = params;
|
// TODO: POR AHORA SE QUEDA ASÍ
|
||||||
|
|
||||||
|
return ProformaItemTaxes.empty().getProps();
|
||||||
|
|
||||||
|
/*const { itemIndex, errors } = params;
|
||||||
|
|
||||||
const taxesProps: ProformaItemTaxesProps = {
|
const taxesProps: ProformaItemTaxesProps = {
|
||||||
iva: Maybe.none(),
|
iva: Maybe.none(),
|
||||||
@ -231,7 +237,6 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
|
|||||||
rec: Maybe.none(),
|
rec: Maybe.none(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Normaliza: "" -> []
|
|
||||||
const taxStrCodes = taxesDTO
|
const taxStrCodes = taxesDTO
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
@ -283,6 +288,6 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
|
|||||||
|
|
||||||
this.throwIfValidationErrors(errors);
|
this.throwIfValidationErrors(errors);
|
||||||
|
|
||||||
return taxesProps;
|
return taxesProps;*/
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
import { maybeToNullable } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
import type { Proforma } from "../../../../domain";
|
import type { Proforma } from "../../../../domain";
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
|
|||||||
payment_description,
|
payment_description,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
() => undefined
|
() => null
|
||||||
);
|
);
|
||||||
|
|
||||||
const allTotals = proforma.totals();
|
const allTotals = proforma.totals();
|
||||||
@ -40,16 +40,17 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
|
|||||||
id: proforma.id.toString(),
|
id: proforma.id.toString(),
|
||||||
company_id: proforma.companyId.toString(),
|
company_id: proforma.companyId.toString(),
|
||||||
|
|
||||||
|
is_proforma: true,
|
||||||
invoice_number: proforma.invoiceNumber.toString(),
|
invoice_number: proforma.invoiceNumber.toString(),
|
||||||
status: proforma.status.toPrimitive(),
|
status: proforma.status.toPrimitive(),
|
||||||
series: maybeToEmptyString(proforma.series, (value) => value.toString()),
|
series: maybeToNullable(proforma.series, (value) => value.toString()),
|
||||||
|
|
||||||
invoice_date: proforma.invoiceDate.toDateString(),
|
invoice_date: proforma.invoiceDate.toDateString(),
|
||||||
operation_date: maybeToEmptyString(proforma.operationDate, (value) => value.toDateString()),
|
operation_date: maybeToNullable(proforma.operationDate, (value) => value.toDateString()),
|
||||||
|
|
||||||
reference: maybeToEmptyString(proforma.reference, (value) => value.toString()),
|
reference: maybeToNullable(proforma.reference, (value) => value.toString()),
|
||||||
description: maybeToEmptyString(proforma.description, (value) => value.toString()),
|
description: maybeToNullable(proforma.description, (value) => value.toString()),
|
||||||
notes: maybeToEmptyString(proforma.notes, (value) => value.toString()),
|
notes: maybeToNullable(proforma.notes, (value) => value.toString()),
|
||||||
|
|
||||||
language_code: proforma.languageCode.toString(),
|
language_code: proforma.languageCode.toString(),
|
||||||
currency_code: proforma.currencyCode.toString(),
|
currency_code: proforma.currencyCode.toString(),
|
||||||
@ -57,6 +58,8 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
|
|||||||
customer_id: proforma.customerId.toString(),
|
customer_id: proforma.customerId.toString(),
|
||||||
recipient,
|
recipient,
|
||||||
|
|
||||||
|
linked_invoice_id: maybeToNullable(proforma.linkedInvoiceId, (value) => value.toString()),
|
||||||
|
|
||||||
payment_method: payment,
|
payment_method: payment,
|
||||||
|
|
||||||
subtotal_amount: allTotals.subtotalAmount.toObjectString(),
|
subtotal_amount: allTotals.subtotalAmount.toObjectString(),
|
||||||
|
|||||||
@ -2,20 +2,25 @@ import type { IProformaItemFullSnapshot } from "./proforma-item-full-snapshot.in
|
|||||||
import type { IProformaRecipientFullSnapshot } from "./proforma-recipient-full-snapshot.interface";
|
import type { IProformaRecipientFullSnapshot } from "./proforma-recipient-full-snapshot.interface";
|
||||||
import type { IProformaTaxFullSnapshot } from "./proforma-tax-full-snapshot-interface";
|
import type { IProformaTaxFullSnapshot } from "./proforma-tax-full-snapshot-interface";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fijarse en GetProformaByIdResponseDTO
|
||||||
|
*/
|
||||||
|
|
||||||
export interface IProformaFullSnapshot {
|
export interface IProformaFullSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
company_id: string;
|
company_id: string;
|
||||||
|
|
||||||
|
is_proforma: boolean;
|
||||||
invoice_number: string;
|
invoice_number: string;
|
||||||
status: string;
|
status: string;
|
||||||
series: string;
|
series: string | null;
|
||||||
|
|
||||||
invoice_date: string;
|
invoice_date: string;
|
||||||
operation_date: string;
|
operation_date: string | null;
|
||||||
|
|
||||||
reference: string;
|
reference: string | null;
|
||||||
description: string;
|
description: string | null;
|
||||||
notes: string;
|
notes: string | null;
|
||||||
|
|
||||||
language_code: string;
|
language_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
@ -23,10 +28,14 @@ export interface IProformaFullSnapshot {
|
|||||||
customer_id: string;
|
customer_id: string;
|
||||||
recipient: IProformaRecipientFullSnapshot;
|
recipient: IProformaRecipientFullSnapshot;
|
||||||
|
|
||||||
payment_method?: {
|
linked_invoice_id: string | null;
|
||||||
|
|
||||||
|
taxes: IProformaTaxFullSnapshot[];
|
||||||
|
|
||||||
|
payment_method: {
|
||||||
payment_id: string;
|
payment_id: string;
|
||||||
payment_description: string;
|
payment_description: string;
|
||||||
};
|
} | null;
|
||||||
|
|
||||||
subtotal_amount: { value: string; scale: string; currency_code: string };
|
subtotal_amount: { value: string; scale: string; currency_code: string };
|
||||||
items_discount_amount: { value: string; scale: string; currency_code: string };
|
items_discount_amount: { value: string; scale: string; currency_code: string };
|
||||||
@ -45,9 +54,7 @@ export interface IProformaFullSnapshot {
|
|||||||
taxes_amount: { value: string; scale: string; currency_code: string };
|
taxes_amount: { value: string; scale: string; currency_code: string };
|
||||||
total_amount: { value: string; scale: string; currency_code: string };
|
total_amount: { value: string; scale: string; currency_code: string };
|
||||||
|
|
||||||
taxes: IProformaTaxFullSnapshot[];
|
|
||||||
|
|
||||||
items: IProformaItemFullSnapshot[];
|
items: IProformaItemFullSnapshot[];
|
||||||
|
|
||||||
metadata?: Record<string, string>;
|
metadata: Record<string, string> | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
export interface IProformaItemFullSnapshot {
|
export interface IProformaItemFullSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
is_valued: string;
|
is_valued: boolean;
|
||||||
position: string;
|
position: number;
|
||||||
description: string;
|
description: string | null;
|
||||||
|
|
||||||
quantity: { value: string; scale: string };
|
quantity: { value: string; scale: string };
|
||||||
unit_amount: { value: string; scale: string; currency_code: string };
|
unit_amount: { value: string; scale: string; currency_code: string };
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
maybeToEmptyPercentageObjectString,
|
maybeToEmptyPercentageObjectString,
|
||||||
maybeToEmptyQuantityObjectString,
|
maybeToEmptyQuantityObjectString,
|
||||||
maybeToEmptyString,
|
maybeToEmptyString,
|
||||||
|
maybeToNullable,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
|
|
||||||
import { ItemAmount, type ProformaItem, type ProformaItems } from "../../../../domain";
|
import { ItemAmount, type ProformaItem, type ProformaItems } from "../../../../domain";
|
||||||
@ -20,10 +21,10 @@ export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnaps
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: proformaItem.id.toPrimitive(),
|
id: proformaItem.id.toPrimitive(),
|
||||||
is_valued: String(isValued),
|
is_valued: isValued,
|
||||||
position: String(index),
|
position: index,
|
||||||
|
|
||||||
description: maybeToEmptyString(proformaItem.description, (value) => value.toString()),
|
description: maybeToNullable(proformaItem.description, (value) => value.toString()),
|
||||||
|
|
||||||
quantity: maybeToEmptyQuantityObjectString(proformaItem.quantity),
|
quantity: maybeToEmptyQuantityObjectString(proformaItem.quantity),
|
||||||
unit_amount: maybeToEmptyMoneyObjectString(proformaItem.unitAmount),
|
unit_amount: maybeToEmptyMoneyObjectString(proformaItem.unitAmount),
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
import { DomainValidationError, maybeToEmptyString } from "@repo/rdx-ddd";
|
import { DomainValidationError, maybeToNullable } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
import type { InvoiceRecipient, Proforma } from "../../../../domain";
|
import type { InvoiceRecipient, Proforma } from "../../../../domain";
|
||||||
|
|
||||||
@ -21,24 +21,25 @@ export class ProformaRecipientFullSnapshotBuilder implements IProformaRecipientF
|
|||||||
id: proforma.customerId.toString(),
|
id: proforma.customerId.toString(),
|
||||||
name: recipient.name.toString(),
|
name: recipient.name.toString(),
|
||||||
tin: recipient.tin.toString(),
|
tin: recipient.tin.toString(),
|
||||||
street: maybeToEmptyString(recipient.street, (v) => v.toString()),
|
street: maybeToNullable(recipient.street, (v) => v.toString()),
|
||||||
street2: maybeToEmptyString(recipient.street2, (v) => v.toString()),
|
street2: maybeToNullable(recipient.street2, (v) => v.toString()),
|
||||||
city: maybeToEmptyString(recipient.city, (v) => v.toString()),
|
city: maybeToNullable(recipient.city, (v) => v.toString()),
|
||||||
province: maybeToEmptyString(recipient.province, (v) => v.toString()),
|
province: maybeToNullable(recipient.province, (v) => v.toString()),
|
||||||
postal_code: maybeToEmptyString(recipient.postalCode, (v) => v.toString()),
|
postal_code: maybeToNullable(recipient.postalCode, (v) => v.toString()),
|
||||||
country: maybeToEmptyString(recipient.country, (v) => v.toString()),
|
country: maybeToNullable(recipient.country, (v) => v.toString()),
|
||||||
}),
|
}),
|
||||||
() => ({
|
() =>
|
||||||
id: "",
|
({
|
||||||
name: "",
|
id: null,
|
||||||
tin: "",
|
name: null,
|
||||||
street: "",
|
tin: null,
|
||||||
street2: "",
|
street: null,
|
||||||
city: "",
|
street2: null,
|
||||||
province: "",
|
city: null,
|
||||||
postal_code: "",
|
province: null,
|
||||||
country: "",
|
postal_code: null,
|
||||||
})
|
country: null,
|
||||||
|
}) as IProformaRecipientFullSnapshot
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Fijarse en ProformaRecipientSummarySchema
|
||||||
|
*/
|
||||||
|
|
||||||
export interface IProformaRecipientFullSnapshot {
|
export interface IProformaRecipientFullSnapshot {
|
||||||
id: string;
|
id: string | null;
|
||||||
name: string;
|
name: string | null;
|
||||||
tin: string;
|
tin: string | null;
|
||||||
street: string;
|
street: string | null;
|
||||||
street2: string;
|
street2: string | null;
|
||||||
city: string;
|
city: string | null;
|
||||||
province: string;
|
province: string | null;
|
||||||
postal_code: string;
|
postal_code: string | null;
|
||||||
country: string;
|
country: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
import { maybeToEmptyString, maybeToNullable } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
import type { ProformaSummary } from "../../models";
|
import type { ProformaSummary } from "../../models";
|
||||||
|
|
||||||
@ -15,18 +15,22 @@ export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotB
|
|||||||
return {
|
return {
|
||||||
id: proforma.id.toString(),
|
id: proforma.id.toString(),
|
||||||
company_id: proforma.companyId.toString(),
|
company_id: proforma.companyId.toString(),
|
||||||
customer_id: proforma.customerId.toString(),
|
is_proforma: proforma.isProforma,
|
||||||
|
|
||||||
invoice_number: proforma.invoiceNumber.toString(),
|
invoice_number: proforma.invoiceNumber.toString(),
|
||||||
status: proforma.status.toPrimitive(),
|
status: proforma.status.toPrimitive(),
|
||||||
series: maybeToEmptyString(proforma.series, (value) => value.toString()),
|
series: maybeToNullable(proforma.series, (value) => value.toString()),
|
||||||
|
|
||||||
invoice_date: proforma.invoiceDate.toDateString(),
|
invoice_date: proforma.invoiceDate.toDateString(),
|
||||||
operation_date: maybeToEmptyString(proforma.operationDate, (value) => value.toDateString()),
|
operation_date: maybeToNullable(proforma.operationDate, (value) => value.toDateString()),
|
||||||
reference: maybeToEmptyString(proforma.reference, (value) => value.toString()),
|
reference: maybeToEmptyString(proforma.reference, (value) => value.toString()),
|
||||||
description: maybeToEmptyString(proforma.description, (value) => value.toString()),
|
description: maybeToNullable(proforma.description, (value) => value.toString()),
|
||||||
|
|
||||||
recipient,
|
customer_id: proforma.customerId.toString(),
|
||||||
|
recipient: {
|
||||||
|
id: proforma.customerId.toString(),
|
||||||
|
...recipient,
|
||||||
|
},
|
||||||
|
|
||||||
language_code: proforma.languageCode.code,
|
language_code: proforma.languageCode.code,
|
||||||
currency_code: proforma.currencyCode.code,
|
currency_code: proforma.currencyCode.code,
|
||||||
@ -37,11 +41,7 @@ export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotB
|
|||||||
taxes_amount: proforma.taxesAmount.toObjectString(),
|
taxes_amount: proforma.taxesAmount.toObjectString(),
|
||||||
total_amount: proforma.totalAmount.toObjectString(),
|
total_amount: proforma.totalAmount.toObjectString(),
|
||||||
|
|
||||||
linked_invoice_id: maybeToEmptyString(proforma.linkedInvoiceId, (value) => value.toString()),
|
linked_invoice_id: maybeToNullable(proforma.linkedInvoiceId, (value) => value.toString()),
|
||||||
|
|
||||||
metadata: {
|
|
||||||
entity: "proforma",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,31 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Fijarse en ListProformasResponseDTO["items"]
|
||||||
|
*/
|
||||||
export interface IProformaSummarySnapshot {
|
export interface IProformaSummarySnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
company_id: string;
|
company_id: string;
|
||||||
|
is_proforma: boolean;
|
||||||
customer_id: string;
|
|
||||||
|
|
||||||
invoice_number: string;
|
invoice_number: string;
|
||||||
status: string;
|
status: string;
|
||||||
series: string;
|
series: string | null;
|
||||||
|
|
||||||
invoice_date: string;
|
invoice_date: string;
|
||||||
operation_date: string;
|
operation_date: string | null;
|
||||||
|
|
||||||
language_code: string;
|
language_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
|
||||||
reference: string;
|
reference: string | null;
|
||||||
description: string;
|
description: string | null;
|
||||||
|
|
||||||
|
customer_id: string;
|
||||||
recipient: {
|
recipient: {
|
||||||
|
id: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
name: string;
|
name: string;
|
||||||
street: string;
|
street: string | null;
|
||||||
street2: string;
|
street2: string | null;
|
||||||
city: string;
|
city: string | null;
|
||||||
postal_code: string;
|
postal_code: string | null;
|
||||||
province: string;
|
province: string | null;
|
||||||
country: string;
|
country: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
subtotal_amount: { value: string; scale: string; currency_code: string };
|
subtotal_amount: { value: string; scale: string; currency_code: string };
|
||||||
@ -34,7 +38,5 @@ export interface IProformaSummarySnapshot {
|
|||||||
taxes_amount: { value: string; scale: string; currency_code: string };
|
taxes_amount: { value: string; scale: string; currency_code: string };
|
||||||
total_amount: { value: string; scale: string; currency_code: string };
|
total_amount: { value: string; scale: string; currency_code: string };
|
||||||
|
|
||||||
linked_invoice_id: string;
|
linked_invoice_id: string | null;
|
||||||
|
|
||||||
metadata?: Record<string, string>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
type Street,
|
type Street,
|
||||||
type TINNumber,
|
type TINNumber,
|
||||||
ValueObject,
|
ValueObject,
|
||||||
maybeToEmptyString,
|
maybeToNullable,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { type Maybe, Result } from "@repo/rdx-utils";
|
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
@ -90,12 +90,12 @@ export class InvoiceRecipient extends ValueObject<InvoiceRecipientProps> {
|
|||||||
return {
|
return {
|
||||||
tin: this.tin.toString(),
|
tin: this.tin.toString(),
|
||||||
name: this.name.toString(),
|
name: this.name.toString(),
|
||||||
street: maybeToEmptyString(this.street, (value) => value.toString()),
|
street: maybeToNullable(this.street, (value) => value.toString()),
|
||||||
street2: maybeToEmptyString(this.street2, (value) => value.toString()),
|
street2: maybeToNullable(this.street2, (value) => value.toString()),
|
||||||
city: maybeToEmptyString(this.city, (value) => value.toString()),
|
city: maybeToNullable(this.city, (value) => value.toString()),
|
||||||
postal_code: maybeToEmptyString(this.postalCode, (value) => value.toString()),
|
postal_code: maybeToNullable(this.postalCode, (value) => value.toString()),
|
||||||
province: maybeToEmptyString(this.province, (value) => value.toString()),
|
province: maybeToNullable(this.province, (value) => value.toString()),
|
||||||
country: maybeToEmptyString(this.country, (value) => value.toString()),
|
country: maybeToNullable(this.country, (value) => value.toString()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,19 +32,19 @@ export interface IIssuedInvoiceCreateProps {
|
|||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
status: InvoiceStatus;
|
status: InvoiceStatus;
|
||||||
|
|
||||||
proformaId: UniqueID; // <- id de la proforma padre en caso de issue
|
linkedProformaId: Maybe<UniqueID>; // <- id de la proforma padre en caso de issue
|
||||||
|
|
||||||
series: Maybe<InvoiceSerie>;
|
series: InvoiceSerie;
|
||||||
invoiceNumber: InvoiceNumber;
|
invoiceNumber: InvoiceNumber;
|
||||||
|
|
||||||
invoiceDate: UtcDate;
|
invoiceDate: UtcDate;
|
||||||
operationDate: Maybe<UtcDate>;
|
operationDate: Maybe<UtcDate>;
|
||||||
|
|
||||||
customerId: UniqueID;
|
customerId: UniqueID;
|
||||||
recipient: Maybe<InvoiceRecipient>;
|
recipient: InvoiceRecipient;
|
||||||
|
|
||||||
reference: Maybe<string>;
|
reference: Maybe<string>;
|
||||||
description: Maybe<string>;
|
description: string;
|
||||||
notes: Maybe<TextValue>;
|
notes: Maybe<TextValue>;
|
||||||
|
|
||||||
languageCode: LanguageCode;
|
languageCode: LanguageCode;
|
||||||
@ -178,15 +178,15 @@ export class IssuedInvoice
|
|||||||
return this.props.customerId;
|
return this.props.customerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get proformaId(): UniqueID {
|
public get linkedProformaId(): Maybe<UniqueID> {
|
||||||
return this.props.proformaId;
|
return this.props.linkedProformaId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get status(): InvoiceStatus {
|
public get status(): InvoiceStatus {
|
||||||
return this.props.status;
|
return this.props.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get series(): Maybe<InvoiceSerie> {
|
public get series(): InvoiceSerie {
|
||||||
return this.props.series;
|
return this.props.series;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,7 +206,7 @@ export class IssuedInvoice
|
|||||||
return this.props.reference;
|
return this.props.reference;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get description(): Maybe<string> {
|
public get description(): string {
|
||||||
return this.props.description;
|
return this.props.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,7 +214,7 @@ export class IssuedInvoice
|
|||||||
return this.props.notes;
|
return this.props.notes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get recipient(): Maybe<InvoiceRecipient> {
|
public get recipient(): InvoiceRecipient {
|
||||||
return this.props.recipient;
|
return this.props.recipient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,10 +287,6 @@ export class IssuedInvoice
|
|||||||
return this._items;
|
return this._items;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get hasRecipient() {
|
|
||||||
return this.recipient.isSome();
|
|
||||||
}
|
|
||||||
|
|
||||||
public get hasPaymentMethod() {
|
public get hasPaymentMethod() {
|
||||||
return this.paymentMethod.isSome();
|
return this.paymentMethod.isSome();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,8 +35,8 @@ export interface IProformaCreateProps {
|
|||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
status: InvoiceStatus;
|
status: InvoiceStatus;
|
||||||
|
|
||||||
series: Maybe<InvoiceSerie>;
|
|
||||||
invoiceNumber: InvoiceNumber;
|
invoiceNumber: InvoiceNumber;
|
||||||
|
series: Maybe<InvoiceSerie>;
|
||||||
|
|
||||||
invoiceDate: UtcDate;
|
invoiceDate: UtcDate;
|
||||||
operationDate: Maybe<UtcDate>;
|
operationDate: Maybe<UtcDate>;
|
||||||
@ -51,6 +51,8 @@ export interface IProformaCreateProps {
|
|||||||
languageCode: LanguageCode;
|
languageCode: LanguageCode;
|
||||||
currencyCode: CurrencyCode;
|
currencyCode: CurrencyCode;
|
||||||
|
|
||||||
|
linkedInvoiceId: Maybe<UniqueID>;
|
||||||
|
|
||||||
paymentMethod: Maybe<InvoicePaymentMethod>;
|
paymentMethod: Maybe<InvoicePaymentMethod>;
|
||||||
|
|
||||||
items: IProformaItemCreateProps[];
|
items: IProformaItemCreateProps[];
|
||||||
@ -100,17 +102,19 @@ export interface IProforma {
|
|||||||
|
|
||||||
paymentMethod: Maybe<InvoicePaymentMethod>;
|
paymentMethod: Maybe<InvoicePaymentMethod>;
|
||||||
|
|
||||||
|
linkedInvoiceId: Maybe<UniqueID>;
|
||||||
|
|
||||||
items: IProformaItems; // <- Colección
|
items: IProformaItems; // <- Colección
|
||||||
taxes(): Collection<IProformaTaxTotals>;
|
taxes(): Collection<IProformaTaxTotals>;
|
||||||
totals(): IProformaTotals;
|
totals(): IProformaTotals;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InternalProformaProps = Omit<IProformaCreateProps, "items">;
|
export type ProformaInternalProps = Omit<IProformaCreateProps, "items">;
|
||||||
|
|
||||||
export class Proforma extends AggregateRoot<InternalProformaProps> implements IProforma {
|
export class Proforma extends AggregateRoot<ProformaInternalProps> implements IProforma {
|
||||||
private readonly _items: ProformaItems;
|
private _items: ProformaItems;
|
||||||
|
|
||||||
protected constructor(props: InternalProformaProps, items: ProformaItems, id?: UniqueID) {
|
protected constructor(props: ProformaInternalProps, items: ProformaItems, id?: UniqueID) {
|
||||||
super(props, id);
|
super(props, id);
|
||||||
this._items = items;
|
this._items = items;
|
||||||
}
|
}
|
||||||
@ -153,7 +157,7 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rehidratación desde persistencia
|
// Rehidratación desde persistencia
|
||||||
static rehydrate(props: InternalProformaProps, items: ProformaItems, id: UniqueID): Proforma {
|
static rehydrate(props: ProformaInternalProps, items: ProformaItems, id: UniqueID): Proforma {
|
||||||
return new Proforma(props, items, id);
|
return new Proforma(props, items, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +165,7 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
|||||||
public update(patchProps: ProformaPatchProps): Result<Proforma, Error> {
|
public update(patchProps: ProformaPatchProps): Result<Proforma, Error> {
|
||||||
const { items, ...otherProps } = patchProps;
|
const { items, ...otherProps } = patchProps;
|
||||||
|
|
||||||
const candidateProps: InternalProformaProps = {
|
const candidateProps: ProformaInternalProps = {
|
||||||
...this.props,
|
...this.props,
|
||||||
...otherProps,
|
...otherProps,
|
||||||
};
|
};
|
||||||
@ -261,6 +265,10 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
|||||||
return this.props.paymentMethod;
|
return this.props.paymentMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get linkedInvoiceId(): Maybe<UniqueID> {
|
||||||
|
return this.props.linkedInvoiceId;
|
||||||
|
}
|
||||||
|
|
||||||
public get languageCode(): LanguageCode {
|
public get languageCode(): LanguageCode {
|
||||||
return this.props.languageCode;
|
return this.props.languageCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,7 @@ export class GetIssuedInvoiceByIdController extends ExpressController {
|
|||||||
|
|
||||||
return result.match(
|
return result.match(
|
||||||
(data) => {
|
(data) => {
|
||||||
|
console.log(data);
|
||||||
const dto = GetIssuedInvoiceByIdResponseSchema.parse(data);
|
const dto = GetIssuedInvoiceByIdResponseSchema.parse(data);
|
||||||
return this.ok(dto);
|
return this.ok(dto);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -63,20 +63,18 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
|||||||
|
|
||||||
const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors);
|
const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors);
|
||||||
|
|
||||||
// Para issued invoices, proforma_id debe estar relleno
|
const linkedProformaId = extractOrPushError(
|
||||||
const proformaId = extractOrPushError(
|
maybeFromNullableResult(raw.proforma_id, (v) => UniqueID.create(String(v))),
|
||||||
UniqueID.create(String(raw.proforma_id)),
|
|
||||||
"proforma_id",
|
"proforma_id",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors);
|
const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors);
|
||||||
|
|
||||||
const series = extractOrPushError(
|
// En el caso de "serie", al ser un campo opcional en BD
|
||||||
maybeFromNullableResult(raw.series, (v) => InvoiceSerie.create(v)),
|
// pero obligatorio en el dominio,
|
||||||
"series",
|
// se le asignará un valor por defecto (cadena vacía) en caso de venir como nullish, para que así el VO lo valide y lance el error correspondiente. De esta forma evitamos que el mapper devuelva un null o undefined en el campo 'series' del dominio, lo cual podría generar errores difíciles de depurar posteriormente en el código.
|
||||||
errors
|
const series = extractOrPushError(InvoiceSerie.create(raw.series ?? ""), "series", errors);
|
||||||
);
|
|
||||||
|
|
||||||
const invoiceNumber = extractOrPushError(
|
const invoiceNumber = extractOrPushError(
|
||||||
InvoiceNumber.create(raw.invoice_number),
|
InvoiceNumber.create(raw.invoice_number),
|
||||||
@ -117,8 +115,11 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
|||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// En el caso de "description", al ser un campo opcional en BD
|
||||||
|
// pero obligatorio en el dominio,
|
||||||
|
// se le asignará un valor por defecto (cadena vacía) en caso de venir como nullish, para que así el VO lo valide y lance el error correspondiente. De esta forma evitamos que el mapper devuelva un null o undefined en el campo 'series' del dominio, lo cual podría generar errores difíciles de depurar posteriormente en el código.
|
||||||
const description = extractOrPushError(
|
const description = extractOrPushError(
|
||||||
maybeFromNullableResult(raw.description, (value) => Result.ok(String(value))),
|
Result.ok(String(raw.description ?? "")),
|
||||||
"description",
|
"description",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
@ -258,7 +259,6 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
|||||||
invoiceId,
|
invoiceId,
|
||||||
companyId,
|
companyId,
|
||||||
customerId,
|
customerId,
|
||||||
proformaId,
|
|
||||||
status,
|
status,
|
||||||
series,
|
series,
|
||||||
invoiceNumber,
|
invoiceNumber,
|
||||||
@ -282,6 +282,8 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
|||||||
retentionAmount,
|
retentionAmount,
|
||||||
taxesAmount,
|
taxesAmount,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
|
|
||||||
|
linkedProformaId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,7 +352,6 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
|||||||
const invoiceProps: InternalIssuedInvoiceProps = {
|
const invoiceProps: InternalIssuedInvoiceProps = {
|
||||||
companyId: attributes.companyId!,
|
companyId: attributes.companyId!,
|
||||||
|
|
||||||
proformaId: attributes.proformaId!,
|
|
||||||
status: attributes.status!,
|
status: attributes.status!,
|
||||||
series: attributes.series!,
|
series: attributes.series!,
|
||||||
invoiceNumber: attributes.invoiceNumber!,
|
invoiceNumber: attributes.invoiceNumber!,
|
||||||
@ -386,6 +387,8 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
|||||||
|
|
||||||
taxes,
|
taxes,
|
||||||
verifactu,
|
verifactu,
|
||||||
|
|
||||||
|
linkedProformaId: attributes.linkedProformaId!,
|
||||||
};
|
};
|
||||||
|
|
||||||
const invoiceId = attributes.invoiceId!;
|
const invoiceId = attributes.invoiceId!;
|
||||||
@ -464,9 +467,9 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
|||||||
// Flags / estado / serie / número
|
// Flags / estado / serie / número
|
||||||
is_proforma: false,
|
is_proforma: false,
|
||||||
status: source.status.toPrimitive(),
|
status: source.status.toPrimitive(),
|
||||||
proforma_id: source.proformaId.toPrimitive(),
|
proforma_id: maybeToNullable(source.linkedProformaId, (v) => v.toPrimitive()),
|
||||||
|
|
||||||
series: maybeToNullable(source.series, (v) => v.toPrimitive()),
|
series: source.series.toPrimitive(),
|
||||||
invoice_number: source.invoiceNumber.toPrimitive(),
|
invoice_number: source.invoiceNumber.toPrimitive(),
|
||||||
invoice_date: source.invoiceDate.toPrimitive(),
|
invoice_date: source.invoiceDate.toPrimitive(),
|
||||||
operation_date: maybeToNullable(source.operationDate, (v) => v.toPrimitive()),
|
operation_date: maybeToNullable(source.operationDate, (v) => v.toPrimitive()),
|
||||||
@ -474,7 +477,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
|||||||
currency_code: source.currencyCode.toPrimitive(),
|
currency_code: source.currencyCode.toPrimitive(),
|
||||||
|
|
||||||
reference: maybeToNullable(source.reference, (reference) => reference),
|
reference: maybeToNullable(source.reference, (reference) => reference),
|
||||||
description: maybeToNullable(source.description, (description) => description),
|
description: source.description,
|
||||||
notes: maybeToNullable(source.notes, (v) => v.toPrimitive()),
|
notes: maybeToNullable(source.notes, (v) => v.toPrimitive()),
|
||||||
|
|
||||||
payment_method_id: maybeToNullable(
|
payment_method_id: maybeToNullable(
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
maybeFromNullableResult,
|
maybeFromNullableResult,
|
||||||
maybeToNullable,
|
maybeToNullable,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type IIssuedInvoiceCreateProps,
|
type IIssuedInvoiceCreateProps,
|
||||||
@ -26,7 +26,7 @@ export class SequelizeIssuedInvoiceRecipientDomainMapper {
|
|||||||
public mapToDomain(
|
public mapToDomain(
|
||||||
source: CustomerInvoiceModel,
|
source: CustomerInvoiceModel,
|
||||||
params?: MapperParamsType
|
params?: MapperParamsType
|
||||||
): Result<Maybe<InvoiceRecipient>, Error> {
|
): Result<InvoiceRecipient, Error> {
|
||||||
/**
|
/**
|
||||||
* - Issued invoice -> snapshot de los datos (campos customer_*)
|
* - Issued invoice -> snapshot de los datos (campos customer_*)
|
||||||
*/
|
*/
|
||||||
@ -105,7 +105,7 @@ export class SequelizeIssuedInvoiceRecipientDomainMapper {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.ok(Maybe.some(createResult.data));
|
return Result.ok(createResult.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -117,7 +117,7 @@ export class SequelizeIssuedInvoiceRecipientDomainMapper {
|
|||||||
* - Si la factura no es proforma (`isProforma === false`), debe existir `recipient`.
|
* - Si la factura no es proforma (`isProforma === false`), debe existir `recipient`.
|
||||||
* En caso contrario, se agrega un error de validación.
|
* En caso contrario, se agrega un error de validación.
|
||||||
*/
|
*/
|
||||||
mapToPersistence(source: Maybe<InvoiceRecipient>, params?: MapperParamsType) {
|
mapToPersistence(source: InvoiceRecipient, params?: MapperParamsType) {
|
||||||
const { errors, parent } = params as {
|
const { errors, parent } = params as {
|
||||||
parent: IssuedInvoice;
|
parent: IssuedInvoice;
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
@ -140,7 +140,7 @@ export class SequelizeIssuedInvoiceRecipientDomainMapper {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipient = source.unwrap();
|
const recipient = source;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customer_tin: recipient.tin.toPrimitive(),
|
customer_tin: recipient.tin.toPrimitive(),
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export class SequelizeIssuedInvoiceVerifactuDomainMapper extends SequelizeDomain
|
|||||||
Maybe<VerifactuRecord>
|
Maybe<VerifactuRecord>
|
||||||
> {
|
> {
|
||||||
public mapToDomain(
|
public mapToDomain(
|
||||||
source: VerifactuRecordModel,
|
source: VerifactuRecordModel | null | undefined,
|
||||||
params?: MapperParamsType
|
params?: MapperParamsType
|
||||||
): Result<Maybe<VerifactuRecord>, Error> {
|
): Result<Maybe<VerifactuRecord>, Error> {
|
||||||
const { errors, attributes } = params as {
|
const { errors, attributes } = params as {
|
||||||
|
|||||||
@ -110,6 +110,8 @@ export class SequelizeIssuedInvoiceSummaryMapper extends SequelizeQueryMapper<
|
|||||||
taxesAmount: attributes.taxesAmount!,
|
taxesAmount: attributes.taxesAmount!,
|
||||||
totalAmount: attributes.totalAmount!,
|
totalAmount: attributes.totalAmount!,
|
||||||
|
|
||||||
|
linkedProformaId: attributes.linkedProformaId!,
|
||||||
|
|
||||||
verifactu,
|
verifactu,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -221,6 +223,12 @@ export class SequelizeIssuedInvoiceSummaryMapper extends SequelizeQueryMapper<
|
|||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const linkedProformaId = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.proforma_id, (value) => UniqueID.create(value)),
|
||||||
|
"linked_proforma_id",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invoiceId,
|
invoiceId,
|
||||||
companyId,
|
companyId,
|
||||||
@ -241,6 +249,8 @@ export class SequelizeIssuedInvoiceSummaryMapper extends SequelizeQueryMapper<
|
|||||||
taxableAmount,
|
taxableAmount,
|
||||||
taxesAmount,
|
taxesAmount,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
|
|
||||||
|
linkedProformaId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,12 +14,12 @@ import {
|
|||||||
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type InternalProformaProps,
|
|
||||||
InvoiceNumber,
|
InvoiceNumber,
|
||||||
InvoicePaymentMethod,
|
InvoicePaymentMethod,
|
||||||
InvoiceSerie,
|
InvoiceSerie,
|
||||||
InvoiceStatus,
|
InvoiceStatus,
|
||||||
Proforma,
|
Proforma,
|
||||||
|
type ProformaInternalProps,
|
||||||
ProformaItems,
|
ProformaItems,
|
||||||
} from "../../../../../../domain";
|
} from "../../../../../../domain";
|
||||||
import type {
|
import type {
|
||||||
@ -150,6 +150,12 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
|||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const linkedInvoiceId = extractOrPushError(
|
||||||
|
maybeFromNullableResult(raw.linked_invoice?.id, (value) => UniqueID.create(value)),
|
||||||
|
"linked_invoice_id",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invoiceId,
|
invoiceId,
|
||||||
companyId,
|
companyId,
|
||||||
@ -167,6 +173,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
|||||||
paymentMethod,
|
paymentMethod,
|
||||||
|
|
||||||
globalDiscountPercentage,
|
globalDiscountPercentage,
|
||||||
|
linkedInvoiceId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,7 +220,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
|||||||
items: itemCollectionResults.data.getAll(),
|
items: itemCollectionResults.data.getAll(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const invoiceProps: InternalProformaProps = {
|
const invoiceProps: ProformaInternalProps = {
|
||||||
companyId: attributes.companyId!,
|
companyId: attributes.companyId!,
|
||||||
|
|
||||||
status: attributes.status!,
|
status: attributes.status!,
|
||||||
@ -235,6 +242,8 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
|||||||
globalDiscountPercentage: attributes.globalDiscountPercentage!,
|
globalDiscountPercentage: attributes.globalDiscountPercentage!,
|
||||||
|
|
||||||
paymentMethod: attributes.paymentMethod!,
|
paymentMethod: attributes.paymentMethod!,
|
||||||
|
|
||||||
|
linkedInvoiceId: attributes.linkedInvoiceId!, // El id de la factura emitida (linked_invoice) se asigna al hacer issue() desde la proforma, no viene en el modelo de persistencia.
|
||||||
};
|
};
|
||||||
|
|
||||||
const proformaId = attributes.invoiceId!;
|
const proformaId = attributes.invoiceId!;
|
||||||
|
|||||||
@ -58,7 +58,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
|||||||
const { errors, index, parent } = params as {
|
const { errors, index, parent } = params as {
|
||||||
index: number;
|
index: number;
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
parent: Partial<ProformaCreateProps>;
|
parent: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemId = extractOrPushError(
|
const itemId = extractOrPushError(
|
||||||
|
|||||||
@ -303,6 +303,12 @@ export class ProformaRepository
|
|||||||
as: "taxes",
|
as: "taxes",
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
model: CustomerInvoiceModel,
|
||||||
|
as: "linked_invoice",
|
||||||
|
required: false,
|
||||||
|
attributes: ["id"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
transaction,
|
transaction,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./request";
|
export * from "./request";
|
||||||
export * from "./response";
|
export * from "./response";
|
||||||
|
export * from "./shared";
|
||||||
|
|||||||
@ -1,119 +1,68 @@
|
|||||||
import { MetadataSchema, MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
|
import {
|
||||||
|
CurrencyCodeSchema,
|
||||||
|
IsoDateSchema,
|
||||||
|
LanguageCodeSchema,
|
||||||
|
MetadataSchema,
|
||||||
|
MoneySchema,
|
||||||
|
PercentageSchema,
|
||||||
|
} from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import {
|
||||||
|
IssuedInvoiceItemDetailSchema,
|
||||||
|
IssuedInvoiceRecipientSummarySchema,
|
||||||
|
IssuedInvoiceStatusSchema,
|
||||||
|
PaymentMethodRefSchema,
|
||||||
|
TaxesBreakdownSchema,
|
||||||
|
VerifactuRecordSchema,
|
||||||
|
} from "../../shared";
|
||||||
|
|
||||||
export const GetIssuedInvoiceByIdResponseSchema = z.object({
|
export const GetIssuedInvoiceByIdResponseSchema = z.object({
|
||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
company_id: z.uuid(),
|
company_id: z.uuid(),
|
||||||
|
is_proforma: z.boolean(),
|
||||||
|
|
||||||
invoice_number: z.string(),
|
invoice_number: z.string(),
|
||||||
status: z.string(),
|
status: IssuedInvoiceStatusSchema,
|
||||||
series: z.string(),
|
series: z.string(),
|
||||||
|
|
||||||
invoice_date: z.string(),
|
invoice_date: IsoDateSchema,
|
||||||
operation_date: z.string(),
|
operation_date: IsoDateSchema.nullable(),
|
||||||
|
|
||||||
reference: z.string(),
|
language_code: LanguageCodeSchema,
|
||||||
|
currency_code: CurrencyCodeSchema,
|
||||||
|
|
||||||
|
reference: z.string().nullable(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
notes: z.string(),
|
notes: z.string().nullable(),
|
||||||
|
|
||||||
language_code: z.string(),
|
customer_id: z.uuid(),
|
||||||
currency_code: z.string(),
|
recipient: IssuedInvoiceRecipientSummarySchema,
|
||||||
|
|
||||||
customer_id: z.string(),
|
linked_proforma_id: z.uuid().nullable(),
|
||||||
recipient: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
tin: z.string(),
|
|
||||||
street: z.string(),
|
|
||||||
street2: z.string(),
|
|
||||||
city: z.string(),
|
|
||||||
province: z.string(),
|
|
||||||
postal_code: z.string(),
|
|
||||||
country: z.string(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
taxes: z.array(
|
taxes: z.array(TaxesBreakdownSchema),
|
||||||
z.object({
|
|
||||||
taxable_amount: MoneySchema,
|
|
||||||
|
|
||||||
iva_code: z.string(),
|
payment_method: PaymentMethodRefSchema.nullable(),
|
||||||
iva_percentage: PercentageSchema,
|
|
||||||
iva_amount: MoneySchema,
|
|
||||||
|
|
||||||
rec_code: z.string(),
|
|
||||||
rec_percentage: PercentageSchema,
|
|
||||||
rec_amount: MoneySchema,
|
|
||||||
|
|
||||||
retention_code: z.string(),
|
|
||||||
retention_percentage: PercentageSchema,
|
|
||||||
retention_amount: MoneySchema,
|
|
||||||
|
|
||||||
taxes_amount: MoneySchema,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
|
|
||||||
payment_method: z
|
|
||||||
.object({
|
|
||||||
payment_id: z.string(),
|
|
||||||
payment_description: z.string(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
subtotal_amount: MoneySchema,
|
subtotal_amount: MoneySchema,
|
||||||
|
|
||||||
items_discount_amount: MoneySchema,
|
items_discount_amount: MoneySchema,
|
||||||
global_discount_percentage: PercentageSchema,
|
global_discount_percentage: PercentageSchema,
|
||||||
global_discount_amount: MoneySchema,
|
global_discount_amount: MoneySchema,
|
||||||
total_discount_amount: MoneySchema,
|
total_discount_amount: MoneySchema,
|
||||||
|
|
||||||
taxable_amount: MoneySchema,
|
taxable_amount: MoneySchema,
|
||||||
|
|
||||||
iva_amount: MoneySchema,
|
iva_amount: MoneySchema,
|
||||||
rec_amount: MoneySchema,
|
rec_amount: MoneySchema,
|
||||||
retention_amount: MoneySchema,
|
retention_amount: MoneySchema,
|
||||||
|
|
||||||
taxes_amount: MoneySchema,
|
taxes_amount: MoneySchema,
|
||||||
total_amount: MoneySchema,
|
total_amount: MoneySchema,
|
||||||
|
|
||||||
verifactu: z.object({
|
verifactu: VerifactuRecordSchema,
|
||||||
status: z.string(),
|
items: z.array(IssuedInvoiceItemDetailSchema),
|
||||||
url: z.string(),
|
|
||||||
qr_code: z.string(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
items: z.array(
|
|
||||||
z.object({
|
|
||||||
id: z.uuid(),
|
|
||||||
is_valued: z.string(),
|
|
||||||
position: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
|
|
||||||
quantity: QuantitySchema,
|
|
||||||
unit_amount: MoneySchema,
|
|
||||||
|
|
||||||
subtotal_amount: MoneySchema,
|
|
||||||
|
|
||||||
item_discount_percentage: PercentageSchema,
|
|
||||||
item_discount_amount: MoneySchema,
|
|
||||||
|
|
||||||
global_discount_percentage: PercentageSchema,
|
|
||||||
global_discount_amount: MoneySchema,
|
|
||||||
|
|
||||||
taxable_amount: MoneySchema,
|
|
||||||
|
|
||||||
iva_code: z.string(),
|
|
||||||
iva_percentage: PercentageSchema,
|
|
||||||
iva_amount: MoneySchema,
|
|
||||||
|
|
||||||
rec_code: z.string(),
|
|
||||||
rec_percentage: PercentageSchema,
|
|
||||||
rec_amount: MoneySchema,
|
|
||||||
|
|
||||||
retention_code: z.string(),
|
|
||||||
retention_percentage: PercentageSchema,
|
|
||||||
retention_amount: MoneySchema,
|
|
||||||
|
|
||||||
taxes_amount: MoneySchema,
|
|
||||||
|
|
||||||
total_amount: MoneySchema,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
|
|
||||||
metadata: MetadataSchema.optional(),
|
metadata: MetadataSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,36 +1,39 @@
|
|||||||
import { MetadataSchema, MoneySchema, createPaginatedListSchema } from "@erp/core";
|
import {
|
||||||
|
CurrencyCodeSchema,
|
||||||
|
IsoDateSchema,
|
||||||
|
LanguageCodeSchema,
|
||||||
|
MoneySchema,
|
||||||
|
createPaginatedListSchema,
|
||||||
|
} from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import {
|
||||||
|
IssuedInvoiceRecipientSummarySchema,
|
||||||
|
IssuedInvoiceStatusSchema,
|
||||||
|
VerifactuRecordSchema,
|
||||||
|
} from "../../shared";
|
||||||
|
|
||||||
export const ListIssuedInvoicesResponseSchema = createPaginatedListSchema(
|
export const ListIssuedInvoicesResponseSchema = createPaginatedListSchema(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
company_id: z.uuid(),
|
company_id: z.uuid(),
|
||||||
|
is_proforma: z.boolean(),
|
||||||
customer_id: z.string(),
|
|
||||||
|
|
||||||
invoice_number: z.string(),
|
invoice_number: z.string(),
|
||||||
status: z.string(),
|
status: IssuedInvoiceStatusSchema,
|
||||||
series: z.string(),
|
series: z.string(),
|
||||||
|
|
||||||
invoice_date: z.string(),
|
invoice_date: IsoDateSchema,
|
||||||
operation_date: z.string(),
|
operation_date: IsoDateSchema.nullable(),
|
||||||
|
|
||||||
language_code: z.string(),
|
language_code: LanguageCodeSchema,
|
||||||
currency_code: z.string(),
|
currency_code: CurrencyCodeSchema,
|
||||||
|
|
||||||
reference: z.string(),
|
reference: z.string().nullable(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
|
|
||||||
recipient: z.object({
|
customer_id: z.uuid(),
|
||||||
tin: z.string(),
|
recipient: IssuedInvoiceRecipientSummarySchema,
|
||||||
name: z.string(),
|
|
||||||
street: z.string(),
|
|
||||||
street2: z.string(),
|
|
||||||
city: z.string(),
|
|
||||||
postal_code: z.string(),
|
|
||||||
province: z.string(),
|
|
||||||
country: z.string(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
subtotal_amount: MoneySchema,
|
subtotal_amount: MoneySchema,
|
||||||
total_discount_amount: MoneySchema,
|
total_discount_amount: MoneySchema,
|
||||||
@ -38,13 +41,9 @@ export const ListIssuedInvoicesResponseSchema = createPaginatedListSchema(
|
|||||||
taxes_amount: MoneySchema,
|
taxes_amount: MoneySchema,
|
||||||
total_amount: MoneySchema,
|
total_amount: MoneySchema,
|
||||||
|
|
||||||
verifactu: z.object({
|
verifactu: VerifactuRecordSchema,
|
||||||
status: z.string(),
|
|
||||||
url: z.string(),
|
|
||||||
qr_code: z.string(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
metadata: MetadataSchema.optional(),
|
linked_proforma_id: z.uuid().nullable(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,69 +1,52 @@
|
|||||||
import { MetadataSchema, MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
|
import {
|
||||||
|
CurrencyCodeSchema,
|
||||||
|
IsoDateSchema,
|
||||||
|
LanguageCodeSchema,
|
||||||
|
MetadataSchema,
|
||||||
|
MoneySchema,
|
||||||
|
PercentageSchema,
|
||||||
|
} from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import { PaymentMethodRefSchema, TaxesBreakdownSchema } from "../../shared";
|
||||||
|
import {
|
||||||
|
ProformaItemDetailSchema,
|
||||||
|
ProformaRecipientSummarySchema,
|
||||||
|
ProformaStatusSchema,
|
||||||
|
} from "../../shared/proforma";
|
||||||
|
|
||||||
export const GetProformaByIdResponseSchema = z.object({
|
export const GetProformaByIdResponseSchema = z.object({
|
||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
company_id: z.uuid(),
|
company_id: z.uuid(),
|
||||||
|
|
||||||
is_proforma: z.string(),
|
is_proforma: z.boolean(),
|
||||||
invoice_number: z.string(),
|
invoice_number: z.string(),
|
||||||
status: z.string(),
|
status: ProformaStatusSchema,
|
||||||
series: z.string(),
|
series: z.string().nullable(),
|
||||||
|
|
||||||
invoice_date: z.string(),
|
invoice_date: IsoDateSchema,
|
||||||
operation_date: z.string(),
|
operation_date: IsoDateSchema.nullable(),
|
||||||
|
|
||||||
reference: z.string(),
|
reference: z.string().nullable(),
|
||||||
description: z.string(),
|
description: z.string().nullable(),
|
||||||
notes: z.string(),
|
notes: z.string().nullable(),
|
||||||
|
|
||||||
language_code: z.string(),
|
language_code: LanguageCodeSchema,
|
||||||
currency_code: z.string(),
|
currency_code: CurrencyCodeSchema,
|
||||||
|
|
||||||
customer_id: z.string(),
|
customer_id: z.uuid(),
|
||||||
recipient: z.object({
|
recipient: ProformaRecipientSummarySchema,
|
||||||
id: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
tin: z.string(),
|
|
||||||
street: z.string(),
|
|
||||||
street2: z.string(),
|
|
||||||
city: z.string(),
|
|
||||||
province: z.string(),
|
|
||||||
postal_code: z.string(),
|
|
||||||
country: z.string(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
taxes: z.array(
|
linked_invoice_id: z.uuid().nullable(),
|
||||||
z.object({
|
|
||||||
taxable_amount: MoneySchema,
|
|
||||||
|
|
||||||
iva_code: z.string(),
|
taxes: TaxesBreakdownSchema,
|
||||||
iva_percentage: PercentageSchema,
|
|
||||||
iva_amount: MoneySchema,
|
|
||||||
|
|
||||||
rec_code: z.string(),
|
payment_method: PaymentMethodRefSchema.nullable(),
|
||||||
rec_percentage: PercentageSchema,
|
|
||||||
rec_amount: MoneySchema,
|
|
||||||
|
|
||||||
retention_code: z.string(),
|
|
||||||
retention_percentage: PercentageSchema,
|
|
||||||
retention_amount: MoneySchema,
|
|
||||||
|
|
||||||
taxes_amount: MoneySchema,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
|
|
||||||
payment_method: z
|
|
||||||
.object({
|
|
||||||
payment_id: z.string(),
|
|
||||||
payment_description: z.string(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
subtotal_amount: MoneySchema,
|
subtotal_amount: MoneySchema,
|
||||||
items_discount_amount: MoneySchema,
|
items_discount_amount: MoneySchema,
|
||||||
discount_percentage: PercentageSchema,
|
global_discount_percentage: PercentageSchema,
|
||||||
discount_amount: MoneySchema,
|
global_discount_amount: MoneySchema,
|
||||||
taxable_amount: MoneySchema,
|
taxable_amount: MoneySchema,
|
||||||
iva_amount: MoneySchema,
|
iva_amount: MoneySchema,
|
||||||
rec_amount: MoneySchema,
|
rec_amount: MoneySchema,
|
||||||
@ -71,43 +54,7 @@ export const GetProformaByIdResponseSchema = z.object({
|
|||||||
taxes_amount: MoneySchema,
|
taxes_amount: MoneySchema,
|
||||||
total_amount: MoneySchema,
|
total_amount: MoneySchema,
|
||||||
|
|
||||||
items: z.array(
|
items: z.array(ProformaItemDetailSchema),
|
||||||
z.object({
|
|
||||||
id: z.uuid(),
|
|
||||||
is_valued: z.string(),
|
|
||||||
position: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
|
|
||||||
quantity: QuantitySchema,
|
|
||||||
unit_amount: MoneySchema,
|
|
||||||
|
|
||||||
subtotal_amount: MoneySchema,
|
|
||||||
|
|
||||||
item_discount_percentage: PercentageSchema,
|
|
||||||
item_discount_amount: MoneySchema,
|
|
||||||
|
|
||||||
global_discount_percentage: PercentageSchema,
|
|
||||||
global_discount_amount: MoneySchema,
|
|
||||||
|
|
||||||
taxable_amount: MoneySchema,
|
|
||||||
|
|
||||||
iva_code: z.string(),
|
|
||||||
iva_percentage: PercentageSchema,
|
|
||||||
iva_amount: MoneySchema,
|
|
||||||
|
|
||||||
rec_code: z.string(),
|
|
||||||
rec_percentage: PercentageSchema,
|
|
||||||
rec_amount: MoneySchema,
|
|
||||||
|
|
||||||
retention_code: z.string(),
|
|
||||||
retention_percentage: PercentageSchema,
|
|
||||||
retention_amount: MoneySchema,
|
|
||||||
|
|
||||||
taxes_amount: MoneySchema,
|
|
||||||
|
|
||||||
total_amount: MoneySchema,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
|
|
||||||
metadata: MetadataSchema.optional(),
|
metadata: MetadataSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,42 +1,36 @@
|
|||||||
import {
|
import {
|
||||||
MetadataSchema,
|
CurrencyCodeSchema,
|
||||||
|
IsoDateSchema,
|
||||||
MoneySchema,
|
MoneySchema,
|
||||||
|
LanguageCodeSchema,
|
||||||
PercentageSchema,
|
PercentageSchema,
|
||||||
createPaginatedListSchema,
|
createPaginatedListSchema,
|
||||||
} from "@erp/core";
|
} from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import { ProformaRecipientSummarySchema, ProformaStatusSchema } from "../../shared/proforma";
|
||||||
|
|
||||||
export const ListProformasResponseSchema = createPaginatedListSchema(
|
export const ListProformasResponseSchema = createPaginatedListSchema(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
company_id: z.uuid(),
|
company_id: z.uuid(),
|
||||||
is_proforma: z.string(),
|
is_proforma: z.boolean(),
|
||||||
|
|
||||||
customer_id: z.string(),
|
|
||||||
|
|
||||||
invoice_number: z.string(),
|
invoice_number: z.string(),
|
||||||
status: z.string(),
|
status: ProformaStatusSchema,
|
||||||
series: z.string(),
|
series: z.string().nullable(),
|
||||||
|
|
||||||
invoice_date: z.string(),
|
invoice_date: IsoDateSchema,
|
||||||
operation_date: z.string(),
|
operation_date: IsoDateSchema.nullable(),
|
||||||
|
|
||||||
language_code: z.string(),
|
language_code: LanguageCodeSchema,
|
||||||
currency_code: z.string(),
|
currency_code: CurrencyCodeSchema,
|
||||||
|
|
||||||
reference: z.string(),
|
reference: z.string().nullable(),
|
||||||
description: z.string(),
|
description: z.string().nullable(),
|
||||||
|
|
||||||
recipient: z.object({
|
customer_id: z.uuid(),
|
||||||
tin: z.string(),
|
recipient: ProformaRecipientSummarySchema,
|
||||||
name: z.string(),
|
|
||||||
street: z.string(),
|
|
||||||
street2: z.string(),
|
|
||||||
city: z.string(),
|
|
||||||
postal_code: z.string(),
|
|
||||||
province: z.string(),
|
|
||||||
country: z.string(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
subtotal_amount: MoneySchema,
|
subtotal_amount: MoneySchema,
|
||||||
discount_percentage: PercentageSchema,
|
discount_percentage: PercentageSchema,
|
||||||
@ -45,9 +39,7 @@ export const ListProformasResponseSchema = createPaginatedListSchema(
|
|||||||
taxes_amount: MoneySchema,
|
taxes_amount: MoneySchema,
|
||||||
total_amount: MoneySchema,
|
total_amount: MoneySchema,
|
||||||
|
|
||||||
linked_invoice_id: z.string(),
|
linked_invoice_id: z.uuid().nullable(),
|
||||||
|
|
||||||
metadata: MetadataSchema.optional(),
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
5
modules/customer-invoices/src/common/dto/shared/index.ts
Normal file
5
modules/customer-invoices/src/common/dto/shared/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./issued-invoices";
|
||||||
|
export * from "./item-taxes-breakdown.dto";
|
||||||
|
export * from "./payment-methof-ref.dto";
|
||||||
|
export * from "./proforma";
|
||||||
|
export * from "./taxes-breakdown.dto";
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./issued-invoice-item-detail.dto";
|
||||||
|
export * from "./issued-invoice-recipient-summary.dto";
|
||||||
|
export * from "./issued-invoice-status.dto";
|
||||||
|
export * from "./verifactu-record.dto";
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import { ItemTaxesBreakdownSchema } from "../item-taxes-breakdown.dto";
|
||||||
|
|
||||||
|
export const IssuedInvoiceItemDetailSchema = z.object({
|
||||||
|
id: z.uuid(),
|
||||||
|
is_valued: z.boolean(),
|
||||||
|
position: z.number(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
|
||||||
|
quantity: QuantitySchema,
|
||||||
|
unit_amount: MoneySchema,
|
||||||
|
|
||||||
|
subtotal_amount: MoneySchema,
|
||||||
|
|
||||||
|
item_discount_percentage: PercentageSchema,
|
||||||
|
item_discount_amount: MoneySchema,
|
||||||
|
|
||||||
|
global_discount_percentage: PercentageSchema,
|
||||||
|
global_discount_amount: MoneySchema,
|
||||||
|
|
||||||
|
...ItemTaxesBreakdownSchema.shape,
|
||||||
|
|
||||||
|
total_amount: MoneySchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IssuedInvoiceItemDetailDTO = z.infer<typeof IssuedInvoiceItemDetailSchema>;
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { CountryCodeSchema, PostalCodeSchema, TinSchema } from "@erp/core";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const IssuedInvoiceRecipientSummarySchema = z.object({
|
||||||
|
id: z.uuid(),
|
||||||
|
tin: TinSchema,
|
||||||
|
name: z.string(),
|
||||||
|
street: z.string().nullable(),
|
||||||
|
street2: z.string().nullable(),
|
||||||
|
city: z.string().nullable(),
|
||||||
|
province: z.string().nullable(),
|
||||||
|
postal_code: PostalCodeSchema.nullable(),
|
||||||
|
country: CountryCodeSchema.nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IssuedInvoiceRecipientSummaryDTO = z.infer<typeof IssuedInvoiceRecipientSummarySchema>;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const IssuedInvoiceStatusSchema = z.templateLiteral(["issued"]);
|
||||||
|
|
||||||
|
export type IssuedInvoiceStatusDTO = z.infer<typeof IssuedInvoiceStatusSchema>;
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { URLSchema } from "@erp/core";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const VerifactuRecordStatusSchema = z.enum(["pending", "verified", "rejected"]);
|
||||||
|
|
||||||
|
export const VerifactuRecordSchema = z.object({
|
||||||
|
status: VerifactuRecordStatusSchema,
|
||||||
|
url: URLSchema.nullable(),
|
||||||
|
qr_code: z.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type VerifactuRecordStatusDTO = z.infer<typeof VerifactuRecordStatusSchema>;
|
||||||
|
export type VerifactuRecordDTO = z.infer<typeof VerifactuRecordSchema>;
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
|
import { TaxesBreakdownSchema } from "./taxes-breakdown.dto";
|
||||||
|
|
||||||
|
export const ItemTaxesBreakdownSchema = TaxesBreakdownSchema;
|
||||||
|
export type ItemTaxesBreakdownDTO = z.infer<typeof ItemTaxesBreakdownSchema>;
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const PaymentMethodRefSchema = z.object({
|
||||||
|
payment_id: z.uuid(),
|
||||||
|
payment_description: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type PaymentMethodRefDTO = z.infer<typeof PaymentMethodRefSchema>;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./proforma-item-detail.dto";
|
||||||
|
export * from "./proforma-recipient-summary.dto";
|
||||||
|
export * from "./proforma-status.dto";
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import { ItemTaxesBreakdownSchema } from "../item-taxes-breakdown.dto";
|
||||||
|
|
||||||
|
export const ProformaItemDetailSchema = z.object({
|
||||||
|
id: z.uuid(),
|
||||||
|
is_valued: z.boolean(),
|
||||||
|
position: z.number(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
|
||||||
|
quantity: QuantitySchema,
|
||||||
|
unit_amount: MoneySchema,
|
||||||
|
|
||||||
|
subtotal_amount: MoneySchema,
|
||||||
|
|
||||||
|
item_discount_percentage: PercentageSchema,
|
||||||
|
item_discount_amount: MoneySchema,
|
||||||
|
|
||||||
|
global_discount_percentage: PercentageSchema,
|
||||||
|
global_discount_amount: MoneySchema,
|
||||||
|
|
||||||
|
...ItemTaxesBreakdownSchema.shape,
|
||||||
|
|
||||||
|
total_amount: MoneySchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ProformaItemDetailDTO = z.infer<typeof ProformaItemDetailSchema>;
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { CountryCodeSchema, PostalCodeSchema, TinSchema } from "@erp/core";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const ProformaRecipientSummarySchema = z.object({
|
||||||
|
id: z.uuid().nullable(),
|
||||||
|
tin: TinSchema.nullable(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
street: z.string().nullable(),
|
||||||
|
street2: z.string().nullable(),
|
||||||
|
city: z.string().nullable(),
|
||||||
|
province: z.string().nullable(),
|
||||||
|
postal_code: PostalCodeSchema.nullable(),
|
||||||
|
country: CountryCodeSchema.nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ProformaRecipientSummaryDTO = z.infer<typeof ProformaRecipientSummarySchema>;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const ProformaStatusSchema = z.enum(["draft", "sent", "approved", "rejected", "issued"]);
|
||||||
|
|
||||||
|
export type ProformaStatusDTO = z.infer<typeof ProformaStatusSchema>;
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { MoneySchema, PercentageSchema } from "@erp/core";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const TaxesBreakdownSchema = z.object({
|
||||||
|
taxable_amount: MoneySchema,
|
||||||
|
|
||||||
|
iva_code: z.string(),
|
||||||
|
iva_percentage: PercentageSchema,
|
||||||
|
iva_amount: MoneySchema,
|
||||||
|
|
||||||
|
rec_code: z.string().nullable(),
|
||||||
|
rec_percentage: PercentageSchema,
|
||||||
|
rec_amount: MoneySchema,
|
||||||
|
|
||||||
|
retention_code: z.string().nullable(),
|
||||||
|
retention_percentage: PercentageSchema,
|
||||||
|
retention_amount: MoneySchema,
|
||||||
|
|
||||||
|
taxes_amount: MoneySchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TaxesBreakdownDTO = z.infer<typeof TaxesBreakdownSchema>;
|
||||||
@ -6,7 +6,7 @@ export const CustomerInvoiceItemDataFormSchema = CreateProformaRequestSchema.ext
|
|||||||
subtotal_price: z.object({
|
subtotal_price: z.object({
|
||||||
amount: z.number().nullable(),
|
amount: z.number().nullable(),
|
||||||
scale: z.number(),
|
scale: z.number(),
|
||||||
currency_code: z.string(),
|
currency_code: CurrencyCodeSchema,
|
||||||
}),
|
}),
|
||||||
discount: z.object({
|
discount: z.object({
|
||||||
amount: z.number().nullable(),
|
amount: z.number().nullable(),
|
||||||
@ -15,12 +15,12 @@ export const CustomerInvoiceItemDataFormSchema = CreateProformaRequestSchema.ext
|
|||||||
discount_price: z.object({
|
discount_price: z.object({
|
||||||
amount: z.number().nullable(),
|
amount: z.number().nullable(),
|
||||||
scale: z.number(),
|
scale: z.number(),
|
||||||
currency_code: z.string(),
|
currency_code: CurrencyCodeSchema,
|
||||||
}),
|
}),
|
||||||
before_tax_price: z.object({
|
before_tax_price: z.object({
|
||||||
amount: z.number().nullable(),
|
amount: z.number().nullable(),
|
||||||
scale: z.number(),
|
scale: z.number(),
|
||||||
currency_code: z.string(),
|
currency_code: CurrencyCodeSchema,
|
||||||
}),
|
}),
|
||||||
tax: z.object({
|
tax: z.object({
|
||||||
amount: z.number().nullable(),
|
amount: z.number().nullable(),
|
||||||
@ -29,12 +29,12 @@ export const CustomerInvoiceItemDataFormSchema = CreateProformaRequestSchema.ext
|
|||||||
tax_price: z.object({
|
tax_price: z.object({
|
||||||
amount: z.number().nullable(),
|
amount: z.number().nullable(),
|
||||||
scale: z.number(),
|
scale: z.number(),
|
||||||
currency_code: z.string(),
|
currency_code: CurrencyCodeSchema,
|
||||||
}),
|
}),
|
||||||
total_price: z.object({
|
total_price: z.object({
|
||||||
amount: z.number().nullable(),
|
amount: z.number().nullable(),
|
||||||
scale: z.number(),
|
scale: z.number(),
|
||||||
currency_code: z.string(),
|
currency_code: CurrencyCodeSchema,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,4 @@ export * from "./items-editor";
|
|||||||
export * from "./proforma-basic-info-fields";
|
export * from "./proforma-basic-info-fields";
|
||||||
export * from "./proforma-form-field-shell";
|
export * from "./proforma-form-field-shell";
|
||||||
export * from "./proforma-header-fields-card";
|
export * from "./proforma-header-fields-card";
|
||||||
export * from "./proforma-header-form-grid";
|
|
||||||
export * from "./proforma-section-card";
|
|
||||||
export * from "./selected-recipient";
|
export * from "./selected-recipient";
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { Button } from "@repo/shadcn-ui/components";
|
|||||||
import { ProformaUpdateRecipientEditor } from ".";
|
import { ProformaUpdateRecipientEditor } from ".";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { Proforma } from "../../../shared/entities";
|
|
||||||
import type { UseUpdateProformaItemsControllerResult } from "../../controllers";
|
import type { UseUpdateProformaItemsControllerResult } from "../../controllers";
|
||||||
|
|
||||||
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
|
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
|
||||||
@ -14,7 +13,6 @@ import { ProformaUpdateItemsEditor } from "./proforma-update-items-editor";
|
|||||||
|
|
||||||
type ProformaUpdateEditorProps = {
|
type ProformaUpdateEditorProps = {
|
||||||
formId: string;
|
formId: string;
|
||||||
proforma?: Proforma;
|
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
onSubmit: React.SubmitEventHandler<HTMLFormElement>;
|
onSubmit: React.SubmitEventHandler<HTMLFormElement>;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
@ -24,8 +22,6 @@ type ProformaUpdateEditorProps = {
|
|||||||
onCreateCustomerClick: () => void;
|
onCreateCustomerClick: () => void;
|
||||||
|
|
||||||
itemsCtrl: UseUpdateProformaItemsControllerResult;
|
itemsCtrl: UseUpdateProformaItemsControllerResult;
|
||||||
|
|
||||||
className?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProformaUpdateEditorForm = ({
|
export const ProformaUpdateEditorForm = ({
|
||||||
@ -37,7 +33,6 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
onChangeCustomerClick,
|
onChangeCustomerClick,
|
||||||
onCreateCustomerClick,
|
onCreateCustomerClick,
|
||||||
itemsCtrl,
|
itemsCtrl,
|
||||||
className,
|
|
||||||
}: ProformaUpdateEditorProps) => {
|
}: ProformaUpdateEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import { DatePickerField, SelectField, TextField } from "@repo/rdx-ui/components";
|
import {
|
||||||
|
DatePickerField,
|
||||||
|
FormSectionCard,
|
||||||
|
FormSectionGrid,
|
||||||
|
SelectField,
|
||||||
|
TextField,
|
||||||
|
} from "@repo/rdx-ui/components";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import { ProformaHeaderFormGrid, ProformaSectionCard } from "../blocks";
|
|
||||||
|
|
||||||
interface ProformaUpdateHeaderEditorProps {
|
interface ProformaUpdateHeaderEditorProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@ -15,11 +20,11 @@ export const ProformaUpdateHeaderEditor = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProformaSectionCard
|
<FormSectionCard
|
||||||
description={t("form_groups.proformas.basic_info.description")}
|
description={t("form_groups.proformas.basic_info.description")}
|
||||||
title={t("form_groups.proformas.basic_info.title")}
|
title={t("form_groups.proformas.basic_info.title")}
|
||||||
>
|
>
|
||||||
<ProformaHeaderFormGrid>
|
<FormSectionGrid>
|
||||||
<SelectField
|
<SelectField
|
||||||
className="md:col-span-2"
|
className="md:col-span-2"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -67,7 +72,7 @@ export const ProformaUpdateHeaderEditor = ({
|
|||||||
placeholder={t("form_fields.proformas.description.placeholder")}
|
placeholder={t("form_fields.proformas.description.placeholder")}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
</ProformaHeaderFormGrid>
|
</FormSectionGrid>
|
||||||
</ProformaSectionCard>
|
</FormSectionCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
|
import { FormSectionCard } from "@repo/rdx-ui/components";
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { UseUpdateProformaItemsControllerResult } from "../../controllers/use-update-proforma-items-controller";
|
import type { UseUpdateProformaItemsControllerResult } from "../../controllers/use-update-proforma-items-controller";
|
||||||
import { ProformaSectionCard } from "../blocks";
|
|
||||||
|
|
||||||
import { ProformaUpdateItemRowEditor } from "./proforma-update-item-row-editor";
|
import { ProformaUpdateItemRowEditor } from "./proforma-update-item-row-editor";
|
||||||
import { ProformaUpdateItemsTotals } from "./proforma-update-items-totals";
|
import { ProformaUpdateItemsTotals } from "./proforma-update-items-totals";
|
||||||
@ -21,7 +21,7 @@ export const ProformaUpdateItemsEditor = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProformaSectionCard
|
<FormSectionCard
|
||||||
description={t("form_groups.items.description")}
|
description={t("form_groups.items.description")}
|
||||||
title={t("form_groups.items.title")}
|
title={t("form_groups.items.title")}
|
||||||
>
|
>
|
||||||
@ -57,6 +57,6 @@ export const ProformaUpdateItemsEditor = ({
|
|||||||
|
|
||||||
<ProformaUpdateItemsTotals totals={itemsCtrl.totals} />
|
<ProformaUpdateItemsTotals totals={itemsCtrl.totals} />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</ProformaSectionCard>
|
</FormSectionCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { CustomerSelectionOption } from "@erp/customers";
|
import type { CustomerSelectionOption } from "@erp/customers";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import { ProformaSectionCard, SelectedRecipientSummary } from "../blocks";
|
import { FormSectionCard, SelectedRecipientSummary } from "../blocks";
|
||||||
|
|
||||||
interface ProformaUpdateRecipientEditorProps {
|
interface ProformaUpdateRecipientEditorProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@ -23,7 +23,7 @@ export const ProformaUpdateRecipientEditor = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProformaSectionCard title={t("form_groups.proformas.customer.title")}>
|
<FormSectionCard title={t("form_groups.proformas.customer.title")}>
|
||||||
<SelectedRecipientSummary
|
<SelectedRecipientSummary
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChangeClick={onChangeCustomerClick}
|
onChangeClick={onChangeCustomerClick}
|
||||||
@ -31,6 +31,6 @@ export const ProformaUpdateRecipientEditor = ({
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
recipient={selectedCustomer}
|
recipient={selectedCustomer}
|
||||||
/>
|
/>
|
||||||
</ProformaSectionCard>
|
</FormSectionCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -43,17 +43,15 @@ export const ProformaUpdatePage = () => {
|
|||||||
|
|
||||||
if (!updateCtrl.proformaData)
|
if (!updateCtrl.proformaData)
|
||||||
return (
|
return (
|
||||||
<>
|
<AppContent>
|
||||||
<AppContent>
|
<NotFoundCard
|
||||||
<NotFoundCard
|
message={t(
|
||||||
message={t(
|
"pages.proformas.update.not_found_msg",
|
||||||
"pages.proformas.update.not_found_msg",
|
"Revisa el identificador o vuelve al listado."
|
||||||
"Revisa el identificador o vuelve al listado."
|
)}
|
||||||
)}
|
title={t("pages.proformas.update.not_found_title", "Proforma no encontrada")}
|
||||||
title={t("pages.proformas.update.not_found_title", "Proforma no encontrada")}
|
/>
|
||||||
/>
|
</AppContent>
|
||||||
</AppContent>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -66,7 +64,7 @@ export const ProformaUpdatePage = () => {
|
|||||||
<UpdateCommitButtonGroup
|
<UpdateCommitButtonGroup
|
||||||
cancel={{
|
cancel={{
|
||||||
formId: updateCtrl.formId,
|
formId: updateCtrl.formId,
|
||||||
to: "/customers/list",
|
to: "/proformas/list",
|
||||||
disabled: updateCtrl.isUpdating,
|
disabled: updateCtrl.isUpdating,
|
||||||
}}
|
}}
|
||||||
disabled={updateCtrl.isUpdating}
|
disabled={updateCtrl.isUpdating}
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import {
|
|||||||
Province,
|
Province,
|
||||||
Street,
|
Street,
|
||||||
TINNumber,
|
TINNumber,
|
||||||
type TaxCode,
|
|
||||||
TextValue,
|
TextValue,
|
||||||
URLAddress,
|
URLAddress,
|
||||||
type UniqueID,
|
type UniqueID,
|
||||||
@ -21,9 +20,13 @@ import {
|
|||||||
extractOrPushError,
|
extractOrPushError,
|
||||||
maybeFromNullableResult,
|
maybeFromNullableResult,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Collection, Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
import { Result, toPatchField } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { UpdateCustomerByIdRequestDTO } from "../../../common";
|
import type {
|
||||||
|
UpdateCustomerAddressPatchRequestDTO,
|
||||||
|
UpdateCustomerByIdRequestDTO,
|
||||||
|
UpdateCustomerContactPatchRequestDTO,
|
||||||
|
} from "../../../common";
|
||||||
import type { CustomerPatchProps } from "../../domain";
|
import type { CustomerPatchProps } from "../../domain";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,18 +47,21 @@ export interface IUpdateCustomerInputMapper {
|
|||||||
map(
|
map(
|
||||||
dto: UpdateCustomerByIdRequestDTO,
|
dto: UpdateCustomerByIdRequestDTO,
|
||||||
params: { companyId: UniqueID }
|
params: { companyId: UniqueID }
|
||||||
): Result<CustomerPatchProps>;
|
): Result<CustomerPatchProps, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper {
|
export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper {
|
||||||
public map(dto: UpdateCustomerByIdRequestDTO, params: { companyId: UniqueID }) {
|
public map(
|
||||||
|
dto: UpdateCustomerByIdRequestDTO,
|
||||||
|
params: { companyId: UniqueID }
|
||||||
|
): Result<CustomerPatchProps, Error> {
|
||||||
|
console.log("Mapping UpdateCustomerByIdRequestDTO to CustomerPatchProps:", dto);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const errors: ValidationErrorDetail[] = [];
|
const errors: ValidationErrorDetail[] = [];
|
||||||
const customerPatchProps: CustomerPatchProps = {};
|
const customerPatchProps: CustomerPatchProps = {};
|
||||||
|
|
||||||
console.log(dto);
|
toPatchField(dto.reference).ifSetOrNull((reference) => {
|
||||||
|
|
||||||
toPatchField(dto.reference).ifSet((reference) => {
|
|
||||||
customerPatchProps.reference = extractOrPushError(
|
customerPatchProps.reference = extractOrPushError(
|
||||||
maybeFromNullableResult(reference, (value) => Name.create(value)),
|
maybeFromNullableResult(reference, (value) => Name.create(value)),
|
||||||
"reference",
|
"reference",
|
||||||
@ -63,35 +69,23 @@ export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.is_company).ifSet((is_company) => {
|
toPatchField(dto.is_company).ifSet((isCompany) => {
|
||||||
if (isNullishOrEmpty(is_company)) {
|
customerPatchProps.isCompany = isCompany;
|
||||||
errors.push({ path: "is_company", message: "is_company cannot be empty" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
customerPatchProps.isCompany = extractOrPushError(
|
|
||||||
Result.ok(Boolean(is_company!)),
|
|
||||||
"is_company",
|
|
||||||
errors
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.name).ifSet((name) => {
|
toPatchField(dto.name).ifSet((name) => {
|
||||||
if (isNullishOrEmpty(name)) {
|
customerPatchProps.name = extractOrPushError(Name.create(name), "name", errors);
|
||||||
errors.push({ path: "name", message: "Name cannot be empty" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
customerPatchProps.name = extractOrPushError(Name.create(name!), "name", errors);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.trade_name).ifSet((trade_name) => {
|
toPatchField(dto.trade_name).ifSetOrNull((tradeName) => {
|
||||||
customerPatchProps.tradeName = extractOrPushError(
|
customerPatchProps.tradeName = extractOrPushError(
|
||||||
maybeFromNullableResult(trade_name, (value) => Name.create(value)),
|
maybeFromNullableResult(tradeName, (value) => Name.create(value)),
|
||||||
"trade_name",
|
"trade_name",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.tin).ifSet((tin) => {
|
toPatchField(dto.tin).ifSetOrNull((tin) => {
|
||||||
customerPatchProps.tin = extractOrPushError(
|
customerPatchProps.tin = extractOrPushError(
|
||||||
maybeFromNullableResult(tin, (value) => TINNumber.create(value)),
|
maybeFromNullableResult(tin, (value) => TINNumber.create(value)),
|
||||||
"tin",
|
"tin",
|
||||||
@ -99,71 +93,7 @@ export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.email_primary).ifSet((email_primary) => {
|
toPatchField(dto.legal_record).ifSetOrNull((legalRecord) => {
|
||||||
customerPatchProps.emailPrimary = extractOrPushError(
|
|
||||||
maybeFromNullableResult(email_primary, (value) => EmailAddress.create(value)),
|
|
||||||
"email_primary",
|
|
||||||
errors
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
toPatchField(dto.email_secondary).ifSet((email_secondary) => {
|
|
||||||
customerPatchProps.emailSecondary = extractOrPushError(
|
|
||||||
maybeFromNullableResult(email_secondary, (value) => EmailAddress.create(value)),
|
|
||||||
"email_secondary",
|
|
||||||
errors
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
toPatchField(dto.mobile_primary).ifSet((mobile_primary) => {
|
|
||||||
customerPatchProps.mobilePrimary = extractOrPushError(
|
|
||||||
maybeFromNullableResult(mobile_primary, (value) => PhoneNumber.create(value)),
|
|
||||||
"mobile_primary",
|
|
||||||
errors
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
toPatchField(dto.mobile_secondary).ifSet((mobile_secondary) => {
|
|
||||||
customerPatchProps.mobilePrimary = extractOrPushError(
|
|
||||||
maybeFromNullableResult(mobile_secondary, (value) => PhoneNumber.create(value)),
|
|
||||||
"mobile_secondary",
|
|
||||||
errors
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
toPatchField(dto.phone_primary).ifSet((phone_primary) => {
|
|
||||||
customerPatchProps.phonePrimary = extractOrPushError(
|
|
||||||
maybeFromNullableResult(phone_primary, (value) => PhoneNumber.create(value)),
|
|
||||||
"phone_primary",
|
|
||||||
errors
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
toPatchField(dto.phone_secondary).ifSet((phone_secondary) => {
|
|
||||||
customerPatchProps.phoneSecondary = extractOrPushError(
|
|
||||||
maybeFromNullableResult(phone_secondary, (value) => PhoneNumber.create(value)),
|
|
||||||
"phone_secondary",
|
|
||||||
errors
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
toPatchField(dto.fax).ifSet((fax) => {
|
|
||||||
customerPatchProps.fax = extractOrPushError(
|
|
||||||
maybeFromNullableResult(fax, (value) => PhoneNumber.create(value)),
|
|
||||||
"fax",
|
|
||||||
errors
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
toPatchField(dto.website).ifSet((website) => {
|
|
||||||
customerPatchProps.website = extractOrPushError(
|
|
||||||
maybeFromNullableResult(website, (value) => URLAddress.create(value)),
|
|
||||||
"website",
|
|
||||||
errors
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
toPatchField(dto.legal_record).ifSet((legalRecord) => {
|
|
||||||
customerPatchProps.legalRecord = extractOrPushError(
|
customerPatchProps.legalRecord = extractOrPushError(
|
||||||
maybeFromNullableResult(legalRecord, (value) => TextValue.create(value)),
|
maybeFromNullableResult(legalRecord, (value) => TextValue.create(value)),
|
||||||
"legal_record",
|
"legal_record",
|
||||||
@ -172,120 +102,186 @@ export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper {
|
|||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.language_code).ifSet((languageCode) => {
|
toPatchField(dto.language_code).ifSet((languageCode) => {
|
||||||
if (isNullishOrEmpty(languageCode)) {
|
|
||||||
errors.push({ path: "language_code", message: "Language code cannot be empty" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
customerPatchProps.languageCode = extractOrPushError(
|
customerPatchProps.languageCode = extractOrPushError(
|
||||||
LanguageCode.create(languageCode!),
|
LanguageCode.create(languageCode),
|
||||||
"language_code",
|
"language_code",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.currency_code).ifSet((currencyCode) => {
|
toPatchField(dto.currency_code).ifSet((currencyCode) => {
|
||||||
if (isNullishOrEmpty(currencyCode)) {
|
|
||||||
errors.push({ path: "currency_code", message: "Currency code cannot be empty" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
customerPatchProps.currencyCode = extractOrPushError(
|
customerPatchProps.currencyCode = extractOrPushError(
|
||||||
CurrencyCode.create(currencyCode!),
|
CurrencyCode.create(currencyCode),
|
||||||
"currency_code",
|
"currency_code",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Default taxes
|
/**
|
||||||
const defaultTaxesCollection = new Collection<TaxCode>();
|
* Se mantiene la compatibilidad con el contrato actual.
|
||||||
/*toPatchField(dto.default_taxes).ifSet((defaultTaxes) => {
|
* Cuando defaultTaxes tenga un contrato final estable, aquí conviene
|
||||||
customerPatchProps.defaultTaxes = defaultTaxesCollection;
|
* mapearlo a CustomerTaxes/Collection<TaxCode> con su schema semántico real.
|
||||||
|
*/
|
||||||
if (isNullishOrEmpty(defaultTaxes)) {
|
toPatchField(dto.default_taxes).ifSet((_defaultTaxes) => {
|
||||||
return;
|
errors.push({
|
||||||
}
|
path: "default_taxes",
|
||||||
|
message: "default_taxes mapping is not implemented yet",
|
||||||
defaultTaxes!.forEach((taxCode, index) => {
|
|
||||||
const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors);
|
|
||||||
if (tax && customerPatchProps.defaultTaxes) {
|
|
||||||
customerPatchProps.defaultTaxes.add(tax);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});*/
|
});
|
||||||
|
|
||||||
// PostalAddress
|
const addressPatchProps = this.mapPostalAddress(dto.address, errors);
|
||||||
const addressPatchProps = this.mapPostalAddress(dto, errors);
|
|
||||||
if (addressPatchProps) {
|
if (addressPatchProps) {
|
||||||
customerPatchProps.address = addressPatchProps;
|
customerPatchProps.address = addressPatchProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.mapContact(dto.contact, customerPatchProps, errors);
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
return Result.fail(
|
return Result.fail(
|
||||||
new ValidationErrorCollection("Customer props mapping failed (update)", errors)
|
new ValidationErrorCollection("Customer props mapping failed (update)", errors)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Mapped CustomerPatchProps:", customerPatchProps);
|
||||||
|
|
||||||
return Result.ok(customerPatchProps);
|
return Result.ok(customerPatchProps);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
return Result.fail(new DomainError("Customer props mapping failed", { cause: err }));
|
return Result.fail(new DomainError("Customer props mapping failed (update)", { cause: err }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public mapPostalAddress(
|
private mapPostalAddress(
|
||||||
dto: UpdateCustomerByIdRequestDTO,
|
dto: UpdateCustomerAddressPatchRequestDTO | undefined,
|
||||||
errors: ValidationErrorDetail[]
|
errors: ValidationErrorDetail[]
|
||||||
): PostalAddressPatchProps | undefined {
|
): PostalAddressPatchProps | undefined {
|
||||||
|
if (!dto) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const postalAddressPatchProps: PostalAddressPatchProps = {};
|
const postalAddressPatchProps: PostalAddressPatchProps = {};
|
||||||
|
|
||||||
toPatchField(dto.street).ifSet((street) => {
|
toPatchField(dto.street).ifSetOrNull((street) => {
|
||||||
postalAddressPatchProps.street = extractOrPushError(
|
postalAddressPatchProps.street = extractOrPushError(
|
||||||
maybeFromNullableResult(street, (value) => Street.create(value)),
|
maybeFromNullableResult(street, (value) => Street.create(value)),
|
||||||
"street",
|
"address.street",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.street2).ifSet((street2) => {
|
toPatchField(dto.street2).ifSetOrNull((street2) => {
|
||||||
postalAddressPatchProps.street2 = extractOrPushError(
|
postalAddressPatchProps.street2 = extractOrPushError(
|
||||||
maybeFromNullableResult(street2, (value) => Street.create(value)),
|
maybeFromNullableResult(street2, (value) => Street.create(value)),
|
||||||
"street2",
|
"address.street2",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.city).ifSet((city) => {
|
toPatchField(dto.city).ifSetOrNull((city) => {
|
||||||
postalAddressPatchProps.city = extractOrPushError(
|
postalAddressPatchProps.city = extractOrPushError(
|
||||||
maybeFromNullableResult(city, (value) => City.create(value)),
|
maybeFromNullableResult(city, (value) => City.create(value)),
|
||||||
"city",
|
"address.city",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.province).ifSet((province) => {
|
toPatchField(dto.province).ifSetOrNull((province) => {
|
||||||
postalAddressPatchProps.province = extractOrPushError(
|
postalAddressPatchProps.province = extractOrPushError(
|
||||||
maybeFromNullableResult(province, (value) => Province.create(value)),
|
maybeFromNullableResult(province, (value) => Province.create(value)),
|
||||||
"province",
|
"address.province",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.postal_code).ifSet((postalCode) => {
|
toPatchField(dto.postal_code).ifSetOrNull((postalCode) => {
|
||||||
postalAddressPatchProps.postalCode = extractOrPushError(
|
postalAddressPatchProps.postalCode = extractOrPushError(
|
||||||
maybeFromNullableResult(postalCode, (value) => PostalCode.create(value)),
|
maybeFromNullableResult(postalCode, (value) => PostalCode.create(value)),
|
||||||
"postal_code",
|
"address.postal_code",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.country).ifSet((country) => {
|
toPatchField(dto.country).ifSetOrNull((country) => {
|
||||||
postalAddressPatchProps.country = extractOrPushError(
|
postalAddressPatchProps.country = extractOrPushError(
|
||||||
maybeFromNullableResult(country, (value) => Country.create(value)),
|
maybeFromNullableResult(country, (value) => Country.create(value)),
|
||||||
"country",
|
"address.country",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Object.keys(postalAddressPatchProps).length > 0 ? postalAddressPatchProps : undefined;
|
return Object.keys(postalAddressPatchProps).length > 0 ? postalAddressPatchProps : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private mapContact(
|
||||||
|
dto: UpdateCustomerContactPatchRequestDTO | undefined,
|
||||||
|
customerPatchProps: CustomerPatchProps,
|
||||||
|
errors: ValidationErrorDetail[]
|
||||||
|
): void {
|
||||||
|
if (!dto) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toPatchField(dto.email_primary).ifSetOrNull((emailPrimary) => {
|
||||||
|
customerPatchProps.emailPrimary = extractOrPushError(
|
||||||
|
maybeFromNullableResult(emailPrimary, (value) => EmailAddress.create(value)),
|
||||||
|
"contact.email_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.email_secondary).ifSetOrNull((emailSecondary) => {
|
||||||
|
customerPatchProps.emailSecondary = extractOrPushError(
|
||||||
|
maybeFromNullableResult(emailSecondary, (value) => EmailAddress.create(value)),
|
||||||
|
"contact.email_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.phone_primary).ifSetOrNull((phonePrimary) => {
|
||||||
|
customerPatchProps.phonePrimary = extractOrPushError(
|
||||||
|
maybeFromNullableResult(phonePrimary, (value) => PhoneNumber.create(value)),
|
||||||
|
"contact.phone_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.phone_secondary).ifSetOrNull((phoneSecondary) => {
|
||||||
|
customerPatchProps.phoneSecondary = extractOrPushError(
|
||||||
|
maybeFromNullableResult(phoneSecondary, (value) => PhoneNumber.create(value)),
|
||||||
|
"contact.phone_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.mobile_primary).ifSetOrNull((mobilePrimary) => {
|
||||||
|
customerPatchProps.mobilePrimary = extractOrPushError(
|
||||||
|
maybeFromNullableResult(mobilePrimary, (value) => PhoneNumber.create(value)),
|
||||||
|
"contact.mobile_primary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.mobile_secondary).ifSetOrNull((mobileSecondary) => {
|
||||||
|
customerPatchProps.mobileSecondary = extractOrPushError(
|
||||||
|
maybeFromNullableResult(mobileSecondary, (value) => PhoneNumber.create(value)),
|
||||||
|
"contact.mobile_secondary",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.fax).ifSetOrNull((fax) => {
|
||||||
|
customerPatchProps.fax = extractOrPushError(
|
||||||
|
maybeFromNullableResult(fax, (value) => PhoneNumber.create(value)),
|
||||||
|
"contact.fax",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
toPatchField(dto.website).ifSetOrNull((website) => {
|
||||||
|
customerPatchProps.website = extractOrPushError(
|
||||||
|
maybeFromNullableResult(website, (value) => URLAddress.create(value)),
|
||||||
|
"contact.website",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,59 +0,0 @@
|
|||||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
|
||||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
|
||||||
|
|
||||||
import type { Customer } from "../../../domain";
|
|
||||||
|
|
||||||
import type { ICustomerFullSnapshot } from "./customer-snapshot.interface";
|
|
||||||
|
|
||||||
export interface ICustomerFullSnapshotBuilder
|
|
||||||
extends ISnapshotBuilder<Customer, ICustomerFullSnapshot> {}
|
|
||||||
|
|
||||||
export class CustomerFullSnapshotBuilder implements ICustomerFullSnapshotBuilder {
|
|
||||||
toOutput(customer: Customer): ICustomerFullSnapshot {
|
|
||||||
const address = customer.address.toPrimitive();
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: customer.id.toPrimitive(),
|
|
||||||
company_id: customer.companyId.toPrimitive(),
|
|
||||||
status: customer.isActive ? "active" : "inactive",
|
|
||||||
reference: maybeToEmptyString(customer.reference, (value) => value.toPrimitive()),
|
|
||||||
|
|
||||||
is_company: String(customer.isCompany),
|
|
||||||
name: customer.name.toPrimitive(),
|
|
||||||
trade_name: maybeToEmptyString(customer.tradeName, (value) => value.toPrimitive()),
|
|
||||||
tin: maybeToEmptyString(customer.tin, (value) => value.toPrimitive()),
|
|
||||||
|
|
||||||
street: maybeToEmptyString(address.street, (value) => value.toPrimitive()),
|
|
||||||
street2: maybeToEmptyString(address.street2, (value) => value.toPrimitive()),
|
|
||||||
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()),
|
|
||||||
|
|
||||||
email_primary: maybeToEmptyString(customer.emailPrimary, (value) => value.toPrimitive()),
|
|
||||||
email_secondary: maybeToEmptyString(customer.emailSecondary, (value) => value.toPrimitive()),
|
|
||||||
|
|
||||||
phone_primary: maybeToEmptyString(customer.phonePrimary, (value) => value.toPrimitive()),
|
|
||||||
phone_secondary: maybeToEmptyString(customer.phoneSecondary, (value) => value.toPrimitive()),
|
|
||||||
|
|
||||||
mobile_primary: maybeToEmptyString(customer.mobilePrimary, (value) => value.toPrimitive()),
|
|
||||||
mobile_secondary: maybeToEmptyString(customer.mobileSecondary, (value) =>
|
|
||||||
value.toPrimitive()
|
|
||||||
),
|
|
||||||
|
|
||||||
fax: maybeToEmptyString(customer.fax, (value) => value.toPrimitive()),
|
|
||||||
website: maybeToEmptyString(customer.website, (value) => value.toPrimitive()),
|
|
||||||
|
|
||||||
legal_record: maybeToEmptyString(customer.legalRecord, (value) => value.toPrimitive()),
|
|
||||||
|
|
||||||
default_taxes: customer.defaultTaxes.toKey(),
|
|
||||||
|
|
||||||
language_code: customer.languageCode.toPrimitive(),
|
|
||||||
currency_code: customer.currencyCode.toPrimitive(),
|
|
||||||
|
|
||||||
metadata: {
|
|
||||||
entity: "customer",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
export interface ICustomerFullSnapshot {
|
|
||||||
id: string;
|
|
||||||
company_id: string;
|
|
||||||
status: string;
|
|
||||||
reference: string;
|
|
||||||
|
|
||||||
is_company: string;
|
|
||||||
name: string;
|
|
||||||
trade_name: string;
|
|
||||||
tin: string;
|
|
||||||
|
|
||||||
street: string;
|
|
||||||
street2: string;
|
|
||||||
city: string;
|
|
||||||
province: string;
|
|
||||||
postal_code: string;
|
|
||||||
country: string;
|
|
||||||
|
|
||||||
email_primary: string;
|
|
||||||
email_secondary: string;
|
|
||||||
|
|
||||||
phone_primary: string;
|
|
||||||
phone_secondary: string;
|
|
||||||
|
|
||||||
mobile_primary: string;
|
|
||||||
mobile_secondary: string;
|
|
||||||
|
|
||||||
fax: string;
|
|
||||||
website: string;
|
|
||||||
|
|
||||||
legal_record: string;
|
|
||||||
|
|
||||||
default_taxes: string;
|
|
||||||
|
|
||||||
language_code: string;
|
|
||||||
currency_code: string;
|
|
||||||
|
|
||||||
metadata?: Record<string, string>;
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./customer-snapshot.interface";
|
|
||||||
export * from "./customer-snapshot-builder";
|
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
|
import { toNullable } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
|
import type { GetCustomerByIdResponseDTO } from "../../../../common";
|
||||||
|
import type { Customer } from "../../../domain";
|
||||||
|
|
||||||
|
export interface ICustomerFullSnapshotBuilder
|
||||||
|
extends ISnapshotBuilder<Customer, GetCustomerByIdResponseDTO> {}
|
||||||
|
|
||||||
|
export class CustomerFullSnapshotBuilder implements ICustomerFullSnapshotBuilder {
|
||||||
|
toOutput(customer: Customer): GetCustomerByIdResponseDTO {
|
||||||
|
const address = customer.address.toPrimitive();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: customer.id.toPrimitive(),
|
||||||
|
company_id: customer.companyId.toPrimitive(),
|
||||||
|
status: customer.isActive ? "active" : "inactive",
|
||||||
|
reference: toNullable(customer.reference, (value) => value.toPrimitive()),
|
||||||
|
|
||||||
|
is_company: customer.isCompany,
|
||||||
|
name: customer.name.toPrimitive(),
|
||||||
|
trade_name: toNullable(customer.tradeName, (value) => value.toPrimitive()),
|
||||||
|
tin: toNullable(customer.tin, (value) => value.toPrimitive()),
|
||||||
|
|
||||||
|
address: {
|
||||||
|
street: toNullable(address.street, (value) => value.toPrimitive()),
|
||||||
|
street2: toNullable(address.street2, (value) => value.toPrimitive()),
|
||||||
|
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()),
|
||||||
|
},
|
||||||
|
|
||||||
|
contact: {
|
||||||
|
email_primary: toNullable(customer.emailPrimary, (value) => value.toPrimitive()),
|
||||||
|
email_secondary: toNullable(customer.emailSecondary, (value) => value.toPrimitive()),
|
||||||
|
|
||||||
|
phone_primary: toNullable(customer.phonePrimary, (value) => value.toPrimitive()),
|
||||||
|
phone_secondary: toNullable(customer.phoneSecondary, (value) => value.toPrimitive()),
|
||||||
|
|
||||||
|
mobile_primary: toNullable(customer.mobilePrimary, (value) => value.toPrimitive()),
|
||||||
|
mobile_secondary: toNullable(customer.mobileSecondary, (value) => value.toPrimitive()),
|
||||||
|
|
||||||
|
fax: toNullable(customer.fax, (value) => value.toPrimitive()),
|
||||||
|
website: toNullable(customer.website, (value) => value.toPrimitive()),
|
||||||
|
},
|
||||||
|
|
||||||
|
legal_record: toNullable(customer.legalRecord, (value) => value.toPrimitive()),
|
||||||
|
|
||||||
|
default_taxes: customer.defaultTaxes.toKey(),
|
||||||
|
|
||||||
|
language_code: customer.languageCode.toPrimitive(),
|
||||||
|
currency_code: customer.currencyCode.toPrimitive(),
|
||||||
|
|
||||||
|
metadata: {
|
||||||
|
entity: "customer",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-full-snapshot-builder";
|
||||||
@ -1,2 +1,2 @@
|
|||||||
export * from "./domain";
|
export * from "./full";
|
||||||
export * from "./summary";
|
export * from "./summary";
|
||||||
|
|||||||
@ -1,49 +1,40 @@
|
|||||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
import { toNullable } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
|
import type { CustomerSummaryDTO } from "../../../../common";
|
||||||
import type { CustomerSummary } from "../../models";
|
import type { CustomerSummary } from "../../models";
|
||||||
|
|
||||||
import type { ICustomerSummarySnapshot } from "./customer-summary-snapshot.interface";
|
|
||||||
|
|
||||||
export interface ICustomerSummarySnapshotBuilder
|
export interface ICustomerSummarySnapshotBuilder
|
||||||
extends ISnapshotBuilder<CustomerSummary, ICustomerSummarySnapshot> {}
|
extends ISnapshotBuilder<CustomerSummary, CustomerSummaryDTO> {}
|
||||||
|
|
||||||
export class CustomerSummarySnapshotBuilder implements ICustomerSummarySnapshotBuilder {
|
export class CustomerSummarySnapshotBuilder implements ICustomerSummarySnapshotBuilder {
|
||||||
toOutput(customer: CustomerSummary): ICustomerSummarySnapshot {
|
toOutput(customer: CustomerSummary): CustomerSummaryDTO {
|
||||||
const { address } = customer;
|
const { address } = customer;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: customer.id.toString(),
|
id: customer.id.toString(),
|
||||||
company_id: customer.companyId.toString(),
|
company_id: customer.companyId.toString(),
|
||||||
status: customer.isActive ? "active" : "inactive",
|
status: customer.isActive ? "active" : "inactive",
|
||||||
reference: maybeToEmptyString(customer.reference, (value) => value.toString()),
|
reference: toNullable(customer.reference, (value) => value.toString()),
|
||||||
|
|
||||||
is_company: String(customer.isCompany),
|
is_company: customer.isCompany,
|
||||||
name: customer.name.toString(),
|
name: customer.name.toString(),
|
||||||
trade_name: maybeToEmptyString(customer.tradeName, (value) => value.toString()),
|
trade_name: toNullable(customer.tradeName, (value) => value.toString()),
|
||||||
tin: maybeToEmptyString(customer.tin, (value) => value.toString()),
|
tin: toNullable(customer.tin, (value) => value.toString()),
|
||||||
|
|
||||||
street: maybeToEmptyString(address.street, (value) => value.toString()),
|
address: {
|
||||||
street2: maybeToEmptyString(address.street2, (value) => value.toString()),
|
street: toNullable(address.street, (value) => value.toString()),
|
||||||
city: maybeToEmptyString(address.city, (value) => value.toString()),
|
city: toNullable(address.city, (value) => value.toString()),
|
||||||
postal_code: maybeToEmptyString(address.postalCode, (value) => value.toString()),
|
postal_code: toNullable(address.postalCode, (value) => value.toString()),
|
||||||
province: maybeToEmptyString(address.province, (value) => value.toString()),
|
province: toNullable(address.province, (value) => value.toString()),
|
||||||
country: maybeToEmptyString(address.country, (value) => value.toString()),
|
country: toNullable(address.country, (value) => value.toString()),
|
||||||
|
},
|
||||||
|
|
||||||
email_primary: maybeToEmptyString(customer.emailPrimary, (value) => value.toString()),
|
contact: {
|
||||||
email_secondary: maybeToEmptyString(customer.emailSecondary, (value) => value.toString()),
|
email_primary: toNullable(customer.emailPrimary, (value) => value.toString()),
|
||||||
|
phone_primary: toNullable(customer.phonePrimary, (value) => value.toString()),
|
||||||
phone_primary: maybeToEmptyString(customer.phonePrimary, (value) => value.toString()),
|
mobile_primary: toNullable(customer.mobilePrimary, (value) => value.toString()),
|
||||||
phone_secondary: maybeToEmptyString(customer.phoneSecondary, (value) => value.toString()),
|
},
|
||||||
|
|
||||||
mobile_primary: maybeToEmptyString(customer.mobilePrimary, (value) => value.toString()),
|
|
||||||
mobile_secondary: maybeToEmptyString(customer.mobileSecondary, (value) => value.toString()),
|
|
||||||
|
|
||||||
fax: maybeToEmptyString(customer.fax, (value) => value.toString()),
|
|
||||||
website: maybeToEmptyString(customer.website, (value) => value.toString()),
|
|
||||||
|
|
||||||
language_code: customer.languageCode.code,
|
|
||||||
currency_code: customer.currencyCode.code,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,35 +0,0 @@
|
|||||||
export type ICustomerSummarySnapshot = {
|
|
||||||
id: string;
|
|
||||||
company_id: string;
|
|
||||||
status: string;
|
|
||||||
reference: string;
|
|
||||||
|
|
||||||
is_company: string;
|
|
||||||
name: string;
|
|
||||||
trade_name: string;
|
|
||||||
tin: string;
|
|
||||||
|
|
||||||
street: string;
|
|
||||||
street2: string;
|
|
||||||
city: string;
|
|
||||||
postal_code: string;
|
|
||||||
province: string;
|
|
||||||
country: string;
|
|
||||||
|
|
||||||
email_primary: string;
|
|
||||||
email_secondary: string;
|
|
||||||
|
|
||||||
phone_primary: string;
|
|
||||||
phone_secondary: string;
|
|
||||||
|
|
||||||
mobile_primary: string;
|
|
||||||
mobile_secondary: string;
|
|
||||||
|
|
||||||
fax: string;
|
|
||||||
website: string;
|
|
||||||
|
|
||||||
language_code: string;
|
|
||||||
currency_code: string;
|
|
||||||
|
|
||||||
metadata?: Record<string, string>;
|
|
||||||
};
|
|
||||||
@ -1,2 +1 @@
|
|||||||
export * from "./customer-summary-snapshot.interface";
|
|
||||||
export * from "./customer-summary-snapshot-builder";
|
export * from "./customer-summary-snapshot-builder";
|
||||||
|
|||||||
@ -93,12 +93,23 @@ export interface ICustomer {
|
|||||||
readonly currencyCode: CurrencyCode;
|
readonly currencyCode: CurrencyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomerInternalProps = Omit<ICustomerCreateProps, "address" | "defaultTaxes"> & {
|
export type CustomerInternalProps = Omit<ICustomerCreateProps, "address" | "defaultTaxes">;
|
||||||
readonly address: PostalAddress;
|
|
||||||
readonly defaultTaxes: CustomerTaxes;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Customer extends AggregateRoot<CustomerInternalProps> implements ICustomer {
|
export class Customer extends AggregateRoot<CustomerInternalProps> implements ICustomer {
|
||||||
|
private _address: PostalAddress;
|
||||||
|
private _defaultTaxes: CustomerTaxes;
|
||||||
|
|
||||||
|
protected constructor(
|
||||||
|
props: CustomerInternalProps,
|
||||||
|
address: PostalAddress,
|
||||||
|
defaultTaxes: CustomerTaxes,
|
||||||
|
id?: UniqueID
|
||||||
|
) {
|
||||||
|
super(props, id);
|
||||||
|
this._address = address;
|
||||||
|
this._defaultTaxes = defaultTaxes;
|
||||||
|
}
|
||||||
|
|
||||||
static create(props: ICustomerCreateProps, id?: UniqueID): Result<Customer, Error> {
|
static create(props: ICustomerCreateProps, id?: UniqueID): Result<Customer, Error> {
|
||||||
const validationResult = Customer.validateCreateProps(props);
|
const validationResult = Customer.validateCreateProps(props);
|
||||||
|
|
||||||
@ -108,30 +119,27 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
|
|||||||
|
|
||||||
const { address, defaultTaxes, ...internalProps } = props;
|
const { address, defaultTaxes, ...internalProps } = props;
|
||||||
|
|
||||||
|
// Postal Address
|
||||||
const postalAddressResult = PostalAddress.create(address);
|
const postalAddressResult = PostalAddress.create(address);
|
||||||
|
|
||||||
if (postalAddressResult.isFailure) {
|
if (postalAddressResult.isFailure) {
|
||||||
return Result.fail(postalAddressResult.error);
|
return Result.fail(postalAddressResult.error);
|
||||||
}
|
}
|
||||||
|
const postalAddress = postalAddressResult.data;
|
||||||
|
|
||||||
const taxes = CustomerTaxes.create(defaultTaxes);
|
// Customer Taxes
|
||||||
if (taxes.isFailure) {
|
const taxesResult = CustomerTaxes.create(defaultTaxes);
|
||||||
return Result.fail(taxes.error);
|
if (taxesResult.isFailure) {
|
||||||
|
return Result.fail(taxesResult.error);
|
||||||
}
|
}
|
||||||
|
const taxes = taxesResult.data;
|
||||||
const contact = new Customer(
|
|
||||||
{
|
|
||||||
...internalProps,
|
|
||||||
defaultTaxes: taxes.data,
|
|
||||||
address: postalAddressResult.data,
|
|
||||||
},
|
|
||||||
id
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reglas de negocio / validaciones
|
// Reglas de negocio / validaciones
|
||||||
// ...
|
// ...
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
|
// Crear instancia de Customer
|
||||||
|
const contact = new Customer(internalProps, postalAddress, taxes, id);
|
||||||
|
|
||||||
// Disparar eventos de dominio
|
// Disparar eventos de dominio
|
||||||
// ...
|
// ...
|
||||||
// ...
|
// ...
|
||||||
@ -144,31 +152,56 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rehidratación desde persistencia
|
// Rehidratación desde persistencia
|
||||||
static rehydrate(props: CustomerInternalProps, id: UniqueID): Customer {
|
static rehydrate(
|
||||||
return new Customer(props, id);
|
props: CustomerInternalProps,
|
||||||
|
address: PostalAddress,
|
||||||
|
defaultTaxes: CustomerTaxes,
|
||||||
|
id: UniqueID
|
||||||
|
): Customer {
|
||||||
|
return new Customer(props, address, defaultTaxes, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(partialCustomer: CustomerPatchProps): Result<void, Error> {
|
public update(partialCustomer: CustomerPatchProps): Result<void, Error> {
|
||||||
const { address: partialAddress, defaultTaxes: partialTaxes, ...rest } = partialCustomer;
|
const { address: partialAddress, defaultTaxes: partialTaxes, ...rest } = partialCustomer;
|
||||||
|
|
||||||
Object.assign(this.props, rest);
|
const nextProps: CustomerInternalProps = {
|
||||||
|
...this.props,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
|
||||||
|
let nextAddress = this._address;
|
||||||
|
let nextDefaultTaxes = this._defaultTaxes;
|
||||||
|
|
||||||
if (partialAddress) {
|
if (partialAddress) {
|
||||||
const addressResult = this.address.update(partialAddress);
|
const nextAddressResult = PostalAddress.create({
|
||||||
|
...this._address.getProps(),
|
||||||
|
...partialAddress,
|
||||||
|
});
|
||||||
|
|
||||||
if (addressResult.isFailure) {
|
if (nextAddressResult.isFailure) {
|
||||||
return Result.fail(addressResult.error);
|
return Result.fail(nextAddressResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextAddress = nextAddressResult.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (partialTaxes) {
|
if (partialTaxes) {
|
||||||
const taxesResult = this.defaultTaxes.update(partialTaxes);
|
const nextTaxesResult = CustomerTaxes.create({
|
||||||
|
...this._defaultTaxes.getProps(),
|
||||||
|
...partialTaxes,
|
||||||
|
});
|
||||||
|
|
||||||
if (taxesResult.isFailure) {
|
if (nextTaxesResult.isFailure) {
|
||||||
return Result.fail(taxesResult.error);
|
return Result.fail(nextTaxesResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextDefaultTaxes = nextTaxesResult.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object.assign(this.props, nextProps);
|
||||||
|
this._address = nextAddress;
|
||||||
|
this._defaultTaxes = nextDefaultTaxes;
|
||||||
|
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,7 +240,7 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get address(): PostalAddress {
|
public get address(): PostalAddress {
|
||||||
return this.props.address;
|
return this._address;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get emailPrimary(): Maybe<EmailAddress> {
|
public get emailPrimary(): Maybe<EmailAddress> {
|
||||||
@ -247,7 +280,7 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get defaultTaxes(): CustomerTaxes {
|
public get defaultTaxes(): CustomerTaxes {
|
||||||
return this.props.defaultTaxes;
|
return this._defaultTaxes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get languageCode(): LanguageCode {
|
public get languageCode(): LanguageCode {
|
||||||
|
|||||||
@ -24,11 +24,6 @@ export class CustomerTaxes extends ValueObject<CustomerTaxesProps> implements IC
|
|||||||
return Result.ok(new CustomerTaxes(props));
|
return Result.ok(new CustomerTaxes(props));
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(partial: CustomerTaxesPatchProps): Result<CustomerTaxes, Error> {
|
|
||||||
Object.assign(this.props, partial);
|
|
||||||
return Result.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reconstruye una instancia de CustomerTaxes a partir de una clave serializada.
|
* Reconstruye una instancia de CustomerTaxes a partir de una clave serializada.
|
||||||
* Este método es la operación inversa de toKey().
|
* Este método es la operación inversa de toKey().
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export * from "./sequelize-customer.mapper";
|
export * from "./sequelize-customer-domain.mapper";
|
||||||
|
|||||||
@ -26,9 +26,9 @@ import { Result } from "@repo/rdx-utils";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Customer,
|
Customer,
|
||||||
|
type CustomerInternalProps,
|
||||||
CustomerStatus,
|
CustomerStatus,
|
||||||
CustomerTaxes,
|
CustomerTaxes,
|
||||||
type ICustomerCreateProps,
|
|
||||||
} from "../../../../../domain";
|
} from "../../../../../domain";
|
||||||
import type { CustomerCreationAttributes, CustomerModel } from "../../models";
|
import type { CustomerCreationAttributes, CustomerModel } from "../../models";
|
||||||
|
|
||||||
@ -206,7 +206,7 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
|
|||||||
return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors));
|
return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors));
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerProps: ICustomerCreateProps = {
|
const customerProps: CustomerInternalProps = {
|
||||||
companyId: companyId!,
|
companyId: companyId!,
|
||||||
status: status!,
|
status: status!,
|
||||||
reference: reference!,
|
reference: reference!,
|
||||||
@ -216,8 +216,6 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
|
|||||||
tradeName: tradeName!,
|
tradeName: tradeName!,
|
||||||
tin: tinNumber!,
|
tin: tinNumber!,
|
||||||
|
|
||||||
address: postalAddress!,
|
|
||||||
|
|
||||||
emailPrimary: emailPrimaryAddress!,
|
emailPrimary: emailPrimaryAddress!,
|
||||||
emailSecondary: emailSecondaryAddress!,
|
emailSecondary: emailSecondaryAddress!,
|
||||||
phonePrimary: phonePrimaryNumber!,
|
phonePrimary: phonePrimaryNumber!,
|
||||||
@ -228,12 +226,17 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
|
|||||||
website: website!,
|
website: website!,
|
||||||
|
|
||||||
legalRecord: legalRecord!,
|
legalRecord: legalRecord!,
|
||||||
defaultTaxes: defaultTaxes!,
|
|
||||||
languageCode: languageCode!,
|
languageCode: languageCode!,
|
||||||
currencyCode: currencyCode!,
|
currencyCode: currencyCode!,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Customer.create(customerProps, customerId);
|
const customer = Customer.rehydrate(
|
||||||
|
customerProps,
|
||||||
|
postalAddress!,
|
||||||
|
defaultTaxes!,
|
||||||
|
customerId!
|
||||||
|
);
|
||||||
|
return Result.ok(customer);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
return Result.fail(err as Error);
|
return Result.fail(err as Error);
|
||||||
}
|
}
|
||||||
@ -1,39 +1,67 @@
|
|||||||
|
import {
|
||||||
|
CountryCodeSchema,
|
||||||
|
EmailSchema,
|
||||||
|
LandPhoneSchema,
|
||||||
|
MobilePhoneSchema,
|
||||||
|
PostalCodeSchema,
|
||||||
|
TinSchema,
|
||||||
|
URLSchema,
|
||||||
|
} from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
export const UpdateCustomerByIdParamsRequestSchema = z.object({
|
export const UpdateCustomerByIdParamsRequestSchema = z.object({
|
||||||
customer_id: z.string(),
|
customer_id: z.uuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UpdateCustomerByIdParamsRequestDTO = z.infer<
|
||||||
|
typeof UpdateCustomerByIdParamsRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const UpdateCustomerAddressPatchRequestSchema = z.object({
|
||||||
|
street: z.string().nullable().optional(),
|
||||||
|
street2: z.string().nullable().optional(),
|
||||||
|
city: z.string().nullable().optional(),
|
||||||
|
province: z.string().nullable().optional(),
|
||||||
|
postal_code: PostalCodeSchema.nullable().optional(),
|
||||||
|
country: CountryCodeSchema.nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateCustomerContactPatchRequestSchema = z.object({
|
||||||
|
email_primary: EmailSchema.nullable().optional(),
|
||||||
|
email_secondary: EmailSchema.nullable().optional(),
|
||||||
|
phone_primary: LandPhoneSchema.nullable().optional(),
|
||||||
|
phone_secondary: LandPhoneSchema.nullable().optional(),
|
||||||
|
mobile_primary: MobilePhoneSchema.nullable().optional(),
|
||||||
|
mobile_secondary: MobilePhoneSchema.nullable().optional(),
|
||||||
|
fax: LandPhoneSchema.nullable().optional(),
|
||||||
|
website: URLSchema.nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateCustomerByIdRequestSchema = z.object({
|
export const UpdateCustomerByIdRequestSchema = z.object({
|
||||||
reference: z.string().optional(),
|
reference: z.string().nullable().optional(),
|
||||||
|
|
||||||
is_company: z.string().optional(),
|
is_company: z.boolean().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
trade_name: z.string().optional(),
|
trade_name: z.string().nullable().optional(),
|
||||||
tin: z.string().optional(),
|
tin: TinSchema.nullable().optional(),
|
||||||
default_taxes: z.string().optional(), // completo (sustituye), o null => vaciar
|
|
||||||
|
|
||||||
street: z.string().optional(),
|
default_taxes: z.string().nullable().optional(),
|
||||||
street2: z.string().optional(),
|
|
||||||
city: z.string().optional(),
|
|
||||||
province: z.string().optional(),
|
|
||||||
postal_code: z.string().optional(),
|
|
||||||
country: z.string().optional(),
|
|
||||||
|
|
||||||
email_primary: z.string().optional(),
|
address: UpdateCustomerAddressPatchRequestSchema.optional(),
|
||||||
email_secondary: z.string().optional(),
|
contact: UpdateCustomerContactPatchRequestSchema.optional(),
|
||||||
phone_primary: z.string().optional(),
|
|
||||||
phone_secondary: z.string().optional(),
|
|
||||||
mobile_primary: z.string().optional(),
|
|
||||||
mobile_secondary: z.string().optional(),
|
|
||||||
|
|
||||||
fax: z.string().optional(),
|
legal_record: z.string().nullable().optional(),
|
||||||
website: z.string().optional(),
|
|
||||||
|
|
||||||
legal_record: z.string().optional(),
|
|
||||||
|
|
||||||
language_code: z.string().optional(),
|
language_code: z.string().optional(),
|
||||||
currency_code: z.string().optional(),
|
currency_code: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UpdateCustomerByIdRequestDTO = Partial<z.infer<typeof UpdateCustomerByIdRequestSchema>>;
|
export type UpdateCustomerAddressPatchRequestDTO = z.infer<
|
||||||
|
typeof UpdateCustomerAddressPatchRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type UpdateCustomerContactPatchRequestDTO = z.infer<
|
||||||
|
typeof UpdateCustomerContactPatchRequestSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type UpdateCustomerByIdRequestDTO = z.infer<typeof UpdateCustomerByIdRequestSchema>;
|
||||||
|
|||||||
@ -1,39 +1,57 @@
|
|||||||
import { MetadataSchema } from "@erp/core";
|
import {
|
||||||
|
CountryCodeSchema,
|
||||||
|
CurrencyCodeSchema,
|
||||||
|
EmailSchema,
|
||||||
|
LandPhoneSchema,
|
||||||
|
LanguageCodeSchema,
|
||||||
|
MetadataSchema,
|
||||||
|
MobilePhoneSchema,
|
||||||
|
PostalCodeSchema,
|
||||||
|
TinSchema,
|
||||||
|
URLSchema,
|
||||||
|
} from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import { CustomerStatusSchema } from "../shared/customer-status.dto";
|
||||||
|
|
||||||
export const GetCustomerByIdResponseSchema = z.object({
|
export const GetCustomerByIdResponseSchema = z.object({
|
||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
company_id: z.uuid(),
|
company_id: z.uuid(),
|
||||||
reference: z.string(),
|
status: CustomerStatusSchema,
|
||||||
|
reference: z.string().nullable(),
|
||||||
|
|
||||||
is_company: z.string(),
|
is_company: z.boolean(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
trade_name: z.string(),
|
trade_name: z.string().nullable(),
|
||||||
tin: z.string(),
|
tin: TinSchema.nullable(),
|
||||||
|
|
||||||
street: z.string(),
|
address: z.object({
|
||||||
street2: z.string(),
|
street: z.string().nullable(),
|
||||||
city: z.string(),
|
street2: z.string().nullable(),
|
||||||
province: z.string(),
|
city: z.string().nullable(),
|
||||||
postal_code: z.string(),
|
province: z.string().nullable(),
|
||||||
country: z.string(),
|
postal_code: PostalCodeSchema.nullable(),
|
||||||
|
country: CountryCodeSchema.nullable(),
|
||||||
|
}),
|
||||||
|
|
||||||
email_primary: z.string(),
|
contact: z.object({
|
||||||
email_secondary: z.string(),
|
email_primary: EmailSchema.nullable(),
|
||||||
phone_primary: z.string(),
|
email_secondary: EmailSchema.nullable(),
|
||||||
phone_secondary: z.string(),
|
phone_primary: LandPhoneSchema.nullable(),
|
||||||
mobile_primary: z.string(),
|
phone_secondary: LandPhoneSchema.nullable(),
|
||||||
mobile_secondary: z.string(),
|
mobile_primary: MobilePhoneSchema.nullable(),
|
||||||
|
mobile_secondary: MobilePhoneSchema.nullable(),
|
||||||
|
|
||||||
fax: z.string(),
|
fax: LandPhoneSchema.nullable(),
|
||||||
website: z.string(),
|
website: URLSchema.nullable(),
|
||||||
|
}),
|
||||||
|
|
||||||
legal_record: z.string(),
|
legal_record: z.string().nullable(),
|
||||||
|
|
||||||
default_taxes: z.string(),
|
default_taxes: z.string().nullable(),
|
||||||
status: z.string(),
|
|
||||||
language_code: z.string(),
|
language_code: LanguageCodeSchema,
|
||||||
currency_code: z.string(),
|
currency_code: CurrencyCodeSchema,
|
||||||
|
|
||||||
metadata: MetadataSchema.optional(),
|
metadata: MetadataSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
export * from "../shared/customer-summary.dto";
|
||||||
|
|
||||||
export * from "./create-customer.result.dto";
|
export * from "./create-customer.result.dto";
|
||||||
export * from "./get-customer-by-id.response.dto";
|
export * from "./get-customer-by-id.response.dto";
|
||||||
export * from "./list-customers.response.dto";
|
export * from "./list-customers.response.dto";
|
||||||
|
|||||||
@ -1,39 +1,8 @@
|
|||||||
import { MetadataSchema, createPaginatedListSchema } from "@erp/core";
|
import { createPaginatedListSchema } from "@erp/core";
|
||||||
import { z } from "zod/v4";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
export const ListCustomersResponseSchema = createPaginatedListSchema(
|
import { CustomerSummarySchema } from "../shared";
|
||||||
z.object({
|
|
||||||
id: z.uuid(),
|
|
||||||
company_id: z.uuid(),
|
|
||||||
status: z.string(),
|
|
||||||
reference: z.string(),
|
|
||||||
|
|
||||||
is_company: z.string(),
|
export const ListCustomersResponseSchema = createPaginatedListSchema(CustomerSummarySchema);
|
||||||
name: z.string(),
|
|
||||||
trade_name: z.string(),
|
|
||||||
tin: z.string(),
|
|
||||||
|
|
||||||
street: z.string(),
|
|
||||||
street2: z.string(),
|
|
||||||
city: z.string(),
|
|
||||||
province: z.string(),
|
|
||||||
postal_code: z.string(),
|
|
||||||
country: z.string(),
|
|
||||||
|
|
||||||
email_primary: z.string(),
|
|
||||||
email_secondary: z.string(),
|
|
||||||
phone_primary: z.string(),
|
|
||||||
phone_secondary: z.string(),
|
|
||||||
mobile_primary: z.string(),
|
|
||||||
mobile_secondary: z.string(),
|
|
||||||
fax: z.string(),
|
|
||||||
website: z.string(),
|
|
||||||
|
|
||||||
language_code: z.string(),
|
|
||||||
currency_code: z.string(),
|
|
||||||
|
|
||||||
metadata: MetadataSchema.optional(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ListCustomersResponseDTO = z.infer<typeof ListCustomersResponseSchema>;
|
export type ListCustomersResponseDTO = z.infer<typeof ListCustomersResponseSchema>;
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const CustomerStatusSchema = z.enum(["active", "inactive"]);
|
||||||
|
|
||||||
|
export type CustomerStatusDTO = z.infer<typeof CustomerStatusSchema>;
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
CountryCodeSchema,
|
||||||
|
EmailSchema,
|
||||||
|
LandPhoneSchema,
|
||||||
|
MobilePhoneSchema,
|
||||||
|
PostalCodeSchema,
|
||||||
|
TinSchema,
|
||||||
|
} from "@erp/core";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import { CustomerStatusSchema } from "./customer-status.dto";
|
||||||
|
|
||||||
|
export const CustomerSummarySchema = z.object({
|
||||||
|
id: z.uuid(),
|
||||||
|
company_id: z.uuid(),
|
||||||
|
status: CustomerStatusSchema,
|
||||||
|
|
||||||
|
reference: z.string().nullable(),
|
||||||
|
|
||||||
|
is_company: z.boolean(),
|
||||||
|
|
||||||
|
name: z.string(),
|
||||||
|
trade_name: z.string().nullable(),
|
||||||
|
tin: TinSchema.nullable(),
|
||||||
|
|
||||||
|
address: z.object({
|
||||||
|
street: z.string().nullable(),
|
||||||
|
city: z.string().nullable(),
|
||||||
|
province: z.string().nullable(),
|
||||||
|
postal_code: PostalCodeSchema.nullable(),
|
||||||
|
country: CountryCodeSchema.nullable(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
contact: z.object({
|
||||||
|
email_primary: EmailSchema.nullable(),
|
||||||
|
phone_primary: LandPhoneSchema.nullable(),
|
||||||
|
mobile_primary: MobilePhoneSchema.nullable(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CustomerSummaryDTO = z.infer<typeof CustomerSummarySchema>;
|
||||||
2
modules/customers/src/common/dto/shared/index.ts
Normal file
2
modules/customers/src/common/dto/shared/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./customer-status.dto";
|
||||||
|
export * from "./customer-summary.dto";
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import { safeHTTPUrl } from "@erp/core/client";
|
|
||||||
import { DataTableColumnHeader, InitialsAvatar } from "@repo/rdx-ui/components";
|
import { DataTableColumnHeader, InitialsAvatar } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@ -101,11 +100,11 @@ export function useCustomersGridColumns(
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
`${row.primaryEmail} ${row.primaryPhone} ${row.primaryMobile} ${row.website}`,
|
`${row.contact.primaryEmail} ${row.contact.primaryPhone} ${row.contact.primaryMobile}`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
size: 140,
|
size: 140,
|
||||||
minSize: 120,
|
minSize: 120,
|
||||||
cell: ({ row }) => <ContactCell customer={row.original} />,
|
cell: ({ row }) => <ContactCell contact={row.original.contact} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "address",
|
id: "address",
|
||||||
@ -117,11 +116,11 @@ export function useCustomersGridColumns(
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
`${row.street} ${row.street2} ${row.city} ${row.postalCode} ${row.province} ${row.country}`,
|
`${row.address.street} ${row.address.city} ${row.address.postalCode} ${row.address.province} ${row.address.country}`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
size: 140,
|
size: 140,
|
||||||
minSize: 120,
|
minSize: 120,
|
||||||
cell: ({ row }) => <AddressCell address={row.original} />,
|
cell: ({ row }) => <AddressCell address={row.original.address} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
@ -138,7 +137,7 @@ export function useCustomersGridColumns(
|
|||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const customer = row.original;
|
const customer = row.original;
|
||||||
const { website, primaryEmail: email_primary } = customer;
|
const { primaryEmail } = customer.contact;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
@ -177,23 +176,17 @@ export function useCustomersGridColumns(
|
|||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem
|
{primaryEmail && (
|
||||||
disabled={!website}
|
<>
|
||||||
onClick={() =>
|
<DropdownMenuItem
|
||||||
window.open(safeHTTPUrl(website), "_blank", "noopener,noreferrer")
|
disabled={!primaryEmail}
|
||||||
}
|
onClick={() => navigator.clipboard.writeText(primaryEmail)}
|
||||||
>
|
>
|
||||||
{t("pages.list.actions.visit_website")}
|
{t("pages.list.actions.copy_email")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
</>
|
||||||
disabled={!email_primary}
|
)}
|
||||||
onClick={() => navigator.clipboard.writeText(email_primary)}
|
|
||||||
>
|
|
||||||
{t("pages.list.actions.copy_email")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-destructive"
|
className="text-destructive"
|
||||||
|
|||||||
@ -3,24 +3,41 @@ import { ExternalLinkIcon, MapPinIcon } from "lucide-react";
|
|||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
|
|
||||||
export interface CustomerAddress {
|
export interface CustomerAddress {
|
||||||
street: string;
|
street?: string | null;
|
||||||
street2: string;
|
street2?: string | null;
|
||||||
postalCode: string;
|
city?: string | null;
|
||||||
city: string;
|
province?: string | null;
|
||||||
province: string;
|
postalCode?: string | null;
|
||||||
country: string;
|
country?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getGoogleMapsUrl = (adress: CustomerAddress) => {
|
const clean = (value?: string | null) => value?.trim() ?? "";
|
||||||
const fullAddress = `${adress.street}, ${adress.postalCode} ${adress.city}, ${adress.province}, ${adress.country}`;
|
|
||||||
|
const join = (parts: Array<string | null | undefined>, separator: string) =>
|
||||||
|
parts.map(clean).filter(Boolean).join(separator);
|
||||||
|
|
||||||
|
const getGoogleMapsUrl = (address: CustomerAddress) => {
|
||||||
|
const fullAddress = join(
|
||||||
|
[
|
||||||
|
join([address.street, address.street2], ", "),
|
||||||
|
join([address.postalCode, address.city], " "),
|
||||||
|
address.province,
|
||||||
|
address.country,
|
||||||
|
],
|
||||||
|
", "
|
||||||
|
);
|
||||||
|
|
||||||
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(fullAddress)}`;
|
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(fullAddress)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddressCell = ({ address }: { address: CustomerAddress }) => {
|
export const AddressCell = ({ address }: { address: CustomerAddress }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const line1 = [address.street, address.street2].filter(Boolean).join(", ");
|
|
||||||
const line2 = [address.postalCode, address.city].filter(Boolean).join(" ");
|
const line1 = join([address.street, address.street2], ", ");
|
||||||
const line3 = [address.province, address.country].filter(Boolean).join(", ");
|
const line2 = join([address.postalCode, address.city], " ");
|
||||||
|
const line3 = join([address.province, address.country], ", ");
|
||||||
|
const line23 = join([line2, line3], " - ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<address className="not-italic flex items-start gap-2 text-muted-foreground hover:text-primary transition-colors">
|
<address className="not-italic flex items-start gap-2 text-muted-foreground hover:text-primary transition-colors">
|
||||||
<a
|
<a
|
||||||
@ -32,24 +49,14 @@ export const AddressCell = ({ address }: { address: CustomerAddress }) => {
|
|||||||
title={t("components.address_cell.open_in_google_maps")}
|
title={t("components.address_cell.open_in_google_maps")}
|
||||||
>
|
>
|
||||||
<MapPinIcon className="mt-0.5 size-4 shrink-0 text-muted-foreground group-hover:text-primary" />
|
<MapPinIcon className="mt-0.5 size-4 shrink-0 text-muted-foreground group-hover:text-primary" />
|
||||||
|
|
||||||
<div className="text-sm text-muted-foreground group-hover:text-foreground">
|
<div className="text-sm text-muted-foreground group-hover:text-foreground">
|
||||||
<p>{line1}</p>
|
{line1 && <p>{line1}</p>}
|
||||||
<p>
|
{line23 && <p>{line23}</p>}
|
||||||
{line2} - {line3}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ExternalLinkIcon className="mt-0.5 size-3 shrink-0 text-muted-foreground opacity-0 group-hover:opacity-100" />
|
<ExternalLinkIcon className="mt-0.5 size-3 shrink-0 text-muted-foreground opacity-0 group-hover:opacity-100" />
|
||||||
</a>
|
</a>
|
||||||
</address>
|
</address>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
<address className="not-italic grid gap-1 text-foreground text-sm">
|
|
||||||
<div>{line1 || <Soft>-</Soft>}</div>
|
|
||||||
<div>{[line2, line3].filter(Boolean).join(" • ")}</div>
|
|
||||||
</address>
|
|
||||||
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|||||||
@ -1,67 +1,81 @@
|
|||||||
import type { CustomerListRow } from "@erp/customers/web/shared";
|
import { GlobeIcon, MailIcon, PhoneIcon, PrinterIcon, SmartphoneIcon } from "lucide-react";
|
||||||
import { MailIcon, PhoneIcon } from "lucide-react";
|
|
||||||
|
|
||||||
export const ContactCell = ({ customer }: { customer: CustomerListRow }) => (
|
interface ContactCellData {
|
||||||
<div className="flex flex-col gap-1.5 text-sm text-muted-foreground transition-colors ">
|
primaryEmail?: string | null;
|
||||||
{customer.primaryEmail && (
|
secondaryEmail?: string | null;
|
||||||
<a
|
primaryPhone?: string | null;
|
||||||
className="flex items-center gap-2 hover:text-foreground"
|
secondaryPhone?: string | null;
|
||||||
href={`mailto:${customer.primaryEmail}`}
|
primaryMobile?: string | null;
|
||||||
>
|
secondaryMobile?: string | null;
|
||||||
<MailIcon className="size-3.5" />
|
website?: string | null;
|
||||||
{customer.primaryEmail}
|
fax?: string | null;
|
||||||
</a>
|
}
|
||||||
)}
|
|
||||||
|
|
||||||
{customer.secondaryEmail && (
|
const clean = (value?: string | null) => value?.trim() ?? "";
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:text-foreground"
|
|
||||||
href={`mailto:${customer.secondaryEmail}`}
|
|
||||||
>
|
|
||||||
<MailIcon className="size-3.5" />
|
|
||||||
{customer.secondaryEmail}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{customer.primaryPhone && (
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:text-foreground"
|
|
||||||
href={`tel:${customer.primaryPhone}`}
|
|
||||||
>
|
|
||||||
<PhoneIcon className="size-3.5" />
|
|
||||||
{customer.primaryPhone}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
type ContactItem = {
|
||||||
|
value: string;
|
||||||
|
href: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
<div className="grid gap-1 text-foreground text-sm my-1.5">
|
export const ContactCell = ({ contact }: { contact: ContactCellData }) => {
|
||||||
{customer.email_primary && (
|
const emails = [contact.primaryEmail, contact.secondaryEmail].map(clean).filter(Boolean);
|
||||||
<div className="flex items-center gap-2">
|
const mobiles = [contact.primaryMobile, contact.secondaryMobile].map(clean).filter(Boolean);
|
||||||
<MailIcon className="size-3.5" />
|
const phones = [contact.primaryPhone, contact.secondaryPhone].map(clean).filter(Boolean);
|
||||||
<a className="group" href={`mailto:${customer.email_primary}`}>
|
const website = clean(contact.website);
|
||||||
{customer.email_primary}
|
const fax = clean(contact.fax);
|
||||||
|
|
||||||
|
const items: ContactItem[] = [
|
||||||
|
...emails.map((v) => ({
|
||||||
|
value: v,
|
||||||
|
href: `mailto:${v}`,
|
||||||
|
icon: <MailIcon className="size-3.5" />,
|
||||||
|
})),
|
||||||
|
|
||||||
|
...mobiles.map((v) => ({
|
||||||
|
value: v,
|
||||||
|
href: `tel:${v}`,
|
||||||
|
icon: <SmartphoneIcon className="size-3.5" />,
|
||||||
|
})),
|
||||||
|
|
||||||
|
...phones.map((v) => ({
|
||||||
|
value: v,
|
||||||
|
href: `tel:${v}`,
|
||||||
|
icon: <PhoneIcon className="size-3.5" />,
|
||||||
|
})),
|
||||||
|
|
||||||
|
...(website
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
value: website,
|
||||||
|
href: website.startsWith("http") ? website : `https://${website}`,
|
||||||
|
icon: <GlobeIcon className="size-3.5" />,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
|
||||||
|
...(fax
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
value: fax,
|
||||||
|
href: `tel:${fax}`,
|
||||||
|
icon: <PrinterIcon className="size-3.5" />,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (items.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1.5 text-sm text-muted-foreground transition-colors">
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<a className="flex items-center gap-2 hover:text-foreground" href={item.href} key={idx}>
|
||||||
|
{item.icon}
|
||||||
|
{item.value}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
))}
|
||||||
)}
|
|
||||||
|
|
||||||
{customer.email_secondary && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<MailIcon className="size-3.5" />
|
|
||||||
{customer.email_secondary}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<PhoneIcon className="size-3.5 group" />
|
|
||||||
<span>{customer.phone_primary || customer.mobile_primary || <Soft>-</Soft>}</span>
|
|
||||||
{customer.phone_secondary && <Soft>• {customer.phone_secondary}</Soft>}
|
|
||||||
{customer.mobile_secondary && <Soft>• {customer.mobile_secondary}</Soft>}
|
|
||||||
{false}
|
|
||||||
</div>
|
</div>
|
||||||
{false}
|
);
|
||||||
</div>
|
};
|
||||||
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import type { Customer } from "../entities";
|
|||||||
|
|
||||||
export const GetCustomerByIdAdapter = {
|
export const GetCustomerByIdAdapter = {
|
||||||
fromDTO(dto: GetCustomerByIdResult, context?: unknown): Customer {
|
fromDTO(dto: GetCustomerByIdResult, context?: unknown): Customer {
|
||||||
const taxesAdapter = (taxes: string) =>
|
const taxesAdapter = (taxes: string | null) =>
|
||||||
taxes.split(";").filter((item) => item !== "#" && item.trim() !== "");
|
taxes?.split(";").filter((item) => item !== "#" && item.trim() !== "") || null;
|
||||||
|
|
||||||
const defaultTaxes = taxesAdapter(dto.default_taxes);
|
const defaultTaxes = taxesAdapter(dto.default_taxes);
|
||||||
|
|
||||||
@ -22,27 +22,30 @@ export const GetCustomerByIdAdapter = {
|
|||||||
companyId: dto.company_id,
|
companyId: dto.company_id,
|
||||||
reference: dto.reference,
|
reference: dto.reference,
|
||||||
|
|
||||||
isCompany: dto.is_company === "1",
|
isCompany: dto.is_company,
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
tradeName: dto.trade_name,
|
tradeName: dto.trade_name,
|
||||||
tin: dto.tin,
|
tin: dto.tin,
|
||||||
|
|
||||||
street: dto.street,
|
address: {
|
||||||
street2: dto.street2,
|
street: dto.address.street,
|
||||||
city: dto.city,
|
street2: dto.address.street2,
|
||||||
province: dto.province,
|
city: dto.address.city,
|
||||||
postalCode: dto.postal_code,
|
province: dto.address.province,
|
||||||
country: dto.country,
|
postalCode: dto.address.postal_code,
|
||||||
|
country: dto.address.country,
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
primaryEmail: dto.contact.email_primary,
|
||||||
|
secondaryEmail: dto.contact.email_secondary,
|
||||||
|
primaryPhone: dto.contact.phone_primary,
|
||||||
|
secondaryPhone: dto.contact.phone_secondary,
|
||||||
|
primaryMobile: dto.contact.mobile_primary,
|
||||||
|
secondaryMobile: dto.contact.mobile_secondary,
|
||||||
|
|
||||||
primaryEmail: dto.email_primary,
|
fax: dto.contact.fax,
|
||||||
secondaryEmail: dto.email_secondary,
|
website: dto.contact.website,
|
||||||
primaryPhone: dto.phone_primary,
|
},
|
||||||
secondaryPhone: dto.phone_secondary,
|
|
||||||
primaryMobile: dto.mobile_primary,
|
|
||||||
secondaryMobile: dto.mobile_secondary,
|
|
||||||
|
|
||||||
fax: dto.fax,
|
|
||||||
website: dto.website,
|
|
||||||
|
|
||||||
legalRecord: dto.legal_record,
|
legalRecord: dto.legal_record,
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user