Compare commits

..

No commits in common. "64b48d707bdcbfe2ebfad069d91b9456935a5085" and "220d57cb7b7ffe69f44250ee61fcdfb6fd00cabd" have entirely different histories.

30 changed files with 431 additions and 618 deletions

View File

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

View File

@ -1,7 +1,6 @@
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 { CreateProformaRequestDTO } from "../../../../../common";
import type { ICreateProformaInputMapper } from "../../mappers";
@ -44,7 +43,7 @@ export class CreateProformaUseCase {
const { props, id } = mappedPropsResult.data;
return this.transactionManager.complete(async (transaction: Transaction) => {
return this.transactionManager.complete(async (transaction) => {
try {
const createResult = await this.creator.create({ companyId, id, props, transaction });

View File

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

View File

@ -1,12 +0,0 @@
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,8 +1,6 @@
import type { ICustomerRepository } from "../repositories";
import { CustomerFinder, type ICustomerFinder } from "../services";
export function buildCustomerFinder(params: { repository: ICustomerRepository }): ICustomerFinder {
const { repository } = params;
export function buildCustomerFinder(repository: ICustomerRepository): ICustomerFinder {
return new CustomerFinder(repository);
}

View File

@ -1,19 +0,0 @@
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,12 +1,11 @@
import type { ITransactionManager } from "@erp/core/api";
import type { ICreateCustomerInputMapper } from "../mappers";
import type { ICustomerCreator, ICustomerFinder } from "../services";
import type { ICustomerFinder } from "../services";
import type {
ICustomerFullSnapshotBuilder,
ICustomerSummarySnapshotBuilder,
} from "../snapshot-builders";
import { CreateCustomerUseCase, GetCustomerByIdUseCase, ListCustomersUseCase } from "../use-cases";
import { GetCustomerByIdUseCase, ListCustomersUseCase } from "../use-cases";
export function buildGetCustomerByIdUseCase(deps: {
finder: ICustomerFinder;
@ -28,20 +27,6 @@ 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: {
finder: ICustomerFinder;
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
@ -56,6 +41,20 @@ export function buildCreateCustomerUseCase(deps: {
deps.documentService,
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: {

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,238 +0,0 @@
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

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

View File

@ -1,75 +0,0 @@
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

@ -1,61 +0,0 @@
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,2 +1 @@
export * from "./customer-creator";
export * from "./customer-finder";

View File

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

View File

@ -1,64 +0,0 @@
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

@ -0,0 +1,90 @@
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

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

View File

@ -0,0 +1,231 @@
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-customer.use-case";
export * from "./create";
export * from "./delete-customer.use-case";
export * from "./get-customer-by-id.use-case";
export * from "./list-customers.use-case";
export * from "./update/update-customer.use-case";
export * from "./update";

View File

@ -6,6 +6,9 @@ import type { Transaction } from "sequelize";
import type { UpdateCustomerByIdRequestDTO } from "../../../../common/dto";
import type { CustomerPatchProps } from "../../../domain";
import type { CustomerApplicationService } from "../../customer-application.service";
import type { CustomerFullSnapshotBuilder } from "../../presenters";
import { mapDTOToUpdateCustomerPatchProps } from "./map-dto-to-update-customer-props";
type UpdateCustomerUseCaseInput = {
companyId: UniqueID;
@ -27,10 +30,17 @@ export class UpdateCustomerUseCase {
if (idOrError.isFailure) {
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
const patchPropsResult = this.dtoMapper.map(dto, { companyId });
const patchPropsResult = mapDTOToUpdateCustomerPatchProps(dto);
if (patchPropsResult.isFailure) {
return patchPropsResult;
return Result.fail(patchPropsResult.error);
}
const patchProps: CustomerPatchProps = patchPropsResult.data;

View File

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

View File

@ -4,7 +4,7 @@ export type CustomersServicesDeps = {
services: {
listCustomers: (filters: 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,
//internal.useCases.getCustomerById().execute(id, context),
//generateCustomerReport: (id, options, context) => null,
generateCustomerReport: (id, options, context) => null,
//internal.useCases.reportCustomer().execute(id, options, context),
},
};

View File

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

View File

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

View File

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

View File

@ -22,14 +22,14 @@ import {
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { CustomerSummary } from "../../../application";
import type { CustomerSummary, ICustomerSummaryMapper } from "../../../application";
import { CustomerStatus } from "../../../domain";
import type { CustomerModel } from "../../sequelize";
export class SequelizeCustomerSummaryMapper extends SequelizeQueryMapper<
CustomerModel,
CustomerSummary
> {
export class SequelizeCustomerSummaryMapper
extends SequelizeQueryMapper<CustomerModel, CustomerSummary>
implements ICustomerSummaryMapper
{
public mapToReadModel(
raw: CustomerModel,
params?: MapperParamsType

View File

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

View File

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