Proveedores
This commit is contained in:
parent
0eed8453cc
commit
34520a18ba
@ -45,6 +45,7 @@
|
|||||||
"@erp/document-series": "workspace:*",
|
"@erp/document-series": "workspace:*",
|
||||||
"@erp/identity": "workspace:*",
|
"@erp/identity": "workspace:*",
|
||||||
"@erp/factuges": "workspace:*",
|
"@erp/factuges": "workspace:*",
|
||||||
|
"@erp/suppliers": "workspace:*",
|
||||||
"@repo/rdx-logger": "workspace:*",
|
"@repo/rdx-logger": "workspace:*",
|
||||||
"@repo/rdx-utils": "workspace:*",
|
"@repo/rdx-utils": "workspace:*",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
@ -78,4 +79,4 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,7 @@ import customersAPIModule from "@erp/customers/api";
|
|||||||
import documentSeriesAPIModule from "@erp/document-series/api";
|
import documentSeriesAPIModule from "@erp/document-series/api";
|
||||||
import factuGESAPIModule from "@erp/factuges/api";
|
import factuGESAPIModule from "@erp/factuges/api";
|
||||||
import identityAPIModule from "@erp/identity/api";
|
import identityAPIModule from "@erp/identity/api";
|
||||||
|
import suppliersAPIModule from "@erp/suppliers/api";
|
||||||
//import suppliersAPIModule from "@erp/suppliers/api";
|
|
||||||
|
|
||||||
import { registerModule } from "./lib";
|
import { registerModule } from "./lib";
|
||||||
|
|
||||||
@ -23,5 +22,5 @@ export const registerModules = () => {
|
|||||||
registerModule(documentSeriesAPIModule);
|
registerModule(documentSeriesAPIModule);
|
||||||
registerModule(customerInvoicesAPIModule);
|
registerModule(customerInvoicesAPIModule);
|
||||||
registerModule(factuGESAPIModule);
|
registerModule(factuGESAPIModule);
|
||||||
//registerModule(suppliersAPIModule);
|
registerModule(suppliersAPIModule);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
"@erp/customer-invoices": "workspace:*",
|
"@erp/customer-invoices": "workspace:*",
|
||||||
"@erp/customers": "workspace:*",
|
"@erp/customers": "workspace:*",
|
||||||
"@erp/identity": "workspace:*",
|
"@erp/identity": "workspace:*",
|
||||||
|
"@erp/suppliers": "workspace:*",
|
||||||
"@repo/rdx-utils": "workspace:*",
|
"@repo/rdx-utils": "workspace:*",
|
||||||
"@fontsource-variable/geist": "^5.2.8",
|
"@fontsource-variable/geist": "^5.2.8",
|
||||||
"@fontsource-variable/geist-mono": "^5.2.7",
|
"@fontsource-variable/geist-mono": "^5.2.7",
|
||||||
@ -61,4 +62,4 @@
|
|||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"vite-plugin-html": "^3.2.2"
|
"vite-plugin-html": "^3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import CoreModuleManifest, { type IModuleClient } from "@erp/core/client";
|
|||||||
import { CustomerInvoicesModuleManifest } from "@erp/customer-invoices/client";
|
import { CustomerInvoicesModuleManifest } from "@erp/customer-invoices/client";
|
||||||
import { CustomersModuleManifest } from "@erp/customers/client";
|
import { CustomersModuleManifest } from "@erp/customers/client";
|
||||||
import { IdentityModuleManifest } from "@erp/identity/client";
|
import { IdentityModuleManifest } from "@erp/identity/client";
|
||||||
|
import { SuppliersModuleManifest } from "@erp/suppliers/client";
|
||||||
|
|
||||||
export const modules: IModuleClient[] = [
|
export const modules: IModuleClient[] = [
|
||||||
IdentityModuleManifest,
|
IdentityModuleManifest,
|
||||||
@ -11,5 +12,6 @@ export const modules: IModuleClient[] = [
|
|||||||
CompaniesModuleManifest,
|
CompaniesModuleManifest,
|
||||||
CatalogsModuleManifest,
|
CatalogsModuleManifest,
|
||||||
CustomersModuleManifest,
|
CustomersModuleManifest,
|
||||||
|
SuppliersModuleManifest,
|
||||||
CustomerInvoicesModuleManifest,
|
CustomerInvoicesModuleManifest,
|
||||||
];
|
];
|
||||||
|
|||||||
174
docs/suppliers/frontend-suppliers-ui-ux-design.md
Normal file
174
docs/suppliers/frontend-suppliers-ui-ux-design.md
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# Frontend de suppliers: diseño e implementación UI/UX
|
||||||
|
|
||||||
|
## 1. Objetivo
|
||||||
|
|
||||||
|
El frontend de `suppliers` debe ofrecer listado, detalle, creación y edición con el mismo lenguaje visual y los mismos patrones de navegación que `customers`, respetando el contrato funcional más pequeño de Supplier.
|
||||||
|
|
||||||
|
Este documento es la fuente normativa para la implementación. La paridad buscada es de arquitectura y experiencia, no de campos ni capacidades de negocio.
|
||||||
|
|
||||||
|
## 2. Estado actual
|
||||||
|
|
||||||
|
Antes de esta implementación, Supplier solo disponía de entidades frontend, adapters, APIs y hooks de TanStack Query. La sidebar enlazaba a `/suppliers`, pero no existían manifest, rutas, páginas, formularios ni traducciones del módulo.
|
||||||
|
|
||||||
|
El API client tenía una incompatibilidad activa: la actualización delegaba en `IDataSource.updateOne()`, que usa PUT, aunque Supplier solo expone PATCH.
|
||||||
|
|
||||||
|
## 3. Customer como referencia
|
||||||
|
|
||||||
|
Se adoptan de Customer:
|
||||||
|
|
||||||
|
- manifest y rutas lazy por módulo;
|
||||||
|
- páginas de listado con `RdxPageContainer`, `RdxPageHeader`, búsqueda con debounce y `DataTable`;
|
||||||
|
- editores fullscreen con `RdxEditorPageLayout`, header persistente, protección de cambios sin guardar y secciones Rdx;
|
||||||
|
- `PageErrorState`, skeletons, alertas y toasts del sistema;
|
||||||
|
- React Hook Form con validación Zod;
|
||||||
|
- queries y mutations encapsuladas en hooks y adapters de transporte.
|
||||||
|
|
||||||
|
No se copia la fiscalidad, la eliminación, el panel lateral ni las acciones exclusivas de Customer.
|
||||||
|
|
||||||
|
## 4. Diferencias funcionales Customer/Supplier
|
||||||
|
|
||||||
|
| Área | Customer | Supplier |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Lifecycle | Puede tener capacidades propias | Status solo informativo |
|
||||||
|
| Eliminación | Existe contrato Customer | No existe DELETE Supplier |
|
||||||
|
| Fiscalidad | Incluye configuración fiscal | Fuera de alcance |
|
||||||
|
| Pagos | Puede incluir términos y métodos | Fuera de alcance |
|
||||||
|
| Dirección | Contrato agrupado en áreas modernas | DTO Supplier plano |
|
||||||
|
| Actualización | Contrato propio | PATCH parcial, sin `null` para borrar |
|
||||||
|
|
||||||
|
## 5. Arquitectura frontend Supplier
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Page
|
||||||
|
-> controller hook
|
||||||
|
-> query/mutation hook
|
||||||
|
-> API function
|
||||||
|
-> DTO adapter
|
||||||
|
-> frontend entity
|
||||||
|
```
|
||||||
|
|
||||||
|
Los DTO snake_case quedan limitados a `common`, API y adapters. Páginas, formularios y entidades usan camelCase.
|
||||||
|
|
||||||
|
## 6. Rutas
|
||||||
|
|
||||||
|
| Ruta | Pantalla | Layout |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `/suppliers` | Listado | `app-sidebar` |
|
||||||
|
| `/suppliers/create` | Creación | `app-fullscreen` |
|
||||||
|
| `/suppliers/:id` | Detalle | `app-fullscreen` |
|
||||||
|
| `/suppliers/:id/edit` | Edición | `app-fullscreen` |
|
||||||
|
|
||||||
|
Se adopta `edit`, que es la convención vigente de Customer. No se crean rutas DELETE, activate o deactivate.
|
||||||
|
|
||||||
|
## 7. Listado
|
||||||
|
|
||||||
|
El listado usa header, descripción, acción principal, `SimpleSearchInput`, tabla server-side y paginación. Las columnas son:
|
||||||
|
|
||||||
|
- identidad: nombre, nombre comercial, TIN y tipo empresa/persona;
|
||||||
|
- referencia;
|
||||||
|
- contacto principal;
|
||||||
|
- status no interactivo;
|
||||||
|
- acciones Ver y Editar.
|
||||||
|
|
||||||
|
Solo `name`, `reference` y `email_primary` ofrecen sorting, porque pertenecen a la whitelist real. La búsqueda usa el query param `q`, debounce de 300 ms y reinicia la página.
|
||||||
|
|
||||||
|
## 8. Detail
|
||||||
|
|
||||||
|
El detalle presenta un header con nombre, contexto secundario y badge de status. Se agrupa en Identificación, Contacto, Dirección y Preferencias. Los valores ausentes muestran `-` para mantener una cuadrícula estable. Las únicas acciones son Volver y Editar.
|
||||||
|
|
||||||
|
## 9. Create
|
||||||
|
|
||||||
|
Create reutiliza el editor común Supplier y genera el UUID en el cliente. El payload conserva el DTO plano del backend y omite campos opcionales vacíos. Tras éxito actualiza caché, muestra feedback y navega al detalle.
|
||||||
|
|
||||||
|
## 10. Update
|
||||||
|
|
||||||
|
Update carga el detalle, adapta la entidad al formulario y construye el PATCH desde `dirtyFields`. Solo se envían campos modificados con valor utilizable. Un campo vacío se omite porque el contrato vigente no admite `null => borrar`; la UI no inventa esa semántica.
|
||||||
|
|
||||||
|
La petición se realiza explícitamente con método PATCH, nunca con `IDataSource.updateOne()`.
|
||||||
|
|
||||||
|
## 11. Formularios y validación
|
||||||
|
|
||||||
|
Un único `SupplierFormSchema` en camelCase valida create y update. Se reutilizan schemas semánticos de Core para TIN, país, código postal, idioma, moneda, teléfono y web. Las secciones son Identificación, Dirección, Contacto y Preferencias.
|
||||||
|
|
||||||
|
`isCompany` es boolean y usa el selector visual empresa/persona. `status` no forma parte del formulario.
|
||||||
|
|
||||||
|
## 12. API / React Query
|
||||||
|
|
||||||
|
Las keys separan listado y detalle. Create establece el detalle e invalida listados. Update establece el detalle, parchea filas presentes e invalida listados. El cambio de empresa reutiliza el reset global de queries y el interceptor común que añade `X-Company-Id`.
|
||||||
|
|
||||||
|
## 13. Estados loading/error/empty
|
||||||
|
|
||||||
|
- List usa `SkeletonDataTable` en carga inicial.
|
||||||
|
- Detail y Update usan skeleton de editor.
|
||||||
|
- Errores bloqueantes usan `PageErrorState` con reintento y regreso.
|
||||||
|
- Errores de mutación usan `ErrorAlert` y toast.
|
||||||
|
- La tabla conserva el empty state común; una búsqueda sin resultados mantiene visible el buscador.
|
||||||
|
|
||||||
|
## 14. Responsive
|
||||||
|
|
||||||
|
Header y acciones apilan en móvil. Formularios pasan de dos columnas a una. Las tarjetas de detalle usan una columna en móvil y dos en escritorio. La tabla conserva scroll horizontal y paginación del componente compartido.
|
||||||
|
|
||||||
|
## 15. Accesibilidad
|
||||||
|
|
||||||
|
Todos los inputs tienen labels asociados mediante los componentes Rdx. Los botones incluyen nombre accesible, los iconos decorativos usan `aria-hidden`, las filas de acciones son navegables y los errores se asocian mediante React Hook Form. El foco se mueve al primer campo inválido.
|
||||||
|
|
||||||
|
## 16. i18n
|
||||||
|
|
||||||
|
Todos los textos visibles pertenecen al namespace `Suppliers`. Se mantienen recursos completos en `es` y `en`; no se insertan textos de interfaz específicos en componentes.
|
||||||
|
|
||||||
|
## 17. Componentes reutilizados
|
||||||
|
|
||||||
|
- `RdxPageContainer`, `RdxPageStack`, `RdxPageHeader`, `RdxPageActions`;
|
||||||
|
- `RdxEditorPageLayout`, `RdxEditorSection`, `RdxFormGrid`;
|
||||||
|
- `DataTable`, `SkeletonDataTable`, `DataTableColumnHeader`, `RdxRowActions`;
|
||||||
|
- `TextField`, `SelectField`, `PageFormHeader`, `FormActionsBar`;
|
||||||
|
- `SimpleSearchInput`, `PageErrorState`, `ErrorAlert`;
|
||||||
|
- `UnsavedChangesProvider` y `useReturnToNavigation`.
|
||||||
|
|
||||||
|
## 18. Componentes Supplier específicos
|
||||||
|
|
||||||
|
- `SuppliersGrid` y sus columnas, porque expresan identidad y acciones Supplier;
|
||||||
|
- `SupplierStatusBadge`, limitado al status del dominio;
|
||||||
|
- `SupplierEditorForm` y sus secciones, porque el DTO plano y la ausencia de fiscalidad son específicos;
|
||||||
|
- páginas y controllers de List, Detail, Create y Update.
|
||||||
|
|
||||||
|
No se extraen componentes desde Customer: la similitud visual no justifica acoplar ambos dominios.
|
||||||
|
|
||||||
|
## 19. Decisiones descartadas
|
||||||
|
|
||||||
|
Quedan expresamente fuera:
|
||||||
|
|
||||||
|
- DELETE;
|
||||||
|
- activate/deactivate y cualquier lifecycle HTTP;
|
||||||
|
- `paymentTermId`;
|
||||||
|
- `paymentMethodId`;
|
||||||
|
- fiscalidad y defaults fiscales;
|
||||||
|
- integración con `supplier-invoices`;
|
||||||
|
- edición de status;
|
||||||
|
- refactor del DTO plano de dirección;
|
||||||
|
- semántica `null => borrar` no soportada.
|
||||||
|
|
||||||
|
## 20. Plan de implementación
|
||||||
|
|
||||||
|
1. Corregir API PATCH, keys, adapters y estrategia de caché.
|
||||||
|
2. Registrar manifest y rutas Supplier.
|
||||||
|
3. Implementar listado y navegación.
|
||||||
|
4. Implementar detalle.
|
||||||
|
5. Implementar formulario compartido y Create.
|
||||||
|
6. Implementar Update con `dirtyFields`.
|
||||||
|
7. Añadir recursos `es` y `en`.
|
||||||
|
8. Revisar responsive y accesibilidad.
|
||||||
|
9. Añadir tests de mappers, PATCH parcial y rutas.
|
||||||
|
10. Ejecutar tests, typecheck, lint, build dirigido y checklist manual.
|
||||||
|
|
||||||
|
### Checklist manual
|
||||||
|
|
||||||
|
- Abrir listado, buscar por nombre, TIN y referencia.
|
||||||
|
- Ordenar nombre, referencia y email.
|
||||||
|
- Abrir detalle y navegar a editar.
|
||||||
|
- Guardar un único campo y confirmar que el PATCH es parcial.
|
||||||
|
- Guardar `isCompany = false`.
|
||||||
|
- Crear Supplier y validar errores de formulario/API.
|
||||||
|
- Confirmar actualización de detail y list.
|
||||||
|
- Cambiar de empresa y comprobar aislamiento.
|
||||||
|
- Revisar listado, detalle y formularios en desktop, tablet y móvil.
|
||||||
@ -83,6 +83,7 @@ test("resolvePaymentTerm normaliza percentage.value cuando llega numérico del c
|
|||||||
assert.equal(currencyCodeResult.isSuccess, true);
|
assert.equal(currencyCodeResult.isSuccess, true);
|
||||||
assert.equal(discountResult.isSuccess, true);
|
assert.equal(discountResult.isSuccess, true);
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: <Solucion rápida para test, no es necesario refactorizar>
|
||||||
const result = converter["resolvePaymentTerm"]({
|
const result = converter["resolvePaymentTerm"]({
|
||||||
proforma: {
|
proforma: {
|
||||||
companyId,
|
companyId,
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
|
||||||
import type { Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
import type { ISupplierCreateProps, Supplier } from "../../domain";
|
|
||||||
|
|
||||||
export interface ISupplierServicesContext {
|
|
||||||
transaction: unknown;
|
|
||||||
companyId: UniqueID;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISupplierPublicServices {
|
|
||||||
findSupplierByTIN: (
|
|
||||||
tin: TINNumber,
|
|
||||||
context: ISupplierServicesContext
|
|
||||||
) => Promise<Result<Supplier, Error>>;
|
|
||||||
|
|
||||||
createSupplier: (
|
|
||||||
id: UniqueID,
|
|
||||||
props: ISupplierCreateProps,
|
|
||||||
context: ISupplierServicesContext
|
|
||||||
) => Promise<Result<Supplier, Error>>;
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./supplier-snapshot.interface";
|
|
||||||
export * from "./supplier-snapshot-builder";
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
export interface ISupplierFullSnapshot {
|
|
||||||
id: string;
|
|
||||||
company_id: string;
|
|
||||||
status: string;
|
|
||||||
reference: string;
|
|
||||||
|
|
||||||
is_company: string;
|
|
||||||
name: string;
|
|
||||||
trade_name: string;
|
|
||||||
tin: string;
|
|
||||||
|
|
||||||
street: string;
|
|
||||||
street2: string;
|
|
||||||
city: string;
|
|
||||||
province: string;
|
|
||||||
postal_code: string;
|
|
||||||
country: string;
|
|
||||||
|
|
||||||
email_primary: string;
|
|
||||||
email_secondary: string;
|
|
||||||
|
|
||||||
phone_primary: string;
|
|
||||||
phone_secondary: string;
|
|
||||||
|
|
||||||
mobile_primary: string;
|
|
||||||
mobile_secondary: string;
|
|
||||||
|
|
||||||
fax: string;
|
|
||||||
website: string;
|
|
||||||
|
|
||||||
language_code: string;
|
|
||||||
currency_code: string;
|
|
||||||
|
|
||||||
metadata?: Record<string, string>;
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./supplier-summary-snapshot.interface";
|
|
||||||
export * from "./supplier-summary-snapshot-builder";
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
export type ISupplierSummarySnapshot = {
|
|
||||||
id: string;
|
|
||||||
company_id: string;
|
|
||||||
status: string;
|
|
||||||
reference: string;
|
|
||||||
|
|
||||||
is_company: string;
|
|
||||||
name: string;
|
|
||||||
trade_name: string;
|
|
||||||
tin: string;
|
|
||||||
|
|
||||||
street: string;
|
|
||||||
street2: string;
|
|
||||||
city: string;
|
|
||||||
postal_code: string;
|
|
||||||
province: string;
|
|
||||||
country: string;
|
|
||||||
|
|
||||||
email_primary: string;
|
|
||||||
email_secondary: string;
|
|
||||||
|
|
||||||
phone_primary: string;
|
|
||||||
phone_secondary: string;
|
|
||||||
|
|
||||||
mobile_primary: string;
|
|
||||||
mobile_secondary: string;
|
|
||||||
|
|
||||||
fax: string;
|
|
||||||
website: string;
|
|
||||||
|
|
||||||
language_code: string;
|
|
||||||
currency_code: string;
|
|
||||||
|
|
||||||
metadata?: Record<string, string>;
|
|
||||||
};
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export * from './activate-supplier.use-case';
|
|
||||||
export * from './create-supplier.use-case';
|
|
||||||
export * from './deactivate-supplier.use-case';
|
|
||||||
export * from './get-supplier.use-case';
|
|
||||||
export * from './list-suppliers.use-case';
|
|
||||||
export * from './update-supplier.use-case';
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
MODULE_ROOT="./"
|
|
||||||
|
|
||||||
DIRS=(
|
|
||||||
"$MODULE_ROOT"
|
|
||||||
"$MODULE_ROOT/application"
|
|
||||||
"$MODULE_ROOT/application/di"
|
|
||||||
"$MODULE_ROOT/application/dtos"
|
|
||||||
"$MODULE_ROOT/application/mappers"
|
|
||||||
"$MODULE_ROOT/application/repositories"
|
|
||||||
"$MODULE_ROOT/application/services"
|
|
||||||
"$MODULE_ROOT/application/use-cases"
|
|
||||||
"$MODULE_ROOT/domain"
|
|
||||||
"$MODULE_ROOT/domain/aggregates"
|
|
||||||
"$MODULE_ROOT/domain/collections"
|
|
||||||
"$MODULE_ROOT/domain/errors"
|
|
||||||
"$MODULE_ROOT/domain/value-objects"
|
|
||||||
"$MODULE_ROOT/infrastructure"
|
|
||||||
"$MODULE_ROOT/infrastructure/di"
|
|
||||||
"$MODULE_ROOT/infrastructure/express"
|
|
||||||
"$MODULE_ROOT/infrastructure/express/controllers"
|
|
||||||
"$MODULE_ROOT/infrastructure/persistence"
|
|
||||||
"$MODULE_ROOT/infrastructure/persistence/sequelize"
|
|
||||||
"$MODULE_ROOT/infrastructure/persistence/sequelize/mappers"
|
|
||||||
"$MODULE_ROOT/infrastructure/persistence/sequelize/models"
|
|
||||||
"$MODULE_ROOT/infrastructure/persistence/sequelize/repositories"
|
|
||||||
)
|
|
||||||
|
|
||||||
FILES=(
|
|
||||||
"$MODULE_ROOT/application/repositories/supplier-repository.interface.ts"
|
|
||||||
|
|
||||||
"$MODULE_ROOT/application/services/supplier-creator.service.ts"
|
|
||||||
"$MODULE_ROOT/application/services/supplier-updater.service.ts"
|
|
||||||
"$MODULE_ROOT/application/services/supplier-finder.service.ts"
|
|
||||||
|
|
||||||
"$MODULE_ROOT/application/use-cases/create-supplier.use-case.ts"
|
|
||||||
"$MODULE_ROOT/application/use-cases/update-supplier.use-case.ts"
|
|
||||||
"$MODULE_ROOT/application/use-cases/get-supplier.use-case.ts"
|
|
||||||
"$MODULE_ROOT/application/use-cases/list-suppliers.use-case.ts"
|
|
||||||
"$MODULE_ROOT/application/use-cases/activate-supplier.use-case.ts"
|
|
||||||
"$MODULE_ROOT/application/use-cases/deactivate-supplier.use-case.ts"
|
|
||||||
|
|
||||||
"$MODULE_ROOT/domain/aggregates/supplier.aggregate.ts"
|
|
||||||
"$MODULE_ROOT/domain/collections/supplier-taxes.collection.ts"
|
|
||||||
"$MODULE_ROOT/domain/errors/supplier-domain.errors.ts"
|
|
||||||
"$MODULE_ROOT/domain/value-objects/supplier-status.value-object.ts"
|
|
||||||
|
|
||||||
"$MODULE_ROOT/infrastructure/express/supplier.routes.ts"
|
|
||||||
"$MODULE_ROOT/infrastructure/persistence/sequelize/models/supplier-sequelize.model.ts"
|
|
||||||
"$MODULE_ROOT/infrastructure/persistence/sequelize/mappers/sequelize-supplier-domain.mapper.ts"
|
|
||||||
"$MODULE_ROOT/infrastructure/persistence/sequelize/mappers/supplier-summary.mapper.ts"
|
|
||||||
"$MODULE_ROOT/infrastructure/persistence/sequelize/repositories/sequelize-supplier.repository.ts"
|
|
||||||
)
|
|
||||||
|
|
||||||
mkdir -p "${DIRS[@]}"
|
|
||||||
touch "${FILES[@]}"
|
|
||||||
|
|
||||||
create_index() {
|
|
||||||
local dir="$1"
|
|
||||||
local index_file="$dir/index.ts"
|
|
||||||
|
|
||||||
: > "$index_file"
|
|
||||||
|
|
||||||
mapfile -t ts_files < <(
|
|
||||||
find "$dir" -maxdepth 1 -type f -name "*.ts" ! -name "index.ts" \
|
|
||||||
| sort
|
|
||||||
)
|
|
||||||
|
|
||||||
mapfile -t subdirs < <(
|
|
||||||
find "$dir" -maxdepth 1 -mindepth 1 -type d \
|
|
||||||
| sort
|
|
||||||
)
|
|
||||||
|
|
||||||
for file in "${ts_files[@]}"; do
|
|
||||||
local base
|
|
||||||
base="$(basename "$file" .ts)"
|
|
||||||
echo "export * from './$base';" >> "$index_file"
|
|
||||||
done
|
|
||||||
|
|
||||||
for subdir in "${subdirs[@]}"; do
|
|
||||||
local name
|
|
||||||
name="$(basename "$subdir")"
|
|
||||||
echo "export * from './$name';" >> "$index_file"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
for dir in "${DIRS[@]}"; do
|
|
||||||
create_index "$dir"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Módulo supplier creado correctamente en $MODULE_ROOT"
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./supplier-public-services";
|
|
||||||
export * from "./suppliers.di";
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
import { type SetupParams, buildCatalogs } from "@erp/core/api";
|
|
||||||
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
import {
|
|
||||||
type ISupplierPublicServices,
|
|
||||||
type ISupplierServicesContext,
|
|
||||||
buildSupplierCreator,
|
|
||||||
buildSupplierFinder,
|
|
||||||
} from "../../application";
|
|
||||||
import type { ISupplierCreateProps } from "../../domain";
|
|
||||||
|
|
||||||
import { buildSupplierPersistenceMappers } from "./supplier-persistence-mappers.di";
|
|
||||||
import { buildSupplierRepository } from "./supplier-repositories.di";
|
|
||||||
import type { SuppliersInternalDeps } from "./suppliers.di";
|
|
||||||
|
|
||||||
export function buildSupplierPublicServices(
|
|
||||||
params: SetupParams,
|
|
||||||
deps: SuppliersInternalDeps
|
|
||||||
): ISupplierPublicServices {
|
|
||||||
const { database } = params;
|
|
||||||
const catalogs = buildCatalogs();
|
|
||||||
|
|
||||||
// Infrastructure
|
|
||||||
const persistenceMappers = buildSupplierPersistenceMappers(catalogs);
|
|
||||||
const repository = buildSupplierRepository({ database, mappers: persistenceMappers });
|
|
||||||
|
|
||||||
const finder = buildSupplierFinder({ repository });
|
|
||||||
const creator = buildSupplierCreator({ repository });
|
|
||||||
|
|
||||||
return {
|
|
||||||
findSupplierByTIN: async (tin: TINNumber, context: ISupplierServicesContext) => {
|
|
||||||
const { companyId, transaction } = context;
|
|
||||||
|
|
||||||
const supplierResult = await finder.findSupplierByTIN(companyId, tin, transaction);
|
|
||||||
|
|
||||||
if (supplierResult.isFailure) {
|
|
||||||
return Result.fail(supplierResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(supplierResult.data);
|
|
||||||
},
|
|
||||||
|
|
||||||
createSupplier: async (
|
|
||||||
id: UniqueID,
|
|
||||||
props: ISupplierCreateProps,
|
|
||||||
context: ISupplierServicesContext
|
|
||||||
) => {
|
|
||||||
const { companyId, transaction } = context;
|
|
||||||
|
|
||||||
const supplierResult = await creator.create({
|
|
||||||
companyId,
|
|
||||||
id,
|
|
||||||
props,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (supplierResult.isFailure) {
|
|
||||||
return Result.fail(supplierResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(supplierResult.data);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import {
|
|
||||||
ExpressController,
|
|
||||||
forbidQueryFieldGuard,
|
|
||||||
requireAuthenticatedGuard,
|
|
||||||
requireCompanyContextGuard,
|
|
||||||
} from "@erp/core/api";
|
|
||||||
|
|
||||||
import type { DeleteSupplierUseCase } from "../../../application";
|
|
||||||
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
|
||||||
|
|
||||||
export class DeleteSupplierController extends ExpressController {
|
|
||||||
public constructor(private readonly useCase: DeleteSupplierUseCase) {
|
|
||||||
super();
|
|
||||||
this.errorMapper = suppliersApiErrorMapper;
|
|
||||||
|
|
||||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
|
||||||
this.registerGuards(
|
|
||||||
requireAuthenticatedGuard(),
|
|
||||||
requireCompanyContextGuard(),
|
|
||||||
forbidQueryFieldGuard("companyId")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async executeImpl(): Promise<any> {
|
|
||||||
const companyId = this.getTenantId();
|
|
||||||
if (!companyId) {
|
|
||||||
return this.forbiddenError("Tenant ID not found");
|
|
||||||
}
|
|
||||||
const { supplier_id } = this.req.params;
|
|
||||||
|
|
||||||
const result = await this.useCase.execute({ supplier_id, companyId });
|
|
||||||
|
|
||||||
return result.match(
|
|
||||||
(data) => this.ok(data),
|
|
||||||
(err) => this.handleError(err)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { z } from "zod/v4";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Este DTO es utilizado por el endpoint:
|
|
||||||
* `DELETE /suppliers/:id` (eliminar una factura por ID).
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const DeleteSupplierByIdRequestSchema = z.object({
|
|
||||||
supplier_id: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type DeleteSupplierByIdRequestDTO = z.infer<typeof DeleteSupplierByIdRequestSchema>;
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import type { IDataSource } from "@erp/core/client";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Elimina un proveedor existente en el sistema utilizando la fuente de datos proporcionada.
|
|
||||||
*
|
|
||||||
* @param dataSource - La fuente de datos para interactuar con la API.
|
|
||||||
* @param params - Los parámetros necesarios para eliminar el proveedor.
|
|
||||||
* @returns Una promesa que se resuelve cuando el proveedor ha sido eliminado exitosamente.
|
|
||||||
* @throws Error si el ID del proveedor no es proporcionado o si la eliminación falla.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface DeleteSupplierByIdParams {
|
|
||||||
id: string;
|
|
||||||
signal?: AbortSignal;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DeleteSupplierByIdResult = void;
|
|
||||||
|
|
||||||
export function deleteSupplierById(
|
|
||||||
dataSource: IDataSource,
|
|
||||||
params: DeleteSupplierByIdParams
|
|
||||||
): Promise<DeleteSupplierByIdResult> {
|
|
||||||
const { id, signal } = params;
|
|
||||||
|
|
||||||
if (!id) throw new Error("supplierId is required");
|
|
||||||
|
|
||||||
return dataSource.deleteOne<DeleteSupplierByIdResult>("suppliers", id, {
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
|
||||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import { type DeleteSupplierByIdParams, deleteSupplierById } from "../api";
|
|
||||||
|
|
||||||
import { SUPPLIER_DELETE_KEY } from "./keys";
|
|
||||||
import {
|
|
||||||
type DeleteSupplierCacheContext,
|
|
||||||
invalidateSupplierListQueries,
|
|
||||||
prepareDeleteSupplierOptimisticUpdate,
|
|
||||||
rollbackDeleteSupplierOptimisticUpdate,
|
|
||||||
} from "./supplier-cache-strategy";
|
|
||||||
|
|
||||||
interface DeleteSupplierContext extends DeleteSupplierCacheContext {}
|
|
||||||
|
|
||||||
export const useSupplierDeleteMutation = () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const dataSource = useDataSource();
|
|
||||||
|
|
||||||
return useMutation<{ id: string }, DefaultError, DeleteSupplierByIdParams, DeleteSupplierContext>(
|
|
||||||
{
|
|
||||||
mutationKey: SUPPLIER_DELETE_KEY,
|
|
||||||
mutationFn: async ({ id, signal }) => {
|
|
||||||
if (!id) {
|
|
||||||
throw new Error("supplierId is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
await deleteSupplierById(dataSource, { id, signal });
|
|
||||||
return { id };
|
|
||||||
},
|
|
||||||
|
|
||||||
onMutate: async ({ id }) => {
|
|
||||||
return prepareDeleteSupplierOptimisticUpdate(queryClient, id);
|
|
||||||
},
|
|
||||||
|
|
||||||
onError: (_error, _variables, context) => {
|
|
||||||
rollbackDeleteSupplierOptimisticUpdate(queryClient, context);
|
|
||||||
},
|
|
||||||
|
|
||||||
onSettled: async () => {
|
|
||||||
await invalidateSupplierListQueries(queryClient);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import type { ICatalogs } from "@erp/core/api";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CreateSupplierInputMapper,
|
CreateSupplierInputMapper,
|
||||||
type ICreateSupplierInputMapper,
|
type ICreateSupplierInputMapper,
|
||||||
@ -12,11 +10,9 @@ export interface ISupplierInputMappers {
|
|||||||
updateInputMapper: IUpdateSupplierInputMapper;
|
updateInputMapper: IUpdateSupplierInputMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildSupplierInputMappers = (catalogs: ICatalogs): ISupplierInputMappers => {
|
export const buildSupplierInputMappers = (): ISupplierInputMappers => {
|
||||||
const { taxCatalog } = catalogs;
|
|
||||||
|
|
||||||
// Mappers el DTO a las props validadas (SupplierProps) y luego construir agregado
|
// Mappers el DTO a las props validadas (SupplierProps) y luego construir agregado
|
||||||
const createInputMapper = new CreateSupplierInputMapper({ taxCatalog });
|
const createInputMapper = new CreateSupplierInputMapper();
|
||||||
const updateInputMapper = new UpdateSupplierInputMapper();
|
const updateInputMapper = new UpdateSupplierInputMapper();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -48,6 +48,7 @@ export function buildCreateSupplierUseCase(deps: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildUpdateSupplierUseCase(deps: {
|
export function buildUpdateSupplierUseCase(deps: {
|
||||||
|
finder: ISupplierFinder;
|
||||||
updater: ISupplierUpdater;
|
updater: ISupplierUpdater;
|
||||||
dtoMapper: IUpdateSupplierInputMapper;
|
dtoMapper: IUpdateSupplierInputMapper;
|
||||||
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||||
@ -55,41 +56,9 @@ export function buildUpdateSupplierUseCase(deps: {
|
|||||||
}) {
|
}) {
|
||||||
return new UpdateSupplierUseCase({
|
return new UpdateSupplierUseCase({
|
||||||
dtoMapper: deps.dtoMapper,
|
dtoMapper: deps.dtoMapper,
|
||||||
|
finder: deps.finder,
|
||||||
updater: deps.updater,
|
updater: deps.updater,
|
||||||
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||||
transactionManager: deps.transactionManager,
|
transactionManager: deps.transactionManager,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*export function buildReportSupplierUseCase(deps: {
|
|
||||||
finder: ISupplierFinder;
|
|
||||||
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
|
||||||
reportSnapshotBuilder: ISupplierReportSnapshotBuilder;
|
|
||||||
documentService: SupplierDocumentGeneratorService;
|
|
||||||
transactionManager: ITransactionManager;
|
|
||||||
}) {
|
|
||||||
return new ReportSupplierUseCase(
|
|
||||||
deps.finder,
|
|
||||||
deps.fullSnapshotBuilder,
|
|
||||||
deps.reportSnapshotBuilder,
|
|
||||||
deps.documentService,
|
|
||||||
deps.transactionManager
|
|
||||||
);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
export function buildDeleteSupplierUseCase(deps: { finder: ISupplierFinder }) {
|
|
||||||
return new DeleteSupplierUseCase(deps.finder);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildIssueSupplierUseCase(deps: { finder: ISupplierFinder }) {
|
|
||||||
return new IssueSupplierUseCase(deps.finder);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildChangeStatusSupplierUseCase(deps: {
|
|
||||||
finder: ISupplierFinder;
|
|
||||||
transactionManager: ITransactionManager;
|
|
||||||
}) {
|
|
||||||
return new ChangeStatusSupplierUseCase(deps.finder, deps.transactionManager);
|
|
||||||
}*/
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import type { JsonTaxCatalogProvider } from "@erp/core";
|
|
||||||
import {
|
import {
|
||||||
City,
|
City,
|
||||||
Country,
|
Country,
|
||||||
@ -22,148 +21,142 @@ import {
|
|||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { CreateSupplierRequestDTO } from "../../../common";
|
|
||||||
import { type ISupplierCreateProps, SupplierStatus } from "../../domain";
|
import { type ISupplierCreateProps, SupplierStatus } from "../../domain";
|
||||||
|
import type { CreateSupplierCommand } from "../use-cases/create-supplier.command";
|
||||||
|
|
||||||
export interface ICreateSupplierInputMapper {
|
export interface ICreateSupplierInputMapper {
|
||||||
map(
|
map(
|
||||||
dto: CreateSupplierRequestDTO,
|
command: CreateSupplierCommand,
|
||||||
params: { companyId: UniqueID }
|
params: { companyId: UniqueID }
|
||||||
): Result<{ id: UniqueID; props: ISupplierCreateProps }>;
|
): Result<{ id: UniqueID; props: ISupplierCreateProps }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateSupplierInputMapper implements ICreateSupplierInputMapper {
|
export class CreateSupplierInputMapper implements ICreateSupplierInputMapper {
|
||||||
private readonly taxCatalog: JsonTaxCatalogProvider;
|
|
||||||
|
|
||||||
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
|
|
||||||
this.taxCatalog = params.taxCatalog;
|
|
||||||
}
|
|
||||||
|
|
||||||
public map(
|
public map(
|
||||||
dto: CreateSupplierRequestDTO,
|
command: CreateSupplierCommand,
|
||||||
params: { companyId: UniqueID }
|
params: { companyId: UniqueID }
|
||||||
): Result<{ id: UniqueID; props: ISupplierCreateProps }> {
|
): Result<{ id: UniqueID; props: ISupplierCreateProps }> {
|
||||||
try {
|
try {
|
||||||
const errors: ValidationErrorDetail[] = [];
|
const errors: ValidationErrorDetail[] = [];
|
||||||
const { companyId } = params;
|
const { companyId } = params;
|
||||||
|
|
||||||
const supplierId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
|
const supplierId = extractOrPushError(UniqueID.create(command.supplierId), "id", errors);
|
||||||
|
|
||||||
const status = SupplierStatus.active();
|
const status = SupplierStatus.active();
|
||||||
const isCompany = dto.is_company === "true";
|
const isCompany = command.isCompany;
|
||||||
|
|
||||||
const reference = extractOrPushError(
|
const reference = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.reference, (value) => Name.create(value)),
|
maybeFromNullableResult(command.reference, (value) => Name.create(value)),
|
||||||
"reference",
|
"reference",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const name = extractOrPushError(Name.create(dto.name), "name", errors);
|
const name = extractOrPushError(Name.create(command.name), "name", errors);
|
||||||
|
|
||||||
const tradeName = extractOrPushError(
|
const tradeName = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.trade_name, (value) => Name.create(value)),
|
maybeFromNullableResult(command.tradeName, (value) => Name.create(value)),
|
||||||
"trade_name",
|
"trade_name",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const tinNumber = extractOrPushError(
|
const tinNumber = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.tin, (value) => TINNumber.create(value)),
|
maybeFromNullableResult(command.tin, (value) => TINNumber.create(value)),
|
||||||
"tin",
|
"tin",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const street = extractOrPushError(
|
const street = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.street, (value) => Street.create(value)),
|
maybeFromNullableResult(command.street, (value) => Street.create(value)),
|
||||||
"street",
|
"street",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const street2 = extractOrPushError(
|
const street2 = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.street2, (value) => Street.create(value)),
|
maybeFromNullableResult(command.street2, (value) => Street.create(value)),
|
||||||
"street2",
|
"street2",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const city = extractOrPushError(
|
const city = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.city, (value) => City.create(value)),
|
maybeFromNullableResult(command.city, (value) => City.create(value)),
|
||||||
"city",
|
"city",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const province = extractOrPushError(
|
const province = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.province, (value) => Province.create(value)),
|
maybeFromNullableResult(command.province, (value) => Province.create(value)),
|
||||||
"province",
|
"province",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const postalCode = extractOrPushError(
|
const postalCode = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.postal_code, (value) => PostalCode.create(value)),
|
maybeFromNullableResult(command.postalCode, (value) => PostalCode.create(value)),
|
||||||
"postal_code",
|
"postal_code",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const country = extractOrPushError(
|
const country = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.country, (value) => Country.create(value)),
|
maybeFromNullableResult(command.country, (value) => Country.create(value)),
|
||||||
"country",
|
"country",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const primaryEmailAddress = extractOrPushError(
|
const primaryEmailAddress = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.email_primary, (value) => EmailAddress.create(value)),
|
maybeFromNullableResult(command.emailPrimary, (value) => EmailAddress.create(value)),
|
||||||
"email_primary",
|
"email_primary",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const secondaryEmailAddress = extractOrPushError(
|
const secondaryEmailAddress = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.email_secondary, (value) => EmailAddress.create(value)),
|
maybeFromNullableResult(command.emailSecondary, (value) => EmailAddress.create(value)),
|
||||||
"email_secondary",
|
"email_secondary",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const primaryPhoneNumber = extractOrPushError(
|
const primaryPhoneNumber = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.phone_primary, (value) => PhoneNumber.create(value)),
|
maybeFromNullableResult(command.phonePrimary, (value) => PhoneNumber.create(value)),
|
||||||
"phone_primary",
|
"phone_primary",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const secondaryPhoneNumber = extractOrPushError(
|
const secondaryPhoneNumber = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.phone_secondary, (value) => PhoneNumber.create(value)),
|
maybeFromNullableResult(command.phoneSecondary, (value) => PhoneNumber.create(value)),
|
||||||
"phone_secondary",
|
"phone_secondary",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const primaryMobileNumber = extractOrPushError(
|
const primaryMobileNumber = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.mobile_primary, (value) => PhoneNumber.create(value)),
|
maybeFromNullableResult(command.mobilePrimary, (value) => PhoneNumber.create(value)),
|
||||||
"mobile_primary",
|
"mobile_primary",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const secondaryMobileNumber = extractOrPushError(
|
const secondaryMobileNumber = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.mobile_secondary, (value) => PhoneNumber.create(value)),
|
maybeFromNullableResult(command.mobileSecondary, (value) => PhoneNumber.create(value)),
|
||||||
"mobile_secondary",
|
"mobile_secondary",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const faxNumber = extractOrPushError(
|
const faxNumber = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.fax, (value) => PhoneNumber.create(value)),
|
maybeFromNullableResult(command.fax, (value) => PhoneNumber.create(value)),
|
||||||
"fax",
|
"fax",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const website = extractOrPushError(
|
const website = extractOrPushError(
|
||||||
maybeFromNullableResult(dto.website, (value) => URLAddress.create(value)),
|
maybeFromNullableResult(command.website, (value) => URLAddress.create(value)),
|
||||||
"website",
|
"website",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const languageCode = extractOrPushError(
|
const languageCode = extractOrPushError(
|
||||||
LanguageCode.create(dto.language_code),
|
LanguageCode.create(command.languageCode),
|
||||||
"language_code",
|
"language_code",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const currencyCode = extractOrPushError(
|
const currencyCode = extractOrPushError(
|
||||||
CurrencyCode.create(dto.currency_code),
|
CurrencyCode.create(command.currencyCode),
|
||||||
"currency_code",
|
"currency_code",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
|
import { UpdateSupplierInputMapper } from "./update-supplier-input.mapper";
|
||||||
|
|
||||||
|
test("mobile_secondary se mapea sin modificar mobilePrimary", () => {
|
||||||
|
const mapper = new UpdateSupplierInputMapper();
|
||||||
|
|
||||||
|
const result = mapper.map(
|
||||||
|
{ mobileSecondary: "+34600111222" },
|
||||||
|
{ companyId: UniqueID.generateNewID() }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.isSuccess, true);
|
||||||
|
assert.equal(result.data.mobilePrimary, undefined);
|
||||||
|
assert.equal(result.data.mobileSecondary?.unwrap().toPrimitive(), "+34600111222");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("is_company false se conserva como boolean", () => {
|
||||||
|
const mapper = new UpdateSupplierInputMapper();
|
||||||
|
|
||||||
|
const result = mapper.map(
|
||||||
|
{ isCompany: false },
|
||||||
|
{ companyId: UniqueID.generateNewID() }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.isSuccess, true);
|
||||||
|
assert.equal(result.data.isCompany, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("is_company true se conserva como boolean", () => {
|
||||||
|
const mapper = new UpdateSupplierInputMapper();
|
||||||
|
|
||||||
|
const result = mapper.map(
|
||||||
|
{ isCompany: true },
|
||||||
|
{ companyId: UniqueID.generateNewID() }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.isSuccess, true);
|
||||||
|
assert.equal(result.data.isCompany, true);
|
||||||
|
});
|
||||||
@ -21,17 +21,15 @@ import {
|
|||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { UpdateSupplierByIdRequestDTO } from "../../../common";
|
|
||||||
import type { SupplierPatchProps } from "../../domain";
|
import type { SupplierPatchProps } from "../../domain";
|
||||||
|
import type { UpdateSupplierPatchInput } from "../use-cases/update-supplier.command";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdateSupplierInputMapper
|
* UpdateSupplierInputMapper
|
||||||
* Convierte el DTO a las props validadas (SupplierProps).
|
* Convierte el DTO a las props validadas (SupplierProps).
|
||||||
* No construye directamente el agregado.
|
* No construye directamente el agregado.
|
||||||
* Tri-estado:
|
* Los campos omitidos no se modifican. El DTO HTTP actual todavía no expresa
|
||||||
* - campo omitido → no se cambia
|
* borrado explícito mediante null; esa semántica se incorporará al modernizar el contrato.
|
||||||
* - campo con valor null/"" → se quita el valor -> set(None()),
|
|
||||||
* - campo con valor no-vacío → se pone el nuevo valor -> set(Some(VO)).
|
|
||||||
*
|
*
|
||||||
* @param dto - DTO con los datos a cambiar en el cliente
|
* @param dto - DTO con los datos a cambiar en el cliente
|
||||||
* @returns Cambios en las propiedades del cliente
|
* @returns Cambios en las propiedades del cliente
|
||||||
@ -40,13 +38,13 @@ import type { SupplierPatchProps } from "../../domain";
|
|||||||
|
|
||||||
export interface IUpdateSupplierInputMapper {
|
export interface IUpdateSupplierInputMapper {
|
||||||
map(
|
map(
|
||||||
dto: UpdateSupplierByIdRequestDTO,
|
dto: UpdateSupplierPatchInput,
|
||||||
params: { companyId: UniqueID }
|
params: { companyId: UniqueID }
|
||||||
): Result<SupplierPatchProps>;
|
): Result<SupplierPatchProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
||||||
public map(dto: UpdateSupplierByIdRequestDTO, params: { companyId: UniqueID }) {
|
public map(dto: UpdateSupplierPatchInput, _params: { companyId: UniqueID }) {
|
||||||
try {
|
try {
|
||||||
const errors: ValidationErrorDetail[] = [];
|
const errors: ValidationErrorDetail[] = [];
|
||||||
const supplierPatchProps: SupplierPatchProps = {};
|
const supplierPatchProps: SupplierPatchProps = {};
|
||||||
@ -59,16 +57,8 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.is_company).ifSet((is_company) => {
|
toPatchField(dto.isCompany).ifSet((isCompany) => {
|
||||||
if (isNullishOrEmpty(is_company)) {
|
supplierPatchProps.isCompany = isCompany;
|
||||||
errors.push({ path: "is_company", message: "is_company cannot be empty" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
supplierPatchProps.isCompany = extractOrPushError(
|
|
||||||
Result.ok(Boolean(is_company!)),
|
|
||||||
"is_company",
|
|
||||||
errors
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.name).ifSet((name) => {
|
toPatchField(dto.name).ifSet((name) => {
|
||||||
@ -79,9 +69,9 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
|||||||
supplierPatchProps.name = extractOrPushError(Name.create(name!), "name", errors);
|
supplierPatchProps.name = extractOrPushError(Name.create(name!), "name", errors);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.trade_name).ifSet((trade_name) => {
|
toPatchField(dto.tradeName).ifSet((tradeName) => {
|
||||||
supplierPatchProps.tradeName = extractOrPushError(
|
supplierPatchProps.tradeName = extractOrPushError(
|
||||||
maybeFromNullableResult(trade_name, (value) => Name.create(value)),
|
maybeFromNullableResult(tradeName, (value) => Name.create(value)),
|
||||||
"trade_name",
|
"trade_name",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
@ -95,49 +85,49 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.email_primary).ifSet((email_primary) => {
|
toPatchField(dto.emailPrimary).ifSet((emailPrimary) => {
|
||||||
supplierPatchProps.emailPrimary = extractOrPushError(
|
supplierPatchProps.emailPrimary = extractOrPushError(
|
||||||
maybeFromNullableResult(email_primary, (value) => EmailAddress.create(value)),
|
maybeFromNullableResult(emailPrimary, (value) => EmailAddress.create(value)),
|
||||||
"email_primary",
|
"email_primary",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.email_secondary).ifSet((email_secondary) => {
|
toPatchField(dto.emailSecondary).ifSet((emailSecondary) => {
|
||||||
supplierPatchProps.emailSecondary = extractOrPushError(
|
supplierPatchProps.emailSecondary = extractOrPushError(
|
||||||
maybeFromNullableResult(email_secondary, (value) => EmailAddress.create(value)),
|
maybeFromNullableResult(emailSecondary, (value) => EmailAddress.create(value)),
|
||||||
"email_secondary",
|
"email_secondary",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.mobile_primary).ifSet((mobile_primary) => {
|
toPatchField(dto.mobilePrimary).ifSet((mobilePrimary) => {
|
||||||
supplierPatchProps.mobilePrimary = extractOrPushError(
|
supplierPatchProps.mobilePrimary = extractOrPushError(
|
||||||
maybeFromNullableResult(mobile_primary, (value) => PhoneNumber.create(value)),
|
maybeFromNullableResult(mobilePrimary, (value) => PhoneNumber.create(value)),
|
||||||
"mobile_primary",
|
"mobile_primary",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.mobile_secondary).ifSet((mobile_secondary) => {
|
toPatchField(dto.mobileSecondary).ifSet((mobileSecondary) => {
|
||||||
supplierPatchProps.mobilePrimary = extractOrPushError(
|
supplierPatchProps.mobileSecondary = extractOrPushError(
|
||||||
maybeFromNullableResult(mobile_secondary, (value) => PhoneNumber.create(value)),
|
maybeFromNullableResult(mobileSecondary, (value) => PhoneNumber.create(value)),
|
||||||
"mobile_secondary",
|
"mobile_secondary",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.phone_primary).ifSet((phone_primary) => {
|
toPatchField(dto.phonePrimary).ifSet((phonePrimary) => {
|
||||||
supplierPatchProps.phonePrimary = extractOrPushError(
|
supplierPatchProps.phonePrimary = extractOrPushError(
|
||||||
maybeFromNullableResult(phone_primary, (value) => PhoneNumber.create(value)),
|
maybeFromNullableResult(phonePrimary, (value) => PhoneNumber.create(value)),
|
||||||
"phone_primary",
|
"phone_primary",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.phone_secondary).ifSet((phone_secondary) => {
|
toPatchField(dto.phoneSecondary).ifSet((phoneSecondary) => {
|
||||||
supplierPatchProps.phoneSecondary = extractOrPushError(
|
supplierPatchProps.phoneSecondary = extractOrPushError(
|
||||||
maybeFromNullableResult(phone_secondary, (value) => PhoneNumber.create(value)),
|
maybeFromNullableResult(phoneSecondary, (value) => PhoneNumber.create(value)),
|
||||||
"phone_secondary",
|
"phone_secondary",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
@ -159,7 +149,7 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.language_code).ifSet((languageCode) => {
|
toPatchField(dto.languageCode).ifSet((languageCode) => {
|
||||||
if (isNullishOrEmpty(languageCode)) {
|
if (isNullishOrEmpty(languageCode)) {
|
||||||
errors.push({ path: "language_code", message: "Language code cannot be empty" });
|
errors.push({ path: "language_code", message: "Language code cannot be empty" });
|
||||||
return;
|
return;
|
||||||
@ -172,7 +162,7 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.currency_code).ifSet((currencyCode) => {
|
toPatchField(dto.currencyCode).ifSet((currencyCode) => {
|
||||||
if (isNullishOrEmpty(currencyCode)) {
|
if (isNullishOrEmpty(currencyCode)) {
|
||||||
errors.push({ path: "currency_code", message: "Currency code cannot be empty" });
|
errors.push({ path: "currency_code", message: "Currency code cannot be empty" });
|
||||||
return;
|
return;
|
||||||
@ -204,7 +194,7 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public mapPostalAddress(
|
public mapPostalAddress(
|
||||||
dto: UpdateSupplierByIdRequestDTO,
|
dto: UpdateSupplierPatchInput,
|
||||||
errors: ValidationErrorDetail[]
|
errors: ValidationErrorDetail[]
|
||||||
): PostalAddressPatchProps | undefined {
|
): PostalAddressPatchProps | undefined {
|
||||||
const postalAddressPatchProps: PostalAddressPatchProps = {};
|
const postalAddressPatchProps: PostalAddressPatchProps = {};
|
||||||
@ -241,7 +231,7 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
toPatchField(dto.postal_code).ifSet((postalCode) => {
|
toPatchField(dto.postalCode).ifSet((postalCode) => {
|
||||||
postalAddressPatchProps.postalCode = extractOrPushError(
|
postalAddressPatchProps.postalCode = extractOrPushError(
|
||||||
maybeFromNullableResult(postalCode, (value) => PostalCode.create(value)),
|
maybeFromNullableResult(postalCode, (value) => PostalCode.create(value)),
|
||||||
"postal_code",
|
"postal_code",
|
||||||
@ -69,14 +69,4 @@ export interface ISupplierRepository {
|
|||||||
transaction: unknown
|
transaction: unknown
|
||||||
): Promise<Result<Collection<SupplierSummary>, Error>>;
|
): Promise<Result<Collection<SupplierSummary>, Error>>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Elimina un Supplier por su ID, dentro de una empresa.
|
|
||||||
* Retorna `void` si se elimina correctamente, o `NotFoundError` si no existía.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
deleteByIdInCompany(
|
|
||||||
companyId: UniqueID,
|
|
||||||
id: UniqueID,
|
|
||||||
transaction: unknown
|
|
||||||
): Promise<Result<boolean, Error>>;
|
|
||||||
}
|
}
|
||||||
@ -1,4 +1,3 @@
|
|||||||
export * from "./supplier-creator.service";
|
export * from "./supplier-creator.service";
|
||||||
export * from "./supplier-finder.service";
|
export * from "./supplier-finder.service";
|
||||||
export * from "./supplier-public-services.interface";
|
|
||||||
export * from "./supplier-updater.service";
|
export * from "./supplier-updater.service";
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { Supplier, SupplierPatchProps } from "../../domain";
|
import type { Supplier, SupplierPatchProps } from "../../domain";
|
||||||
@ -6,8 +5,7 @@ import type { ISupplierRepository } from "../repositories";
|
|||||||
|
|
||||||
export interface ISupplierUpdater {
|
export interface ISupplierUpdater {
|
||||||
update(params: {
|
update(params: {
|
||||||
companyId: UniqueID;
|
supplier: Supplier;
|
||||||
id: UniqueID;
|
|
||||||
props: SupplierPatchProps;
|
props: SupplierPatchProps;
|
||||||
transaction: unknown;
|
transaction: unknown;
|
||||||
}): Promise<Result<Supplier, Error>>;
|
}): Promise<Result<Supplier, Error>>;
|
||||||
@ -25,21 +23,11 @@ export class SupplierUpdater implements ISupplierUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(params: {
|
async update(params: {
|
||||||
companyId: UniqueID;
|
supplier: Supplier;
|
||||||
id: UniqueID;
|
|
||||||
props: SupplierPatchProps;
|
props: SupplierPatchProps;
|
||||||
transaction: unknown;
|
transaction: unknown;
|
||||||
}): Promise<Result<Supplier, Error>> {
|
}): Promise<Result<Supplier, Error>> {
|
||||||
const { companyId, id, props, transaction } = params;
|
const { supplier, props, transaction } = params;
|
||||||
|
|
||||||
// Recuperar agregado existente
|
|
||||||
const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction);
|
|
||||||
|
|
||||||
if (existingResult.isFailure) {
|
|
||||||
return Result.fail(existingResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const supplier = existingResult.data;
|
|
||||||
|
|
||||||
// Aplicar cambios en el agregado
|
// Aplicar cambios en el agregado
|
||||||
const updateResult = supplier.update(props);
|
const updateResult = supplier.update(props);
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./supplier-snapshot-builder";
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { Maybe } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import { GetSupplierByIdResponseSchema } from "../../../../common";
|
||||||
|
import { SupplierFullSnapshotBuilder } from "./supplier-snapshot-builder";
|
||||||
|
|
||||||
|
test("el snapshot de detalle satisface el contrato de respuesta", () => {
|
||||||
|
const builder = new SupplierFullSnapshotBuilder();
|
||||||
|
|
||||||
|
const snapshot = builder.toOutput({
|
||||||
|
id: { toPrimitive: () => "11111111-1111-4111-8111-111111111111" },
|
||||||
|
companyId: { toPrimitive: () => "22222222-2222-4222-8222-222222222222" },
|
||||||
|
isActive: true,
|
||||||
|
reference: Maybe.none(),
|
||||||
|
isCompany: true,
|
||||||
|
name: { toPrimitive: () => "Proveedor de prueba" },
|
||||||
|
tradeName: Maybe.none(),
|
||||||
|
tin: Maybe.none(),
|
||||||
|
address: {
|
||||||
|
toPrimitive: () => ({
|
||||||
|
street: Maybe.none(),
|
||||||
|
street2: Maybe.none(),
|
||||||
|
city: Maybe.none(),
|
||||||
|
province: Maybe.none(),
|
||||||
|
postalCode: Maybe.none(),
|
||||||
|
country: Maybe.none(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
emailPrimary: Maybe.none(),
|
||||||
|
emailSecondary: Maybe.none(),
|
||||||
|
phonePrimary: Maybe.none(),
|
||||||
|
phoneSecondary: Maybe.none(),
|
||||||
|
mobilePrimary: Maybe.none(),
|
||||||
|
mobileSecondary: Maybe.none(),
|
||||||
|
fax: Maybe.none(),
|
||||||
|
website: Maybe.none(),
|
||||||
|
languageCode: { toPrimitive: () => "es" },
|
||||||
|
currencyCode: { toPrimitive: () => "EUR" },
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
const result = GetSupplierByIdResponseSchema.safeParse(snapshot);
|
||||||
|
|
||||||
|
assert.equal(result.success, true);
|
||||||
|
});
|
||||||
@ -2,14 +2,13 @@ import type { ISnapshotBuilder } from "@erp/core/api";
|
|||||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
import type { Supplier } from "../../../domain";
|
import type { Supplier } from "../../../domain";
|
||||||
|
import type { SupplierDetailDTO } from "../../../../common";
|
||||||
import type { ISupplierFullSnapshot } from "./supplier-snapshot.interface";
|
|
||||||
|
|
||||||
export interface ISupplierFullSnapshotBuilder
|
export interface ISupplierFullSnapshotBuilder
|
||||||
extends ISnapshotBuilder<Supplier, ISupplierFullSnapshot> {}
|
extends ISnapshotBuilder<Supplier, SupplierDetailDTO> {}
|
||||||
|
|
||||||
export class SupplierFullSnapshotBuilder implements ISupplierFullSnapshotBuilder {
|
export class SupplierFullSnapshotBuilder implements ISupplierFullSnapshotBuilder {
|
||||||
toOutput(supplier: Supplier): ISupplierFullSnapshot {
|
toOutput(supplier: Supplier): SupplierDetailDTO {
|
||||||
const address = supplier.address.toPrimitive();
|
const address = supplier.address.toPrimitive();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -18,7 +17,7 @@ export class SupplierFullSnapshotBuilder implements ISupplierFullSnapshotBuilder
|
|||||||
status: supplier.isActive ? "active" : "inactive",
|
status: supplier.isActive ? "active" : "inactive",
|
||||||
reference: maybeToEmptyString(supplier.reference, (value) => value.toPrimitive()),
|
reference: maybeToEmptyString(supplier.reference, (value) => value.toPrimitive()),
|
||||||
|
|
||||||
is_company: String(supplier.isCompany),
|
is_company: supplier.isCompany,
|
||||||
name: supplier.name.toPrimitive(),
|
name: supplier.name.toPrimitive(),
|
||||||
trade_name: maybeToEmptyString(supplier.tradeName, (value) => value.toPrimitive()),
|
trade_name: maybeToEmptyString(supplier.tradeName, (value) => value.toPrimitive()),
|
||||||
tin: maybeToEmptyString(supplier.tin, (value) => value.toPrimitive()),
|
tin: maybeToEmptyString(supplier.tin, (value) => value.toPrimitive()),
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./supplier-summary-snapshot-builder";
|
||||||
@ -2,14 +2,13 @@ import type { ISnapshotBuilder } from "@erp/core/api";
|
|||||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
import type { SupplierSummary } from "../../models";
|
import type { SupplierSummary } from "../../models";
|
||||||
|
import type { SupplierSummaryDTO } from "../../../../common";
|
||||||
import type { ISupplierSummarySnapshot } from "./supplier-summary-snapshot.interface";
|
|
||||||
|
|
||||||
export interface ISupplierSummarySnapshotBuilder
|
export interface ISupplierSummarySnapshotBuilder
|
||||||
extends ISnapshotBuilder<SupplierSummary, ISupplierSummarySnapshot> {}
|
extends ISnapshotBuilder<SupplierSummary, SupplierSummaryDTO> {}
|
||||||
|
|
||||||
export class SupplierSummarySnapshotBuilder implements ISupplierSummarySnapshotBuilder {
|
export class SupplierSummarySnapshotBuilder implements ISupplierSummarySnapshotBuilder {
|
||||||
toOutput(supplier: SupplierSummary): ISupplierSummarySnapshot {
|
toOutput(supplier: SupplierSummary): SupplierSummaryDTO {
|
||||||
const { address } = supplier;
|
const { address } = supplier;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -18,7 +17,7 @@ export class SupplierSummarySnapshotBuilder implements ISupplierSummarySnapshotB
|
|||||||
status: supplier.isActive ? "active" : "inactive",
|
status: supplier.isActive ? "active" : "inactive",
|
||||||
reference: maybeToEmptyString(supplier.reference, (value) => value.toString()),
|
reference: maybeToEmptyString(supplier.reference, (value) => value.toString()),
|
||||||
|
|
||||||
is_company: String(supplier.isCompany),
|
is_company: supplier.isCompany,
|
||||||
name: supplier.name.toString(),
|
name: supplier.name.toString(),
|
||||||
trade_name: maybeToEmptyString(supplier.tradeName, (value) => value.toString()),
|
trade_name: maybeToEmptyString(supplier.tradeName, (value) => value.toString()),
|
||||||
tin: maybeToEmptyString(supplier.tin, (value) => value.toString()),
|
tin: maybeToEmptyString(supplier.tin, (value) => value.toString()),
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
export type CreateSupplierCommand = {
|
||||||
|
supplierId: string;
|
||||||
|
reference?: string;
|
||||||
|
isCompany: boolean;
|
||||||
|
name: string;
|
||||||
|
tradeName?: string;
|
||||||
|
tin?: string;
|
||||||
|
street?: string;
|
||||||
|
street2?: string;
|
||||||
|
city?: string;
|
||||||
|
province?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
country?: string;
|
||||||
|
emailPrimary?: string;
|
||||||
|
emailSecondary?: string;
|
||||||
|
phonePrimary?: string;
|
||||||
|
phoneSecondary?: string;
|
||||||
|
mobilePrimary?: string;
|
||||||
|
mobileSecondary?: string;
|
||||||
|
fax?: string;
|
||||||
|
website?: string;
|
||||||
|
languageCode: string;
|
||||||
|
currencyCode: string;
|
||||||
|
};
|
||||||
@ -2,16 +2,14 @@ import type { ITransactionManager } from "@erp/core/api";
|
|||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { CreateSupplierRequestDTO } from "../../../common";
|
import type { ICreateSupplierInputMapper } from "../mappers";
|
||||||
import type { ISupplierCreator } from "../services";
|
import type { ISupplierCreator } from "../services";
|
||||||
import type {
|
import type { ISupplierFullSnapshotBuilder } from "../snapshot-builders";
|
||||||
ICreateSupplierInputMapper,
|
import type { CreateSupplierCommand } from "./create-supplier.command";
|
||||||
ISupplierFullSnapshotBuilder,
|
|
||||||
} from "../snapshot-builders";
|
|
||||||
|
|
||||||
type CreateSupplierUseCaseInput = {
|
type CreateSupplierUseCaseInput = {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
dto: CreateSupplierRequestDTO;
|
command: CreateSupplierCommand;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CreateSupplierUseCaseDeps = {
|
type CreateSupplierUseCaseDeps = {
|
||||||
@ -35,9 +33,9 @@ export class CreateSupplierUseCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public execute(params: CreateSupplierUseCaseInput) {
|
public execute(params: CreateSupplierUseCaseInput) {
|
||||||
const { dto, companyId } = params;
|
const { command, companyId } = params;
|
||||||
|
|
||||||
const mappedPropsResult = this.dtoMapper.map(dto, { companyId });
|
const mappedPropsResult = this.dtoMapper.map(command, { companyId });
|
||||||
if (mappedPropsResult.isFailure) {
|
if (mappedPropsResult.isFailure) {
|
||||||
return mappedPropsResult;
|
return mappedPropsResult;
|
||||||
}
|
}
|
||||||
5
modules/suppliers/src/api/application/use-cases/index.ts
Normal file
5
modules/suppliers/src/api/application/use-cases/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./create-supplier.command";
|
||||||
|
export * from "./create-supplier.use-case";
|
||||||
|
export * from "./get-supplier.use-case";
|
||||||
|
export * from "./list-suppliers.use-case";
|
||||||
|
export * from "./update-supplier.use-case";
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
export type UpdateSupplierPatchInput = {
|
||||||
|
reference?: string;
|
||||||
|
isCompany?: boolean;
|
||||||
|
name?: string;
|
||||||
|
tradeName?: string;
|
||||||
|
tin?: string;
|
||||||
|
street?: string;
|
||||||
|
street2?: string;
|
||||||
|
city?: string;
|
||||||
|
province?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
country?: string;
|
||||||
|
emailPrimary?: string;
|
||||||
|
emailSecondary?: string;
|
||||||
|
phonePrimary?: string;
|
||||||
|
phoneSecondary?: string;
|
||||||
|
mobilePrimary?: string;
|
||||||
|
mobileSecondary?: string;
|
||||||
|
fax?: string;
|
||||||
|
website?: string;
|
||||||
|
languageCode?: string;
|
||||||
|
currencyCode?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateSupplierCommand = {
|
||||||
|
supplierId: string;
|
||||||
|
patch: UpdateSupplierPatchInput;
|
||||||
|
};
|
||||||
@ -0,0 +1,205 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import type { ITransactionManager } from "@erp/core/api";
|
||||||
|
import { EntityNotFoundError } from "@erp/core/api";
|
||||||
|
import {
|
||||||
|
CurrencyCode,
|
||||||
|
EmailAddress,
|
||||||
|
LanguageCode,
|
||||||
|
Name,
|
||||||
|
PostalAddress,
|
||||||
|
UniqueID,
|
||||||
|
} from "@repo/rdx-ddd";
|
||||||
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import { Supplier, type SupplierPatchProps, SupplierStatus } from "../../domain";
|
||||||
|
import { SupplierUpdater } from "../services";
|
||||||
|
import { UpdateSupplierUseCase } from "./update-supplier.use-case";
|
||||||
|
|
||||||
|
const buildTransactionManager = (): ITransactionManager => ({
|
||||||
|
start: async () => undefined,
|
||||||
|
getTransaction: () => undefined,
|
||||||
|
complete: async <T>(work: (transaction: unknown) => Promise<T>) => work(undefined),
|
||||||
|
commit: async () => undefined,
|
||||||
|
rollback: async () => undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildSupplier = (companyId: UniqueID) => {
|
||||||
|
const supplierId = UniqueID.generateNewID();
|
||||||
|
const name = Name.create("Proveedor original");
|
||||||
|
const address = PostalAddress.create({
|
||||||
|
street: Maybe.none(),
|
||||||
|
street2: Maybe.none(),
|
||||||
|
city: Maybe.none(),
|
||||||
|
province: Maybe.none(),
|
||||||
|
postalCode: Maybe.none(),
|
||||||
|
country: Maybe.none(),
|
||||||
|
});
|
||||||
|
const languageCode = LanguageCode.create("es");
|
||||||
|
const currencyCode = CurrencyCode.create("EUR");
|
||||||
|
|
||||||
|
assert.equal(name.isSuccess, true);
|
||||||
|
assert.equal(address.isSuccess, true);
|
||||||
|
assert.equal(languageCode.isSuccess, true);
|
||||||
|
assert.equal(currencyCode.isSuccess, true);
|
||||||
|
|
||||||
|
return Supplier.rehydrate(
|
||||||
|
{
|
||||||
|
companyId,
|
||||||
|
status: SupplierStatus.active(),
|
||||||
|
reference: Maybe.none(),
|
||||||
|
isCompany: true,
|
||||||
|
name: name.data,
|
||||||
|
tradeName: Maybe.none(),
|
||||||
|
tin: Maybe.none(),
|
||||||
|
address: address.data,
|
||||||
|
emailPrimary: Maybe.none(),
|
||||||
|
emailSecondary: Maybe.none(),
|
||||||
|
phonePrimary: Maybe.none(),
|
||||||
|
phoneSecondary: Maybe.none(),
|
||||||
|
mobilePrimary: Maybe.none(),
|
||||||
|
mobileSecondary: Maybe.none(),
|
||||||
|
fax: Maybe.none(),
|
||||||
|
website: Maybe.none(),
|
||||||
|
languageCode: languageCode.data,
|
||||||
|
currencyCode: currencyCode.data,
|
||||||
|
},
|
||||||
|
supplierId
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildUseCase = (params: {
|
||||||
|
patch?: SupplierPatchProps;
|
||||||
|
supplier?: Supplier;
|
||||||
|
repositoryUpdate?: () => Promise<Result<void, Error>>;
|
||||||
|
}) => {
|
||||||
|
let repositoryUpdateCalls = 0;
|
||||||
|
let finderCompanyId: UniqueID | undefined;
|
||||||
|
|
||||||
|
const updater = new SupplierUpdater({
|
||||||
|
repository: {
|
||||||
|
create: async () => Result.ok(),
|
||||||
|
update: async () => {
|
||||||
|
repositoryUpdateCalls += 1;
|
||||||
|
return params.repositoryUpdate ? params.repositoryUpdate() : Result.ok();
|
||||||
|
},
|
||||||
|
existsByIdInCompany: async () => Result.ok(false),
|
||||||
|
getByIdInCompany: async () => Result.fail(new Error("unused")),
|
||||||
|
getByTINInCompany: async () => Result.fail(new Error("unused")),
|
||||||
|
findByCriteriaInCompany: async () => Result.fail(new Error("unused")),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const useCase = new UpdateSupplierUseCase({
|
||||||
|
dtoMapper: {
|
||||||
|
map: () => Result.ok(params.patch ?? { isCompany: false }),
|
||||||
|
},
|
||||||
|
finder: {
|
||||||
|
findSupplierById: async (companyId) => {
|
||||||
|
finderCompanyId = companyId;
|
||||||
|
return params.supplier && companyId.toPrimitive() === params.supplier.companyId.toPrimitive()
|
||||||
|
? Result.ok(params.supplier)
|
||||||
|
: Result.fail(new EntityNotFoundError("Supplier", "id", "missing"));
|
||||||
|
},
|
||||||
|
findSupplierByTIN: async () => Result.fail(new Error("unused")),
|
||||||
|
supplierExists: async () => Result.ok(false),
|
||||||
|
findSuppliersByCriteria: async () => Result.fail(new Error("unused")),
|
||||||
|
},
|
||||||
|
updater,
|
||||||
|
fullSnapshotBuilder: {
|
||||||
|
toOutput: (supplier: Supplier) => ({ id: supplier.id.toPrimitive() }),
|
||||||
|
} as never,
|
||||||
|
transactionManager: buildTransactionManager(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
useCase,
|
||||||
|
getFinderCompanyId: () => finderCompanyId,
|
||||||
|
getRepositoryUpdateCalls: () => repositoryUpdateCalls,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
test("actualiza un supplier dentro de su empresa y persiste el agregado", async () => {
|
||||||
|
const companyId = UniqueID.generateNewID();
|
||||||
|
const supplier = buildSupplier(companyId);
|
||||||
|
const { getFinderCompanyId, getRepositoryUpdateCalls, useCase } = buildUseCase({ supplier });
|
||||||
|
|
||||||
|
const result = await useCase.execute({
|
||||||
|
companyId,
|
||||||
|
command: { supplierId: supplier.id.toPrimitive(), patch: {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.isSuccess, true);
|
||||||
|
assert.equal(getFinderCompanyId(), companyId);
|
||||||
|
assert.equal(getRepositoryUpdateCalls(), 1);
|
||||||
|
assert.equal(supplier.isCompany, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no persiste cuando supplier pertenece a otra empresa", async () => {
|
||||||
|
const companyId = UniqueID.generateNewID();
|
||||||
|
const supplier = buildSupplier(UniqueID.generateNewID());
|
||||||
|
const { getRepositoryUpdateCalls, useCase } = buildUseCase({ supplier });
|
||||||
|
|
||||||
|
const result = await useCase.execute({
|
||||||
|
companyId,
|
||||||
|
command: { supplierId: supplier.id.toPrimitive(), patch: {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.isFailure, true);
|
||||||
|
assert.equal(getRepositoryUpdateCalls(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no persiste cuando supplier no existe", async () => {
|
||||||
|
const companyId = UniqueID.generateNewID();
|
||||||
|
const { getRepositoryUpdateCalls, useCase } = buildUseCase({});
|
||||||
|
|
||||||
|
const result = await useCase.execute({
|
||||||
|
companyId,
|
||||||
|
command: { supplierId: UniqueID.generateNewID().toPrimitive(), patch: {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.isFailure, true);
|
||||||
|
assert.equal(getRepositoryUpdateCalls(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("un patch parcial de email no modifica campos omitidos", async () => {
|
||||||
|
const companyId = UniqueID.generateNewID();
|
||||||
|
const supplier = buildSupplier(companyId);
|
||||||
|
const email = EmailAddress.create("supplier@example.com");
|
||||||
|
|
||||||
|
assert.equal(email.isSuccess, true);
|
||||||
|
|
||||||
|
const { useCase } = buildUseCase({
|
||||||
|
supplier,
|
||||||
|
patch: { emailPrimary: Maybe.some(email.data) },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await useCase.execute({
|
||||||
|
companyId,
|
||||||
|
command: { supplierId: supplier.id.toPrimitive(), patch: {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.isSuccess, true);
|
||||||
|
assert.equal(supplier.name.toPrimitive(), "Proveedor original");
|
||||||
|
assert.equal(supplier.isCompany, true);
|
||||||
|
assert.equal(supplier.emailPrimary.unwrap().toPrimitive(), "supplier@example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("propaga el error de persistencia sin convertirlo en éxito", async () => {
|
||||||
|
const companyId = UniqueID.generateNewID();
|
||||||
|
const supplier = buildSupplier(companyId);
|
||||||
|
const { getRepositoryUpdateCalls, useCase } = buildUseCase({
|
||||||
|
supplier,
|
||||||
|
repositoryUpdate: async () => Result.fail(new Error("repository failed")),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await useCase.execute({
|
||||||
|
companyId,
|
||||||
|
command: { supplierId: supplier.id.toPrimitive(), patch: {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.isFailure, true);
|
||||||
|
assert.equal(result.error.message, "repository failed");
|
||||||
|
assert.equal(getRepositoryUpdateCalls(), 1);
|
||||||
|
});
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import type { ITransactionManager } from "@erp/core/api";
|
||||||
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { SupplierPatchProps } from "../../domain";
|
||||||
|
import type { IUpdateSupplierInputMapper } from "../mappers";
|
||||||
|
import type { ISupplierFinder, ISupplierUpdater } from "../services";
|
||||||
|
import type { ISupplierFullSnapshotBuilder } from "../snapshot-builders";
|
||||||
|
import type { UpdateSupplierCommand } from "./update-supplier.command";
|
||||||
|
|
||||||
|
type UpdateSupplierUseCaseInput = {
|
||||||
|
companyId: UniqueID;
|
||||||
|
command: UpdateSupplierCommand;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UpdateSupplierUseCaseDeps = {
|
||||||
|
dtoMapper: IUpdateSupplierInputMapper;
|
||||||
|
finder: ISupplierFinder;
|
||||||
|
updater: ISupplierUpdater;
|
||||||
|
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UpdateSupplierUseCase {
|
||||||
|
constructor(private readonly deps: UpdateSupplierUseCaseDeps) {}
|
||||||
|
|
||||||
|
public execute(params: UpdateSupplierUseCaseInput) {
|
||||||
|
const { companyId, command } = params;
|
||||||
|
|
||||||
|
const idOrError = UniqueID.create(command.supplierId);
|
||||||
|
if (idOrError.isFailure) {
|
||||||
|
return Result.fail(idOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchPropsResult = this.deps.dtoMapper.map(command.patch, { companyId });
|
||||||
|
if (patchPropsResult.isFailure) {
|
||||||
|
return patchPropsResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchProps: SupplierPatchProps = patchPropsResult.data;
|
||||||
|
const supplierId = idOrError.data;
|
||||||
|
|
||||||
|
return this.deps.transactionManager.complete(async (transaction: unknown) => {
|
||||||
|
try {
|
||||||
|
const existingSupplierResult = await this.deps.finder.findSupplierById(
|
||||||
|
companyId,
|
||||||
|
supplierId,
|
||||||
|
transaction
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingSupplierResult.isFailure) {
|
||||||
|
return Result.fail(existingSupplierResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateResult = await this.deps.updater.update({
|
||||||
|
supplier: existingSupplierResult.data,
|
||||||
|
props: patchProps,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updateResult.isFailure) {
|
||||||
|
return Result.fail(updateResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(this.deps.fullSnapshotBuilder.toOutput(updateResult.data));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return Result.fail(error as Error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CurrencyCode,
|
||||||
|
LanguageCode,
|
||||||
|
Name,
|
||||||
|
Street,
|
||||||
|
UniqueID,
|
||||||
|
} from "@repo/rdx-ddd";
|
||||||
|
import { Maybe } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import { Supplier } from "./supplier.aggregate";
|
||||||
|
|
||||||
|
test("crea Supplier activo por defecto y actualiza su dirección", () => {
|
||||||
|
const nameResult = Name.create("Proveedor de prueba");
|
||||||
|
const languageCodeResult = LanguageCode.create("es");
|
||||||
|
const currencyCodeResult = CurrencyCode.create("EUR");
|
||||||
|
|
||||||
|
assert.equal(nameResult.isSuccess, true);
|
||||||
|
assert.equal(languageCodeResult.isSuccess, true);
|
||||||
|
assert.equal(currencyCodeResult.isSuccess, true);
|
||||||
|
|
||||||
|
const supplierResult = Supplier.create({
|
||||||
|
companyId: UniqueID.generateNewID(),
|
||||||
|
reference: Maybe.none(),
|
||||||
|
isCompany: true,
|
||||||
|
name: nameResult.data,
|
||||||
|
tradeName: Maybe.none(),
|
||||||
|
tin: Maybe.none(),
|
||||||
|
address: {
|
||||||
|
street: Maybe.none(),
|
||||||
|
street2: Maybe.none(),
|
||||||
|
city: Maybe.none(),
|
||||||
|
postalCode: Maybe.none(),
|
||||||
|
province: Maybe.none(),
|
||||||
|
country: Maybe.none(),
|
||||||
|
},
|
||||||
|
emailPrimary: Maybe.none(),
|
||||||
|
emailSecondary: Maybe.none(),
|
||||||
|
phonePrimary: Maybe.none(),
|
||||||
|
phoneSecondary: Maybe.none(),
|
||||||
|
mobilePrimary: Maybe.none(),
|
||||||
|
mobileSecondary: Maybe.none(),
|
||||||
|
fax: Maybe.none(),
|
||||||
|
website: Maybe.none(),
|
||||||
|
languageCode: languageCodeResult.data,
|
||||||
|
currencyCode: currencyCodeResult.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(supplierResult.isSuccess, true);
|
||||||
|
assert.equal(supplierResult.data.isActive, true);
|
||||||
|
|
||||||
|
const streetResult = Street.create("Calle de prueba");
|
||||||
|
assert.equal(streetResult.isSuccess, true);
|
||||||
|
|
||||||
|
const updateResult = supplierResult.data.update({
|
||||||
|
address: { street: Maybe.some(streetResult.data) },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(updateResult.isSuccess, true);
|
||||||
|
assert.equal(supplierResult.data.address.street.unwrap().toPrimitive(), "Calle de prueba");
|
||||||
|
});
|
||||||
@ -5,9 +5,9 @@ import {
|
|||||||
type LanguageCode,
|
type LanguageCode,
|
||||||
type Name,
|
type Name,
|
||||||
type PhoneNumber,
|
type PhoneNumber,
|
||||||
type PostalAddress,
|
|
||||||
type PostalAddressPatchProps,
|
type PostalAddressPatchProps,
|
||||||
type PostalAddressProps,
|
type PostalAddressProps,
|
||||||
|
PostalAddress,
|
||||||
type TINNumber,
|
type TINNumber,
|
||||||
type URLAddress,
|
type URLAddress,
|
||||||
type UniqueID,
|
type UniqueID,
|
||||||
@ -87,8 +87,9 @@ export interface ISupplier {
|
|||||||
readonly currencyCode: CurrencyCode;
|
readonly currencyCode: CurrencyCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupplierInternalProps = Omit<ISupplierCreateProps, "address"> & {
|
export type SupplierInternalProps = Omit<ISupplierCreateProps, "address" | "status"> & {
|
||||||
readonly address: PostalAddress;
|
address: PostalAddress;
|
||||||
|
status: SupplierStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,6 +115,7 @@ export class Supplier extends AggregateRoot<SupplierInternalProps> implements IS
|
|||||||
{
|
{
|
||||||
...internalProps,
|
...internalProps,
|
||||||
address: postalAddressResult.data,
|
address: postalAddressResult.data,
|
||||||
|
status: props.status ?? SupplierStatus.active(),
|
||||||
},
|
},
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
@ -129,7 +131,7 @@ export class Supplier extends AggregateRoot<SupplierInternalProps> implements IS
|
|||||||
return Result.ok(contact);
|
return Result.ok(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static validateCreateProps(props: ISupplierCreateProps): Result<void, Error> {
|
private static validateCreateProps(_props: ISupplierCreateProps): Result<void, Error> {
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,18 +146,27 @@ export class Supplier extends AggregateRoot<SupplierInternalProps> implements IS
|
|||||||
|
|
||||||
public update(partialSupplier: SupplierPatchProps): Result<Supplier, Error> {
|
public update(partialSupplier: SupplierPatchProps): Result<Supplier, Error> {
|
||||||
const { address: partialAddress, ...rest } = partialSupplier;
|
const { address: partialAddress, ...rest } = partialSupplier;
|
||||||
|
const nextProps: SupplierInternalProps = {
|
||||||
Object.assign(this.props, rest);
|
...this.props,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
|
||||||
if (partialAddress) {
|
if (partialAddress) {
|
||||||
const addressResult = this.address.update(partialAddress);
|
const addressResult = PostalAddress.create({
|
||||||
|
...this.address.getProps(),
|
||||||
|
...partialAddress,
|
||||||
|
});
|
||||||
|
|
||||||
if (addressResult.isFailure) {
|
if (addressResult.isFailure) {
|
||||||
return Result.fail(addressResult.error);
|
return Result.fail(addressResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextProps.address = addressResult.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.ok();
|
Object.assign(this.props, nextProps);
|
||||||
|
|
||||||
|
return Result.ok(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public activate(): Result<Supplier, Error> {
|
public activate(): Result<Supplier, Error> {
|
||||||
26
modules/suppliers/src/api/index.test.ts
Normal file
26
modules/suppliers/src/api/index.test.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import type { ModuleParams } from "@erp/core/api";
|
||||||
|
|
||||||
|
import { suppliersAPIModule } from "./index";
|
||||||
|
|
||||||
|
test("compone suppliers sin publicar servicios de infraestructura", async () => {
|
||||||
|
const params: ModuleParams = {
|
||||||
|
database: {},
|
||||||
|
getService: <T>() => undefined as T,
|
||||||
|
logger: {
|
||||||
|
info: () => undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await suppliersAPIModule.setup?.(params);
|
||||||
|
|
||||||
|
assert.ok(result);
|
||||||
|
assert.ok(result.models);
|
||||||
|
assert.equal(result.services, undefined);
|
||||||
|
|
||||||
|
const useCases = result.internal?.useCases;
|
||||||
|
assert.ok(useCases && typeof useCases === "object");
|
||||||
|
assert.equal(typeof (useCases as Record<string, unknown>).updateSupplier, "function");
|
||||||
|
});
|
||||||
@ -1,10 +1,7 @@
|
|||||||
import type { IModuleServer } from "@erp/core/api";
|
import type { IModuleServer } from "@erp/core/api";
|
||||||
|
|
||||||
import type { ISupplierPublicServices } from "./application";
|
|
||||||
import { models, suppliersRouter } from "./infrastructure";
|
import { models, suppliersRouter } from "./infrastructure";
|
||||||
import { buildSupplierPublicServices, buildSuppliersDependencies } from "./infrastructure/di";
|
import { buildSuppliersDependencies } from "./infrastructure/di";
|
||||||
|
|
||||||
export * from "./infrastructure/persistence/sequelize";
|
|
||||||
|
|
||||||
export const suppliersAPIModule: IModuleServer = {
|
export const suppliersAPIModule: IModuleServer = {
|
||||||
name: "suppliers",
|
name: "suppliers",
|
||||||
@ -19,17 +16,11 @@ export const suppliersAPIModule: IModuleServer = {
|
|||||||
* - NO conecta infraestructura
|
* - NO conecta infraestructura
|
||||||
*/
|
*/
|
||||||
async setup(params) {
|
async setup(params) {
|
||||||
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
const { logger } = params;
|
||||||
|
|
||||||
// 1) Dominio interno
|
// El grafo se construye una sola vez y permanece privado al módulo.
|
||||||
const internal = buildSuppliersDependencies(params);
|
const internal = buildSuppliersDependencies(params);
|
||||||
|
|
||||||
// 2) Servicios públicos (Application Services)
|
|
||||||
const suppliersServices: ISupplierPublicServices = buildSupplierPublicServices(
|
|
||||||
params,
|
|
||||||
internal
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info("🚀 Suppliers module dependencies registered", {
|
logger.info("🚀 Suppliers module dependencies registered", {
|
||||||
label: this.name,
|
label: this.name,
|
||||||
});
|
});
|
||||||
@ -38,11 +29,6 @@ export const suppliersAPIModule: IModuleServer = {
|
|||||||
// Modelos Sequelize del módulo
|
// Modelos Sequelize del módulo
|
||||||
models,
|
models,
|
||||||
|
|
||||||
// Servicios expuestos a otros módulos
|
|
||||||
services: {
|
|
||||||
general: suppliersServices, // 'suppliers:general'
|
|
||||||
},
|
|
||||||
|
|
||||||
// Implementación privada del módulo
|
// Implementación privada del módulo
|
||||||
internal,
|
internal,
|
||||||
};
|
};
|
||||||
@ -56,7 +42,7 @@ export const suppliersAPIModule: IModuleServer = {
|
|||||||
* - NO construye dominio
|
* - NO construye dominio
|
||||||
*/
|
*/
|
||||||
async start(params) {
|
async start(params) {
|
||||||
const { app, baseRoutePath, logger, getInternal } = params;
|
const { logger } = params;
|
||||||
|
|
||||||
// Registro de rutas HTTP
|
// Registro de rutas HTTP
|
||||||
suppliersRouter(params);
|
suppliersRouter(params);
|
||||||
1
modules/suppliers/src/api/infrastructure/di/index.ts
Normal file
1
modules/suppliers/src/api/infrastructure/di/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./suppliers.di";
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import type { ICatalogs } from "@erp/core/api";
|
|
||||||
|
|
||||||
import { SequelizeSupplierDomainMapper, SequelizeSupplierSummaryMapper } from "../persistence";
|
import { SequelizeSupplierDomainMapper, SequelizeSupplierSummaryMapper } from "../persistence";
|
||||||
|
|
||||||
export interface ISupplierPersistenceMappers {
|
export interface ISupplierPersistenceMappers {
|
||||||
@ -9,13 +7,9 @@ export interface ISupplierPersistenceMappers {
|
|||||||
//createMapper: CreateSupplierInputMapper;
|
//createMapper: CreateSupplierInputMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildSupplierPersistenceMappers = (
|
export const buildSupplierPersistenceMappers = (): ISupplierPersistenceMappers => {
|
||||||
catalogs: ICatalogs
|
|
||||||
): ISupplierPersistenceMappers => {
|
|
||||||
const { taxCatalog } = catalogs;
|
|
||||||
|
|
||||||
// Mappers para el repositorio
|
// Mappers para el repositorio
|
||||||
const domainMapper = new SequelizeSupplierDomainMapper({ taxCatalog });
|
const domainMapper = new SequelizeSupplierDomainMapper();
|
||||||
const summaryMapper = new SequelizeSupplierSummaryMapper();
|
const summaryMapper = new SequelizeSupplierSummaryMapper();
|
||||||
|
|
||||||
// Mappers el DTO a las props validadas (SupplierProps) y luego construir agregado
|
// Mappers el DTO a las props validadas (SupplierProps) y luego construir agregado
|
||||||
@ -1,17 +1,19 @@
|
|||||||
import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api";
|
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type CreateSupplierUseCase,
|
type CreateSupplierUseCase,
|
||||||
type GetSupplierByIdUseCase,
|
type GetSupplierByIdUseCase,
|
||||||
type ListSuppliersUseCase,
|
type ListSuppliersUseCase,
|
||||||
|
type UpdateSupplierUseCase,
|
||||||
buildCreateSupplierUseCase,
|
buildCreateSupplierUseCase,
|
||||||
buildGetSupplierByIdUseCase,
|
buildGetSupplierByIdUseCase,
|
||||||
buildListSuppliersUseCase,
|
buildListSuppliersUseCase,
|
||||||
|
buildSupplierUpdater,
|
||||||
|
buildUpdateSupplierUseCase,
|
||||||
buildSupplierCreator,
|
buildSupplierCreator,
|
||||||
buildSupplierFinder,
|
buildSupplierFinder,
|
||||||
buildSupplierInputMappers,
|
buildSupplierInputMappers,
|
||||||
buildSupplierSnapshotBuilders,
|
buildSupplierSnapshotBuilders,
|
||||||
buildSupplierUpdater,
|
|
||||||
} from "../../application";
|
} from "../../application";
|
||||||
|
|
||||||
import { buildSupplierPersistenceMappers } from "./supplier-persistence-mappers.di";
|
import { buildSupplierPersistenceMappers } from "./supplier-persistence-mappers.di";
|
||||||
@ -22,12 +24,7 @@ export type SuppliersInternalDeps = {
|
|||||||
listSuppliers: () => ListSuppliersUseCase;
|
listSuppliers: () => ListSuppliersUseCase;
|
||||||
getSupplierById: () => GetSupplierByIdUseCase;
|
getSupplierById: () => GetSupplierByIdUseCase;
|
||||||
createSupplier: () => CreateSupplierUseCase;
|
createSupplier: () => CreateSupplierUseCase;
|
||||||
//updateSupplier: () => UpdateSupplierUseCase;
|
updateSupplier: () => UpdateSupplierUseCase;
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
deleteSupplier: () => DeleteSupplierUseCase;
|
|
||||||
changeStatusSupplier: () => ChangeStatusSupplierUseCase;*/
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -36,20 +33,17 @@ export function buildSuppliersDependencies(params: ModuleParams): SuppliersInter
|
|||||||
|
|
||||||
// Infrastructure
|
// Infrastructure
|
||||||
const transactionManager = buildTransactionManager(database);
|
const transactionManager = buildTransactionManager(database);
|
||||||
const catalogs = buildCatalogs();
|
const persistenceMappers = buildSupplierPersistenceMappers();
|
||||||
const persistenceMappers = buildSupplierPersistenceMappers(catalogs);
|
|
||||||
|
|
||||||
const repository = buildSupplierRepository({ database, mappers: persistenceMappers });
|
const repository = buildSupplierRepository({ database, mappers: persistenceMappers });
|
||||||
//const numberService = buildSupplierNumberGenerator();
|
|
||||||
|
|
||||||
// Application helpers
|
// Application helpers
|
||||||
const inputMappers = buildSupplierInputMappers(catalogs);
|
const inputMappers = buildSupplierInputMappers();
|
||||||
const finder = buildSupplierFinder({ repository });
|
const finder = buildSupplierFinder({ repository });
|
||||||
const creator = buildSupplierCreator({ repository });
|
const creator = buildSupplierCreator({ repository });
|
||||||
const updater = buildSupplierUpdater({ repository });
|
const updater = buildSupplierUpdater({ repository });
|
||||||
|
|
||||||
const snapshotBuilders = buildSupplierSnapshotBuilders();
|
const snapshotBuilders = buildSupplierSnapshotBuilders();
|
||||||
//const documentGeneratorPipeline = buildSupplierDocumentService(params);
|
|
||||||
|
|
||||||
// Internal use cases (factories)
|
// Internal use cases (factories)
|
||||||
return {
|
return {
|
||||||
@ -76,13 +70,14 @@ export function buildSuppliersDependencies(params: ModuleParams): SuppliersInter
|
|||||||
transactionManager,
|
transactionManager,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/*updateSupplier: () =>
|
updateSupplier: () =>
|
||||||
buildUpdateSupplierUseCase({
|
buildUpdateSupplierUseCase({
|
||||||
|
finder,
|
||||||
updater,
|
updater,
|
||||||
dtoMapper: inputMappers.updateInputMapper,
|
dtoMapper: inputMappers.updateInputMapper,
|
||||||
fullSnapshotBuilder: snapshotBuilders.full,
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
transactionManager,
|
transactionManager,
|
||||||
}),*/
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -8,9 +8,13 @@ import {
|
|||||||
import type { CreateSupplierRequestDTO } from "../../../../common/dto";
|
import type { CreateSupplierRequestDTO } from "../../../../common/dto";
|
||||||
import type { CreateSupplierUseCase } from "../../../application";
|
import type { CreateSupplierUseCase } from "../../../application";
|
||||||
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
||||||
|
import type { CreateSupplierRequestMapper } from "../mappers/create-supplier-request.mapper";
|
||||||
|
|
||||||
export class CreateSupplierController extends ExpressController {
|
export class CreateSupplierController extends ExpressController {
|
||||||
public constructor(private readonly useCase: CreateSupplierUseCase) {
|
public constructor(
|
||||||
|
private readonly useCase: CreateSupplierUseCase,
|
||||||
|
private readonly requestMapper: CreateSupplierRequestMapper
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
this.errorMapper = suppliersApiErrorMapper;
|
this.errorMapper = suppliersApiErrorMapper;
|
||||||
|
|
||||||
@ -28,8 +32,9 @@ export class CreateSupplierController extends ExpressController {
|
|||||||
return this.forbiddenError("Tenant ID not found");
|
return this.forbiddenError("Tenant ID not found");
|
||||||
}
|
}
|
||||||
const dto = this.req.body as CreateSupplierRequestDTO;
|
const dto = this.req.body as CreateSupplierRequestDTO;
|
||||||
|
const command = this.requestMapper.map(dto);
|
||||||
|
|
||||||
const result = await this.useCase.execute({ dto, companyId });
|
const result = await this.useCase.execute({ command, companyId });
|
||||||
|
|
||||||
return result.match(
|
return result.match(
|
||||||
(data) => this.created(data),
|
(data) => this.created(data),
|
||||||
@ -5,11 +5,11 @@ import {
|
|||||||
requireCompanyContextGuard,
|
requireCompanyContextGuard,
|
||||||
} from "@erp/core/api";
|
} from "@erp/core/api";
|
||||||
|
|
||||||
import type { GetSupplierUseCase } from "../../../application";
|
import type { GetSupplierByIdUseCase } from "../../../application";
|
||||||
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
||||||
|
|
||||||
export class GetSupplierController extends ExpressController {
|
export class GetSupplierController extends ExpressController {
|
||||||
public constructor(private readonly useCase: GetSupplierUseCase) {
|
public constructor(private readonly useCase: GetSupplierByIdUseCase) {
|
||||||
super();
|
super();
|
||||||
this.errorMapper = suppliersApiErrorMapper;
|
this.errorMapper = suppliersApiErrorMapper;
|
||||||
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
export * from "./create-supplier.controller";
|
export * from "./create-supplier.controller";
|
||||||
export * from "./delete-supplier.controller";
|
|
||||||
export * from "./get-supplier.controller";
|
export * from "./get-supplier.controller";
|
||||||
export * from "./list-suppliers.controller";
|
export * from "./list-suppliers.controller";
|
||||||
export * from "./update-supplier.controller";
|
export * from "./update-supplier.controller";
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { EntityNotFoundError } from "@erp/core/api";
|
||||||
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import {
|
||||||
|
UpdateSupplierByIdParamsRequestSchema,
|
||||||
|
UpdateSupplierByIdRequestSchema,
|
||||||
|
} from "../../../../common";
|
||||||
|
import { UpdateSupplierByIdRequestMapper } from "../mappers/update-supplier-by-id-request.mapper";
|
||||||
|
import { UpdateSupplierController } from "./update-supplier.controller";
|
||||||
|
|
||||||
|
const companyId = UniqueID.create("22222222-2222-4222-8222-222222222222").data;
|
||||||
|
const supplierId = "11111111-1111-4111-8111-111111111111";
|
||||||
|
|
||||||
|
const createResponse = () => {
|
||||||
|
const response = {
|
||||||
|
body: undefined as unknown,
|
||||||
|
statusCode: 0,
|
||||||
|
status(statusCode: number) {
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
json(body: unknown) {
|
||||||
|
this.body = body;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
type() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
setHeader() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRequest = (params: { body: unknown; companyId: typeof companyId }) =>
|
||||||
|
({
|
||||||
|
body: params.body,
|
||||||
|
params: { supplier_id: supplierId },
|
||||||
|
query: {},
|
||||||
|
user: { companyId: params.companyId },
|
||||||
|
originalUrl: `/suppliers/${supplierId}`,
|
||||||
|
method: "PATCH",
|
||||||
|
protocol: "http",
|
||||||
|
get: () => undefined,
|
||||||
|
}) as never;
|
||||||
|
|
||||||
|
test("PATCH construye command parcial y conserva is_company false", async () => {
|
||||||
|
let receivedInput: unknown;
|
||||||
|
const controller = new UpdateSupplierController(
|
||||||
|
{
|
||||||
|
execute: async (input: unknown) => {
|
||||||
|
receivedInput = input;
|
||||||
|
return Result.ok({ id: supplierId, is_company: false });
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
new UpdateSupplierByIdRequestMapper()
|
||||||
|
);
|
||||||
|
const response = createResponse();
|
||||||
|
|
||||||
|
await controller.execute(
|
||||||
|
createRequest({
|
||||||
|
body: { is_company: false, email_primary: "new@example.com" },
|
||||||
|
companyId,
|
||||||
|
}),
|
||||||
|
response as never,
|
||||||
|
() => undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(receivedInput, {
|
||||||
|
companyId,
|
||||||
|
command: {
|
||||||
|
supplierId,
|
||||||
|
patch: { isCompany: false, emailPrimary: "new@example.com" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.equal(response.statusCode, 200);
|
||||||
|
assert.deepEqual(response.body, { id: supplierId, is_company: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("PATCH cross-company devuelve not found y no expone una actualización", async () => {
|
||||||
|
let updateAttempts = 0;
|
||||||
|
const controller = new UpdateSupplierController(
|
||||||
|
{
|
||||||
|
execute: async () => {
|
||||||
|
updateAttempts += 1;
|
||||||
|
return Result.fail(new EntityNotFoundError("Supplier", "id", supplierId));
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
new UpdateSupplierByIdRequestMapper()
|
||||||
|
);
|
||||||
|
const response = createResponse();
|
||||||
|
const otherCompanyId = UniqueID.create("33333333-3333-4333-8333-333333333333").data;
|
||||||
|
|
||||||
|
await controller.execute(
|
||||||
|
createRequest({ body: { name: "No permitido" }, companyId: otherCompanyId }),
|
||||||
|
response as never,
|
||||||
|
() => undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(updateAttempts, 1);
|
||||||
|
assert.equal(response.statusCode, 404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("los schemas de PATCH rechazan UUID y body inválidos", () => {
|
||||||
|
assert.equal(
|
||||||
|
UpdateSupplierByIdParamsRequestSchema.safeParse({ supplier_id: "not-a-uuid" }).success,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
assert.equal(UpdateSupplierByIdRequestSchema.safeParse({ is_company: "false" }).success, false);
|
||||||
|
});
|
||||||
@ -8,9 +8,13 @@ import {
|
|||||||
import type { UpdateSupplierByIdRequestDTO } from "../../../../common/dto";
|
import type { UpdateSupplierByIdRequestDTO } from "../../../../common/dto";
|
||||||
import type { UpdateSupplierUseCase } from "../../../application";
|
import type { UpdateSupplierUseCase } from "../../../application";
|
||||||
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
||||||
|
import type { UpdateSupplierByIdRequestMapper } from "../mappers/update-supplier-by-id-request.mapper";
|
||||||
|
|
||||||
export class UpdateSupplierController extends ExpressController {
|
export class UpdateSupplierController extends ExpressController {
|
||||||
public constructor(private readonly useCase: UpdateSupplierUseCase) {
|
public constructor(
|
||||||
|
private readonly useCase: UpdateSupplierUseCase,
|
||||||
|
private readonly requestMapper: UpdateSupplierByIdRequestMapper
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
this.errorMapper = suppliersApiErrorMapper;
|
this.errorMapper = suppliersApiErrorMapper;
|
||||||
|
|
||||||
@ -29,8 +33,9 @@ export class UpdateSupplierController extends ExpressController {
|
|||||||
}
|
}
|
||||||
const { supplier_id } = this.req.params;
|
const { supplier_id } = this.req.params;
|
||||||
const dto = this.req.body as UpdateSupplierByIdRequestDTO;
|
const dto = this.req.body as UpdateSupplierByIdRequestDTO;
|
||||||
|
const command = this.requestMapper.map({ supplierId: supplier_id, dto });
|
||||||
|
|
||||||
const result = await this.useCase.execute({ supplier_id, companyId, dto });
|
const result = await this.useCase.execute({ companyId, command });
|
||||||
|
|
||||||
return result.match(
|
return result.match(
|
||||||
(data) => this.ok(data),
|
(data) => this.ok(data),
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { CreateSupplierRequestMapper } from "./create-supplier-request.mapper";
|
||||||
|
|
||||||
|
test("mapea el DTO de creación sin alterar booleanos ni móviles", () => {
|
||||||
|
const command = new CreateSupplierRequestMapper().map({
|
||||||
|
id: "11111111-1111-4111-8111-111111111111",
|
||||||
|
is_company: false,
|
||||||
|
name: "Proveedor de prueba",
|
||||||
|
mobile_primary: "+34111111111",
|
||||||
|
mobile_secondary: "+34222222222",
|
||||||
|
language_code: "es",
|
||||||
|
currency_code: "EUR",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(command.isCompany, false);
|
||||||
|
assert.equal(command.mobilePrimary, "+34111111111");
|
||||||
|
assert.equal(command.mobileSecondary, "+34222222222");
|
||||||
|
});
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import type { CreateSupplierRequestDTO } from "../../../../common";
|
||||||
|
import type { CreateSupplierCommand } from "../../../application";
|
||||||
|
|
||||||
|
export class CreateSupplierRequestMapper {
|
||||||
|
public map(dto: CreateSupplierRequestDTO): CreateSupplierCommand {
|
||||||
|
return {
|
||||||
|
supplierId: dto.id,
|
||||||
|
reference: dto.reference,
|
||||||
|
isCompany: dto.is_company,
|
||||||
|
name: dto.name,
|
||||||
|
tradeName: dto.trade_name,
|
||||||
|
tin: dto.tin,
|
||||||
|
street: dto.street,
|
||||||
|
street2: dto.street2,
|
||||||
|
city: dto.city,
|
||||||
|
province: dto.province,
|
||||||
|
postalCode: dto.postal_code,
|
||||||
|
country: dto.country,
|
||||||
|
emailPrimary: dto.email_primary,
|
||||||
|
emailSecondary: dto.email_secondary,
|
||||||
|
phonePrimary: dto.phone_primary,
|
||||||
|
phoneSecondary: dto.phone_secondary,
|
||||||
|
mobilePrimary: dto.mobile_primary,
|
||||||
|
mobileSecondary: dto.mobile_secondary,
|
||||||
|
fax: dto.fax,
|
||||||
|
website: dto.website,
|
||||||
|
languageCode: dto.language_code,
|
||||||
|
currencyCode: dto.currency_code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { UpdateSupplierByIdRequestMapper } from "./update-supplier-by-id-request.mapper";
|
||||||
|
|
||||||
|
test("mapea el RequestDTO al comando con propiedades camelCase", () => {
|
||||||
|
const mapper = new UpdateSupplierByIdRequestMapper();
|
||||||
|
|
||||||
|
const command = mapper.map({
|
||||||
|
supplierId: "supplier-123",
|
||||||
|
dto: {
|
||||||
|
name: "Supplier name",
|
||||||
|
trade_name: "Supplier trade name",
|
||||||
|
is_company: false,
|
||||||
|
postal_code: "28001",
|
||||||
|
email_primary: "supplier@example.com",
|
||||||
|
mobile_primary: "+34600111222",
|
||||||
|
mobile_secondary: "+34600333444",
|
||||||
|
language_code: "es",
|
||||||
|
currency_code: "EUR",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(command.supplierId, "supplier-123");
|
||||||
|
assert.equal(command.patch.name, "Supplier name");
|
||||||
|
assert.equal(command.patch.tradeName, "Supplier trade name");
|
||||||
|
assert.equal(command.patch.isCompany, false);
|
||||||
|
assert.equal(command.patch.postalCode, "28001");
|
||||||
|
assert.equal(command.patch.emailPrimary, "supplier@example.com");
|
||||||
|
assert.equal(command.patch.mobilePrimary, "+34600111222");
|
||||||
|
assert.equal(command.patch.mobileSecondary, "+34600333444");
|
||||||
|
assert.equal(command.patch.languageCode, "es");
|
||||||
|
assert.equal(command.patch.currencyCode, "EUR");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("conserva is_company true como boolean", () => {
|
||||||
|
const mapper = new UpdateSupplierByIdRequestMapper();
|
||||||
|
|
||||||
|
const command = mapper.map({
|
||||||
|
supplierId: "supplier-123",
|
||||||
|
dto: { is_company: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(command.patch.isCompany, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserva los campos omitidos fuera del comando", () => {
|
||||||
|
const mapper = new UpdateSupplierByIdRequestMapper();
|
||||||
|
|
||||||
|
const command = mapper.map({
|
||||||
|
supplierId: "supplier-123",
|
||||||
|
dto: { email_primary: "new@example.com" },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(command.patch, { emailPrimary: "new@example.com" });
|
||||||
|
assert.equal("isCompany" in command.patch, false);
|
||||||
|
assert.equal("mobileSecondary" in command.patch, false);
|
||||||
|
assert.equal("name" in command.patch, false);
|
||||||
|
});
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import type { UpdateSupplierByIdRequestDTO } from "../../../../common";
|
||||||
|
import type {
|
||||||
|
UpdateSupplierCommand,
|
||||||
|
UpdateSupplierPatchInput,
|
||||||
|
} from "../../../application/use-cases/update-supplier.command";
|
||||||
|
|
||||||
|
const PATCH_FIELD_MAPPING: Readonly<
|
||||||
|
Record<keyof UpdateSupplierByIdRequestDTO, keyof UpdateSupplierPatchInput>
|
||||||
|
> = {
|
||||||
|
reference: "reference",
|
||||||
|
is_company: "isCompany",
|
||||||
|
name: "name",
|
||||||
|
trade_name: "tradeName",
|
||||||
|
tin: "tin",
|
||||||
|
street: "street",
|
||||||
|
street2: "street2",
|
||||||
|
city: "city",
|
||||||
|
province: "province",
|
||||||
|
postal_code: "postalCode",
|
||||||
|
country: "country",
|
||||||
|
email_primary: "emailPrimary",
|
||||||
|
email_secondary: "emailSecondary",
|
||||||
|
phone_primary: "phonePrimary",
|
||||||
|
phone_secondary: "phoneSecondary",
|
||||||
|
mobile_primary: "mobilePrimary",
|
||||||
|
mobile_secondary: "mobileSecondary",
|
||||||
|
fax: "fax",
|
||||||
|
website: "website",
|
||||||
|
language_code: "languageCode",
|
||||||
|
currency_code: "currencyCode",
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UpdateSupplierByIdRequestMapper {
|
||||||
|
public map(params: {
|
||||||
|
supplierId: string;
|
||||||
|
dto: UpdateSupplierByIdRequestDTO;
|
||||||
|
}): UpdateSupplierCommand {
|
||||||
|
return {
|
||||||
|
supplierId: params.supplierId,
|
||||||
|
patch: this.mapPatch(params.dto),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapPatch(dto: UpdateSupplierByIdRequestDTO): UpdateSupplierPatchInput {
|
||||||
|
const entries = Object.entries(PATCH_FIELD_MAPPING).flatMap(([requestField, commandField]) => {
|
||||||
|
const value = dto[requestField as keyof UpdateSupplierByIdRequestDTO];
|
||||||
|
|
||||||
|
return value === undefined ? [] : [[commandField, value]];
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.fromEntries(entries) as UpdateSupplierPatchInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import type { StartParams } from "@erp/core/api";
|
||||||
|
|
||||||
|
import { suppliersRouter } from "./suppliers.routes";
|
||||||
|
|
||||||
|
type ExpressRouteLayer = {
|
||||||
|
route?: {
|
||||||
|
methods: Record<string, boolean>;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExpressRouter = {
|
||||||
|
stack: ExpressRouteLayer[];
|
||||||
|
};
|
||||||
|
|
||||||
|
test("registra PATCH como única operación de actualización", () => {
|
||||||
|
let registeredRouter: ExpressRouter | undefined;
|
||||||
|
|
||||||
|
const params: StartParams = {
|
||||||
|
app: {
|
||||||
|
use: (_path: string, router: ExpressRouter) => {
|
||||||
|
registeredRouter = router;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
server: {
|
||||||
|
apiBasePath: "/api",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getService: <T>() => undefined as T,
|
||||||
|
getInternal: <T>() => ({ useCases: {} }) as T,
|
||||||
|
};
|
||||||
|
|
||||||
|
suppliersRouter(params);
|
||||||
|
|
||||||
|
if (!registeredRouter) {
|
||||||
|
assert.fail("El router de suppliers no se registró en la aplicación");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
registeredRouter.stack.some(
|
||||||
|
(layer) => layer.route?.path === "/:supplier_id" && layer.route.methods.put
|
||||||
|
),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
registeredRouter.stack.some(
|
||||||
|
(layer) => layer.route?.path === "/:supplier_id" && layer.route.methods.patch
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -17,6 +17,8 @@ import {
|
|||||||
ListSuppliersController,
|
ListSuppliersController,
|
||||||
UpdateSupplierController,
|
UpdateSupplierController,
|
||||||
} from "./controllers";
|
} from "./controllers";
|
||||||
|
import { CreateSupplierRequestMapper } from "./mappers/create-supplier-request.mapper";
|
||||||
|
import { UpdateSupplierByIdRequestMapper } from "./mappers/update-supplier-by-id-request.mapper";
|
||||||
|
|
||||||
export const suppliersRouter = (params: StartParams) => {
|
export const suppliersRouter = (params: StartParams) => {
|
||||||
const { app, config, getInternal } = params;
|
const { app, config, getInternal } = params;
|
||||||
@ -78,35 +80,22 @@ export const suppliersRouter = (params: StartParams) => {
|
|||||||
validateRequest(CreateSupplierRequestSchema, "body"),
|
validateRequest(CreateSupplierRequestSchema, "body"),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const useCase = deps.useCases.createSupplier();
|
const useCase = deps.useCases.createSupplier();
|
||||||
const controller = new CreateSupplierController(useCase);
|
const controller = new CreateSupplierController(useCase, new CreateSupplierRequestMapper());
|
||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
router.put(
|
router.patch(
|
||||||
"/:supplier_id",
|
"/:supplier_id",
|
||||||
//checkTabContext,
|
|
||||||
|
|
||||||
validateRequest(UpdateSupplierByIdParamsRequestSchema, "params"),
|
validateRequest(UpdateSupplierByIdParamsRequestSchema, "params"),
|
||||||
validateRequest(UpdateSupplierByIdRequestSchema, "body"),
|
validateRequest(UpdateSupplierByIdRequestSchema, "body"),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const useCase = deps.useCases.updateSupplier();
|
const controller = new UpdateSupplierController(
|
||||||
const controller = new UpdateSupplierController(useCase);
|
deps.useCases.updateSupplier(),
|
||||||
|
new UpdateSupplierByIdRequestMapper()
|
||||||
|
);
|
||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/*router.delete(
|
|
||||||
"/:supplier_id",
|
|
||||||
//checkTabContext,
|
|
||||||
|
|
||||||
validateRequest(DeleteSupplierByIdRequestSchema, "params"),
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const useCase = deps.build.delete();
|
|
||||||
const controller = new DeleteSupplierController(useCase);
|
|
||||||
return controller.execute(req, res, next);
|
|
||||||
}
|
|
||||||
);*/
|
|
||||||
|
|
||||||
app.use(`${config.server.apiBasePath}/suppliers`, router);
|
app.use(`${config.server.apiBasePath}/suppliers`, router);
|
||||||
};
|
};
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { SequelizeSupplierDomainMapper } from "./sequelize-supplier.mapper";
|
||||||
|
|
||||||
|
const persistedSupplier = {
|
||||||
|
id: "11111111-1111-4111-8111-111111111111",
|
||||||
|
company_id: "22222222-2222-4222-8222-222222222222",
|
||||||
|
reference: null,
|
||||||
|
is_company: false,
|
||||||
|
name: "Proveedor inactivo",
|
||||||
|
trade_name: null,
|
||||||
|
tin: null,
|
||||||
|
street: null,
|
||||||
|
street2: null,
|
||||||
|
city: null,
|
||||||
|
province: null,
|
||||||
|
postal_code: null,
|
||||||
|
country: null,
|
||||||
|
email_primary: null,
|
||||||
|
email_secondary: null,
|
||||||
|
phone_primary: null,
|
||||||
|
phone_secondary: null,
|
||||||
|
mobile_primary: null,
|
||||||
|
mobile_secondary: null,
|
||||||
|
fax: null,
|
||||||
|
website: null,
|
||||||
|
status: "inactive",
|
||||||
|
language_code: "es",
|
||||||
|
currency_code: "EUR",
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildMapper = () => new SequelizeSupplierDomainMapper();
|
||||||
|
|
||||||
|
test("rehidrata identidad, estado, booleanos y opcionales desde persistencia", () => {
|
||||||
|
const result = buildMapper().mapToDomain(persistedSupplier as never);
|
||||||
|
|
||||||
|
assert.equal(result.isSuccess, true);
|
||||||
|
|
||||||
|
const supplier = result.data;
|
||||||
|
assert.equal(supplier.id.toPrimitive(), "11111111-1111-4111-8111-111111111111");
|
||||||
|
assert.equal(supplier.companyId.toPrimitive(), "22222222-2222-4222-8222-222222222222");
|
||||||
|
assert.equal(supplier.isActive, false);
|
||||||
|
assert.equal(supplier.isCompany, false);
|
||||||
|
assert.equal(supplier.reference.isNone(), true);
|
||||||
|
assert.equal(supplier.tradeName.isNone(), true);
|
||||||
|
assert.equal(supplier.mobileSecondary.isNone(), true);
|
||||||
|
assert.equal(supplier.address.country.isNone(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("el round-trip conserva los campos persistibles relevantes", () => {
|
||||||
|
const mapper = buildMapper();
|
||||||
|
const rehydrated = mapper.mapToDomain(persistedSupplier as never);
|
||||||
|
|
||||||
|
assert.equal(rehydrated.isSuccess, true);
|
||||||
|
|
||||||
|
const result = mapper.mapToPersistence(rehydrated.data);
|
||||||
|
|
||||||
|
assert.equal(result.isSuccess, true);
|
||||||
|
assert.equal(result.data.id, "11111111-1111-4111-8111-111111111111");
|
||||||
|
assert.equal(result.data.company_id, "22222222-2222-4222-8222-222222222222");
|
||||||
|
assert.equal(result.data.status, "inactive");
|
||||||
|
assert.equal(result.data.is_company, false);
|
||||||
|
assert.equal(result.data.mobile_secondary, null);
|
||||||
|
assert.equal(result.data.country, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rehidrata status activo e is_company true sin conversiones legacy", () => {
|
||||||
|
const result = buildMapper().mapToDomain({
|
||||||
|
...persistedSupplier,
|
||||||
|
is_company: true,
|
||||||
|
status: "active",
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
assert.equal(result.isSuccess, true);
|
||||||
|
assert.equal(result.data.isActive, true);
|
||||||
|
assert.equal(result.data.isCompany, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rechaza un status persistido fuera del contrato", () => {
|
||||||
|
const result = buildMapper().mapToDomain({
|
||||||
|
...persistedSupplier,
|
||||||
|
status: "pending",
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
assert.equal(result.isFailure, true);
|
||||||
|
});
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import type { TaxCatalogProvider } from "@erp/core";
|
import { SequelizeDomainMapper } from "@erp/core/api";
|
||||||
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
|
||||||
import {
|
import {
|
||||||
City,
|
City,
|
||||||
Country,
|
Country,
|
||||||
@ -23,22 +22,25 @@ import {
|
|||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import { type ISupplierCreateProps, Supplier, SupplierStatus } from "../../../../../domain";
|
import {
|
||||||
|
type SupplierInternalProps,
|
||||||
|
Supplier,
|
||||||
|
SupplierStatus,
|
||||||
|
SUPPLIER_STATUS,
|
||||||
|
} from "../../../../../domain";
|
||||||
import type { SupplierCreationAttributes, SupplierModel } from "../../../sequelize";
|
import type { SupplierCreationAttributes, SupplierModel } from "../../../sequelize";
|
||||||
|
|
||||||
|
const PERSISTED_STATUS_TO_DOMAIN: Readonly<Record<string, SUPPLIER_STATUS>> = {
|
||||||
|
active: SUPPLIER_STATUS.ACTIVE,
|
||||||
|
inactive: SUPPLIER_STATUS.INACTIVE,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
|
export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
|
||||||
SupplierModel,
|
SupplierModel,
|
||||||
SupplierCreationAttributes,
|
SupplierCreationAttributes,
|
||||||
Supplier
|
Supplier
|
||||||
> {
|
> {
|
||||||
private readonly taxCatalog: TaxCatalogProvider;
|
public mapToDomain(source: SupplierModel): Result<Supplier, Error> {
|
||||||
|
|
||||||
constructor(params: { taxCatalog: TaxCatalogProvider }) {
|
|
||||||
super();
|
|
||||||
this.taxCatalog = params.taxCatalog;
|
|
||||||
}
|
|
||||||
|
|
||||||
public mapToDomain(source: SupplierModel, params?: MapperParamsType): Result<Supplier, Error> {
|
|
||||||
try {
|
try {
|
||||||
const errors: ValidationErrorDetail[] = [];
|
const errors: ValidationErrorDetail[] = [];
|
||||||
|
|
||||||
@ -50,7 +52,11 @@ export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isCompany = source.is_company;
|
const isCompany = source.is_company;
|
||||||
const status = extractOrPushError(SupplierStatus.create(source.status), "status", errors);
|
const status = extractOrPushError(
|
||||||
|
SupplierStatus.create(PERSISTED_STATUS_TO_DOMAIN[source.status] ?? source.status),
|
||||||
|
"status",
|
||||||
|
errors
|
||||||
|
);
|
||||||
const reference = extractOrPushError(
|
const reference = extractOrPushError(
|
||||||
maybeFromNullableResult(source.reference, (value) => Name.create(value)),
|
maybeFromNullableResult(source.reference, (value) => Name.create(value)),
|
||||||
"reference",
|
"reference",
|
||||||
@ -188,7 +194,7 @@ export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
|
|||||||
return Result.fail(new ValidationErrorCollection("Supplier props mapping failed", errors));
|
return Result.fail(new ValidationErrorCollection("Supplier props mapping failed", errors));
|
||||||
}
|
}
|
||||||
|
|
||||||
const supplierProps: ISupplierCreateProps = {
|
const supplierProps: SupplierInternalProps = {
|
||||||
companyId: companyId!,
|
companyId: companyId!,
|
||||||
status: status!,
|
status: status!,
|
||||||
reference: reference!,
|
reference: reference!,
|
||||||
@ -213,16 +219,13 @@ export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
|
|||||||
currencyCode: currencyCode!,
|
currencyCode: currencyCode!,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Supplier.create(supplierProps, supplierId);
|
return Result.ok(Supplier.rehydrate(supplierProps, supplierId!));
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
return Result.fail(err as Error);
|
return Result.fail(err as Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public mapToPersistence(
|
public mapToPersistence(source: Supplier): Result<SupplierCreationAttributes, Error> {
|
||||||
source: Supplier,
|
|
||||||
params?: MapperParamsType
|
|
||||||
): Result<SupplierCreationAttributes, Error> {
|
|
||||||
const supplierValues: Partial<SupplierCreationAttributes> = {
|
const supplierValues: Partial<SupplierCreationAttributes> = {
|
||||||
id: source.id.toPrimitive(),
|
id: source.id.toPrimitive(),
|
||||||
company_id: source.companyId.toPrimitive(),
|
company_id: source.companyId.toPrimitive(),
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { SequelizeSupplierSummaryMapper } from "./sequelize-supplier-summary.mapper";
|
||||||
|
|
||||||
|
const persistedSupplier = {
|
||||||
|
id: "11111111-1111-4111-8111-111111111111",
|
||||||
|
company_id: "22222222-2222-4222-8222-222222222222",
|
||||||
|
reference: null,
|
||||||
|
is_company: false,
|
||||||
|
name: "Proveedor listado",
|
||||||
|
trade_name: null,
|
||||||
|
tin: null,
|
||||||
|
street: null,
|
||||||
|
street2: null,
|
||||||
|
city: null,
|
||||||
|
province: null,
|
||||||
|
postal_code: null,
|
||||||
|
country: null,
|
||||||
|
email_primary: null,
|
||||||
|
email_secondary: null,
|
||||||
|
phone_primary: null,
|
||||||
|
phone_secondary: null,
|
||||||
|
mobile_primary: null,
|
||||||
|
mobile_secondary: null,
|
||||||
|
fax: null,
|
||||||
|
website: null,
|
||||||
|
status: "inactive",
|
||||||
|
language_code: "es",
|
||||||
|
currency_code: "EUR",
|
||||||
|
};
|
||||||
|
|
||||||
|
test("normaliza status, booleanos, street2 y nullability en el listado", () => {
|
||||||
|
const result = new SequelizeSupplierSummaryMapper().mapToReadModel({
|
||||||
|
...persistedSupplier,
|
||||||
|
is_company: true,
|
||||||
|
street2: "Planta 2",
|
||||||
|
status: "active",
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
assert.equal(result.isSuccess, true);
|
||||||
|
assert.equal(result.data.isActive, true);
|
||||||
|
assert.equal(result.data.isCompany, true);
|
||||||
|
assert.equal(result.data.address.street2.unwrap().toPrimitive(), "Planta 2");
|
||||||
|
assert.equal(result.data.mobileSecondary.isNone(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rechaza status no persistible en el listado", () => {
|
||||||
|
const result = new SequelizeSupplierSummaryMapper().mapToReadModel({
|
||||||
|
...persistedSupplier,
|
||||||
|
status: "pending",
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
assert.equal(result.isFailure, true);
|
||||||
|
});
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api";
|
import { SequelizeQueryMapper } from "@erp/core/api";
|
||||||
import {
|
import {
|
||||||
City,
|
City,
|
||||||
Country,
|
Country,
|
||||||
@ -22,17 +22,19 @@ import {
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { SupplierSummary } from "../../../../../application";
|
import type { SupplierSummary } from "../../../../../application";
|
||||||
import { SupplierStatus } from "../../../../../domain";
|
import { SupplierStatus, SUPPLIER_STATUS } from "../../../../../domain";
|
||||||
import type { SupplierModel } from "../../models";
|
import type { SupplierModel } from "../../models";
|
||||||
|
|
||||||
|
const PERSISTED_STATUS_TO_DOMAIN: Readonly<Record<string, SUPPLIER_STATUS>> = {
|
||||||
|
active: SUPPLIER_STATUS.ACTIVE,
|
||||||
|
inactive: SUPPLIER_STATUS.INACTIVE,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export class SequelizeSupplierSummaryMapper extends SequelizeQueryMapper<
|
export class SequelizeSupplierSummaryMapper extends SequelizeQueryMapper<
|
||||||
SupplierModel,
|
SupplierModel,
|
||||||
SupplierSummary
|
SupplierSummary
|
||||||
> {
|
> {
|
||||||
public mapToReadModel(
|
public mapToReadModel(raw: SupplierModel): Result<SupplierSummary, Error> {
|
||||||
raw: SupplierModel,
|
|
||||||
params?: MapperParamsType
|
|
||||||
): Result<SupplierSummary, Error> {
|
|
||||||
const errors: ValidationErrorDetail[] = [];
|
const errors: ValidationErrorDetail[] = [];
|
||||||
|
|
||||||
// 1) Valores escalares (atributos generales)
|
// 1) Valores escalares (atributos generales)
|
||||||
@ -40,7 +42,11 @@ export class SequelizeSupplierSummaryMapper extends SequelizeQueryMapper<
|
|||||||
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
|
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
|
||||||
|
|
||||||
const isCompany = raw.is_company;
|
const isCompany = raw.is_company;
|
||||||
const status = extractOrPushError(SupplierStatus.create(raw.status), "status", errors);
|
const status = extractOrPushError(
|
||||||
|
SupplierStatus.create(PERSISTED_STATUS_TO_DOMAIN[raw.status] ?? raw.status),
|
||||||
|
"status",
|
||||||
|
errors
|
||||||
|
);
|
||||||
const reference = extractOrPushError(
|
const reference = extractOrPushError(
|
||||||
maybeFromNullableResult(raw.reference, (value) => Name.create(value)),
|
maybeFromNullableResult(raw.reference, (value) => Name.create(value)),
|
||||||
"reference",
|
"reference",
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user