Compare commits
No commits in common. "34520a18ba1f5474a3949fff2843cbc0d8ccd6fe" and "f1f329fa0d6ec89c99b09633940551b38b8360a7" have entirely different histories.
34520a18ba
...
f1f329fa0d
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/factuges-server",
|
||||
"version": "0.10.5",
|
||||
"version": "0.10.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --config tsup.config.ts",
|
||||
@ -45,7 +45,6 @@
|
||||
"@erp/document-series": "workspace:*",
|
||||
"@erp/identity": "workspace:*",
|
||||
"@erp/factuges": "workspace:*",
|
||||
"@erp/suppliers": "workspace:*",
|
||||
"@repo/rdx-logger": "workspace:*",
|
||||
"@repo/rdx-utils": "workspace:*",
|
||||
"bcrypt": "^6.0.0",
|
||||
@ -79,4 +78,4 @@
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,8 @@ import customersAPIModule from "@erp/customers/api";
|
||||
import documentSeriesAPIModule from "@erp/document-series/api";
|
||||
import factuGESAPIModule from "@erp/factuges/api";
|
||||
import identityAPIModule from "@erp/identity/api";
|
||||
import suppliersAPIModule from "@erp/suppliers/api";
|
||||
|
||||
//import suppliersAPIModule from "@erp/suppliers/api";
|
||||
|
||||
import { registerModule } from "./lib";
|
||||
|
||||
@ -22,5 +23,5 @@ export const registerModules = () => {
|
||||
registerModule(documentSeriesAPIModule);
|
||||
registerModule(customerInvoicesAPIModule);
|
||||
registerModule(factuGESAPIModule);
|
||||
registerModule(suppliersAPIModule);
|
||||
//registerModule(suppliersAPIModule);
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/factuges-web",
|
||||
"private": true,
|
||||
"version": "0.10.5",
|
||||
"version": "0.10.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
@ -36,7 +36,6 @@
|
||||
"@erp/customer-invoices": "workspace:*",
|
||||
"@erp/customers": "workspace:*",
|
||||
"@erp/identity": "workspace:*",
|
||||
"@erp/suppliers": "workspace:*",
|
||||
"@repo/rdx-utils": "workspace:*",
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"@fontsource-variable/geist-mono": "^5.2.7",
|
||||
@ -62,4 +61,4 @@
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"vite-plugin-html": "^3.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,6 @@ import CoreModuleManifest, { type IModuleClient } from "@erp/core/client";
|
||||
import { CustomerInvoicesModuleManifest } from "@erp/customer-invoices/client";
|
||||
import { CustomersModuleManifest } from "@erp/customers/client";
|
||||
import { IdentityModuleManifest } from "@erp/identity/client";
|
||||
import { SuppliersModuleManifest } from "@erp/suppliers/client";
|
||||
|
||||
export const modules: IModuleClient[] = [
|
||||
IdentityModuleManifest,
|
||||
@ -12,6 +11,5 @@ export const modules: IModuleClient[] = [
|
||||
CompaniesModuleManifest,
|
||||
CatalogsModuleManifest,
|
||||
CustomersModuleManifest,
|
||||
SuppliersModuleManifest,
|
||||
CustomerInvoicesModuleManifest,
|
||||
];
|
||||
|
||||
@ -1,174 +0,0 @@
|
||||
# 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.
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/auth",
|
||||
"version": "0.10.5",
|
||||
"version": "0.10.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/catalogs",
|
||||
"description": "Catalogs module",
|
||||
"version": "0.10.5",
|
||||
"version": "0.10.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/companies",
|
||||
"description": "Companies module",
|
||||
"version": "0.10.5",
|
||||
"version": "0.10.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/core",
|
||||
"version": "0.10.5",
|
||||
"version": "0.10.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/customer-invoices",
|
||||
"version": "0.10.5",
|
||||
"version": "0.10.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -83,7 +83,6 @@ test("resolvePaymentTerm normaliza percentage.value cuando llega numérico del c
|
||||
assert.equal(currencyCodeResult.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"]({
|
||||
proforma: {
|
||||
companyId,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/customers",
|
||||
"description": "Customers",
|
||||
"version": "0.10.5",
|
||||
"version": "0.10.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/document-series",
|
||||
"description": "Document series module",
|
||||
"version": "0.10.5",
|
||||
"version": "0.10.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@erp/factuges",
|
||||
"version": "0.10.5",
|
||||
"version": "0.10.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/identity",
|
||||
"description": "Identity module",
|
||||
"version": "0.10.5",
|
||||
"version": "0.10.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/supplier-invoices",
|
||||
"description": "Supplier invoices",
|
||||
"version": "0.10.5",
|
||||
"version": "0.10.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@erp/suppliers",
|
||||
"description": "Suppliers",
|
||||
"version": "0.10.5",
|
||||
"version": "0.10.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
@ -1,3 +1,5 @@
|
||||
import type { ICatalogs } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
CreateSupplierInputMapper,
|
||||
type ICreateSupplierInputMapper,
|
||||
@ -10,9 +12,11 @@ export interface ISupplierInputMappers {
|
||||
updateInputMapper: IUpdateSupplierInputMapper;
|
||||
}
|
||||
|
||||
export const buildSupplierInputMappers = (): ISupplierInputMappers => {
|
||||
export const buildSupplierInputMappers = (catalogs: ICatalogs): ISupplierInputMappers => {
|
||||
const { taxCatalog } = catalogs;
|
||||
|
||||
// Mappers el DTO a las props validadas (SupplierProps) y luego construir agregado
|
||||
const createInputMapper = new CreateSupplierInputMapper();
|
||||
const createInputMapper = new CreateSupplierInputMapper({ taxCatalog });
|
||||
const updateInputMapper = new UpdateSupplierInputMapper();
|
||||
|
||||
return {
|
||||
@ -48,7 +48,6 @@ export function buildCreateSupplierUseCase(deps: {
|
||||
}
|
||||
|
||||
export function buildUpdateSupplierUseCase(deps: {
|
||||
finder: ISupplierFinder;
|
||||
updater: ISupplierUpdater;
|
||||
dtoMapper: IUpdateSupplierInputMapper;
|
||||
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||
@ -56,9 +55,41 @@ export function buildUpdateSupplierUseCase(deps: {
|
||||
}) {
|
||||
return new UpdateSupplierUseCase({
|
||||
dtoMapper: deps.dtoMapper,
|
||||
finder: deps.finder,
|
||||
updater: deps.updater,
|
||||
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||
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,3 +1,4 @@
|
||||
import type { JsonTaxCatalogProvider } from "@erp/core";
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
@ -21,142 +22,148 @@ import {
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CreateSupplierRequestDTO } from "../../../common";
|
||||
import { type ISupplierCreateProps, SupplierStatus } from "../../domain";
|
||||
import type { CreateSupplierCommand } from "../use-cases/create-supplier.command";
|
||||
|
||||
export interface ICreateSupplierInputMapper {
|
||||
map(
|
||||
command: CreateSupplierCommand,
|
||||
dto: CreateSupplierRequestDTO,
|
||||
params: { companyId: UniqueID }
|
||||
): Result<{ id: UniqueID; props: ISupplierCreateProps }>;
|
||||
}
|
||||
|
||||
export class CreateSupplierInputMapper implements ICreateSupplierInputMapper {
|
||||
private readonly taxCatalog: JsonTaxCatalogProvider;
|
||||
|
||||
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
|
||||
this.taxCatalog = params.taxCatalog;
|
||||
}
|
||||
|
||||
public map(
|
||||
command: CreateSupplierCommand,
|
||||
dto: CreateSupplierRequestDTO,
|
||||
params: { companyId: UniqueID }
|
||||
): Result<{ id: UniqueID; props: ISupplierCreateProps }> {
|
||||
try {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
const { companyId } = params;
|
||||
|
||||
const supplierId = extractOrPushError(UniqueID.create(command.supplierId), "id", errors);
|
||||
const supplierId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
|
||||
|
||||
const status = SupplierStatus.active();
|
||||
const isCompany = command.isCompany;
|
||||
const isCompany = dto.is_company === "true";
|
||||
|
||||
const reference = extractOrPushError(
|
||||
maybeFromNullableResult(command.reference, (value) => Name.create(value)),
|
||||
maybeFromNullableResult(dto.reference, (value) => Name.create(value)),
|
||||
"reference",
|
||||
errors
|
||||
);
|
||||
|
||||
const name = extractOrPushError(Name.create(command.name), "name", errors);
|
||||
const name = extractOrPushError(Name.create(dto.name), "name", errors);
|
||||
|
||||
const tradeName = extractOrPushError(
|
||||
maybeFromNullableResult(command.tradeName, (value) => Name.create(value)),
|
||||
maybeFromNullableResult(dto.trade_name, (value) => Name.create(value)),
|
||||
"trade_name",
|
||||
errors
|
||||
);
|
||||
|
||||
const tinNumber = extractOrPushError(
|
||||
maybeFromNullableResult(command.tin, (value) => TINNumber.create(value)),
|
||||
maybeFromNullableResult(dto.tin, (value) => TINNumber.create(value)),
|
||||
"tin",
|
||||
errors
|
||||
);
|
||||
|
||||
const street = extractOrPushError(
|
||||
maybeFromNullableResult(command.street, (value) => Street.create(value)),
|
||||
maybeFromNullableResult(dto.street, (value) => Street.create(value)),
|
||||
"street",
|
||||
errors
|
||||
);
|
||||
|
||||
const street2 = extractOrPushError(
|
||||
maybeFromNullableResult(command.street2, (value) => Street.create(value)),
|
||||
maybeFromNullableResult(dto.street2, (value) => Street.create(value)),
|
||||
"street2",
|
||||
errors
|
||||
);
|
||||
|
||||
const city = extractOrPushError(
|
||||
maybeFromNullableResult(command.city, (value) => City.create(value)),
|
||||
maybeFromNullableResult(dto.city, (value) => City.create(value)),
|
||||
"city",
|
||||
errors
|
||||
);
|
||||
|
||||
const province = extractOrPushError(
|
||||
maybeFromNullableResult(command.province, (value) => Province.create(value)),
|
||||
maybeFromNullableResult(dto.province, (value) => Province.create(value)),
|
||||
"province",
|
||||
errors
|
||||
);
|
||||
|
||||
const postalCode = extractOrPushError(
|
||||
maybeFromNullableResult(command.postalCode, (value) => PostalCode.create(value)),
|
||||
maybeFromNullableResult(dto.postal_code, (value) => PostalCode.create(value)),
|
||||
"postal_code",
|
||||
errors
|
||||
);
|
||||
|
||||
const country = extractOrPushError(
|
||||
maybeFromNullableResult(command.country, (value) => Country.create(value)),
|
||||
maybeFromNullableResult(dto.country, (value) => Country.create(value)),
|
||||
"country",
|
||||
errors
|
||||
);
|
||||
|
||||
const primaryEmailAddress = extractOrPushError(
|
||||
maybeFromNullableResult(command.emailPrimary, (value) => EmailAddress.create(value)),
|
||||
maybeFromNullableResult(dto.email_primary, (value) => EmailAddress.create(value)),
|
||||
"email_primary",
|
||||
errors
|
||||
);
|
||||
|
||||
const secondaryEmailAddress = extractOrPushError(
|
||||
maybeFromNullableResult(command.emailSecondary, (value) => EmailAddress.create(value)),
|
||||
maybeFromNullableResult(dto.email_secondary, (value) => EmailAddress.create(value)),
|
||||
"email_secondary",
|
||||
errors
|
||||
);
|
||||
|
||||
const primaryPhoneNumber = extractOrPushError(
|
||||
maybeFromNullableResult(command.phonePrimary, (value) => PhoneNumber.create(value)),
|
||||
maybeFromNullableResult(dto.phone_primary, (value) => PhoneNumber.create(value)),
|
||||
"phone_primary",
|
||||
errors
|
||||
);
|
||||
|
||||
const secondaryPhoneNumber = extractOrPushError(
|
||||
maybeFromNullableResult(command.phoneSecondary, (value) => PhoneNumber.create(value)),
|
||||
maybeFromNullableResult(dto.phone_secondary, (value) => PhoneNumber.create(value)),
|
||||
"phone_secondary",
|
||||
errors
|
||||
);
|
||||
|
||||
const primaryMobileNumber = extractOrPushError(
|
||||
maybeFromNullableResult(command.mobilePrimary, (value) => PhoneNumber.create(value)),
|
||||
maybeFromNullableResult(dto.mobile_primary, (value) => PhoneNumber.create(value)),
|
||||
"mobile_primary",
|
||||
errors
|
||||
);
|
||||
|
||||
const secondaryMobileNumber = extractOrPushError(
|
||||
maybeFromNullableResult(command.mobileSecondary, (value) => PhoneNumber.create(value)),
|
||||
maybeFromNullableResult(dto.mobile_secondary, (value) => PhoneNumber.create(value)),
|
||||
"mobile_secondary",
|
||||
errors
|
||||
);
|
||||
|
||||
const faxNumber = extractOrPushError(
|
||||
maybeFromNullableResult(command.fax, (value) => PhoneNumber.create(value)),
|
||||
maybeFromNullableResult(dto.fax, (value) => PhoneNumber.create(value)),
|
||||
"fax",
|
||||
errors
|
||||
);
|
||||
|
||||
const website = extractOrPushError(
|
||||
maybeFromNullableResult(command.website, (value) => URLAddress.create(value)),
|
||||
maybeFromNullableResult(dto.website, (value) => URLAddress.create(value)),
|
||||
"website",
|
||||
errors
|
||||
);
|
||||
|
||||
const languageCode = extractOrPushError(
|
||||
LanguageCode.create(command.languageCode),
|
||||
LanguageCode.create(dto.language_code),
|
||||
"language_code",
|
||||
errors
|
||||
);
|
||||
|
||||
const currencyCode = extractOrPushError(
|
||||
CurrencyCode.create(command.currencyCode),
|
||||
CurrencyCode.create(dto.currency_code),
|
||||
"currency_code",
|
||||
errors
|
||||
);
|
||||
@ -21,15 +21,17 @@ import {
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
||||
|
||||
import type { UpdateSupplierByIdRequestDTO } from "../../../common";
|
||||
import type { SupplierPatchProps } from "../../domain";
|
||||
import type { UpdateSupplierPatchInput } from "../use-cases/update-supplier.command";
|
||||
|
||||
/**
|
||||
* UpdateSupplierInputMapper
|
||||
* Convierte el DTO a las props validadas (SupplierProps).
|
||||
* No construye directamente el agregado.
|
||||
* Los campos omitidos no se modifican. El DTO HTTP actual todavía no expresa
|
||||
* borrado explícito mediante null; esa semántica se incorporará al modernizar el contrato.
|
||||
* Tri-estado:
|
||||
* - campo omitido → no se cambia
|
||||
* - 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
|
||||
* @returns Cambios en las propiedades del cliente
|
||||
@ -38,13 +40,13 @@ import type { UpdateSupplierPatchInput } from "../use-cases/update-supplier.comm
|
||||
|
||||
export interface IUpdateSupplierInputMapper {
|
||||
map(
|
||||
dto: UpdateSupplierPatchInput,
|
||||
dto: UpdateSupplierByIdRequestDTO,
|
||||
params: { companyId: UniqueID }
|
||||
): Result<SupplierPatchProps>;
|
||||
}
|
||||
|
||||
export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
||||
public map(dto: UpdateSupplierPatchInput, _params: { companyId: UniqueID }) {
|
||||
public map(dto: UpdateSupplierByIdRequestDTO, params: { companyId: UniqueID }) {
|
||||
try {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
const supplierPatchProps: SupplierPatchProps = {};
|
||||
@ -57,8 +59,16 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.isCompany).ifSet((isCompany) => {
|
||||
supplierPatchProps.isCompany = isCompany;
|
||||
toPatchField(dto.is_company).ifSet((is_company) => {
|
||||
if (isNullishOrEmpty(is_company)) {
|
||||
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) => {
|
||||
@ -69,9 +79,9 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
||||
supplierPatchProps.name = extractOrPushError(Name.create(name!), "name", errors);
|
||||
});
|
||||
|
||||
toPatchField(dto.tradeName).ifSet((tradeName) => {
|
||||
toPatchField(dto.trade_name).ifSet((trade_name) => {
|
||||
supplierPatchProps.tradeName = extractOrPushError(
|
||||
maybeFromNullableResult(tradeName, (value) => Name.create(value)),
|
||||
maybeFromNullableResult(trade_name, (value) => Name.create(value)),
|
||||
"trade_name",
|
||||
errors
|
||||
);
|
||||
@ -85,49 +95,49 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.emailPrimary).ifSet((emailPrimary) => {
|
||||
toPatchField(dto.email_primary).ifSet((email_primary) => {
|
||||
supplierPatchProps.emailPrimary = extractOrPushError(
|
||||
maybeFromNullableResult(emailPrimary, (value) => EmailAddress.create(value)),
|
||||
maybeFromNullableResult(email_primary, (value) => EmailAddress.create(value)),
|
||||
"email_primary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.emailSecondary).ifSet((emailSecondary) => {
|
||||
toPatchField(dto.email_secondary).ifSet((email_secondary) => {
|
||||
supplierPatchProps.emailSecondary = extractOrPushError(
|
||||
maybeFromNullableResult(emailSecondary, (value) => EmailAddress.create(value)),
|
||||
maybeFromNullableResult(email_secondary, (value) => EmailAddress.create(value)),
|
||||
"email_secondary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.mobilePrimary).ifSet((mobilePrimary) => {
|
||||
toPatchField(dto.mobile_primary).ifSet((mobile_primary) => {
|
||||
supplierPatchProps.mobilePrimary = extractOrPushError(
|
||||
maybeFromNullableResult(mobilePrimary, (value) => PhoneNumber.create(value)),
|
||||
maybeFromNullableResult(mobile_primary, (value) => PhoneNumber.create(value)),
|
||||
"mobile_primary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.mobileSecondary).ifSet((mobileSecondary) => {
|
||||
supplierPatchProps.mobileSecondary = extractOrPushError(
|
||||
maybeFromNullableResult(mobileSecondary, (value) => PhoneNumber.create(value)),
|
||||
toPatchField(dto.mobile_secondary).ifSet((mobile_secondary) => {
|
||||
supplierPatchProps.mobilePrimary = extractOrPushError(
|
||||
maybeFromNullableResult(mobile_secondary, (value) => PhoneNumber.create(value)),
|
||||
"mobile_secondary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.phonePrimary).ifSet((phonePrimary) => {
|
||||
toPatchField(dto.phone_primary).ifSet((phone_primary) => {
|
||||
supplierPatchProps.phonePrimary = extractOrPushError(
|
||||
maybeFromNullableResult(phonePrimary, (value) => PhoneNumber.create(value)),
|
||||
maybeFromNullableResult(phone_primary, (value) => PhoneNumber.create(value)),
|
||||
"phone_primary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.phoneSecondary).ifSet((phoneSecondary) => {
|
||||
toPatchField(dto.phone_secondary).ifSet((phone_secondary) => {
|
||||
supplierPatchProps.phoneSecondary = extractOrPushError(
|
||||
maybeFromNullableResult(phoneSecondary, (value) => PhoneNumber.create(value)),
|
||||
maybeFromNullableResult(phone_secondary, (value) => PhoneNumber.create(value)),
|
||||
"phone_secondary",
|
||||
errors
|
||||
);
|
||||
@ -149,7 +159,7 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.languageCode).ifSet((languageCode) => {
|
||||
toPatchField(dto.language_code).ifSet((languageCode) => {
|
||||
if (isNullishOrEmpty(languageCode)) {
|
||||
errors.push({ path: "language_code", message: "Language code cannot be empty" });
|
||||
return;
|
||||
@ -162,7 +172,7 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.currencyCode).ifSet((currencyCode) => {
|
||||
toPatchField(dto.currency_code).ifSet((currencyCode) => {
|
||||
if (isNullishOrEmpty(currencyCode)) {
|
||||
errors.push({ path: "currency_code", message: "Currency code cannot be empty" });
|
||||
return;
|
||||
@ -194,7 +204,7 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
||||
}
|
||||
|
||||
public mapPostalAddress(
|
||||
dto: UpdateSupplierPatchInput,
|
||||
dto: UpdateSupplierByIdRequestDTO,
|
||||
errors: ValidationErrorDetail[]
|
||||
): PostalAddressPatchProps | undefined {
|
||||
const postalAddressPatchProps: PostalAddressPatchProps = {};
|
||||
@ -231,7 +241,7 @@ export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.postalCode).ifSet((postalCode) => {
|
||||
toPatchField(dto.postal_code).ifSet((postalCode) => {
|
||||
postalAddressPatchProps.postalCode = extractOrPushError(
|
||||
maybeFromNullableResult(postalCode, (value) => PostalCode.create(value)),
|
||||
"postal_code",
|
||||
@ -69,4 +69,14 @@ export interface ISupplierRepository {
|
||||
transaction: unknown
|
||||
): 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,3 +1,4 @@
|
||||
export * from "./supplier-creator.service";
|
||||
export * from "./supplier-finder.service";
|
||||
export * from "./supplier-public-services.interface";
|
||||
export * from "./supplier-updater.service";
|
||||
@ -0,0 +1,22 @@
|
||||
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,3 +1,4 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { Supplier, SupplierPatchProps } from "../../domain";
|
||||
@ -5,7 +6,8 @@ import type { ISupplierRepository } from "../repositories";
|
||||
|
||||
export interface ISupplierUpdater {
|
||||
update(params: {
|
||||
supplier: Supplier;
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: SupplierPatchProps;
|
||||
transaction: unknown;
|
||||
}): Promise<Result<Supplier, Error>>;
|
||||
@ -23,11 +25,21 @@ export class SupplierUpdater implements ISupplierUpdater {
|
||||
}
|
||||
|
||||
async update(params: {
|
||||
supplier: Supplier;
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: SupplierPatchProps;
|
||||
transaction: unknown;
|
||||
}): Promise<Result<Supplier, Error>> {
|
||||
const { supplier, props, transaction } = params;
|
||||
const { companyId, id, 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
|
||||
const updateResult = supplier.update(props);
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./supplier-snapshot.interface";
|
||||
export * from "./supplier-snapshot-builder";
|
||||
@ -2,13 +2,14 @@ import type { ISnapshotBuilder } from "@erp/core/api";
|
||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
||||
|
||||
import type { Supplier } from "../../../domain";
|
||||
import type { SupplierDetailDTO } from "../../../../common";
|
||||
|
||||
import type { ISupplierFullSnapshot } from "./supplier-snapshot.interface";
|
||||
|
||||
export interface ISupplierFullSnapshotBuilder
|
||||
extends ISnapshotBuilder<Supplier, SupplierDetailDTO> {}
|
||||
extends ISnapshotBuilder<Supplier, ISupplierFullSnapshot> {}
|
||||
|
||||
export class SupplierFullSnapshotBuilder implements ISupplierFullSnapshotBuilder {
|
||||
toOutput(supplier: Supplier): SupplierDetailDTO {
|
||||
toOutput(supplier: Supplier): ISupplierFullSnapshot {
|
||||
const address = supplier.address.toPrimitive();
|
||||
|
||||
return {
|
||||
@ -17,7 +18,7 @@ export class SupplierFullSnapshotBuilder implements ISupplierFullSnapshotBuilder
|
||||
status: supplier.isActive ? "active" : "inactive",
|
||||
reference: maybeToEmptyString(supplier.reference, (value) => value.toPrimitive()),
|
||||
|
||||
is_company: supplier.isCompany,
|
||||
is_company: String(supplier.isCompany),
|
||||
name: supplier.name.toPrimitive(),
|
||||
trade_name: maybeToEmptyString(supplier.tradeName, (value) => value.toPrimitive()),
|
||||
tin: maybeToEmptyString(supplier.tin, (value) => value.toPrimitive()),
|
||||
@ -0,0 +1,35 @@
|
||||
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>;
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./supplier-summary-snapshot.interface";
|
||||
export * from "./supplier-summary-snapshot-builder";
|
||||
@ -2,13 +2,14 @@ import type { ISnapshotBuilder } from "@erp/core/api";
|
||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
||||
|
||||
import type { SupplierSummary } from "../../models";
|
||||
import type { SupplierSummaryDTO } from "../../../../common";
|
||||
|
||||
import type { ISupplierSummarySnapshot } from "./supplier-summary-snapshot.interface";
|
||||
|
||||
export interface ISupplierSummarySnapshotBuilder
|
||||
extends ISnapshotBuilder<SupplierSummary, SupplierSummaryDTO> {}
|
||||
extends ISnapshotBuilder<SupplierSummary, ISupplierSummarySnapshot> {}
|
||||
|
||||
export class SupplierSummarySnapshotBuilder implements ISupplierSummarySnapshotBuilder {
|
||||
toOutput(supplier: SupplierSummary): SupplierSummaryDTO {
|
||||
toOutput(supplier: SupplierSummary): ISupplierSummarySnapshot {
|
||||
const { address } = supplier;
|
||||
|
||||
return {
|
||||
@ -17,7 +18,7 @@ export class SupplierSummarySnapshotBuilder implements ISupplierSummarySnapshotB
|
||||
status: supplier.isActive ? "active" : "inactive",
|
||||
reference: maybeToEmptyString(supplier.reference, (value) => value.toString()),
|
||||
|
||||
is_company: supplier.isCompany,
|
||||
is_company: String(supplier.isCompany),
|
||||
name: supplier.name.toString(),
|
||||
trade_name: maybeToEmptyString(supplier.tradeName, (value) => value.toString()),
|
||||
tin: maybeToEmptyString(supplier.tin, (value) => value.toString()),
|
||||
@ -0,0 +1,35 @@
|
||||
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>;
|
||||
};
|
||||
@ -2,14 +2,16 @@ import type { ITransactionManager } from "@erp/core/api";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { ICreateSupplierInputMapper } from "../mappers";
|
||||
import type { CreateSupplierRequestDTO } from "../../../common";
|
||||
import type { ISupplierCreator } from "../services";
|
||||
import type { ISupplierFullSnapshotBuilder } from "../snapshot-builders";
|
||||
import type { CreateSupplierCommand } from "./create-supplier.command";
|
||||
import type {
|
||||
ICreateSupplierInputMapper,
|
||||
ISupplierFullSnapshotBuilder,
|
||||
} from "../snapshot-builders";
|
||||
|
||||
type CreateSupplierUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
command: CreateSupplierCommand;
|
||||
dto: CreateSupplierRequestDTO;
|
||||
};
|
||||
|
||||
type CreateSupplierUseCaseDeps = {
|
||||
@ -33,9 +35,9 @@ export class CreateSupplierUseCase {
|
||||
}
|
||||
|
||||
public execute(params: CreateSupplierUseCaseInput) {
|
||||
const { command, companyId } = params;
|
||||
const { dto, companyId } = params;
|
||||
|
||||
const mappedPropsResult = this.dtoMapper.map(command, { companyId });
|
||||
const mappedPropsResult = this.dtoMapper.map(dto, { companyId });
|
||||
if (mappedPropsResult.isFailure) {
|
||||
return mappedPropsResult;
|
||||
}
|
||||
6
modules/supplier/src/api/application/use-cases/index.ts
Normal file
6
modules/supplier/src/api/application/use-cases/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
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';
|
||||
93
modules/supplier/src/api/create-supplier-module.sh
Normal file
93
modules/supplier/src/api/create-supplier-module.sh
Normal file
@ -0,0 +1,93 @@
|
||||
#!/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"
|
||||
@ -5,9 +5,9 @@ import {
|
||||
type LanguageCode,
|
||||
type Name,
|
||||
type PhoneNumber,
|
||||
type PostalAddress,
|
||||
type PostalAddressPatchProps,
|
||||
type PostalAddressProps,
|
||||
PostalAddress,
|
||||
type TINNumber,
|
||||
type URLAddress,
|
||||
type UniqueID,
|
||||
@ -87,9 +87,8 @@ export interface ISupplier {
|
||||
readonly currencyCode: CurrencyCode;
|
||||
}
|
||||
|
||||
export type SupplierInternalProps = Omit<ISupplierCreateProps, "address" | "status"> & {
|
||||
address: PostalAddress;
|
||||
status: SupplierStatus;
|
||||
type SupplierInternalProps = Omit<ISupplierCreateProps, "address"> & {
|
||||
readonly address: PostalAddress;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -115,7 +114,6 @@ export class Supplier extends AggregateRoot<SupplierInternalProps> implements IS
|
||||
{
|
||||
...internalProps,
|
||||
address: postalAddressResult.data,
|
||||
status: props.status ?? SupplierStatus.active(),
|
||||
},
|
||||
id
|
||||
);
|
||||
@ -131,7 +129,7 @@ export class Supplier extends AggregateRoot<SupplierInternalProps> implements IS
|
||||
return Result.ok(contact);
|
||||
}
|
||||
|
||||
private static validateCreateProps(_props: ISupplierCreateProps): Result<void, Error> {
|
||||
private static validateCreateProps(props: ISupplierCreateProps): Result<void, Error> {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@ -146,27 +144,18 @@ export class Supplier extends AggregateRoot<SupplierInternalProps> implements IS
|
||||
|
||||
public update(partialSupplier: SupplierPatchProps): Result<Supplier, Error> {
|
||||
const { address: partialAddress, ...rest } = partialSupplier;
|
||||
const nextProps: SupplierInternalProps = {
|
||||
...this.props,
|
||||
...rest,
|
||||
};
|
||||
|
||||
Object.assign(this.props, rest);
|
||||
|
||||
if (partialAddress) {
|
||||
const addressResult = PostalAddress.create({
|
||||
...this.address.getProps(),
|
||||
...partialAddress,
|
||||
});
|
||||
const addressResult = this.address.update(partialAddress);
|
||||
|
||||
if (addressResult.isFailure) {
|
||||
return Result.fail(addressResult.error);
|
||||
}
|
||||
|
||||
nextProps.address = addressResult.data;
|
||||
}
|
||||
|
||||
Object.assign(this.props, nextProps);
|
||||
|
||||
return Result.ok(this);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
public activate(): Result<Supplier, Error> {
|
||||
@ -1,7 +1,10 @@
|
||||
import type { IModuleServer } from "@erp/core/api";
|
||||
|
||||
import type { ISupplierPublicServices } from "./application";
|
||||
import { models, suppliersRouter } from "./infrastructure";
|
||||
import { buildSuppliersDependencies } from "./infrastructure/di";
|
||||
import { buildSupplierPublicServices, buildSuppliersDependencies } from "./infrastructure/di";
|
||||
|
||||
export * from "./infrastructure/persistence/sequelize";
|
||||
|
||||
export const suppliersAPIModule: IModuleServer = {
|
||||
name: "suppliers",
|
||||
@ -16,11 +19,17 @@ export const suppliersAPIModule: IModuleServer = {
|
||||
* - NO conecta infraestructura
|
||||
*/
|
||||
async setup(params) {
|
||||
const { logger } = params;
|
||||
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
||||
|
||||
// El grafo se construye una sola vez y permanece privado al módulo.
|
||||
// 1) Dominio interno
|
||||
const internal = buildSuppliersDependencies(params);
|
||||
|
||||
// 2) Servicios públicos (Application Services)
|
||||
const suppliersServices: ISupplierPublicServices = buildSupplierPublicServices(
|
||||
params,
|
||||
internal
|
||||
);
|
||||
|
||||
logger.info("🚀 Suppliers module dependencies registered", {
|
||||
label: this.name,
|
||||
});
|
||||
@ -29,6 +38,11 @@ export const suppliersAPIModule: IModuleServer = {
|
||||
// Modelos Sequelize del módulo
|
||||
models,
|
||||
|
||||
// Servicios expuestos a otros módulos
|
||||
services: {
|
||||
general: suppliersServices, // 'suppliers:general'
|
||||
},
|
||||
|
||||
// Implementación privada del módulo
|
||||
internal,
|
||||
};
|
||||
@ -42,7 +56,7 @@ export const suppliersAPIModule: IModuleServer = {
|
||||
* - NO construye dominio
|
||||
*/
|
||||
async start(params) {
|
||||
const { logger } = params;
|
||||
const { app, baseRoutePath, logger, getInternal } = params;
|
||||
|
||||
// Registro de rutas HTTP
|
||||
suppliersRouter(params);
|
||||
2
modules/supplier/src/api/infrastructure/di/index.ts
Normal file
2
modules/supplier/src/api/infrastructure/di/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./supplier-public-services";
|
||||
export * from "./suppliers.di";
|
||||
@ -1,3 +1,5 @@
|
||||
import type { ICatalogs } from "@erp/core/api";
|
||||
|
||||
import { SequelizeSupplierDomainMapper, SequelizeSupplierSummaryMapper } from "../persistence";
|
||||
|
||||
export interface ISupplierPersistenceMappers {
|
||||
@ -7,9 +9,13 @@ export interface ISupplierPersistenceMappers {
|
||||
//createMapper: CreateSupplierInputMapper;
|
||||
}
|
||||
|
||||
export const buildSupplierPersistenceMappers = (): ISupplierPersistenceMappers => {
|
||||
export const buildSupplierPersistenceMappers = (
|
||||
catalogs: ICatalogs
|
||||
): ISupplierPersistenceMappers => {
|
||||
const { taxCatalog } = catalogs;
|
||||
|
||||
// Mappers para el repositorio
|
||||
const domainMapper = new SequelizeSupplierDomainMapper();
|
||||
const domainMapper = new SequelizeSupplierDomainMapper({ taxCatalog });
|
||||
const summaryMapper = new SequelizeSupplierSummaryMapper();
|
||||
|
||||
// Mappers el DTO a las props validadas (SupplierProps) y luego construir agregado
|
||||
@ -0,0 +1,65 @@
|
||||
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,19 +1,17 @@
|
||||
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
||||
import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
type CreateSupplierUseCase,
|
||||
type GetSupplierByIdUseCase,
|
||||
type ListSuppliersUseCase,
|
||||
type UpdateSupplierUseCase,
|
||||
buildCreateSupplierUseCase,
|
||||
buildGetSupplierByIdUseCase,
|
||||
buildListSuppliersUseCase,
|
||||
buildSupplierUpdater,
|
||||
buildUpdateSupplierUseCase,
|
||||
buildSupplierCreator,
|
||||
buildSupplierFinder,
|
||||
buildSupplierInputMappers,
|
||||
buildSupplierSnapshotBuilders,
|
||||
buildSupplierUpdater,
|
||||
} from "../../application";
|
||||
|
||||
import { buildSupplierPersistenceMappers } from "./supplier-persistence-mappers.di";
|
||||
@ -24,7 +22,12 @@ export type SuppliersInternalDeps = {
|
||||
listSuppliers: () => ListSuppliersUseCase;
|
||||
getSupplierById: () => GetSupplierByIdUseCase;
|
||||
createSupplier: () => CreateSupplierUseCase;
|
||||
updateSupplier: () => UpdateSupplierUseCase;
|
||||
//updateSupplier: () => UpdateSupplierUseCase;
|
||||
|
||||
/*
|
||||
|
||||
deleteSupplier: () => DeleteSupplierUseCase;
|
||||
changeStatusSupplier: () => ChangeStatusSupplierUseCase;*/
|
||||
};
|
||||
};
|
||||
|
||||
@ -33,17 +36,20 @@ export function buildSuppliersDependencies(params: ModuleParams): SuppliersInter
|
||||
|
||||
// Infrastructure
|
||||
const transactionManager = buildTransactionManager(database);
|
||||
const persistenceMappers = buildSupplierPersistenceMappers();
|
||||
const catalogs = buildCatalogs();
|
||||
const persistenceMappers = buildSupplierPersistenceMappers(catalogs);
|
||||
|
||||
const repository = buildSupplierRepository({ database, mappers: persistenceMappers });
|
||||
//const numberService = buildSupplierNumberGenerator();
|
||||
|
||||
// Application helpers
|
||||
const inputMappers = buildSupplierInputMappers();
|
||||
const inputMappers = buildSupplierInputMappers(catalogs);
|
||||
const finder = buildSupplierFinder({ repository });
|
||||
const creator = buildSupplierCreator({ repository });
|
||||
const updater = buildSupplierUpdater({ repository });
|
||||
|
||||
const snapshotBuilders = buildSupplierSnapshotBuilders();
|
||||
//const documentGeneratorPipeline = buildSupplierDocumentService(params);
|
||||
|
||||
// Internal use cases (factories)
|
||||
return {
|
||||
@ -70,14 +76,13 @@ export function buildSuppliersDependencies(params: ModuleParams): SuppliersInter
|
||||
transactionManager,
|
||||
}),
|
||||
|
||||
updateSupplier: () =>
|
||||
/*updateSupplier: () =>
|
||||
buildUpdateSupplierUseCase({
|
||||
finder,
|
||||
updater,
|
||||
dtoMapper: inputMappers.updateInputMapper,
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
transactionManager,
|
||||
}),
|
||||
}),*/
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -8,13 +8,9 @@ import {
|
||||
import type { CreateSupplierRequestDTO } from "../../../../common/dto";
|
||||
import type { CreateSupplierUseCase } from "../../../application";
|
||||
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
||||
import type { CreateSupplierRequestMapper } from "../mappers/create-supplier-request.mapper";
|
||||
|
||||
export class CreateSupplierController extends ExpressController {
|
||||
public constructor(
|
||||
private readonly useCase: CreateSupplierUseCase,
|
||||
private readonly requestMapper: CreateSupplierRequestMapper
|
||||
) {
|
||||
public constructor(private readonly useCase: CreateSupplierUseCase) {
|
||||
super();
|
||||
this.errorMapper = suppliersApiErrorMapper;
|
||||
|
||||
@ -32,9 +28,8 @@ export class CreateSupplierController extends ExpressController {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
const dto = this.req.body as CreateSupplierRequestDTO;
|
||||
const command = this.requestMapper.map(dto);
|
||||
|
||||
const result = await this.useCase.execute({ command, companyId });
|
||||
const result = await this.useCase.execute({ dto, companyId });
|
||||
|
||||
return result.match(
|
||||
(data) => this.created(data),
|
||||
@ -0,0 +1,38 @@
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -5,11 +5,11 @@ import {
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { GetSupplierByIdUseCase } from "../../../application";
|
||||
import type { GetSupplierUseCase } from "../../../application";
|
||||
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
||||
|
||||
export class GetSupplierController extends ExpressController {
|
||||
public constructor(private readonly useCase: GetSupplierByIdUseCase) {
|
||||
public constructor(private readonly useCase: GetSupplierUseCase) {
|
||||
super();
|
||||
this.errorMapper = suppliersApiErrorMapper;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export * from "./create-supplier.controller";
|
||||
export * from "./delete-supplier.controller";
|
||||
export * from "./get-supplier.controller";
|
||||
export * from "./list-suppliers.controller";
|
||||
export * from "./update-supplier.controller";
|
||||
@ -8,13 +8,9 @@ import {
|
||||
import type { UpdateSupplierByIdRequestDTO } from "../../../../common/dto";
|
||||
import type { UpdateSupplierUseCase } from "../../../application";
|
||||
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
||||
import type { UpdateSupplierByIdRequestMapper } from "../mappers/update-supplier-by-id-request.mapper";
|
||||
|
||||
export class UpdateSupplierController extends ExpressController {
|
||||
public constructor(
|
||||
private readonly useCase: UpdateSupplierUseCase,
|
||||
private readonly requestMapper: UpdateSupplierByIdRequestMapper
|
||||
) {
|
||||
public constructor(private readonly useCase: UpdateSupplierUseCase) {
|
||||
super();
|
||||
this.errorMapper = suppliersApiErrorMapper;
|
||||
|
||||
@ -33,9 +29,8 @@ export class UpdateSupplierController extends ExpressController {
|
||||
}
|
||||
const { supplier_id } = this.req.params;
|
||||
const dto = this.req.body as UpdateSupplierByIdRequestDTO;
|
||||
const command = this.requestMapper.map({ supplierId: supplier_id, dto });
|
||||
|
||||
const result = await this.useCase.execute({ companyId, command });
|
||||
const result = await this.useCase.execute({ supplier_id, companyId, dto });
|
||||
|
||||
return result.match(
|
||||
(data) => this.ok(data),
|
||||
@ -17,8 +17,6 @@ import {
|
||||
ListSuppliersController,
|
||||
UpdateSupplierController,
|
||||
} 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) => {
|
||||
const { app, config, getInternal } = params;
|
||||
@ -80,22 +78,35 @@ export const suppliersRouter = (params: StartParams) => {
|
||||
validateRequest(CreateSupplierRequestSchema, "body"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.createSupplier();
|
||||
const controller = new CreateSupplierController(useCase, new CreateSupplierRequestMapper());
|
||||
const controller = new CreateSupplierController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
router.put(
|
||||
"/:supplier_id",
|
||||
//checkTabContext,
|
||||
|
||||
validateRequest(UpdateSupplierByIdParamsRequestSchema, "params"),
|
||||
validateRequest(UpdateSupplierByIdRequestSchema, "body"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const controller = new UpdateSupplierController(
|
||||
deps.useCases.updateSupplier(),
|
||||
new UpdateSupplierByIdRequestMapper()
|
||||
);
|
||||
const useCase = deps.useCases.updateSupplier();
|
||||
const controller = new UpdateSupplierController(useCase);
|
||||
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);
|
||||
};
|
||||
@ -1,4 +1,5 @@
|
||||
import { SequelizeDomainMapper } from "@erp/core/api";
|
||||
import type { TaxCatalogProvider } from "@erp/core";
|
||||
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
@ -22,25 +23,22 @@ import {
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type SupplierInternalProps,
|
||||
Supplier,
|
||||
SupplierStatus,
|
||||
SUPPLIER_STATUS,
|
||||
} from "../../../../../domain";
|
||||
import { type ISupplierCreateProps, Supplier, SupplierStatus } from "../../../../../domain";
|
||||
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<
|
||||
SupplierModel,
|
||||
SupplierCreationAttributes,
|
||||
Supplier
|
||||
> {
|
||||
public mapToDomain(source: SupplierModel): Result<Supplier, Error> {
|
||||
private readonly taxCatalog: TaxCatalogProvider;
|
||||
|
||||
constructor(params: { taxCatalog: TaxCatalogProvider }) {
|
||||
super();
|
||||
this.taxCatalog = params.taxCatalog;
|
||||
}
|
||||
|
||||
public mapToDomain(source: SupplierModel, params?: MapperParamsType): Result<Supplier, Error> {
|
||||
try {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
|
||||
@ -52,11 +50,7 @@ export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
|
||||
);
|
||||
|
||||
const isCompany = source.is_company;
|
||||
const status = extractOrPushError(
|
||||
SupplierStatus.create(PERSISTED_STATUS_TO_DOMAIN[source.status] ?? source.status),
|
||||
"status",
|
||||
errors
|
||||
);
|
||||
const status = extractOrPushError(SupplierStatus.create(source.status), "status", errors);
|
||||
const reference = extractOrPushError(
|
||||
maybeFromNullableResult(source.reference, (value) => Name.create(value)),
|
||||
"reference",
|
||||
@ -194,7 +188,7 @@ export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
|
||||
return Result.fail(new ValidationErrorCollection("Supplier props mapping failed", errors));
|
||||
}
|
||||
|
||||
const supplierProps: SupplierInternalProps = {
|
||||
const supplierProps: ISupplierCreateProps = {
|
||||
companyId: companyId!,
|
||||
status: status!,
|
||||
reference: reference!,
|
||||
@ -219,13 +213,16 @@ export class SequelizeSupplierDomainMapper extends SequelizeDomainMapper<
|
||||
currencyCode: currencyCode!,
|
||||
};
|
||||
|
||||
return Result.ok(Supplier.rehydrate(supplierProps, supplierId!));
|
||||
return Supplier.create(supplierProps, supplierId);
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(err as Error);
|
||||
}
|
||||
}
|
||||
|
||||
public mapToPersistence(source: Supplier): Result<SupplierCreationAttributes, Error> {
|
||||
public mapToPersistence(
|
||||
source: Supplier,
|
||||
params?: MapperParamsType
|
||||
): Result<SupplierCreationAttributes, Error> {
|
||||
const supplierValues: Partial<SupplierCreationAttributes> = {
|
||||
id: source.id.toPrimitive(),
|
||||
company_id: source.companyId.toPrimitive(),
|
||||
@ -1,4 +1,4 @@
|
||||
import { SequelizeQueryMapper } from "@erp/core/api";
|
||||
import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api";
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
@ -22,19 +22,17 @@ import {
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { SupplierSummary } from "../../../../../application";
|
||||
import { SupplierStatus, SUPPLIER_STATUS } from "../../../../../domain";
|
||||
import { SupplierStatus } from "../../../../../domain";
|
||||
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<
|
||||
SupplierModel,
|
||||
SupplierSummary
|
||||
> {
|
||||
public mapToReadModel(raw: SupplierModel): Result<SupplierSummary, Error> {
|
||||
public mapToReadModel(
|
||||
raw: SupplierModel,
|
||||
params?: MapperParamsType
|
||||
): Result<SupplierSummary, Error> {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
|
||||
// 1) Valores escalares (atributos generales)
|
||||
@ -42,11 +40,7 @@ export class SequelizeSupplierSummaryMapper extends SequelizeQueryMapper<
|
||||
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
|
||||
|
||||
const isCompany = raw.is_company;
|
||||
const status = extractOrPushError(
|
||||
SupplierStatus.create(PERSISTED_STATUS_TO_DOMAIN[raw.status] ?? raw.status),
|
||||
"status",
|
||||
errors
|
||||
);
|
||||
const status = extractOrPushError(SupplierStatus.create(raw.status), "status", errors);
|
||||
const reference = extractOrPushError(
|
||||
maybeFromNullableResult(raw.reference, (value) => Name.create(value)),
|
||||
"reference",
|
||||
@ -217,13 +217,12 @@ export default (database: Sequelize) => {
|
||||
{ name: "idx_tin", fields: ["tin"] }, // <- para servicios externos
|
||||
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get
|
||||
|
||||
// Quiksearch => "CriteriaToSequelizeConverter" construye la sentencia SQL de búsqueda FULLTEXT
|
||||
// a partir de este índice. Las columnas deben coincidir con los campos de búsqueda rápida definidos en el repositorio.
|
||||
{
|
||||
name: "idx_suppliers_fulltext_search",
|
||||
// Para búsquedas simples => se hace con el "CriteriaToSequelizeConverter"
|
||||
/*{
|
||||
name: "ft_supplier",
|
||||
type: "FULLTEXT",
|
||||
fields: ["name", "trade_name", "reference", "tin", "email_primary", "mobile_primary"],
|
||||
},
|
||||
},*/
|
||||
],
|
||||
|
||||
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
||||
@ -11,7 +11,7 @@ import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction }
|
||||
|
||||
import type { ISupplierRepository, SupplierSummary } from "../../../../application";
|
||||
import type { Supplier } from "../../../../domain";
|
||||
import type { SequelizeSupplierDomainMapper, SequelizeSupplierSummaryMapper } from "../mappers";
|
||||
import type { SequelizeSupplierDomainMapper, SequelizeSupplierSummaryMapper } from "../../mappers";
|
||||
import { SupplierModel } from "../models";
|
||||
|
||||
export class SupplierRepository
|
||||
@ -66,13 +66,9 @@ export class SupplierRepository
|
||||
try {
|
||||
const dtoResult = this.domainMapper.mapToPersistence(supplier);
|
||||
|
||||
if (dtoResult.isFailure) {
|
||||
return Result.fail(dtoResult.error);
|
||||
}
|
||||
|
||||
const { id, company_id, ...updatePayload } = dtoResult.data;
|
||||
const { id, ...updatePayload } = dtoResult.data;
|
||||
const [affected] = await SupplierModel.update(updatePayload, {
|
||||
where: { id, company_id /*, version */ },
|
||||
where: { id /*, version */ },
|
||||
//fields: Object.keys(updatePayload),
|
||||
transaction,
|
||||
individualHooks: true,
|
||||
@ -306,4 +302,33 @@ export class SupplierRepository
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Elimina o marca como eliminado un proveedor.
|
||||
*
|
||||
* @param companyId - Identificador UUID de la empresa a la que pertenece el proveedor.
|
||||
* @param id - UUID del proveedor a eliminar.
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<boolean, Error>
|
||||
*/
|
||||
async deleteByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
transaction: Transaction
|
||||
): Promise<Result<boolean, Error>> {
|
||||
try {
|
||||
const deleted = await SupplierModel.destroy({
|
||||
where: { id: id.toString(), company_id: companyId.toString() },
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (deleted === 0) {
|
||||
return Result.fail(new EntityNotFoundError("Supplier", "id", id.toString()));
|
||||
}
|
||||
|
||||
return Result.ok(true);
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@ export const CreateSupplierRequestSchema = z.object({
|
||||
id: z.string().nonempty(),
|
||||
|
||||
reference: z.string().optional(),
|
||||
is_company: z.boolean(),
|
||||
is_company: z.string().toLowerCase().default("true"),
|
||||
name: z.string(),
|
||||
trade_name: z.string().optional(),
|
||||
tin: z.string().optional(),
|
||||
@ -0,0 +1,13 @@
|
||||
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>;
|
||||
@ -7,7 +7,7 @@ import { z } from "zod/v4";
|
||||
*/
|
||||
|
||||
export const GetSupplierByIdRequestSchema = z.object({
|
||||
supplier_id: z.uuid(),
|
||||
supplier_id: z.string(),
|
||||
});
|
||||
|
||||
export type GetSupplierByIdRequestDTO = z.infer<typeof GetSupplierByIdRequestSchema>;
|
||||
@ -1,4 +1,5 @@
|
||||
export * from "./create-supplier.request.dto";
|
||||
export * from "./delete-supplier-by-id.request.dto";
|
||||
export * from "./get-supplier-by-id.request.dto";
|
||||
export * from "./supplier-list.request.dto";
|
||||
export * from "./update-supplier-by-id.request.dto";
|
||||
@ -1,13 +1,13 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const UpdateSupplierByIdParamsRequestSchema = z.object({
|
||||
supplier_id: z.uuid(),
|
||||
supplier_id: z.string(),
|
||||
});
|
||||
|
||||
export const UpdateSupplierByIdRequestSchema = z.object({
|
||||
reference: z.string().optional(),
|
||||
|
||||
is_company: z.boolean().optional(),
|
||||
is_company: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
trade_name: z.string().optional(),
|
||||
tin: z.string().optional(),
|
||||
@ -7,12 +7,12 @@ import {
|
||||
} from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const SupplierDetailSchema = z.object({
|
||||
export const GetSupplierByIdResponseSchema = z.object({
|
||||
id: z.uuid(),
|
||||
company_id: z.uuid(),
|
||||
reference: z.string(),
|
||||
|
||||
is_company: z.boolean(),
|
||||
is_company: z.string(),
|
||||
name: z.string(),
|
||||
trade_name: z.string(),
|
||||
tin: z.string(),
|
||||
@ -21,8 +21,8 @@ export const SupplierDetailSchema = z.object({
|
||||
street2: z.string(),
|
||||
city: z.string(),
|
||||
province: z.string(),
|
||||
postal_code: z.union([PostalCodeSchema, z.literal("")]),
|
||||
country: z.union([CountryCodeSchema, z.literal("")]),
|
||||
postal_code: PostalCodeSchema,
|
||||
country: CountryCodeSchema,
|
||||
|
||||
email_primary: z.string(),
|
||||
email_secondary: z.string(),
|
||||
@ -34,6 +34,9 @@ export const SupplierDetailSchema = z.object({
|
||||
fax: z.string(),
|
||||
website: z.string(),
|
||||
|
||||
legal_record: z.string(),
|
||||
|
||||
default_taxes: TaxCombinationCodeSchema,
|
||||
status: z.string(),
|
||||
language_code: LanguageCodeSchema,
|
||||
currency_code: CurrencyCodeSchema,
|
||||
@ -41,7 +44,4 @@ export const SupplierDetailSchema = z.object({
|
||||
metadata: MetadataSchema.optional(),
|
||||
});
|
||||
|
||||
export type SupplierDetailDTO = z.infer<typeof SupplierDetailSchema>;
|
||||
|
||||
export const GetSupplierByIdResponseSchema = SupplierDetailSchema;
|
||||
export type GetSupplierByIdResponseDTO = SupplierDetailDTO;
|
||||
export type GetSupplierByIdResponseDTO = z.infer<typeof GetSupplierByIdResponseSchema>;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user