.
This commit is contained in:
parent
93e0e6be65
commit
64b48d707b
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -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
|
||||||
|
|||||||
@ -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 });
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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: {
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
modules/customers/src/api/application/mappers/index.ts
Normal file
1
modules/customers/src/api/application/mappers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./create-customer-input.mapper";
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +1,2 @@
|
|||||||
|
export * from "./customer-creator";
|
||||||
export * from "./customer-finder";
|
export * from "./customer-finder";
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./create-customer.use-case";
|
|
||||||
@ -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 }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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";
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
|
||||||
}),*/
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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!,
|
||||||
|
|||||||
@ -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[] = [];
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user