From d5c6079d26c09f4b60fd6b162d43715088f7635b Mon Sep 17 00:00:00 2001 From: david Date: Sun, 14 Sep 2025 12:50:29 +0200 Subject: [PATCH] Facturas de cliente --- .../services/participantAddressFinder.ts | 64 --------- .../application/services/participantFinder.ts | 21 --- .../application/{services => specs}/index.ts | 0 .../create-customer-invoice.use-case.ts | 41 +++--- .../express/customer-invoices.routes.ts | 2 +- .../specs/customer-not-exists.spec.ts | 21 +++ .../src/api/application/specs/index.ts | 1 + .../create/create-customer.use-case.ts | 71 +++++----- .../src/api/infrastructure/dependencies.ts | 11 +- .../express/customers.routes.ts | 16 ++- packages/rdx-ddd/src/index.ts | 1 + packages/rdx-ddd/src/specification.ts | 132 ++++++++++++++++++ 12 files changed, 233 insertions(+), 148 deletions(-) delete mode 100644 modules/customer-invoices/src/api/application/services/participantAddressFinder.ts delete mode 100644 modules/customer-invoices/src/api/application/services/participantFinder.ts rename modules/customer-invoices/src/api/application/{services => specs}/index.ts (100%) create mode 100644 modules/customers/src/api/application/specs/customer-not-exists.spec.ts create mode 100644 modules/customers/src/api/application/specs/index.ts create mode 100644 packages/rdx-ddd/src/specification.ts diff --git a/modules/customer-invoices/src/api/application/services/participantAddressFinder.ts b/modules/customer-invoices/src/api/application/services/participantAddressFinder.ts deleted file mode 100644 index f6fc0f36..00000000 --- a/modules/customer-invoices/src/api/application/services/participantAddressFinder.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* import { - ApplicationServiceError, - type IApplicationServiceError, -} from "@/contexts/common/application/services/ApplicationServiceError"; -import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain"; -import { Result, UniqueID } from "@shared/contexts"; -import { NullOr } from "@shared/utilities"; -import { ICustomerInvoiceParticipantAddress, ICustomerInvoiceParticipantAddressRepository } from "../../domain"; - -export const participantAddressFinder = async ( - addressId: UniqueID, - adapter: IAdapter, - repository: RepositoryBuilder -) => { - if (addressId.isNull()) { - return Result.fail( - ApplicationServiceError.create( - ApplicationServiceError.INVALID_REQUEST_PARAM, - `Participant address ID required` - ) - ); - } - - const transaction = adapter.startTransaction(); - let address: NullOr = null; - - try { - await transaction.complete(async (t) => { - address = await repository({ transaction: t }).getById(addressId); - }); - - if (address === null) { - return Result.fail( - ApplicationServiceError.create(ApplicationServiceError.NOT_FOUND_ERROR, "", { - id: addressId.toString(), - entity: "participant address", - }) - ); - } - - return Result.ok(address); - } catch (error: unknown) { - const _error = error as Error; - - if (repository().isRepositoryError(_error)) { - return Result.fail( - ApplicationServiceError.create( - ApplicationServiceError.REPOSITORY_ERROR, - _error.message, - _error - ) - ); - } - - return Result.fail( - ApplicationServiceError.create( - ApplicationServiceError.UNEXCEPTED_ERROR, - _error.message, - _error - ) - ); - } -}; - */ diff --git a/modules/customer-invoices/src/api/application/services/participantFinder.ts b/modules/customer-invoices/src/api/application/services/participantFinder.ts deleted file mode 100644 index 22f5594b..00000000 --- a/modules/customer-invoices/src/api/application/services/participantFinder.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain"; -import { UniqueID } from "@shared/contexts"; -import { ICustomerInvoiceParticipantRepository } from "../../domain"; -import { CustomerInvoiceCustomer } from "../../domain/entities/customer-invoice-customer/customer-invoice-customer"; - -export const participantFinder = async ( - participantId: UniqueID, - adapter: IAdapter, - repository: RepositoryBuilder -): Promise => { - if (!participantId || (participantId && participantId.isNull())) { - return Promise.resolve(undefined); - } - - const participant = await adapter - .startTransaction() - .complete((t) => repository({ transaction: t }).getById(participantId)); - - return Promise.resolve(participant ? participant : undefined); -}; - */ diff --git a/modules/customer-invoices/src/api/application/services/index.ts b/modules/customer-invoices/src/api/application/specs/index.ts similarity index 100% rename from modules/customer-invoices/src/api/application/services/index.ts rename to modules/customer-invoices/src/api/application/specs/index.ts diff --git a/modules/customer-invoices/src/api/application/use-cases/create/create-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/create/create-customer-invoice.use-case.ts index 368ac1c4..5f4c25f0 100644 --- a/modules/customer-invoices/src/api/application/use-cases/create/create-customer-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/create/create-customer-invoice.use-case.ts @@ -1,11 +1,11 @@ import { JsonTaxCatalogProvider } from "@erp/core"; -import { DuplicateEntityError, ITransactionManager } from "@erp/core/api"; +import { DuplicateEntityError, IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto"; import { CustomerInvoiceService } from "../../../domain"; -import { CreateCustomerInvoiceAssembler } from "./assembler"; +import { CustomerInvoiceFullPresenter } from "../../presenters"; import { CreateCustomerInvoicePropsMapper } from "./map-dto-to-create-customer-invoice-props"; type CreateCustomerInvoiceUseCaseInput = { @@ -17,15 +17,19 @@ export class CreateCustomerInvoiceUseCase { constructor( private readonly service: CustomerInvoiceService, private readonly transactionManager: ITransactionManager, - private readonly assembler: CreateCustomerInvoiceAssembler, + private readonly presenterRegistry: IPresenterRegistry, private readonly taxCatalog: JsonTaxCatalogProvider ) {} public execute(params: CreateCustomerInvoiceUseCaseInput) { const { dto, companyId } = params; - const dtoMapper = new CreateCustomerInvoicePropsMapper({ taxCatalog: this.taxCatalog }); + const presenter = this.presenterRegistry.getPresenter({ + resource: "customer-invoice", + projection: "FULL", + }) as CustomerInvoiceFullPresenter; // 1) Mapear DTO → props de dominio + const dtoMapper = new CreateCustomerInvoicePropsMapper({ taxCatalog: this.taxCatalog }); const dtoResult = dtoMapper.map(dto); if (dtoResult.isFailure) { return Result.fail(dtoResult.error); @@ -43,19 +47,24 @@ export class CreateCustomerInvoiceUseCase { // 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista return this.transactionManager.complete(async (transaction: Transaction) => { - const existsGuard = await this.ensureNotExists(companyId, id, transaction); - if (existsGuard.isFailure) { - return Result.fail(existsGuard.error); + try { + const existsGuard = await this.ensureNotExists(companyId, id, transaction); + if (existsGuard.isFailure) { + return Result.fail(existsGuard.error); + } + + const saveResult = await this.service.saveInvoice(newInvoice, transaction); + if (saveResult.isFailure) { + return Result.fail(saveResult.error); + } + + const invoice = saveResult.data; + const dto = presenter.toOutput(invoice); + + return Result.ok(dto); + } catch (error: unknown) { + return Result.fail(error as Error); } - - const saveResult = await this.service.saveInvoice(newInvoice, transaction); - if (saveResult.isFailure) { - return Result.fail(saveResult.error); - } - - const viewDTO = this.assembler.toDTO(saveResult.data); - - return Result.ok(viewDTO); }); } diff --git a/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts index fc24ce9f..242feab0 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts @@ -72,7 +72,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => { "/", //checkTabContext, - validateRequest(CreateCustomerInvoiceRequestSchema), + validateRequest(CreateCustomerInvoiceRequestSchema, "body"), (req: Request, res: Response, next: NextFunction) => { const useCase = deps.build.create(); const controller = new CreateCustomerInvoiceController(useCase); diff --git a/modules/customers/src/api/application/specs/customer-not-exists.spec.ts b/modules/customers/src/api/application/specs/customer-not-exists.spec.ts new file mode 100644 index 00000000..593550f3 --- /dev/null +++ b/modules/customers/src/api/application/specs/customer-not-exists.spec.ts @@ -0,0 +1,21 @@ +import { CompositeSpecification, UniqueID } from "@repo/rdx-ddd"; +import { CustomerService } from "../../domain"; + +export class CustomerNotExistsInCompanySpecification extends CompositeSpecification { + constructor( + private readonly service: CustomerService, + private readonly companyId: UniqueID, + private readonly transaction?: any + ) { + super(); + } + + public async isSatisfiedBy(customerId: UniqueID): Promise { + const exists = await this.service.existsByIdInCompany( + customerId, + this.companyId, + this.transaction + ); + return !exists; + } +} diff --git a/modules/customers/src/api/application/specs/index.ts b/modules/customers/src/api/application/specs/index.ts new file mode 100644 index 00000000..49d50e09 --- /dev/null +++ b/modules/customers/src/api/application/specs/index.ts @@ -0,0 +1 @@ +export * from "./customer-not-exists.spec"; diff --git a/modules/customers/src/api/application/use-cases/create/create-customer.use-case.ts b/modules/customers/src/api/application/use-cases/create/create-customer.use-case.ts index 342fe379..900d24ab 100644 --- a/modules/customers/src/api/application/use-cases/create/create-customer.use-case.ts +++ b/modules/customers/src/api/application/use-cases/create/create-customer.use-case.ts @@ -1,10 +1,11 @@ -import { DuplicateEntityError, ITransactionManager } from "@erp/core/api"; +import { DuplicateEntityError, IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; -import { CreateCustomerRequestDTO } from "../../../common"; -import { CustomerService } from "../../domain"; -import { CreateCustomersAssembler } from "./assembler"; +import { CreateCustomerRequestDTO } from "../../../../common"; +import { CustomerService } from "../../../domain"; +import { CustomerFullPresenter } from "../../presenters"; +import { CustomerNotExistsInCompanySpecification } from "../../specs"; import { mapDTOToCreateCustomerProps } from "./map-dto-to-create-customer-props"; type CreateCustomerUseCaseInput = { @@ -16,11 +17,15 @@ export class CreateCustomerUseCase { constructor( private readonly service: CustomerService, private readonly transactionManager: ITransactionManager, - private readonly assembler: CreateCustomersAssembler + private readonly presenterRegistry: IPresenterRegistry ) {} public execute(params: CreateCustomerUseCaseInput) { const { dto, companyId } = params; + const presenter = this.presenterRegistry.getPresenter({ + resource: "customer", + projection: "FULL", + }) as CustomerFullPresenter; // 1) Mapear DTO → props de dominio const dtoResult = mapDTOToCreateCustomerProps(dto); @@ -40,39 +45,31 @@ export class CreateCustomerUseCase { // 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista return this.transactionManager.complete(async (transaction: Transaction) => { - const existsGuard = await this.ensureNotExists(companyId, id, transaction); - if (existsGuard.isFailure) { - return Result.fail(existsGuard.error); + try { + // Verificar que no exista ya un cliente con el mismo id en la companyId + const spec = new CustomerNotExistsInCompanySpecification( + this.service, + companyId, + transaction + ); + const isNew = await spec.isSatisfiedBy(newCustomer.id); + + if (!isNew) { + return Result.fail(new DuplicateEntityError("Customer", "id", String(newCustomer.id))); + } + + const saveResult = await this.service.saveCustomer(newCustomer, transaction); + if (saveResult.isFailure) { + return Result.fail(saveResult.error); + } + + const customer = saveResult.data; + const dto = presenter.toOutput(customer); + + return Result.ok(dto); + } catch (error: unknown) { + return Result.fail(error as Error); } - - const saveResult = await this.service.saveCustomer(newCustomer, transaction); - if (saveResult.isFailure) { - return Result.fail(saveResult.error); - } - - const viewDTO = this.assembler.toDTO(saveResult.data); - - return Result.ok(viewDTO); }); } - - /** - Verifica que no exista un Customer con el mismo id en la companyId. - */ - private async ensureNotExists( - companyId: UniqueID, - id: UniqueID, - transaction: Transaction - ): Promise> { - const existsResult = await this.service.existsByIdInCompany(companyId, id, transaction); - if (existsResult.isFailure) { - return Result.fail(existsResult.error); - } - - if (existsResult.data) { - return Result.fail(new DuplicateEntityError("Customer", "id", String(id))); - } - - return Result.ok(undefined); - } } diff --git a/modules/customers/src/api/infrastructure/dependencies.ts b/modules/customers/src/api/infrastructure/dependencies.ts index 8a0f5037..cf48aee9 100644 --- a/modules/customers/src/api/infrastructure/dependencies.ts +++ b/modules/customers/src/api/infrastructure/dependencies.ts @@ -5,6 +5,7 @@ import { SequelizeTransactionManager, } from "@erp/core/api"; import { + CreateCustomerUseCase, CustomerFullPresenter, ListCustomersPresenter, ListCustomersUseCase, @@ -23,8 +24,8 @@ export type CustomerDeps = { build: { list: () => ListCustomersUseCase; get: () => GetCustomerUseCase; - /*create: () => CreateCustomerUseCase; - update: () => UpdateCustomerUseCase; + create: () => CreateCustomerUseCase; + /*update: () => UpdateCustomerUseCase; delete: () => DeleteCustomerUseCase;*/ }; }; @@ -64,9 +65,9 @@ export function buildCustomerDependencies(params: ModuleParams): CustomerDeps { service, build: { list: () => new ListCustomersUseCase(service, transactionManager, presenterRegistry), - /*get: () => new GetCustomerUseCase(_service!, transactionManager!, presenterRegistry!), - create: () => new CreateCustomerUseCase(_service!, transactionManager!, presenterRegistry!), - update: () => new UpdateCustomerUseCase(_service!, transactionManager!, presenterRegistry!), + get: () => new GetCustomerUseCase(service, transactionManager, presenterRegistry), + create: () => new CreateCustomerUseCase(service, transactionManager, presenterRegistry), + /*update: () => new UpdateCustomerUseCase(_service!, transactionManager!, presenterRegistry!), delete: () => new DeleteCustomerUseCase(_service!, transactionManager!),*/ }, }; diff --git a/modules/customers/src/api/infrastructure/express/customers.routes.ts b/modules/customers/src/api/infrastructure/express/customers.routes.ts index aba37506..a9849394 100644 --- a/modules/customers/src/api/infrastructure/express/customers.routes.ts +++ b/modules/customers/src/api/infrastructure/express/customers.routes.ts @@ -2,9 +2,17 @@ import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth import { ILogger, ModuleParams, validateRequest } from "@erp/core/api"; import { Application, NextFunction, Request, Response, Router } from "express"; import { Sequelize } from "sequelize"; -import { CustomerListRequestSchema, GetCustomerByIdRequestSchema } from "../../../common/dto"; +import { + CreateCustomerRequestSchema, + CustomerListRequestSchema, + GetCustomerByIdRequestSchema, +} from "../../../common/dto"; import { buildCustomerDependencies } from "../dependencies"; -import { GetCustomerController, ListCustomersController } from "./controllers"; +import { + CreateCustomerController, + GetCustomerController, + ListCustomersController, +} from "./controllers"; export const customersRouter = (params: ModuleParams) => { const { app, database, baseRoutePath, logger } = params as { @@ -61,7 +69,7 @@ export const customersRouter = (params: ModuleParams) => { } ); - /*router.post( + router.post( "/", //checkTabContext, @@ -73,7 +81,7 @@ export const customersRouter = (params: ModuleParams) => { } ); - router.put( + /*router.put( "/:customer_id", //checkTabContext, validateRequest(UpdateCustomerByIdParamsRequestSchema, "params"), diff --git a/packages/rdx-ddd/src/index.ts b/packages/rdx-ddd/src/index.ts index a4f4da5b..030636e4 100644 --- a/packages/rdx-ddd/src/index.ts +++ b/packages/rdx-ddd/src/index.ts @@ -3,4 +3,5 @@ export * from "./aggregate-root-repository.interface"; export * from "./domain-entity"; export * from "./events/domain-event.interface"; export * from "./helpers"; +export * from "./specification"; export * from "./value-objects"; diff --git a/packages/rdx-ddd/src/specification.ts b/packages/rdx-ddd/src/specification.ts new file mode 100644 index 00000000..a44ab31a --- /dev/null +++ b/packages/rdx-ddd/src/specification.ts @@ -0,0 +1,132 @@ +export interface IBaseSpecification { + isSatisfiedBy(candidate: T): Promise; +} + +export interface ICompositeSpecification extends IBaseSpecification { + and(other: ICompositeSpecification): ICompositeSpecification; + andNot(other: ICompositeSpecification): ICompositeSpecification; + or(other: ICompositeSpecification): ICompositeSpecification; + orNot(other: ICompositeSpecification): ICompositeSpecification; + not(): ICompositeSpecification; +} + +export abstract class CompositeSpecification implements ICompositeSpecification { + abstract isSatisfiedBy(candidate: T): Promise; + + public and(other: ICompositeSpecification): ICompositeSpecification { + return new AndSpecification(this, other); + } + + public andNot(other: ICompositeSpecification): ICompositeSpecification { + return new AndNotSpecification(this, other); + } + + public or(other: ICompositeSpecification): ICompositeSpecification { + return new OrSpecification(this, other); + } + + public orNot(other: ICompositeSpecification): ICompositeSpecification { + return new OrNotSpecification(this, other); + } + + public not(): ICompositeSpecification { + return new NotSpecification(this); + } +} + +class AndSpecification extends CompositeSpecification { + constructor( + private readonly left: ICompositeSpecification, + private readonly right: ICompositeSpecification + ) { + super(); + } + + public async isSatisfiedBy(candidate: T): Promise { + const leftResult = await this.left.isSatisfiedBy(candidate); + if (!leftResult) return false; + + const rightResult = await this.right.isSatisfiedBy(candidate); + return rightResult; + } + + toString(): string { + return `(${this.left.toString()} and ${this.right.toString()})`; + } +} + +class AndNotSpecification extends AndSpecification { + public async isSatisfiedBy(candidate: T): Promise { + return (await super.isSatisfiedBy(candidate)) !== true; + } + + toString(): string { + return `not ${super.toString()}`; + } +} + +class OrSpecification extends CompositeSpecification { + constructor( + private readonly left: ICompositeSpecification, + private readonly right: ICompositeSpecification + ) { + super(); + } + + public async isSatisfiedBy(candidate: T): Promise { + const leftResult = await this.left.isSatisfiedBy(candidate); + if (leftResult) return true; + + const rightResult = await this.right.isSatisfiedBy(candidate); + return rightResult; + } + + toString(): string { + return `(${this.left.toString()} or ${this.right.toString()})`; + } +} + +class OrNotSpecification extends OrSpecification { + public async isSatisfiedBy(candidate: T): Promise { + return (await super.isSatisfiedBy(candidate)) !== true; + } + + toString(): string { + return `not ${super.toString()}`; + } +} + +export class NotSpecification extends CompositeSpecification { + constructor(private readonly spec: ICompositeSpecification) { + super(); + } + + public async isSatisfiedBy(candidate: T): Promise { + return !this.spec.isSatisfiedBy(candidate); + } + + toString(): string { + return `(not ${this.spec.toString()})`; + } +} + +export class RangeSpecification extends CompositeSpecification { + private readonly min: T; + private readonly max: T; + private readonly compareFn: (a: T, b: T) => number; + + constructor(min: T, max: T, compareFn: (a: T, b: T) => number) { + super(); + this.min = min; + this.max = max; + this.compareFn = compareFn; + } + + public async isSatisfiedBy(candidate: T): Promise { + return this.compareFn(candidate, this.min) >= 0 && this.compareFn(candidate, this.max) <= 0; + } + + toString(): string { + return `range [${this.min}, ${this.max}]`; + } +}