Facturas de cliente
This commit is contained in:
parent
f47a97bfa0
commit
4a73456c46
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["esbenp.prettier-vscode", "biomejs.biome"]
|
"recommendations": ["biomejs.biome"]
|
||||||
}
|
}
|
||||||
|
|||||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -16,6 +16,7 @@
|
|||||||
"typescript.suggest.includeAutomaticOptionalChainCompletions": true,
|
"typescript.suggest.includeAutomaticOptionalChainCompletions": true,
|
||||||
"typescript.suggestionActions.enabled": true,
|
"typescript.suggestionActions.enabled": true,
|
||||||
"typescript.preferences.importModuleSpecifier": "shortest",
|
"typescript.preferences.importModuleSpecifier": "shortest",
|
||||||
|
"typescript.autoClosingTags": true,
|
||||||
|
|
||||||
"editor.quickSuggestions": {
|
"editor.quickSuggestions": {
|
||||||
"strings": "on"
|
"strings": "on"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
export const ListAccountsRequestSchema = z.object({});
|
export const ListAccountsRequestSchema = z.object({});
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ValueObject } from "@repo/rdx-ddd";
|
import { ValueObject } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
const RoleSchema = z.enum(["Admin", "User", "Manager", "Editor"]);
|
const RoleSchema = z.enum(["Admin", "User", "Manager", "Editor"]);
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { ValueObject } from "@repo/rdx-ddd";
|
import { ValueObject } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
interface HashPasswordProps {
|
interface HashPasswordProps {
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ValueObject } from "@repo/rdx-ddd";
|
import { ValueObject } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
interface PlainPasswordProps {
|
interface PlainPasswordProps {
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ValueObject } from "@repo/rdx-ddd";
|
import { ValueObject } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
interface TokenProps {
|
interface TokenProps {
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ValueObject } from "@repo/rdx-ddd";
|
import { ValueObject } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
interface UsernameProps {
|
interface UsernameProps {
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
export const RegisterUserSchema = z.object({
|
export const RegisterUserSchema = z.object({
|
||||||
username: z.string().min(3, "Username must be at least 3 characters long"),
|
username: z.string().min(3, "Username must be at least 3 characters long"),
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
export const ListUsersSchema = z.object({});
|
export const ListUsersSchema = z.object({});
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
export const ListContactsSchema = z.object({});
|
export const ListContactsSchema = z.object({});
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
export const ListCustomerInvoicesSchema = z.object({});
|
export const ListCustomerInvoicesSchema = z.object({});
|
||||||
export const GetCustomerInvoiceSchema = z.object({});
|
export const GetCustomerInvoiceSchema = z.object({});
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
export const ListCustomersSchema = z.object({});
|
export const ListCustomersSchema = z.object({});
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="factuges"></div>
|
<div id="factuges" class="app"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@ -41,7 +41,7 @@
|
|||||||
"i18next-browser-languagedetector": "^8.1.0",
|
"i18next-browser-languagedetector": "^8.1.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.56.2",
|
"react-hook-form": "^7.56.4",
|
||||||
"react-hook-form-persist": "^3.0.0",
|
"react-hook-form-persist": "^3.0.0",
|
||||||
"react-i18next": "^15.0.1",
|
"react-i18next": "^15.0.1",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.0",
|
||||||
|
|||||||
@ -38,6 +38,7 @@ export const AppRoutes = (): JSX.Element => {
|
|||||||
<Router>
|
<Router>
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
|
|
||||||
|
{/* Fallback Route */}
|
||||||
<Suspense fallback={<LoadingOverlay />}>
|
<Suspense fallback={<LoadingOverlay />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Auth Layout */}
|
{/* Auth Layout */}
|
||||||
@ -56,11 +57,7 @@ export const AppRoutes = (): JSX.Element => {
|
|||||||
<Route path='/settings' element={<ErrorPage />} />
|
<Route path='/settings' element={<ErrorPage />} />
|
||||||
<Route path='/catalog' element={<ErrorPage />} />
|
<Route path='/catalog' element={<ErrorPage />} />
|
||||||
<Route path='/quotes' element={<ErrorPage />} />
|
<Route path='/quotes' element={<ErrorPage />} />
|
||||||
<Route path='*' element={<ErrorPage />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Fallback Route */}
|
|
||||||
<Route path='*' element={<ErrorPage />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
"recommended": true,
|
"recommended": true,
|
||||||
"correctness": {
|
"correctness": {
|
||||||
"useExhaustiveDependencies": "info",
|
"useExhaustiveDependencies": "info",
|
||||||
"noUnreachable": "off"
|
"noUnreachable": "warn"
|
||||||
},
|
},
|
||||||
"complexity": {
|
"complexity": {
|
||||||
"noForEach": "off",
|
"noForEach": "off",
|
||||||
|
|||||||
150
docs/DTOS - GUIA DE ESTILO.md
Normal file
150
docs/DTOS - GUIA DE ESTILO.md
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# 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,6 +1,6 @@
|
|||||||
import { ValueObject } from "core/common/domain";
|
import { ValueObject } from "core/common/domain";
|
||||||
import { Maybe, Result } from "core/common/helpers";
|
import { Maybe, Result } from "core/common/helpers";
|
||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
interface IInvoiceItemDescriptionProps {
|
interface IInvoiceItemDescriptionProps {
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ValueObject } from "core/common/domain";
|
import { ValueObject } from "core/common/domain";
|
||||||
import { Result } from "core/common/helpers";
|
import { Result } from "core/common/helpers";
|
||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
interface IInvoiceNumberProps {
|
interface IInvoiceNumberProps {
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ValueObject } from "core/common/domain";
|
import { ValueObject } from "core/common/domain";
|
||||||
import { Maybe, Result } from "core/common/helpers";
|
import { Maybe, Result } from "core/common/helpers";
|
||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
interface IInvoiceSerieProps {
|
interface IInvoiceSerieProps {
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
export const ICreateInvoiceRequestSchema = z.object({
|
export const ICreateInvoiceRequestSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
|
|||||||
@ -14,18 +14,21 @@
|
|||||||
"@types/jest": "29.5.14",
|
"@types/jest": "29.5.14",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.3",
|
"@types/react-dom": "^19.1.3",
|
||||||
"typescript": "^5.8.3"
|
"ts-to-zod": "^3.15.0",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"zod-to-ts": "^1.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@repo/rdx-criteria": "workspace:*",
|
||||||
"@repo/rdx-ddd": "workspace:*",
|
"@repo/rdx-ddd": "workspace:*",
|
||||||
"@repo/rdx-utils": "workspace:*",
|
"@repo/rdx-utils": "workspace:*",
|
||||||
"@repo/rdx-criteria": "workspace:*",
|
|
||||||
"@tanstack/react-query": "^5.75.4",
|
"@tanstack/react-query": "^5.75.4",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"http-status": "^2.1.0",
|
"http-status": "^2.1.0",
|
||||||
"joi": "^17.13.3",
|
"joi": "^17.13.3",
|
||||||
"libphonenumber-js": "^1.11.20",
|
"libphonenumber-js": "^1.11.20",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.0",
|
||||||
"sequelize": "^6.37.5"
|
"sequelize": "^6.37.5",
|
||||||
|
"zod": "^3.25.67"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
modules/core/src/api/errors/conflict-api-error.ts
Normal file
12
modules/core/src/api/errors/conflict-api-error.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ApiError } from "./api-error";
|
||||||
|
|
||||||
|
export class ConflictApiError extends ApiError {
|
||||||
|
constructor(detail: string) {
|
||||||
|
super({
|
||||||
|
status: 409,
|
||||||
|
title: "Conflict",
|
||||||
|
detail,
|
||||||
|
type: "https://httpstatuses.com/409",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
29
modules/core/src/api/errors/domain-validation-error.ts
Normal file
29
modules/core/src/api/errors/domain-validation-error.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Clase DomainValidationError
|
||||||
|
* Representa un error de validación de dominio.
|
||||||
|
*
|
||||||
|
* Esta clase extiende la clase Error de JavaScript y se utiliza para manejar errores
|
||||||
|
* específicos de validación dentro del dominio de la aplicación. Permite identificar
|
||||||
|
* el código de error, el campo afectado y un detalle descriptivo del error.
|
||||||
|
*
|
||||||
|
* @class DomainValidationError
|
||||||
|
* @extends {Error}
|
||||||
|
* @property {string} code - Código del error de validación.
|
||||||
|
* @property {string} field - Campo afectado por el error de validación.
|
||||||
|
* @property {string} detail - Detalle descriptivo del error de validación.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const error = new DomainValidationError("INVALID_EMAIL", "email", "El email no es válido");
|
||||||
|
* console.error(error);
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class DomainValidationError extends Error {
|
||||||
|
constructor(
|
||||||
|
public readonly code: string,
|
||||||
|
public readonly field: string,
|
||||||
|
public readonly detail: string
|
||||||
|
) {
|
||||||
|
super(`[${field}] ${detail}`);
|
||||||
|
this.name = "DomainValidationError";
|
||||||
|
}
|
||||||
|
}
|
||||||
96
modules/core/src/api/errors/error-mapper.ts
Normal file
96
modules/core/src/api/errors/error-mapper.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { ConnectionError, UniqueConstraintError } from "sequelize";
|
||||||
|
|
||||||
|
import { ApiError } from "./api-error";
|
||||||
|
import { ConflictApiError } from "./conflict-api-error";
|
||||||
|
import { DomainValidationError } from "./domain-validation-error";
|
||||||
|
import { ForbiddenApiError } from "./forbidden-api-error";
|
||||||
|
import { InternalApiError } from "./internal-api-error";
|
||||||
|
import { NotFoundApiError } from "./not-found-api-error";
|
||||||
|
import { UnauthorizedApiError } from "./unauthorized-api-error";
|
||||||
|
import { UnavailableApiError } from "./unavailable-api-error";
|
||||||
|
import { ValidationApiError } from "./validation-api-error";
|
||||||
|
import { ValidationErrorCollection } from "./validation-error-collection";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapea errores de la aplicación a errores de la API.
|
||||||
|
*
|
||||||
|
* Esta función toma un error de la aplicación y lo convierte en un objeto ApiError
|
||||||
|
* adecuado para enviar como respuesta HTTP. Maneja errores comunes como validación,
|
||||||
|
* conflictos, no encontrados, autenticación y errores de infraestructura.
|
||||||
|
*
|
||||||
|
* @param error - El error de la aplicación a mapear.
|
||||||
|
* @returns Un objeto ApiError que representa el error mapeado.
|
||||||
|
* @example
|
||||||
|
* const error = new Error("Invalid input");
|
||||||
|
* const apiError = errorMapper.toApiError(error);
|
||||||
|
* console.log(apiError);
|
||||||
|
* // Output: ValidationApiError { status: 422, title: 'Validation Failed', detail: 'Invalid input', type: 'https://httpstatuses.com/422' }
|
||||||
|
* @throws {ApiError} Si el error no puede ser mapeado a un tipo conocido.
|
||||||
|
* @see ApiError
|
||||||
|
* @see ValidationApiError
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const errorMapper = {
|
||||||
|
toApiError: (error: Error): ApiError => {
|
||||||
|
const message = error.message || "An unexpected error occurred";
|
||||||
|
|
||||||
|
// 1. 🔍 Errores de validación complejos (agrupados)
|
||||||
|
if (error instanceof ValidationErrorCollection) {
|
||||||
|
return new ValidationApiError(error.message, error.details);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 🔍 Errores individuales de validación de dominio
|
||||||
|
if (error instanceof DomainValidationError) {
|
||||||
|
return new ValidationApiError(error.detail, [{ path: error.field, message: error.detail }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 🔍 Errores individuales de validación
|
||||||
|
if (
|
||||||
|
message.includes("invalid") ||
|
||||||
|
message.includes("is not valid") ||
|
||||||
|
message.includes("must be") ||
|
||||||
|
message.includes("cannot be") ||
|
||||||
|
message.includes("empty")
|
||||||
|
) {
|
||||||
|
return new ValidationApiError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 🔍 Recurso no encontrado
|
||||||
|
if (error.name === "NotFoundError" || message.includes("not found")) {
|
||||||
|
return new NotFoundApiError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 🔍 Conflicto (por ejemplo, duplicado)
|
||||||
|
if (
|
||||||
|
error.name === "ConflictError" ||
|
||||||
|
error instanceof UniqueConstraintError ||
|
||||||
|
message.includes("already exists") ||
|
||||||
|
message.includes("duplicate key")
|
||||||
|
) {
|
||||||
|
return new ConflictApiError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 🔍 No autenticado
|
||||||
|
if (error.name === "UnauthorizedError" || message.includes("unauthorized")) {
|
||||||
|
return new UnauthorizedApiError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 🔍 Prohibido
|
||||||
|
if (error.name === "ForbiddenError" || message.includes("forbidden")) {
|
||||||
|
return new ForbiddenApiError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 🔍 Error de conexión o indisponibilidad de servicio
|
||||||
|
if (
|
||||||
|
error instanceof ConnectionError ||
|
||||||
|
message.includes("Database connection lost") ||
|
||||||
|
message.includes("timeout") ||
|
||||||
|
message.includes("ECONNREFUSED")
|
||||||
|
) {
|
||||||
|
return new UnavailableApiError("Service temporarily unavailable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 🔍 Fallback: error no identificado
|
||||||
|
return new InternalApiError(`Unexpected error: ${message}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
12
modules/core/src/api/errors/forbidden-api-error.ts
Normal file
12
modules/core/src/api/errors/forbidden-api-error.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ApiError } from "./api-error";
|
||||||
|
|
||||||
|
export class ForbiddenApiError extends ApiError {
|
||||||
|
constructor(detail: string) {
|
||||||
|
super({
|
||||||
|
status: 403,
|
||||||
|
title: "Forbidden",
|
||||||
|
detail,
|
||||||
|
type: "https://httpstatuses.com/403",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
3
modules/core/src/api/errors/index.ts
Normal file
3
modules/core/src/api/errors/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./domain-validation-error";
|
||||||
|
export * from "./error-mapper";
|
||||||
|
export * from "./validation-error-collection";
|
||||||
12
modules/core/src/api/errors/internal-api-error.ts
Normal file
12
modules/core/src/api/errors/internal-api-error.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ApiError } from "./api-error";
|
||||||
|
|
||||||
|
export class InternalApiError extends ApiError {
|
||||||
|
constructor(detail: string) {
|
||||||
|
super({
|
||||||
|
status: 500,
|
||||||
|
title: "Internal Server Error",
|
||||||
|
detail,
|
||||||
|
type: "https://httpstatuses.com/500",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
12
modules/core/src/api/errors/not-found-api-error.ts
Normal file
12
modules/core/src/api/errors/not-found-api-error.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ApiError } from "./api-error";
|
||||||
|
|
||||||
|
export class NotFoundApiError extends ApiError {
|
||||||
|
constructor(detail: string) {
|
||||||
|
super({
|
||||||
|
status: 404,
|
||||||
|
title: "Resource Not Found",
|
||||||
|
detail,
|
||||||
|
type: "https://httpstatuses.com/404",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
12
modules/core/src/api/errors/unauthorized-api-error.ts
Normal file
12
modules/core/src/api/errors/unauthorized-api-error.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ApiError } from "./api-error";
|
||||||
|
|
||||||
|
export class UnauthorizedApiError extends ApiError {
|
||||||
|
constructor(detail: string) {
|
||||||
|
super({
|
||||||
|
status: 401,
|
||||||
|
title: "Unauthorized",
|
||||||
|
detail,
|
||||||
|
type: "https://httpstatuses.com/401",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
12
modules/core/src/api/errors/unavailable-api-error.ts
Normal file
12
modules/core/src/api/errors/unavailable-api-error.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ApiError } from "./api-error";
|
||||||
|
|
||||||
|
export class UnavailableApiError extends ApiError {
|
||||||
|
constructor(detail: string) {
|
||||||
|
super({
|
||||||
|
status: 503,
|
||||||
|
title: "Service Unavailable",
|
||||||
|
detail,
|
||||||
|
type: "https://httpstatuses.com/503",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
13
modules/core/src/api/errors/validation-api-error.ts
Normal file
13
modules/core/src/api/errors/validation-api-error.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ApiError } from "./api-error";
|
||||||
|
|
||||||
|
export class ValidationApiError extends ApiError {
|
||||||
|
constructor(detail: string, errors?: any[]) {
|
||||||
|
super({
|
||||||
|
status: 422,
|
||||||
|
title: "Validation Failed",
|
||||||
|
detail,
|
||||||
|
type: "https://httpstatuses.com/422",
|
||||||
|
errors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
33
modules/core/src/api/errors/validation-error-collection.ts
Normal file
33
modules/core/src/api/errors/validation-error-collection.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* ValidationErrorCollection
|
||||||
|
*
|
||||||
|
* Esta clase representa un error de validación que agrega múltiples detalles de errores
|
||||||
|
* en un formato estructurado. Es útil para manejar múltiples errores de validación
|
||||||
|
* en una sola respuesta, permitiendo a los clientes entender qué campos fallaron y por qué.
|
||||||
|
*
|
||||||
|
* Ejemplo de uso:
|
||||||
|
*
|
||||||
|
* const errors: ValidationErrorDetail[] = [
|
||||||
|
* { path: "lines[1].unitPrice.amount", message: "Amount must be a positive number" },
|
||||||
|
* { path: "lines[1].unitPrice.scale", message: "Scale must be a non-negative integer" },
|
||||||
|
* ];
|
||||||
|
* const validationError = new ValidationErrorCollection(errors);
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ValidationErrorDetail {
|
||||||
|
path: string; // ejemplo: "lines[1].unitPrice.amount"
|
||||||
|
message: string; // ejemplo: "Amount must be a positive number"
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValidationErrorCollection extends Error {
|
||||||
|
public readonly details: ValidationErrorDetail[];
|
||||||
|
|
||||||
|
constructor(details: ValidationErrorDetail[]) {
|
||||||
|
super("Validation failed");
|
||||||
|
Object.setPrototypeOf(this, ValidationErrorCollection.prototype);
|
||||||
|
|
||||||
|
this.name = "ValidationErrorCollection";
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./errors";
|
||||||
export * from "./infrastructure";
|
export * from "./infrastructure";
|
||||||
export * from "./logger";
|
export * from "./logger";
|
||||||
export * from "./modules";
|
export * from "./modules";
|
||||||
|
|||||||
@ -1,7 +1,16 @@
|
|||||||
import { Criteria, CriteriaFromUrlConverter } from "@repo/rdx-criteria/server";
|
import { Criteria, CriteriaFromUrlConverter } from "@repo/rdx-criteria/server";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import httpStatus from "http-status";
|
import httpStatus from "http-status";
|
||||||
import { ApiError } from "./api-error";
|
import {
|
||||||
|
ApiError,
|
||||||
|
ConflictApiError,
|
||||||
|
ForbiddenApiError,
|
||||||
|
InternalApiError,
|
||||||
|
NotFoundApiError,
|
||||||
|
UnauthorizedApiError,
|
||||||
|
UnavailableApiError,
|
||||||
|
ValidationApiError,
|
||||||
|
} from "../../errors";
|
||||||
|
|
||||||
export abstract class ExpressController {
|
export abstract class ExpressController {
|
||||||
protected req!: Request; //| AuthenticatedRequest | TabContextRequest;
|
protected req!: Request; //| AuthenticatedRequest | TabContextRequest;
|
||||||
@ -16,140 +25,99 @@ export abstract class ExpressController {
|
|||||||
protected abstract executeImpl(): Promise<unknown>;
|
protected abstract executeImpl(): Promise<unknown>;
|
||||||
|
|
||||||
protected ok<T>(dto?: T) {
|
protected ok<T>(dto?: T) {
|
||||||
return dto ? this.res.status(httpStatus.OK).json(dto) : this.res.status(httpStatus.OK).send();
|
return dto ? this.res.status(httpStatus.OK).json(dto) : this.res.sendStatus(httpStatus.OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected created<T>(dto?: T) {
|
protected created<T>(dto?: T) {
|
||||||
return dto
|
return dto
|
||||||
? this.res.status(httpStatus.CREATED).json(dto)
|
? this.res.status(httpStatus.CREATED).json(dto)
|
||||||
: this.res.status(httpStatus.CREATED).send();
|
: this.res.sendStatus(httpStatus.CREATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected noContent() {
|
protected noContent() {
|
||||||
return this.res.status(httpStatus.NO_CONTENT).send();
|
return this.res.sendStatus(httpStatus.NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🔹 Respuesta para errores de cliente (400 Bad Request)
|
* Respuesta para errores de cliente (400 Bad Request)
|
||||||
*/
|
*/
|
||||||
public clientError(message: string, errors?: any[] | any) {
|
protected clientError(message: string, errors?: any[] | any) {
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(
|
||||||
new ApiError({
|
new ValidationApiError(message, Array.isArray(errors) ? errors : [errors]),
|
||||||
status: 400,
|
|
||||||
title: "Bad Request",
|
|
||||||
detail: message,
|
|
||||||
errors: Array.isArray(errors) ? errors : [errors],
|
|
||||||
}),
|
|
||||||
this.res
|
this.res
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🔹 Respuesta para errores de autenticación (401 Unauthorized)
|
* Respuesta para errores de autenticación (401 Unauthorized)
|
||||||
*/
|
*/
|
||||||
protected unauthorizedError(message?: string) {
|
protected unauthorizedError(message?: string) {
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(
|
||||||
new ApiError({
|
new UnauthorizedApiError(message ?? "Unauthorized"),
|
||||||
status: 401,
|
|
||||||
title: httpStatus["401"],
|
|
||||||
name: httpStatus["401_NAME"],
|
|
||||||
detail: message ?? httpStatus["401_MESSAGE"],
|
|
||||||
}),
|
|
||||||
this.res
|
this.res
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🔹 Respuesta para errores de autorización (403 Forbidden)
|
* Respuesta para errores de autorización (403 Forbidden)
|
||||||
*/
|
*/
|
||||||
protected forbiddenError(message?: string) {
|
protected forbiddenError(message?: string) {
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(
|
||||||
new ApiError({
|
new ForbiddenApiError(message ?? "You do not have permission to perform this action."),
|
||||||
status: 403,
|
|
||||||
title: "Forbidden",
|
|
||||||
detail: message ?? "You do not have permission to perform this action.",
|
|
||||||
}),
|
|
||||||
this.res
|
this.res
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🔹 Respuesta para recursos no encontrados (404 Not Found)
|
* Respuesta para recursos no encontrados (404 Not Found)
|
||||||
*/
|
*/
|
||||||
protected notFoundError(message: string) {
|
protected notFoundError(message: string) {
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(new NotFoundApiError(message), this.res);
|
||||||
new ApiError({
|
|
||||||
status: 404,
|
|
||||||
title: "Not Found",
|
|
||||||
detail: message,
|
|
||||||
}),
|
|
||||||
this.res
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🔹 Respuesta para conflictos (409 Conflict)
|
* Respuesta para conflictos (409 Conflict)
|
||||||
*/
|
*/
|
||||||
protected conflictError(message: string, errors?: any[]) {
|
protected conflictError(message: string, errors?: any[]) {
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(new ConflictApiError(message), this.res);
|
||||||
new ApiError({
|
|
||||||
status: 409,
|
|
||||||
title: "Conflict",
|
|
||||||
detail: message,
|
|
||||||
errors,
|
|
||||||
}),
|
|
||||||
this.res
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🔹 Respuesta para errores de validación de entrada (422 Unprocessable Entity)
|
* Respuesta para errores de validación de entrada (422 Unprocessable Entity)
|
||||||
*/
|
*/
|
||||||
protected invalidInputError(message: string, errors?: any[]) {
|
protected invalidInputError(message: string, errors?: any[]) {
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(new ValidationApiError(message, errors), this.res);
|
||||||
new ApiError({
|
|
||||||
status: 422,
|
|
||||||
title: httpStatus["422"],
|
|
||||||
name: httpStatus["422_NAME"],
|
|
||||||
detail: message ?? httpStatus["422_MESSAGE"],
|
|
||||||
errors,
|
|
||||||
}),
|
|
||||||
this.res
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Respuesta para errores de servidor no disponible (503 Service Unavailable)
|
* Respuesta para errores de servidor no disponible (503 Service Unavailable)
|
||||||
* @param message
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
protected unavailableError(message?: string) {
|
protected unavailableError(message?: string) {
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(
|
||||||
new ApiError({
|
new UnavailableApiError(message ?? "Service temporarily unavailable."),
|
||||||
status: 503,
|
this.res
|
||||||
title: httpStatus["503"],
|
);
|
||||||
name: httpStatus["503_NAME"],
|
}
|
||||||
detail: message ?? httpStatus["503_MESSAGE"],
|
/**
|
||||||
}),
|
* Respuesta para errores internos del servidor (500 Internal Server Error)
|
||||||
|
*/
|
||||||
|
protected internalServerError(message?: string) {
|
||||||
|
return ExpressController.errorResponse(
|
||||||
|
new InternalApiError(message ?? "Internal Server Error"),
|
||||||
this.res
|
this.res
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🔹 Respuesta para errores internos del servidor (500 Internal Server Error)
|
* Respuesta para cualquier error de la API
|
||||||
*/
|
*/
|
||||||
protected internalServerError(message?: string) {
|
protected handleApiError(apiError: ApiError) {
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(apiError, this.res);
|
||||||
new ApiError({
|
|
||||||
status: 500,
|
|
||||||
title: httpStatus["500"],
|
|
||||||
name: httpStatus["500_NAME"],
|
|
||||||
detail: message ?? httpStatus["500_MESSAGE"],
|
|
||||||
}),
|
|
||||||
this.res
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Método principal que se invoca desde el router de Express.
|
||||||
|
* Maneja la conversión de la URL a criterios y llama a executeImpl.
|
||||||
|
*/
|
||||||
public execute(req: Request, res: Response, next: NextFunction): void {
|
public execute(req: Request, res: Response, next: NextFunction): void {
|
||||||
this.req = req;
|
this.req = req;
|
||||||
this.res = res;
|
this.res = res;
|
||||||
@ -161,8 +129,12 @@ export abstract class ExpressController {
|
|||||||
|
|
||||||
this.executeImpl();
|
this.executeImpl();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const _error = error as Error;
|
const err = error as Error;
|
||||||
this.internalServerError(_error.message);
|
if (err instanceof ApiError) {
|
||||||
|
ExpressController.errorResponse(err, this.res);
|
||||||
|
} else {
|
||||||
|
ExpressController.errorResponse(new InternalApiError(err.message), this.res);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,2 @@
|
|||||||
export * from "./api-error";
|
|
||||||
export * from "./express-controller";
|
export * from "./express-controller";
|
||||||
export * from "./middlewares";
|
export * from "./middlewares";
|
||||||
export * from "./validate-request-dto";
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { ApiError } from "../api-error";
|
import { ApiError } from "../../../errors/api-error";
|
||||||
|
|
||||||
export const globalErrorHandler = async (
|
export const globalErrorHandler = async (
|
||||||
error: Error,
|
error: Error,
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export * from "./global-error-handler";
|
export * from "./global-error-handler";
|
||||||
|
export * from "./validate-request";
|
||||||
|
|||||||
@ -0,0 +1,63 @@
|
|||||||
|
// src/common/middlewares/validate-dto.ts
|
||||||
|
import { ValidationApiError } from "@erp/core/api";
|
||||||
|
import { RequestHandler } from "express";
|
||||||
|
import { ZodSchema } from "zod/v4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware genérico para validar un objeto de Express
|
||||||
|
* (`body`, `query`, o `params`) mediante un esquema Zod.
|
||||||
|
*
|
||||||
|
* @param schema Esquema Zod que valida la entrada.
|
||||||
|
* @param source Parte del request que se valida ('body' | 'query' | 'params').
|
||||||
|
* Por defecto, 'body'.
|
||||||
|
* @param options Opciones adicionales:
|
||||||
|
* - `sanitize`: Si se debe reescribir `req.body` con los datos validados.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* router.post('/invoices',
|
||||||
|
* validateZod(CreateInvoiceCommandSchema), // body
|
||||||
|
* controller
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* router.get('/invoices',
|
||||||
|
* validateZod(ListInvoicesQuerySchema, 'query'), // query
|
||||||
|
* controller
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* router.patch('/invoices/:id/status',
|
||||||
|
* validateZod(ChangeStatusParamsSchema, 'params'), // params
|
||||||
|
* controller
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ValidateRequestWithSchemaOptions = {
|
||||||
|
sanitize?: boolean; // Si se debe reescribir req.body con los datos validados
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateRequest = <T extends "body" | "query" | "params">(
|
||||||
|
schema: ZodSchema,
|
||||||
|
source: T = "body" as T,
|
||||||
|
options: ValidateRequestWithSchemaOptions = { sanitize: true }
|
||||||
|
): RequestHandler => {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
console.debug(`Validating request ${source} with schema:`, schema);
|
||||||
|
const result = schema.safeParse(req[source]);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
// Construye errores detallados
|
||||||
|
const validationErrors = result.error.errors.map((err) => ({
|
||||||
|
field: err.path.join("."),
|
||||||
|
message: err.message,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return new ValidationApiError("Validation failed", validationErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si pasa la validación, opcionalmente reescribe req.body
|
||||||
|
if (options?.sanitize ?? true) {
|
||||||
|
req[source] = result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
|
||||||
import httpStatus from "http-status";
|
|
||||||
import { ZodSchema } from "zod";
|
|
||||||
import { ApiError } from "./api-error";
|
|
||||||
|
|
||||||
export const validateAndParseBody =
|
|
||||||
(schema: ZodSchema, options?: { sanitize?: boolean }) =>
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const result = schema.safeParse(req.body);
|
|
||||||
try {
|
|
||||||
if (!result.success) {
|
|
||||||
// Construye errores detallados
|
|
||||||
const validationErrors = result.error.errors.map((err) => ({
|
|
||||||
field: err.path.join("."),
|
|
||||||
message: err.message,
|
|
||||||
}));
|
|
||||||
|
|
||||||
throw new ApiError({
|
|
||||||
status: httpStatus.BAD_REQUEST, //400
|
|
||||||
title: "Validation Error",
|
|
||||||
detail: "Algunos campos no cumplen con los criterios de validación.",
|
|
||||||
type: "https://example.com/probs/validation-error",
|
|
||||||
instance: req.originalUrl,
|
|
||||||
errors: validationErrors,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si pasa la validación, opcionalmente reescribe req.body
|
|
||||||
if (options?.sanitize ?? true) {
|
|
||||||
req.body = result.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
} catch (error: unknown) {
|
|
||||||
// Si ocurre un error, delega al manejador de errores global
|
|
||||||
next(error as ApiError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
export * from "./error.dto";
|
export * from "./error.dto";
|
||||||
export * from "./list.dto";
|
export * from "./list.view.dto";
|
||||||
export * from "./metadata.dto";
|
export * from "./metadata.dto";
|
||||||
export * from "./money.dto";
|
export * from "./money.dto";
|
||||||
export * from "./percentage.dto";
|
export * from "./percentage.dto";
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
export interface IListResponseDTO<T> {
|
|
||||||
page: number;
|
|
||||||
per_page: number;
|
|
||||||
total_pages: number;
|
|
||||||
total_items: number;
|
|
||||||
items: T[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isResponseAListDTO = <T>(data: any): data is IListResponseDTO<T> => {
|
|
||||||
return data && typeof data.total_items === "number";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const existsMoreReponsePages = <T>(response: any): response is IListResponseDTO<T> => {
|
|
||||||
return isResponseAListDTO(response) && response.page + 1 < response.total_pages;
|
|
||||||
};
|
|
||||||
18
modules/core/src/common/dto/list.view.dto.ts
Normal file
18
modules/core/src/common/dto/list.view.dto.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import * as z from "zod/v4";
|
||||||
|
import { MetadataSchema } from "./metadata.dto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea un esquema Zod que representa un ListViewDTO genérico.
|
||||||
|
*
|
||||||
|
* @param itemSchema Esquema Zod del elemento T
|
||||||
|
* @returns Zod schema para ListViewDTO<T>
|
||||||
|
*/
|
||||||
|
export const createListViewSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
|
||||||
|
z.object({
|
||||||
|
page: z.number().int().min(1, "Page must be a positive integer"),
|
||||||
|
per_page: z.number().int().min(1, "Items per page must be a positive integer"),
|
||||||
|
total_pages: z.number().int().min(0, "Total pages must be a non-negative integer"),
|
||||||
|
total_items: z.number().int().min(0, "Total items must be a non-negative integer"),
|
||||||
|
items: z.array(itemSchema),
|
||||||
|
metadata: MetadataSchema.optional(),
|
||||||
|
});
|
||||||
@ -1,16 +1,28 @@
|
|||||||
export interface IMetadataDTO {
|
import * as z from "zod/v4";
|
||||||
entity: string;
|
|
||||||
version: string;
|
|
||||||
[key: string]: any; // <- para campos adicionales futuros
|
|
||||||
|
|
||||||
// Futuros campos opcionales que podrían ser útiles:
|
export const MetadataSchema = z
|
||||||
// source?: 'api' | 'manual' | 'imported' | string;
|
.object({
|
||||||
// related_id?: string;
|
entity: z.string(),
|
||||||
// related_entity?: string;
|
version: z.string().optional(),
|
||||||
// created_by?: string;
|
})
|
||||||
// created_at?: string;
|
.catchall(z.any());
|
||||||
// updated_by?: string;
|
|
||||||
// updated_at?: string;
|
export type MetadataDTO = z.infer<typeof MetadataSchema>;
|
||||||
// permissions?: Array<'read' | 'edit' | 'delete' | string>;
|
|
||||||
// visibility?: 'public' | 'private' | 'restricted' | string;
|
// Ejemplo de uso:
|
||||||
}
|
// const metadata: IMetadataDTO = {
|
||||||
|
// entity: "customer_invoice",
|
||||||
|
// version: "1.0",
|
||||||
|
// custom_field: "value",
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// Futuros campos opcionales que podrían ser útiles:
|
||||||
|
// source?: 'api' | 'manual' | 'imported' | string;
|
||||||
|
// related_id?: string;
|
||||||
|
// related_entity?: string;
|
||||||
|
// created_by?: string;
|
||||||
|
// created_at?: string;
|
||||||
|
// updated_by?: string;
|
||||||
|
// updated_at?: string;
|
||||||
|
// permissions?: Array<'read' | 'edit' | 'delete' | string>;
|
||||||
|
// visibility?: 'public' | 'private' | 'restricted' | string;
|
||||||
|
|||||||
@ -1,27 +1,5 @@
|
|||||||
import { Result, RuleValidator } from "@repo/rdx-utils";
|
export type MoneyDTO = {
|
||||||
import Joi from "joi";
|
|
||||||
|
|
||||||
export interface IMoneyDTO {
|
|
||||||
amount: number | null;
|
amount: number | null;
|
||||||
scale: number;
|
scale: number;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface IMoneyRequestDTO extends IMoneyDTO {}
|
|
||||||
export interface IMoneyResponseDTO extends IMoneyDTO {}
|
|
||||||
|
|
||||||
export function ensureMoneyDTOIsValid(money: IMoneyRequestDTO) {
|
|
||||||
const schema = Joi.object({
|
|
||||||
amount: Joi.number(),
|
|
||||||
scale: Joi.number(),
|
|
||||||
currencycode: Joi.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = RuleValidator.validate<IMoneyRequestDTO>(schema, money);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Result.fail(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(true);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
export interface IPercentageDTO {
|
export type IPercentageDTO = {
|
||||||
amount: number | null;
|
amount: number | null;
|
||||||
scale: number;
|
scale: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface IPercentageRequestDTO extends IPercentageDTO {}
|
|
||||||
export interface IPercentageResponseDTO extends IPercentageDTO {}
|
|
||||||
|
|||||||
@ -1,25 +1,4 @@
|
|||||||
import { Result, RuleValidator } from "@repo/rdx-utils";
|
export type IQuantityDTO = {
|
||||||
import Joi from "joi";
|
|
||||||
|
|
||||||
export interface IQuantityDTO {
|
|
||||||
amount: number | null;
|
amount: number | null;
|
||||||
scale: number;
|
scale: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function ensureQuantityDTOIsValid(quantity: IQuantityRequestDTO) {
|
|
||||||
const schema = Joi.object({
|
|
||||||
amount: Joi.number(),
|
|
||||||
scale: Joi.number(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = RuleValidator.validate<IQuantityRequestDTO>(schema, quantity);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Result.fail(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IQuantityRequestDTO extends IQuantityDTO {}
|
|
||||||
export interface IQuantityResponseDTO extends IQuantityDTO {}
|
|
||||||
|
|||||||
@ -14,9 +14,9 @@ export interface ICustomParams {
|
|||||||
|
|
||||||
export interface IDataSource {
|
export interface IDataSource {
|
||||||
getBaseUrl(): string;
|
getBaseUrl(): string;
|
||||||
getList<T>(resource: string, params?: Record<string, any>): Promise<T[]>;
|
getList<T>(resource: string, params?: Record<string, any>): Promise<T>;
|
||||||
getOne<T>(resource: string, id: string | number): Promise<T>;
|
getOne<T>(resource: string, id: string | number): Promise<T>;
|
||||||
getMany<T>(resource: string, ids: Array<string | number>): Promise<T[]>;
|
getMany<T>(resource: string, ids: Array<string | number>): Promise<T>;
|
||||||
createOne<T>(resource: string, data: Partial<T>): Promise<T>;
|
createOne<T>(resource: string, data: Partial<T>): Promise<T>;
|
||||||
updateOne<T>(resource: string, id: string | number, data: Partial<T>): Promise<T>;
|
updateOne<T>(resource: string, id: string | number, data: Partial<T>): Promise<T>;
|
||||||
deleteOne<T>(resource: string, id: string | number): Promise<void>;
|
deleteOne<T>(resource: string, id: string | number): Promise<void>;
|
||||||
|
|||||||
@ -18,23 +18,26 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@erp/core": "workspace:*",
|
"@erp/core": "workspace:*",
|
||||||
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@repo/rdx-criteria": "workspace:*",
|
"@repo/rdx-criteria": "workspace:*",
|
||||||
"@repo/rdx-ddd": "workspace:*",
|
"@repo/rdx-ddd": "workspace:*",
|
||||||
"@repo/rdx-utils": "workspace:*",
|
|
||||||
"@repo/rdx-ui": "workspace:*",
|
"@repo/rdx-ui": "workspace:*",
|
||||||
|
"@repo/rdx-utils": "workspace:*",
|
||||||
"@repo/shadcn-ui": "workspace:*",
|
"@repo/shadcn-ui": "workspace:*",
|
||||||
"@tanstack/react-query": "^5.74.11",
|
"@tanstack/react-query": "^5.74.11",
|
||||||
"ag-grid-community": "^33.3.0",
|
"ag-grid-community": "^33.3.0",
|
||||||
"ag-grid-react": "^33.3.0",
|
"ag-grid-react": "^33.3.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"i18next": "^25.1.1",
|
"i18next": "^25.1.1",
|
||||||
"lucide-react": "^0.503.0",
|
"lucide-react": "^0.503.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
"react-hook-form": "^7.58.1",
|
||||||
"react-i18next": "^15.5.1",
|
"react-i18next": "^15.5.1",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.0",
|
||||||
"sequelize": "^6.37.5",
|
"sequelize": "^6.37.5",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"zod": "^3.24.4"
|
"zod": "^3.25.67"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,8 @@
|
|||||||
import { UniqueID, UtcDate } from "@/core/common/domain";
|
import { ITransactionManager } from "@erp/core/api";
|
||||||
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import {
|
import { Transaction } from "sequelize";
|
||||||
type CustomerInvoice,
|
|
||||||
CustomerInvoiceNumber,
|
|
||||||
CustomerInvoiceSerie,
|
|
||||||
CustomerInvoiceStatus,
|
|
||||||
type ICustomerInvoiceProps,
|
|
||||||
type ICustomerInvoiceService,
|
|
||||||
} from "@/contexts/customer-invoices/domain";
|
|
||||||
import { ITransactionManager } from "@/core/common/infrastructure/database";
|
|
||||||
import { logger } from "@/core/logger";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import { ICreateCustomerInvoiceRequestDTO } from "../../common/dto";
|
import { ICreateCustomerInvoiceRequestDTO } from "../../common/dto";
|
||||||
|
import { ICustomerInvoiceProps, ICustomerInvoiceService } from "../domain";
|
||||||
|
|
||||||
export class CreateCustomerInvoiceUseCase {
|
export class CreateCustomerInvoiceUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
@ -19,23 +10,28 @@ export class CreateCustomerInvoiceUseCase {
|
|||||||
private readonly transactionManager: ITransactionManager
|
private readonly transactionManager: ITransactionManager
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(
|
public execute(customerInvoiceID: UniqueID, data: ICreateCustomerInvoiceRequestDTO) {
|
||||||
customerInvoiceID: UniqueID,
|
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||||
dto: ICreateCustomerInvoiceRequestDTO
|
|
||||||
): Promise<Result<CustomerInvoice, Error>> {
|
|
||||||
return this.transactionManager.complete(async (transaction) => {
|
|
||||||
try {
|
try {
|
||||||
const validOrErrors = this.validateCustomerInvoiceData(dto);
|
/*const validOrErrors = this.validateCustomerInvoiceData(dto);
|
||||||
if (validOrErrors.isFailure) {
|
if (validOrErrors.isFailure) {
|
||||||
return Result.fail(validOrErrors.error);
|
return Result.fail(validOrErrors.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = validOrErrors.data;
|
const data = validOrErrors.data;*/
|
||||||
|
|
||||||
|
const invoiceProps: ICustomerInvoiceProps = {
|
||||||
|
customerInvoiceNumber: data.customerInvoice_number,
|
||||||
|
customerInvoiceSeries: data.customerInvoice_series,
|
||||||
|
issueDate: data.issue_date,
|
||||||
|
operationDate: data.operation_date,
|
||||||
|
customerInvoiceCurrency: data.currency,
|
||||||
|
};
|
||||||
|
|
||||||
// Update customerInvoice with dto
|
// Update customerInvoice with dto
|
||||||
return await this.customerInvoiceService.createCustomerInvoice(
|
return await this.customerInvoiceService.createCustomerInvoice(
|
||||||
customerInvoiceID,
|
customerInvoiceID,
|
||||||
data,
|
invoiceProps,
|
||||||
transaction
|
transaction
|
||||||
);
|
);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./create-customer-invoice.use-case";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./delete-customer-invoice.use-case";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./get-customer-invoice.use-case";
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import { ValidationErrorCollection, ValidationErrorDetail } from "@erp/core/api";
|
||||||
|
import { UniqueID, UtcDate } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
import { CreateCustomerInvoiceCommandDTO } from "../../../common/dto";
|
||||||
|
import { CustomerInvoiceNumber, CustomerInvoiceProps, CustomerInvoiceSerie } from "../../domain";
|
||||||
|
import { buildInvoiceItemsFromDTO } from "./build-customer-invoice-items-from-dto";
|
||||||
|
import { extractOrPushError } from "./extract-or-push-error";
|
||||||
|
|
||||||
|
export async function buildInvoiceFromDTO(
|
||||||
|
dto: CreateCustomerInvoiceCommandDTO
|
||||||
|
): Promise<Result<CustomerInvoiceProps, Error>> {
|
||||||
|
const errors: ValidationErrorDetail[] = [];
|
||||||
|
|
||||||
|
const invoiceNumber = extractOrPushError(
|
||||||
|
CustomerInvoiceNumber.create(dto.invoice_number),
|
||||||
|
"invoice_number",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
const invoiceSeries = extractOrPushError(
|
||||||
|
CustomerInvoiceSerie.create(dto.invoice_series),
|
||||||
|
"invoice_series",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
const issueDate = extractOrPushError(UtcDate.createFromISO(dto.issue_date), "issue_date", errors);
|
||||||
|
const operationDate = extractOrPushError(
|
||||||
|
UtcDate.createFromISO(dto.operation_date),
|
||||||
|
"operation_date",
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔄 Validar y construir los items de factura con helper especializado
|
||||||
|
const itemsResult = await buildInvoiceItemsFromDTO(dto.items);
|
||||||
|
if (itemsResult.isFailure) {
|
||||||
|
return Result.fail(itemsResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Result.fail(new ValidationErrorCollection(errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok({
|
||||||
|
id: UniqueID.create(),
|
||||||
|
customerId: customerId.data,
|
||||||
|
invoiceNumber: invoiceNumber.data,
|
||||||
|
invoiceSeries: invoiceSeries.data,
|
||||||
|
issueDate: issueDate.data,
|
||||||
|
operationDate: operationDate.data,
|
||||||
|
subtotalPrice: subtotalPrice.data,
|
||||||
|
discount: discount.data,
|
||||||
|
tax: tax.data,
|
||||||
|
totalAmount: totalAmount.data,
|
||||||
|
lines: itemsResult.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
import { ValidationErrorCollection, ValidationErrorDetail } from "@erp/core/api";
|
||||||
|
import { CreateCustomerInvoiceCommandDTO } from "@erp/customer-invoices/common/dto";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
import {
|
||||||
|
CustomerInvoiceItem,
|
||||||
|
CustomerInvoiceItemDescription,
|
||||||
|
CustomerInvoiceItemDiscount,
|
||||||
|
CustomerInvoiceItemQuantity,
|
||||||
|
CustomerInvoiceItemUnitPrice,
|
||||||
|
} from "../../domain";
|
||||||
|
import { extractOrPushError } from "./extract-or-push-error";
|
||||||
|
import { hasNoUndefinedFields } from "./has-no-undefined-fields";
|
||||||
|
|
||||||
|
export function buildInvoiceItemsFromDTO(
|
||||||
|
dtoItems: Pick<CreateCustomerInvoiceCommandDTO, "items">["items"]
|
||||||
|
): Result<CustomerInvoiceItem[], ValidationErrorCollection> {
|
||||||
|
const errors: ValidationErrorDetail[] = [];
|
||||||
|
const items: CustomerInvoiceItem[] = [];
|
||||||
|
|
||||||
|
dtoItems.forEach((item, index) => {
|
||||||
|
const path = (field: string) => `items[${index}].${field}`;
|
||||||
|
|
||||||
|
const description = extractOrPushError(
|
||||||
|
CustomerInvoiceItemDescription.create(item.description),
|
||||||
|
path("description"),
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const quantity = extractOrPushError(
|
||||||
|
CustomerInvoiceItemQuantity.create({
|
||||||
|
amount: item.quantity.amount,
|
||||||
|
scale: item.quantity.scale,
|
||||||
|
}),
|
||||||
|
path("quantity"),
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const unitPrice = extractOrPushError(
|
||||||
|
CustomerInvoiceItemUnitPrice.create({
|
||||||
|
amount: item.unitPrice.amount,
|
||||||
|
scale: item.unitPrice.scale,
|
||||||
|
currency_code: item.unitPrice.currency,
|
||||||
|
}),
|
||||||
|
path("unit_price"),
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
const discount = extractOrPushError(
|
||||||
|
CustomerInvoiceItemDiscount.create({
|
||||||
|
amount: item.discount.amount,
|
||||||
|
scale: item.discount.scale,
|
||||||
|
}),
|
||||||
|
path("discount"),
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
|
||||||
|
if (errors.length === 0) {
|
||||||
|
const itemProps = {
|
||||||
|
description: description,
|
||||||
|
quantity: quantity,
|
||||||
|
unitPrice: unitPrice,
|
||||||
|
discount: discount,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (hasNoUndefinedFields(itemProps)) {
|
||||||
|
// Validar y crear el item de factura
|
||||||
|
const itemOrError = CustomerInvoiceItem.create(itemProps);
|
||||||
|
|
||||||
|
if (itemOrError.isSuccess) {
|
||||||
|
items.push(itemOrError.data);
|
||||||
|
} else {
|
||||||
|
errors.push({ path: `items[${index}]`, message: itemOrError.error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Result.fail(new ValidationErrorCollection(errors));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Result.ok(items);
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import { DomainValidationError, ValidationErrorDetail } from "@erp/core/api";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrae un valor de un Result si es válido.
|
||||||
|
* Si es un fallo, agrega un ValidationErrorDetail al array proporcionado.
|
||||||
|
* @param result - El resultado a evaluar.
|
||||||
|
* @param path - La ruta del error para el detalle de validación.
|
||||||
|
* @param errors - El array donde se agregarán los errores de validación.
|
||||||
|
* @returns El valor extraído si el resultado es exitoso, o undefined si es un fallo.
|
||||||
|
* @template T - El tipo de dato esperado en el resultado exitoso.
|
||||||
|
* @throws {Error} Si el resultado es un fallo y no es una instancia de DomainValidationError.
|
||||||
|
* @example
|
||||||
|
* const result = Result.ok(42);
|
||||||
|
* const value = extractOrPushError(result, 'some.path', []);
|
||||||
|
* console.log(value); // 42
|
||||||
|
* const errorResult = Result.fail(new Error('Something went wrong'));
|
||||||
|
* const value = extractOrPushError(errorResult, 'some.path', []);
|
||||||
|
* console.log(value); // undefined
|
||||||
|
* // errors will contain [{ path: 'some.path', message: 'Something went wrong' }]
|
||||||
|
*
|
||||||
|
* @see Result
|
||||||
|
* @see DomainValidationError
|
||||||
|
* @see ValidationErrorDetail
|
||||||
|
|
||||||
|
*/
|
||||||
|
export function extractOrPushError<T>(
|
||||||
|
result: Result<T, Error>,
|
||||||
|
path: string,
|
||||||
|
errors: ValidationErrorDetail[]
|
||||||
|
): T | undefined {
|
||||||
|
if (result.isFailure) {
|
||||||
|
const error = result.error;
|
||||||
|
|
||||||
|
if (error instanceof DomainValidationError) {
|
||||||
|
errors.push({ path, message: error.detail });
|
||||||
|
} else {
|
||||||
|
errors.push({ path, message: error.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param obj - El objeto a evaluar.
|
||||||
|
* @template T - El tipo del objeto.
|
||||||
|
* @description Verifica si un objeto no tiene campos con valor undefined.
|
||||||
|
*
|
||||||
|
* Esta función recorre los valores del objeto y devuelve true si todos los valores son diferentes de undefined.
|
||||||
|
* Si al menos un valor es undefined, devuelve false.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const obj = { a: 1, b: 'test', c: null };
|
||||||
|
* console.log(hasNoUndefinedFields(obj)); // true
|
||||||
|
*
|
||||||
|
* const objWithUndefined = { a: 1, b: undefined, c: null };
|
||||||
|
* console.log(hasNoUndefinedFields(objWithUndefined)); // false
|
||||||
|
*
|
||||||
|
* @template T - El tipo del objeto.
|
||||||
|
* @param obj - El objeto a evaluar.
|
||||||
|
* @returns true si el objeto no tiene campos undefined, false en caso contrario.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function hasNoUndefinedFields<T extends Record<string, any>>(
|
||||||
|
obj: T
|
||||||
|
): obj is { [K in keyof T]-?: Exclude<T[K], undefined> } {
|
||||||
|
return Object.values(obj).every((value) => value !== undefined);
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./build-customer-invoice-from-dto";
|
||||||
@ -1,5 +1,5 @@
|
|||||||
//export * from "./create-customer-invoice.use-case";
|
//export * from "./create-customer-invoice";
|
||||||
//export * from "./delete-customer-invoice.use-case";
|
//export * from "./delete-customer-invoice";
|
||||||
export * from "./get-customer-invoice.use-case";
|
//export * from "./get-customer-invoice";
|
||||||
export * from "./list-customer-invoices.use-case";
|
export * from "./list-customer-invoices";
|
||||||
//export * from "./update-customer-invoice.use-case";
|
//export * from "./update-customer-invoice";
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
import { ITransactionManager } from "@erp/core/api";
|
|
||||||
import { Criteria } from "@repo/rdx-criteria/server";
|
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
|
||||||
import { Transaction } from "sequelize";
|
|
||||||
import { CustomerInvoice, ICustomerInvoiceService } from "../domain";
|
|
||||||
|
|
||||||
export class ListCustomerInvoicesUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly customerInvoiceService: ICustomerInvoiceService,
|
|
||||||
private readonly transactionManager: ITransactionManager
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public execute(criteria: Criteria): Promise<Result<Collection<CustomerInvoice>, Error>> {
|
|
||||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
|
||||||
try {
|
|
||||||
return await this.customerInvoiceService.findCustomerInvoices(criteria, transaction);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./list-customer-invoices.use-case";
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { ITransactionManager } from "@erp/core/api";
|
||||||
|
import { ListCustomerInvoicesResultDTO } from "@erp/customer-invoices/common/dto";
|
||||||
|
import { Criteria } from "@repo/rdx-criteria/server";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
import { Transaction } from "sequelize";
|
||||||
|
import { ICustomerInvoiceService } from "../../domain";
|
||||||
|
import { ListCustomerInvoicesPresenter } from "./presenter";
|
||||||
|
|
||||||
|
export class ListCustomerInvoicesUseCase {
|
||||||
|
constructor(
|
||||||
|
private readonly customerInvoiceService: ICustomerInvoiceService,
|
||||||
|
private readonly transactionManager: ITransactionManager,
|
||||||
|
private readonly presenter: ListCustomerInvoicesPresenter
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public execute(criteria: Criteria): Promise<Result<ListCustomerInvoicesResultDTO, Error>> {
|
||||||
|
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||||
|
try {
|
||||||
|
const result = await this.customerInvoiceService.findCustomerInvoices(
|
||||||
|
criteria,
|
||||||
|
transaction
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
return Result.fail(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto: ListCustomerInvoicesResultDTO = this.presenter.toDTO(result.data, criteria);
|
||||||
|
return Result.ok(dto);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return Result.fail(error as Error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,21 +1,20 @@
|
|||||||
import { IListResponseDTO } from "@erp/core";
|
import { ListCustomerInvoicesViewDTO } from "@erp/customer-invoices/common/dto";
|
||||||
import { Criteria } from "@repo/rdx-criteria/server";
|
import { Criteria } from "@repo/rdx-criteria/server";
|
||||||
import { Collection } from "@repo/rdx-utils";
|
import { Collection } from "@repo/rdx-utils";
|
||||||
import { IListCustomerInvoicesResponseDTO } from "../../../../common/dto";
|
|
||||||
import { CustomerInvoice } from "../../../domain";
|
import { CustomerInvoice } from "../../../domain";
|
||||||
|
|
||||||
export interface IListCustomerInvoicesPresenter {
|
export interface ListCustomerInvoicesPresenter {
|
||||||
toDTO: (
|
toDTO: (
|
||||||
customerInvoices: Collection<CustomerInvoice>,
|
customerInvoices: Collection<CustomerInvoice>,
|
||||||
criteria: Criteria
|
criteria: Criteria
|
||||||
) => IListResponseDTO<IListCustomerInvoicesResponseDTO>;
|
) => ListCustomerInvoicesViewDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const listCustomerInvoicesPresenter: IListCustomerInvoicesPresenter = {
|
export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
|
||||||
toDTO: (
|
toDTO: (
|
||||||
customerInvoices: Collection<CustomerInvoice>,
|
customerInvoices: Collection<CustomerInvoice>,
|
||||||
criteria: Criteria
|
criteria: Criteria
|
||||||
): IListResponseDTO<IListCustomerInvoicesResponseDTO> => {
|
): ListCustomerInvoicesViewDTO => {
|
||||||
const items = customerInvoices.map((invoice) => {
|
const items = customerInvoices.map((invoice) => {
|
||||||
return {
|
return {
|
||||||
id: invoice.id.toPrimitive(),
|
id: invoice.id.toPrimitive(),
|
||||||
@ -26,17 +25,17 @@ export const listCustomerInvoicesPresenter: IListCustomerInvoicesPresenter = {
|
|||||||
issue_date: invoice.issueDate.toISOString(),
|
issue_date: invoice.issueDate.toISOString(),
|
||||||
operation_date: invoice.operationDate.toISOString(),
|
operation_date: invoice.operationDate.toISOString(),
|
||||||
language_code: "ES",
|
language_code: "ES",
|
||||||
|
|
||||||
currency: invoice.customerInvoiceCurrency.toString(),
|
currency: invoice.customerInvoiceCurrency.toString(),
|
||||||
subtotal: invoice.calculateSubtotal().toPrimitive(),
|
|
||||||
total: invoice.calculateTotal().toPrimitive(),
|
subtotal_price: invoice.calculateSubtotal().toPrimitive(),
|
||||||
|
total_price: invoice.calculateTotal().toPrimitive(),
|
||||||
|
|
||||||
//recipient: CustomerInvoiceParticipantPresenter(customerInvoice.recipient),
|
//recipient: CustomerInvoiceParticipantPresenter(customerInvoice.recipient),
|
||||||
|
|
||||||
metadata: {
|
metadata: {
|
||||||
entity: "customer-invoice",
|
entity: "customer-invoice",
|
||||||
},
|
},
|
||||||
} as IListCustomerInvoicesResponseDTO;
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalItems = customerInvoices.total();
|
const totalItems = customerInvoices.total();
|
||||||
@ -47,6 +46,15 @@ export const listCustomerInvoicesPresenter: IListCustomerInvoicesPresenter = {
|
|||||||
total_pages: Math.ceil(totalItems / criteria.pageSize),
|
total_pages: Math.ceil(totalItems / criteria.pageSize),
|
||||||
total_items: totalItems,
|
total_items: totalItems,
|
||||||
items: items,
|
items: items,
|
||||||
|
metadata: {
|
||||||
|
entity: "customer-invoices",
|
||||||
|
criteria: criteria.toJSON(),
|
||||||
|
links: {
|
||||||
|
self: `/api/customer-invoices?page=${criteria.pageNumber}&per_page=${criteria.pageSize}`,
|
||||||
|
first: `/api/customer-invoices?page=1&per_page=${criteria.pageSize}`,
|
||||||
|
last: `/api/customer-invoices?page=${Math.ceil(totalItems / criteria.pageSize)}&per_page=${criteria.pageSize}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./update-customer-invoice.use-case";
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { ExpressController, errorMapper } from "@erp/core/api";
|
||||||
|
import { CreateCustomerInvoiceCommandDTO } from "../../../common/dto";
|
||||||
|
|
||||||
|
export class CreateCustomerInvoiceController extends ExpressController {
|
||||||
|
public constructor(
|
||||||
|
private readonly createCustomerInvoice: any, // Replace with actual type
|
||||||
|
private readonly presenter: any // Replace with actual type
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl() {
|
||||||
|
const dto = this.req.body as CreateCustomerInvoiceCommandDTO;
|
||||||
|
/*
|
||||||
|
const user = this.req.user; // asumimos middleware authenticateJWT inyecta user
|
||||||
|
|
||||||
|
if (!user || !user.companyId) {
|
||||||
|
this.unauthorized(res, "Unauthorized: user or company not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inyectar empresa del usuario autenticado (ownership)
|
||||||
|
dto.customerCompanyId = user.companyId;
|
||||||
|
*/
|
||||||
|
|
||||||
|
const result = await this.createCustomerInvoice.execute(dto);
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
/*if (error instanceof AggregatedValidationError) {
|
||||||
|
return this.invalidInputError(error.message, error.details);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
const apiError = errorMapper.toApiError(result.error);
|
||||||
|
return this.handleApiError(apiError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.created(this.presenter.toDTO(result.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./create-customer-invoice";
|
||||||
5
modules/customer-invoices/src/api/controllers/index.ts
Normal file
5
modules/customer-invoices/src/api/controllers/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./create-customer-invoice";
|
||||||
|
//export * from "./delete-customer-invoice";
|
||||||
|
export * from "./get-customer-invoice";
|
||||||
|
export * from "./list-customer-invoices";
|
||||||
|
///export * from "./update-customer-invoice";
|
||||||
@ -1,18 +1,22 @@
|
|||||||
import { SequelizeTransactionManager } from "@erp/core/api";
|
import { SequelizeTransactionManager } from "@erp/core/api";
|
||||||
import { Sequelize } from "sequelize";
|
import { Sequelize } from "sequelize";
|
||||||
import { ListCustomerInvoicesUseCase } from "../../application";
|
import { ListCustomerInvoicesUseCase } from "../../application";
|
||||||
|
import { listCustomerInvoicesPresenter } from "../../application/list-customer-invoices/presenter";
|
||||||
import { CustomerInvoiceService } from "../../domain";
|
import { CustomerInvoiceService } from "../../domain";
|
||||||
import { CustomerInvoiceRepository, customerInvoiceMapper } from "../../infrastructure";
|
import { CustomerInvoiceRepository, customerInvoiceMapper } from "../../infrastructure";
|
||||||
import { ListCustomerInvoicesController } from "./list-invoices.controller";
|
import { ListCustomerInvoicesController } from "./list-customer-invoices.controller";
|
||||||
import { listCustomerInvoicesPresenter } from "./presenter";
|
|
||||||
|
|
||||||
export const buildListCustomerInvoicesController = (database: Sequelize) => {
|
export const buildListCustomerInvoicesController = (database: Sequelize) => {
|
||||||
const transactionManager = new SequelizeTransactionManager(database);
|
const transactionManager = new SequelizeTransactionManager(database);
|
||||||
const customerInvoiceRepository = new CustomerInvoiceRepository(database, customerInvoiceMapper);
|
const customerInvoiceRepository = new CustomerInvoiceRepository(database, customerInvoiceMapper);
|
||||||
const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository);
|
const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository);
|
||||||
|
|
||||||
const useCase = new ListCustomerInvoicesUseCase(customerInvoiceService, transactionManager);
|
|
||||||
const presenter = listCustomerInvoicesPresenter;
|
const presenter = listCustomerInvoicesPresenter;
|
||||||
|
|
||||||
return new ListCustomerInvoicesController(useCase, presenter);
|
const useCase = new ListCustomerInvoicesUseCase(
|
||||||
|
customerInvoiceService,
|
||||||
|
transactionManager,
|
||||||
|
presenter
|
||||||
|
);
|
||||||
|
|
||||||
|
return new ListCustomerInvoicesController(useCase);
|
||||||
};
|
};
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { ExpressController, errorMapper } from "@erp/core/api";
|
||||||
|
import { ListCustomerInvoicesUseCase } from "../../application";
|
||||||
|
|
||||||
|
export class ListCustomerInvoicesController extends ExpressController {
|
||||||
|
public constructor(private readonly listCustomerInvoices: ListCustomerInvoicesUseCase) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl() {
|
||||||
|
const criteria = this.criteria;
|
||||||
|
|
||||||
|
/*
|
||||||
|
const user = this.req.user; // asumimos middleware authenticateJWT inyecta user
|
||||||
|
|
||||||
|
if (!user || !user.companyId) {
|
||||||
|
this.unauthorized(res, "Unauthorized: user or company not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inyectar empresa del usuario autenticado (ownership)
|
||||||
|
this.criteria.addFilter("companyId", "=", companyId);
|
||||||
|
*/
|
||||||
|
|
||||||
|
const result = await this.listCustomerInvoices.execute(criteria);
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
/*if (error instanceof AggregatedValidationError) {
|
||||||
|
return this.invalidInputError(error.message, error.details);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
const apiError = errorMapper.toApiError(result.error);
|
||||||
|
return this.handleApiError(apiError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.ok(result.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,7 +7,7 @@ import {
|
|||||||
CustomerInvoiceStatus,
|
CustomerInvoiceStatus,
|
||||||
} from "../value-objects";
|
} from "../value-objects";
|
||||||
|
|
||||||
export interface ICustomerInvoiceProps {
|
export interface CustomerInvoiceProps {
|
||||||
customerInvoiceNumber: CustomerInvoiceNumber;
|
customerInvoiceNumber: CustomerInvoiceNumber;
|
||||||
customerInvoiceSeries: CustomerInvoiceSerie;
|
customerInvoiceSeries: CustomerInvoiceSerie;
|
||||||
|
|
||||||
@ -69,19 +69,19 @@ export interface ICustomerInvoice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CustomerInvoice
|
export class CustomerInvoice
|
||||||
extends AggregateRoot<ICustomerInvoiceProps>
|
extends AggregateRoot<CustomerInvoiceProps>
|
||||||
implements ICustomerInvoice
|
implements ICustomerInvoice
|
||||||
{
|
{
|
||||||
private _items!: Collection<CustomerInvoiceItem>;
|
private _items!: Collection<CustomerInvoiceItem>;
|
||||||
//protected _status: CustomerInvoiceStatus;
|
//protected _status: CustomerInvoiceStatus;
|
||||||
|
|
||||||
protected constructor(props: ICustomerInvoiceProps, id?: UniqueID) {
|
protected constructor(props: CustomerInvoiceProps, id?: UniqueID) {
|
||||||
super(props, id);
|
super(props, id);
|
||||||
|
|
||||||
this._items = props.items || CustomerInvoiceItems.create();
|
this._items = props.items || CustomerInvoiceItems.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
static create(props: ICustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> {
|
static create(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> {
|
||||||
const customerInvoice = new CustomerInvoice(props, id);
|
const customerInvoice = new CustomerInvoice(props, id);
|
||||||
|
|
||||||
// Reglas de negocio / validaciones
|
// Reglas de negocio / validaciones
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
export * from "./aggregates";
|
export * from "./aggregates";
|
||||||
export * from "./entities";
|
export * from "./entities";
|
||||||
|
export * from "./errors";
|
||||||
export * from "./repositories";
|
export * from "./repositories";
|
||||||
export * from "./services";
|
export * from "./services";
|
||||||
export * from "./value-objects";
|
export * from "./value-objects";
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Criteria } from "@repo/rdx-criteria/server";
|
import { Criteria } from "@repo/rdx-criteria/server";
|
||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
import { Collection, Result } from "@repo/rdx-utils";
|
||||||
import { CustomerInvoice, ICustomerInvoiceProps } from "../aggregates";
|
import { CustomerInvoice, CustomerInvoiceProps } from "../aggregates";
|
||||||
|
|
||||||
export interface ICustomerInvoiceService {
|
export interface ICustomerInvoiceService {
|
||||||
findCustomerInvoices(
|
findCustomerInvoices(
|
||||||
@ -15,13 +15,13 @@ export interface ICustomerInvoiceService {
|
|||||||
|
|
||||||
updateCustomerInvoiceById(
|
updateCustomerInvoiceById(
|
||||||
customerInvoiceId: UniqueID,
|
customerInvoiceId: UniqueID,
|
||||||
data: Partial<ICustomerInvoiceProps>,
|
data: Partial<CustomerInvoiceProps>,
|
||||||
transaction?: any
|
transaction?: any
|
||||||
): Promise<Result<CustomerInvoice, Error>>;
|
): Promise<Result<CustomerInvoice, Error>>;
|
||||||
|
|
||||||
createCustomerInvoice(
|
createCustomerInvoice(
|
||||||
customerInvoiceId: UniqueID,
|
customerInvoiceId: UniqueID,
|
||||||
data: ICustomerInvoiceProps,
|
data: CustomerInvoiceProps,
|
||||||
transaction?: any
|
transaction?: any
|
||||||
): Promise<Result<CustomerInvoice, Error>>;
|
): Promise<Result<CustomerInvoice, Error>>;
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Criteria } from "@repo/rdx-criteria/server";
|
|||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
import { Collection, Result } from "@repo/rdx-utils";
|
||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { CustomerInvoice, ICustomerInvoiceProps } from "../aggregates";
|
import { CustomerInvoice, CustomerInvoiceProps } from "../aggregates";
|
||||||
import { ICustomerInvoiceRepository } from "../repositories";
|
import { ICustomerInvoiceRepository } from "../repositories";
|
||||||
import { ICustomerInvoiceService } from "./customer-invoice-service.interface";
|
import { ICustomerInvoiceService } from "./customer-invoice-service.interface";
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ export class CustomerInvoiceService implements ICustomerInvoiceService {
|
|||||||
|
|
||||||
async updateCustomerInvoiceById(
|
async updateCustomerInvoiceById(
|
||||||
customerInvoiceId: UniqueID,
|
customerInvoiceId: UniqueID,
|
||||||
data: Partial<ICustomerInvoiceProps>,
|
data: Partial<CustomerInvoiceProps>,
|
||||||
transaction?: Transaction
|
transaction?: Transaction
|
||||||
): Promise<Result<CustomerInvoice, Error>> {
|
): Promise<Result<CustomerInvoice, Error>> {
|
||||||
// Verificar si la factura existe
|
// Verificar si la factura existe
|
||||||
@ -60,7 +60,7 @@ export class CustomerInvoiceService implements ICustomerInvoiceService {
|
|||||||
|
|
||||||
async createCustomerInvoice(
|
async createCustomerInvoice(
|
||||||
customerInvoiceId: UniqueID,
|
customerInvoiceId: UniqueID,
|
||||||
data: ICustomerInvoiceProps,
|
data: CustomerInvoiceProps,
|
||||||
transaction?: Transaction
|
transaction?: Transaction
|
||||||
): Promise<Result<CustomerInvoice, Error>> {
|
): Promise<Result<CustomerInvoice, Error>> {
|
||||||
// Verificar si la factura existe
|
// Verificar si la factura existe
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
import { DomainValidationError } from "@erp/core/api";
|
||||||
import { ValueObject } from "@repo/rdx-ddd";
|
import { ValueObject } from "@repo/rdx-ddd";
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
interface ICustomerInvoiceItemDescriptionProps {
|
interface ICustomerInvoiceItemDescriptionProps {
|
||||||
value: string;
|
value: string;
|
||||||
@ -8,6 +9,8 @@ interface ICustomerInvoiceItemDescriptionProps {
|
|||||||
|
|
||||||
export class CustomerInvoiceItemDescription extends ValueObject<ICustomerInvoiceItemDescriptionProps> {
|
export class CustomerInvoiceItemDescription extends ValueObject<ICustomerInvoiceItemDescriptionProps> {
|
||||||
private static readonly MAX_LENGTH = 255;
|
private static readonly MAX_LENGTH = 255;
|
||||||
|
private static readonly FIELD = "invoiceItemDescription";
|
||||||
|
private static readonly ERROR_CODE = "INVALID_INVOICE_ITEM_DESCRIPTION";
|
||||||
|
|
||||||
protected static validate(value: string) {
|
protected static validate(value: string) {
|
||||||
const schema = z
|
const schema = z
|
||||||
@ -20,10 +23,17 @@ export class CustomerInvoiceItemDescription extends ValueObject<ICustomerInvoice
|
|||||||
}
|
}
|
||||||
|
|
||||||
static create(value: string) {
|
static create(value: string) {
|
||||||
const valueIsValid = CustomerInvoiceItemDescription.validate(value);
|
const result = CustomerInvoiceItemDescription.validate(value);
|
||||||
|
|
||||||
if (!valueIsValid.success) {
|
if (!result.success) {
|
||||||
return Result.fail(new Error(valueIsValid.error.errors[0].message));
|
const detail = result.error.message;
|
||||||
|
return Result.fail(
|
||||||
|
new DomainValidationError(
|
||||||
|
CustomerInvoiceItemDescription.ERROR_CODE,
|
||||||
|
CustomerInvoiceItemDescription.FIELD,
|
||||||
|
detail
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Result.ok(new CustomerInvoiceItemDescription({ value }));
|
return Result.ok(new CustomerInvoiceItemDescription({ value }));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
import { DomainValidationError } from "@erp/core/api";
|
||||||
import { ValueObject } from "@repo/rdx-ddd";
|
import { ValueObject } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { z } from "zod";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
interface ICustomerInvoiceNumberProps {
|
interface ICustomerInvoiceNumberProps {
|
||||||
value: string;
|
value: string;
|
||||||
@ -8,22 +9,31 @@ interface ICustomerInvoiceNumberProps {
|
|||||||
|
|
||||||
export class CustomerInvoiceNumber extends ValueObject<ICustomerInvoiceNumberProps> {
|
export class CustomerInvoiceNumber extends ValueObject<ICustomerInvoiceNumberProps> {
|
||||||
private static readonly MAX_LENGTH = 255;
|
private static readonly MAX_LENGTH = 255;
|
||||||
|
private static readonly FIELD = "invoiceNumber";
|
||||||
|
private static readonly ERROR_CODE = "INVALID_INVOICE_NUMBER";
|
||||||
|
|
||||||
protected static validate(value: string) {
|
protected static validate(value: string) {
|
||||||
const schema = z
|
const schema = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.max(CustomerInvoiceNumber.MAX_LENGTH, {
|
.max(CustomerInvoiceNumber.MAX_LENGTH, {
|
||||||
message: `Name must be at most ${CustomerInvoiceNumber.MAX_LENGTH} characters long`,
|
message: `String must be at most ${CustomerInvoiceNumber.MAX_LENGTH} characters long`,
|
||||||
});
|
});
|
||||||
return schema.safeParse(value);
|
return schema.safeParse(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
static create(value: string) {
|
static create(value: string) {
|
||||||
const valueIsValid = CustomerInvoiceNumber.validate(value);
|
const result = CustomerInvoiceNumber.validate(value);
|
||||||
|
|
||||||
if (!valueIsValid.success) {
|
if (!result.success) {
|
||||||
return Result.fail(new Error(valueIsValid.error.errors[0].message));
|
const detail = result.error.message;
|
||||||
|
return Result.fail(
|
||||||
|
new DomainValidationError(
|
||||||
|
CustomerInvoiceNumber.ERROR_CODE,
|
||||||
|
CustomerInvoiceNumber.FIELD,
|
||||||
|
detail
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Result.ok(new CustomerInvoiceNumber({ value }));
|
return Result.ok(new CustomerInvoiceNumber({ value }));
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user