From e80cc572f9f9d3d0ac9f8140031bddfc517437ce Mon Sep 17 00:00:00 2001 From: david Date: Thu, 11 Sep 2025 14:05:50 +0200 Subject: [PATCH] Facturas de cliente --- modules/core/src/api/domain/index.ts | 1 + .../core/src/api/domain/repositories/index.ts | 1 + .../repositories/repository.interface.ts | 78 +++++++ modules/core/src/api/infrastructure/index.ts | 1 + .../src/api/infrastructure/mappers/index.ts | 2 + .../mappers/mapper-registry.interface.ts | 38 ++++ .../infrastructure/mappers/mapper-registry.ts | 30 +++ .../src/api/infrastructure/sequelize/index.ts | 2 +- .../infrastructure/sequelize/mappers/index.ts | 3 + .../mappers/sequelize-domain-mapper.ts | 88 ++++++++ .../sequelize-mapper copy.ts} | 0 .../mappers/sequelize-mapper.interface.ts | 7 + .../mappers/sequelize-read-model-mapper.ts | 31 +++ .../InvoiceParticipant.presenter.ts.bak | 22 -- ...InvoiceParticipantAddress.presenter.ts.bak | 14 -- .../list-invoices-items.assembler.ts | 71 ------- .../assembler/list-invoices.assembler.ts | 29 +-- .../invoice-recipient/invoice-recipient.ts | 2 +- .../src/api/infrastructure/dependencies.ts | 45 ++-- .../list-customer-invoices.controller.ts | 8 +- .../list-customer-invoices.presenter.ts | 56 ----- .../customer-invoice-item.full.mapper.ts} | 28 +-- .../customer-invoice.full.mapper.ts} | 72 ++++--- .../mappers/full-domain/index.ts | 1 + .../invoice-recipient.full.mapper.ts} | 4 +- .../item-taxes.full.mapper.ts} | 11 +- .../taxes.full.mapper.ts} | 10 +- .../src/api/infrastructure/mappers/index.ts | 3 +- .../list/customer-invoice.list.mapper.ts | 192 ++++++++++++++++++ .../api/infrastructure/mappers/list/index.ts | 1 + .../list/invoice-recipient.list.mapper.ts | 116 +++++++++++ .../sequelize/contact.mo.del.ts.bak | 84 -------- .../sequelize/contactAddress.mo.del.ts.bak | 75 ------- .../sequelize/customer-invoice.repository.ts | 39 ++-- .../customer-invoiceParticipant.mo.del.ts.bak | 106 ---------- ...er-invoiceParticipantAddress.mo.del.ts.bak | 94 --------- .../delete-customer.use-case.ts | 4 +- .../controllers/list-customers.controller.ts | 8 +- .../infrastructure/mappers/customer.mapper.ts | 4 +- .../src/helpers/result-collection.ts | 6 +- 40 files changed, 741 insertions(+), 646 deletions(-) create mode 100644 modules/core/src/api/domain/repositories/index.ts create mode 100644 modules/core/src/api/domain/repositories/repository.interface.ts create mode 100644 modules/core/src/api/infrastructure/mappers/index.ts create mode 100644 modules/core/src/api/infrastructure/mappers/mapper-registry.interface.ts create mode 100644 modules/core/src/api/infrastructure/mappers/mapper-registry.ts create mode 100644 modules/core/src/api/infrastructure/sequelize/mappers/index.ts create mode 100644 modules/core/src/api/infrastructure/sequelize/mappers/sequelize-domain-mapper.ts rename modules/core/src/api/infrastructure/sequelize/{sequelize-mapper.ts => mappers/sequelize-mapper copy.ts} (100%) create mode 100644 modules/core/src/api/infrastructure/sequelize/mappers/sequelize-mapper.interface.ts create mode 100644 modules/core/src/api/infrastructure/sequelize/mappers/sequelize-read-model-mapper.ts delete mode 100644 modules/customer-invoices/src/api/application/list-customer-invoices/assembler/InvoiceParticipant.presenter.ts.bak delete mode 100644 modules/customer-invoices/src/api/application/list-customer-invoices/assembler/InvoiceParticipantAddress.presenter.ts.bak delete mode 100644 modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices-items.assembler.ts delete mode 100644 modules/customer-invoices/src/api/infrastructure/express/presenter/list-customer-invoices.presenter.ts rename modules/customer-invoices/src/api/infrastructure/mappers/{customer-invoice-item.mapper.ts => full-domain/customer-invoice-item.full.mapper.ts} (92%) rename modules/customer-invoices/src/api/infrastructure/mappers/{customer-invoice.mapper.ts => full-domain/customer-invoice.full.mapper.ts} (84%) create mode 100644 modules/customer-invoices/src/api/infrastructure/mappers/full-domain/index.ts rename modules/customer-invoices/src/api/infrastructure/mappers/{invoice-recipient.mapper.ts => full-domain/invoice-recipient.full.mapper.ts} (96%) rename modules/customer-invoices/src/api/infrastructure/mappers/{item-taxes.mapper.ts => full-domain/item-taxes.full.mapper.ts} (86%) rename modules/customer-invoices/src/api/infrastructure/mappers/{taxes.mapper.ts => full-domain/taxes.full.mapper.ts} (87%) create mode 100644 modules/customer-invoices/src/api/infrastructure/mappers/list/customer-invoice.list.mapper.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/mappers/list/index.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/mappers/list/invoice-recipient.list.mapper.ts delete mode 100644 modules/customer-invoices/src/api/infrastructure/sequelize/contact.mo.del.ts.bak delete mode 100644 modules/customer-invoices/src/api/infrastructure/sequelize/contactAddress.mo.del.ts.bak delete mode 100644 modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoiceParticipant.mo.del.ts.bak delete mode 100644 modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoiceParticipantAddress.mo.del.ts.bak diff --git a/modules/core/src/api/domain/index.ts b/modules/core/src/api/domain/index.ts index ba0555aa..02c26663 100644 --- a/modules/core/src/api/domain/index.ts +++ b/modules/core/src/api/domain/index.ts @@ -1,2 +1,3 @@ export * from "./errors"; +export * from "./repositories"; export * from "./value-objects"; diff --git a/modules/core/src/api/domain/repositories/index.ts b/modules/core/src/api/domain/repositories/index.ts new file mode 100644 index 00000000..fa66b99c --- /dev/null +++ b/modules/core/src/api/domain/repositories/index.ts @@ -0,0 +1 @@ +export * from "./repository.interface"; diff --git a/modules/core/src/api/domain/repositories/repository.interface.ts b/modules/core/src/api/domain/repositories/repository.interface.ts new file mode 100644 index 00000000..7b413ec7 --- /dev/null +++ b/modules/core/src/api/domain/repositories/repository.interface.ts @@ -0,0 +1,78 @@ +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; + +/** + * 🧭 Mapper de Dominio (Persistencia ↔ Dominio/Agregado) + * - Responsabilidad: transformar un registro de persistencia en un agregado de dominio y viceversa. + * - No debe contener lógica de negocio; sólo construcción/serialización de objetos. + */ +export interface IDomainMapper { + /** + * Convierte un registro crudo de persistencia (ORM/row) en un agregado/entidad de dominio. + * Debe devolver Result.fail(...) si la construcción del dominio no es posible/consistente. + */ + mapToDomain(raw: TPersistence, params?: MapperParamsType): Result; + + /** + * Convierte un agregado/entidad de dominio en un objeto de persistencia listo para el ORM. + * Debe devolver Result.fail(...) si hay incoherencias o datos no serializables. + */ + mapToPersistence(domain: TDomain, params?: MapperParamsType): Result; +} + +/** + * 📦 Extensión opcional para operaciones en lote de mapeo de dominio. + * Útil para repos que recuperan múltiples filas y necesitan entregar colecciones de agregados. + */ +export interface IBulkDomainMapper { + /** + * Mapea múltiples registros crudos de persistencia a una Collection de agregados de dominio. + */ + mapToDomainCollection( + raws: TPersistence[], + totalCount: number, + params?: MapperParamsType + ): Result, Error>; + + /** + * Mapea múltiples agregados de dominio a un array de objetos de persistencia. + */ + mapToPersistenceArray( + domains: Collection, + params?: MapperParamsType + ): Result; +} + +/** + * 🧩 Tipo para cuando se desea un mapper de dominio con soporte opcional de operaciones en lote. + * Puedes implementar sólo IDomainMapper y añadir IBulkDomainMapper cuando lo necesites. + */ +export type DomainMapperWithBulk = IDomainMapper & + Partial>; + +/** + * + * 👓 Mapper de Read Model (Persistencia ↔ DTO/Proyección de Lectura) + * - Responsabilidad: transformar registros de persistencia en DTOs para lectura (listados, resúmenes, informes). + * - No intenta reconstruir agregados ni validar value objects de dominio. + **/ +export interface IReadModelMapperWithBulk { + /** + * Convierte un registro crudo en un DTO de lectura. + */ + mapToDTO(raw: TPersistence, params?: MapperParamsType): Result; + + /** + * Convierte múltiples registros crudos en una Collection de DTOs de lectura. + */ + mapToDTOCollection( + raws: TPersistence[], + totalCount: number, + params?: MapperParamsType + ): Result, Error>; +} diff --git a/modules/core/src/api/infrastructure/index.ts b/modules/core/src/api/infrastructure/index.ts index e77d9642..fe212001 100644 --- a/modules/core/src/api/infrastructure/index.ts +++ b/modules/core/src/api/infrastructure/index.ts @@ -1,4 +1,5 @@ export * from "./database"; export * from "./errors"; export * from "./express"; +export * from "./mappers"; export * from "./sequelize"; diff --git a/modules/core/src/api/infrastructure/mappers/index.ts b/modules/core/src/api/infrastructure/mappers/index.ts new file mode 100644 index 00000000..45584ec3 --- /dev/null +++ b/modules/core/src/api/infrastructure/mappers/index.ts @@ -0,0 +1,2 @@ +export * from "./mapper-registry"; +export * from "./mapper-registry.interface"; diff --git a/modules/core/src/api/infrastructure/mappers/mapper-registry.interface.ts b/modules/core/src/api/infrastructure/mappers/mapper-registry.interface.ts new file mode 100644 index 00000000..c588de78 --- /dev/null +++ b/modules/core/src/api/infrastructure/mappers/mapper-registry.interface.ts @@ -0,0 +1,38 @@ +/** + * 🔑 Claves de proyección comunes para seleccionar mappers en lectura. + * Puedes extender con otras cadenas según tus necesidades ("SUMMARY", "EXPORT", etc.). + */ +export type MapperProjectionKey = "FULL" | "LIST" | "REPORTS" | (string & {}); + +/** + * 🏗️ Registro/Fábrica de mappers (Strategy/Factory) + * - Permite resolver diferentes mappers según la proyección (FULL, SUMMARY, etc.) + * - Facilita inyección y test (DIP), evitando dependencias duras en implementaciones concretas. + * + * Ejemplo de uso: + * - registry.registerDomainMapper("FULL", customerInvoiceFullMapper); + * - registry.registerReadModelMapper("SUMMARY", customerInvoiceSummaryMapper); + * - registry.registerReadModelMapper("REPORT", customerInvoiceReportMapper); + */ + +export interface IMapperRegistry { + /** + * Obtiene un mapper de dominio por clave de proyección. + */ + getDomainMapper(key: MapperProjectionKey): T; + + /** + * Obtiene un mapper de read model por clave de proyección. + */ + getReadModelMapper(key: MapperProjectionKey): T; + + /** + * Registra un mapper de dominio bajo una clave de proyección. + */ + registerDomainMapper(key: MapperProjectionKey, mapper: T): void; + + /** + * Registra un mapper de read model bajo una clave de proyección. + */ + registerReadModelMapper(key: MapperProjectionKey, mapper: T): void; +} diff --git a/modules/core/src/api/infrastructure/mappers/mapper-registry.ts b/modules/core/src/api/infrastructure/mappers/mapper-registry.ts new file mode 100644 index 00000000..6d50b86f --- /dev/null +++ b/modules/core/src/api/infrastructure/mappers/mapper-registry.ts @@ -0,0 +1,30 @@ +import { InfrastructureError } from "../errors"; +import { IMapperRegistry, MapperProjectionKey } from "./mapper-registry.interface"; + +export class InMemoryMapperRegistry implements IMapperRegistry { + private domainMappers: Map = new Map(); + private readModelMappers: Map = new Map(); + + getDomainMapper(key: MapperProjectionKey): T { + if (!this.readModelMappers.has(key)) { + throw new InfrastructureError(`Error. Domain model mapper ${key} not registred!`); + } + + return this.domainMappers.get(key); + } + + getReadModelMapper(key: MapperProjectionKey): T { + if (!this.readModelMappers.has(key)) { + throw new InfrastructureError(`Error. Read model mapper ${key} not registred!`); + } + return this.readModelMappers.get(key); + } + + registerDomainMapper(key: MapperProjectionKey, mapper: T): void { + this.domainMappers.set(key, mapper); + } + + registerReadModelMapper(key: MapperProjectionKey, mapper: T): void { + this.readModelMappers.set(key, mapper); + } +} diff --git a/modules/core/src/api/infrastructure/sequelize/index.ts b/modules/core/src/api/infrastructure/sequelize/index.ts index 2a0722aa..041b14c4 100644 --- a/modules/core/src/api/infrastructure/sequelize/index.ts +++ b/modules/core/src/api/infrastructure/sequelize/index.ts @@ -1,4 +1,4 @@ +export * from "./mappers"; export * from "./sequelize-error-translator"; -export * from "./sequelize-mapper"; export * from "./sequelize-repository"; export * from "./sequelize-transaction-manager"; diff --git a/modules/core/src/api/infrastructure/sequelize/mappers/index.ts b/modules/core/src/api/infrastructure/sequelize/mappers/index.ts new file mode 100644 index 00000000..7de8cdaf --- /dev/null +++ b/modules/core/src/api/infrastructure/sequelize/mappers/index.ts @@ -0,0 +1,3 @@ +export * from "./sequelize-domain-mapper"; +export * from "./sequelize-mapper.interface"; +export * from "./sequelize-read-model-mapper"; diff --git a/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-domain-mapper.ts b/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-domain-mapper.ts new file mode 100644 index 00000000..6457833d --- /dev/null +++ b/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-domain-mapper.ts @@ -0,0 +1,88 @@ +import { Collection, Result, ResultCollection } from "@repo/rdx-utils"; +import { Model } from "sequelize"; +import { MapperParamsType } from "../../../domain"; +import { ISequelizeDomainMapper } from "./sequelize-mapper.interface"; + +export abstract class SequelizeDomainMapper + implements ISequelizeDomainMapper +{ + public abstract mapToDomain(raw: TModel, params?: MapperParamsType): Result; + public abstract mapToPersistence( + domain: TEntity, + params?: MapperParamsType + ): Result; + + public mapToDomainCollection( + raws: (TModel | TModelAttributes)[], + totalCount: number, + params?: MapperParamsType + ): Result, Error> { + const _source = raws ?? []; + + try { + if (_source.length === 0) { + return Result.ok(new Collection([], totalCount)); + } + + const items = _source.map( + (value, index) => this.mapToDomain(value as TModel, { index, ...params }).data + ); + return Result.ok(new Collection(items, totalCount)); + } catch (error) { + return Result.fail(error as Error); + } + } + + public mapToPersistenceArray( + domains: Collection, + params?: MapperParamsType + ): Result { + const results = new ResultCollection( + domains.map((domain, index) => this.mapToPersistence(domain, { index, ...params })) + ); + if (results.hasSomeFaultyResult()) return results.getFirstFaultyResult(); + + return Result.ok(results.objects); + } + + /*protected _safeMap(operation: () => T, key: string): Result { + try { + return Result.ok(operation()); + } catch (error: unknown) { + return Result.fail(error as Error); + } + } + + protected _mapsValue( + row: TModel, + key: string, + customMapFn: (value: any, params: MapperParamsType) => Result, + params: MapperParamsType = { defaultValue: null } + ): Result { + return customMapFn(row?.dataValues[key] ?? params.defaultValue, params); + } + + protected _mapsAssociation( + row: TModel, + associationName: string, + customMapper: DomainMapperWithBulk, + params: MapperParamsType = {} + ): Result { + if (!customMapper) { + Result.fail(Error(`Custom mapper undefined for ${associationName}`)); + } + + const { filter, ...otherParams } = params; + let associationRows = row?.dataValues[associationName] ?? []; + + if (filter) { + associationRows = Array.isArray(associationRows) + ? associationRows.filter(filter) + : filter(associationRows); + } + + return Array.isArray(associationRows) + ? customMapper.mapToDomainCollection(associationRows, associationRows.length, otherParams) + : customMapper.mapToDomain(associationRows, otherParams); + }*/ +} diff --git a/modules/core/src/api/infrastructure/sequelize/sequelize-mapper.ts b/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-mapper copy.ts similarity index 100% rename from modules/core/src/api/infrastructure/sequelize/sequelize-mapper.ts rename to modules/core/src/api/infrastructure/sequelize/mappers/sequelize-mapper copy.ts diff --git a/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-mapper.interface.ts b/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-mapper.interface.ts new file mode 100644 index 00000000..236b7ffd --- /dev/null +++ b/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-mapper.interface.ts @@ -0,0 +1,7 @@ +import { DomainMapperWithBulk, IReadModelMapperWithBulk } from "../../../domain"; + +export interface ISequelizeDomainMapper + extends DomainMapperWithBulk {} + +export interface ISequelizeReadModelMapper + extends IReadModelMapperWithBulk {} diff --git a/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-read-model-mapper.ts b/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-read-model-mapper.ts new file mode 100644 index 00000000..f5c308f0 --- /dev/null +++ b/modules/core/src/api/infrastructure/sequelize/mappers/sequelize-read-model-mapper.ts @@ -0,0 +1,31 @@ +import { Collection, Result } from "@repo/rdx-utils"; +import { Model } from "sequelize"; +import { MapperParamsType } from "../../../domain"; +import { ISequelizeReadModelMapper } from "./sequelize-mapper.interface"; + +export abstract class SequelizeReadModelMapper + implements ISequelizeReadModelMapper +{ + public abstract mapToDTO(raw: TModel, params?: MapperParamsType): Result; + + public mapToDTOCollection( + raws: TModel[], + totalCount: number, + params?: MapperParamsType + ): Result, Error> { + const _source = raws ?? []; + + try { + if (_source.length === 0) { + return Result.ok(new Collection([], totalCount)); + } + + const items = _source.map( + (value, index) => this.mapToDTO(value as TModel, { index, ...params }).data + ); + return Result.ok(new Collection(items, totalCount)); + } catch (error) { + return Result.fail(error as Error); + } + } +} diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/InvoiceParticipant.presenter.ts.bak b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/InvoiceParticipant.presenter.ts.bak deleted file mode 100644 index aacc3b31..00000000 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/InvoiceParticipant.presenter.ts.bak +++ /dev/null @@ -1,22 +0,0 @@ -import { ICustomerInvoiceParticipant } from "@/contexts/invoicing/domain"; -import { IListCustomerInvoice_Participant_Response_DTO } from "@shared/contexts"; -import { CustomerInvoiceParticipantAddressAssembler } from "./CustomerInvoiceParticipantAddress.assembler"; - -export const CustomerInvoiceParticipantAssembler = ( - participant: ICustomerInvoiceParticipant, -): IListCustomerInvoice_Participant_Response_DTO => { - return { - participant_id: participant?.id?.toString(), - tin: participant?.tin?.toString(), - first_name: participant?.firstName?.toString(), - last_name: participant?.lastName?.toString(), - company_name: participant?.companyName?.toString(), - - billing_address: CustomerInvoiceParticipantAddressAssembler( - participant?.billingAddress!, - ), - shipping_address: CustomerInvoiceParticipantAddressAssembler( - participant?.shippingAddress!, - ), - }; -}; diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/InvoiceParticipantAddress.presenter.ts.bak b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/InvoiceParticipantAddress.presenter.ts.bak deleted file mode 100644 index 1e19158d..00000000 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/InvoiceParticipantAddress.presenter.ts.bak +++ /dev/null @@ -1,14 +0,0 @@ -export const CustomerInvoiceParticipantAddressAssembler = ( - address: CustomerInvoiceParticipantAddress -): IListCustomerInvoice_AddressParticipant_Response_DTO => { - return { - address_id: address?.id.toString(), - street: address?.street.toString(), - city: address?.city.toString(), - postal_code: address?.postalCode.toString(), - province: address?.province.toString(), - country: address?.country.toString(), - email: address?.email.toString(), - phone: address?.phone.toString(), - }; -}; diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices-items.assembler.ts b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices-items.assembler.ts deleted file mode 100644 index 575b5ed4..00000000 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices-items.assembler.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Criteria } from "@repo/rdx-criteria/server"; -import { toEmptyString } from "@repo/rdx-ddd"; -import { Collection } from "@repo/rdx-utils"; -import { CustomerInvoiceListResponseDTO } from "../../../../common/dto"; -import { CustomerInvoice } from "../../../domain"; - -export class ListCustomerInvoicesAssembler { - toDTO( - customerInvoices: Collection, - criteria: Criteria - ): CustomerInvoiceListResponseDTO { - const items = customerInvoices.map((invoice) => { - const recipient = invoice.recipient.match( - (recipient) => recipient.toString(), - () => ({ - tin: "", - name: "", - street: "", - street2: "", - city: "", - postal_code: "", - province: "", - country: "", - }) - ); - - return { - 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(), - ...recipient, - }, - - items, - - metadata: { - entity: "customer-invoice", - }, - }; - }); - - const totalItems = customerInvoices.total(); - - return { - page: criteria.pageNumber, - per_page: criteria.pageSize, - total_pages: Math.ceil(totalItems / criteria.pageSize), - total_items: totalItems, - items: items, - 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/list-customer-invoices/assembler/list-invoices.assembler.ts b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices.assembler.ts index 90877b74..dc985330 100644 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices.assembler.ts +++ b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices.assembler.ts @@ -1,30 +1,16 @@ +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"; -import { CustomerInvoice } from "../../../domain"; export class ListCustomerInvoicesAssembler { toDTO( - customerInvoices: Collection, + customerInvoices: Collection, criteria: Criteria ): CustomerInvoiceListResponseDTO { const invoices = customerInvoices.map((invoice) => { - const recipientDTO = invoice.recipient.match( - (recipient) => recipient.toString(), - () => ({ - tin: "", - name: "", - street: "", - street2: "", - city: "", - postal_code: "", - province: "", - country: "", - }) - ); - - const allAmounts = invoice.getAllAmounts(); + const recipientDTO = invoice.recipient.toObjectString(); const invoiceDTO: ArrayElement = { id: invoice.id.toString(), @@ -43,16 +29,13 @@ export class ListCustomerInvoicesAssembler { ...recipientDTO, }, - taxes: invoice.taxes - .getAll() - .map((taxItem) => taxItem.tax.code) - .join(","), + taxes: invoice.taxes, - subtotal_amount: allAmounts.subtotalAmount.toObjectString(), + /*subtotal_amount: allAmounts.subtotalAmount.toObjectString(), discount_amount: allAmounts.discountAmount.toObjectString(), taxable_amount: allAmounts.taxableAmount.toObjectString(), taxes_amount: allAmounts.taxesAmount.toObjectString(), - total_amount: allAmounts.totalAmount.toObjectString(), + total_amount: allAmounts.totalAmount.toObjectString(),*/ metadata: { entity: "customer-invoice", diff --git a/modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/invoice-recipient.ts b/modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/invoice-recipient.ts index ce2a0b54..4d5b5a7f 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/invoice-recipient.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/invoice-recipient.ts @@ -86,7 +86,7 @@ export class InvoiceRecipient extends ValueObject { return this.getProps(); } - toString() { + toObjectString() { return { tin: this.tin.toString(), name: this.name.toString(), diff --git a/modules/customer-invoices/src/api/infrastructure/dependencies.ts b/modules/customer-invoices/src/api/infrastructure/dependencies.ts index 27499e1f..64922ed6 100644 --- a/modules/customer-invoices/src/api/infrastructure/dependencies.ts +++ b/modules/customer-invoices/src/api/infrastructure/dependencies.ts @@ -1,6 +1,6 @@ import { JsonTaxCatalogProvider, spainTaxCatalogProvider } from "@erp/core"; -import type { ModuleParams } from "@erp/core/api"; -import { SequelizeTransactionManager } from "@erp/core/api"; +import type { IMapperRegistry, ModuleParams } from "@erp/core/api"; +import { InMemoryMapperRegistry, SequelizeTransactionManager } from "@erp/core/api"; import { CreateCustomerInvoiceAssembler, CreateCustomerInvoiceUseCase, @@ -13,13 +13,13 @@ import { UpdateCustomerInvoiceUseCase, } from "../application"; import { CustomerInvoiceService } from "../domain"; -import { CustomerInvoiceMapper } from "./mappers"; +import { CustomerInvoiceFullMapper, CustomerInvoiceListMapper } from "./mappers"; import { CustomerInvoiceRepository } from "./sequelize"; type InvoiceDeps = { transactionManager: SequelizeTransactionManager; + mapperRegistry: IMapperRegistry; repo: CustomerInvoiceRepository; - mapper: CustomerInvoiceMapper; service: CustomerInvoiceService; catalogs: { taxes: JsonTaxCatalogProvider; @@ -43,7 +43,7 @@ type InvoiceDeps = { }; let _repo: CustomerInvoiceRepository | null = null; -let _mapper: CustomerInvoiceMapper | null = null; +let _mapperRegistry: IMapperRegistry | null = null; let _service: CustomerInvoiceService | null = null; let _assemblers: InvoiceDeps["assemblers"] | null = null; let _catalogs: InvoiceDeps["catalogs"] | null = null; @@ -51,13 +51,19 @@ let _catalogs: InvoiceDeps["catalogs"] | null = null; export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps { const { database } = params; const transactionManager = new SequelizeTransactionManager(database); - if (!_catalogs) _catalogs = { taxes: spainTaxCatalogProvider }; - if (!_mapper) - _mapper = new CustomerInvoiceMapper({ - taxCatalog: _catalogs!.taxes, - }); - if (!_repo) _repo = new CustomerInvoiceRepository({ mapper: _mapper, database }); + + const fullMapper: CustomerInvoiceFullMapper = new CustomerInvoiceFullMapper({ + taxCatalog: _catalogs!.taxes, + }); + const listMapper = new CustomerInvoiceListMapper(); + + if (!_mapperRegistry) { + _mapperRegistry = new InMemoryMapperRegistry(); + _mapperRegistry.registerDomainMapper("FULL", fullMapper); + _mapperRegistry.registerReadModelMapper("LIST", listMapper); + } + if (!_repo) _repo = new CustomerInvoiceRepository({ mapperRegistry: _mapperRegistry, database }); if (!_service) _service = new CustomerInvoiceService(_repo); if (!_assemblers) { @@ -72,25 +78,14 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps { return { transactionManager, repo: _repo, - mapper: _mapper, + mapperRegistry: _mapperRegistry, service: _service, assemblers: _assemblers, catalogs: _catalogs, build: { list: () => - new ListCustomerInvoicesUseCase( - _service!, - transactionManager!, - _assemblers!.list, - _catalogs!.taxes - ), - get: () => - new GetCustomerInvoiceUseCase( - _service!, - transactionManager!, - _assemblers!.get, - _catalogs!.taxes - ), + new ListCustomerInvoicesUseCase(_service!, transactionManager!, _assemblers!.list), + get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.get), create: () => new CreateCustomerInvoiceUseCase( _service!, diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/list-customer-invoices.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/list-customer-invoices.controller.ts index 4883000e..c57cebe9 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/list-customer-invoices.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/list-customer-invoices.controller.ts @@ -13,7 +13,13 @@ export class ListCustomerInvoicesController extends ExpressController { const result = await this.useCase.execute({ criteria: this.criteria, companyId }); return result.match( - (data) => this.ok(data), + (data) => + this.ok(data, { + "X-Total-Count": String(data.total_items), + "Pagination-Count": String(data.total_pages), + "Pagination-Page": String(data.page), + "Pagination-Limit": String(data.per_page), + }), (err) => this.handleError(err) ); } diff --git a/modules/customer-invoices/src/api/infrastructure/express/presenter/list-customer-invoices.presenter.ts b/modules/customer-invoices/src/api/infrastructure/express/presenter/list-customer-invoices.presenter.ts deleted file mode 100644 index b211feb8..00000000 --- a/modules/customer-invoices/src/api/infrastructure/express/presenter/list-customer-invoices.presenter.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Response } from "express"; - -export type ListResult = { - items: T[]; - total: number; - limit: number; - offset: number; -}; - -export type ListPresenterOptions = { - includeMetaInBody?: boolean; // por defecto false (solo items en body) -}; - -export class ListPresenter { - constructor( - private readonly res: Response, - private readonly opts?: ListPresenterOptions - ) {} - - /** - Envía cabeceras de paginación y devuelve el cuerpo según la opción: - por defecto: items[] - includeMetaInBody: objeto con { items, total, limit, offset, page } - */ - present(result: ListResult) { - const { total, limit } = result; - const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 25; - const page = Math.floor(result.offset / (safeLimit || 1)) + 1; - - // Cabeceras de paginación (ya expuestas por CORS en app.ts) - this.res.setHeader("X-Total-Count", String(total)); - this.res.setHeader("Pagination-Count", String(total)); - this.res.setHeader("Pagination-Page", String(page)); - this.res.setHeader("Pagination-Limit", String(safeLimit)); - - if (this.opts?.includeMetaInBody) { - return this.res.status(200).json({ - items: result.items, - total, - limit: safeLimit, - offset: result.offset, - page, - }); - } - - // Contrato clásico: solo items en el body - return this.res.status(200).json(result.items); - } -} - -/** - Factoría simple para integrarla en dependencies.ts -*/ -export function createListPresenter(res: Response, opts?: ListPresenterOptions) { - return new ListPresenter(res, opts); -} diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice-item.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/full-domain/customer-invoice-item.full.mapper.ts similarity index 92% rename from modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice-item.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/mappers/full-domain/customer-invoice-item.full.mapper.ts index 55e6b0c8..089538fe 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice-item.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/full-domain/customer-invoice-item.full.mapper.ts @@ -1,7 +1,7 @@ import { - ISequelizeMapper, + ISequelizeDomainMapper, MapperParamsType, - SequelizeMapper, + SequelizeDomainMapper, ValidationErrorCollection, ValidationErrorDetail, extractOrPushError, @@ -17,19 +17,19 @@ import { ItemDiscount, ItemQuantity, ItemTaxes, -} from "../../domain"; -import { CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemModel } from "../sequelize"; -import { ItemTaxesMapper } from "./item-taxes.mapper"; +} from "../../../domain"; +import { CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemModel } from "../../sequelize"; +import { ItemTaxesMapper } from "./item-taxes.full.mapper"; export interface ICustomerInvoiceItemMapper - extends ISequelizeMapper< + extends ISequelizeDomainMapper< CustomerInvoiceItemModel, CustomerInvoiceItemCreationAttributes, CustomerInvoiceItem > {} export class CustomerInvoiceItemMapper - extends SequelizeMapper< + extends SequelizeDomainMapper< CustomerInvoiceItemModel, CustomerInvoiceItemCreationAttributes, CustomerInvoiceItem @@ -123,10 +123,14 @@ export class CustomerInvoiceItemMapper ); // 4) Taxes (colección a nivel de item/línea) - const taxesResults = this._taxesMapper.mapArrayToDomain(source.taxes, { - attributes, - ...params, - }); + const taxesResults = this._taxesMapper.mapToDomainCollection( + source.taxes, + source.taxes.length, + { + attributes, + ...params, + } + ); if (taxesResults.isFailure) { errors.push({ @@ -168,7 +172,7 @@ export class CustomerInvoiceItemMapper public mapToPersistence( source: CustomerInvoiceItem, params?: MapperParamsType - ): InferCreationAttributes { + ): Result, Error> { throw new Error("not implemented"); /* diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/full-domain/customer-invoice.full.mapper.ts similarity index 84% rename from modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/mappers/full-domain/customer-invoice.full.mapper.ts index 8d29715a..30135820 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/full-domain/customer-invoice.full.mapper.ts @@ -1,7 +1,7 @@ import { - ISequelizeMapper, + ISequelizeDomainMapper, MapperParamsType, - SequelizeMapper, + SequelizeDomainMapper, ValidationErrorCollection, ValidationErrorDetail, extractOrPushError, @@ -23,34 +23,38 @@ import { CustomerInvoiceProps, CustomerInvoiceSerie, CustomerInvoiceStatus, -} from "../../domain"; -import { InvoiceTaxes } from "../../domain/entities/invoice-taxes"; -import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../sequelize"; -import { CustomerInvoiceItemMapper } from "./customer-invoice-item.mapper"; -import { InvoiceRecipientMapper } from "./invoice-recipient.mapper"; -import { TaxesMapper } from "./taxes.mapper"; +} from "../../../domain"; +import { InvoiceTaxes } from "../../../domain/entities/invoice-taxes"; +import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../../sequelize"; +import { CustomerInvoiceItemMapper as CustomerInvoiceItemFullMapper } from "./customer-invoice-item.full.mapper"; +import { InvoiceRecipientMapper as InvoiceRecipientFullMapper } from "./invoice-recipient.full.mapper"; +import { TaxesMapper as TaxesFullMapper } from "./taxes.full.mapper"; -export interface ICustomerInvoiceMapper - extends ISequelizeMapper< +export interface ICustomerInvoiceFullMapper + extends ISequelizeDomainMapper< CustomerInvoiceModel, CustomerInvoiceCreationAttributes, CustomerInvoice > {} -export class CustomerInvoiceMapper - extends SequelizeMapper - implements ICustomerInvoiceMapper +export class CustomerInvoiceFullMapper + extends SequelizeDomainMapper< + CustomerInvoiceModel, + CustomerInvoiceCreationAttributes, + CustomerInvoice + > + implements ICustomerInvoiceFullMapper { - private _itemsMapper: CustomerInvoiceItemMapper; - private _recipientMapper: InvoiceRecipientMapper; - private _taxesMapper: TaxesMapper; + private _itemsMapper: CustomerInvoiceItemFullMapper; + private _recipientMapper: InvoiceRecipientFullMapper; + private _taxesMapper: TaxesFullMapper; constructor(params: MapperParamsType) { super(); - this._itemsMapper = new CustomerInvoiceItemMapper(params); // Instanciar el mapper de items - this._recipientMapper = new InvoiceRecipientMapper(); - this._taxesMapper = new TaxesMapper(params); + this._itemsMapper = new CustomerInvoiceItemFullMapper(params); // Instanciar el mapper de items + this._recipientMapper = new InvoiceRecipientFullMapper(); + this._taxesMapper = new TaxesFullMapper(params); } private mapAttributesToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) { @@ -169,11 +173,15 @@ export class CustomerInvoiceMapper } // 3) Items (colección) - const itemsResults = this._itemsMapper.mapArrayToDomain(source.items, { - errors, - attributes, - ...params, - }); + const itemsResults = this._itemsMapper.mapToDomainCollection( + source.items, + source.items.length, + { + errors, + attributes, + ...params, + } + ); if (itemsResults.isFailure) { errors.push({ @@ -183,11 +191,15 @@ export class CustomerInvoiceMapper } // 4) Taxes (colección a nivel factura) - const taxesResults = this._taxesMapper.mapArrayToDomain(source.taxes, { - errors, - attributes, - ...params, - }); + const taxesResults = this._taxesMapper.mapToDomainCollection( + source.taxes, + source.taxes.length, + { + errors, + attributes, + ...params, + } + ); if (taxesResults.isFailure) { errors.push({ @@ -260,7 +272,7 @@ export class CustomerInvoiceMapper public mapToPersistence( source: CustomerInvoice, params?: MapperParamsType - ): CustomerInvoiceCreationAttributes { + ): Result { throw new Error("not implemented"); /*const items = this._itemsMapper.mapCollectionToPersistence(source.items, params); diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/full-domain/index.ts b/modules/customer-invoices/src/api/infrastructure/mappers/full-domain/index.ts new file mode 100644 index 00000000..b2cbe530 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/mappers/full-domain/index.ts @@ -0,0 +1 @@ +export * from "./customer-invoice.full.mapper"; diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/invoice-recipient.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/full-domain/invoice-recipient.full.mapper.ts similarity index 96% rename from modules/customer-invoices/src/api/infrastructure/mappers/invoice-recipient.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/mappers/full-domain/invoice-recipient.full.mapper.ts index a92aaa72..4ac0c50f 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/invoice-recipient.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/full-domain/invoice-recipient.full.mapper.ts @@ -16,8 +16,8 @@ import { extractOrPushError, } from "@erp/core/api"; import { Maybe, Result } from "@repo/rdx-utils"; -import { CustomerInvoiceProps, InvoiceRecipient } from "../../domain"; -import { CustomerInvoiceModel } from "../sequelize"; +import { CustomerInvoiceProps, InvoiceRecipient } from "../../../domain"; +import { CustomerInvoiceModel } from "../../sequelize"; export class InvoiceRecipientMapper { public mapToDomain( diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/item-taxes.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/full-domain/item-taxes.full.mapper.ts similarity index 86% rename from modules/customer-invoices/src/api/infrastructure/mappers/item-taxes.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/mappers/full-domain/item-taxes.full.mapper.ts index 22f00166..51afcf83 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/item-taxes.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/full-domain/item-taxes.full.mapper.ts @@ -1,17 +1,20 @@ import { JsonTaxCatalogProvider } from "@erp/core"; import { MapperParamsType, - SequelizeMapper, + SequelizeDomainMapper, Tax, ValidationErrorCollection, ValidationErrorDetail, extractOrPushError, } from "@erp/core/api"; import { Result } from "@repo/rdx-utils"; -import { ItemTax } from "../../domain"; -import { CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemTaxModel } from "../sequelize"; +import { ItemTax } from "../../../domain"; +import { + CustomerInvoiceItemCreationAttributes, + CustomerInvoiceItemTaxModel, +} from "../../sequelize"; -export class ItemTaxesMapper extends SequelizeMapper< +export class ItemTaxesMapper extends SequelizeDomainMapper< CustomerInvoiceItemTaxModel, CustomerInvoiceItemCreationAttributes, ItemTax diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/taxes.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/full-domain/taxes.full.mapper.ts similarity index 87% rename from modules/customer-invoices/src/api/infrastructure/mappers/taxes.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/mappers/full-domain/taxes.full.mapper.ts index c94689b4..082149f2 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/taxes.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/full-domain/taxes.full.mapper.ts @@ -1,18 +1,18 @@ import { JsonTaxCatalogProvider } from "@erp/core"; import { MapperParamsType, - SequelizeMapper, + SequelizeDomainMapper, Tax, ValidationErrorCollection, ValidationErrorDetail, extractOrPushError, } from "@erp/core/api"; import { Result } from "@repo/rdx-utils"; -import { CustomerInvoiceProps } from "../../domain"; -import { InvoiceTax } from "../../domain/entities/invoice-taxes"; -import { CustomerInvoiceTaxCreationAttributes, CustomerInvoiceTaxModel } from "../sequelize"; +import { CustomerInvoiceProps } from "../../../domain"; +import { InvoiceTax } from "../../../domain/entities/invoice-taxes"; +import { CustomerInvoiceTaxCreationAttributes, CustomerInvoiceTaxModel } from "../../sequelize"; -export class TaxesMapper extends SequelizeMapper< +export class TaxesMapper extends SequelizeDomainMapper< CustomerInvoiceTaxModel, CustomerInvoiceTaxCreationAttributes, InvoiceTax diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/index.ts b/modules/customer-invoices/src/api/infrastructure/mappers/index.ts index 5d78e8fc..cce380e7 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/index.ts @@ -1 +1,2 @@ -export * from "./customer-invoice.mapper"; +export * from "./full-domain"; +export * from "./list"; diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/list/customer-invoice.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/list/customer-invoice.list.mapper.ts new file mode 100644 index 00000000..45cda91e --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/mappers/list/customer-invoice.list.mapper.ts @@ -0,0 +1,192 @@ +import { + ISequelizeReadModelMapper, + MapperParamsType, + SequelizeReadModelMapper, + ValidationErrorCollection, + ValidationErrorDetail, + extractOrPushError, +} from "@erp/core/api"; +import { + CurrencyCode, + LanguageCode, + Percentage, + UniqueID, + UtcDate, + maybeFromNullableVO, +} from "@repo/rdx-ddd"; + +import { Maybe, Result } from "@repo/rdx-utils"; +import { + CustomerInvoiceNumber, + CustomerInvoiceSerie, + CustomerInvoiceStatus, + InvoiceRecipient, +} from "../../../domain"; +import { CustomerInvoiceModel } from "../../sequelize"; +import { InvoiceRecipientListMapper } from "./invoice-recipient.list.mapper"; + +export type CustomerInvoiceListDTO = { + id: UniqueID; + companyId: UniqueID; + + isProforma: boolean; + invoiceNumber: CustomerInvoiceNumber; + status: CustomerInvoiceStatus; + series: Maybe; + + invoiceDate: UtcDate; + operationDate: Maybe; + + customerId: UniqueID; + recipient: InvoiceRecipient; + + languageCode: LanguageCode; + currencyCode: CurrencyCode; + + taxes: string; + + discountPercentage: Percentage; +}; + +export interface ICustomerInvoiceListMapper + extends ISequelizeReadModelMapper {} + +export class CustomerInvoiceListMapper + extends SequelizeReadModelMapper + implements ICustomerInvoiceListMapper +{ + private _recipientMapper: InvoiceRecipientListMapper; + + constructor() { + super(); + this._recipientMapper = new InvoiceRecipientListMapper(); + } + + public mapToDTO( + raw: CustomerInvoiceModel, + params?: MapperParamsType + ): Result { + const errors: ValidationErrorDetail[] = []; + + // 1) Valores escalares (atributos generales) + const attributes = this.mapAttributesToDTO(raw, { errors, ...params }); + + // 2) Recipient (snapshot en la factura o include) + const recipientResult = this._recipientMapper.mapToDTO(raw, { + errors, + attributes, + ...params, + }); + + if (recipientResult.isFailure) { + errors.push({ + path: "recipient", + message: recipientResult.error.message, + }); + } + + // 3) Taxes + const taxes = raw.taxes.map((tax) => tax.tax_code).join(", "); + + // 5) Si hubo errores de mapeo, devolvemos colección de validación + if (errors.length > 0) { + return Result.fail(new ValidationErrorCollection("Customer invoice mapping failed", errors)); + } + + return Result.ok({ + id: attributes.invoiceId!, + companyId: attributes.companyId!, + isProforma: attributes.isProforma, + status: attributes.status!, + series: attributes.series!, + invoiceNumber: attributes.invoiceNumber!, + invoiceDate: attributes.invoiceDate!, + operationDate: attributes.operationDate!, + + customerId: attributes.customerId!, + recipient: recipientResult.data, + + languageCode: attributes.languageCode!, + currencyCode: attributes.currencyCode!, + + discountPercentage: attributes.discountPercentage!, + + taxes, + }); + } + + private mapAttributesToDTO(raw: CustomerInvoiceModel, params?: MapperParamsType) { + const { errors } = params as { + errors: ValidationErrorDetail[]; + }; + + const invoiceId = extractOrPushError(UniqueID.create(raw.id), "id", errors); + const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors); + + const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors); + + const isProforma = Boolean(raw.is_proforma); + + const status = extractOrPushError(CustomerInvoiceStatus.create(raw.status), "status", errors); + + const series = extractOrPushError( + maybeFromNullableVO(raw.series, (value) => CustomerInvoiceSerie.create(value)), + "serie", + errors + ); + + const invoiceNumber = extractOrPushError( + CustomerInvoiceNumber.create(raw.invoice_number), + "invoice_number", + errors + ); + + const invoiceDate = extractOrPushError( + UtcDate.createFromISO(raw.invoice_date), + "invoice_date", + errors + ); + + const operationDate = extractOrPushError( + maybeFromNullableVO(raw.operation_date, (value) => UtcDate.createFromISO(value)), + "operation_date", + errors + ); + + const languageCode = extractOrPushError( + LanguageCode.create(raw.language_code), + "language_code", + errors + ); + + const currencyCode = extractOrPushError( + CurrencyCode.create(raw.currency_code), + "currency_code", + errors + ); + + const discountPercentage = extractOrPushError( + Percentage.create({ + value: raw.discount_amount_scale, + scale: raw.discount_percentage_scale, + }), + "discount_percentage_value", + errors + ); + + return { + invoiceId, + companyId, + customerId, + isProforma, + status, + series, + invoiceNumber, + invoiceDate, + operationDate, + languageCode, + currencyCode, + discountPercentage, + }; + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/list/index.ts b/modules/customer-invoices/src/api/infrastructure/mappers/list/index.ts new file mode 100644 index 00000000..5bbb658a --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/mappers/list/index.ts @@ -0,0 +1 @@ +export * from "./customer-invoice.list.mapper"; diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/list/invoice-recipient.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/list/invoice-recipient.list.mapper.ts new file mode 100644 index 00000000..4826e164 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/mappers/list/invoice-recipient.list.mapper.ts @@ -0,0 +1,116 @@ +import { + City, + Country, + Name, + PostalCode, + Province, + Street, + TINNumber, + maybeFromNullableVO, +} from "@repo/rdx-ddd"; + +import { + IReadModelMapperWithBulk, + MapperParamsType, + SequelizeReadModelMapper, + ValidationErrorDetail, + extractOrPushError, +} from "@erp/core/api"; + +import { Result } from "@repo/rdx-utils"; +import { InvoiceRecipient } from "../../../domain"; +import { CustomerInvoiceModel } from "../../sequelize"; +import { CustomerInvoiceListDTO } from "./customer-invoice.list.mapper"; + +interface IInvoiceRecipientListMapper + extends IReadModelMapperWithBulk {} + +export class InvoiceRecipientListMapper + extends SequelizeReadModelMapper + implements IInvoiceRecipientListMapper +{ + public mapToDTO( + raw: CustomerInvoiceModel, + params?: MapperParamsType + ): Result { + /** + * - Factura === proforma -> datos de "current_customer" + * - Factura !== proforma -> snapshot de los datos (campos customer_*) + */ + + const { errors, attributes } = params as { + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + const { isProforma } = attributes; + + if (isProforma && !raw.current_customer) { + errors.push({ + path: "current_customer", + message: "Current customer not included in query (InvoiceRecipientListMapper)", + }); + } + + const _name = isProforma ? raw.current_customer.name : raw.customer_name; + const _tin = isProforma ? raw.current_customer.tin : raw.customer_tin; + const _street = isProforma ? raw.current_customer.street : raw.customer_street; + const _street2 = isProforma ? raw.current_customer.street2 : raw.customer_street2; + const _city = isProforma ? raw.current_customer.city : raw.customer_city; + const _postal_code = isProforma ? raw.current_customer.postal_code : raw.customer_postal_code; + const _province = isProforma ? raw.current_customer.province : raw.customer_province; + const _country = isProforma ? raw.current_customer.country : raw.customer_country; + + // Customer (snapshot) + const customerName = extractOrPushError(Name.create(_name), "customer_name", errors); + + const customerTin = extractOrPushError(TINNumber.create(_tin), "customer_tin", errors); + + const customerStreet = extractOrPushError( + maybeFromNullableVO(_street, (value) => Street.create(value)), + "customer_street", + errors + ); + + const customerStreet2 = extractOrPushError( + maybeFromNullableVO(_street2, (value) => Street.create(value)), + "customer_street2", + errors + ); + + const customerCity = extractOrPushError( + maybeFromNullableVO(_city, (value) => City.create(value)), + "customer_city", + errors + ); + + const customerProvince = extractOrPushError( + maybeFromNullableVO(_province, (value) => Province.create(value)), + "customer_province", + errors + ); + + const customerPostalCode = extractOrPushError( + maybeFromNullableVO(_postal_code, (value) => PostalCode.create(value)), + "customer_postal_code", + errors + ); + + const customerCountry = extractOrPushError( + maybeFromNullableVO(_country, (value) => Country.create(value)), + "customer_country", + errors + ); + + return InvoiceRecipient.create({ + name: customerName!, + tin: customerTin!, + street: customerStreet!, + street2: customerStreet2!, + city: customerCity!, + postalCode: customerPostalCode!, + province: customerProvince!, + country: customerCountry!, + }); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/contact.mo.del.ts.bak b/modules/customer-invoices/src/api/infrastructure/sequelize/contact.mo.del.ts.bak deleted file mode 100644 index 7c062411..00000000 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/contact.mo.del.ts.bak +++ /dev/null @@ -1,84 +0,0 @@ -import { - CreationOptional, - DataTypes, - InferAttributes, - InferCreationAttributes, - Model, - NonAttribute, - Sequelize, -} from "sequelize"; - -import { ContactAddress_Model, TCreationContactAddress_Attributes } from "./contactAddress.mo.del"; - -export type TCreationContact_Model = InferCreationAttributes< - Contact_Model, - { omit: "shippingAddress" | "billingAddress" } -> & { - billingAddress: TCreationContactAddress_Attributes; - shippingAddress: TCreationContactAddress_Attributes; -}; - -export class Contact_Model extends Model< - InferAttributes, - InferCreationAttributes -> { - // To avoid table creation - static async sync(): Promise { - return Promise.resolve(); - } - - static associate(connection: Sequelize) { - const { Contact_Model, ContactAddress_Model } = connection.models; - - Contact_Model.hasOne(ContactAddress_Model, { - as: "shippingAddress", - foreignKey: "customer_id", - onDelete: "CASCADE", - }); - - Contact_Model.hasOne(ContactAddress_Model, { - as: "billingAddress", - foreignKey: "customer_id", - onDelete: "CASCADE", - }); - } - - declare id: string; - declare tin: CreationOptional; - declare company_name: CreationOptional; - declare first_name: CreationOptional; - declare last_name: CreationOptional; - - declare shippingAddress?: NonAttribute; - declare billingAddress?: NonAttribute; -} - -export default (sequelize: Sequelize) => { - Contact_Model.init( - { - id: { - type: DataTypes.UUID, - primaryKey: true, - }, - tin: { - type: new DataTypes.STRING(), - }, - company_name: { - type: new DataTypes.STRING(), - }, - first_name: { - type: new DataTypes.STRING(), - }, - last_name: { - type: new DataTypes.STRING(), - }, - }, - { - sequelize, - tableName: "customers", - timestamps: false, - } - ); - - return Contact_Model; -}; diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/contactAddress.mo.del.ts.bak b/modules/customer-invoices/src/api/infrastructure/sequelize/contactAddress.mo.del.ts.bak deleted file mode 100644 index 67e85d9e..00000000 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/contactAddress.mo.del.ts.bak +++ /dev/null @@ -1,75 +0,0 @@ -import { - CreationOptional, - DataTypes, - ForeignKey, - InferAttributes, - InferCreationAttributes, - Model, - NonAttribute, - Sequelize, -} from "sequelize"; -import { Contact_Model } from "./contact.mo.del.ts.bak"; - -export type TCreationContactAddress_Attributes = InferCreationAttributes< - ContactAddress_Model, - { omit: "customer" } ->; - -export class ContactAddress_Model extends Model< - InferAttributes, - TCreationContactAddress_Attributes -> { - // To avoid table creation - static async sync(): Promise { - return Promise.resolve(); - } - - static associate(connection: Sequelize) { - const { Contact_Model, ContactAddress_Model } = connection.models; - - ContactAddress_Model.belongsTo(Contact_Model, { - as: "customer", - foreignKey: "customer_id", - }); - } - - declare id: string; - declare customer_id: ForeignKey; - declare type: string; - declare street: CreationOptional; - declare postal_code: CreationOptional; - declare city: CreationOptional; - declare province: CreationOptional; - declare country: CreationOptional; - declare phone: CreationOptional; - declare email: CreationOptional; - - declare customer?: NonAttribute; -} - -export default (sequelize: Sequelize) => { - ContactAddress_Model.init( - { - id: { - type: DataTypes.UUID, - primaryKey: true, - }, - customer_id: DataTypes.UUID, - type: DataTypes.STRING(), - street: DataTypes.STRING(), - postal_code: DataTypes.STRING(), - city: DataTypes.STRING, - province: DataTypes.STRING, - country: DataTypes.STRING, - email: DataTypes.STRING, - phone: DataTypes.STRING, - }, - { - sequelize, - tableName: "customer_addresses", - timestamps: false, - } - ); - - return ContactAddress_Model; -}; 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 a719f7ba..9bdbc76c 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 @@ -1,10 +1,15 @@ -import { EntityNotFoundError, SequelizeRepository, translateSequelizeError } from "@erp/core/api"; +import { + EntityNotFoundError, + IMapperRegistry, + SequelizeRepository, + translateSequelizeError, +} from "@erp/core/api"; import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; import { UniqueID } from "@repo/rdx-ddd"; import { Collection, Result } from "@repo/rdx-utils"; import { Sequelize, Transaction } from "sequelize"; import { CustomerInvoice, ICustomerInvoiceRepository } from "../../domain"; -import { ICustomerInvoiceMapper } from "../mappers/customer-invoice.mapper"; +import { ICustomerInvoiceFullMapper, ICustomerInvoiceListMapper } from "../mappers"; import { CustomerInvoiceItemTaxModel } from "./customer-invoice-item-tax.model"; import { CustomerInvoiceItemModel } from "./customer-invoice-item.model"; import { CustomerInvoiceTaxModel } from "./customer-invoice-tax.model"; @@ -15,11 +20,11 @@ export class CustomerInvoiceRepository implements ICustomerInvoiceRepository { private readonly _database!: Sequelize; - private readonly _mapper!: ICustomerInvoiceMapper; + private readonly _registry!: IMapperRegistry; - constructor(params: { mapper: ICustomerInvoiceMapper; database: Sequelize }) { + constructor(params: { mapperRegistry: IMapperRegistry; database: Sequelize }) { super(); - this._mapper = params.mapper; + this._registry = params.mapperRegistry; this._database = params.database; } @@ -69,9 +74,17 @@ export class CustomerInvoiceRepository transaction: Transaction ): Promise> { try { - const data = this._mapper.mapToPersistence(invoice); + const mapper: ICustomerInvoiceFullMapper = this._registry.getDomainMapper("FULL"); + const mapperData = mapper.mapToPersistence(invoice); + + if (mapperData.isFailure) { + return Result.fail(mapperData.error); + } + + const { data } = mapperData; + const [instance] = await CustomerInvoiceModel.upsert(data, { transaction, returning: true }); - const savedInvoice = this._mapper.mapToDomain(instance); + const savedInvoice = mapper.mapToDomain(instance); return savedInvoice; } catch (err: unknown) { return Result.fail(translateSequelizeError(err)); @@ -97,7 +110,7 @@ export class CustomerInvoiceRepository transaction, }); return Result.ok(Boolean(count > 0)); - } catch (error: any) { + } catch (error: unknown) { return Result.fail(translateSequelizeError(error)); } } @@ -117,6 +130,7 @@ export class CustomerInvoiceRepository transaction: Transaction ): Promise> { try { + const mapper: ICustomerInvoiceFullMapper = this._registry.getReadModelMapper("FULL"); const { CustomerModel } = this._database.models; const row = await CustomerInvoiceModel.findOne({ @@ -152,7 +166,7 @@ export class CustomerInvoiceRepository return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString())); } - const customer = this._mapper.mapToDomain(row); + const customer = mapper.mapToDomain(row); return customer; } catch (err: unknown) { return Result.fail(translateSequelizeError(err)); @@ -174,8 +188,9 @@ export class CustomerInvoiceRepository companyId: UniqueID, criteria: Criteria, transaction: Transaction - ): Promise, Error>> { + ): Promise, Error>> { try { + const mapper: ICustomerInvoiceListMapper = this._registry.getReadModelMapper("LIST"); const { CustomerModel } = this._database.models; const converter = new CriteriaToSequelizeConverter(); const query = converter.convert(criteria); @@ -199,12 +214,12 @@ export class CustomerInvoiceRepository }, ]; - const instances = await CustomerInvoiceModel.findAll({ + const raws = await CustomerInvoiceModel.findAll({ ...query, transaction, }); - return this._mapper.mapArrayToDomain(instances); + return mapper.mapToDTOCollection(raws, raws.length); } catch (err: unknown) { return Result.fail(translateSequelizeError(err)); } diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoiceParticipant.mo.del.ts.bak b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoiceParticipant.mo.del.ts.bak deleted file mode 100644 index 54a1a176..00000000 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoiceParticipant.mo.del.ts.bak +++ /dev/null @@ -1,106 +0,0 @@ -import { - CreationOptional, - DataTypes, - InferAttributes, - InferCreationAttributes, - Model, - NonAttribute, - Sequelize, -} from "sequelize"; -import { CustomerInvoiceModel } from "./customer-invoice.model"; -import { - CustomerInvoiceParticipantAddress_Model, - TCreationCustomerInvoiceParticipantAddress_Model, -} from "./customer-invoiceParticipantAddress.mo.del.ts.bak"; - -export type TCreationCustomerInvoiceParticipant_Model = InferCreationAttributes< - CustomerInvoiceParticipant_Model, - { omit: "shippingAddress" | "billingAddress" | "customerInvoice" } -> & { - billingAddress: TCreationCustomerInvoiceParticipantAddress_Model; - shippingAddress: TCreationCustomerInvoiceParticipantAddress_Model; -}; - -export class CustomerInvoiceParticipant_Model extends Model< - InferAttributes< - CustomerInvoiceParticipant_Model, - { omit: "shippingAddress" | "billingAddress" | "customerInvoice" } - >, - InferCreationAttributes< - CustomerInvoiceParticipant_Model, - { omit: "shippingAddress" | "billingAddress" | "customerInvoice" } - > -> { - static associate(connection: Sequelize) { - const { CustomerInvoice_Model, CustomerInvoiceParticipantAddress_Model, CustomerInvoiceParticipant_Model } = - connection.models; - - CustomerInvoiceParticipant_Model.belongsTo(CustomerInvoice_Model, { - as: "customerInvoice", - foreignKey: "customerInvoice_id", - onDelete: "CASCADE", - }); - - CustomerInvoiceParticipant_Model.hasOne(CustomerInvoiceParticipantAddress_Model, { - as: "shippingAddress", - foreignKey: "participant_id", - onDelete: "CASCADE", - }); - - CustomerInvoiceParticipant_Model.hasOne(CustomerInvoiceParticipantAddress_Model, { - as: "billingAddress", - foreignKey: "participant_id", - onDelete: "CASCADE", - }); - } - - declare participant_id: string; - declare customerInvoice_id: string; - declare tin: CreationOptional; - declare company_name: CreationOptional; - declare first_name: CreationOptional; - declare last_name: CreationOptional; - - declare shippingAddress?: NonAttribute; - declare billingAddress?: NonAttribute; - - declare customerInvoice?: NonAttribute; -} - -export default (sequelize: Sequelize) => { - CustomerInvoiceParticipant_Model.init( - { - participant_id: { - type: DataTypes.UUID, - primaryKey: true, - }, - customerInvoice_id: { - type: DataTypes.UUID, - primaryKey: true, - }, - tin: { - type: new DataTypes.STRING(), - allowNull: true, - }, - company_name: { - type: new DataTypes.STRING(), - allowNull: true, - }, - first_name: { - type: new DataTypes.STRING(), - allowNull: true, - }, - last_name: { - type: new DataTypes.STRING(), - allowNull: true, - }, - }, - { - sequelize, - tableName: "customerInvoice_participants", - timestamps: false, - } - ); - - return CustomerInvoiceParticipant_Model; -}; diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoiceParticipantAddress.mo.del.ts.bak b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoiceParticipantAddress.mo.del.ts.bak deleted file mode 100644 index a1ff1bcc..00000000 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoiceParticipantAddress.mo.del.ts.bak +++ /dev/null @@ -1,94 +0,0 @@ -import { - CreationOptional, - DataTypes, - InferAttributes, - InferCreationAttributes, - Model, - NonAttribute, - Sequelize, -} from "sequelize"; -import { CustomerInvoiceParticipant_Model } from "./customer-invoiceParticipant.mo.del.ts.bak"; - -export type TCreationCustomerInvoiceParticipantAddress_Model = InferCreationAttributes< - CustomerInvoiceParticipantAddress_Model, - { omit: "participant" } ->; - -export class CustomerInvoiceParticipantAddress_Model extends Model< - InferAttributes, - InferCreationAttributes -> { - static associate(connection: Sequelize) { - const { CustomerInvoiceParticipantAddress_Model, CustomerInvoiceParticipant_Model } = connection.models; - CustomerInvoiceParticipantAddress_Model.belongsTo(CustomerInvoiceParticipant_Model, { - as: "participant", - foreignKey: "participant_id", - }); - } - - declare address_id: string; - declare participant_id: string; - declare type: string; - declare street: CreationOptional; - declare postal_code: CreationOptional; - declare city: CreationOptional; - declare province: CreationOptional; - declare country: CreationOptional; - declare phone: CreationOptional; - declare email: CreationOptional; - - declare participant?: NonAttribute; -} - -export default (sequelize: Sequelize) => { - CustomerInvoiceParticipantAddress_Model.init( - { - address_id: { - type: DataTypes.UUID, - primaryKey: true, - }, - participant_id: { - type: DataTypes.UUID, - primaryKey: true, - }, - type: { - type: new DataTypes.STRING(), - allowNull: false, - }, - street: { - type: new DataTypes.STRING(), - allowNull: true, - }, - postal_code: { - type: new DataTypes.STRING(), - allowNull: true, - }, - city: { - type: new DataTypes.STRING(), - allowNull: true, - }, - province: { - type: new DataTypes.STRING(), - allowNull: true, - }, - country: { - type: new DataTypes.STRING(), - allowNull: true, - }, - email: { - type: new DataTypes.STRING(), - allowNull: true, - }, - phone: { - type: new DataTypes.STRING(), - allowNull: true, - }, - }, - { - sequelize, - tableName: "customerInvoice_participant_addresses", - } - ); - - return CustomerInvoiceParticipantAddress_Model; -}; diff --git a/modules/customers/src/api/application/delete-customer/delete-customer.use-case.ts b/modules/customers/src/api/application/delete-customer/delete-customer.use-case.ts index 2853d586..b3f44954 100644 --- a/modules/customers/src/api/application/delete-customer/delete-customer.use-case.ts +++ b/modules/customers/src/api/application/delete-customer/delete-customer.use-case.ts @@ -40,7 +40,9 @@ export class DeleteCustomerUseCase { const customerExists = existsCheck.data; if (!customerExists) { - return Result.fail(new EntityNotFoundError("Customer", "id", customerId.toString())); + return Result.fail( + new EntityNotFoundError("Customer", "id", customerId.toObjectString()) + ); } return await this.service.deleteCustomerByIdInCompany(customerId, companyId, transaction); diff --git a/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts index 06c66c62..44ce2a48 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts @@ -13,7 +13,13 @@ export class ListCustomersController extends ExpressController { const result = await this.listCustomers.execute({ criteria: this.criteria, companyId }); return result.match( - (data) => this.ok(data), + (data) => + this.ok(data, { + "X-Total-Count": String(data.total_items), + "Pagination-Count": String(data.total_pages), + "Pagination-Page": String(data.page), + "Pagination-Limit": String(data.per_page), + }), (err) => this.handleError(err) ); } diff --git a/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts b/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts index cebd1882..dd7132d4 100644 --- a/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts +++ b/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts @@ -1,7 +1,7 @@ import { ISequelizeMapper, MapperParamsType, - SequelizeMapper, + SequelizeDomainMapper, ValidationErrorCollection, ValidationErrorDetail, extractOrPushError, @@ -34,7 +34,7 @@ export interface ICustomerMapper extends ISequelizeMapper {} export class CustomerMapper - extends SequelizeMapper + extends SequelizeDomainMapper implements ICustomerMapper { public mapToDomain(source: CustomerModel, params?: MapperParamsType): Result { diff --git a/packages/rdx-utils/src/helpers/result-collection.ts b/packages/rdx-utils/src/helpers/result-collection.ts index ec4d2f7d..a50ddecd 100644 --- a/packages/rdx-utils/src/helpers/result-collection.ts +++ b/packages/rdx-utils/src/helpers/result-collection.ts @@ -25,9 +25,9 @@ export class ResultCollection implements IResultColl return this._collection.some((result) => result.isFailure); } - public getFirstFaultyResult(): Result { - // biome-ignore lint/style/noNonNullAssertion: - return this._collection.find((result) => result.isFailure)!; + public getFirstFaultyResult(): Result { + const firstFaultyResult = this._collection.find((result) => result.isFailure); + return Result.fail(firstFaultyResult?.error); } public getAllFaultyResults(): Result[] {