From 64b48d707bdcbfe2ebfad069d91b9456935a5085 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 9 Mar 2026 21:23:48 +0100 Subject: [PATCH] . --- .vscode/settings.json | 3 - .../create-proforma.use-case.ts | 3 +- .../customer-application.service.ts | 20 +- .../api/application/di/customer-creator.di.ts | 12 + .../api/application/di/customer-finder.di.ts | 4 +- .../di/customer-input-mappers.di.ts | 19 ++ .../application/di/customer-use-cases.di.ts | 33 +-- .../customers/src/api/application/di/index.ts | 4 +- .../customers/src/api/application/index.ts | 1 + .../mappers/create-customer-input.mapper.ts | 238 ++++++++++++++++++ .../src/api/application/mappers/index.ts | 1 + .../application/services/customer-creator.ts | 75 ++++++ .../application/services/customer-updater.ts | 61 +++++ .../src/api/application/services/index.ts | 1 + .../specs/customer-not-exists.spec.ts | 11 +- .../use-cases/create-customer.use-case.ts | 64 +++++ .../create/create-customer.use-case.ts | 90 ------- .../api/application/use-cases/create/index.ts | 1 - .../map-dto-to-create-customer-props.ts | 231 ----------------- .../src/api/application/use-cases/index.ts | 4 +- .../update/update-customer.use-case.ts | 14 +- .../domain/aggregates/customer.aggregate.ts | 57 +++-- .../di/customer-public-services.ts | 4 +- .../src/api/infrastructure/di/customers.di.ts | 41 ++- .../express/customers.routes.ts | 22 +- .../domain/sequelize-customer.mapper.ts | 4 +- packages/rdx-ddd/src/aggregate-root.ts | 2 +- packages/rdx-ddd/src/domain-entity.ts | 17 +- .../src/value-objects/postal-address.ts | 2 +- 29 files changed, 613 insertions(+), 426 deletions(-) create mode 100644 modules/customers/src/api/application/di/customer-creator.di.ts create mode 100644 modules/customers/src/api/application/di/customer-input-mappers.di.ts create mode 100644 modules/customers/src/api/application/mappers/create-customer-input.mapper.ts create mode 100644 modules/customers/src/api/application/mappers/index.ts create mode 100644 modules/customers/src/api/application/services/customer-creator.ts create mode 100644 modules/customers/src/api/application/services/customer-updater.ts create mode 100644 modules/customers/src/api/application/use-cases/create-customer.use-case.ts delete mode 100644 modules/customers/src/api/application/use-cases/create/create-customer.use-case.ts delete mode 100644 modules/customers/src/api/application/use-cases/create/index.ts delete mode 100644 modules/customers/src/api/application/use-cases/create/map-dto-to-create-customer-props.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 59d6d2f3..f2a548e3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -52,9 +52,6 @@ }, // other vscode settings - "[handlebars]": { - "editor.defaultFormatter": "mfeckies.handlebars-formatter" - }, "[sql]": { "editor.defaultFormatter": "cweijan.vscode-mysql-client2" }, // <- your root font size here diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/create-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/create-proforma.use-case.ts index cba7ba46..fed8e90e 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/create-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/create-proforma.use-case.ts @@ -1,6 +1,7 @@ import type { ITransactionManager } from "@erp/core/api"; import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; +import type { Transaction } from "sequelize"; import type { CreateProformaRequestDTO } from "../../../../../common"; import type { ICreateProformaInputMapper } from "../../mappers"; @@ -43,7 +44,7 @@ export class CreateProformaUseCase { const { props, id } = mappedPropsResult.data; - return this.transactionManager.complete(async (transaction) => { + return this.transactionManager.complete(async (transaction: Transaction) => { try { const createResult = await this.creator.create({ companyId, id, props, transaction }); diff --git a/modules/customers/src/api/application/customer-application.service.ts b/modules/customers/src/api/application/customer-application.service.ts index 9fba012d..bc1290b9 100644 --- a/modules/customers/src/api/application/customer-application.service.ts +++ b/modules/customers/src/api/application/customer-application.service.ts @@ -1,10 +1,16 @@ // application/customer-application-service.ts -import { Criteria } from "@repo/rdx-criteria/server"; -import { UniqueID } from "@repo/rdx-ddd"; -import { Collection, Result } from "@repo/rdx-utils"; -import { Transaction } from "sequelize"; -import { Customer, CustomerPatchProps, ICustomerProps, ICustomerRepository } from "../domain"; -import { CustomerListDTO } from "../infrastructure"; +import type { Criteria } from "@repo/rdx-criteria/server"; +import type { UniqueID } from "@repo/rdx-ddd"; +import { type Collection, Result } from "@repo/rdx-utils"; +import type { Transaction } from "sequelize"; + +import { + Customer, + type CustomerPatchProps, + type ICustomerCreateProps, + type ICustomerRepository, +} from "../domain"; +import type { CustomerListDTO } from "../infrastructure"; export class CustomerApplicationService { constructor(private readonly repository: ICustomerRepository) {} @@ -19,7 +25,7 @@ export class CustomerApplicationService { */ buildCustomerInCompany( companyId: UniqueID, - props: Omit, + props: Omit, customerId?: UniqueID ): Result { return Customer.create({ ...props, companyId }, customerId); diff --git a/modules/customers/src/api/application/di/customer-creator.di.ts b/modules/customers/src/api/application/di/customer-creator.di.ts new file mode 100644 index 00000000..0fca6ae1 --- /dev/null +++ b/modules/customers/src/api/application/di/customer-creator.di.ts @@ -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, + }); +}; diff --git a/modules/customers/src/api/application/di/customer-finder.di.ts b/modules/customers/src/api/application/di/customer-finder.di.ts index c9b5f1c2..fe25df4f 100644 --- a/modules/customers/src/api/application/di/customer-finder.di.ts +++ b/modules/customers/src/api/application/di/customer-finder.di.ts @@ -1,6 +1,8 @@ import type { ICustomerRepository } from "../repositories"; 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); } diff --git a/modules/customers/src/api/application/di/customer-input-mappers.di.ts b/modules/customers/src/api/application/di/customer-input-mappers.di.ts new file mode 100644 index 00000000..b62f6aee --- /dev/null +++ b/modules/customers/src/api/application/di/customer-input-mappers.di.ts @@ -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, + }; +}; diff --git a/modules/customers/src/api/application/di/customer-use-cases.di.ts b/modules/customers/src/api/application/di/customer-use-cases.di.ts index 20234edb..94067a4f 100644 --- a/modules/customers/src/api/application/di/customer-use-cases.di.ts +++ b/modules/customers/src/api/application/di/customer-use-cases.di.ts @@ -1,11 +1,12 @@ 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 { ICustomerFullSnapshotBuilder, ICustomerSummarySnapshotBuilder, } from "../snapshot-builders"; -import { GetCustomerByIdUseCase, ListCustomersUseCase } from "../use-cases"; +import { CreateCustomerUseCase, GetCustomerByIdUseCase, ListCustomersUseCase } from "../use-cases"; export function buildGetCustomerByIdUseCase(deps: { 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: { finder: ICustomerFinder; fullSnapshotBuilder: ICustomerFullSnapshotBuilder; @@ -41,20 +56,6 @@ export function buildListCustomersUseCase(deps: { deps.documentService, deps.transactionManager ); -} - -export function buildCreateCustomerUseCase(deps: { - creator: ICustomerCreator; - dtoMapper: ICreateCustomerInputMapper; - fullSnapshotBuilder: ICustomerFullSnapshotBuilder; - transactionManager: ITransactionManager; -}) { - return new CreateCustomerUseCase({ - dtoMapper: deps.dtoMapper, - creator: deps.creator, - fullSnapshotBuilder: deps.fullSnapshotBuilder, - transactionManager: deps.transactionManager, - }); }*/ /*export function buildUpdateCustomerUseCase(deps: { diff --git a/modules/customers/src/api/application/di/index.ts b/modules/customers/src/api/application/di/index.ts index 75d14c0c..4e27b007 100644 --- a/modules/customers/src/api/application/di/index.ts +++ b/modules/customers/src/api/application/di/index.ts @@ -1,5 +1,5 @@ -//export * from "./customer-creator.di"; +export * from "./customer-creator.di"; export * from "./customer-finder.di"; -//export * from "./customer-input-mappers.di"; +export * from "./customer-input-mappers.di"; export * from "./customer-snapshot-builders.di"; export * from "./customer-use-cases.di"; diff --git a/modules/customers/src/api/application/index.ts b/modules/customers/src/api/application/index.ts index 21fd64a4..b68ea0dc 100644 --- a/modules/customers/src/api/application/index.ts +++ b/modules/customers/src/api/application/index.ts @@ -1,4 +1,5 @@ export * from "./di"; +export * from "./mappers"; export * from "./models"; export * from "./repositories"; export * from "./services"; diff --git a/modules/customers/src/api/application/mappers/create-customer-input.mapper.ts b/modules/customers/src/api/application/mappers/create-customer-input.mapper.ts new file mode 100644 index 00000000..c0fd4cdc --- /dev/null +++ b/modules/customers/src/api/application/mappers/create-customer-input.mapper.ts @@ -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(); + + /*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 })); + } + } +} diff --git a/modules/customers/src/api/application/mappers/index.ts b/modules/customers/src/api/application/mappers/index.ts new file mode 100644 index 00000000..80f95a4d --- /dev/null +++ b/modules/customers/src/api/application/mappers/index.ts @@ -0,0 +1 @@ +export * from "./create-customer-input.mapper"; diff --git a/modules/customers/src/api/application/services/customer-creator.ts b/modules/customers/src/api/application/services/customer-creator.ts new file mode 100644 index 00000000..371007bf --- /dev/null +++ b/modules/customers/src/api/application/services/customer-creator.ts @@ -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>; +} + +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> { + 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); + } +} diff --git a/modules/customers/src/api/application/services/customer-updater.ts b/modules/customers/src/api/application/services/customer-updater.ts new file mode 100644 index 00000000..fed2ed41 --- /dev/null +++ b/modules/customers/src/api/application/services/customer-updater.ts @@ -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; + transaction: Transaction; + }): Promise>; +} + +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; + transaction: Transaction; + }): Promise> { + 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); + } +} diff --git a/modules/customers/src/api/application/services/index.ts b/modules/customers/src/api/application/services/index.ts index 92596a25..dbcc05eb 100644 --- a/modules/customers/src/api/application/services/index.ts +++ b/modules/customers/src/api/application/services/index.ts @@ -1 +1,2 @@ +export * from "./customer-creator"; export * from "./customer-finder"; diff --git a/modules/customers/src/api/application/specs/customer-not-exists.spec.ts b/modules/customers/src/api/application/specs/customer-not-exists.spec.ts index 78871332..c8a4ae85 100644 --- a/modules/customers/src/api/application/specs/customer-not-exists.spec.ts +++ b/modules/customers/src/api/application/specs/customer-not-exists.spec.ts @@ -1,11 +1,12 @@ -import { CompositeSpecification, UniqueID } from "@repo/rdx-ddd"; -import { Transaction } from "sequelize"; -import { CustomerApplicationService } from "../../application"; +import { CompositeSpecification, type UniqueID } from "@repo/rdx-ddd"; +import type { Transaction } from "sequelize"; + +import type { ICustomerRepository } from "../../application"; import { logger } from "../../helpers"; export class CustomerNotExistsInCompanySpecification extends CompositeSpecification { constructor( - private readonly service: CustomerApplicationService, + private readonly repository: ICustomerRepository, private readonly companyId: UniqueID, private readonly transaction?: Transaction ) { @@ -13,7 +14,7 @@ export class CustomerNotExistsInCompanySpecification extends CompositeSpecificat } public async isSatisfiedBy(customerId: UniqueID): Promise { - const existsCheck = await this.service.existsByIdInCompany( + const existsCheck = await this.repository.existsByIdInCompany( this.companyId, customerId, this.transaction diff --git a/modules/customers/src/api/application/use-cases/create-customer.use-case.ts b/modules/customers/src/api/application/use-cases/create-customer.use-case.ts new file mode 100644 index 00000000..e232fba9 --- /dev/null +++ b/modules/customers/src/api/application/use-cases/create-customer.use-case.ts @@ -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); + } + }); + } +} diff --git a/modules/customers/src/api/application/use-cases/create/create-customer.use-case.ts b/modules/customers/src/api/application/use-cases/create/create-customer.use-case.ts deleted file mode 100644 index a4241cc9..00000000 --- a/modules/customers/src/api/application/use-cases/create/create-customer.use-case.ts +++ /dev/null @@ -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); - } - }); - } -} diff --git a/modules/customers/src/api/application/use-cases/create/index.ts b/modules/customers/src/api/application/use-cases/create/index.ts deleted file mode 100644 index 507465dd..00000000 --- a/modules/customers/src/api/application/use-cases/create/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./create-customer.use-case"; diff --git a/modules/customers/src/api/application/use-cases/create/map-dto-to-create-customer-props.ts b/modules/customers/src/api/application/use-cases/create/map-dto-to-create-customer-props.ts deleted file mode 100644 index 57c4be32..00000000 --- a/modules/customers/src/api/application/use-cases/create/map-dto-to-create-customer-props.ts +++ /dev/null @@ -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(); - - 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 = { - 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 })); - } -} diff --git a/modules/customers/src/api/application/use-cases/index.ts b/modules/customers/src/api/application/use-cases/index.ts index e2b69a11..5a6abf0d 100644 --- a/modules/customers/src/api/application/use-cases/index.ts +++ b/modules/customers/src/api/application/use-cases/index.ts @@ -1,5 +1,5 @@ -export * from "./create"; +export * from "./create-customer.use-case"; export * from "./delete-customer.use-case"; export * from "./get-customer-by-id.use-case"; export * from "./list-customers.use-case"; -export * from "./update"; +export * from "./update/update-customer.use-case"; diff --git a/modules/customers/src/api/application/use-cases/update/update-customer.use-case.ts b/modules/customers/src/api/application/use-cases/update/update-customer.use-case.ts index 6bb0d51c..54ae78ce 100644 --- a/modules/customers/src/api/application/use-cases/update/update-customer.use-case.ts +++ b/modules/customers/src/api/application/use-cases/update/update-customer.use-case.ts @@ -6,9 +6,6 @@ import type { Transaction } from "sequelize"; import type { UpdateCustomerByIdRequestDTO } from "../../../../common/dto"; import type { CustomerPatchProps } from "../../../domain"; import type { CustomerApplicationService } from "../../customer-application.service"; -import type { CustomerFullSnapshotBuilder } from "../../presenters"; - -import { mapDTOToUpdateCustomerPatchProps } from "./map-dto-to-update-customer-props"; type UpdateCustomerUseCaseInput = { companyId: UniqueID; @@ -30,17 +27,10 @@ export class UpdateCustomerUseCase { if (idOrError.isFailure) { return Result.fail(idOrError.error); } - - const customerId = idOrError.data; - const presenter = this.presenterRegistry.getPresenter({ - resource: "customer", - projection: "FULL", - }) as CustomerFullSnapshotBuilder; - // Mapear DTO → props de dominio - const patchPropsResult = mapDTOToUpdateCustomerPatchProps(dto); + const patchPropsResult = this.dtoMapper.map(dto, { companyId }); if (patchPropsResult.isFailure) { - return Result.fail(patchPropsResult.error); + return patchPropsResult; } const patchProps: CustomerPatchProps = patchPropsResult.data; diff --git a/modules/customers/src/api/domain/aggregates/customer.aggregate.ts b/modules/customers/src/api/domain/aggregates/customer.aggregate.ts index 576270ea..ab352065 100644 --- a/modules/customers/src/api/domain/aggregates/customer.aggregate.ts +++ b/modules/customers/src/api/domain/aggregates/customer.aggregate.ts @@ -5,8 +5,9 @@ import { type LanguageCode, type Name, type PhoneNumber, - type PostalAddress, + PostalAddress, type PostalAddressPatchProps, + type PostalAddressProps, type TINNumber, type TaxCode, type TextValue, @@ -17,7 +18,7 @@ import { type Collection, type Maybe, Result } from "@repo/rdx-utils"; import type { CustomerStatus } from "../value-objects"; -export interface ICustomerProps { +export interface ICustomerCreateProps { companyId: UniqueID; status: CustomerStatus; reference: Maybe; @@ -27,7 +28,7 @@ export interface ICustomerProps { tradeName: Maybe; tin: Maybe; - address: PostalAddress; + address: PostalAddressProps; emailPrimary: Maybe; emailSecondary: Maybe; @@ -43,13 +44,15 @@ export interface ICustomerProps { legalRecord: Maybe; - defaultTaxes: Collection; + defaultTaxes: TaxCode[]; languageCode: LanguageCode; currencyCode: CurrencyCode; } -export type CustomerPatchProps = Partial> & { +export type CustomerPatchProps = Partial< + Omit +> & { address?: PostalAddressPatchProps; }; @@ -90,13 +93,27 @@ export interface ICustomer { readonly currencyCode: CurrencyCode; } -type CreateCustomerProps = ICustomerProps; -type InternalCustomerProps = ICustomerProps; +type CustomerInternalProps = Omit & { + readonly address: PostalAddress; +}; -export class Customer extends AggregateRoot implements ICustomer { +export class Customer extends AggregateRoot implements ICustomer { + static create(props: ICustomerCreateProps, id?: UniqueID): Result { + const { address, ...internalProps } = props; - static create(props: CreateCustomerProps, id?: UniqueID): Result { - const contact = new Customer(props, id); + const postalAddressResult = PostalAddress.create(address); + + if (postalAddressResult.isFailure) { + return Result.fail(postalAddressResult.error); + } + + const contact = new Customer( + { + ...internalProps, + address: postalAddressResult.data, + }, + id + ); // Reglas de negocio / validaciones // ... @@ -110,28 +127,26 @@ export class Customer extends AggregateRoot implements IC } // Rehidratación desde persistencia - static rehydrate(props: InternalCustomerProps, id: UniqueID): Customer { + static rehydrate(props: CustomerInternalProps, id: UniqueID): Customer { return new Customer(props, id); } - public update(partialCustomer: CustomerPatchProps): Result { const { address: partialAddress, ...rest } = partialCustomer; - const updatedProps = { - ...this.props, - ...rest, - } as ICustomerProps; + + Object.assign(this.props, rest); if (partialAddress) { - const updatedAddressOrError = this.address.update(partialAddress); - if (updatedAddressOrError.isFailure) { - return Result.fail(updatedAddressOrError.error); + const addressResult = this.address.update(partialAddress); + + 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 diff --git a/modules/customers/src/api/infrastructure/di/customer-public-services.ts b/modules/customers/src/api/infrastructure/di/customer-public-services.ts index 7cb843fa..e0b3ac14 100644 --- a/modules/customers/src/api/infrastructure/di/customer-public-services.ts +++ b/modules/customers/src/api/infrastructure/di/customer-public-services.ts @@ -4,7 +4,7 @@ export type CustomersServicesDeps = { services: { listCustomers: (filters: unknown, context: unknown) => null; getCustomerById: (id: unknown, context: unknown) => null; - generateCustomerReport: (id: unknown, options: unknown, context: unknown) => null; + //generateCustomerReport: (id: unknown, options: unknown, context: unknown) => null; }; }; @@ -17,7 +17,7 @@ export function buildCustomerServices(deps: CustomersInternalDeps): CustomersSer getCustomerById: (id, context) => null, //internal.useCases.getCustomerById().execute(id, context), - generateCustomerReport: (id, options, context) => null, + //generateCustomerReport: (id, options, context) => null, //internal.useCases.reportCustomer().execute(id, options, context), }, }; diff --git a/modules/customers/src/api/infrastructure/di/customers.di.ts b/modules/customers/src/api/infrastructure/di/customers.di.ts index 1a959a46..10ddb23b 100644 --- a/modules/customers/src/api/infrastructure/di/customers.di.ts +++ b/modules/customers/src/api/infrastructure/di/customers.di.ts @@ -1,9 +1,14 @@ import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api"; import { + type CreateCustomerUseCase, type GetCustomerByIdUseCase, type ListCustomersUseCase, + type UpdateCustomerUseCase, + buildCreateCustomerUseCase, + buildCustomerCreator, buildCustomerFinder, + buildCustomerInputMappers, buildCustomerSnapshotBuilders, buildGetCustomerByIdUseCase, buildListCustomersUseCase, @@ -16,11 +21,13 @@ export type CustomersInternalDeps = { useCases: { listCustomers: () => ListCustomersUseCase; getCustomerById: () => GetCustomerByIdUseCase; + createCustomer: () => CreateCustomerUseCase; + updateCustomer: () => UpdateCustomerUseCase; + //reportCustomer: () => ReportCustomerUseCase; - //createCustomer: () => CreateCustomerUseCase; /* - updateCustomer: () => UpdateCustomerUseCase; + deleteCustomer: () => DeleteCustomerUseCase; issueCustomer: () => IssueCustomerUseCase; changeStatusCustomer: () => ChangeStatusCustomerUseCase;*/ @@ -39,9 +46,9 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter //const numberService = buildCustomerNumberGenerator(); // Application helpers - //const inputMappers = buildCustomerInputMappers(catalogs); - const finder = buildCustomerFinder(repository); - //const creator = buildCustomerCreator({ numberService, repository }); + const inputMappers = buildCustomerInputMappers(catalogs); + const finder = buildCustomerFinder({ repository }); + const creator = buildCustomerCreator({ repository }); const snapshotBuilders = buildCustomerSnapshotBuilders(); //const documentGeneratorPipeline = buildCustomerDocumentService(params); @@ -63,6 +70,22 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter transactionManager, }), + createCustomer: () => + buildCreateCustomerUseCase({ + creator, + dtoMapper: inputMappers.createInputMapper, + fullSnapshotBuilder: snapshotBuilders.full, + transactionManager, + }), + + updateCustomer: () => + buildUpdateCustomerUseCase({ + creator, + dtoMapper: inputMappers.updateInputMapper, + fullSnapshotBuilder: snapshotBuilders.full, + transactionManager, + }), + /*reportCustomer: () => buildReportCustomerUseCase({ finder, @@ -72,13 +95,7 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter transactionManager, }), - createCustomer: () => - buildCreateCustomerUseCase({ - creator, - dtoMapper: inputMappers.createInputMapper, - fullSnapshotBuilder: snapshotBuilders.full, - transactionManager, - }),*/ +*/ }, }; } diff --git a/modules/customers/src/api/infrastructure/express/customers.routes.ts b/modules/customers/src/api/infrastructure/express/customers.routes.ts index ef1528bc..9d26bd70 100644 --- a/modules/customers/src/api/infrastructure/express/customers.routes.ts +++ b/modules/customers/src/api/infrastructure/express/customers.routes.ts @@ -1,20 +1,18 @@ -import { - mockUser, - requireAuthenticated, - requireCompanyContext, -} from "@erp/auth/api"; -import { type ModuleParams, RequestWithAuth, validateRequest } from "@erp/core/api"; +import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api"; +import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api"; import { type NextFunction, type Request, type Response, Router } from "express"; import { + CreateCustomerRequestSchema, CustomerListRequestSchema, - GetCustomerByIdRequestSchema + GetCustomerByIdRequestSchema, } from "../../../common/dto"; import type { CustomersInternalDeps } from "../di"; import { + CreateCustomerController, GetCustomerController, - ListCustomersController + ListCustomersController, } from "./controllers"; export const customersRouter = (params: ModuleParams, deps: CustomersInternalDeps) => { @@ -53,7 +51,7 @@ export const customersRouter = (params: ModuleParams, deps: CustomersInternalDep } ); - router.get( + router.get( "/:customer_id", //checkTabContext, @@ -65,17 +63,17 @@ export const customersRouter = (params: ModuleParams, deps: CustomersInternalDep } ); - /* router.post( + router.post( "/", //checkTabContext, validateRequest(CreateCustomerRequestSchema, "body"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.create(); + const useCase = deps.useCases.createCustomer(); const controller = new CreateCustomerController(useCase); return controller.execute(req, res, next); } - ); */ + ); /* router.put( "/:customer_id", diff --git a/modules/customers/src/api/infrastructure/mappers/domain/sequelize-customer.mapper.ts b/modules/customers/src/api/infrastructure/mappers/domain/sequelize-customer.mapper.ts index 7695b93d..1b005d85 100644 --- a/modules/customers/src/api/infrastructure/mappers/domain/sequelize-customer.mapper.ts +++ b/modules/customers/src/api/infrastructure/mappers/domain/sequelize-customer.mapper.ts @@ -24,7 +24,7 @@ import { } from "@repo/rdx-ddd"; 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"; export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper< @@ -199,7 +199,7 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper< return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors)); } - const customerProps: ICustomerProps = { + const customerProps: ICustomerCreateProps = { companyId: companyId!, status: status!, reference: reference!, diff --git a/packages/rdx-ddd/src/aggregate-root.ts b/packages/rdx-ddd/src/aggregate-root.ts index bfc84cfe..ac952ee1 100644 --- a/packages/rdx-ddd/src/aggregate-root.ts +++ b/packages/rdx-ddd/src/aggregate-root.ts @@ -1,5 +1,5 @@ import { DomainEntity } from "./domain-entity"; -import { IDomainEvent } from "./events"; +import type { IDomainEvent } from "./events"; export abstract class AggregateRoot extends DomainEntity { private _domainEvents: IDomainEvent[] = []; diff --git a/packages/rdx-ddd/src/domain-entity.ts b/packages/rdx-ddd/src/domain-entity.ts index 0204c655..d64832c9 100644 --- a/packages/rdx-ddd/src/domain-entity.ts +++ b/packages/rdx-ddd/src/domain-entity.ts @@ -1,12 +1,20 @@ import { UniqueID } from "./value-objects"; export abstract class DomainEntity { - protected readonly props: T; + private _props: T; public readonly id: UniqueID; protected constructor(props: T, id?: UniqueID) { - this.id = id ? id : UniqueID.generateNewID(); - this.props = props; + this.id = id ?? UniqueID.generateNewID(); + 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 } { @@ -18,7 +26,8 @@ export abstract class DomainEntity { }, {}); } - equals(other: DomainEntity): boolean { + equals(other?: DomainEntity): boolean { + if (!other) return false; return other instanceof DomainEntity && this.id.equals(other.id); } diff --git a/packages/rdx-ddd/src/value-objects/postal-address.ts b/packages/rdx-ddd/src/value-objects/postal-address.ts index 0a1594ac..47eb177c 100644 --- a/packages/rdx-ddd/src/value-objects/postal-address.ts +++ b/packages/rdx-ddd/src/value-objects/postal-address.ts @@ -34,7 +34,7 @@ export class PostalAddress extends ValueObject { return Result.ok(new PostalAddress(values)); } - public update(partial: Partial): Result { + public update(partial: PostalAddressPatchProps): Result { const updatedProps = { ...this.props, ...partial,