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 // 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,7 +1,6 @@
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";
@ -44,7 +43,7 @@ export class CreateProformaUseCase {
const { props, id } = mappedPropsResult.data; const { props, id } = mappedPropsResult.data;
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (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,16 +1,10 @@
// application/customer-application-service.ts // application/customer-application-service.ts
import type { Criteria } from "@repo/rdx-criteria/server"; import { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { type Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { Customer, CustomerPatchProps, ICustomerProps, ICustomerRepository } from "../domain";
import { import { CustomerListDTO } from "../infrastructure";
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) {}
@ -25,7 +19,7 @@ export class CustomerApplicationService {
*/ */
buildCustomerInCompany( buildCustomerInCompany(
companyId: UniqueID, companyId: UniqueID,
props: Omit<ICustomerCreateProps, "companyId">, props: Omit<ICustomerProps, "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

@ -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 type { ICustomerRepository } from "../repositories";
import { CustomerFinder, type ICustomerFinder } from "../services"; import { CustomerFinder, type ICustomerFinder } from "../services";
export function buildCustomerFinder(params: { repository: ICustomerRepository }): ICustomerFinder { export function buildCustomerFinder(repository: ICustomerRepository): ICustomerFinder {
const { repository } = params;
return new CustomerFinder(repository); 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 { ITransactionManager } from "@erp/core/api";
import type { ICreateCustomerInputMapper } from "../mappers"; import type { ICustomerFinder } from "../services";
import type { ICustomerCreator, ICustomerFinder } from "../services";
import type { import type {
ICustomerFullSnapshotBuilder, ICustomerFullSnapshotBuilder,
ICustomerSummarySnapshotBuilder, ICustomerSummarySnapshotBuilder,
} from "../snapshot-builders"; } from "../snapshot-builders";
import { CreateCustomerUseCase, GetCustomerByIdUseCase, ListCustomersUseCase } from "../use-cases"; import { GetCustomerByIdUseCase, ListCustomersUseCase } from "../use-cases";
export function buildGetCustomerByIdUseCase(deps: { export function buildGetCustomerByIdUseCase(deps: {
finder: ICustomerFinder; 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: { /*export function buildReportCustomerUseCase(deps: {
finder: ICustomerFinder; finder: ICustomerFinder;
fullSnapshotBuilder: ICustomerFullSnapshotBuilder; fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
@ -56,6 +41,20 @@ export function buildCreateCustomerUseCase(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,5 +1,4 @@
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

@ -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"; export * from "./customer-finder";

View File

@ -1,12 +1,11 @@
import { CompositeSpecification, type UniqueID } from "@repo/rdx-ddd"; import { CompositeSpecification, UniqueID } from "@repo/rdx-ddd";
import type { Transaction } from "sequelize"; import { 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 repository: ICustomerRepository, private readonly service: CustomerApplicationService,
private readonly companyId: UniqueID, private readonly companyId: UniqueID,
private readonly transaction?: Transaction private readonly transaction?: Transaction
) { ) {
@ -14,7 +13,7 @@ export class CustomerNotExistsInCompanySpecification extends CompositeSpecificat
} }
public async isSatisfiedBy(customerId: UniqueID): Promise<boolean> { public async isSatisfiedBy(customerId: UniqueID): Promise<boolean> {
const existsCheck = await this.repository.existsByIdInCompany( const existsCheck = await this.service.existsByIdInCompany(
this.companyId, this.companyId,
customerId, customerId,
this.transaction 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 "./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/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 { 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;
@ -27,10 +30,17 @@ 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 = this.dtoMapper.map(dto, { companyId }); const patchPropsResult = mapDTOToUpdateCustomerPatchProps(dto);
if (patchPropsResult.isFailure) { if (patchPropsResult.isFailure) {
return patchPropsResult; return Result.fail(patchPropsResult.error);
} }
const patchProps: CustomerPatchProps = patchPropsResult.data; const patchProps: CustomerPatchProps = patchPropsResult.data;

View File

@ -5,9 +5,8 @@ import {
type LanguageCode, type LanguageCode,
type Name, type Name,
type PhoneNumber, type PhoneNumber,
PostalAddress, type PostalAddress,
type PostalAddressPatchProps, type PostalAddressPatchProps,
type PostalAddressProps,
type TINNumber, type TINNumber,
type TaxCode, type TaxCode,
type TextValue, type TextValue,
@ -18,7 +17,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 ICustomerCreateProps { export interface ICustomerProps {
companyId: UniqueID; companyId: UniqueID;
status: CustomerStatus; status: CustomerStatus;
reference: Maybe<Name>; reference: Maybe<Name>;
@ -28,7 +27,7 @@ export interface ICustomerCreateProps {
tradeName: Maybe<Name>; tradeName: Maybe<Name>;
tin: Maybe<TINNumber>; tin: Maybe<TINNumber>;
address: PostalAddressProps; address: PostalAddress;
emailPrimary: Maybe<EmailAddress>; emailPrimary: Maybe<EmailAddress>;
emailSecondary: Maybe<EmailAddress>; emailSecondary: Maybe<EmailAddress>;
@ -44,15 +43,13 @@ export interface ICustomerCreateProps {
legalRecord: Maybe<TextValue>; legalRecord: Maybe<TextValue>;
defaultTaxes: TaxCode[]; defaultTaxes: Collection<TaxCode>;
languageCode: LanguageCode; languageCode: LanguageCode;
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
} }
export type CustomerPatchProps = Partial< export type CustomerPatchProps = Partial<Omit<ICustomerProps, "companyId" | "address">> & {
Omit<ICustomerCreateProps, "companyId" | "address" | "isCompany" | "status">
> & {
address?: PostalAddressPatchProps; address?: PostalAddressPatchProps;
}; };
@ -93,27 +90,13 @@ export interface ICustomer {
readonly currencyCode: CurrencyCode; readonly currencyCode: CurrencyCode;
} }
type CustomerInternalProps = Omit<ICustomerCreateProps, "address"> & { type CreateCustomerProps = ICustomerProps;
readonly address: PostalAddress; type InternalCustomerProps = ICustomerProps;
};
export class Customer extends AggregateRoot<CustomerInternalProps> implements ICustomer { export class Customer extends AggregateRoot<InternalCustomerProps> implements ICustomer {
static create(props: ICustomerCreateProps, id?: UniqueID): Result<Customer, Error> {
const { address, ...internalProps } = props;
const postalAddressResult = PostalAddress.create(address); static create(props: CreateCustomerProps, id?: UniqueID): Result<Customer, Error> {
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
// ... // ...
@ -127,26 +110,28 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
} }
// Rehidratación desde persistencia // Rehidratación desde persistencia
static rehydrate(props: CustomerInternalProps, id: UniqueID): Customer { static rehydrate(props: InternalCustomerProps, 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 = {
Object.assign(this.props, rest); ...this.props,
...rest,
} as ICustomerProps;
if (partialAddress) { if (partialAddress) {
const addressResult = this.address.update(partialAddress); const updatedAddressOrError = this.address.update(partialAddress);
if (updatedAddressOrError.isFailure) {
if (addressResult.isFailure) { return Result.fail(updatedAddressOrError.error);
return Result.fail(addressResult.error);
} }
this.props.address = addressResult.data; updatedProps.address = updatedAddressOrError.data;
} }
return Result.ok(); return Customer.create(updatedProps, this.id);
} }
// 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,14 +1,9 @@
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,
@ -21,13 +16,11 @@ 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;*/
@ -46,9 +39,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({ repository }); //const creator = buildCustomerCreator({ numberService, repository });
const snapshotBuilders = buildCustomerSnapshotBuilders(); const snapshotBuilders = buildCustomerSnapshotBuilders();
//const documentGeneratorPipeline = buildCustomerDocumentService(params); //const documentGeneratorPipeline = buildCustomerDocumentService(params);
@ -70,22 +63,6 @@ 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,
@ -95,7 +72,13 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
transactionManager, 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 {
import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api"; 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 { 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) => {
@ -51,7 +53,7 @@ export const customersRouter = (params: ModuleParams, deps: CustomersInternalDep
} }
); );
router.get( router.get(
"/:customer_id", "/:customer_id",
//checkTabContext, //checkTabContext,
@ -63,17 +65,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.createCustomer(); const useCase = deps.useCases.create();
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 ICustomerCreateProps } from "../../../domain"; import { Customer, CustomerStatus, type ICustomerProps } 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: ICustomerCreateProps = { const customerProps: ICustomerProps = {
companyId: companyId!, companyId: companyId!,
status: status!, status: status!,
reference: reference!, reference: reference!,

View File

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

View File

@ -1,5 +1,5 @@
import { DomainEntity } from "./domain-entity"; import { DomainEntity } from "./domain-entity";
import type { IDomainEvent } from "./events"; import { 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,20 +1,12 @@
import { UniqueID } from "./value-objects"; import { UniqueID } from "./value-objects";
export abstract class DomainEntity<T extends object> { export abstract class DomainEntity<T extends object> {
private _props: T; protected readonly 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 ?? UniqueID.generateNewID(); this.id = 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 } {
@ -26,8 +18,7 @@ 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: PostalAddressPatchProps): Result<PostalAddress, Error> { public update(partial: Partial<PostalAddressPatchProps>): Result<PostalAddress, Error> {
const updatedProps = { const updatedProps = {
...this.props, ...this.props,
...partial, ...partial,