Uecko_ERP/docs/DTOS - GUÍA DE DISEÑO.md
2026-04-20 19:20:42 +02:00

412 lines
11 KiB
Markdown

# Guía de diseño de DTOs para el ERP
## 1. Principios generales
- Un DTO representa únicamente el contrato de transporte entre cliente y servidor.
- Cada endpoint debe tener sus DTOs de `request` y `response`, aunque internamente reutilice subesquemas.
- No reutilizar el mismo DTO entre endpoints salvo que el contrato sea realmente idéntico y estable.
- Siempre que sea posible, definir DTOs a partir de esquemas Zod v4 e inferir el tipo desde el esquema.
```ts
export type ListIssuedInvoicesRequestDTO = z.infer<typeof ListIssuedInvoicesRequestSchema>;
```
- El esquema valida; no debe “arreglar” entradas. Evitar `.default()`, `.transform()` y similares salvo excepción justificada.
- No mezclar DTOs con lógica de negocio, mapping ni estado de formulario.
## 2. Organización y naming
```plaintext
common/dto/
request/
create-proforma.request.dto.ts
get-proforma-by-id.request.dto.ts
list-proformas.request.dto.ts
update-proforma-by-id.request.dto.ts
...
response/
create-proforma.response.dto.ts
get-proforma-by-id.response.dto.ts
list-proformas.response.dto.ts
update-proforma-by-id.response.dto.ts
...
shared/
proforma-summary.dto.ts
proforma-detail.dto.ts
proforma-item.dto.ts
proforma-tax.dto.ts
recipient.dto.ts
```
* Estructurar DTOs por módulo en `request/`, `response/` y `shared/`.
* `shared/` solo para subesquemas reutilizables y estables.
* Nombres de fichero en `kebab-case`.
* Patrón de nombres:
```plaintext
<action>-<resource>[...].request.dto.ts
<action>-<resource>[...].response.dto.ts
```
* Usar singular/plural de forma consistente:
* colección -> plural
* elemento -> singular
Ejemplos:
```plaintext
list-proformas.request.dto.ts
get-proforma-by-id.response.dto.ts
update-proforma-by-id.request.dto.ts
```
* `<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.
## 3. Reglas de tipado
* 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:
* booleano -> `z.boolean()`
* UUID -> `z.uuid()`
* conjunto cerrado -> `z.enum([...])`
* fecha/timestamp ISO -> schema específico
* arrays -> `z.array(...)`
* objetos -> `z.object(...)`
## 4. Reglas para primitivas de negocio
* 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.
## 5. Fechas, porcentajes, importes, cantidades
### 5.1. Amount, Money, Percentage y Quantity
* Los valores exactos no deben viajar como `number`.
* Deben viajar siempre con una única forma de transporte para toda la API.
```ts
type MoneyDTO = {
value: string;
scale: string;
currency_code: string;
};
type AmountDTO = {
value: string;
scale: string;
};
type PercentageDTO = {
value: string;
scale: string;
}
type QuantityDTO = {
value: string;
scale: string;
}
```
### 5.2. Fechas y timestamps
Distinguir entre:
* fecha de calendario sin hora -> "2020-01-01"
* timestamp con fecha y hora ->
```ts
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 se prefiere no usar `z.iso.date()`, crear schema propio.
## 6. Opcionalidad, nullabilidad y ausencia
Significados:
* `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
No cambiar estos significados salvo decisión explícita de contrato.
### 6.1. Para requests
* Usar `optional()` para campos no obligatorios.
* Usar `nullable()` solo cuando tenga semántica de negocio, por ejemplo “borrar” o “desvincular”.
Ejemplo:
```ts
payment_method_id: z.uuid().nullable().optional()
```
Interpretación:
* omitido -> no tocar
* `null` -> quitar relación
* 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
```
## 8. Reglas para UPDATE
* `update` debe seguir semántica tipo `PATCH`:
* solo viajan los campos a modificar
* los campos omitidos no se tocan
* La opcionalidad debe declararse en el esquema con `optional()`.
* No usar `Partial<>` sobre tipos inferidos si el esquema ya expresa opcionalidad.
* No usar defaults en `update`, especialmente en colecciones.
Incorrecto:
```ts
items: z.array(...).default([])
```
Correcto:
```ts
items: z.array(...).optional()
```
### 8.1. UPDATE de colecciones
* Por defecto, mantener reemplazo completo de la colección.
* Si se necesita granularidad real, usar una estructura explícita:
```ts
items: {
create: [...],
update: [...],
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 {
...
}
}
```
---
### Reglas obligatorias
* No duplicar el contrato mediante interfaces manuales paralelas si ya existe un schema Zod equivalente.
* Eliminar tipos como `ICustomerSummarySnapshot` si representan el mismo contrato que un DTO.
* 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”).
---
### 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
ListCustomersResponseSchema
```
Entonces:
* Subschema:
```ts
CustomerSummarySchema
```
* Builder:
```ts
CustomerSummarySnapshotBuilder -> CustomerSummaryDTO
```
No:
```ts
Builder -> ListCustomersResponseDTO
```
---
### Naming obligatorio
DTO:
```plaintext
<resource>-summary.dto.ts
<resource>-detail.dto.ts
<action>-<resource>.response.dto.ts
```
Builders:
```plaintext
<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 ❌
```
---
### 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.
* 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.
## 12. Metadata
* `metadata` solo debe contener datos auxiliares, trazabilidad no crítica, versionado o hints no funcionales.
* Si un dato es necesario para que el cliente funcione, no debe ir en `metadata`, sino en el DTO principal.
## 13. Evolución de la API
* Es mejor añadir campos que reinterpretar campos existentes.
* Cambiar el significado o la estructura de un campo implica cambio de contrato.
## 14. Criterio para detectar overengineering
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
* se extraen subesquemas que apenas se reutilizan
* el DTO intenta reflejar demasiado fielmente el dominio interno
Regla práctica:
* preferir duplicación pequeña y clara frente a abstracción prematura
## 15. Checklist de revisión
Antes de cerrar un DTO, comprobar:
1. ¿Representa solo contrato de transporte?
2. ¿Cada campo usa 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 ocultando intención?
5. ¿El naming es consistente?
6. ¿Las estructuras repetidas merecen extraerse?
7. ¿Hay abstracciones prematuras?
8. ¿El cliente puede consumirlo sin heurísticas frágiles?
9. ¿El mismo campo mantiene el mismo tipo en todo el módulo?
10. ¿El DTO refleja una vista concreta del endpoint?
---