From b87082754be47f180dd9bff95ec57ed89e652b88 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 26 Jun 2025 20:05:33 +0200 Subject: [PATCH] Facturas de cliente --- apps/server/package.json | 13 ++--- apps/server/src/index.ts | 3 +- .../src/api/errors/duplicate-entity-error.ts | 6 +++ modules/core/src/api/errors/error-mapper.ts | 5 ++ modules/core/src/api/errors/index.ts | 8 +++ .../express/middlewares/validate-request.ts | 1 + .../sequelize/sequelize-mapper.ts | 9 ++-- .../create-customer-invoice.use-case.ts | 40 ++++++++++----- .../get-customer-invoice.use-case.ts | 34 ++++++++----- .../application/get-customer-invoice/index.ts | 1 + .../presenter/InvoiceItem.presenter.ts.bak | 0 .../InvoiceParticipant.presenter.ts.bak | 0 ...InvoiceParticipantAddress.presenter.ts.bak | 0 .../presenter/get-invoice.presenter.ts | 34 +++++++------ .../get-customer-invoice/presenter/index.ts | 0 .../map-dto-to-customer-invoice-props.ts | 13 +++-- .../src/api/application/index.ts | 2 +- .../list-customer-invoices.use-case.ts | 5 +- .../presenter/list-invoices.presenter.ts | 8 +-- .../create-customer-invoice/index.ts | 7 ++- .../get-invoice.controller.ts | 49 ++++++------------- .../controllers/get-customer-invoice/index.ts | 14 +++--- .../src/api/domain/errors/index.ts | 0 .../customer-invoices/src/api/domain/index.ts | 1 - .../customer-invoice-repository.interface.ts | 2 + .../customer-invoice-service.interface.ts | 4 +- .../services/customer-invoice.service.ts | 20 ++++++-- .../express/customer-invoices.routes.ts | 12 +++-- .../mappers/customer-invoice.mapper.ts | 8 +-- .../sequelize/customer-invoice.repository.ts | 22 ++++++--- .../request/get-customer-invoice.query.dto.ts | 13 +++++ .../src/common/dto/request/index.ts | 1 + .../get-customer-invoice.result.dto.ts | 17 +++++++ .../src/common/dto/response/index.ts | 1 + pnpm-lock.yaml | 4 +- 35 files changed, 227 insertions(+), 130 deletions(-) create mode 100644 modules/core/src/api/errors/duplicate-entity-error.ts rename modules/customer-invoices/src/api/{controllers => application}/get-customer-invoice/presenter/InvoiceItem.presenter.ts.bak (100%) rename modules/customer-invoices/src/api/{controllers => application}/get-customer-invoice/presenter/InvoiceParticipant.presenter.ts.bak (100%) rename modules/customer-invoices/src/api/{controllers => application}/get-customer-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak (100%) rename modules/customer-invoices/src/api/{controllers => application}/get-customer-invoice/presenter/get-invoice.presenter.ts (60%) rename modules/customer-invoices/src/api/{controllers => application}/get-customer-invoice/presenter/index.ts (100%) delete mode 100644 modules/customer-invoices/src/api/domain/errors/index.ts create mode 100644 modules/customer-invoices/src/common/dto/request/get-customer-invoice.query.dto.ts create mode 100644 modules/customer-invoices/src/common/dto/response/get-customer-invoice.result.dto.ts diff --git a/apps/server/package.json b/apps/server/package.json index 0d4722e7..58b93de7 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -27,7 +27,7 @@ "@types/glob": "^8.1.0", "@types/jest": "^29.5.14", "@types/jsonwebtoken": "^9.0.8", - "@types/luxon": "^3.4.2", + "@types/luxon": "^3.6.2", "@types/node": "^22.15.12", "@types/passport": "^1.0.16", "@types/passport-jwt": "^4.0.1", @@ -54,7 +54,7 @@ "helmet": "^8.0.0", "http": "0.0.1-security", "jsonwebtoken": "^9.0.2", - "luxon": "^3.5.0", + "luxon": "^3.6.1", "module-alias": "^2.2.3", "mysql2": "^3.12.0", "passport": "^0.7.0", @@ -75,14 +75,9 @@ "node": ">=22" }, "tsup": { - "entry": [ - "src/index.ts" - ], + "entry": ["src/index.ts"], "outDir": "dist", - "format": [ - "esm", - "cjs" - ], + "format": ["esm", "cjs"], "target": "es2020", "sourcemap": true, "clean": true, diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index e68d8215..ae9539db 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -105,7 +105,8 @@ const server = http // Manejo de promesas no capturadas process.on("unhandledRejection", (reason: any, promise: Promise) => { - logger.error(`❌ Unhandled rejection at:", ${promise}, "reason:", ${reason}`); + const error = `❌ Unhandled rejection at:", ${promise}, "reason:", ${reason}`; + logger.error(error); // Dependiendo de la aplicación, podrías desear una salida total o un cierre controlado process.exit(1); }); diff --git a/modules/core/src/api/errors/duplicate-entity-error.ts b/modules/core/src/api/errors/duplicate-entity-error.ts new file mode 100644 index 00000000..fa02c9aa --- /dev/null +++ b/modules/core/src/api/errors/duplicate-entity-error.ts @@ -0,0 +1,6 @@ +export class DuplicateEntityError extends Error { + constructor(entity: string, id: string) { + super(`Entity '${entity}' with ID '${id}' already exists.`); + this.name = "DuplicateEntityError"; + } +} diff --git a/modules/core/src/api/errors/error-mapper.ts b/modules/core/src/api/errors/error-mapper.ts index 9b7d8bfd..118f3893 100644 --- a/modules/core/src/api/errors/error-mapper.ts +++ b/modules/core/src/api/errors/error-mapper.ts @@ -9,6 +9,7 @@ import { import { ApiError } from "./api-error"; import { ConflictApiError } from "./conflict-api-error"; import { DomainValidationError } from "./domain-validation-error"; +import { DuplicateEntityError } from "./duplicate-entity-error"; import { ForbiddenApiError } from "./forbidden-api-error"; import { InternalApiError } from "./internal-api-error"; import { NotFoundApiError } from "./not-found-api-error"; @@ -74,6 +75,10 @@ export const errorMapper = { return new ValidationApiError(error.detail, [{ path: error.field, message: error.detail }]); } + if (error instanceof DuplicateEntityError) { + return new ConflictApiError(error.message); + } + // 3. 🔍 Errores individuales de validación if ( message.includes("invalid") || diff --git a/modules/core/src/api/errors/index.ts b/modules/core/src/api/errors/index.ts index 123c1df7..f2d52462 100644 --- a/modules/core/src/api/errors/index.ts +++ b/modules/core/src/api/errors/index.ts @@ -1,4 +1,12 @@ +export * from "./api-error"; +export * from "./conflict-api-error"; export * from "./domain-validation-error"; +export * from "./duplicate-entity-error"; export * from "./error-mapper"; +export * from "./forbidden-api-error"; +export * from "./internal-api-error"; +export * from "./not-found-api-error"; +export * from "./unauthorized-api-error"; +export * from "./unavailable-api-error"; export * from "./validation-api-error"; export * from "./validation-error-collection"; diff --git a/modules/core/src/api/infrastructure/express/middlewares/validate-request.ts b/modules/core/src/api/infrastructure/express/middlewares/validate-request.ts index 6165cd25..3c9800bd 100644 --- a/modules/core/src/api/infrastructure/express/middlewares/validate-request.ts +++ b/modules/core/src/api/infrastructure/express/middlewares/validate-request.ts @@ -41,6 +41,7 @@ export const validateRequest = ( ): RequestHandler => { return async (req, res, next) => { console.debug(`Validating request ${source} with schema.`); + console.debug(req[source]); const result = schema.safeParse(req[source]); if (!result.success) { diff --git a/modules/core/src/api/infrastructure/sequelize/sequelize-mapper.ts b/modules/core/src/api/infrastructure/sequelize/sequelize-mapper.ts index 5f1f3c06..9e809785 100644 --- a/modules/core/src/api/infrastructure/sequelize/sequelize-mapper.ts +++ b/modules/core/src/api/infrastructure/sequelize/sequelize-mapper.ts @@ -41,7 +41,8 @@ export abstract class SequelizeMapper< source: TModel[], params?: MapperParamsType ): Result, Error> { - return this.mapArrayAndCountToDomain(source, source.length, params); + const items = source ?? []; + return this.mapArrayAndCountToDomain(items, items.length, params); } public mapArrayAndCountToDomain( @@ -49,12 +50,14 @@ export abstract class SequelizeMapper< totalCount: number, params?: MapperParamsType ): Result, Error> { + const _source = source ?? []; + try { - if (source.length === 0) { + if (_source.length === 0) { return Result.ok(new Collection([], totalCount)); } - const items = source.map( + const items = _source.map( (value, index) => this.mapToDomain(value, { index, ...params }).data ); return Result.ok(new Collection(items, totalCount)); diff --git a/modules/customer-invoices/src/api/application/create-customer-invoice/create-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/create-customer-invoice/create-customer-invoice.use-case.ts index ecc125e9..c994523a 100644 --- a/modules/customer-invoices/src/api/application/create-customer-invoice/create-customer-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/create-customer-invoice/create-customer-invoice.use-case.ts @@ -1,4 +1,4 @@ -import { ITransactionManager } from "@erp/core/api"; +import { DuplicateEntityError, ITransactionManager } from "@erp/core/api"; import { CreateCustomerInvoiceCommandDTO } from "@erp/customer-invoices/common/dto"; import { Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; @@ -8,19 +8,21 @@ import { CreateCustomerInvoicesPresenter } from "./presenter"; export class CreateCustomerInvoiceUseCase { constructor( - private readonly customerInvoiceService: ICustomerInvoiceService, + private readonly service: ICustomerInvoiceService, private readonly transactionManager: ITransactionManager, private readonly presenter: CreateCustomerInvoicesPresenter ) {} public execute(dto: CreateCustomerInvoiceCommandDTO) { - const invoicePropOrError = mapDTOToCustomerInvoiceProps(dto); + const invoicePropsOrError = mapDTOToCustomerInvoiceProps(dto); - if (invoicePropOrError.isFailure) { - return Result.fail(invoicePropOrError.error); + if (invoicePropsOrError.isFailure) { + return Result.fail(invoicePropsOrError.error); } - const invoiceOrError = this.customerInvoiceService.build(invoicePropOrError.data); + const { props, id } = invoicePropsOrError.data; + + const invoiceOrError = this.service.build(props, id); if (invoiceOrError.isFailure) { return Result.fail(invoiceOrError.error); @@ -29,13 +31,27 @@ export class CreateCustomerInvoiceUseCase { const newInvoice = invoiceOrError.data; return this.transactionManager.complete(async (transaction: Transaction) => { - const result = await this.customerInvoiceService.save(newInvoice, transaction); - if (result.isFailure) { - return Result.fail(result.error); - } + try { + const duplicateCheck = await this.service.existsById(id, transaction); - const viewDTO = this.presenter.toDTO(newInvoice); - return Result.ok(viewDTO); + if (duplicateCheck.isFailure) { + return Result.fail(duplicateCheck.error); + } + + if (duplicateCheck.data) { + return Result.fail(new DuplicateEntityError("CustomerInvoice", id.toString())); + } + + const result = await this.service.save(newInvoice, transaction); + if (result.isFailure) { + return Result.fail(result.error); + } + + const viewDTO = this.presenter.toDTO(newInvoice); + return Result.ok(viewDTO); + } catch (error: unknown) { + return Result.fail(error as Error); + } }); } } diff --git a/modules/customer-invoices/src/api/application/get-customer-invoice/get-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/get-customer-invoice/get-customer-invoice.use-case.ts index c122dddf..cda1bc5a 100644 --- a/modules/customer-invoices/src/api/application/get-customer-invoice/get-customer-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/get-customer-invoice/get-customer-invoice.use-case.ts @@ -1,24 +1,34 @@ -import { UniqueID } from "@/core/common/domain"; -import { ITransactionManager } from "@/core/common/infrastructure/database"; -import { logger } from "@/lib/logger"; +import { ITransactionManager } from "@erp/core/api"; +import { GetCustomerInvoiceByIdQueryDTO } from "@erp/customer-invoices/common/dto"; +import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { CustomerInvoice, ICustomerInvoiceService } from "../domain"; +import { ICustomerInvoiceService } from "../../domain"; +import { GetCustomerInvoicePresenter } from "./presenter"; export class GetCustomerInvoiceUseCase { constructor( - private readonly customerInvoiceService: ICustomerInvoiceService, - private readonly transactionManager: ITransactionManager + private readonly service: ICustomerInvoiceService, + private readonly transactionManager: ITransactionManager, + private readonly presenter: GetCustomerInvoicePresenter ) {} - public execute(customerInvoiceID: UniqueID): Promise> { + public execute(dto: GetCustomerInvoiceByIdQueryDTO) { + const idOrError = UniqueID.create(dto.id); + + if (idOrError.isFailure) { + return Result.fail(idOrError.error); + } + return this.transactionManager.complete(async (transaction) => { try { - return await this.customerInvoiceService.findCustomerInvoiceById( - customerInvoiceID, - transaction - ); + const invoiceOrError = await this.service.getById(idOrError.data, transaction); + if (invoiceOrError.isFailure) { + return Result.fail(invoiceOrError.error); + } + + const getDTO = this.presenter.toDTO(invoiceOrError.data); + return Result.ok(getDTO); } catch (error: unknown) { - logger.error(error as Error); return Result.fail(error as Error); } }); diff --git a/modules/customer-invoices/src/api/application/get-customer-invoice/index.ts b/modules/customer-invoices/src/api/application/get-customer-invoice/index.ts index 960446a8..b3f84d23 100644 --- a/modules/customer-invoices/src/api/application/get-customer-invoice/index.ts +++ b/modules/customer-invoices/src/api/application/get-customer-invoice/index.ts @@ -1 +1,2 @@ export * from "./get-customer-invoice.use-case"; +export * from "./presenter"; diff --git a/modules/customer-invoices/src/api/controllers/get-customer-invoice/presenter/InvoiceItem.presenter.ts.bak b/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceItem.presenter.ts.bak similarity index 100% rename from modules/customer-invoices/src/api/controllers/get-customer-invoice/presenter/InvoiceItem.presenter.ts.bak rename to modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceItem.presenter.ts.bak diff --git a/modules/customer-invoices/src/api/controllers/get-customer-invoice/presenter/InvoiceParticipant.presenter.ts.bak b/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceParticipant.presenter.ts.bak similarity index 100% rename from modules/customer-invoices/src/api/controllers/get-customer-invoice/presenter/InvoiceParticipant.presenter.ts.bak rename to modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceParticipant.presenter.ts.bak diff --git a/modules/customer-invoices/src/api/controllers/get-customer-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak b/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak similarity index 100% rename from modules/customer-invoices/src/api/controllers/get-customer-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak rename to modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak diff --git a/modules/customer-invoices/src/api/controllers/get-customer-invoice/presenter/get-invoice.presenter.ts b/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/get-invoice.presenter.ts similarity index 60% rename from modules/customer-invoices/src/api/controllers/get-customer-invoice/presenter/get-invoice.presenter.ts rename to modules/customer-invoices/src/api/application/get-customer-invoice/presenter/get-invoice.presenter.ts index de9b53eb..afa03589 100644 --- a/modules/customer-invoices/src/api/controllers/get-customer-invoice/presenter/get-invoice.presenter.ts +++ b/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/get-invoice.presenter.ts @@ -1,25 +1,31 @@ -import { IGetCustomerInvoiceResponseDTO } from "../../../../common/dto"; -import { CustomerInvoice, CustomerInvoiceItem } from "../../../domain"; +import { GetCustomerInvoiceResultDTO } from "../../../../common/dto"; +import { CustomerInvoice } from "../../../domain"; -export interface IGetCustomerInvoicePresenter { - toDTO: (customerInvoice: CustomerInvoice) => IGetCustomerInvoiceResponseDTO; +export interface GetCustomerInvoicePresenter { + toDTO: (customerInvoice: CustomerInvoice) => GetCustomerInvoiceResultDTO; } -export const getCustomerInvoicePresenter: IGetCustomerInvoicePresenter = { - toDTO: (customerInvoice: CustomerInvoice): IGetCustomerInvoiceResponseDTO => ({ +export const getCustomerInvoicePresenter: GetCustomerInvoicePresenter = { + toDTO: (customerInvoice: CustomerInvoice): GetCustomerInvoiceResultDTO => ({ id: customerInvoice.id.toPrimitive(), - customerInvoice_status: customerInvoice.status.toString(), - customerInvoice_number: customerInvoice.invoiceNumber.toString(), - customerInvoice_series: customerInvoice.invoiceSeries.toString(), + invoice_status: customerInvoice.status.toString(), + invoice_number: customerInvoice.invoiceNumber.toString(), + invoice_series: customerInvoice.invoiceSeries.toString(), issue_date: customerInvoice.issueDate.toDateString(), operation_date: customerInvoice.operationDate.toDateString(), language_code: "ES", - currency: customerInvoice.customerInvoiceCurrency.toString(), - subtotal: customerInvoice.calculateSubtotal().toPrimitive(), - total: customerInvoice.calculateTotal().toPrimitive(), + currency: customerInvoice.currency, - items: + metadata: { + entity: "customer-invoices", + }, + + //subtotal: customerInvoice.calculateSubtotal().toPrimitive(), + + //total: customerInvoice.calculateTotal().toPrimitive(), + + /*items: customerInvoice.items.size() > 0 ? customerInvoice.items.map((item: CustomerInvoiceItem) => ({ description: item.description.toString(), @@ -30,7 +36,7 @@ export const getCustomerInvoicePresenter: IGetCustomerInvoicePresenter = { //tax_amount: item.calculateTaxAmount().toPrimitive(), total: item.calculateTotal().toPrimitive(), })) - : [], + : [],*/ //sender: {}, //await CustomerInvoiceParticipantPresenter(customerInvoice.senderId, context), diff --git a/modules/customer-invoices/src/api/controllers/get-customer-invoice/presenter/index.ts b/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/index.ts similarity index 100% rename from modules/customer-invoices/src/api/controllers/get-customer-invoice/presenter/index.ts rename to modules/customer-invoices/src/api/application/get-customer-invoice/presenter/index.ts diff --git a/modules/customer-invoices/src/api/application/helpers/map-dto-to-customer-invoice-props.ts b/modules/customer-invoices/src/api/application/helpers/map-dto-to-customer-invoice-props.ts index 4625e5d0..b78d3bcf 100644 --- a/modules/customer-invoices/src/api/application/helpers/map-dto-to-customer-invoice-props.ts +++ b/modules/customer-invoices/src/api/application/helpers/map-dto-to-customer-invoice-props.ts @@ -1,5 +1,5 @@ import { ValidationErrorCollection, ValidationErrorDetail } from "@erp/core/api"; -import { UtcDate } from "@repo/rdx-ddd"; +import { UniqueID, UtcDate } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { CreateCustomerInvoiceCommandDTO } from "../../../common/dto"; import { @@ -16,16 +16,15 @@ import { mapDTOToCustomerInvoiceItemsProps } from "./map-dto-to-customer-invoice * No construye directamente el agregado. * * @param dto - DTO con los datos de la factura de cliente - * @returns CustomerInvoiceProps - Las propiedades para crear una factura de cliente o error + * @returns + * */ -export function mapDTOToCustomerInvoiceProps( - dto: CreateCustomerInvoiceCommandDTO -): Result { +export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceCommandDTO) { const errors: ValidationErrorDetail[] = []; - //const invoiceId = extractOrPushError(UniqueID.create(dto.id), "invoice_id", errors); + const invoiceId = extractOrPushError(UniqueID.create(dto.id), "id", errors); const invoiceNumber = extractOrPushError( CustomerInvoiceNumber.create(dto.invoice_number), @@ -66,7 +65,7 @@ export function mapDTOToCustomerInvoiceProps( currency, }; - return Result.ok(invoiceProps); + return Result.ok({ id: invoiceId!, props: invoiceProps }); /*if (hasNoUndefinedFields(invoiceProps)) { const invoiceOrError = CustomerInvoice.create(invoiceProps, invoiceId); diff --git a/modules/customer-invoices/src/api/application/index.ts b/modules/customer-invoices/src/api/application/index.ts index cbb37010..ec97e0a7 100644 --- a/modules/customer-invoices/src/api/application/index.ts +++ b/modules/customer-invoices/src/api/application/index.ts @@ -1,5 +1,5 @@ export * from "./create-customer-invoice"; //export * from "./delete-customer-invoice"; -//export * from "./get-customer-invoice"; +export * from "./get-customer-invoice"; export * from "./list-customer-invoices"; //export * from "./update-customer-invoice"; diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/list-customer-invoices.use-case.ts b/modules/customer-invoices/src/api/application/list-customer-invoices/list-customer-invoices.use-case.ts index feb17b27..3ed104c0 100644 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/list-customer-invoices.use-case.ts +++ b/modules/customer-invoices/src/api/application/list-customer-invoices/list-customer-invoices.use-case.ts @@ -16,10 +16,7 @@ export class ListCustomerInvoicesUseCase { public execute(criteria: Criteria): Promise> { return this.transactionManager.complete(async (transaction: Transaction) => { try { - const result = await this.customerInvoiceService.findCustomerInvoices( - criteria, - transaction - ); + const result = await this.customerInvoiceService.findByCriteria(criteria, transaction); if (result.isFailure) { return Result.fail(result.error); diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/list-invoices.presenter.ts b/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/list-invoices.presenter.ts index 82f1660b..0ebe160b 100644 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/list-invoices.presenter.ts +++ b/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/list-invoices.presenter.ts @@ -1,20 +1,20 @@ -import { ListCustomerInvoicesViewDTO } from "@erp/customer-invoices/common/dto"; import { Criteria } from "@repo/rdx-criteria/server"; import { Collection } from "@repo/rdx-utils"; +import { ListCustomerInvoicesResultDTO } from "../../../../common/dto"; import { CustomerInvoice } from "../../../domain"; export interface ListCustomerInvoicesPresenter { toDTO: ( customerInvoices: Collection, criteria: Criteria - ) => ListCustomerInvoicesViewDTO; + ) => ListCustomerInvoicesResultDTO; } export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = { toDTO: ( customerInvoices: Collection, criteria: Criteria - ): ListCustomerInvoicesViewDTO => { + ): ListCustomerInvoicesResultDTO => { const items = customerInvoices.map((invoice) => { return { id: invoice.id.toPrimitive(), @@ -25,7 +25,7 @@ export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = { issue_date: invoice.issueDate.toISOString(), operation_date: invoice.operationDate.toISOString(), language_code: "ES", - currency: invoice.customerInvoiceCurrency.toString(), + currency: "EUR", subtotal_price: invoice.calculateSubtotal().toPrimitive(), total_price: invoice.calculateTotal().toPrimitive(), diff --git a/modules/customer-invoices/src/api/controllers/create-customer-invoice/index.ts b/modules/customer-invoices/src/api/controllers/create-customer-invoice/index.ts index 413e267c..7166c0a3 100644 --- a/modules/customer-invoices/src/api/controllers/create-customer-invoice/index.ts +++ b/modules/customer-invoices/src/api/controllers/create-customer-invoice/index.ts @@ -2,12 +2,15 @@ import { SequelizeTransactionManager } from "@erp/core/api"; import { Sequelize } from "sequelize"; import { CreateCustomerInvoiceUseCase, CreateCustomerInvoicesPresenter } from "../../application/"; import { CustomerInvoiceService } from "../../domain"; -import { CustomerInvoiceRepository, customerInvoiceMapper } from "../../infrastructure"; +import { CustomerInvoiceMapper, CustomerInvoiceRepository } from "../../infrastructure"; import { CreateCustomerInvoiceController } from "./create-customer-invoice"; export const buildCreateCustomerInvoicesController = (database: Sequelize) => { const transactionManager = new SequelizeTransactionManager(database); - const customerInvoiceRepository = new CustomerInvoiceRepository(database, customerInvoiceMapper); + const customerInvoiceRepository = new CustomerInvoiceRepository( + database, + new CustomerInvoiceMapper() + ); const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository); const presenter = new CreateCustomerInvoicesPresenter(); diff --git a/modules/customer-invoices/src/api/controllers/get-customer-invoice/get-invoice.controller.ts b/modules/customer-invoices/src/api/controllers/get-customer-invoice/get-invoice.controller.ts index 5ca0370c..fdff70f3 100644 --- a/modules/customer-invoices/src/api/controllers/get-customer-invoice/get-invoice.controller.ts +++ b/modules/customer-invoices/src/api/controllers/get-customer-invoice/get-invoice.controller.ts @@ -1,47 +1,30 @@ -import { ExpressController } from "@erp/core/api"; -import { UniqueID } from "@repo/rdx-ddd"; +import { ExpressController, errorMapper } from "@erp/core/api"; import { GetCustomerInvoiceUseCase } from "../../application"; -import { IGetCustomerInvoicePresenter } from "./presenter"; export class GetCustomerInvoiceController extends ExpressController { - public constructor( - private readonly getCustomerInvoice: GetCustomerInvoiceUseCase, - private readonly presenter: IGetCustomerInvoicePresenter - ) { + public constructor(private readonly getCustomerInvoice: GetCustomerInvoiceUseCase) { super(); } protected async executeImpl() { - const { customerInvoiceId } = this.req.params; + const { id } = this.req.params; - // Validar ID - const customerInvoiceIdOrError = UniqueID.create(customerInvoiceId); - if (customerInvoiceIdOrError.isFailure) - return this.invalidInputError("CustomerInvoice ID not valid"); + /* + const user = this.req.user; // asumimos middleware authenticateJWT inyecta user - const customerInvoiceOrError = await this.getCustomerInvoice.execute( - customerInvoiceIdOrError.data - ); + if (!user || !user.companyId) { + this.unauthorized(res, "Unauthorized: user or company not found"); + return; + } + */ - if (customerInvoiceOrError.isFailure) { - return this.handleError(customerInvoiceOrError.error); + const result = await this.getCustomerInvoice.execute({ id }); + + if (result.isFailure) { + const apiError = errorMapper.toApiError(result.error); + return this.handleApiError(apiError); } - return this.ok(this.presenter.toDTO(customerInvoiceOrError.data)); - } - - private handleError(error: Error) { - const message = error.message; - - if ( - message.includes("Database connection lost") || - message.includes("Database request timed out") - ) { - return this.unavailableError( - "Database service is currently unavailable. Please try again later." - ); - } - - return this.conflictError(message); + return this.ok(result.data); } } diff --git a/modules/customer-invoices/src/api/controllers/get-customer-invoice/index.ts b/modules/customer-invoices/src/api/controllers/get-customer-invoice/index.ts index c1c22dd9..7aa6f3bd 100644 --- a/modules/customer-invoices/src/api/controllers/get-customer-invoice/index.ts +++ b/modules/customer-invoices/src/api/controllers/get-customer-invoice/index.ts @@ -1,19 +1,21 @@ import { SequelizeTransactionManager } from "@erp/core/api"; import { Sequelize } from "sequelize"; +import { GetCustomerInvoiceUseCase, getCustomerInvoicePresenter } from "../../application"; import { CustomerInvoiceService } from "../../domain"; import { CustomerInvoiceRepository, customerInvoiceMapper } from "../../infrastructure"; - -import { GetCustomerInvoiceUseCase } from "../../application"; import { GetCustomerInvoiceController } from "./get-invoice.controller"; -import { getCustomerInvoicePresenter } from "./presenter"; export const buildGetCustomerInvoiceController = (database: Sequelize) => { const transactionManager = new SequelizeTransactionManager(database); const customerInvoiceRepository = new CustomerInvoiceRepository(database, customerInvoiceMapper); const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository); - - const useCase = new GetCustomerInvoiceUseCase(customerInvoiceService, transactionManager); const presenter = getCustomerInvoicePresenter; - return new GetCustomerInvoiceController(useCase, presenter); + const useCase = new GetCustomerInvoiceUseCase( + customerInvoiceService, + transactionManager, + presenter + ); + + return new GetCustomerInvoiceController(useCase); }; diff --git a/modules/customer-invoices/src/api/domain/errors/index.ts b/modules/customer-invoices/src/api/domain/errors/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/modules/customer-invoices/src/api/domain/index.ts b/modules/customer-invoices/src/api/domain/index.ts index ed8d70d5..2c5c423d 100644 --- a/modules/customer-invoices/src/api/domain/index.ts +++ b/modules/customer-invoices/src/api/domain/index.ts @@ -1,6 +1,5 @@ export * from "./aggregates"; export * from "./entities"; -export * from "./errors"; export * from "./repositories"; export * from "./services"; export * from "./value-objects"; diff --git a/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts b/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts index aec94265..df230096 100644 --- a/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts +++ b/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts @@ -4,6 +4,8 @@ import { Collection, Result } from "@repo/rdx-utils"; import { CustomerInvoice } from "../aggregates"; export interface ICustomerInvoiceRepository { + existsById(id: UniqueID, transaction?: any): Promise>; + /** * * Persiste una nueva factura o actualiza una existente. diff --git a/modules/customer-invoices/src/api/domain/services/customer-invoice-service.interface.ts b/modules/customer-invoices/src/api/domain/services/customer-invoice-service.interface.ts index e5c2639d..ae99347a 100644 --- a/modules/customer-invoices/src/api/domain/services/customer-invoice-service.interface.ts +++ b/modules/customer-invoices/src/api/domain/services/customer-invoice-service.interface.ts @@ -4,10 +4,12 @@ import { Collection, Result } from "@repo/rdx-utils"; import { CustomerInvoice, CustomerInvoiceProps } from "../aggregates"; export interface ICustomerInvoiceService { - build(props: CustomerInvoiceProps): Result; + build(props: CustomerInvoiceProps, id?: UniqueID): Result; save(invoice: CustomerInvoice, transaction: any): Promise>; + existsById(id: UniqueID, transaction?: any): Promise>; + findByCriteria( criteria: Criteria, transaction?: any diff --git a/modules/customer-invoices/src/api/domain/services/customer-invoice.service.ts b/modules/customer-invoices/src/api/domain/services/customer-invoice.service.ts index 6f9a99cb..0533ba1f 100644 --- a/modules/customer-invoices/src/api/domain/services/customer-invoice.service.ts +++ b/modules/customer-invoices/src/api/domain/services/customer-invoice.service.ts @@ -13,10 +13,11 @@ export class CustomerInvoiceService implements ICustomerInvoiceService { * Construye un nuevo agregado CustomerInvoice a partir de props validadas. * * @param props - Las propiedades ya validadas para crear la factura. + * @param id - Identificador UUID de la factura (opcional). * @returns Result - El agregado construido o un error si falla la creación. */ - build(props: CustomerInvoiceProps): Result { - return CustomerInvoice.create(props); + build(props: CustomerInvoiceProps, id?: UniqueID): Result { + return CustomerInvoice.create(props, id); } /** @@ -31,6 +32,19 @@ export class CustomerInvoiceService implements ICustomerInvoiceService { return saved.isSuccess ? Result.ok(invoice) : Result.fail(saved.error); } + /** + * + * Comprueba si existe o no en persistencia una factura con el ID proporcionado + * + * @param id - Identificador UUID de la factura. + * @param transaction - Transacción activa para la operación. + * @returns Result - Existe la factura o no. + */ + + async existsById(id: UniqueID, transaction?: any): Promise> { + return this.repository.existsById(id, transaction); + } + /** * Obtiene una colección de facturas que cumplen con los filtros definidos en un objeto Criteria. * @@ -62,7 +76,7 @@ export class CustomerInvoiceService implements ICustomerInvoiceService { * @returns Result - Factura encontrada o error. */ async getById(id: UniqueID, transaction?: Transaction): Promise> { - return await this.repository.getById(id, transaction); + return await this.repository.findById(id, transaction); } /** 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 4baacde9..fa71364f 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 @@ -3,10 +3,12 @@ import { Application, NextFunction, Request, Response, Router } from "express"; import { Sequelize } from "sequelize"; import { CreateCustomerInvoiceCommandSchema, + GetCustomerInvoiceByIdQuerySchema, ListCustomerInvoicesQuerySchema, } from "../../../common/dto"; import { buildCreateCustomerInvoicesController, + buildGetCustomerInvoiceController, buildListCustomerInvoicesController, } from "../../controllers"; @@ -24,21 +26,21 @@ export const customerInvoicesRouter = (params: ModuleParams) => { "/", //checkTabContext, //checkUser, - validateRequest(ListCustomerInvoicesQuerySchema, "query"), + validateRequest(ListCustomerInvoicesQuerySchema, "params"), (req: Request, res: Response, next: NextFunction) => { buildListCustomerInvoicesController(database).execute(req, res, next); } ); - /*routes.get( - "/:customerInvoiceId", + routes.get( + "/:id", //checkTabContext, //checkUser, - validateRequest(GetCustomerInvoiceByIdQuerySchema, "query"), + validateRequest(GetCustomerInvoiceByIdQuerySchema, "params"), (req: Request, res: Response, next: NextFunction) => { buildGetCustomerInvoiceController(database).execute(req, res, next); } - );*/ + ); routes.post( "/", diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts index d0f3c132..2f6623eb 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts @@ -67,11 +67,11 @@ export class CustomerInvoiceMapper return CustomerInvoice.create( { status: statusOrError.data, - customerInvoiceSeries: customerInvoiceSeriesOrError.data, - customerInvoiceNumber: customerInvoiceNumberOrError.data, + invoiceSeries: customerInvoiceSeriesOrError.data, + invoiceNumber: customerInvoiceNumberOrError.data, issueDate: issueDateOrError.data, operationDate: operationDateOrError.data, - customerInvoiceCurrency, + currency: customerInvoiceCurrency, items: itemsOrErrors.data, }, idOrError.data @@ -95,7 +95,7 @@ export class CustomerInvoiceMapper issue_date: source.issueDate.toPrimitive(), operation_date: source.operationDate.toPrimitive(), invoice_language: "es", - invoice_currency: source.customerInvoiceCurrency || "EUR", + invoice_currency: source.currency || "EUR", subtotal_amount: subtotal.amount, subtotal_scale: subtotal.scale, diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts index f05360c8..91fe2677 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts @@ -11,16 +11,26 @@ export class CustomerInvoiceRepository extends SequelizeRepository implements ICustomerInvoiceRepository { - private readonly model: typeof CustomerInvoiceModel; + //private readonly model: typeof CustomerInvoiceModel; private readonly mapper!: ICustomerInvoiceMapper; constructor(database: Sequelize, mapper: ICustomerInvoiceMapper) { super(database); - this.model = database.model("CustomerInvoice") as typeof CustomerInvoiceModel; + //CustomerInvoice = database.model("CustomerInvoice") as typeof CustomerInvoiceModel; this.mapper = mapper; } + async existsById(id: UniqueID, transaction?: Transaction): Promise> { + try { + const result = await this._exists(CustomerInvoiceModel, "id", id.toString(), transaction); + + return Result.ok(Boolean(result)); + } catch (err: unknown) { + return Result.fail(errorMapper.toDomainError(err)); + } + } + /** * * Persiste una nueva factura o actualiza una existente. @@ -35,7 +45,7 @@ export class CustomerInvoiceRepository ): Promise> { try { const data = this.mapper.mapToPersistence(invoice); - await this.model.upsert(data, { transaction }); + await CustomerInvoiceModel.upsert(data, { transaction }); return Result.ok(invoice); } catch (err: unknown) { return Result.fail(errorMapper.toDomainError(err)); @@ -51,7 +61,7 @@ export class CustomerInvoiceRepository */ async findById(id: UniqueID, transaction: Transaction): Promise> { try { - const rawData = await this._findById(this.model, id.toString(), { transaction }); + const rawData = await this._findById(CustomerInvoiceModel, id.toString(), { transaction }); if (!rawData) { return Result.fail(new Error(`Invoice with id ${id} not found.`)); @@ -80,7 +90,7 @@ export class CustomerInvoiceRepository const converter = new CriteriaToSequelizeConverter(); const query = converter.convert(criteria); - const instances = await this.model.findAll({ + const instances = await CustomerInvoiceModel.findAll({ ...query, transaction, }); @@ -100,7 +110,7 @@ export class CustomerInvoiceRepository */ async deleteById(id: UniqueID, transaction: any): Promise> { try { - await this._deleteById(this.model, id, false, transaction); + await this._deleteById(CustomerInvoiceModel, id, false, transaction); return Result.ok(); } catch (err: unknown) { return Result.fail(errorMapper.toDomainError(err)); diff --git a/modules/customer-invoices/src/common/dto/request/get-customer-invoice.query.dto.ts b/modules/customer-invoices/src/common/dto/request/get-customer-invoice.query.dto.ts new file mode 100644 index 00000000..5f840aff --- /dev/null +++ b/modules/customer-invoices/src/common/dto/request/get-customer-invoice.query.dto.ts @@ -0,0 +1,13 @@ +import * as z from "zod/v4"; + +/** + * Este DTO es utilizado por el endpoint: + * `GET /customer-invoices/:id` (consultar una factura por ID). + * + */ + +export const GetCustomerInvoiceByIdQuerySchema = z.object({ + id: z.string(), +}); + +export type GetCustomerInvoiceByIdQueryDTO = z.infer; diff --git a/modules/customer-invoices/src/common/dto/request/index.ts b/modules/customer-invoices/src/common/dto/request/index.ts index 8abb398f..ef34c462 100644 --- a/modules/customer-invoices/src/common/dto/request/index.ts +++ b/modules/customer-invoices/src/common/dto/request/index.ts @@ -1,2 +1,3 @@ export * from "./create-customer-invoice.command.dto"; +export * from "./get-customer-invoice.query.dto"; export * from "./list-customer-invoices.query.dto"; diff --git a/modules/customer-invoices/src/common/dto/response/get-customer-invoice.result.dto.ts b/modules/customer-invoices/src/common/dto/response/get-customer-invoice.result.dto.ts new file mode 100644 index 00000000..b01eceac --- /dev/null +++ b/modules/customer-invoices/src/common/dto/response/get-customer-invoice.result.dto.ts @@ -0,0 +1,17 @@ +import { MetadataSchema } from "@erp/core"; +import * as z from "zod/v4"; + +export const GetCustomerInvoiceResultSchema = z.object({ + id: z.uuid(), + invoice_status: z.string(), + invoice_number: z.string(), + invoice_series: z.string(), + issue_date: z.iso.datetime({ offset: true }), + operation_date: z.iso.datetime({ offset: true }), + language_code: z.string(), + currency: z.string(), + + metadata: MetadataSchema.optional(), +}); + +export type GetCustomerInvoiceResultDTO = z.infer; diff --git a/modules/customer-invoices/src/common/dto/response/index.ts b/modules/customer-invoices/src/common/dto/response/index.ts index 6b34fd8e..422c8aa5 100644 --- a/modules/customer-invoices/src/common/dto/response/index.ts +++ b/modules/customer-invoices/src/common/dto/response/index.ts @@ -1,2 +1,3 @@ export * from "./customer-invoice-creation.result.dto"; +export * from "./get-customer-invoice.result.dto"; export * from "./list-customer-invoices.result.dto"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18c0a752..2cc7b34c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,7 +75,7 @@ importers: specifier: ^9.0.2 version: 9.0.2 luxon: - specifier: ^3.5.0 + specifier: ^3.6.1 version: 3.6.1 module-alias: specifier: ^2.2.3 @@ -151,7 +151,7 @@ importers: specifier: ^9.0.8 version: 9.0.10 '@types/luxon': - specifier: ^3.4.2 + specifier: ^3.6.2 version: 3.6.2 '@types/node': specifier: ^22.15.12