This commit is contained in:
David Arranz 2026-03-07 22:39:21 +01:00
parent bf9ed99a90
commit 05f048cc72
85 changed files with 450 additions and 960 deletions

View File

@ -1,6 +1,6 @@
// application/issued-invoices/di/snapshot-builders.di.ts
import { IssuedInvoiceListItemSnapshotBuilder } from "../snapshot-builders";
import { IssuedInvoiceSummarySnapshotBuilder } from "../snapshot-builders";
import {
IssuedInvoiceFullSnapshotBuilder,
IssuedInvoiceItemsFullSnapshotBuilder,
@ -30,7 +30,7 @@ export function buildIssuedInvoiceSnapshotBuilders() {
taxesBuilder
);
const listSnapshotBuilder = new IssuedInvoiceListItemSnapshotBuilder();
const listSnapshotBuilder = new IssuedInvoiceSummarySnapshotBuilder();
const itemsReportBuilder = new IssuedInvoiceReportItemSnapshotBuilder();
const taxesReportBuilder = new IssuedInvoiceReportTaxSnapshotBuilder();

View File

@ -1,7 +1,7 @@
import type { ITransactionManager } from "@erp/core/api";
import type { IIssuedInvoiceFinder, IssuedInvoiceDocumentGeneratorService } from "../services";
import type { IIssuedInvoiceListItemSnapshotBuilder } from "../snapshot-builders";
import type { IIssuedInvoiceSummarySnapshotBuilder } from "../snapshot-builders";
import type { IIssuedInvoiceFullSnapshotBuilder } from "../snapshot-builders/full";
import type { IIssuedInvoiceReportSnapshotBuilder } from "../snapshot-builders/report";
import {
@ -24,12 +24,12 @@ export function buildGetIssuedInvoiceByIdUseCase(deps: {
export function buildListIssuedInvoicesUseCase(deps: {
finder: IIssuedInvoiceFinder;
itemSnapshotBuilder: IIssuedInvoiceListItemSnapshotBuilder;
summarySnapshotBuilder: IIssuedInvoiceSummarySnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new ListIssuedInvoicesUseCase(
deps.finder,
deps.itemSnapshotBuilder,
deps.summarySnapshotBuilder,
deps.transactionManager
);
}

View File

@ -1,5 +1,4 @@
export * from "./di";
export * from "./mappers";
export * from "./models";
export * from "./repositories";
export * from "./services";

View File

@ -1,2 +0,0 @@
export * from "./issued-invoice-domain-mapper.interface";
export * from "./issued-invoice-list-mapper.interface";

View File

@ -1,9 +0,0 @@
import type { MapperParamsType } from "@erp/core/api";
import type { Result } from "@repo/rdx-utils";
import type { IssuedInvoice } from "../../../domain";
export interface IIssuedInvoiceDomainMapper {
mapToPersistence(invoice: IssuedInvoice, params?: MapperParamsType): Result<unknown, Error>;
mapToDomain(raw: unknown, params?: MapperParamsType): Result<IssuedInvoice, Error>;
}

View File

@ -1,8 +0,0 @@
import type { MapperParamsType } from "@erp/core/api";
import type { Result } from "@repo/rdx-utils";
import type { IssuedInvoiceSummary } from "../models";
export interface IIssuedInvoiceSummaryMapper {
mapToDTO(raw: unknown, params?: MapperParamsType): Result<IssuedInvoiceSummary, Error>;
}

View File

@ -1,2 +1,2 @@
export * from "./full";
export * from "./list";
export * from "./summary";

View File

@ -1,2 +0,0 @@
export * from "./issued-invoice-list-item-snapshot.interface";
export * from "./issued-invoice-list-item-snapshot-builder";

View File

@ -0,0 +1,2 @@
export * from "./issued-invoice-summary-snapshot.interface";
export * from "./issued-invoice-summary-snapshot-builder";

View File

@ -3,13 +3,13 @@ import { maybeToEmptyString } from "@repo/rdx-ddd";
import type { IssuedInvoiceSummary } from "../../models";
import type { IIssuedInvoiceListItemSnapshot } from "./issued-invoice-list-item-snapshot.interface";
import type { IIssuedInvoiceSummarySnapshot } from "./issued-invoice-summary-snapshot.interface";
export interface IIssuedInvoiceListItemSnapshotBuilder
extends ISnapshotBuilder<IssuedInvoiceSummary, IIssuedInvoiceListItemSnapshot> {}
export interface IIssuedInvoiceSummarySnapshotBuilder
extends ISnapshotBuilder<IssuedInvoiceSummary, IIssuedInvoiceSummarySnapshot> {}
export class IssuedInvoiceListItemSnapshotBuilder implements IIssuedInvoiceListItemSnapshotBuilder {
toOutput(invoice: IssuedInvoiceSummary): IIssuedInvoiceListItemSnapshot {
export class IssuedInvoiceSummarySnapshotBuilder implements IIssuedInvoiceSummarySnapshotBuilder {
toOutput(invoice: IssuedInvoiceSummary): IIssuedInvoiceSummarySnapshot {
const recipient = invoice.recipient.toObjectString();
const verifactu = invoice.verifactu.match(

View File

@ -5,7 +5,7 @@ import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { IIssuedInvoiceFinder } from "../services";
import type { IIssuedInvoiceListItemSnapshotBuilder } from "../snapshot-builders";
import type { IIssuedInvoiceSummarySnapshotBuilder } from "../snapshot-builders";
type ListIssuedInvoicesUseCaseInput = {
companyId: UniqueID;
@ -15,7 +15,7 @@ type ListIssuedInvoicesUseCaseInput = {
export class ListIssuedInvoicesUseCase {
constructor(
private readonly finder: IIssuedInvoiceFinder,
private readonly listItemSnapshotBuilder: IIssuedInvoiceListItemSnapshotBuilder,
private readonly summarySnapshotBuilder: IIssuedInvoiceSummarySnapshotBuilder,
private readonly transactionManager: ITransactionManager
) {}
@ -37,7 +37,7 @@ export class ListIssuedInvoicesUseCase {
const invoices = result.data;
const totalInvoices = invoices.total();
const items = invoices.map((item) => this.listItemSnapshotBuilder.toOutput(item));
const items = invoices.map((item) => this.summarySnapshotBuilder.toOutput(item));
const snapshot = {
page: criteria.pageNumber,

View File

@ -4,9 +4,9 @@ import {
ProformaFullSnapshotBuilder,
ProformaItemReportSnapshotBuilder,
ProformaItemsFullSnapshotBuilder,
ProformaListItemSnapshotBuilder,
ProformaRecipientFullSnapshotBuilder,
ProformaReportSnapshotBuilder,
ProformaSummarySnapshotBuilder,
ProformaTaxReportSnapshotBuilder,
ProformaTaxesFullSnapshotBuilder,
} from "../snapshot-builders";
@ -24,7 +24,7 @@ export function buildProformaSnapshotBuilders() {
taxesBuilder
);
const listSnapshotBuilder = new ProformaListItemSnapshotBuilder();
const summarySnapshotBuilder = new ProformaSummarySnapshotBuilder();
const itemsReportBuilder = new ProformaItemReportSnapshotBuilder();
const taxesReportBuilder = new ProformaTaxReportSnapshotBuilder();
@ -35,7 +35,7 @@ export function buildProformaSnapshotBuilders() {
return {
full: fullSnapshotBuilder,
list: listSnapshotBuilder,
summary: summarySnapshotBuilder,
report: reportSnapshotBuilder,
};
}

View File

@ -7,8 +7,8 @@ import type {
ProformaDocumentGeneratorService,
} from "../services";
import type {
IProformaListItemSnapshotBuilder,
IProformaReportSnapshotBuilder,
IProformaSummarySnapshotBuilder,
} from "../snapshot-builders";
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders/full";
import { GetProformaByIdUseCase, ListProformasUseCase, ReportProformaUseCase } from "../use-cases";
@ -24,10 +24,14 @@ export function buildGetProformaByIdUseCase(deps: {
export function buildListProformasUseCase(deps: {
finder: IProformaFinder;
itemSnapshotBuilder: IProformaListItemSnapshotBuilder;
summarySnapshotBuilder: IProformaSummarySnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new ListProformasUseCase(deps.finder, deps.itemSnapshotBuilder, deps.transactionManager);
return new ListProformasUseCase(
deps.finder,
deps.summarySnapshotBuilder,
deps.transactionManager
);
}
export function buildReportProformaUseCase(deps: {

View File

@ -15,7 +15,7 @@ import {
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common";
import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../common";
import {
type IProformaItemProps,
type IProformaProps,
@ -28,7 +28,7 @@ import {
ItemDescription,
ItemQuantity,
type ProformaItemTaxesProps,
} from "../../../../domain";
} from "../../../domain";
/**
* CreateProformaPropsMapper

View File

@ -1,3 +1,2 @@
export * from "./inputs";
export * from "./proforma-domain-mapper.interface";
export * from "./proforma-list-mapper.interface";
export * from "./create-proforma-input.mapper";
export * from "./update-proforma-input.mapper";

View File

@ -1,2 +0,0 @@
export * from "./create-proforma-input.mapper";
export * from "./update-proforma-input.mapper";

View File

@ -1,9 +0,0 @@
import type { MapperParamsType } from "@erp/core/api";
import type { Result } from "@repo/rdx-utils";
import type { Proforma } from "../../../domain";
export interface IProformaDomainMapper {
mapToPersistence(proforma: Proforma, params?: MapperParamsType): Result<unknown, Error>;
mapToDomain(raw: unknown, params?: MapperParamsType): Result<Proforma, Error>;
}

View File

@ -1,8 +0,0 @@
import type { MapperParamsType } from "@erp/core/api";
import type { Result } from "@repo/rdx-utils";
import type { ProformaListDTO } from "../models";
export interface IProformaListMapper {
mapToDTO(raw: unknown, params?: MapperParamsType): Result<ProformaListDTO, Error>;
}

View File

@ -12,8 +12,8 @@ import {
} from "@repo/rdx-ddd";
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto";
import type { ProformaPatchProps } from "../../../../domain";
import type { UpdateProformaByIdRequestDTO } from "../../../../common/dto";
import { InvoiceSerie, type ProformaPatchProps } from "../../../domain";
/**
* UpdateProformaPropsMapper

View File

@ -1 +1 @@
export * from "./proforma-resume";
export * from "./proforma-summary";

View File

@ -9,7 +9,7 @@ import type {
InvoiceStatus,
} from "../../../domain";
export type ProformaListDTO = {
export type ProformaSummary = {
id: UniqueID;
companyId: UniqueID;

View File

@ -3,7 +3,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils";
import type { InvoiceStatus, Proforma } from "../../../domain";
import type { ProformaListDTO } from "../models";
import type { ProformaSummary } from "../models";
export interface IProformaRepository {
create(proforma: Proforma, transaction?: unknown): Promise<Result<void, Error>>;
@ -26,7 +26,7 @@ export interface IProformaRepository {
companyId: UniqueID,
criteria: Criteria,
transaction: unknown
): Promise<Result<Collection<ProformaListDTO>, Error>>;
): Promise<Result<Collection<ProformaSummary>, Error>>;
deleteByIdInCompany(
companyId: UniqueID,

View File

@ -4,7 +4,7 @@ import type { Collection, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { Proforma } from "../../../domain";
import type { ProformaListDTO } from "../models";
import type { ProformaSummary } from "../models";
import type { IProformaRepository } from "../repositories";
export interface IProformaFinder {
@ -24,7 +24,7 @@ export interface IProformaFinder {
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<ProformaListDTO>, Error>>;
): Promise<Result<Collection<ProformaSummary>, Error>>;
}
export class ProformaFinder implements IProformaFinder {
@ -50,7 +50,7 @@ export class ProformaFinder implements IProformaFinder {
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<ProformaListDTO>, Error>> {
): Promise<Result<Collection<ProformaSummary>, Error>> {
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction, {});
}
}

View File

@ -1,3 +1,3 @@
export * from "./full";
export * from "./list";
export * from "./report";
export * from "./summary";

View File

@ -1,2 +0,0 @@
export * from "./proforma-list-item-snapshot.interface";
export * from "./proforma-list-item-snapshot-builder";

View File

@ -0,0 +1,2 @@
export * from "./proforma-summary-snapshot.interface";
export * from "./proforma-summary-snapshot-builder";

View File

@ -1,15 +1,15 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToEmptyString } from "@repo/rdx-ddd";
import type { ProformaListDTO } from "../../models";
import type { ProformaSummary } from "../../models";
import type { IProformaListItemSnapshot } from "./proforma-list-item-snapshot.interface";
import type { IProformaSummarySnapshot } from "./proforma-summary-snapshot.interface";
export interface IProformaListItemSnapshotBuilder
extends ISnapshotBuilder<ProformaListDTO, IProformaListItemSnapshot> {}
export interface IProformaSummarySnapshotBuilder
extends ISnapshotBuilder<ProformaSummary, IProformaSummarySnapshot> {}
export class ProformaListItemSnapshotBuilder implements IProformaListItemSnapshotBuilder {
toOutput(proforma: ProformaListDTO): IProformaListItemSnapshot {
export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotBuilder {
toOutput(proforma: ProformaSummary): IProformaSummarySnapshot {
const recipient = proforma.recipient.toObjectString();
return {

View File

@ -5,7 +5,7 @@ import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { IProformaFinder } from "../services";
import type { IProformaListItemSnapshotBuilder } from "../snapshot-builders";
import type { IProformaSummarySnapshotBuilder } from "../snapshot-builders";
type ListProformasUseCaseInput = {
companyId: UniqueID;
@ -15,7 +15,7 @@ type ListProformasUseCaseInput = {
export class ListProformasUseCase {
constructor(
private readonly finder: IProformaFinder,
private readonly listItemSnapshotBuilder: IProformaListItemSnapshotBuilder,
private readonly summarySnapshotBuilder: IProformaSummarySnapshotBuilder,
private readonly transactionManager: ITransactionManager
) {}
@ -33,7 +33,7 @@ export class ListProformasUseCase {
const proformas = result.data;
const totalProformas = proformas.total();
const items = proformas.map((item) => this.listItemSnapshotBuilder.toOutput(item));
const items = proformas.map((item) => this.summarySnapshotBuilder.toOutput(item));
const snapshot = {
page: criteria.pageNumber,

View File

@ -1,343 +0,0 @@
import {
AggregateRoot,
type CurrencyCode,
DomainValidationError,
type LanguageCode,
type Percentage,
type TextValue,
type UniqueID,
type UtcDate,
} from "@repo/rdx-ddd";
import { Collection, type Maybe, Result } from "@repo/rdx-utils";
import {
CustomerInvoiceItems,
type InvoicePaymentMethod,
type VerifactuRecord,
} from "../common/entities";
import {
InvoiceAmount,
type InvoiceNumber,
type InvoiceRecipient,
type InvoiceSerie,
type InvoiceStatus,
InvoiceTaxGroup,
type ItemAmount,
} from "../common/value-objects";
export interface CustomerInvoiceProps {
companyId: UniqueID;
isProforma: boolean;
status: InvoiceStatus;
proformaId: Maybe<UniqueID>; // <- proforma padre en caso de issue
series: Maybe<InvoiceSerie>;
invoiceNumber: InvoiceNumber;
invoiceDate: UtcDate;
operationDate: Maybe<UtcDate>;
customerId: UniqueID;
recipient: Maybe<InvoiceRecipient>;
reference: Maybe<string>;
description: Maybe<string>;
notes: Maybe<TextValue>;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
items: CustomerInvoiceItems;
paymentMethod: Maybe<InvoicePaymentMethod>;
discountPercentage: Percentage;
verifactu: Maybe<VerifactuRecord>;
}
export type CustomerInvoicePatchProps = Partial<
Omit<CustomerInvoiceProps, "companyId" | "items">
> & {
items?: CustomerInvoiceItems;
};
export interface ICustomerInvoice {
canTransitionTo(nextStatus: string): boolean;
hasRecipient: boolean;
hasPaymentMethod: boolean;
//getTaxes(): Collection<InvoiceTaxGroup>;
getProps(): CustomerInvoiceProps;
}
export class CustomerInvoice
extends AggregateRoot<CustomerInvoiceProps>
implements ICustomerInvoice
{
private _items!: CustomerInvoiceItems;
protected constructor(props: CustomerInvoiceProps, id?: UniqueID) {
super(props, id);
this._items =
props.items ||
CustomerInvoiceItems.create({
languageCode: props.languageCode,
currencyCode: props.currencyCode,
globalDiscountPercentage: props.discountPercentage,
});
}
static create(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> {
const customerInvoice = new CustomerInvoice(props, id);
// Reglas de negocio / validaciones
if (!(customerInvoice.isProforma || customerInvoice.hasRecipient)) {
return Result.fail(
new DomainValidationError(
"MISSING_CUSTOMER_DATA",
"recipient",
"Customer data must be provided for non-proforma invoices"
)
);
}
// 🔹 Disparar evento de dominio "CustomerInvoiceAuthenticatedEvent"
//const { customerInvoice } = props;
//user.addDomainEvent(new CustomerInvoiceAuthenticatedEvent(id, customerInvoice.toString()));
return Result.ok(customerInvoice);
}
// Getters
public get companyId(): UniqueID {
return this.props.companyId;
}
public get customerId(): UniqueID {
return this.props.customerId;
}
public get isProforma(): boolean {
return this.props.isProforma;
}
public get proformaId(): Maybe<UniqueID> {
return this.props.proformaId;
}
public get status(): InvoiceStatus {
return this.props.status;
}
canTransitionTo(nextStatus: string): boolean {
return this.props.status.canTransitionTo(nextStatus);
}
public get series(): Maybe<InvoiceSerie> {
return this.props.series;
}
public get invoiceNumber() {
return this.props.invoiceNumber;
}
public get invoiceDate(): UtcDate {
return this.props.invoiceDate;
}
public get operationDate(): Maybe<UtcDate> {
return this.props.operationDate;
}
public get reference(): Maybe<string> {
return this.props.reference;
}
public get description(): Maybe<string> {
return this.props.description;
}
public get notes(): Maybe<TextValue> {
return this.props.notes;
}
public get recipient(): Maybe<InvoiceRecipient> {
return this.props.recipient;
}
public get paymentMethod(): Maybe<InvoicePaymentMethod> {
return this.props.paymentMethod;
}
public get languageCode(): LanguageCode {
return this.props.languageCode;
}
public get currencyCode(): CurrencyCode {
return this.props.currencyCode;
}
public get verifactu(): Maybe<VerifactuRecord> {
return this.props.verifactu;
}
public get discountPercentage(): Percentage {
return this.props.discountPercentage;
}
// Method to get the complete list of line items
public get items(): CustomerInvoiceItems {
return this._items;
}
public get hasRecipient() {
return this.recipient.isSome();
}
public get hasPaymentMethod() {
return this.paymentMethod.isSome();
}
// Helpers
/**
* @summary Convierte un ItemAmount a InvoiceAmount (mantiene moneda y escala homogénea).
*/
private _toInvoiceAmount(itemAmount: ItemAmount): InvoiceAmount {
return InvoiceAmount.create({
value: itemAmount.convertScale(InvoiceAmount.DEFAULT_SCALE).value,
currency_code: this.currencyCode.code,
}).data;
}
// Cálculos
/**
* @summary Calcula todos los totales de factura a partir de los totales de las líneas.
* La cabecera NO recalcula lógica de porcentaje toda la lógica está en Item/Items.
*/
public calculateAllAmounts() {
const itemsTotals = this.items.calculateAllAmounts();
const subtotalAmount = this._toInvoiceAmount(itemsTotals.subtotalAmount);
const itemDiscountAmount = this._toInvoiceAmount(itemsTotals.itemDiscountAmount);
const globalDiscountAmount = this._toInvoiceAmount(itemsTotals.globalDiscountAmount);
const totalDiscountAmount = this._toInvoiceAmount(itemsTotals.totalDiscountAmount);
const taxableAmount = this._toInvoiceAmount(itemsTotals.taxableAmount);
const taxesAmount = this._toInvoiceAmount(itemsTotals.taxesAmount);
const totalAmount = this._toInvoiceAmount(itemsTotals.totalAmount);
const taxGroups = this._groupTaxes();
return {
subtotalAmount,
itemDiscountAmount,
globalDiscountAmount,
totalDiscountAmount,
taxableAmount,
taxesAmount,
totalAmount,
taxGroups,
} as const;
}
// Métodos públicos
public getProps(): CustomerInvoiceProps {
return this.props;
}
public update(partialInvoice: CustomerInvoicePatchProps): Result<CustomerInvoice, Error> {
const { items, ...rest } = partialInvoice;
const updatedProps = {
...this.props,
...rest,
} as CustomerInvoiceProps;
/*if (partialAddress) {
const updatedAddressOrError = this.address.update(partialAddress);
if (updatedAddressOrError.isFailure) {
return Result.fail(updatedAddressOrError.error);
}
updatedProps.address = updatedAddressOrError.data;
}*/
return CustomerInvoice.create(updatedProps, this.id);
}
public getSubtotalAmount(): InvoiceAmount {
return this.calculateAllAmounts().subtotalAmount;
}
public getItemDiscountAmount(): InvoiceAmount {
return this.calculateAllAmounts().itemDiscountAmount;
}
public getGlobalDiscountAmount(): InvoiceAmount {
return this.calculateAllAmounts().globalDiscountAmount;
}
public getTotalDiscountAmount(): InvoiceAmount {
return this.calculateAllAmounts().totalDiscountAmount;
}
public getTaxableAmount(): InvoiceAmount {
return this.calculateAllAmounts().taxableAmount;
}
public getTaxesAmount(): InvoiceAmount {
return this.calculateAllAmounts().taxesAmount;
}
public getTotalAmount(): InvoiceAmount {
return this.calculateAllAmounts().totalAmount;
}
public getTaxes(): Collection<InvoiceTaxGroup> {
return this._groupTaxes();
}
/**
* @summary Agrupa impuestos a nivel factura usando el trío (iva|rec|ret),
* construyendo InvoiceTaxGroup desde los datos de los ítems.
*/
private _groupTaxes(): Collection<InvoiceTaxGroup> {
const map = this.items.groupTaxesByCode();
const groups: InvoiceTaxGroup[] = [];
for (const [, entry] of map.entries()) {
const { taxes, taxable } = entry;
const iva = taxes.iva.unwrap(); // IVA siempre obligatorio
const rec = taxes.rec; // Maybe<Tax>
const retention = taxes.retention; // Maybe<Tax>
const invoiceAmount = InvoiceAmount.create({
value: taxable.convertScale(InvoiceAmount.DEFAULT_SCALE).value,
currency_code: this.currencyCode.code,
}).data;
const item = InvoiceTaxGroup.create({
iva,
rec,
retention,
taxableAmount: invoiceAmount,
}).data;
groups.push(item);
}
return new Collection(groups);
}
}

View File

@ -1,73 +0,0 @@
import {
AggregateRoot,
type CurrencyCode,
type LanguageCode,
type Percentage,
type TextValue,
type UniqueID,
type UtcDate,
} from "@repo/rdx-ddd";
import type { Maybe } from "@repo/rdx-utils";
import type { CustomerInvoiceItems, InvoicePaymentMethod } from "./entities";
import type { InvoiceNumber, InvoiceRecipient, InvoiceSerie, InvoiceStatus } from "./value-objects";
export interface ICustomerInvoiceBaseProps {
companyId: UniqueID;
invoiceNumber: InvoiceNumber;
invoiceDate: UtcDate;
status: InvoiceStatus;
series: Maybe<InvoiceSerie>;
operationDate: Maybe<UtcDate>;
customerId: UniqueID;
recipient: Maybe<InvoiceRecipient>;
reference: Maybe<string>;
description: Maybe<string>;
notes: Maybe<TextValue>;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
items: CustomerInvoiceItems;
paymentMethod: Maybe<InvoicePaymentMethod>;
discountPercentage: Percentage;
}
export abstract class CustomerInvoiceBase<
TProps extends ICustomerInvoiceBaseProps,
> extends AggregateRoot<TProps> {
protected constructor(props: TProps, id?: UniqueID) {
super(props, id);
}
public get companyId(): UniqueID {
return this.props.companyId;
}
public get invoiceNumber(): InvoiceNumber {
return this.props.invoiceNumber;
}
public get invoiceDate(): UtcDate {
return this.props.invoiceDate;
}
public get currencyCode(): CurrencyCode {
return this.props.currencyCode;
}
public get languageCode(): LanguageCode {
return this.props.languageCode;
}
public get recipient(): Maybe<InvoiceRecipient> {
return this.props.recipient;
}
}

View File

@ -0,0 +1,28 @@
import type { ICatalogs } from "@erp/core/api";
import {
SequelizeIssuedInvoiceDomainMapper,
SequelizeIssuedInvoiceSummaryMapper,
} from "../persistence";
export interface IIssuedInvoicePersistenceMappers {
domainMapper: SequelizeIssuedInvoiceDomainMapper;
summaryMapper: SequelizeIssuedInvoiceSummaryMapper;
}
export const buildIssuedInvoicePersistenceMappers = (
catalogs: ICatalogs
): IIssuedInvoicePersistenceMappers => {
const { taxCatalog } = catalogs;
// Mappers para el repositorio
const domainMapper = new SequelizeIssuedInvoiceDomainMapper({
taxCatalog,
});
const summaryMapper = new SequelizeIssuedInvoiceSummaryMapper();
return {
domainMapper,
summaryMapper,
};
};

View File

@ -1,19 +1,14 @@
import { SpainTaxCatalogProvider } from "@erp/core";
import type { Sequelize } from "sequelize";
import {
IssuedInvoiceRepository,
SequelizeIssuedInvoiceDomainMapper,
SequelizeIssuedInvoiceListMapper,
} from "../persistence";
import { IssuedInvoiceRepository } from "../persistence";
export const buildIssuedInvoiceRepository = (database: Sequelize) => {
const taxCatalog = SpainTaxCatalogProvider();
import type { IIssuedInvoicePersistenceMappers } from "./issued-invoice-persistence-mappers.di";
const domainMapper = new SequelizeIssuedInvoiceDomainMapper({
taxCatalog,
});
const listMapper = new SequelizeIssuedInvoiceListMapper();
export const buildIssuedInvoiceRepository = (params: {
database: Sequelize;
mappers: IIssuedInvoicePersistenceMappers;
}) => {
const { database, mappers } = params;
return new IssuedInvoiceRepository(domainMapper, listMapper, database);
return new IssuedInvoiceRepository(mappers.domainMapper, mappers.summaryMapper, database);
};

View File

@ -1,4 +1,4 @@
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api";
import {
type GetIssuedInvoiceByIdUseCase,
@ -12,6 +12,7 @@ import {
} from "../../../application/issued-invoices";
import { buildIssuedInvoiceDocumentService } from "./issued-invoice-documents.di";
import { buildIssuedInvoicePersistenceMappers } from "./issued-invoice-persistence-mappers.di";
import { buildIssuedInvoiceRepository } from "./issued-invoice-repositories.di";
export type IssuedInvoicesInternalDeps = {
@ -27,7 +28,10 @@ export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInv
// Infrastructure
const transactionManager = buildTransactionManager(database);
const repository = buildIssuedInvoiceRepository(database);
const catalogs = buildCatalogs();
const persistenceMappers = buildIssuedInvoicePersistenceMappers(catalogs);
const repository = buildIssuedInvoiceRepository({ database, mappers: persistenceMappers });
// Application helpers
const finder = buildIssuedInvoiceFinder(repository);
@ -40,7 +44,7 @@ export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInv
listIssuedInvoices: () =>
buildListIssuedInvoicesUseCase({
finder,
itemSnapshotBuilder: snapshotBuilders.list,
summarySnapshotBuilder: snapshotBuilders.list,
transactionManager,
}),

View File

@ -1,4 +1,4 @@
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import { DiscountPercentage, type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import {
CurrencyCode,
LanguageCode,
@ -13,9 +13,7 @@ import {
} from "@repo/rdx-ddd";
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import type { IIssuedInvoiceDomainMapper } from "../../../../../../application";
import {
DiscountPercentage,
type IIssuedInvoiceProps,
InvoiceAmount,
InvoiceNumber,
@ -36,14 +34,11 @@ import { SequelizeIssuedInvoiceRecipientDomainMapper } from "./sequelize-issued-
import { SequelizeIssuedInvoiceTaxesDomainMapper } from "./sequelize-issued-invoice-taxes-domain.mapper";
import { SequelizeIssuedInvoiceVerifactuDomainMapper } from "./sequelize-verifactu-record-domain.mapper";
export class SequelizeIssuedInvoiceDomainMapper
extends SequelizeDomainMapper<
CustomerInvoiceModel,
CustomerInvoiceCreationAttributes,
IssuedInvoice
>
implements IIssuedInvoiceDomainMapper
{
export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
CustomerInvoiceModel,
CustomerInvoiceCreationAttributes,
IssuedInvoice
> {
private _itemsMapper: SequelizeIssuedInvoiceItemDomainMapper;
private _recipientMapper: SequelizeIssuedInvoiceRecipientDomainMapper;
private _taxesMapper: SequelizeIssuedInvoiceTaxesDomainMapper;

View File

@ -1,5 +1,5 @@
import type { JsonTaxCatalogProvider } from "@erp/core";
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import { type MapperParamsType, SequelizeDomainMapper, TaxPercentage } from "@erp/core/api";
import {
Percentage,
UniqueID,
@ -19,8 +19,6 @@ import {
type IssuedInvoice,
IssuedInvoiceTax,
ItemAmount,
ItemDiscountPercentage,
TaxPercentage,
} from "../../../../../../domain";
import type {
CustomerInvoiceTaxCreationAttributes,

View File

@ -1,2 +1,2 @@
export * from "./domain";
export * from "./list";
export * from "./summary";

View File

@ -1 +0,0 @@
export * from "./sequelize-issued-invoice.list.mapper";

View File

@ -0,0 +1 @@
export * from "./sequelize-issued-invoice-summary.mapper";

View File

@ -22,7 +22,7 @@ export class SequelizeIssuedInvoiceRecipientListMapper extends SequelizeQueryMap
CustomerInvoiceModel,
InvoiceRecipient
> {
public mapToDTO(
public mapToReadModel(
raw: CustomerInvoiceModel,
params?: MapperParamsType
): Result<InvoiceRecipient, Error> {

View File

@ -11,10 +11,7 @@ import {
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import type {
IIssuedInvoiceSummaryMapper,
IssuedInvoiceSummary,
} from "../../../../../../application";
import type { IssuedInvoiceSummary } from "../../../../../../application";
import {
InvoiceAmount,
InvoiceNumber,
@ -24,20 +21,20 @@ import {
} from "../../../../../../domain";
import type { CustomerInvoiceModel } from "../../../../../common";
import { SequelizeIssuedInvoiceRecipientListMapper } from "./sequelize-issued-invoice-recipient.list.mapper";
import { SequelizeVerifactuRecordListMapper } from "./sequelize-verifactu-record.list.mapper";
import { SequelizeIssuedInvoiceRecipientListMapper } from "./sequelize-issued-invoice-recipient-summary.mapper";
import { SequelizeVerifactuRecordSummaryMapper } from "./sequelize-verifactu-record-summary.mapper";
export class SequelizeIssuedInvoiceListMapper
extends SequelizeQueryMapper<CustomerInvoiceModel, IssuedInvoiceSummary>
implements IIssuedInvoiceSummaryMapper
{
export class SequelizeIssuedInvoiceSummaryMapper extends SequelizeQueryMapper<
CustomerInvoiceModel,
IssuedInvoiceSummary
> {
private _recipientMapper: SequelizeIssuedInvoiceRecipientListMapper;
private _verifactuMapper: SequelizeVerifactuRecordListMapper;
private _verifactuMapper: SequelizeVerifactuRecordSummaryMapper;
constructor() {
super();
this._recipientMapper = new SequelizeIssuedInvoiceRecipientListMapper();
this._verifactuMapper = new SequelizeVerifactuRecordListMapper();
this._verifactuMapper = new SequelizeVerifactuRecordSummaryMapper();
}
public mapToReadModel(
@ -50,7 +47,7 @@ export class SequelizeIssuedInvoiceListMapper
const attributes = this._mapAttributesToReadModel(raw, { errors, ...params });
// 2) Recipient (snapshot en la factura o include)
const recipientResult = this._recipientMapper.mapToDTO(raw, {
const recipientResult = this._recipientMapper.mapToReadModel(raw, {
errors,
attributes,
...params,
@ -66,7 +63,10 @@ export class SequelizeIssuedInvoiceListMapper
// 4) Verifactu record
let verifactu: Maybe<VerifactuRecord> = Maybe.none();
if (raw.verifactu) {
const verifactuResult = this._verifactuMapper.mapToDTO(raw.verifactu, { errors, ...params });
const verifactuResult = this._verifactuMapper.mapToReadModel(raw.verifactu, {
errors,
...params,
});
if (verifactuResult.isFailure) {
errors.push({

View File

@ -12,11 +12,11 @@ import { Result } from "@repo/rdx-utils";
import { VerifactuRecord, VerifactuRecordEstado } from "../../../../../../domain";
import type { VerifactuRecordModel } from "../../../../../common";
export class SequelizeVerifactuRecordListMapper extends SequelizeQueryMapper<
export class SequelizeVerifactuRecordSummaryMapper extends SequelizeQueryMapper<
VerifactuRecordModel,
VerifactuRecord
> {
public mapToDTO(
public mapToReadModel(
raw: VerifactuRecordModel,
params?: MapperParamsType
): Result<VerifactuRecord, Error> {

View File

@ -14,7 +14,7 @@ import {
} from "../../../../common";
import type {
SequelizeIssuedInvoiceDomainMapper,
SequelizeIssuedInvoiceListMapper,
SequelizeIssuedInvoiceSummaryMapper,
} from "../mappers";
export class IssuedInvoiceRepository
@ -23,7 +23,7 @@ export class IssuedInvoiceRepository
{
constructor(
private readonly domainMapper: SequelizeIssuedInvoiceDomainMapper,
private readonly listMapper: SequelizeIssuedInvoiceListMapper,
private readonly summaryMapper: SequelizeIssuedInvoiceSummaryMapper,
database: Sequelize
) {
super({ database });
@ -216,8 +216,8 @@ export class IssuedInvoiceRepository
const { CustomerModel } = this.database.models;
try {
const converter = new CriteriaToSequelizeConverter();
const query = converter.convert(criteria, {
const criteriaConverter = new CriteriaToSequelizeConverter();
const query = criteriaConverter.convert(criteria, {
searchableFields: ["invoice_number", "reference", "description"],
mappings: {
reference: "CustomerInvoiceModel.reference",
@ -301,7 +301,7 @@ export class IssuedInvoiceRepository
}),
]);
return this.listMapper.mapToReadModelCollection(rows, count);
return this.summaryMapper.mapToReadModelCollection(rows, count);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}

View File

@ -1,14 +1,11 @@
import {
CreateProformaInputMapper,
type ICatalogs,
type IProformaDomainMapper,
type IProformaListMapper,
} from "../../../application";
import { SequelizeProformaDomainMapper, SequelizeProformaListMapper } from "../persistence";
import type { ICatalogs } from "@erp/core/api";
import { CreateProformaInputMapper } from "../../../application";
import { SequelizeProformaDomainMapper, SequelizeProformaSummaryMapper } from "../persistence";
export interface IProformaPersistenceMappers {
domainMapper: IProformaDomainMapper;
listMapper: IProformaListMapper;
domainMapper: SequelizeProformaDomainMapper;
listMapper: SequelizeProformaSummaryMapper;
createMapper: CreateProformaInputMapper;
}
@ -22,7 +19,7 @@ export const buildProformaPersistenceMappers = (
const domainMapper = new SequelizeProformaDomainMapper({
taxCatalog,
});
const listMapper = new SequelizeProformaListMapper();
const listMapper = new SequelizeProformaSummaryMapper();
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
const createMapper = new CreateProformaInputMapper({ taxCatalog });

View File

@ -60,7 +60,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
listProformas: () =>
buildListProformasUseCase({
finder,
itemSnapshotBuilder: snapshotBuilders.list,
summarySnapshotBuilder: snapshotBuilders.summary,
transactionManager,
}),
@ -90,76 +90,3 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
},
};
}
/*const mapperRegistry = new InMemoryMapperRegistry();
mapperRegistry
.registerDomainMapper(
{ resource: "customer-invoice" },
new CustomerInvoiceDomainMapper({ taxCatalog: catalogs.taxes })
)
.registerQueryMappers([
{
key: { resource: "customer-invoice", query: "LIST" },
mapper: new CustomerInvoiceListMapper(),
},
]);
// Repository & Services
const numberGenerator = new SequelizeInvoiceNumberGenerator();
const appService = new CustomerInvoiceApplicationService(repository, numberGenerator);
// Presenter Registry
const presenterRegistry = new InMemoryPresenterRegistry();
presenterRegistry.registerPresenters([
// FULL
{
key: { resource: "proforma-items", projection: "FULL" },
presenter: new ProformaItemsFullPresenter(presenterRegistry),
},
{
key: { resource: "proforma-recipient", projection: "FULL" },
presenter: new ProformaRecipientFullPresenter(presenterRegistry),
},
{
key: { resource: "proforma", projection: "FULL" },
presenter: new ProformaFullPresenter(presenterRegistry),
},
// LIST
{
key: { resource: "proforma", projection: "LIST" },
presenter: new ProformaListPresenter(presenterRegistry),
},
// REPORT
{
key: { resource: "proforma", projection: "REPORT" },
presenter: new ProformaReportPresenter(presenterRegistry),
},
{
key: { resource: "proforma-taxes", projection: "REPORT" },
presenter: new ProformaTaxesReportPresenter(presenterRegistry),
},
{
key: { resource: "proforma-items", projection: "REPORT" },
presenter: new ProformaItemsReportPresenter(presenterRegistry),
},
]);
const useCases: ProformasDeps["useCases"] = {
// Proformas
list_proformas: () => new ListProformasUseCase(appService, transactionManager, presenterRegistry),
get_proforma: () => new GetProformaUseCase(appService, transactionManager, presenterRegistry),
create_proforma: () =>
new CreateProformaUseCase(appService, transactionManager, presenterRegistry, catalogs.taxes),
update_proforma: () =>
new UpdateProformaUseCase(appService, transactionManager, presenterRegistry),
delete_proforma: () => new DeleteProformaUseCase(appService, transactionManager),
report_proforma: () =>
new ReportProformaUseCase(appService, transactionManager, presenterRegistry),
issue_proforma: () => new IssueProformaUseCase(appService, transactionManager, presenterRegistry),
changeStatus_proforma: () => new ChangeStatusProformaUseCase(appService, transactionManager),
*/

View File

@ -13,7 +13,6 @@ import {
} from "@repo/rdx-ddd";
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import type { IProformaDomainMapper } from "../../../../../../application";
import {
type IProformaProps,
InvoiceNumber,
@ -32,10 +31,11 @@ import { SequelizeProformaItemDomainMapper } from "./sequelize-proforma-item-dom
import { SequelizeProformaRecipientDomainMapper } from "./sequelize-proforma-recipient-domain.mapper";
import { SequelizeProformaTaxesDomainMapper } from "./sequelize-proforma-taxes-domain.mapper";
export class SequelizeProformaDomainMapper
extends SequelizeDomainMapper<CustomerInvoiceModel, CustomerInvoiceCreationAttributes, Proforma>
implements IProformaDomainMapper
{
export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
CustomerInvoiceModel,
CustomerInvoiceCreationAttributes,
Proforma
> {
private _itemsMapper: SequelizeProformaItemDomainMapper;
private _recipientMapper: SequelizeProformaRecipientDomainMapper;
private _taxesMapper: SequelizeProformaTaxesDomainMapper;

View File

@ -1,2 +1,2 @@
export * from "./domain";
export * from "./list";
export * from "./summary";

View File

@ -1 +0,0 @@
export * from "./sequelize-proforma.list.mapper";

View File

@ -0,0 +1 @@
export * from "./sequelize-proforma-summary.mapper";

View File

@ -1,8 +1,4 @@
import {
type IQueryMapperWithBulk,
type MapperParamsType,
SequelizeQueryMapper,
} from "@erp/core/api";
import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api";
import {
City,
Country,
@ -18,18 +14,15 @@ import {
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { ProformaListDTO } from "../../../../../../application";
import type { ProformaSummary } from "../../../../../../application";
import { InvoiceRecipient } from "../../../../../../domain";
import type { CustomerInvoiceModel } from "../../../../../common";
interface IInvoiceRecipientListMapper
extends IQueryMapperWithBulk<CustomerInvoiceModel, InvoiceRecipient> {}
export class SequelizeInvoiceRecipientListMapper
extends SequelizeQueryMapper<CustomerInvoiceModel, InvoiceRecipient>
implements IInvoiceRecipientListMapper
{
public mapToDTO(
export class SequelizeInvoiceRecipientSummaryMapper extends SequelizeQueryMapper<
CustomerInvoiceModel,
InvoiceRecipient
> {
public mapToReadModel(
raw: CustomerInvoiceModel,
params?: MapperParamsType
): Result<InvoiceRecipient, Error> {
@ -39,7 +32,7 @@ export class SequelizeInvoiceRecipientListMapper
const { errors, attributes } = params as {
errors: ValidationErrorDetail[];
attributes: Partial<ProformaListDTO>;
attributes: Partial<ProformaSummary>;
};
const { isProforma } = attributes;

View File

@ -11,7 +11,7 @@ import {
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IProformaListMapper, ProformaListDTO } from "../../../../../../application";
import type { ProformaSummary } from "../../../../../../application";
import {
InvoiceAmount,
InvoiceNumber,
@ -20,30 +20,30 @@ import {
} from "../../../../../../domain";
import type { CustomerInvoiceModel } from "../../../../../common";
import { SequelizeInvoiceRecipientListMapper } from "./sequelize-proforma-recipient.list.mapper";
import { SequelizeInvoiceRecipientSummaryMapper } from "./sequelize-proforma-recipient-summary.mapper";
export class SequelizeProformaListMapper
extends SequelizeQueryMapper<CustomerInvoiceModel, ProformaListDTO>
implements IProformaListMapper
{
private _recipientMapper: SequelizeInvoiceRecipientListMapper;
export class SequelizeProformaSummaryMapper extends SequelizeQueryMapper<
CustomerInvoiceModel,
ProformaSummary
> {
private _recipientMapper: SequelizeInvoiceRecipientSummaryMapper;
constructor() {
super();
this._recipientMapper = new SequelizeInvoiceRecipientListMapper();
this._recipientMapper = new SequelizeInvoiceRecipientSummaryMapper();
}
public mapToDTO(
public mapToReadModel(
raw: CustomerInvoiceModel,
params?: MapperParamsType
): Result<ProformaListDTO, Error> {
): Result<ProformaSummary, Error> {
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, {
const recipientResult = this._recipientMapper.mapToReadModel(raw, {
errors,
attributes,
...params,

View File

@ -9,26 +9,22 @@ import type { UniqueID } from "@repo/rdx-ddd";
import { type Collection, Result } from "@repo/rdx-utils";
import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize";
import type {
IProformaDomainMapper,
IProformaListMapper,
IProformaRepository,
ProformaListDTO,
} from "../../../../../application";
import type { IProformaRepository, ProformaSummary } from "../../../../../application";
import type { InvoiceStatus, Proforma } from "../../../../../domain";
import {
CustomerInvoiceItemModel,
CustomerInvoiceModel,
CustomerInvoiceTaxModel,
} from "../../../../common";
import type { SequelizeProformaDomainMapper, SequelizeProformaSummaryMapper } from "../mappers";
export class ProformaRepository
extends SequelizeRepository<Proforma>
implements IProformaRepository
{
constructor(
private readonly domainMapper: IProformaDomainMapper,
private readonly listMapper: IProformaListMapper,
private readonly domainMapper: SequelizeProformaDomainMapper,
private readonly summaryMapper: SequelizeProformaSummaryMapper,
database: Sequelize
) {
super({ database });
@ -339,12 +335,12 @@ export class ProformaRepository
criteria: Criteria,
transaction: Transaction,
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
): Promise<Result<Collection<ProformaListDTO>, Error>> {
): Promise<Result<Collection<ProformaSummary>, Error>> {
const { CustomerModel } = this.database.models;
try {
const converter = new CriteriaToSequelizeConverter();
const query = converter.convert(criteria, {
const criteriaConverter = new CriteriaToSequelizeConverter();
const query = criteriaConverter.convert(criteria, {
searchableFields: ["invoice_number", "reference", "description"],
mappings: {
reference: "CustomerInvoiceModel.reference",
@ -424,7 +420,7 @@ export class ProformaRepository
}),
]);
return this.listMapper.mapToReadModelCollection(rows, count);
return this.summaryMapper.mapToReadModelCollection(rows, count);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}

View File

@ -1,6 +1,6 @@
import type { IProformaRepository } from "../repositories";
import { type IProformaFinder, ProformaFinder } from "../services";
import type { ICustomerRepository } from "../repositories";
import { CustomerFinder, type ICustomerFinder } from "../services";
export function buildProformaFinder(repository: IProformaRepository): IProformaFinder {
return new ProformaFinder(repository);
export function buildCustomerFinder(repository: ICustomerRepository): ICustomerFinder {
return new CustomerFinder(repository);
}

View File

@ -1,18 +1,9 @@
// application/issued-invoices/di/snapshot-builders.di.ts
import {
CustomerFullSnapshotBuilder,
CustomerItemReportSnapshotBuilder,
CustomerItemsFullSnapshotBuilder,
CustomerListItemSnapshotBuilder,
CustomerRecipientFullSnapshotBuilder,
CustomerReportSnapshotBuilder,
CustomerTaxReportSnapshotBuilder,
CustomerTaxesFullSnapshotBuilder,
} from "../snapshot-builders";
import { CustomerSummarySnapshotBuilder } from "../snapshot-builders";
export function buildCustomerSnapshotBuilders() {
const itemsBuilder = new CustomerItemsFullSnapshotBuilder();
/*const itemsBuilder = new CustomerItemsFullSnapshotBuilder();
const taxesBuilder = new CustomerTaxesFullSnapshotBuilder();
@ -22,20 +13,20 @@ export function buildCustomerSnapshotBuilders() {
itemsBuilder,
recipientBuilder,
taxesBuilder
);
);*/
const listSnapshotBuilder = new CustomerListItemSnapshotBuilder();
const summarySnapshotBuilder = new CustomerSummarySnapshotBuilder();
const itemsReportBuilder = new CustomerItemReportSnapshotBuilder();
/*const itemsReportBuilder = new CustomerItemReportSnapshotBuilder();
const taxesReportBuilder = new CustomerTaxReportSnapshotBuilder();
const reportSnapshotBuilder = new CustomerReportSnapshotBuilder(
itemsReportBuilder,
taxesReportBuilder
);
);*/
return {
full: fullSnapshotBuilder,
list: listSnapshotBuilder,
report: reportSnapshotBuilder,
//full: fullSnapshotBuilder,
summary: summarySnapshotBuilder,
//report: reportSnapshotBuilder,
};
}

View File

@ -1,36 +1,30 @@
import type { ITransactionManager } from "@erp/core/api";
import type { ICreateCustomerInputMapper } from "../mappers";
import type {
ICustomerCreator,
ICustomerFinder,
CustomerDocumentGeneratorService,
} from "../services";
import type {
ICustomerListItemSnapshotBuilder,
ICustomerReportSnapshotBuilder,
} from "../snapshot-builders";
import type { ICustomerFullSnapshotBuilder } from "../snapshot-builders/full";
import { GetCustomerByIdUseCase, ListCustomersUseCase, ReportCustomerUseCase } from "../use-cases";
import { CreateCustomerUseCase } from "../use-cases/create-customer";
import type { ICustomerFinder } from "../services";
import type { ICustomerSummarySnapshotBuilder } from "../snapshot-builders";
import { ListCustomersUseCase } from "../use-cases";
export function buildGetCustomerByIdUseCase(deps: {
/*export function buildGetCustomerByIdUseCase(deps: {
finder: ICustomerFinder;
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new GetCustomerByIdUseCase(deps.finder, deps.fullSnapshotBuilder, deps.transactionManager);
}
}*/
export function buildListCustomersUseCase(deps: {
finder: ICustomerFinder;
itemSnapshotBuilder: ICustomerListItemSnapshotBuilder;
summarySnapshotBuilder: ICustomerSummarySnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new ListCustomersUseCase(deps.finder, deps.itemSnapshotBuilder, deps.transactionManager);
return new ListCustomersUseCase(
deps.finder,
deps.summarySnapshotBuilder,
deps.transactionManager
);
}
export function buildReportCustomerUseCase(deps: {
/*export function buildReportCustomerUseCase(deps: {
finder: ICustomerFinder;
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
reportSnapshotBuilder: ICustomerReportSnapshotBuilder;
@ -58,7 +52,7 @@ export function buildCreateCustomerUseCase(deps: {
fullSnapshotBuilder: deps.fullSnapshotBuilder,
transactionManager: deps.transactionManager,
});
}
}*/
/*export function buildUpdateCustomerUseCase(deps: {
finder: ICustomerFinder;

View File

@ -1,5 +1,5 @@
export * from "./customer-creator.di";
//export * from "./customer-creator.di";
export * from "./customer-finder.di";
export * from "./customer-input-mappers.di";
//export * from "./customer-input-mappers.di";
export * from "./customer-snapshot-builders.di";
export * from "./customer-use-cases.di";

View File

@ -1,5 +1,4 @@
export * from "./di";
export * from "./mappers";
export * from "./models";
export * from "./repositories";
export * from "./services";

View File

@ -1,7 +0,0 @@
import type { DomainMapperWithBulk } from "@erp/core/api";
import type { Customer } from "../../domain";
import type { CustomerCreationAttributes, CustomerModel } from "../../infrastructure";
export interface ICustomerDomainMapper
extends DomainMapperWithBulk<CustomerModel | CustomerCreationAttributes, Customer> {}

View File

@ -1,7 +0,0 @@
import type { IQueryMapperWithBulk } from "@erp/core/api";
import type { CustomerModel } from "../../infrastructure";
import type { CustomerSummary } from "../models";
export interface ICustomerSummaryMapper
extends IQueryMapperWithBulk<CustomerModel, CustomerSummary> {}

View File

@ -1,2 +0,0 @@
export * from "./customer-domain-mapper.interface";
export * from "./customer-summary-mapper.interface";

View File

@ -4,7 +4,7 @@ import type { Collection, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { Customer } from "../../domain";
import type { CustomerListDTO } from "../dtos";
import type { CustomerSummary } from "../models";
import type { ICustomerRepository } from "../repositories";
export interface ICustomerFinder {
@ -24,7 +24,7 @@ export interface ICustomerFinder {
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<CustomerListDTO>, Error>>;
): Promise<Result<Collection<CustomerSummary>, Error>>;
}
export class CustomerFinder implements ICustomerFinder {
@ -50,7 +50,7 @@ export class CustomerFinder implements ICustomerFinder {
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<CustomerListDTO>, Error>> {
): Promise<Result<Collection<CustomerSummary>, Error>> {
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
}
}

View File

@ -1,3 +1,3 @@
//export * from "./full";
export * from "./list";
export * from "./summary";
//export * from "./report";

View File

@ -1,14 +0,0 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import type { CustomerListDTO } from "../../dtos";
import type { ICustomerListItemSnapshot } from "./customer-list-item-snapshot.interface";
export interface ICustomerListItemSnapshotBuilder
extends ISnapshotBuilder<CustomerListDTO, ICustomerListItemSnapshot> {}
export class CustomerListItemSnapshotBuilder implements ICustomerListItemSnapshotBuilder {
toOutput(customer: CustomerListDTO): ICustomerListItemSnapshot {
return {};
}
}

View File

@ -1 +0,0 @@
export type ICustomerListItemSnapshot = {};

View File

@ -1,2 +0,0 @@
export * from "./customer-list-item-snapshot.interface";
export * from "./customer-list-item-snapshot-builder";

View File

@ -0,0 +1,49 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToEmptyString } from "@repo/rdx-ddd";
import type { CustomerSummary } from "../../models";
import type { ICustomerSummarySnapshot } from "./customer-summary-snapshot.interface";
export interface ICustomerSummarySnapshotBuilder
extends ISnapshotBuilder<CustomerSummary, ICustomerSummarySnapshot> {}
export class CustomerSummarySnapshotBuilder implements ICustomerSummarySnapshotBuilder {
toOutput(customer: CustomerSummary): ICustomerSummarySnapshot {
const { address } = customer;
return {
id: customer.id.toString(),
company_id: customer.companyId.toString(),
status: customer.status.toString(),
reference: maybeToEmptyString(customer.reference, (value) => value.toString()),
is_company: String(customer.isCompany),
name: customer.name.toString(),
trade_name: maybeToEmptyString(customer.tradeName, (value) => value.toString()),
tin: maybeToEmptyString(customer.tin, (value) => value.toString()),
street: maybeToEmptyString(address.street, (value) => value.toString()),
street2: maybeToEmptyString(address.street2, (value) => value.toString()),
city: maybeToEmptyString(address.city, (value) => value.toString()),
postal_code: maybeToEmptyString(address.postalCode, (value) => value.toString()),
province: maybeToEmptyString(address.province, (value) => value.toString()),
country: maybeToEmptyString(address.country, (value) => value.toString()),
email_primary: maybeToEmptyString(customer.emailPrimary, (value) => value.toString()),
email_secondary: maybeToEmptyString(customer.emailSecondary, (value) => value.toString()),
phone_primary: maybeToEmptyString(customer.phonePrimary, (value) => value.toString()),
phone_secondary: maybeToEmptyString(customer.phoneSecondary, (value) => value.toString()),
mobile_primary: maybeToEmptyString(customer.mobilePrimary, (value) => value.toString()),
mobile_secondary: maybeToEmptyString(customer.mobileSecondary, (value) => value.toString()),
fax: maybeToEmptyString(customer.fax, (value) => value.toString()),
website: maybeToEmptyString(customer.website, (value) => value.toString()),
language_code: customer.languageCode.code,
currency_code: customer.currencyCode.code,
};
}
}

View File

@ -0,0 +1,35 @@
export type ICustomerSummarySnapshot = {
id: string;
company_id: string;
status: string;
reference: string;
is_company: string;
name: string;
trade_name: string;
tin: string;
street: string;
street2: string;
city: string;
postal_code: string;
province: string;
country: string;
email_primary: string;
email_secondary: string;
phone_primary: string;
phone_secondary: string;
mobile_primary: string;
mobile_secondary: string;
fax: string;
website: string;
language_code: string;
currency_code: string;
metadata?: Record<string, string>;
};

View File

@ -0,0 +1,2 @@
export * from "./customer-summary-snapshot.interface";
export * from "./customer-summary-snapshot-builder";

View File

@ -4,9 +4,8 @@ import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { ListCustomersResponseDTO } from "../../../common/dto";
import type { ICustomerFinder } from "../services";
import type { ICustomerListItemSnapshotBuilder } from "../snapshot-builders/list";
import type { ICustomerSummarySnapshotBuilder } from "../snapshot-builders/summary";
type ListCustomersUseCaseInput = {
companyId: UniqueID;
@ -16,13 +15,11 @@ type ListCustomersUseCaseInput = {
export class ListCustomersUseCase {
constructor(
private readonly finder: ICustomerFinder,
private readonly listItemSnapshotBuilder: ICustomerListItemSnapshotBuilder,
private readonly summarySnapshotBuilder: ICustomerSummarySnapshotBuilder,
private readonly transactionManager: ITransactionManager
) {}
public execute(
params: ListCustomersUseCaseInput
): Promise<Result<ListCustomersResponseDTO, Error>> {
public execute(params: ListCustomersUseCaseInput) {
const { criteria, companyId } = params;
return this.transactionManager.complete(async (transaction: Transaction) => {
@ -36,7 +33,7 @@ export class ListCustomersUseCase {
const customers = result.data;
const totalCustomers = customers.total();
const items = customers.map((item) => this.listItemSnapshotBuilder.toOutput(item));
const items = customers.map((item) => this.summarySnapshotBuilder.toOutput(item));
const snapshot = {
page: criteria.pageNumber,

View File

@ -1,6 +1,7 @@
import type { IModuleServer } from "@erp/core/api";
import { customersRouter, models } from "./infrastructure";
import { buildCustomersDependencies, buildCustomerServices, CustomersInternalDeps } from "./infrastructure/di";
export * from "./infrastructure/sequelize";
@ -19,12 +20,11 @@ export const customersAPIModule: IModuleServer = {
async setup(params) {
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
// 1) Dominio interno
//const customerInternalDeps = buildCustomerDependencies(params);
const customerInternalDeps = buildCustomersDependencies(params);
// 2) Servicios públicos (Application Services)
//const customerServices = buildCustomerServices(customerInternalDeps);
const customerServices = buildCustomerServices(customerInternalDeps);
logger.info("🚀 Customers module dependencies registered", {
label: this.name,
@ -35,14 +35,10 @@ export const customersAPIModule: IModuleServer = {
models,
// Servicios expuestos a otros módulos
services: {
//customers: customerServices,
},
services: customerServices,
// Implementación privada del módulo
internal: {
//customers: customerInternalDeps,
},
internal:customerInternalDeps,
};
},
@ -57,7 +53,7 @@ export const customersAPIModule: IModuleServer = {
const { app, baseRoutePath, logger, getInternal } = params;
// Recuperamos el dominio interno del módulo
const customersInternalDeps = getInternal("customers", "customers");
const customersInternalDeps = getInternal<CustomersInternalDeps>("customers");
// Registro de rutas HTTP
customersRouter(params, customersInternalDeps);

View File

@ -1,76 +0,0 @@
import type { IMapperRegistry, IPresenterRegistry, ModuleParams } from "@erp/core/api";
import {
InMemoryMapperRegistry,
InMemoryPresenterRegistry,
SequelizeTransactionManager,
} from "@erp/core/api";
import {
CreateCustomerUseCase,
GetCustomerUseCase,
ListCustomersUseCase,
UpdateCustomerUseCase,
} from "../application";
import { CustomerApplicationService } from "../application/customer-application.service";
import { CustomerFullPresenter, ListCustomersPresenter } from "../application/presenters";
import { CustomerDomainMapper, CustomerSummaryMapper } from "./mappers";
import { CustomerRepository } from "./sequelize";
export type CustomerDeps = {
transactionManager: SequelizeTransactionManager;
mapperRegistry: IMapperRegistry;
presenterRegistry: IPresenterRegistry;
repo: CustomerRepository;
service: CustomerApplicationService;
build: {
list: () => ListCustomersUseCase;
get: () => GetCustomerUseCase;
create: () => CreateCustomerUseCase;
update: () => UpdateCustomerUseCase;
//delete: () => DeleteCustomerUseCase;
};
};
export function buildCustomerDependencies(params: ModuleParams): CustomerDeps {
const { database } = params;
const transactionManager = new SequelizeTransactionManager(database);
// Mapper Registry
const mapperRegistry = new InMemoryMapperRegistry();
mapperRegistry
.registerDomainMapper({ resource: "customer" }, new CustomerDomainMapper())
.registerQueryMapper({ resource: "customer", query: "LIST" }, new CustomerSummaryMapper());
// Repository & Services
const repo = new CustomerRepository({ mapperRegistry, database });
const service = new CustomerApplicationService(repo);
// Presenter Registry
const presenterRegistry = new InMemoryPresenterRegistry();
presenterRegistry.registerPresenters([
{
key: { resource: "customer", projection: "FULL" },
presenter: new CustomerFullPresenter(presenterRegistry),
},
{
key: { resource: "customer", projection: "LIST" },
presenter: new ListCustomersPresenter(presenterRegistry),
},
]);
return {
transactionManager,
repo,
mapperRegistry,
presenterRegistry,
service,
build: {
list: () => new ListCustomersUseCase(service, transactionManager, presenterRegistry),
get: () => new GetCustomerUseCase(service, transactionManager, presenterRegistry),
create: () => new CreateCustomerUseCase(service, transactionManager, presenterRegistry),
update: () => new UpdateCustomerUseCase(service, transactionManager, presenterRegistry),
//delete: () => new DeleteCustomerUseCase(_service!, transactionManager!),
},
};
}

View File

@ -1,36 +1,24 @@
import {
CreateCustomerInputMapper,
type ICatalogs,
type ICustomerDomainMapper,
type ICustomerListMapper,
} from "../../../application";
import { SequelizeCustomerDomainMapper, SequelizeCustomerListMapper } from "../persistence";
import { SequelizeCustomerDomainMapper, SequelizeCustomerSummaryMapper } from "../mappers";
export interface ICustomerPersistenceMappers {
domainMapper: ICustomerDomainMapper;
listMapper: ICustomerListMapper;
domainMapper: SequelizeCustomerDomainMapper;
summaryMapper: SequelizeCustomerSummaryMapper;
createMapper: CreateCustomerInputMapper;
//createMapper: CreateCustomerInputMapper;
}
export const buildCustomerPersistenceMappers = (
catalogs: ICatalogs
): ICustomerPersistenceMappers => {
const { taxCatalog } = catalogs;
export const buildCustomerPersistenceMappers = (): ICustomerPersistenceMappers => {
// Mappers para el repositorio
const domainMapper = new SequelizeCustomerDomainMapper({
taxCatalog,
});
const listMapper = new SequelizeCustomerListMapper();
const domainMapper = new SequelizeCustomerDomainMapper();
const summaryMapper = new SequelizeCustomerSummaryMapper();
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
const createMapper = new CreateCustomerInputMapper({ taxCatalog });
//const createMapper = new CreateCustomerInputMapper();
return {
domainMapper,
listMapper,
summaryMapper,
createMapper,
//createMapper,
};
};

View File

@ -0,0 +1,24 @@
import type { CustomersInternalDeps } from "./customers.di";
export type CustomersServicesDeps = {
services: {
listCustomers: (filters: unknown, context: unknown) => null;
getCustomerById: (id: unknown, context: unknown) => null;
generateCustomerReport: (id: unknown, options: unknown, context: unknown) => null;
};
};
export function buildCustomerServices(deps: CustomersInternalDeps): CustomersServicesDeps {
return {
services: {
listCustomers: (filters, context) => null,
//internal.useCases.listCustomers().execute(filters, context),
getCustomerById: (id, context) => null,
//internal.useCases.getCustomerById().execute(id, context),
generateCustomerReport: (id, options, context) => null,
//internal.useCases.reportCustomer().execute(id, options, context),
},
};
}

View File

@ -1,6 +1,6 @@
import type { Sequelize } from "sequelize";
import { CustomerRepository } from "../persistence";
import { CustomerRepository } from "../sequelize";
import type { ICustomerPersistenceMappers } from "./customer-persistence-mappers.di";
@ -10,5 +10,5 @@ export const buildCustomerRepository = (params: {
}) => {
const { database, mappers } = params;
return new CustomerRepository(mappers.domainMapper, mappers.listMapper, database);
return new CustomerRepository(mappers.domainMapper, mappers.summaryMapper, database);
};

View File

@ -1,11 +1,21 @@
import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api";
import {
type ListCustomersUseCase,
buildCustomerFinder,
buildCustomerSnapshotBuilders,
buildListCustomersUseCase,
} from "../../application";
import { buildCustomerPersistenceMappers } from "./customer-persistence-mappers.di";
import { buildCustomerRepository } from "./customer-repositories.di";
export type CustomersInternalDeps = {
useCases: {
listCustomers: () => ListCustomersUseCase;
getCustomerById: () => GetCustomerByIdUseCase;
reportCustomer: () => ReportCustomerUseCase;
createCustomer: () => CreateCustomerUseCase;
//getCustomerById: () => GetCustomerByIdUseCase;
//reportCustomer: () => ReportCustomerUseCase;
//createCustomer: () => CreateCustomerUseCase;
/*
updateCustomer: () => UpdateCustomerUseCase;
@ -15,25 +25,24 @@ export type CustomersInternalDeps = {
};
};
export function buildCustomersDependencies(params: ModuleParams): CustomersInternalDeps {
const { database } = params;
// Infrastructure
const transactionManager = buildTransactionManager(database);
const catalogs = buildCatalogs();
const persistenceMappers = buildCustomerPersistenceMappers(catalogs);
const persistenceMappers = buildCustomerPersistenceMappers();
const repository = buildCustomerRepository({ database, mappers: persistenceMappers });
const numberService = buildCustomerNumberGenerator();
//const numberService = buildCustomerNumberGenerator();
// Application helpers
const inputMappers = buildCustomerInputMappers(catalogs);
//const inputMappers = buildCustomerInputMappers(catalogs);
const finder = buildCustomerFinder(repository);
const creator = buildCustomerCreator({ numberService, repository });
//const creator = buildCustomerCreator({ numberService, repository });
const snapshotBuilders = buildCustomersnapshotBuilders();
const documentGeneratorPipeline = buildCustomerDocumentService(params);
const snapshotBuilders = buildCustomerSnapshotBuilders();
//const documentGeneratorPipeline = buildCustomerDocumentService(params);
// Internal use cases (factories)
return {
@ -41,11 +50,11 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
listCustomers: () =>
buildListCustomersUseCase({
finder,
itemSnapshotBuilder: snapshotBuilders.list,
summarySnapshotBuilder: snapshotBuilders.summary,
transactionManager,
}),
getCustomerById: () =>
/*getCustomerById: () =>
buildGetCustomerByIdUseCase({
finder,
fullSnapshotBuilder: snapshotBuilders.full,
@ -67,7 +76,7 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
dtoMapper: inputMappers.createInputMapper,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
}),*/
},
};
}

View File

@ -1 +1,2 @@
export * from "./customers.di";
export * from "./customer-public-services";
export * from "./customers.di";

View File

@ -1,39 +1,22 @@
import {
type RequestWithAuth,
mockUser,
requireAuthenticated,
requireCompanyContext,
} from "@erp/auth/api";
import { type ModuleParams, validateRequest } from "@erp/core/api";
import type { ILogger } from "@repo/rdx-logger";
import { type Application, type NextFunction, type Request, type Response, Router } from "express";
import type { Sequelize } from "sequelize";
import { type ModuleParams, RequestWithAuth, validateRequest } from "@erp/core/api";
import { type NextFunction, type Request, type Response, Router } from "express";
import {
CreateCustomerRequestSchema,
CustomerListRequestSchema,
GetCustomerByIdRequestSchema,
UpdateCustomerByIdParamsRequestSchema,
UpdateCustomerByIdRequestSchema,
CustomerListRequestSchema
} from "../../../common/dto";
import { buildCustomerDependencies } from "../dependencies";
import type { CustomersInternalDeps } from "../di";
import {
CreateCustomerController,
GetCustomerController,
ListCustomersController,
UpdateCustomerController,
ListCustomersController
} from "./controllers";
export const customersRouter = (params: ModuleParams, deps: CustomerInternalDeps) => {
const { app, baseRoutePath, logger } = params as {
app: Application;
database: Sequelize;
baseRoutePath: string;
logger: ILogger;
};
const deps = buildCustomerDependencies(params);
export const customersRouter = (params: ModuleParams, deps: CustomersInternalDeps) => {
const { app, config } = params;
const router: Router = Router({ mergeParams: true });
@ -62,49 +45,49 @@ export const customersRouter = (params: ModuleParams, deps: CustomerInternalDeps
validateRequest(CustomerListRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.build.list();
const useCase = deps.useCases.listCustomers();
const controller = new ListCustomersController(useCase /*, deps.presenters.list */);
return controller.execute(req, res, next);
}
);
router.get(
/* router.get(
"/:customer_id",
//checkTabContext,
validateRequest(GetCustomerByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.build.get();
const useCase = deps.useCases.get();
const controller = new GetCustomerController(useCase);
return controller.execute(req, res, next);
}
);
); */
router.post(
/* router.post(
"/",
//checkTabContext,
validateRequest(CreateCustomerRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.build.create();
const useCase = deps.useCases.create();
const controller = new CreateCustomerController(useCase);
return controller.execute(req, res, next);
}
);
); */
router.put(
/* router.put(
"/:customer_id",
//checkTabContext,
validateRequest(UpdateCustomerByIdParamsRequestSchema, "params"),
validateRequest(UpdateCustomerByIdRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.build.update();
const useCase = deps.useCases.update();
const controller = new UpdateCustomerController(useCase);
return controller.execute(req, res, next);
}
);
*/
/*router.delete(
"/:customer_id",
//checkTabContext,
@ -117,5 +100,5 @@ export const customersRouter = (params: ModuleParams, deps: CustomerInternalDeps
}
);*/
app.use(`${baseRoutePath}/customers`, router);
app.use(`${config.server.apiBasePath}/customers`, router);
};

View File

@ -1 +1 @@
export * from "./customer.mapper";
export * from "./sequelize-customer.mapper";

View File

@ -1,5 +1,4 @@
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import type { ICustomerDomainMapper } from "@erp/customers/api/application";
import {
City,
Country,
@ -28,10 +27,11 @@ import { Collection, Result } from "@repo/rdx-utils";
import { Customer, CustomerStatus, type ICustomerProps } from "../../../domain";
import type { CustomerCreationAttributes, CustomerModel } from "../../sequelize";
export class CustomerDomainMapper
extends SequelizeDomainMapper<CustomerModel, CustomerCreationAttributes, Customer>
implements ICustomerDomainMapper
{
export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
CustomerModel,
CustomerCreationAttributes,
Customer
> {
public mapToDomain(source: CustomerModel, params?: MapperParamsType): Result<Customer, Error> {
try {
const errors: ValidationErrorDetail[] = [];

View File

@ -1,2 +1,2 @@
export * from "./domain";
export * from "./queries";
export * from "./summary";

View File

@ -1 +0,0 @@
export * from "./customer-summary.mapper";

View File

@ -0,0 +1 @@
export * from "./sequelize-customer-summary.mapper";

View File

@ -26,7 +26,7 @@ import type { CustomerSummary, ICustomerSummaryMapper } from "../../../applicati
import { CustomerStatus } from "../../../domain";
import type { CustomerModel } from "../../sequelize";
export class CustomerSummaryMapper
export class SequelizeCustomerSummaryMapper
extends SequelizeQueryMapper<CustomerModel, CustomerSummary>
implements ICustomerSummaryMapper
{

View File

@ -7,11 +7,11 @@ import {
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import { type Collection, Result } from "@repo/rdx-utils";
import type { Sequelize, Transaction } from "sequelize";
import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize";
import type { CustomerSummary, ICustomerRepository } from "../../../application";
import type { Customer } from "../../../domain";
import type { ICustomerDomainMapper, ICustomerListMapper } from "../../mappers";
import type { SequelizeCustomerDomainMapper, SequelizeCustomerSummaryMapper } from "../../mappers";
import { CustomerModel } from "../models/customer.model";
export class CustomerRepository
@ -19,8 +19,8 @@ export class CustomerRepository
implements ICustomerRepository
{
constructor(
private readonly domainMapper: ICustomerDomainMapper,
private readonly listMapper: ICustomerListMapper,
private readonly domainMapper: SequelizeCustomerDomainMapper,
private readonly summaryMapper: SequelizeCustomerSummaryMapper,
database: Sequelize
) {
super({ database });
@ -120,23 +120,42 @@ export class CustomerRepository
async getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: Transaction
transaction?: Transaction,
options: FindOptions<InferAttributes<CustomerModel>> = {}
): Promise<Result<Customer, Error>> {
try {
const mapper: ICustomerDomainMapper = this._registry.getDomainMapper({
resource: "customer",
});
// Normalización defensiva de order/include
const normalizedOrder = Array.isArray(options.order)
? options.order
: options.order
? [options.order]
: [];
const row = await CustomerModel.findOne({
where: { id: id.toString(), company_id: companyId.toString() },
const normalizedInclude = Array.isArray(options.include)
? options.include
: options.include
? [options.include]
: [];
const mergedOptions: FindOptions<InferAttributes<CustomerModel>> = {
...options,
where: {
...(options.where ?? {}),
id: id.toString(),
company_id: companyId.toString(),
},
order: normalizedOrder,
include: normalizedInclude,
transaction,
});
};
const row = await CustomerModel.findOne(mergedOptions);
if (!row) {
return Result.fail(new EntityNotFoundError("Customer", "id", id.toString()));
}
const customer = mapper.mapToDomain(row);
const customer = this.domainMapper.mapToDomain(row);
return customer;
} catch (error: unknown) {
return Result.fail(translateSequelizeError(error));
@ -156,7 +175,8 @@ export class CustomerRepository
async findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
transaction?: Transaction,
options: FindOptions<InferAttributes<CustomerModel>> = {}
): Promise<Result<Collection<CustomerSummary>, Error>> {
try {
const criteriaConverter = new CriteriaToSequelizeConverter();
@ -182,18 +202,48 @@ export class CustomerRepository
strictMode: true, // fuerza error si ORDER BY no permitido
});
// Normalización defensiva de order/include
const normalizedOrder = Array.isArray(options.order)
? options.order
: options.order
? [options.order]
: [];
const normalizedInclude = Array.isArray(options.include)
? options.include
: options.include
? [options.include]
: [];
query.where = {
...query.where,
company_id: companyId.toString(),
deleted_at: null,
};
query.order = [...(query.order as OrderItem[]), ...normalizedOrder];
query.include = normalizedInclude;
// Reemplazar findAndCountAll por findAll + count (más control y mejor rendimiento)
/*
const { rows, count } = await CustomerModel.findAndCountAll({
...query,
transaction,
});
});*/
const [rows, count] = await Promise.all([
CustomerModel.findAll({
...query,
transaction,
}),
CustomerModel.count({
where: query.where,
distinct: true, // evita duplicados por LEFT JOIN
transaction,
}),
]);
return this.listMapper.mapToDTOCollection(rows, count);
return this.summaryMapper.mapToReadModelCollection(rows, count);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}