This commit is contained in:
David Arranz 2026-03-09 21:23:48 +01:00
parent 93e0e6be65
commit 64b48d707b
29 changed files with 613 additions and 426 deletions

View File

@ -52,9 +52,6 @@
}, },
// other vscode settings // other vscode settings
"[handlebars]": {
"editor.defaultFormatter": "mfeckies.handlebars-formatter"
},
"[sql]": { "[sql]": {
"editor.defaultFormatter": "cweijan.vscode-mysql-client2" "editor.defaultFormatter": "cweijan.vscode-mysql-client2"
}, // <- your root font size here }, // <- your root font size here

View File

@ -1,6 +1,7 @@
import type { ITransactionManager } from "@erp/core/api"; import type { ITransactionManager } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { CreateProformaRequestDTO } from "../../../../../common"; import type { CreateProformaRequestDTO } from "../../../../../common";
import type { ICreateProformaInputMapper } from "../../mappers"; import type { ICreateProformaInputMapper } from "../../mappers";
@ -43,7 +44,7 @@ export class CreateProformaUseCase {
const { props, id } = mappedPropsResult.data; const { props, id } = mappedPropsResult.data;
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction: Transaction) => {
try { try {
const createResult = await this.creator.create({ companyId, id, props, transaction }); const createResult = await this.creator.create({ companyId, id, props, transaction });

View File

@ -1,10 +1,16 @@
// application/customer-application-service.ts // application/customer-application-service.ts
import { Criteria } from "@repo/rdx-criteria/server"; import type { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils"; import { type Collection, Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import type { Transaction } from "sequelize";
import { Customer, CustomerPatchProps, ICustomerProps, ICustomerRepository } from "../domain";
import { CustomerListDTO } from "../infrastructure"; import {
Customer,
type CustomerPatchProps,
type ICustomerCreateProps,
type ICustomerRepository,
} from "../domain";
import type { CustomerListDTO } from "../infrastructure";
export class CustomerApplicationService { export class CustomerApplicationService {
constructor(private readonly repository: ICustomerRepository) {} constructor(private readonly repository: ICustomerRepository) {}
@ -19,7 +25,7 @@ export class CustomerApplicationService {
*/ */
buildCustomerInCompany( buildCustomerInCompany(
companyId: UniqueID, companyId: UniqueID,
props: Omit<ICustomerProps, "companyId">, props: Omit<ICustomerCreateProps, "companyId">,
customerId?: UniqueID customerId?: UniqueID
): Result<Customer, Error> { ): Result<Customer, Error> {
return Customer.create({ ...props, companyId }, customerId); return Customer.create({ ...props, companyId }, customerId);

View File

@ -0,0 +1,12 @@
import type { ICustomerRepository } from "../repositories";
import { CustomerCreator, type ICustomerCreator } from "../services";
export const buildCustomerCreator = (params: {
repository: ICustomerRepository;
}): ICustomerCreator => {
const { repository } = params;
return new CustomerCreator({
repository,
});
};

View File

@ -1,6 +1,8 @@
import type { ICustomerRepository } from "../repositories"; import type { ICustomerRepository } from "../repositories";
import { CustomerFinder, type ICustomerFinder } from "../services"; import { CustomerFinder, type ICustomerFinder } from "../services";
export function buildCustomerFinder(repository: ICustomerRepository): ICustomerFinder { export function buildCustomerFinder(params: { repository: ICustomerRepository }): ICustomerFinder {
const { repository } = params;
return new CustomerFinder(repository); return new CustomerFinder(repository);
} }

View File

@ -0,0 +1,19 @@
import type { ICatalogs } from "@erp/core/api";
import { CreateCustomerInputMapper, type ICreateCustomerInputMapper } from "../mappers";
export interface ICustomerInputMappers {
createInputMapper: ICreateCustomerInputMapper;
}
export const buildCustomerInputMappers = (catalogs: ICatalogs): ICustomerInputMappers => {
const { taxCatalog } = catalogs;
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
const createInputMapper = new CreateCustomerInputMapper({ taxCatalog });
//const updateCustomerInputMapper = new UpdateCustomerInputMapper();
return {
createInputMapper,
};
};

View File

@ -1,11 +1,12 @@
import type { ITransactionManager } from "@erp/core/api"; import type { ITransactionManager } from "@erp/core/api";
import type { ICustomerFinder } from "../services"; import type { ICreateCustomerInputMapper } from "../mappers";
import type { ICustomerCreator, ICustomerFinder } from "../services";
import type { import type {
ICustomerFullSnapshotBuilder, ICustomerFullSnapshotBuilder,
ICustomerSummarySnapshotBuilder, ICustomerSummarySnapshotBuilder,
} from "../snapshot-builders"; } from "../snapshot-builders";
import { GetCustomerByIdUseCase, ListCustomersUseCase } from "../use-cases"; import { CreateCustomerUseCase, GetCustomerByIdUseCase, ListCustomersUseCase } from "../use-cases";
export function buildGetCustomerByIdUseCase(deps: { export function buildGetCustomerByIdUseCase(deps: {
finder: ICustomerFinder; finder: ICustomerFinder;
@ -27,6 +28,20 @@ export function buildListCustomersUseCase(deps: {
); );
} }
export function buildCreateCustomerUseCase(deps: {
creator: ICustomerCreator;
dtoMapper: ICreateCustomerInputMapper;
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new CreateCustomerUseCase({
dtoMapper: deps.dtoMapper,
creator: deps.creator,
fullSnapshotBuilder: deps.fullSnapshotBuilder,
transactionManager: deps.transactionManager,
});
}
/*export function buildReportCustomerUseCase(deps: { /*export function buildReportCustomerUseCase(deps: {
finder: ICustomerFinder; finder: ICustomerFinder;
fullSnapshotBuilder: ICustomerFullSnapshotBuilder; fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
@ -41,20 +56,6 @@ export function buildListCustomersUseCase(deps: {
deps.documentService, deps.documentService,
deps.transactionManager deps.transactionManager
); );
}
export function buildCreateCustomerUseCase(deps: {
creator: ICustomerCreator;
dtoMapper: ICreateCustomerInputMapper;
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new CreateCustomerUseCase({
dtoMapper: deps.dtoMapper,
creator: deps.creator,
fullSnapshotBuilder: deps.fullSnapshotBuilder,
transactionManager: deps.transactionManager,
});
}*/ }*/
/*export function buildUpdateCustomerUseCase(deps: { /*export function buildUpdateCustomerUseCase(deps: {

View File

@ -1,5 +1,5 @@
//export * from "./customer-creator.di"; export * from "./customer-creator.di";
export * from "./customer-finder.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-snapshot-builders.di";
export * from "./customer-use-cases.di"; export * from "./customer-use-cases.di";

View File

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

View File

@ -0,0 +1,238 @@
import type { JsonTaxCatalogProvider } from "@erp/core";
import {
City,
Country,
CurrencyCode,
DomainError,
EmailAddress,
LanguageCode,
Name,
PhoneNumber,
type PostalAddressProps,
PostalCode,
Province,
Street,
TINNumber,
type TaxCode,
TextValue,
URLAddress,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils";
import type { CreateCustomerRequestDTO } from "../../../common";
import { CustomerStatus, type ICustomerCreateProps } from "../../domain";
export interface ICreateCustomerInputMapper {
map(
dto: CreateCustomerRequestDTO,
params: { companyId: UniqueID }
): Result<{ id: UniqueID; props: ICustomerCreateProps }>;
}
export class CreateCustomerInputMapper implements ICreateCustomerInputMapper {
private readonly taxCatalog: JsonTaxCatalogProvider;
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
this.taxCatalog = params.taxCatalog;
}
public map(
dto: CreateCustomerRequestDTO,
params: { companyId: UniqueID }
): Result<{ id: UniqueID; props: ICustomerCreateProps }> {
try {
const errors: ValidationErrorDetail[] = [];
const { companyId } = params;
const customerId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const status = CustomerStatus.createActive();
const isCompany = dto.is_company === "true";
const reference = extractOrPushError(
maybeFromNullableResult(dto.reference, (value) => Name.create(value)),
"reference",
errors
);
const name = extractOrPushError(Name.create(dto.name), "name", errors);
const tradeName = extractOrPushError(
maybeFromNullableResult(dto.trade_name, (value) => Name.create(value)),
"trade_name",
errors
);
const tinNumber = extractOrPushError(
maybeFromNullableResult(dto.tin, (value) => TINNumber.create(value)),
"tin",
errors
);
const street = extractOrPushError(
maybeFromNullableResult(dto.street, (value) => Street.create(value)),
"street",
errors
);
const street2 = extractOrPushError(
maybeFromNullableResult(dto.street2, (value) => Street.create(value)),
"street2",
errors
);
const city = extractOrPushError(
maybeFromNullableResult(dto.city, (value) => City.create(value)),
"city",
errors
);
const province = extractOrPushError(
maybeFromNullableResult(dto.province, (value) => Province.create(value)),
"province",
errors
);
const postalCode = extractOrPushError(
maybeFromNullableResult(dto.postal_code, (value) => PostalCode.create(value)),
"postal_code",
errors
);
const country = extractOrPushError(
maybeFromNullableResult(dto.country, (value) => Country.create(value)),
"country",
errors
);
const primaryEmailAddress = extractOrPushError(
maybeFromNullableResult(dto.email_primary, (value) => EmailAddress.create(value)),
"email_primary",
errors
);
const secondaryEmailAddress = extractOrPushError(
maybeFromNullableResult(dto.email_secondary, (value) => EmailAddress.create(value)),
"email_secondary",
errors
);
const primaryPhoneNumber = extractOrPushError(
maybeFromNullableResult(dto.phone_primary, (value) => PhoneNumber.create(value)),
"phone_primary",
errors
);
const secondaryPhoneNumber = extractOrPushError(
maybeFromNullableResult(dto.phone_secondary, (value) => PhoneNumber.create(value)),
"phone_secondary",
errors
);
const primaryMobileNumber = extractOrPushError(
maybeFromNullableResult(dto.mobile_primary, (value) => PhoneNumber.create(value)),
"mobile_primary",
errors
);
const secondaryMobileNumber = extractOrPushError(
maybeFromNullableResult(dto.mobile_secondary, (value) => PhoneNumber.create(value)),
"mobile_secondary",
errors
);
const faxNumber = extractOrPushError(
maybeFromNullableResult(dto.fax, (value) => PhoneNumber.create(value)),
"fax",
errors
);
const website = extractOrPushError(
maybeFromNullableResult(dto.website, (value) => URLAddress.create(value)),
"website",
errors
);
const legalRecord = extractOrPushError(
maybeFromNullableResult(dto.legal_record, (value) => TextValue.create(value)),
"legal_record",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(dto.language_code),
"language_code",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(dto.currency_code),
"currency_code",
errors
);
const defaultTaxes = new Collection<TaxCode>();
/*if (!isNullishOrEmpty(dto.default_taxes)) {
dto.default_taxes!.map((taxCode, index) => {
const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors);
if (tax) {
defaultTaxes.add(tax!);
}
});
}*/
if (errors.length > 0) {
return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors));
}
const postalAddressProps: PostalAddressProps = {
street: street!,
street2: street2!,
city: city!,
postalCode: postalCode!,
province: province!,
country: country!,
};
const customerProps: ICustomerCreateProps = {
companyId,
status: status!,
reference: reference!,
isCompany: isCompany,
name: name!,
tradeName: tradeName!,
tin: tinNumber!,
address: postalAddressProps!,
emailPrimary: primaryEmailAddress!,
emailSecondary: secondaryEmailAddress!,
phonePrimary: primaryPhoneNumber!,
phoneSecondary: secondaryPhoneNumber!,
mobilePrimary: primaryMobileNumber!,
mobileSecondary: secondaryMobileNumber!,
fax: faxNumber!,
website: website!,
legalRecord: legalRecord!,
defaultTaxes: defaultTaxes!,
languageCode: languageCode!,
currencyCode: currencyCode!,
};
return Result.ok({ id: customerId!, props: customerProps });
} catch (err: unknown) {
return Result.fail(new DomainError("Customer props mapping failed", { cause: err }));
}
}
}

View File

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

View File

@ -0,0 +1,75 @@
import { DuplicateEntityError } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import { Customer, type ICustomerCreateProps } from "../../domain";
import type { ICustomerRepository } from "../repositories";
import { CustomerNotExistsInCompanySpecification } from "../specs";
export interface ICustomerCreator {
create(params: {
companyId: UniqueID;
id: UniqueID;
props: ICustomerCreateProps;
transaction: Transaction;
}): Promise<Result<Customer, Error>>;
}
type CustomerCreatorDeps = {
repository: ICustomerRepository;
};
export class CustomerCreator implements ICustomerCreator {
private readonly repository: ICustomerRepository;
constructor(deps: CustomerCreatorDeps) {
this.repository = deps.repository;
}
async create(params: {
companyId: UniqueID;
id: UniqueID;
props: ICustomerCreateProps;
transaction: Transaction;
}): Promise<Result<Customer, Error>> {
const { companyId, id, props, transaction } = params;
// 1. Verificar unicidad
const spec = new CustomerNotExistsInCompanySpecification(
this.repository,
companyId,
transaction
);
const isNew = await spec.isSatisfiedBy(id);
if (!isNew) {
return Result.fail(new DuplicateEntityError("Customer", "id", String(id)));
}
// 2. Crear agregado
const createResult = Customer.create(
{
...props,
companyId,
},
id
);
if (createResult.isFailure) {
return createResult;
}
const newCustomer = createResult.data;
// 3. Persistir agregado
const saveResult = await this.repository.create(newCustomer, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
return Result.ok(newCustomer);
}
}

View File

@ -0,0 +1,61 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { Customer, ICustomerCreateProps } from "../../domain";
import type { ICustomerRepository } from "../repositories";
export interface ICustomerUpdater {
update(params: {
companyId: UniqueID;
id: UniqueID;
props: Partial<ICustomerCreateProps>;
transaction: Transaction;
}): Promise<Result<Customer, Error>>;
}
type CustomerUpdaterDeps = {
repository: ICustomerRepository;
};
export class CustomerUpdater implements ICustomerUpdater {
private readonly repository: ICustomerRepository;
constructor(deps: CustomerUpdaterDeps) {
this.repository = deps.repository;
}
async update(params: {
companyId: UniqueID;
id: UniqueID;
props: Partial<ICustomerCreateProps>;
transaction: Transaction;
}): Promise<Result<Customer, Error>> {
const { companyId, id, props, transaction } = params;
// Recuperar agregado existente
const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction);
if (existingResult.isFailure) {
return Result.fail(existingResult.error);
}
const customer = existingResult.data;
// Aplicar cambios en el agregado
const updateResult = customer.update(props);
if (updateResult.isFailure) {
return Result.fail(updateResult.error);
}
// Persistir cambios
const saveResult = await this.repository.update(customer, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
return Result.ok(customer);
}
}

View File

@ -1 +1,2 @@
export * from "./customer-creator";
export * from "./customer-finder"; export * from "./customer-finder";

View File

@ -1,11 +1,12 @@
import { CompositeSpecification, UniqueID } from "@repo/rdx-ddd"; import { CompositeSpecification, type UniqueID } from "@repo/rdx-ddd";
import { Transaction } from "sequelize"; import type { Transaction } from "sequelize";
import { CustomerApplicationService } from "../../application";
import type { ICustomerRepository } from "../../application";
import { logger } from "../../helpers"; import { logger } from "../../helpers";
export class CustomerNotExistsInCompanySpecification extends CompositeSpecification<UniqueID> { export class CustomerNotExistsInCompanySpecification extends CompositeSpecification<UniqueID> {
constructor( constructor(
private readonly service: CustomerApplicationService, private readonly repository: ICustomerRepository,
private readonly companyId: UniqueID, private readonly companyId: UniqueID,
private readonly transaction?: Transaction private readonly transaction?: Transaction
) { ) {
@ -13,7 +14,7 @@ export class CustomerNotExistsInCompanySpecification extends CompositeSpecificat
} }
public async isSatisfiedBy(customerId: UniqueID): Promise<boolean> { public async isSatisfiedBy(customerId: UniqueID): Promise<boolean> {
const existsCheck = await this.service.existsByIdInCompany( const existsCheck = await this.repository.existsByIdInCompany(
this.companyId, this.companyId,
customerId, customerId,
this.transaction this.transaction

View File

@ -0,0 +1,64 @@
import type { ITransactionManager } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { CreateCustomerRequestDTO } from "../../../common";
import type { ICreateCustomerInputMapper } from "../mappers";
import type { ICustomerCreator } from "../services";
import type { ICustomerFullSnapshotBuilder } from "../snapshot-builders";
type CreateCustomerUseCaseInput = {
companyId: UniqueID;
dto: CreateCustomerRequestDTO;
};
type CreateCustomerUseCaseDeps = {
dtoMapper: ICreateCustomerInputMapper;
creator: ICustomerCreator;
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
transactionManager: ITransactionManager;
};
export class CreateCustomerUseCase {
private readonly dtoMapper: ICreateCustomerInputMapper;
private readonly creator: ICustomerCreator;
private readonly fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
private readonly transactionManager: ITransactionManager;
constructor(deps: CreateCustomerUseCaseDeps) {
this.dtoMapper = deps.dtoMapper;
this.creator = deps.creator;
this.fullSnapshotBuilder = deps.fullSnapshotBuilder;
this.transactionManager = deps.transactionManager;
}
public execute(params: CreateCustomerUseCaseInput) {
const { dto, companyId } = params;
const mappedPropsResult = this.dtoMapper.map(dto, { companyId });
if (mappedPropsResult.isFailure) {
return mappedPropsResult;
}
const { props, id } = mappedPropsResult.data;
return this.transactionManager.complete(async (transaction: Transaction) => {
try {
const createResult = await this.creator.create({ companyId, id, props, transaction });
if (createResult.isFailure) {
return createResult;
}
const newCustomer = createResult.data;
const snapshot = this.fullSnapshotBuilder.toOutput(newCustomer);
return Result.ok(snapshot);
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -1,90 +0,0 @@
import {
DuplicateEntityError,
type IPresenterRegistry,
type ITransactionManager,
} from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { CreateCustomerRequestDTO } from "../../../../common";
import { logger } from "../../..//helpers";
import type { CustomerApplicationService } from "../../customer-application.service";
import type { CustomerFullSnapshotBuilder } from "../../presenters";
import { CustomerNotExistsInCompanySpecification } from "../../specs";
import { mapDTOToCreateCustomerProps } from "./map-dto-to-create-customer-props";
type CreateCustomerUseCaseInput = {
companyId: UniqueID;
dto: CreateCustomerRequestDTO;
};
export class CreateCustomerUseCase {
constructor(
private readonly service: CustomerApplicationService,
private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry
) {}
public execute(params: CreateCustomerUseCaseInput) {
const { dto, companyId } = params;
const presenter = this.presenterRegistry.getPresenter({
resource: "customer",
projection: "FULL",
}) as CustomerFullSnapshotBuilder;
// 1) Mapear DTO → props de dominio
const dtoResult = mapDTOToCreateCustomerProps(dto);
if (dtoResult.isFailure) {
return Result.fail(dtoResult.error);
}
const { props, id } = dtoResult.data;
// 2) Construir entidad de dominio
const buildResult = this.service.buildCustomerInCompany(companyId, props, id);
if (buildResult.isFailure) {
return Result.fail(buildResult.error);
}
const newCustomer = buildResult.data;
// 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista
return this.transactionManager.complete(async (transaction: Transaction) => {
try {
// Verificar que no exista ya un cliente con el mismo id en la companyId
const spec = new CustomerNotExistsInCompanySpecification(
this.service,
companyId,
transaction
);
const isNew = await spec.isSatisfiedBy(newCustomer.id);
logger.debug(`isNew => ${isNew}`, { label: "CreateCustomerUseCase.execute" });
if (!isNew) {
return Result.fail(new DuplicateEntityError("Customer", "id", String(newCustomer.id)));
}
logger.debug(JSON.stringify(newCustomer, null, 6));
const saveResult = await this.service.createCustomerInCompany(
companyId,
newCustomer,
transaction
);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
const customer = saveResult.data;
const dto = presenter.toOutput(customer);
return Result.ok(dto);
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -1 +0,0 @@
export * from "./create-customer.use-case";

View File

@ -1,231 +0,0 @@
import {
City,
Country,
CurrencyCode,
DomainError,
EmailAddress,
LanguageCode,
Name,
PhoneNumber,
PostalAddress,
PostalCode,
Province,
Street,
TINNumber,
TaxCode,
TextValue,
URLAddress,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { Collection, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import type { CreateCustomerRequestDTO } from "../../../../common";
import { type ICustomerProps, CustomerStatus } from "../../../domain";
/**
* Convierte el DTO a las props validadas (CustomerProps).
* No construye directamente el agregado.
*
* @param dto - DTO con los datos de la factura de cliente
* @returns
*
*/
export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) {
try {
const errors: ValidationErrorDetail[] = [];
const customerId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const status = CustomerStatus.createActive();
const isCompany = dto.is_company === "true";
const reference = extractOrPushError(
maybeFromNullableResult(dto.reference, (value) => Name.create(value)),
"reference",
errors
);
const name = extractOrPushError(Name.create(dto.name), "name", errors);
const tradeName = extractOrPushError(
maybeFromNullableResult(dto.trade_name, (value) => Name.create(value)),
"trade_name",
errors
);
const tinNumber = extractOrPushError(
maybeFromNullableResult(dto.tin, (value) => TINNumber.create(value)),
"tin",
errors
);
const street = extractOrPushError(
maybeFromNullableResult(dto.street, (value) => Street.create(value)),
"street",
errors
);
const street2 = extractOrPushError(
maybeFromNullableResult(dto.street2, (value) => Street.create(value)),
"street2",
errors
);
const city = extractOrPushError(
maybeFromNullableResult(dto.city, (value) => City.create(value)),
"city",
errors
);
const province = extractOrPushError(
maybeFromNullableResult(dto.province, (value) => Province.create(value)),
"province",
errors
);
const postalCode = extractOrPushError(
maybeFromNullableResult(dto.postal_code, (value) => PostalCode.create(value)),
"postal_code",
errors
);
const country = extractOrPushError(
maybeFromNullableResult(dto.country, (value) => Country.create(value)),
"country",
errors
);
const primaryEmailAddress = extractOrPushError(
maybeFromNullableResult(dto.email_primary, (value) => EmailAddress.create(value)),
"email_primary",
errors
);
const secondaryEmailAddress = extractOrPushError(
maybeFromNullableResult(dto.email_secondary, (value) => EmailAddress.create(value)),
"email_secondary",
errors
);
const primaryPhoneNumber = extractOrPushError(
maybeFromNullableResult(dto.phone_primary, (value) => PhoneNumber.create(value)),
"phone_primary",
errors
);
const secondaryPhoneNumber = extractOrPushError(
maybeFromNullableResult(dto.phone_secondary, (value) => PhoneNumber.create(value)),
"phone_secondary",
errors
);
const primaryMobileNumber = extractOrPushError(
maybeFromNullableResult(dto.mobile_primary, (value) => PhoneNumber.create(value)),
"mobile_primary",
errors
);
const secondaryMobileNumber = extractOrPushError(
maybeFromNullableResult(dto.mobile_secondary, (value) => PhoneNumber.create(value)),
"mobile_secondary",
errors
);
const faxNumber = extractOrPushError(
maybeFromNullableResult(dto.fax, (value) => PhoneNumber.create(value)),
"fax",
errors
);
const website = extractOrPushError(
maybeFromNullableResult(dto.website, (value) => URLAddress.create(value)),
"website",
errors
);
const legalRecord = extractOrPushError(
maybeFromNullableResult(dto.legal_record, (value) => TextValue.create(value)),
"legal_record",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(dto.language_code),
"language_code",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(dto.currency_code),
"currency_code",
errors
);
const defaultTaxes = new Collection<TaxCode>();
if (!isNullishOrEmpty(dto.default_taxes)) {
dto.default_taxes!.map((taxCode, index) => {
const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors);
if (tax) {
defaultTaxes.add(tax!);
}
});
}
if (errors.length > 0) {
return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors));
}
const postalAddressProps = {
street: street!,
street2: street2!,
city: city!,
postalCode: postalCode!,
province: province!,
country: country!,
};
const postalAddress = extractOrPushError(
PostalAddress.create(postalAddressProps),
"address",
errors
);
const customerProps: Omit<ICustomerProps, "companyId"> = {
status: status!,
reference: reference!,
isCompany: isCompany,
name: name!,
tradeName: tradeName!,
tin: tinNumber!,
address: postalAddress!,
emailPrimary: primaryEmailAddress!,
emailSecondary: secondaryEmailAddress!,
phonePrimary: primaryPhoneNumber!,
phoneSecondary: secondaryPhoneNumber!,
mobilePrimary: primaryMobileNumber!,
mobileSecondary: secondaryMobileNumber!,
fax: faxNumber!,
website: website!,
legalRecord: legalRecord!,
defaultTaxes: defaultTaxes!,
languageCode: languageCode!,
currencyCode: currencyCode!,
};
return Result.ok({ id: customerId!, props: customerProps });
} catch (err: unknown) {
return Result.fail(new DomainError("Customer props mapping failed", { cause: err }));
}
}

View File

@ -1,5 +1,5 @@
export * from "./create"; export * from "./create-customer.use-case";
export * from "./delete-customer.use-case"; export * from "./delete-customer.use-case";
export * from "./get-customer-by-id.use-case"; export * from "./get-customer-by-id.use-case";
export * from "./list-customers.use-case"; export * from "./list-customers.use-case";
export * from "./update"; export * from "./update/update-customer.use-case";

View File

@ -6,9 +6,6 @@ import type { Transaction } from "sequelize";
import type { UpdateCustomerByIdRequestDTO } from "../../../../common/dto"; import type { UpdateCustomerByIdRequestDTO } from "../../../../common/dto";
import type { CustomerPatchProps } from "../../../domain"; import type { CustomerPatchProps } from "../../../domain";
import type { CustomerApplicationService } from "../../customer-application.service"; import type { CustomerApplicationService } from "../../customer-application.service";
import type { CustomerFullSnapshotBuilder } from "../../presenters";
import { mapDTOToUpdateCustomerPatchProps } from "./map-dto-to-update-customer-props";
type UpdateCustomerUseCaseInput = { type UpdateCustomerUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
@ -30,17 +27,10 @@ export class UpdateCustomerUseCase {
if (idOrError.isFailure) { if (idOrError.isFailure) {
return Result.fail(idOrError.error); return Result.fail(idOrError.error);
} }
const customerId = idOrError.data;
const presenter = this.presenterRegistry.getPresenter({
resource: "customer",
projection: "FULL",
}) as CustomerFullSnapshotBuilder;
// Mapear DTO → props de dominio // Mapear DTO → props de dominio
const patchPropsResult = mapDTOToUpdateCustomerPatchProps(dto); const patchPropsResult = this.dtoMapper.map(dto, { companyId });
if (patchPropsResult.isFailure) { if (patchPropsResult.isFailure) {
return Result.fail(patchPropsResult.error); return patchPropsResult;
} }
const patchProps: CustomerPatchProps = patchPropsResult.data; const patchProps: CustomerPatchProps = patchPropsResult.data;

View File

@ -5,8 +5,9 @@ import {
type LanguageCode, type LanguageCode,
type Name, type Name,
type PhoneNumber, type PhoneNumber,
type PostalAddress, PostalAddress,
type PostalAddressPatchProps, type PostalAddressPatchProps,
type PostalAddressProps,
type TINNumber, type TINNumber,
type TaxCode, type TaxCode,
type TextValue, type TextValue,
@ -17,7 +18,7 @@ import { type Collection, type Maybe, Result } from "@repo/rdx-utils";
import type { CustomerStatus } from "../value-objects"; import type { CustomerStatus } from "../value-objects";
export interface ICustomerProps { export interface ICustomerCreateProps {
companyId: UniqueID; companyId: UniqueID;
status: CustomerStatus; status: CustomerStatus;
reference: Maybe<Name>; reference: Maybe<Name>;
@ -27,7 +28,7 @@ export interface ICustomerProps {
tradeName: Maybe<Name>; tradeName: Maybe<Name>;
tin: Maybe<TINNumber>; tin: Maybe<TINNumber>;
address: PostalAddress; address: PostalAddressProps;
emailPrimary: Maybe<EmailAddress>; emailPrimary: Maybe<EmailAddress>;
emailSecondary: Maybe<EmailAddress>; emailSecondary: Maybe<EmailAddress>;
@ -43,13 +44,15 @@ export interface ICustomerProps {
legalRecord: Maybe<TextValue>; legalRecord: Maybe<TextValue>;
defaultTaxes: Collection<TaxCode>; defaultTaxes: TaxCode[];
languageCode: LanguageCode; languageCode: LanguageCode;
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
} }
export type CustomerPatchProps = Partial<Omit<ICustomerProps, "companyId" | "address">> & { export type CustomerPatchProps = Partial<
Omit<ICustomerCreateProps, "companyId" | "address" | "isCompany" | "status">
> & {
address?: PostalAddressPatchProps; address?: PostalAddressPatchProps;
}; };
@ -90,13 +93,27 @@ export interface ICustomer {
readonly currencyCode: CurrencyCode; readonly currencyCode: CurrencyCode;
} }
type CreateCustomerProps = ICustomerProps; type CustomerInternalProps = Omit<ICustomerCreateProps, "address"> & {
type InternalCustomerProps = ICustomerProps; readonly address: PostalAddress;
};
export class Customer extends AggregateRoot<InternalCustomerProps> implements ICustomer { export class Customer extends AggregateRoot<CustomerInternalProps> implements ICustomer {
static create(props: ICustomerCreateProps, id?: UniqueID): Result<Customer, Error> {
const { address, ...internalProps } = props;
static create(props: CreateCustomerProps, id?: UniqueID): Result<Customer, Error> { const postalAddressResult = PostalAddress.create(address);
const contact = new Customer(props, id);
if (postalAddressResult.isFailure) {
return Result.fail(postalAddressResult.error);
}
const contact = new Customer(
{
...internalProps,
address: postalAddressResult.data,
},
id
);
// Reglas de negocio / validaciones // Reglas de negocio / validaciones
// ... // ...
@ -110,28 +127,26 @@ export class Customer extends AggregateRoot<InternalCustomerProps> implements IC
} }
// Rehidratación desde persistencia // Rehidratación desde persistencia
static rehydrate(props: InternalCustomerProps, id: UniqueID): Customer { static rehydrate(props: CustomerInternalProps, id: UniqueID): Customer {
return new Customer(props, id); return new Customer(props, id);
} }
public update(partialCustomer: CustomerPatchProps): Result<Customer, Error> { public update(partialCustomer: CustomerPatchProps): Result<Customer, Error> {
const { address: partialAddress, ...rest } = partialCustomer; const { address: partialAddress, ...rest } = partialCustomer;
const updatedProps = {
...this.props, Object.assign(this.props, rest);
...rest,
} as ICustomerProps;
if (partialAddress) { if (partialAddress) {
const updatedAddressOrError = this.address.update(partialAddress); const addressResult = this.address.update(partialAddress);
if (updatedAddressOrError.isFailure) {
return Result.fail(updatedAddressOrError.error); if (addressResult.isFailure) {
return Result.fail(addressResult.error);
} }
updatedProps.address = updatedAddressOrError.data; this.props.address = addressResult.data;
} }
return Customer.create(updatedProps, this.id); return Result.ok();
} }
// Getters // Getters

View File

@ -4,7 +4,7 @@ export type CustomersServicesDeps = {
services: { services: {
listCustomers: (filters: unknown, context: unknown) => null; listCustomers: (filters: unknown, context: unknown) => null;
getCustomerById: (id: unknown, context: unknown) => null; getCustomerById: (id: unknown, context: unknown) => null;
generateCustomerReport: (id: unknown, options: unknown, context: unknown) => null; //generateCustomerReport: (id: unknown, options: unknown, context: unknown) => null;
}; };
}; };
@ -17,7 +17,7 @@ export function buildCustomerServices(deps: CustomersInternalDeps): CustomersSer
getCustomerById: (id, context) => null, getCustomerById: (id, context) => null,
//internal.useCases.getCustomerById().execute(id, context), //internal.useCases.getCustomerById().execute(id, context),
generateCustomerReport: (id, options, context) => null, //generateCustomerReport: (id, options, context) => null,
//internal.useCases.reportCustomer().execute(id, options, context), //internal.useCases.reportCustomer().execute(id, options, context),
}, },
}; };

View File

@ -1,9 +1,14 @@
import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api"; import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api";
import { import {
type CreateCustomerUseCase,
type GetCustomerByIdUseCase, type GetCustomerByIdUseCase,
type ListCustomersUseCase, type ListCustomersUseCase,
type UpdateCustomerUseCase,
buildCreateCustomerUseCase,
buildCustomerCreator,
buildCustomerFinder, buildCustomerFinder,
buildCustomerInputMappers,
buildCustomerSnapshotBuilders, buildCustomerSnapshotBuilders,
buildGetCustomerByIdUseCase, buildGetCustomerByIdUseCase,
buildListCustomersUseCase, buildListCustomersUseCase,
@ -16,11 +21,13 @@ export type CustomersInternalDeps = {
useCases: { useCases: {
listCustomers: () => ListCustomersUseCase; listCustomers: () => ListCustomersUseCase;
getCustomerById: () => GetCustomerByIdUseCase; getCustomerById: () => GetCustomerByIdUseCase;
createCustomer: () => CreateCustomerUseCase;
updateCustomer: () => UpdateCustomerUseCase;
//reportCustomer: () => ReportCustomerUseCase; //reportCustomer: () => ReportCustomerUseCase;
//createCustomer: () => CreateCustomerUseCase;
/* /*
updateCustomer: () => UpdateCustomerUseCase;
deleteCustomer: () => DeleteCustomerUseCase; deleteCustomer: () => DeleteCustomerUseCase;
issueCustomer: () => IssueCustomerUseCase; issueCustomer: () => IssueCustomerUseCase;
changeStatusCustomer: () => ChangeStatusCustomerUseCase;*/ changeStatusCustomer: () => ChangeStatusCustomerUseCase;*/
@ -39,9 +46,9 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
//const numberService = buildCustomerNumberGenerator(); //const numberService = buildCustomerNumberGenerator();
// Application helpers // Application helpers
//const inputMappers = buildCustomerInputMappers(catalogs); const inputMappers = buildCustomerInputMappers(catalogs);
const finder = buildCustomerFinder(repository); const finder = buildCustomerFinder({ repository });
//const creator = buildCustomerCreator({ numberService, repository }); const creator = buildCustomerCreator({ repository });
const snapshotBuilders = buildCustomerSnapshotBuilders(); const snapshotBuilders = buildCustomerSnapshotBuilders();
//const documentGeneratorPipeline = buildCustomerDocumentService(params); //const documentGeneratorPipeline = buildCustomerDocumentService(params);
@ -63,6 +70,22 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
transactionManager, transactionManager,
}), }),
createCustomer: () =>
buildCreateCustomerUseCase({
creator,
dtoMapper: inputMappers.createInputMapper,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
updateCustomer: () =>
buildUpdateCustomerUseCase({
creator,
dtoMapper: inputMappers.updateInputMapper,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
/*reportCustomer: () => /*reportCustomer: () =>
buildReportCustomerUseCase({ buildReportCustomerUseCase({
finder, finder,
@ -72,13 +95,7 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
transactionManager, transactionManager,
}), }),
createCustomer: () => */
buildCreateCustomerUseCase({
creator,
dtoMapper: inputMappers.createInputMapper,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),*/
}, },
}; };
} }

View File

@ -1,20 +1,18 @@
import { import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
mockUser, import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api";
requireAuthenticated,
requireCompanyContext,
} from "@erp/auth/api";
import { type ModuleParams, RequestWithAuth, validateRequest } from "@erp/core/api";
import { type NextFunction, type Request, type Response, Router } from "express"; import { type NextFunction, type Request, type Response, Router } from "express";
import { import {
CreateCustomerRequestSchema,
CustomerListRequestSchema, CustomerListRequestSchema,
GetCustomerByIdRequestSchema GetCustomerByIdRequestSchema,
} from "../../../common/dto"; } from "../../../common/dto";
import type { CustomersInternalDeps } from "../di"; import type { CustomersInternalDeps } from "../di";
import { import {
CreateCustomerController,
GetCustomerController, GetCustomerController,
ListCustomersController ListCustomersController,
} from "./controllers"; } from "./controllers";
export const customersRouter = (params: ModuleParams, deps: CustomersInternalDeps) => { export const customersRouter = (params: ModuleParams, deps: CustomersInternalDeps) => {
@ -53,7 +51,7 @@ export const customersRouter = (params: ModuleParams, deps: CustomersInternalDep
} }
); );
router.get( router.get(
"/:customer_id", "/:customer_id",
//checkTabContext, //checkTabContext,
@ -65,17 +63,17 @@ export const customersRouter = (params: ModuleParams, deps: CustomersInternalDep
} }
); );
/* router.post( router.post(
"/", "/",
//checkTabContext, //checkTabContext,
validateRequest(CreateCustomerRequestSchema, "body"), validateRequest(CreateCustomerRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.create(); const useCase = deps.useCases.createCustomer();
const controller = new CreateCustomerController(useCase); const controller = new CreateCustomerController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); */ );
/* router.put( /* router.put(
"/:customer_id", "/:customer_id",

View File

@ -24,7 +24,7 @@ import {
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import { Customer, CustomerStatus, type ICustomerProps } from "../../../domain"; import { Customer, CustomerStatus, type ICustomerCreateProps } from "../../../domain";
import type { CustomerCreationAttributes, CustomerModel } from "../../sequelize"; import type { CustomerCreationAttributes, CustomerModel } from "../../sequelize";
export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper< export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
@ -199,7 +199,7 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors)); return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors));
} }
const customerProps: ICustomerProps = { const customerProps: ICustomerCreateProps = {
companyId: companyId!, companyId: companyId!,
status: status!, status: status!,
reference: reference!, reference: reference!,

View File

@ -1,5 +1,5 @@
import { DomainEntity } from "./domain-entity"; import { DomainEntity } from "./domain-entity";
import { IDomainEvent } from "./events"; import type { IDomainEvent } from "./events";
export abstract class AggregateRoot<T extends object> extends DomainEntity<T> { export abstract class AggregateRoot<T extends object> extends DomainEntity<T> {
private _domainEvents: IDomainEvent[] = []; private _domainEvents: IDomainEvent[] = [];

View File

@ -1,12 +1,20 @@
import { UniqueID } from "./value-objects"; import { UniqueID } from "./value-objects";
export abstract class DomainEntity<T extends object> { export abstract class DomainEntity<T extends object> {
protected readonly props: T; private _props: T;
public readonly id: UniqueID; public readonly id: UniqueID;
protected constructor(props: T, id?: UniqueID) { protected constructor(props: T, id?: UniqueID) {
this.id = id ? id : UniqueID.generateNewID(); this.id = id ?? UniqueID.generateNewID();
this.props = props; this._props = props;
}
protected get props(): T {
return this._props;
}
protected set props(value: T) {
this._props = value;
} }
protected _flattenProps(props: T): { [s: string]: any } { protected _flattenProps(props: T): { [s: string]: any } {
@ -18,7 +26,8 @@ export abstract class DomainEntity<T extends object> {
}, {}); }, {});
} }
equals(other: DomainEntity<T>): boolean { equals(other?: DomainEntity<T>): boolean {
if (!other) return false;
return other instanceof DomainEntity && this.id.equals(other.id); return other instanceof DomainEntity && this.id.equals(other.id);
} }

View File

@ -34,7 +34,7 @@ export class PostalAddress extends ValueObject<PostalAddressProps> {
return Result.ok(new PostalAddress(values)); return Result.ok(new PostalAddress(values));
} }
public update(partial: Partial<PostalAddressPatchProps>): Result<PostalAddress, Error> { public update(partial: PostalAddressPatchProps): Result<PostalAddress, Error> {
const updatedProps = { const updatedProps = {
...this.props, ...this.props,
...partial, ...partial,