From 817dcff8c593ce1a4f376a48053001f672c4b088 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 12 Sep 2025 18:23:36 +0200 Subject: [PATCH] Facturas de cliente --- apps/server/src/index.ts | 2 +- apps/server/src/lib/modules/model-loader.ts | 2 +- .../src/lib/modules/service-registry.ts | 2 +- .../intrastructure/Invoice.repository.ts | 48 ++++----- .../src/api/application/presenters/index.ts | 1 + .../application/presenters/partials/index.ts | 0 .../presenter-registry.interface.ts | 3 +- .../presenters/presenter-registry.ts | 9 +- .../api/application/presenters/presenter.ts | 11 +++ .../repositories/repository.interface.ts | 2 +- .../express/express-controller.ts | 18 ++++ .../infrastructure/mappers/mapper-registry.ts | 2 +- .../mappers/sequelize-mapper copy.ts | 2 +- modules/core/src/common/dto/error.dto.ts | 8 +- .../axios/create-axios-data-source.ts | 2 +- .../lib/data-source/datasource.interface.ts | 2 +- .../get-customer-invoice.use-case.ts | 17 ++-- .../presenter/get-invoice.presenter.ts | 17 +++- .../helpers/has-no-undefined-fields.ts | 4 +- .../src/api/application/index.ts | 1 + .../list-customer-invoices.use-case.ts | 17 +++- .../list-customer-invoices.presenter.ts | 83 ++++++++-------- .../customer-invoice-items.full.presenter.ts | 52 ++++++++++ .../customer-invoice.full.presenter.ts | 51 ++++++++++ .../presenters/full-domain/index.ts | 2 + .../src/api/application/presenters/index.ts | 2 + .../list/customer-invoices.list.presenter.ts | 74 ++++++++++++++ .../api/application/presenters/list/index.ts | 1 + .../report-customer-invoice/index.ts | 1 + .../report-customer-invoice.use-case.ts | 20 ++-- .../reporter/customer-invoice.report.html.ts | 23 +++++ .../reporter/customer-invoice.report.pdf.ts | 53 ++++++++++ .../reporter/customer-invoice.reporter.ts | 97 ------------------- .../report-customer-invoice/reporter/index.ts | 3 +- .../entities/invoice-taxes/invoice-taxes.ts | 6 ++ .../domain/entities/item-taxes/item-taxes.ts | 6 ++ .../api/domain/value-objects/item-amount.ts | 8 ++ .../src/api/infrastructure/dependencies.ts | 79 ++++++++++----- .../express/controllers/index.ts | 1 + .../report-customer-invoice.controller.ts | 9 +- .../express/customer-invoices.routes.ts | 1 + .../sequelize/customer-invoice.repository.ts | 2 +- .../create-customer-invoice.response.dto.ts | 27 ++++-- ...get-customer-invoice-by-id.response.dto.ts | 23 +++-- .../list-customer-invoices.response.dto.ts | 12 +-- ...tomer-invoice-items-sortable-table-row.tsx | 2 +- .../src/api/infrastructure/dependencies.ts | 38 +++----- .../rdx-ddd/src/value-objects/percentage.ts | 7 ++ .../rdx-ddd/src/value-objects/quantity.ts | 7 ++ 49 files changed, 569 insertions(+), 291 deletions(-) create mode 100644 modules/core/src/api/application/presenters/partials/index.ts create mode 100644 modules/core/src/api/application/presenters/presenter.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/full-domain/customer-invoice-items.full.presenter.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/full-domain/customer-invoice.full.presenter.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/full-domain/index.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/index.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/list/customer-invoices.list.presenter.ts create mode 100644 modules/customer-invoices/src/api/application/presenters/list/index.ts create mode 100644 modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.html.ts create mode 100644 modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.pdf.ts delete mode 100644 modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.reporter.ts diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index d77326ea..1aa333fd 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -19,7 +19,7 @@ export const currentState = { host: ENV.HOST, port: ENV.PORT, environment: ENV.NODE_ENV, - connections: {} as Record, + connections: {} as Record, }; // ───────────────────────────────────────────────────────────────────────────── diff --git a/apps/server/src/lib/modules/model-loader.ts b/apps/server/src/lib/modules/model-loader.ts index fbf355a4..230f9e81 100644 --- a/apps/server/src/lib/modules/model-loader.ts +++ b/apps/server/src/lib/modules/model-loader.ts @@ -7,7 +7,7 @@ import { logger } from "../logger"; */ type SequelizeLike = { sync: (options?: any) => Promise; - models: Record; + models: Record; }; type ModelStatic = { diff --git a/apps/server/src/lib/modules/service-registry.ts b/apps/server/src/lib/modules/service-registry.ts index 133b92db..06f1484d 100644 --- a/apps/server/src/lib/modules/service-registry.ts +++ b/apps/server/src/lib/modules/service-registry.ts @@ -1,4 +1,4 @@ -const services: Record = {}; +const services: Record = {}; /** * Registra un objeto de servicio (API) bajo un nombre. diff --git a/modules.bak/invoices/src/server/intrastructure/Invoice.repository.ts b/modules.bak/invoices/src/server/intrastructure/Invoice.repository.ts index c890826a..edfb5b8f 100644 --- a/modules.bak/invoices/src/server/intrastructure/Invoice.repository.ts +++ b/modules.bak/invoices/src/server/intrastructure/Invoice.repository.ts @@ -7,14 +7,11 @@ import { IInvoiceRepository, Invoice } from "../domain"; import { IInvoiceMapper } from "./mappers"; export type QueryParams = { - pagination: Record; - filters: Record; + pagination: Record; + filters: Record; }; -export class InvoiceRepository - extends SequelizeRepository - implements IInvoiceRepository -{ +export class InvoiceRepository extends SequelizeRepository implements IInvoiceRepository { protected mapper: IInvoiceMapper; public constructor(props: { @@ -33,10 +30,7 @@ export class InvoiceRepository { association: "items" }, { association: "participants", - include: [ - { association: "shippingAddress" }, - { association: "billingAddress" }, - ], + include: [{ association: "shippingAddress" }, { association: "billingAddress" }], }, ], }); @@ -48,28 +42,21 @@ export class InvoiceRepository return this.mapper.mapToDomain(rawContact); } - public async findAll( - queryCriteria?: IQueryCriteria - ): Promise> { - const { rows, count } = await this._findAll( - "Invoice_Model", - queryCriteria, - { - include: [ - { - association: "participants", - separate: true, - }, - ], - } - ); + public async findAll(queryCriteria?: IQueryCriteria): Promise> { + const { rows, count } = await this._findAll("Invoice_Model", queryCriteria, { + include: [ + { + association: "participants", + separate: true, + }, + ], + }); return this.mapper.mapArrayAndCountToDomain(rows, count); } public async save(invoice: Invoice): Promise { - const { items, participants, ...invoiceData } = - this.mapper.mapToPersistence(invoice); + const { items, participants, ...invoiceData } = this.mapper.mapToPersistence(invoice); await this.adapter .getModel("Invoice_Model") @@ -85,10 +72,9 @@ export class InvoiceRepository await this.adapter .getModel("InvoiceParticipantAddress_Model") - .bulkCreate( - [participants[0].billingAddress, participants[0].shippingAddress], - { transaction: this.transaction } - ); + .bulkCreate([participants[0].billingAddress, participants[0].shippingAddress], { + transaction: this.transaction, + }); } public removeById(id: UniqueID): Promise { diff --git a/modules/core/src/api/application/presenters/index.ts b/modules/core/src/api/application/presenters/index.ts index 334a04cc..f8c1261a 100644 --- a/modules/core/src/api/application/presenters/index.ts +++ b/modules/core/src/api/application/presenters/index.ts @@ -1,3 +1,4 @@ +export * from "./presenter"; export * from "./presenter-registry"; export * from "./presenter-registry.interface"; export * from "./presenter.interface"; diff --git a/modules/core/src/api/application/presenters/partials/index.ts b/modules/core/src/api/application/presenters/partials/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/modules/core/src/api/application/presenters/presenter-registry.interface.ts b/modules/core/src/api/application/presenters/presenter-registry.interface.ts index 6f54c703..286b15a6 100644 --- a/modules/core/src/api/application/presenters/presenter-registry.interface.ts +++ b/modules/core/src/api/application/presenters/presenter-registry.interface.ts @@ -4,8 +4,9 @@ import { IPresenter } from "./presenter.interface"; * 🔑 Claves de proyección comunes para seleccionar presenters */ export type PresenterKey = { + resource: string; projection: "FULL" | "LIST" | "REPORT" | (string & {}); - format?: "JSON" | "PDF" | "CSV" | (string & {}); + format?: "JSON" | "HTML" | "PDF" | "CSV" | (string & {}); version?: number; // 1 | 2 locale?: string; // es | en | fr }; diff --git a/modules/core/src/api/application/presenters/presenter-registry.ts b/modules/core/src/api/application/presenters/presenter-registry.ts index e867cb63..8f7245a1 100644 --- a/modules/core/src/api/application/presenters/presenter-registry.ts +++ b/modules/core/src/api/application/presenters/presenter-registry.ts @@ -49,10 +49,12 @@ export class InMemoryPresenterRegistry implements IPresenterRegistry { } if (!this.registry.has(exactKey)) { - throw new ApplicationError(`Error. Presenter ${key} not registred!`); + throw new ApplicationError( + `Error. Presenter ${key.resource} ${key.projection} not registred!` + ); } - throw new ApplicationError(`Error. Presenter ${key} not registred!`); + throw new ApplicationError(`Error. Presenter ${key.resource} ${key.projection} not registred!`); } registerPresenter( @@ -76,8 +78,9 @@ export class InMemoryPresenterRegistry implements IPresenterRegistry { * 🔹 Construye la clave única para el registro. */ private _buildKey(key: PresenterKey): string { - const { projection, format, version, locale } = key; + const { resource, projection, format, version, locale } = key; return [ + resource.toLowerCase(), projection.toLowerCase(), format!.toLowerCase(), version ?? "latest", diff --git a/modules/core/src/api/application/presenters/presenter.ts b/modules/core/src/api/application/presenters/presenter.ts new file mode 100644 index 00000000..b323aef4 --- /dev/null +++ b/modules/core/src/api/application/presenters/presenter.ts @@ -0,0 +1,11 @@ +import { IPresenterRegistry } from "./presenter-registry.interface"; +import { IPresenter } from "./presenter.interface"; + +export type IPresenterParams = { + presenterRegistry: IPresenterRegistry; +} & Record; + +export abstract class Presenter implements IPresenter { + constructor(protected presenterRegistry: IPresenterRegistry) {} + abstract toOutput(source: unknown): unknown; +} diff --git a/modules/core/src/api/domain/repositories/repository.interface.ts b/modules/core/src/api/domain/repositories/repository.interface.ts index 7b413ec7..b7c99902 100644 --- a/modules/core/src/api/domain/repositories/repository.interface.ts +++ b/modules/core/src/api/domain/repositories/repository.interface.ts @@ -4,7 +4,7 @@ import { Collection, Result } from "@repo/rdx-utils"; * Tipo para los parámetros que reciben los métodos de los mappers * Es un objeto que puede contener cualquier cosa. */ -export type MapperParamsType = Record; +export type MapperParamsType = Record; /** * 🧭 Mapper de Dominio (Persistencia ↔ Dominio/Agregado) diff --git a/modules/core/src/api/infrastructure/express/express-controller.ts b/modules/core/src/api/infrastructure/express/express-controller.ts index 3acdd179..53e86a12 100644 --- a/modules/core/src/api/infrastructure/express/express-controller.ts +++ b/modules/core/src/api/infrastructure/express/express-controller.ts @@ -111,6 +111,24 @@ export abstract class ExpressController { return this.res.sendStatus(httpStatus.NO_CONTENT); } + public sendFile(filepath: string) { + return this.res.sendFile(filepath); + } + + public downloadFile(filepath: string, filename: string, done?: any) { + return this.res.download(filepath, filename, done); + } + + public downloadPDF(pdfBuffer: Buffer, filename: string) { + this.res.set({ + "Content-Type": "application/pdf", + "Content-Disposition": `attachment; filename=${filename}`, + //"Content-Length": buffer.length, + }); + + return this.res.send(pdfBuffer); + } + protected clientError(message: string, errors?: any[] | any) { return this.handleApiError( new ValidationApiError(message, Array.isArray(errors) ? errors : [errors]) diff --git a/modules/core/src/api/infrastructure/mappers/mapper-registry.ts b/modules/core/src/api/infrastructure/mappers/mapper-registry.ts index 6d50b86f..9176de5b 100644 --- a/modules/core/src/api/infrastructure/mappers/mapper-registry.ts +++ b/modules/core/src/api/infrastructure/mappers/mapper-registry.ts @@ -6,7 +6,7 @@ export class InMemoryMapperRegistry implements IMapperRegistry { private readModelMappers: Map = new Map(); getDomainMapper(key: MapperProjectionKey): T { - if (!this.readModelMappers.has(key)) { + if (!this.domainMappers.has(key)) { throw new InfrastructureError(`Error. Domain model mapper ${key} not registred!`); } diff --git a/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-mapper copy.ts b/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-mapper copy.ts index 87e107f3..90540f7b 100644 --- a/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-mapper copy.ts +++ b/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-mapper copy.ts @@ -1,7 +1,7 @@ import { Collection, Result } from "@repo/rdx-utils"; import { Model } from "sequelize"; -export type MapperParamsType = Record; +export type MapperParamsType = Record; interface IDomainMapper { mapToDomain(source: TModel, params?: MapperParamsType): Result; diff --git a/modules/core/src/common/dto/error.dto.ts b/modules/core/src/common/dto/error.dto.ts index b8f46bd0..fbfa70eb 100644 --- a/modules/core/src/common/dto/error.dto.ts +++ b/modules/core/src/common/dto/error.dto.ts @@ -10,11 +10,11 @@ export interface IErrorResponseDTO { export interface IErrorContextResponseDTO { user?: unknown; - params?: Record; - query?: Record; - body?: Record; + params?: Record; + query?: Record; + body?: Record; } export interface IErrorExtraResponseDTO { - errors: Record[]; + errors: Record[]; } diff --git a/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts b/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts index 0345bc64..d03fdb45 100644 --- a/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts +++ b/modules/core/src/web/lib/data-source/axios/create-axios-data-source.ts @@ -41,7 +41,7 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => { return { getBaseUrl: () => (client as AxiosInstance).getUri(), - getList: async (resource: string, params?: Record): Promise => { + getList: async (resource: string, params?: Record): Promise => { const { pagination } = params as any; const res = await (client as AxiosInstance).get(resource, { diff --git a/modules/core/src/web/lib/data-source/datasource.interface.ts b/modules/core/src/web/lib/data-source/datasource.interface.ts index e76f025e..b3e81b1c 100644 --- a/modules/core/src/web/lib/data-source/datasource.interface.ts +++ b/modules/core/src/web/lib/data-source/datasource.interface.ts @@ -14,7 +14,7 @@ export interface ICustomParams { export interface IDataSource { getBaseUrl(): string; - getList(resource: string, params?: Record): Promise; + getList(resource: string, params?: Record): Promise; getOne(resource: string, id: string | number): Promise; getMany(resource: string, ids: Array): Promise; createOne(resource: string, data: Partial): Promise; 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 5d27136e..752da3bf 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,8 +1,8 @@ -import { ITransactionManager } from "@erp/core/api"; +import { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { CustomerInvoiceService } from "../../domain"; -import { GetCustomerInvoiceAssembler } from "./assembler"; +import { CustomerInvoiceFullPresenter } from "../presenters/full-domain"; type GetCustomerInvoiceUseCaseInput = { companyId: UniqueID; @@ -13,19 +13,22 @@ export class GetCustomerInvoiceUseCase { constructor( private readonly service: CustomerInvoiceService, private readonly transactionManager: ITransactionManager, - private readonly assembler: GetCustomerInvoiceAssembler, + private readonly presenterRegistry: IPresenterRegistry ) {} public execute(params: GetCustomerInvoiceUseCaseInput) { const { invoice_id, companyId } = params; const idOrError = UniqueID.create(invoice_id); - if (idOrError.isFailure) { return Result.fail(idOrError.error); } const invoiceId = idOrError.data; + const presenter = this.presenterRegistry.getPresenter({ + resource: "customer-invoice", + projection: "FULL", + }) as CustomerInvoiceFullPresenter; return this.transactionManager.complete(async (transaction) => { try { @@ -38,8 +41,10 @@ export class GetCustomerInvoiceUseCase { return Result.fail(invoiceOrError.error); } - const getDTO = this.assembler.toDTO(invoiceOrError.data); - return Result.ok(getDTO); + const customerInvoice = invoiceOrError.data; + const dto = presenter.toOutput(customerInvoice); + + return Result.ok(dto); } catch (error: unknown) { return Result.fail(error as Error); } diff --git a/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/get-invoice.presenter.ts b/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/get-invoice.presenter.ts index 3ba1293c..0e7fa2df 100644 --- a/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/get-invoice.presenter.ts +++ b/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/get-invoice.presenter.ts @@ -1,16 +1,23 @@ -import { UpdateCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common/dto"; +import { IPresenter, IPresenterRegistry } from "@erp/core/api"; import { toEmptyString } from "@repo/rdx-ddd"; +import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto"; import { CustomerInvoice } from "../../../domain"; import { GetCustomerInvoiceItemsPresenter } from "./get-invoice-items.presenter"; -export class GetCustomerInvoicePresenter { +export class GetCustomerInvoicePresentwer implements IPresenter { private _itemsPresenter!: GetCustomerInvoiceItemsPresenter; - constructor() { - this._itemsPresenter = new GetCustomerInvoiceItemsPresenter(); + constructor(private presenterRegistry: IPresenterRegistry) { + this._itemsPresenter = this.presenterRegistry.getPresenter({ + resource: "customer-invoice-item", + projection: "FULL", + }); } - public toDTO(invoice: CustomerInvoice): UpdateCustomerInvoiceByIdResponseDTO { + toOutput(params: { + invoice: CustomerInvoice; + }): GetCustomerInvoiceByIdResponseDTO { + const { invoice } = params; const items = this._itemsPresenter.toDTO(invoice); return { diff --git a/modules/customer-invoices/src/api/application/helpers/has-no-undefined-fields.ts b/modules/customer-invoices/src/api/application/helpers/has-no-undefined-fields.ts index beabd781..89984e11 100644 --- a/modules/customer-invoices/src/api/application/helpers/has-no-undefined-fields.ts +++ b/modules/customer-invoices/src/api/application/helpers/has-no-undefined-fields.ts @@ -19,7 +19,7 @@ * @returns true si el objeto no tiene campos undefined, false en caso contrario. */ -export function hasNoUndefinedFields>( +export function hasNoUndefinedFields>( obj: T ): obj is { [K in keyof T]-?: Exclude } { return Object.values(obj).every((value) => value !== undefined); @@ -43,7 +43,7 @@ export function hasNoUndefinedFields>( * */ -export function hasUndefinedFields>( +export function hasUndefinedFields>( obj: T ): obj is { [K in keyof T]-?: Exclude } { return !hasNoUndefinedFields(obj); diff --git a/modules/customer-invoices/src/api/application/index.ts b/modules/customer-invoices/src/api/application/index.ts index 97ba1ed7..e3d93e2d 100644 --- a/modules/customer-invoices/src/api/application/index.ts +++ b/modules/customer-invoices/src/api/application/index.ts @@ -4,3 +4,4 @@ export * from "./get-customer-invoice"; export * from "./list-customer-invoices"; export * from "./report-customer-invoice"; //export * from "./update-customer-invoice"; +export * from "./presenters"; 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 d9f7fd24..5937c964 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 @@ -1,11 +1,11 @@ -import { ITransactionManager } from "@erp/core/api"; +import { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { Criteria } from "@repo/rdx-criteria/server"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; import { CustomerInvoiceListResponseDTO } from "../../../common/dto"; import { CustomerInvoiceService } from "../../domain"; -import { ListCustomerInvoicesAssembler } from "./assembler"; +import { CustomerInvoicesListPresenter } from "../presenters/list"; type ListCustomerInvoicesUseCaseInput = { companyId: UniqueID; @@ -16,13 +16,17 @@ export class ListCustomerInvoicesUseCase { constructor( private readonly service: CustomerInvoiceService, private readonly transactionManager: ITransactionManager, - private readonly assembler: ListCustomerInvoicesAssembler + private readonly presenterRegistry: IPresenterRegistry ) {} public execute( params: ListCustomerInvoicesUseCaseInput ): Promise> { const { criteria, companyId } = params; + const presenter = this.presenterRegistry.getPresenter({ + resource: "customer-invoice", + projection: "LIST", + }) as CustomerInvoicesListPresenter; return this.transactionManager.complete(async (transaction: Transaction) => { try { @@ -36,7 +40,12 @@ export class ListCustomerInvoicesUseCase { return Result.fail(result.error); } - const dto = this.assembler.toDTO(result.data, criteria); + const customerInvoices = result.data; + const dto = presenter.toOutput({ + customerInvoices, + criteria, + }); + return Result.ok(dto); } catch (error: unknown) { return Result.fail(error as Error); diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/list-customer-invoices.presenter.ts b/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/list-customer-invoices.presenter.ts index a6a94b5d..8ff54bcd 100644 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/list-customer-invoices.presenter.ts +++ b/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/list-customer-invoices.presenter.ts @@ -1,56 +1,57 @@ -import { IPresenter } from "@erp/core/api"; +import { Presenter } from "@erp/core/api"; import { CustomerInvoiceListDTO } from "@erp/customer-invoices/api/infrastructure"; import { Criteria } from "@repo/rdx-criteria/server"; import { toEmptyString } from "@repo/rdx-ddd"; import { ArrayElement, Collection } from "@repo/rdx-utils"; import { CustomerInvoiceListResponseDTO } from "../../../../common/dto"; -export class ListCustomerInvoicesPresenter implements IPresenter { +export class ListCustomerInvoicesPresenter extends Presenter { + protected _mapInvoice(invoice: CustomerInvoiceListDTO) { + const recipientDTO = invoice.recipient.toObjectString(); + + const invoiceDTO: ArrayElement = { + id: invoice.id.toString(), + company_id: invoice.companyId.toString(), + customer_id: invoice.customerId.toString(), + + invoice_number: invoice.invoiceNumber.toString(), + status: invoice.status.toPrimitive(), + series: toEmptyString(invoice.series, (value) => value.toString()), + + invoice_date: invoice.invoiceDate.toDateString(), + operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()), + + recipient: { + customer_id: invoice.customerId.toString(), + ...recipientDTO, + }, + + language_code: invoice.languageCode.code, + currency_code: invoice.currencyCode.code, + + taxes: invoice.taxes, + + subtotal_amount: invoice.subtotalAmount.toObjectString(), + discount_amount: invoice.discountAmount.toObjectString(), + taxable_amount: invoice.taxableAmount.toObjectString(), + taxes_amount: invoice.taxesAmount.toObjectString(), + total_amount: invoice.totalAmount.toObjectString(), + + metadata: { + entity: "customer-invoice", + }, + }; + + return invoiceDTO; + } + toOutput(params: { customerInvoices: Collection; criteria: Criteria; }): CustomerInvoiceListResponseDTO { const { customerInvoices, criteria } = params; - const invoices = customerInvoices.map((invoice) => { - const recipientDTO = invoice.recipient.toObjectString(); - - const invoiceDTO: ArrayElement = { - id: invoice.id.toString(), - company_id: invoice.companyId.toString(), - customer_id: invoice.customerId.toString(), - - invoice_number: invoice.invoiceNumber.toString(), - status: invoice.status.toPrimitive(), - series: toEmptyString(invoice.series, (value) => value.toString()), - - invoice_date: invoice.invoiceDate.toDateString(), - operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()), - - recipient: { - customer_id: invoice.customerId.toString(), - ...recipientDTO, - }, - - language_code: invoice.languageCode.code, - currency_code: invoice.currencyCode.code, - - taxes: invoice.taxes, - - subtotal_amount: invoice.subtotalAmount.toObjectString(), - discount_amount: invoice.discountAmount.toObjectString(), - taxable_amount: invoice.taxableAmount.toObjectString(), - taxes_amount: invoice.taxesAmount.toObjectString(), - total_amount: invoice.totalAmount.toObjectString(), - - metadata: { - entity: "customer-invoice", - }, - }; - - return invoiceDTO; - }); - + const invoices = customerInvoices.map((invoice) => this._mapInvoice(invoice)); const totalItems = customerInvoices.total(); return { diff --git a/modules/customer-invoices/src/api/application/presenters/full-domain/customer-invoice-items.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/full-domain/customer-invoice-items.full.presenter.ts new file mode 100644 index 00000000..179b2063 --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/full-domain/customer-invoice-items.full.presenter.ts @@ -0,0 +1,52 @@ +import { Presenter } from "@erp/core/api"; +import { toEmptyString } from "@repo/rdx-ddd"; +import { ArrayElement } from "@repo/rdx-utils"; +import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto"; +import { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../domain"; + +type GetCustomerInvoiceItemByInvoiceIdResponseDTO = ArrayElement< + GetCustomerInvoiceByIdResponseDTO["items"] +>; + +export class CustomerInvoiceItemsFullPresenter extends Presenter { + protected _map( + invoiceItem: CustomerInvoiceItem, + index: number + ): GetCustomerInvoiceItemByInvoiceIdResponseDTO { + const allAmounts = invoiceItem.getAllAmounts(); + + return { + id: invoiceItem.id.toPrimitive(), + position: String(index), + description: toEmptyString(invoiceItem.description, (value) => value.toPrimitive()), + + quantity: invoiceItem.quantity.match( + (quantity) => quantity.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + unit_amount: invoiceItem.unitAmount.match( + (unitAmount) => unitAmount.toObjectString(), + () => ({ value: "", scale: "", currency_code: "" }) + ), + + taxes: invoiceItem.taxes.getCodesToString(), + + subtotal_amount: allAmounts.subtotalAmount.toObjectString(), + + discount_percentage: invoiceItem.discountPercentage.match( + (discountPercentage) => discountPercentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + discount_amount: allAmounts.discountAmount.toObjectString(), + taxable_amount: allAmounts.taxableAmount.toObjectString(), + taxes_amount: allAmounts.taxesAmount.toObjectString(), + total_amount: allAmounts.totalAmount.toObjectString(), + }; + } + + toOutput(invoiceItems: CustomerInvoiceItems): GetCustomerInvoiceByIdResponseDTO["items"] { + return invoiceItems.map(this._map); + } +} diff --git a/modules/customer-invoices/src/api/application/presenters/full-domain/customer-invoice.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/full-domain/customer-invoice.full.presenter.ts new file mode 100644 index 00000000..7ddae3ca --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/full-domain/customer-invoice.full.presenter.ts @@ -0,0 +1,51 @@ +import { Presenter } from "@erp/core/api"; +import { toEmptyString } from "@repo/rdx-ddd"; +import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto"; +import { CustomerInvoice } from "../../../domain"; +import { CustomerInvoiceItemsFullPresenter } from "./customer-invoice-items.full.presenter"; + +export class CustomerInvoiceFullPresenter extends Presenter { + toOutput(invoice: CustomerInvoice): GetCustomerInvoiceByIdResponseDTO { + const itemsPresenter = this.presenterRegistry.getPresenter({ + resource: "customer-invoice-items", + projection: "FULL", + }) as CustomerInvoiceItemsFullPresenter; + + const items = itemsPresenter.toOutput(invoice.items); + const allAmounts = invoice.getAllAmounts(); + + return { + id: invoice.id.toString(), + company_id: invoice.companyId.toString(), + + invoice_number: invoice.invoiceNumber.toString(), + status: invoice.status.toPrimitive(), + series: toEmptyString(invoice.series, (value) => value.toString()), + + invoice_date: invoice.invoiceDate.toDateString(), + operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()), + + notes: toEmptyString(invoice.notes, (value) => value.toString()), + + language_code: invoice.languageCode.toString(), + currency_code: invoice.currencyCode.toString(), + + taxes: invoice.taxes.getCodesToString(), + + subtotal_amount: allAmounts.subtotalAmount.toObjectString(), + + discount_percentage: invoice.discountPercentage.toObjectString(), + + discount_amount: allAmounts.discountAmount.toObjectString(), + taxable_amount: allAmounts.taxableAmount.toObjectString(), + taxes_amount: allAmounts.taxesAmount.toObjectString(), + total_amount: allAmounts.totalAmount.toObjectString(), + + items, + + metadata: { + entity: "customer-invoices", + }, + }; + } +} diff --git a/modules/customer-invoices/src/api/application/presenters/full-domain/index.ts b/modules/customer-invoices/src/api/application/presenters/full-domain/index.ts new file mode 100644 index 00000000..87f61fac --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/full-domain/index.ts @@ -0,0 +1,2 @@ +export * from "./customer-invoice-items.full.presenter"; +export * from "./customer-invoice.full.presenter"; diff --git a/modules/customer-invoices/src/api/application/presenters/index.ts b/modules/customer-invoices/src/api/application/presenters/index.ts new file mode 100644 index 00000000..cce380e7 --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/index.ts @@ -0,0 +1,2 @@ +export * from "./full-domain"; +export * from "./list"; diff --git a/modules/customer-invoices/src/api/application/presenters/list/customer-invoices.list.presenter.ts b/modules/customer-invoices/src/api/application/presenters/list/customer-invoices.list.presenter.ts new file mode 100644 index 00000000..054f3067 --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/list/customer-invoices.list.presenter.ts @@ -0,0 +1,74 @@ +import { Presenter } from "@erp/core/api"; +import { CustomerInvoiceListDTO } from "@erp/customer-invoices/api/infrastructure"; +import { Criteria } from "@repo/rdx-criteria/server"; +import { toEmptyString } from "@repo/rdx-ddd"; +import { ArrayElement, Collection } from "@repo/rdx-utils"; +import { CustomerInvoiceListResponseDTO } from "../../../../common/dto"; + +export class CustomerInvoicesListPresenter extends Presenter { + protected _map(invoice: CustomerInvoiceListDTO) { + const recipientDTO = invoice.recipient.toObjectString(); + + const invoiceDTO: ArrayElement = { + id: invoice.id.toString(), + company_id: invoice.companyId.toString(), + customer_id: invoice.customerId.toString(), + + invoice_number: invoice.invoiceNumber.toString(), + status: invoice.status.toPrimitive(), + series: toEmptyString(invoice.series, (value) => value.toString()), + + invoice_date: invoice.invoiceDate.toDateString(), + operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()), + + recipient: { + customer_id: invoice.customerId.toString(), + ...recipientDTO, + }, + + language_code: invoice.languageCode.code, + currency_code: invoice.currencyCode.code, + + taxes: invoice.taxes, + + subtotal_amount: invoice.subtotalAmount.toObjectString(), + discount_amount: invoice.discountAmount.toObjectString(), + taxable_amount: invoice.taxableAmount.toObjectString(), + taxes_amount: invoice.taxesAmount.toObjectString(), + total_amount: invoice.totalAmount.toObjectString(), + + metadata: { + entity: "customer-invoice", + }, + }; + + return invoiceDTO; + } + + toOutput(params: { + customerInvoices: Collection; + criteria: Criteria; + }): CustomerInvoiceListResponseDTO { + const { customerInvoices, criteria } = params; + + const invoices = customerInvoices.map((invoice) => this._map(invoice)); + const totalItems = customerInvoices.total(); + + return { + page: criteria.pageNumber, + per_page: criteria.pageSize, + total_pages: Math.ceil(totalItems / criteria.pageSize), + total_items: totalItems, + items: invoices, + metadata: { + entity: "customer-invoices", + criteria: criteria.toJSON(), + //links: { + // self: `/api/customer-invoices?page=${criteria.pageNumber}&per_page=${criteria.pageSize}`, + // first: `/api/customer-invoices?page=1&per_page=${criteria.pageSize}`, + // last: `/api/customer-invoices?page=${Math.ceil(totalItems / criteria.pageSize)}&per_page=${criteria.pageSize}`, + //}, + }, + }; + } +} diff --git a/modules/customer-invoices/src/api/application/presenters/list/index.ts b/modules/customer-invoices/src/api/application/presenters/list/index.ts new file mode 100644 index 00000000..9798e040 --- /dev/null +++ b/modules/customer-invoices/src/api/application/presenters/list/index.ts @@ -0,0 +1 @@ +export * from "./customer-invoices.list.presenter"; diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/index.ts b/modules/customer-invoices/src/api/application/report-customer-invoice/index.ts index ebba15ce..08f1ec1c 100644 --- a/modules/customer-invoices/src/api/application/report-customer-invoice/index.ts +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/index.ts @@ -1 +1,2 @@ export * from "./report-customer-invoice.use-case"; +export * from "./reporter"; diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/report-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/report-customer-invoice/report-customer-invoice.use-case.ts index 9c1b0437..dedfa196 100644 --- a/modules/customer-invoices/src/api/application/report-customer-invoice/report-customer-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/report-customer-invoice.use-case.ts @@ -1,8 +1,7 @@ -import { ITransactionManager } from "@erp/core/api"; +import { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { CustomerInvoiceService } from "../../domain"; -import { ReportCustomerInvoiceAssembler } from "./assembler"; type ReportCustomerInvoiceUseCaseInput = { companyId: UniqueID; @@ -13,7 +12,7 @@ export class ReportCustomerInvoiceUseCase { constructor( private readonly service: CustomerInvoiceService, private readonly transactionManager: ITransactionManager, - private readonly assembler: ReportCustomerInvoiceAssembler + private readonly presenterRegistry: IPresenterRegistry ) {} public execute(params: ReportCustomerInvoiceUseCaseInput) { @@ -26,6 +25,11 @@ export class ReportCustomerInvoiceUseCase { } const invoiceId = idOrError.data; + const pdfPresenter = this.presenterRegistry.getPresenter({ + resource: "customer-invoice", + projection: "REPORT", + format: "PDF", + }); return this.transactionManager.complete(async (transaction) => { try { @@ -38,10 +42,12 @@ export class ReportCustomerInvoiceUseCase { return Result.fail(invoiceOrError.error); } - const invoiceDto = this.registry.getPresenter("").toDTO(invoideOIrError.data); - - const pdfData = this.assembler.toPDF(invoiceDto); - return Result.ok(pdfData); + const invoice = invoiceOrError.data; + const pdfData = pdfPresenter.toOutput(invoiceOrError.data); + return Result.ok({ + data: pdfData, + filename: `invoice-${invoice.invoiceNumber}.pdf`, + }); } catch (error: unknown) { return Result.fail(error as Error); } diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.html.ts b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.html.ts new file mode 100644 index 00000000..d26b2b1a --- /dev/null +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.html.ts @@ -0,0 +1,23 @@ +import { Presenter } from "@erp/core/api"; +import * as handlebars from "handlebars"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { CustomerInvoice } from "../../../domain"; + +export class CustomerInvoiceReportHTMLPresenter extends Presenter { + toOutput(customerInvoice: CustomerInvoice): string { + const dtoPresenter = this.presenterRegistry.getPresenter({ + resource: "customer-invoice", + projection: "FULL", + }); + + const invoiceDTO = dtoPresenter.toOutput(customerInvoice); + + // Obtener y compilar la plantilla HTML + const templateHtml = readFileSync( + path.join(__dirname, "./templates/quote/template.hbs") + ).toString(); + const template = handlebars.compile(templateHtml, {}); + return template(invoiceDTO); + } +} diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.pdf.ts b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.pdf.ts new file mode 100644 index 00000000..8ceaafa5 --- /dev/null +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.report.pdf.ts @@ -0,0 +1,53 @@ +import { Presenter } from "@erp/core/api"; +import puppeteer from "puppeteer"; +import report from "puppeteer-report"; +import { CustomerInvoice } from "../../../domain"; +import { CustomerInvoiceReportHTMLPresenter } from "./customer-invoice.report.html"; + +export interface ICustomerInvoiceReporter { + toHTML: (invoice: CustomerInvoice) => Promise; + toPDF: (invoice: CustomerInvoice) => Promise; +} + +// https://plnkr.co/edit/lWk6Yd?preview + +export class CustomerInvoiceReportPDFPresenter extends Presenter { + async toOutput(customerInvoice: CustomerInvoice): Promise { + const htmlPresenter = this.presenterRegistry.getPresenter({ + resource: "customer-invoice", + projection: "REPORT", + format: "HTML", + }) as CustomerInvoiceReportHTMLPresenter; + + const htmlData = htmlPresenter.toOutput(customerInvoice); + + // Generar el PDF con Puppeteer + const browser = await puppeteer.launch({ + args: [ + "--disable-extensions", + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage", + "--disable-gpu", + ], + }); + + const page = await browser.newPage(); + const navigationPromise = page.waitForNavigation(); + await page.setContent(htmlData, { waitUntil: "networkidle2" }); + + await navigationPromise; + const reportPDF = await report.pdfPage(page, { + format: "A4", + margin: { + bottom: "10mm", + left: "10mm", + right: "10mm", + top: "10mm", + }, + }); + + await browser.close(); + return Buffer.from(reportPDF); + } +} diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.reporter.ts b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.reporter.ts deleted file mode 100644 index f052e72c..00000000 --- a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/customer-invoice.reporter.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { toEmptyString } from "@repo/rdx-ddd"; -import * as handlebars from "handlebars"; -import { readFileSync } from "node:fs"; -import path from "node:path"; -import puppeteer from "puppeteer"; -import report from "puppeteer-report"; -import { CustomerInvoice } from "../../../domain"; - -export interface ICustomerInvoiceReporter { - toHTML: (invoice: CustomerInvoice) => Promise; - toPDF: (invoice: CustomerInvoice) => Promise; -} - -// https://plnkr.co/edit/lWk6Yd?preview - -export const CustomerInvoiceReporter: ICustomerInvoiceReporter = { - toHTML: async (invoice: CustomerInvoice): Promise => { - const quote_dto = await map(quote, context); - - // Obtener y compilar la plantilla HTML - const templateHtml = readFileSync( - path.join(__dirname, "./templates/quote/template.hbs") - ).toString(); - const template = handlebars.compile(templateHtml, {}); - return template(quote_dto); - }, - - toPDF: async (quote: CustomerInvoice, context: ISalesContext): Promise => { - const html = await CustomerInvoiceReporter.toHTML(quote, context); - - // Generar el PDF con Puppeteer - const browser = await puppeteer.launch({ - args: [ - "--disable-extensions", - "--no-sandbox", - "--disable-setuid-sandbox", - "--disable-dev-shm-usage", - "--disable-gpu", - ], - }); - - const page = await browser.newPage(); - const navigationPromise = page.waitForNavigation(); - await page.setContent(html, { waitUntil: "networkidle2" }); - - await navigationPromise; - const reportPDF = await report.pdfPage(page, { - format: "A4", - margin: { - bottom: "10mm", - left: "10mm", - right: "10mm", - top: "10mm", - }, - }); - - await browser.close(); - return Buffer.from(reportPDF); - }, -}; - -const map = async (invoice: CustomerInvoice) => { - return { - id: invoice.id.toString(), - company_id: invoice.companyId.toString(), - - invoice_number: invoice.invoiceNumber.toString(), - status: invoice.status.toPrimitive(), - series: toEmptyString(invoice.series, (value) => value.toString()), - - invoice_date: invoice.invoiceDate.toDateString(), - operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()), - - notes: toEmptyString(invoice.notes, (value) => value.toString()), - - language_code: invoice.languageCode.toString(), - currency_code: invoice.currencyCode.toString(), - }; -}; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const quoteItemPresenter = ( - items: ICollection, - context: ISalesContext -): any[] => - items.totalCount > 0 - ? items.items.map((item: CustomerInvoiceItem) => ({ - id_article: item.idArticle.toString(), - description: item.description.toString(), - quantity: item.quantity.toFormat(), - unit_price: item.unitPrice.toFormat(), - subtotal_price: item.subtotalPrice.toFormat(), - discount: item.discount.toFormat(), - total_price: item.totalPrice.toFormat(), - })) - : []; -2; diff --git a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/index.ts b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/index.ts index 8f6ec562..ede4d997 100644 --- a/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/index.ts +++ b/modules/customer-invoices/src/api/application/report-customer-invoice/reporter/index.ts @@ -1 +1,2 @@ -export * from "./customer-invoice.reporter"; +export * from "./customer-invoice.report.html"; +export * from "./customer-invoice.report.pdf"; diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-taxes.ts b/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-taxes.ts index aefd3b6f..27ac32e5 100644 --- a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-taxes.ts +++ b/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-taxes.ts @@ -22,4 +22,10 @@ export class InvoiceTaxes extends Collection { InvoiceAmount.zero(taxableAmount.currencyCode) ) as InvoiceAmount; } + + public getCodesToString(): string { + return this.getAll() + .map((taxItem) => taxItem.tax.code) + .join(", "); + } } diff --git a/modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts b/modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts index b44dd0c1..8f9efe53 100644 --- a/modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts +++ b/modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts @@ -22,4 +22,10 @@ export class ItemTaxes extends Collection { ItemAmount.zero(taxableAmount.currencyCode) ); } + + public getCodesToString(): string { + return this.getAll() + .map((taxItem) => taxItem.tax.code) + .join(", "); + } } diff --git a/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts b/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts index 01e9b9b3..6f63dfd0 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts @@ -23,6 +23,14 @@ export class ItemAmount extends MoneyValue { return ItemAmount.create(props).data; } + toObjectString() { + return { + value: String(this.value), + scale: String(this.scale), + currency_code: this.currencyCode, + }; + } + // Ensure fluent operations keep the subclass type convertScale(newScale: number) { const mv = super.convertScale(newScale); diff --git a/modules/customer-invoices/src/api/infrastructure/dependencies.ts b/modules/customer-invoices/src/api/infrastructure/dependencies.ts index 48855129..c8e75595 100644 --- a/modules/customer-invoices/src/api/infrastructure/dependencies.ts +++ b/modules/customer-invoices/src/api/infrastructure/dependencies.ts @@ -7,13 +7,17 @@ import { } from "@erp/core/api"; import { CreateCustomerInvoiceUseCase, + CustomerInvoiceFullPresenter, + CustomerInvoiceItemsFullPresenter, + CustomerInvoiceReportHTMLPresenter, + CustomerInvoiceReportPDFPresenter, DeleteCustomerInvoiceUseCase, GetCustomerInvoiceUseCase, ListCustomerInvoicesPresenter, ListCustomerInvoicesUseCase, ReportCustomerInvoiceUseCase, - UpdateCustomerInvoiceUseCase, } from "../application"; + import { CustomerInvoiceService } from "../domain"; import { CustomerInvoiceFullMapper, CustomerInvoiceListMapper } from "./mappers"; import { CustomerInvoiceRepository } from "./sequelize"; @@ -31,7 +35,7 @@ type InvoiceDeps = { list: () => ListCustomerInvoicesUseCase; get: () => GetCustomerInvoiceUseCase; create: () => CreateCustomerInvoiceUseCase; - update: () => UpdateCustomerInvoiceUseCase; + //update: () => UpdateCustomerInvoiceUseCase; delete: () => DeleteCustomerInvoiceUseCase; report: () => ReportCustomerInvoiceUseCase; }; @@ -64,12 +68,48 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps { if (!_presenterRegistry) { _presenterRegistry = new InMemoryPresenterRegistry(); - _presenterRegistry.registerPresenter( + _presenterRegistry.registerPresenters([ { - projection: "LIST", + key: { + resource: "customer-invoice-items", + projection: "FULL", + }, + presenter: new CustomerInvoiceItemsFullPresenter(_presenterRegistry), }, - new ListCustomerInvoicesPresenter() - ); + { + key: { + resource: "customer-invoice", + projection: "FULL", + }, + presenter: new CustomerInvoiceFullPresenter(_presenterRegistry), + }, + + { + key: { + resource: "customer-invoice", + projection: "LIST", + }, + presenter: new ListCustomerInvoicesPresenter(_presenterRegistry), + }, + + { + key: { + resource: "customer-invoice", + projection: "REPORT", + format: "HTML", + }, + presenter: new CustomerInvoiceReportHTMLPresenter(_presenterRegistry), + }, + + { + key: { + resource: "customer-invoice", + projection: "REPORT", + format: "PDF", + }, + presenter: new CustomerInvoiceReportPDFPresenter(_presenterRegistry), + }, + ]); } return { @@ -82,38 +122,25 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps { build: { list: () => - new ListCustomerInvoicesUseCase( - _service!, - transactionManager!, - _presenterRegistry?.getPresenter({ projection: "LIST" }) - ), - get: () => - new GetCustomerInvoiceUseCase( - _service!, - transactionManager!, - _presenterRegistry?.getPresenter({ projection: "FULL" }) - ), + new ListCustomerInvoicesUseCase(_service!, transactionManager!, _presenterRegistry!), + get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _presenterRegistry!), create: () => new CreateCustomerInvoiceUseCase( _service!, transactionManager!, - _presenterRegistry?.getPresenter({ projection: "FULL" }), + _presenterRegistry!, _catalogs!.taxes ), - update: () => + /*update: () => new UpdateCustomerInvoiceUseCase( _service!, transactionManager!, - _presenterRegistry?.getPresenter({ projection: "FULL" }), + _presenterRegistry!, _catalogs!.taxes - ), + ),*/ delete: () => new DeleteCustomerInvoiceUseCase(_service!, transactionManager!), report: () => - new ReportCustomerInvoiceUseCase( - _service!, - transactionManager!, - _presenterRegistry?.getPresenter({ projection: "REPORT" }) - ), + new ReportCustomerInvoiceUseCase(_service!, transactionManager!, _presenterRegistry!), }, }; } diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/index.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/index.ts index 99bd883f..fc608651 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/index.ts @@ -3,3 +3,4 @@ export * from "./delete-customer-invoice.controller"; export * from "./get-customer-invoice.controller"; export * from "./list-customer-invoices.controller"; ///export * from "./update-customer-invoice.controller"; +export * from "./report-customer-invoice.controller"; diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/report-customer-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/report-customer-invoice.controller.ts index 5b1984f3..a824ec84 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/report-customer-invoice.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/report-customer-invoice.controller.ts @@ -1,5 +1,5 @@ import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; -import { GetCustomerInvoiceUseCase, GetCustomerInvoiceUseCase as ReportCustomerInvoiceUseCase } from "../../../application"; +import { GetCustomerInvoiceUseCase as ReportCustomerInvoiceUseCase } from "../../../application"; export class ReportCustomerInvoiceController extends ExpressController { public constructor(private readonly useCase: ReportCustomerInvoiceUseCase) { @@ -12,13 +12,10 @@ export class ReportCustomerInvoiceController extends ExpressController { const companyId = this.getTenantId()!; // garantizado por tenantGuard const { invoice_id } = this.req.params; - const getUseCase = getUsecaasdasd; - const invoiceDto = await - - const result = await this.useCase.execute({ invoice_id, companyId, }); + const result = await this.useCase.execute({ invoice_id, companyId }); return result.match( - (data) => this.downloadPDF(result.data), + ({ pdfData, filename }) => this.downloadPDF(pdfData, filename), (err) => this.handleError(err) ); } 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 708ae8e5..11d8a99a 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 @@ -15,6 +15,7 @@ import { DeleteCustomerInvoiceController, GetCustomerInvoiceController, ListCustomerInvoicesController, + ReportCustomerInvoiceController, } from "./controllers"; export const customerInvoicesRouter = (params: ModuleParams) => { 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 dd8c0753..6bf3f4ec 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 @@ -134,7 +134,7 @@ export class CustomerInvoiceRepository transaction: Transaction ): Promise> { try { - const mapper: ICustomerInvoiceFullMapper = this._registry.getReadModelMapper("FULL"); + const mapper: ICustomerInvoiceFullMapper = this._registry.getDomainMapper("FULL"); const { CustomerModel } = this._database.models; const row = await CustomerInvoiceModel.findOne({ diff --git a/modules/customer-invoices/src/common/dto/response/create-customer-invoice.response.dto.ts b/modules/customer-invoices/src/common/dto/response/create-customer-invoice.response.dto.ts index 6da09dc1..dcb95a82 100644 --- a/modules/customer-invoices/src/common/dto/response/create-customer-invoice.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/create-customer-invoice.response.dto.ts @@ -1,4 +1,10 @@ -import { AmountSchema, MetadataSchema, PercentageSchema, QuantitySchema } from "@erp/core"; +import { + AmountSchema, + MetadataSchema, + MoneySchema, + PercentageSchema, + QuantitySchema, +} from "@erp/core"; import * as z from "zod/v4"; export const CreateCustomerInvoiceResponseSchema = z.object({ @@ -17,12 +23,12 @@ export const CreateCustomerInvoiceResponseSchema = z.object({ language_code: z.string(), currency_code: z.string(), - subtotal_amount: AmountSchema, + subtotal_amount: MoneySchema, discount_percentage: PercentageSchema, - discount_amount: AmountSchema, - taxable_amount: AmountSchema, - tax_amount: AmountSchema, - total_amount: AmountSchema, + discount_amount: MoneySchema, + taxable_amount: MoneySchema, + taxes_amount: MoneySchema, + total_amount: MoneySchema, items: z.array( z.object({ @@ -31,8 +37,15 @@ export const CreateCustomerInvoiceResponseSchema = z.object({ description: z.string(), quantity: QuantitySchema, unit_amount: AmountSchema, + + taxes: z.string(), + + subtotal_amount: MoneySchema, discount_percentage: PercentageSchema, - total_amount: AmountSchema, + discount_amount: MoneySchema, + taxable_amount: MoneySchema, + taxes_amount: MoneySchema, + total_amount: MoneySchema, }) ), diff --git a/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts index 80a7c6e1..75fd924f 100644 --- a/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/get-customer-invoice-by-id.response.dto.ts @@ -1,4 +1,4 @@ -import { AmountSchema, MetadataSchema, PercentageSchema, QuantitySchema } from "@erp/core"; +import { MetadataSchema, MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core"; import * as z from "zod/v4"; export const GetCustomerInvoiceByIdResponseSchema = z.object({ @@ -19,11 +19,12 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({ taxes: z.string(), - subtotal_amount: AmountSchema, + subtotal_amount: MoneySchema, discount_percentage: PercentageSchema, - discount_amount: AmountSchema, - taxable_amount: AmountSchema, - total_amount: AmountSchema, + discount_amount: MoneySchema, + taxable_amount: MoneySchema, + taxes_amount: MoneySchema, + total_amount: MoneySchema, items: z.array( z.object({ @@ -31,10 +32,16 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({ position: z.string(), description: z.string(), quantity: QuantitySchema, - unit_amount: AmountSchema, - discount_percentage: PercentageSchema, + unit_amount: MoneySchema, + taxes: z.string(), - total_amount: AmountSchema, + + subtotal_amount: MoneySchema, + discount_percentage: PercentageSchema, + discount_amount: MoneySchema, + taxable_amount: MoneySchema, + taxes_amount: MoneySchema, + total_amount: MoneySchema, }) ), diff --git a/modules/customer-invoices/src/common/dto/response/list-customer-invoices.response.dto.ts b/modules/customer-invoices/src/common/dto/response/list-customer-invoices.response.dto.ts index 163e7da3..8adf9480 100644 --- a/modules/customer-invoices/src/common/dto/response/list-customer-invoices.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/list-customer-invoices.response.dto.ts @@ -1,4 +1,4 @@ -import { AmountSchema, MetadataSchema, createListViewResponseSchema } from "@erp/core"; +import { MetadataSchema, MoneySchema, createListViewResponseSchema } from "@erp/core"; import * as z from "zod/v4"; export const ListCustomerInvoiceResponseSchema = createListViewResponseSchema( @@ -30,11 +30,11 @@ export const ListCustomerInvoiceResponseSchema = createListViewResponseSchema( taxes: z.string(), - subtotal_amount: AmountSchema, - discount_amount: AmountSchema, - taxable_amount: AmountSchema, - taxes_amount: AmountSchema, - total_amount: AmountSchema, + subtotal_amount: MoneySchema, + discount_amount: MoneySchema, + taxable_amount: MoneySchema, + taxes_amount: MoneySchema, + total_amount: MoneySchema, metadata: MetadataSchema.optional(), }) diff --git a/modules/customer-invoices/src/web/components/items/customer-invoice-items-sortable-table-row.tsx b/modules/customer-invoices/src/web/components/items/customer-invoice-items-sortable-table-row.tsx index 861ca983..c163b0c3 100644 --- a/modules/customer-invoices/src/web/components/items/customer-invoice-items-sortable-table-row.tsx +++ b/modules/customer-invoices/src/web/components/items/customer-invoice-items-sortable-table-row.tsx @@ -7,7 +7,7 @@ import { CSSProperties, PropsWithChildren, createContext, useMemo } from "react" import { CustomerInvoiceItemsSortableProps } from "./customer-invoice-items-sortable-datatable"; interface Context { - attributes: Record; + attributes: Record; listeners: DraggableSyntheticListeners; ref(node: HTMLElement | null): void; } diff --git a/modules/customers/src/api/infrastructure/dependencies.ts b/modules/customers/src/api/infrastructure/dependencies.ts index 154d0c96..7452829d 100644 --- a/modules/customers/src/api/infrastructure/dependencies.ts +++ b/modules/customers/src/api/infrastructure/dependencies.ts @@ -1,16 +1,10 @@ import type { IMapperRegistry, IPresenterRegistry, ModuleParams } from "@erp/core/api"; -import { - InMemoryMapperRegistry, - InMemoryPresenterRegistry, - SequelizeTransactionManager, -} from "@erp/core/api"; +import { InMemoryMapperRegistry, SequelizeTransactionManager } from "@erp/core/api"; import { CreateCustomerUseCase, DeleteCustomerUseCase, - GetCustomerAssembler, GetCustomerUseCase, - ListCustomersAssembler, ListCustomersUseCase, UpdateCustomerUseCase, } from "../application"; @@ -33,7 +27,7 @@ type CustomerDeps = { }; }; -let _presenterRegistry: IPresenterRegistry | null = null; +const _presenterRegistry: IPresenterRegistry | null = null; let _mapperRegistry: IMapperRegistry | null = null; let _repo: CustomerRepository | null = null; @@ -51,42 +45,34 @@ export function getCustomerDependencies(params: ModuleParams): CustomerDeps { if (!_repo) _repo = new CustomerRepository({ mapperRegistry: _mapperRegistry }); if (!_service) _service = new CustomerService(_repo); - if (!_presenterRegistry) { + /*if (!_presenterRegistry) { _presenterRegistry = new InMemoryPresenterRegistry(); _presenterRegistry.registerPresenters([ { - key: { projection: "FULL" }, + key: { resource: "customer", projection: "FULL" }, presenter: new ListCustomersAssembler(), }, { - key: { projection: "LIST" }, + key: { resource: "customer", projection: "LIST" }, presenter: new GetCustomerAssembler(), }, ]); - - /*if (!_assemblers) { - _assemblers = { - list: new ListCustomersAssembler(), // transforma domain → ListDTO - get: new GetCustomerAssembler(), // transforma domain → DetailDTO - create: new CreateCustomersAssembler(), // transforma domain → CreatedDTO - update: new UpdateCustomerAssembler(), // transforma domain -> UpdateDTO - };*/ - } + }*/ return { transactionManager, repo: _repo, mapperRegistry: _mapperRegistry, - presenterRegistry: _presenterRegistry, + //presenterRegistry: _presenterRegistry, service: _service, build: { - list: () => new ListCustomersUseCase(_service!, transactionManager!, _assemblers!.list), - get: () => new GetCustomerUseCase(_service!, transactionManager!, _assemblers!.get), - create: () => new CreateCustomerUseCase(_service!, transactionManager!, _assemblers!.create), - update: () => new UpdateCustomerUseCase(_service!, transactionManager!, _assemblers!.update), - delete: () => new DeleteCustomerUseCase(_service!, transactionManager!), + /*list: () => new ListCustomersUseCase(_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/packages/rdx-ddd/src/value-objects/percentage.ts b/packages/rdx-ddd/src/value-objects/percentage.ts index b64d663a..18cca879 100644 --- a/packages/rdx-ddd/src/value-objects/percentage.ts +++ b/packages/rdx-ddd/src/value-objects/percentage.ts @@ -81,4 +81,11 @@ export class Percentage extends ValueObject { toString(): string { return `${this.toNumber().toFixed(this.scale)}%`; } + + toObjectString() { + return { + value: String(this.value), + scale: String(this.scale), + }; + } } diff --git a/packages/rdx-ddd/src/value-objects/quantity.ts b/packages/rdx-ddd/src/value-objects/quantity.ts index 48fcb92f..054b4494 100644 --- a/packages/rdx-ddd/src/value-objects/quantity.ts +++ b/packages/rdx-ddd/src/value-objects/quantity.ts @@ -62,6 +62,13 @@ export class Quantity extends ValueObject { return this.toNumber().toFixed(this.scale); } + toObjectString() { + return { + value: String(this.value), + scale: String(this.scale), + }; + } + isZero(): boolean { return this.value === 0; }