From ad66580d85b7142b34f43cea3bd5efa2bd128152 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 1 Sep 2025 16:07:59 +0200 Subject: [PATCH] Facturas de cliente y clientes --- .../express/express-controller.ts | 3 + .../api/domain/aggregates/customer-invoice.ts | 2 +- .../customer-invoice-item-subtotal-price.ts | 4 +- .../customer-invoice-item-total-price.ts | 4 +- .../customer-invoice-item-unit-price.ts | 4 +- modules/customers/package.json | 3 +- .../assembler/create-customers.assembler.ts | 48 +- .../create-customer.use-case.ts | 8 +- .../map-dto-to-create-customer-props.ts | 228 +++++++++ .../delete-customer.use-case.ts | 23 +- .../assembler/get-customer.assembler.ts | 68 +-- .../get-customer/get-customer.use-case.ts | 21 +- .../customers/src/api/application/index.ts | 2 +- .../assembler/list-customers.assembler.ts | 76 ++- .../list-customers/list-customers.use-case.ts | 5 +- .../src/api/application/services/index.ts | 2 - .../services/participantAddressFinder.ts | 64 --- .../application/services/participantFinder.ts | 21 - .../update-customer/assembler/index.ts | 1 + .../assembler/update-customer.assembler.ts | 83 ++++ .../api/application/update-customer/index.ts | 1 + .../map-dto-to-update-customer-props.ts | 245 ++++++++++ .../update-customer.use-case.ts | 431 ++---------------- .../src/api/domain/aggregates/customer.ts | 249 ++++++---- .../services/customer-service.interface.ts | 66 --- .../api/domain/services/customer.service.ts | 12 +- .../src/api/domain/services/index.ts | 1 - modules/customers/src/api/helpers/index.ts | 2 +- .../api/helpers/map-dto-to-customer-props.ts | 109 ----- .../src/api/infrastructure/dependencies.ts | 17 +- .../controllers/create-customer.controller.ts | 4 +- .../controllers/delete-customer.controller.ts | 4 +- .../controllers/get-customer.controller.ts | 6 +- .../controllers/update-customer.controller.ts | 24 + .../update-invoice.controller.ts.bak | 72 --- .../express/customers.routes.ts | 4 +- .../infrastructure/mappers/customer.mapper.ts | 222 +++++++-- .../sequelize/customer.model.ts | 17 +- .../sequelize/customer.repository.ts | 8 +- .../request/create-customer.request.dto.ts | 9 +- .../request/update-customer.request.dto.ts | 30 ++ ...t.dto.ts => create-customer.result.dto.ts} | 10 +- .../response/customer-list.response.dto.ts | 4 +- .../get-customer-by-id.response.dto.ts | 8 +- .../src/common/dto/response/index.ts | 2 +- .../web/hooks/use-create-customer-mutation.ts | 4 +- .../src/web/pages/create/customer.schema.ts | 4 +- packages/rdx-ddd/src/helpers/index.ts | 1 + packages/rdx-ddd/src/helpers/normalizers.ts | 35 ++ packages/rdx-ddd/src/index.ts | 1 + .../__tests__/email-address.test.ts | 13 - .../src/value-objects/__tests__/name.spec.ts | 13 - .../__tests__/phone-number.test.ts | 13 - .../__tests__/postal-address.test.ts | 21 - .../src/value-objects/__tests__/slug.spec.ts | 13 - .../__tests__/tin-number.test.ts | 13 - .../__tests__/value-objects.test.ts | 4 +- packages/rdx-ddd/src/value-objects/city.ts | 38 ++ packages/rdx-ddd/src/value-objects/country.ts | 38 ++ .../src/value-objects/currency-code.ts | 48 ++ .../src/value-objects/email-address.ts | 12 +- packages/rdx-ddd/src/value-objects/index.ts | 10 + .../src/value-objects/language-code.ts | 48 ++ .../rdx-ddd/src/value-objects/money-value.ts | 12 +- packages/rdx-ddd/src/value-objects/name.ts | 14 +- .../rdx-ddd/src/value-objects/percentage.ts | 17 +- .../src/value-objects/postal-address.ts | 112 +++-- .../rdx-ddd/src/value-objects/postal-code.ts | 46 ++ .../rdx-ddd/src/value-objects/province.ts | 38 ++ .../rdx-ddd/src/value-objects/quantity.ts | 32 +- packages/rdx-ddd/src/value-objects/slug.ts | 10 +- packages/rdx-ddd/src/value-objects/street.ts | 38 ++ .../rdx-ddd/src/value-objects/tax-code.ts | 46 ++ .../rdx-ddd/src/value-objects/text-value.ts | 39 ++ .../rdx-ddd/src/value-objects/tin-number.ts | 10 +- .../rdx-ddd/src/value-objects/url-address.ts | 32 ++ .../rdx-ddd/src/value-objects/utc-date.ts | 6 +- packages/rdx-utils/src/helpers/index.ts | 1 + packages/rdx-utils/src/helpers/maybe.ts | 4 + packages/rdx-utils/src/helpers/patch-field.ts | 38 ++ packages/rdx-utils/src/helpers/utils.ts | 6 + pnpm-lock.yaml | 332 +++++++++++++- 82 files changed, 2121 insertions(+), 1268 deletions(-) create mode 100644 modules/customers/src/api/application/create-customer/map-dto-to-create-customer-props.ts delete mode 100644 modules/customers/src/api/application/services/index.ts delete mode 100644 modules/customers/src/api/application/services/participantAddressFinder.ts delete mode 100644 modules/customers/src/api/application/services/participantFinder.ts create mode 100644 modules/customers/src/api/application/update-customer/assembler/index.ts create mode 100644 modules/customers/src/api/application/update-customer/assembler/update-customer.assembler.ts create mode 100644 modules/customers/src/api/application/update-customer/map-dto-to-update-customer-props.ts delete mode 100644 modules/customers/src/api/domain/services/customer-service.interface.ts delete mode 100644 modules/customers/src/api/helpers/map-dto-to-customer-props.ts create mode 100644 modules/customers/src/api/infrastructure/express/controllers/update-customer.controller.ts delete mode 100644 modules/customers/src/api/infrastructure/express/controllers/update-invoice.controller.ts.bak rename modules/customers/src/common/dto/response/{customer-creation.result.dto.ts => create-customer.result.dto.ts} (70%) create mode 100644 packages/rdx-ddd/src/helpers/index.ts create mode 100644 packages/rdx-ddd/src/helpers/normalizers.ts create mode 100644 packages/rdx-ddd/src/value-objects/city.ts create mode 100644 packages/rdx-ddd/src/value-objects/country.ts create mode 100644 packages/rdx-ddd/src/value-objects/currency-code.ts create mode 100644 packages/rdx-ddd/src/value-objects/language-code.ts create mode 100644 packages/rdx-ddd/src/value-objects/postal-code.ts create mode 100644 packages/rdx-ddd/src/value-objects/province.ts create mode 100644 packages/rdx-ddd/src/value-objects/street.ts create mode 100644 packages/rdx-ddd/src/value-objects/tax-code.ts create mode 100644 packages/rdx-ddd/src/value-objects/text-value.ts create mode 100644 packages/rdx-ddd/src/value-objects/url-address.ts create mode 100644 packages/rdx-utils/src/helpers/patch-field.ts diff --git a/modules/core/src/api/infrastructure/express/express-controller.ts b/modules/core/src/api/infrastructure/express/express-controller.ts index eb30dc26..3acdd179 100644 --- a/modules/core/src/api/infrastructure/express/express-controller.ts +++ b/modules/core/src/api/infrastructure/express/express-controller.ts @@ -34,6 +34,9 @@ export abstract class ExpressController { } satisfies ApiErrorContext; const body = toProblemJson(apiError, ctx); + + console.trace(body); + return res.type("application/problem+json").status(apiError.status).json(body); } diff --git a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts index c16164a0..442f2cb7 100644 --- a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts +++ b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts @@ -1,4 +1,4 @@ -import { AggregateRoot, MoneyValue, UniqueID, UtcDate } from "@repo/rdx-ddd"; +import { AggregateRoot, UniqueID, UtcDate } from "@repo/rdx-ddd"; import { Collection, Result } from "@repo/rdx-utils"; import { CustomerInvoiceCustomer, CustomerInvoiceItem, CustomerInvoiceItems } from "../entities"; import { diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-subtotal-price.ts b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-subtotal-price.ts index da10b0e8..bf0ba908 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-subtotal-price.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-subtotal-price.ts @@ -1,9 +1,9 @@ -import { IMoneyValueProps, MoneyValue } from "@repo/rdx-ddd"; +import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd"; export class CustomerInvoiceItemSubtotalPrice extends MoneyValue { public static DEFAULT_SCALE = 4; - static create({ amount, currency_code, scale }: IMoneyValueProps) { + static create({ amount, currency_code, scale }: MoneyValueProps) { const props = { amount: Number(amount), scale: scale ?? MoneyValue.DEFAULT_SCALE, diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-total-price.ts b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-total-price.ts index 01615905..3a5d91de 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-total-price.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-total-price.ts @@ -1,9 +1,9 @@ -import { IMoneyValueProps, MoneyValue } from "@repo/rdx-ddd"; +import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd"; export class CustomerInvoiceItemTotalPrice extends MoneyValue { public static DEFAULT_SCALE = 4; - static create({ amount, currency_code, scale }: IMoneyValueProps) { + static create({ amount, currency_code, scale }: MoneyValueProps) { const props = { amount: Number(amount), scale: scale ?? MoneyValue.DEFAULT_SCALE, diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-unit-price.ts b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-unit-price.ts index 77b60e14..c860037c 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-unit-price.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-unit-price.ts @@ -1,9 +1,9 @@ -import { IMoneyValueProps, MoneyValue } from "@repo/rdx-ddd"; +import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd"; export class CustomerInvoiceItemUnitPrice extends MoneyValue { public static DEFAULT_SCALE = 4; - static create({ amount, currency_code, scale }: IMoneyValueProps) { + static create({ amount, currency_code, scale }: MoneyValueProps) { const props = { amount: Number(amount), scale: scale ?? MoneyValue.DEFAULT_SCALE, diff --git a/modules/customers/package.json b/modules/customers/package.json index a1d59604..281d9a52 100644 --- a/modules/customers/package.json +++ b/modules/customers/package.json @@ -24,7 +24,8 @@ "@types/express": "^4.17.21", "@types/react": "^19.1.2", "@types/react-i18next": "^8.1.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^3.2.4" }, "dependencies": { "@ag-grid-community/locale": "34.0.0", diff --git a/modules/customers/src/api/application/create-customer/assembler/create-customers.assembler.ts b/modules/customers/src/api/application/create-customer/assembler/create-customers.assembler.ts index 8d7ffdc2..f810e7c8 100644 --- a/modules/customers/src/api/application/create-customer/assembler/create-customers.assembler.ts +++ b/modules/customers/src/api/application/create-customer/assembler/create-customers.assembler.ts @@ -1,35 +1,43 @@ +import { toEmptyString } from "@repo/rdx-ddd"; import { CustomerCreationResponseDTO } from "../../../../common"; import { Customer } from "../../../domain"; export class CreateCustomersAssembler { public toDTO(customer: Customer): CustomerCreationResponseDTO { + const address = customer.address.toPrimitive(); + return { id: customer.id.toPrimitive(), company_id: customer.companyId.toPrimitive(), - reference: customer.reference, - is_company: customer.isCompany, - name: customer.name, - trade_name: customer.tradeName, - tin: customer.tin.toPrimitive(), - email: customer.email.toPrimitive(), - phone: customer.phone.toPrimitive(), - fax: customer.fax.toPrimitive(), - website: customer.website, + reference: toEmptyString(customer.reference, (value) => value.toPrimitive()), - default_tax: customer.defaultTax, - legal_record: customer.legalRecord, - lang_code: customer.langCode, - currency_code: customer.currencyCode, + is_company: String(customer.isCompany), + name: customer.name.toPrimitive(), + + trade_name: toEmptyString(customer.tradeName, (value) => value.toPrimitive()), + + tin: toEmptyString(customer.tin, (value) => value.toPrimitive()), + + street: toEmptyString(address.street, (value) => value.toPrimitive()), + street2: toEmptyString(address.street2, (value) => value.toPrimitive()), + city: toEmptyString(address.city, (value) => value.toPrimitive()), + state: toEmptyString(address.province, (value) => value.toPrimitive()), + postal_code: toEmptyString(address.postalCode, (value) => value.toPrimitive()), + country: toEmptyString(address.country, (value) => value.toPrimitive()), + + email: toEmptyString(customer.email, (value) => value.toPrimitive()), + phone: toEmptyString(customer.phone, (value) => value.toPrimitive()), + fax: toEmptyString(customer.fax, (value) => value.toPrimitive()), + website: toEmptyString(customer.website, (value) => value.toPrimitive()), + + legal_record: toEmptyString(customer.legalRecord, (value) => value.toPrimitive()), + + default_taxes: customer.defaultTaxes.map((item) => item.toPrimitive()), status: customer.isActive ? "active" : "inactive", - - street: customer.address.street, - street2: customer.address.street2, - city: customer.address.city, - state: customer.address.state, - postal_code: customer.address.postalCode, - country: customer.address.country, + language_code: customer.languageCode.toPrimitive(), + currency_code: customer.currencyCode.toPrimitive(), metadata: { entity: "customer", diff --git a/modules/customers/src/api/application/create-customer/create-customer.use-case.ts b/modules/customers/src/api/application/create-customer/create-customer.use-case.ts index 9c15183e..0994114c 100644 --- a/modules/customers/src/api/application/create-customer/create-customer.use-case.ts +++ b/modules/customers/src/api/application/create-customer/create-customer.use-case.ts @@ -3,9 +3,9 @@ import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; import { CreateCustomerRequestDTO } from "../../../common"; -import { ICustomerService } from "../../domain"; -import { mapDTOToCustomerProps } from "../../helpers"; +import { CustomerService } from "../../domain"; import { CreateCustomersAssembler } from "./assembler"; +import { mapDTOToCreateCustomerProps } from "./map-dto-to-create-customer-props"; type CreateCustomerUseCaseInput = { dto: CreateCustomerRequestDTO; @@ -13,7 +13,7 @@ type CreateCustomerUseCaseInput = { export class CreateCustomerUseCase { constructor( - private readonly service: ICustomerService, + private readonly service: CustomerService, private readonly transactionManager: ITransactionManager, private readonly assembler: CreateCustomersAssembler ) {} @@ -22,7 +22,7 @@ export class CreateCustomerUseCase { const { dto } = params; // 1) Mapear DTO → props de dominio - const dtoResult = mapDTOToCustomerProps(dto); + const dtoResult = mapDTOToCreateCustomerProps(dto); if (dtoResult.isFailure) { return Result.fail(dtoResult.error); } diff --git a/modules/customers/src/api/application/create-customer/map-dto-to-create-customer-props.ts b/modules/customers/src/api/application/create-customer/map-dto-to-create-customer-props.ts new file mode 100644 index 00000000..8898ec70 --- /dev/null +++ b/modules/customers/src/api/application/create-customer/map-dto-to-create-customer-props.ts @@ -0,0 +1,228 @@ +import { + DomainError, + ValidationErrorCollection, + ValidationErrorDetail, + extractOrPushError, +} from "@erp/core/api"; +import { + City, + Country, + CurrencyCode, + EmailAddress, + LanguageCode, + Name, + PhoneNumber, + PostalAddress, + PostalCode, + Province, + Street, + TINNumber, + TaxCode, + TextValue, + URLAddress, + UniqueID, + maybeFromNullableVO, +} from "@repo/rdx-ddd"; +import { Collection, Result } from "@repo/rdx-utils"; +import { CreateCustomerRequestDTO } from "../../../common/dto"; +import { CustomerProps, 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 companyId = extractOrPushError(UniqueID.create(dto.company_id), "company_id", errors); + + const isCompany = dto.is_company; + const status = extractOrPushError(CustomerStatus.create(dto.status), "status", errors); + const reference = extractOrPushError( + maybeFromNullableVO(dto.reference, (value) => Name.create(value)), + "reference", + errors + ); + + const name = extractOrPushError(Name.create(dto.name), "name", errors); + + const tradeName = extractOrPushError( + maybeFromNullableVO(dto.trade_name, (value) => Name.create(value)), + "trade_name", + errors + ); + + const tinNumber = extractOrPushError( + maybeFromNullableVO(dto.tin, (value) => TINNumber.create(value)), + "tin", + errors + ); + + const street = extractOrPushError( + maybeFromNullableVO(dto.street, (value) => Street.create(value)), + "street", + errors + ); + + const street2 = extractOrPushError( + maybeFromNullableVO(dto.street2, (value) => Street.create(value)), + "street2", + errors + ); + + const city = extractOrPushError( + maybeFromNullableVO(dto.city, (value) => City.create(value)), + "city", + errors + ); + + const province = extractOrPushError( + maybeFromNullableVO(dto.province, (value) => Province.create(value)), + "province", + errors + ); + + const postalCode = extractOrPushError( + maybeFromNullableVO(dto.postal_code, (value) => PostalCode.create(value)), + "postal_code", + errors + ); + + const country = extractOrPushError( + maybeFromNullableVO(dto.country, (value) => Country.create(value)), + "country", + errors + ); + + const emailAddress = extractOrPushError( + maybeFromNullableVO(dto.email, (value) => EmailAddress.create(value)), + "email", + errors + ); + + const phoneNumber = extractOrPushError( + maybeFromNullableVO(dto.phone, (value) => PhoneNumber.create(value)), + "phone", + errors + ); + + const faxNumber = extractOrPushError( + maybeFromNullableVO(dto.fax, (value) => PhoneNumber.create(value)), + "fax", + errors + ); + + const website = extractOrPushError( + maybeFromNullableVO(dto.website, (value) => URLAddress.create(value)), + "website", + errors + ); + + const legalRecord = extractOrPushError( + maybeFromNullableVO(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(); + + 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) { + console.error(errors); + 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 + ); + + console.debug("Mapped customer props:", { + companyId, + status, + reference, + isCompany, + name, + tradeName, + tinNumber, + street, + street2, + city, + province, + postalCode, + country, + emailAddress, + phoneNumber, + faxNumber, + website, + legalRecord, + languageCode, + currencyCode, + defaultTaxes, + }); + + const customerProps: CustomerProps = { + companyId: companyId!, + status: status!, + reference: reference!, + + isCompany: isCompany, + name: name!, + tradeName: tradeName!, + tin: tinNumber!, + + address: postalAddress!, + + email: emailAddress!, + phone: phoneNumber!, + fax: faxNumber!, + website: website!, + + legalRecord: legalRecord!, + defaultTaxes: defaultTaxes!, + languageCode: languageCode!, + currencyCode: currencyCode!, + }; + + return Result.ok({ id: customerId!, props: customerProps }); + } catch (err: unknown) { + console.error(err); + return Result.fail(new DomainError("Customer props mapping failed", { cause: err })); + } +} diff --git a/modules/customers/src/api/application/delete-customer/delete-customer.use-case.ts b/modules/customers/src/api/application/delete-customer/delete-customer.use-case.ts index 01e130eb..ee874bc0 100644 --- a/modules/customers/src/api/application/delete-customer/delete-customer.use-case.ts +++ b/modules/customers/src/api/application/delete-customer/delete-customer.use-case.ts @@ -1,36 +1,43 @@ import { EntityNotFoundError, ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { ICustomerService } from "../../domain"; +import { CustomerService } from "../../domain"; + +type DeleteCustomerUseCaseInput = { + companyId: UniqueID; + id: string; +}; export class DeleteCustomerUseCase { constructor( - private readonly service: ICustomerService, + private readonly service: CustomerService, private readonly transactionManager: ITransactionManager ) {} - public execute(dto: DeleteCustomerByIdQueryDTO) { - const idOrError = UniqueID.create(dto.id); + public execute(params: DeleteCustomerUseCaseInput) { + const { companyId, id } = params; + + const idOrError = UniqueID.create(id); if (idOrError.isFailure) { return Result.fail(idOrError.error); } - const id = idOrError.data; + const validId = idOrError.data; return this.transactionManager.complete(async (transaction) => { try { - const existsCheck = await this.service.existsByIdInCompany(id, transaction); + const existsCheck = await this.service.existsByIdInCompany(companyId, validId, transaction); if (existsCheck.isFailure) { return Result.fail(existsCheck.error); } if (!existsCheck.data) { - return Result.fail(new EntityNotFoundError("Customer", "id", id.toString())); + return Result.fail(new EntityNotFoundError("Customer", "id", validId.toString())); } - return await this.service.deleteCustomerByIdInCompany(id, transaction); + return await this.service.deleteCustomerByIdInCompany(validId, transaction); } catch (error: unknown) { return Result.fail(error as Error); } diff --git a/modules/customers/src/api/application/get-customer/assembler/get-customer.assembler.ts b/modules/customers/src/api/application/get-customer/assembler/get-customer.assembler.ts index 171377bc..4bc5b194 100644 --- a/modules/customers/src/api/application/get-customer/assembler/get-customer.assembler.ts +++ b/modules/customers/src/api/application/get-customer/assembler/get-customer.assembler.ts @@ -1,63 +1,43 @@ +import { toEmptyString } from "@repo/rdx-ddd"; import { GetCustomerByIdResponseDTO } from "../../../../common/dto"; import { Customer } from "../../../domain"; export class GetCustomerAssembler { toDTO(customer: Customer): GetCustomerByIdResponseDTO { + const address = customer.address.toPrimitive(); + return { id: customer.id.toPrimitive(), - reference: customer.reference, + company_id: customer.companyId.toPrimitive(), - is_company: customer.isCompany, - name: customer.name, - trade_name: customer.tradeName ?? "", - tin: customer.tin.toPrimitive(), + reference: toEmptyString(customer.reference, (value) => value.toPrimitive()), - metadata: { - entity: "customer", - //updated_at: customer.updatedAt.toDateString(), - //created_at: customer.createdAt.toDateString(), - }, + is_company: String(customer.isCompany), + name: customer.name.toPrimitive(), - //subtotal: customer.calculateSubtotal().toPrimitive(), + trade_name: toEmptyString(customer.tradeName, (value) => value.toPrimitive()), - //total: customer.calculateTotal().toPrimitive(), + tin: toEmptyString(customer.tin, (value) => value.toPrimitive()), - /*items: - customer.items.size() > 0 - ? customer.items.map((item: CustomerItem) => ({ - description: item.description.toString(), - quantity: item.quantity.toPrimitive(), - unit_measure: "", - unit_price: item.unitPrice.toPrimitive(), - subtotal: item.calculateSubtotal().toPrimitive(), - //tax_amount: item.calculateTaxAmount().toPrimitive(), - total: item.calculateTotal().toPrimitive(), - })) - : [],*/ + street: toEmptyString(address.street, (value) => value.toPrimitive()), + street2: toEmptyString(address.street2, (value) => value.toPrimitive()), + city: toEmptyString(address.city, (value) => value.toPrimitive()), + state: toEmptyString(address.province, (value) => value.toPrimitive()), + postal_code: toEmptyString(address.postalCode, (value) => value.toPrimitive()), + country: toEmptyString(address.country, (value) => value.toPrimitive()), - //sender: {}, //await CustomerParticipantAssembler(customer.senderId, context), + email: toEmptyString(customer.email, (value) => value.toPrimitive()), + phone: toEmptyString(customer.phone, (value) => value.toPrimitive()), + fax: toEmptyString(customer.fax, (value) => value.toPrimitive()), + website: toEmptyString(customer.website, (value) => value.toPrimitive()), - /*recipient: await CustomerParticipantAssembler(customer.recipient, context), - items: customerItemAssembler(customer.items, context), + legal_record: toEmptyString(customer.legalRecord, (value) => value.toPrimitive()), - payment_term: { - payment_type: "", - due_date: "", - }, + default_taxes: customer.defaultTaxes.map((item) => item.toPrimitive()), - due_amount: { - currency: customer.currency.toString(), - precision: 2, - amount: 0, - }, - - custom_fields: [], - - metadata: { - create_time: "", - last_updated_time: "", - delete_time: "", - },*/ + status: customer.isActive ? "active" : "inactive", + language_code: customer.languageCode.toPrimitive(), + currency_code: customer.currencyCode.toPrimitive(), }; } } diff --git a/modules/customers/src/api/application/get-customer/get-customer.use-case.ts b/modules/customers/src/api/application/get-customer/get-customer.use-case.ts index 64c28b4b..45212f6d 100644 --- a/modules/customers/src/api/application/get-customer/get-customer.use-case.ts +++ b/modules/customers/src/api/application/get-customer/get-customer.use-case.ts @@ -1,23 +1,24 @@ import { ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { ICustomerService } from "../../domain"; +import { CustomerService } from "../../domain"; import { GetCustomerAssembler } from "./assembler"; type GetCustomerUseCaseInput = { - tenantId: string; + companyId: UniqueID; id: string; }; export class GetCustomerUseCase { constructor( - private readonly service: ICustomerService, + private readonly service: CustomerService, private readonly transactionManager: ITransactionManager, private readonly assembler: GetCustomerAssembler ) {} public execute(params: GetCustomerUseCaseInput) { - const { id, tenantId: companyId } = params; + console.log(params); + const { id, companyId } = params; const idOrError = UniqueID.create(id); @@ -25,24 +26,22 @@ export class GetCustomerUseCase { return Result.fail(idOrError.error); } - const companyIdOrError = UniqueID.create(companyId); - - if (companyIdOrError.isFailure) { - return Result.fail(companyIdOrError.error); - } - return this.transactionManager.complete(async (transaction) => { try { const customerOrError = await this.service.getCustomerByIdInCompany( - companyIdOrError.data, + companyId, idOrError.data, transaction ); + + console.log(customerOrError); + if (customerOrError.isFailure) { return Result.fail(customerOrError.error); } const getDTO = this.assembler.toDTO(customerOrError.data); + console.log(getDTO); return Result.ok(getDTO); } catch (error: unknown) { return Result.fail(error as Error); diff --git a/modules/customers/src/api/application/index.ts b/modules/customers/src/api/application/index.ts index 847c0981..0858943b 100644 --- a/modules/customers/src/api/application/index.ts +++ b/modules/customers/src/api/application/index.ts @@ -2,4 +2,4 @@ export * from "./create-customer"; export * from "./delete-customer"; export * from "./get-customer"; export * from "./list-customers"; -//export * from "./update-customer"; +export * from "./update-customer"; diff --git a/modules/customers/src/api/application/list-customers/assembler/list-customers.assembler.ts b/modules/customers/src/api/application/list-customers/assembler/list-customers.assembler.ts index fa914f0a..3045efa7 100644 --- a/modules/customers/src/api/application/list-customers/assembler/list-customers.assembler.ts +++ b/modules/customers/src/api/application/list-customers/assembler/list-customers.assembler.ts @@ -10,30 +10,72 @@ export class ListCustomersAssembler { return { id: customer.id.toPrimitive(), - reference: customer.reference, + company_id: customer.companyId.toPrimitive(), + + reference: customer.reference.match( + (value) => value.toPrimitive(), + () => "" + ), is_company: customer.isCompany, - name: customer.name, - trade_name: customer.tradeName ?? "", - tin: customer.tin.toPrimitive(), + name: customer.name.toPrimitive(), + trade_name: customer.tradeName.match( + (value) => value.toPrimitive(), + () => "" + ), + tin: customer.tin.match( + (value) => value.toPrimitive(), + () => "" + ), - street: address.street, - city: address.city, - state: address.state, - postal_code: address.postalCode, - country: address.country, + street: address.street.match( + (value) => value.toPrimitive(), + () => "" + ), + city: address.city.match( + (value) => value.toPrimitive(), + () => "" + ), + state: address.province.match( + (value) => value.toPrimitive(), + () => "" + ), + postal_code: address.postalCode.match( + (value) => value.toPrimitive(), + () => "" + ), + country: address.country.match( + (value) => value.toPrimitive(), + () => "" + ), - email: customer.email.toPrimitive(), - phone: customer.phone.toPrimitive(), - fax: customer.fax.toPrimitive(), - website: customer.website ?? "", + email: customer.email.match( + (value) => value.toPrimitive(), + () => "" + ), + phone: customer.phone.match( + (value) => value.toPrimitive(), + () => "" + ), + fax: customer.fax.match( + (value) => value.toPrimitive(), + () => "" + ), + website: customer.website.match( + (value) => value.toPrimitive(), + () => "" + ), - legal_record: customer.legalRecord, + legal_record: customer.legalRecord.match( + (value) => value.toPrimitive(), + () => "" + ), + + default_taxes: customer.defaultTaxes.map((item) => item.toPrimitive()), - default_tax: customer.defaultTax, status: customer.isActive ? "active" : "inactive", - lang_code: customer.langCode, - currency_code: customer.currencyCode, + language_code: customer.languageCode.toPrimitive(), + currency_code: customer.currencyCode.toPrimitive(), metadata: { entity: "customer", diff --git a/modules/customers/src/api/application/list-customers/list-customers.use-case.ts b/modules/customers/src/api/application/list-customers/list-customers.use-case.ts index 371c00bd..2967eda4 100644 --- a/modules/customers/src/api/application/list-customers/list-customers.use-case.ts +++ b/modules/customers/src/api/application/list-customers/list-customers.use-case.ts @@ -1,9 +1,10 @@ import { ITransactionManager } from "@erp/core/api"; import { Criteria } from "@repo/rdx-criteria/server"; +import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; import { CustomerListResponsetDTO } from "../../../common/dto"; -import { ICustomerService } from "../../domain"; +import { CustomerService } from "../../domain"; import { ListCustomersAssembler } from "./assembler"; type ListCustomersUseCaseInput = { @@ -13,7 +14,7 @@ type ListCustomersUseCaseInput = { export class ListCustomersUseCase { constructor( - private readonly customerService: ICustomerService, + private readonly customerService: CustomerService, private readonly transactionManager: ITransactionManager, private readonly assembler: ListCustomersAssembler ) {} diff --git a/modules/customers/src/api/application/services/index.ts b/modules/customers/src/api/application/services/index.ts deleted file mode 100644 index 4510ef9a..00000000 --- a/modules/customers/src/api/application/services/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -//export * from "./participantAddressFinder"; -//export * from "./participantFinder"; diff --git a/modules/customers/src/api/application/services/participantAddressFinder.ts b/modules/customers/src/api/application/services/participantAddressFinder.ts deleted file mode 100644 index 3265aa7a..00000000 --- a/modules/customers/src/api/application/services/participantAddressFinder.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* import { - ApplicationServiceError, - type IApplicationServiceError, -} from "@/contexts/common/application/services/ApplicationServiceError"; -import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain"; -import { Result, UniqueID } from "@shared/contexts"; -import { NullOr } from "@shared/utilities"; -import { ICustomerParticipantAddress, ICustomerParticipantAddressRepository } from "../../domain"; - -export const participantAddressFinder = async ( - addressId: UniqueID, - adapter: IAdapter, - repository: RepositoryBuilder -) => { - if (addressId.isNull()) { - return Result.fail( - ApplicationServiceError.create( - ApplicationServiceError.INVALID_REQUEST_PARAM, - `Participant address ID required` - ) - ); - } - - const transaction = adapter.startTransaction(); - let address: NullOr = null; - - try { - await transaction.complete(async (t) => { - address = await repository({ transaction: t }).getById(addressId); - }); - - if (address === null) { - return Result.fail( - ApplicationServiceError.create(ApplicationServiceError.NOT_FOUND_ERROR, "", { - id: addressId.toString(), - entity: "participant address", - }) - ); - } - - return Result.ok(address); - } catch (error: unknown) { - const _error = error as Error; - - if (repository().isRepositoryError(_error)) { - return Result.fail( - ApplicationServiceError.create( - ApplicationServiceError.REPOSITORY_ERROR, - _error.message, - _error - ) - ); - } - - return Result.fail( - ApplicationServiceError.create( - ApplicationServiceError.UNEXCEPTED_ERROR, - _error.message, - _error - ) - ); - } -}; - */ diff --git a/modules/customers/src/api/application/services/participantFinder.ts b/modules/customers/src/api/application/services/participantFinder.ts deleted file mode 100644 index df238b2c..00000000 --- a/modules/customers/src/api/application/services/participantFinder.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain"; -import { UniqueID } from "@shared/contexts"; -import { ICustomerParticipantRepository } from "../../domain"; -import { CustomerCustomer } from "../../domain/entities/customer-customer/customer-customer"; - -export const participantFinder = async ( - participantId: UniqueID, - adapter: IAdapter, - repository: RepositoryBuilder -): Promise => { - if (!participantId || (participantId && participantId.isNull())) { - return Promise.resolve(undefined); - } - - const participant = await adapter - .startTransaction() - .complete((t) => repository({ transaction: t }).getById(participantId)); - - return Promise.resolve(participant ? participant : undefined); -}; - */ diff --git a/modules/customers/src/api/application/update-customer/assembler/index.ts b/modules/customers/src/api/application/update-customer/assembler/index.ts new file mode 100644 index 00000000..a659dbae --- /dev/null +++ b/modules/customers/src/api/application/update-customer/assembler/index.ts @@ -0,0 +1 @@ +export * from "./update-customer.assembler"; diff --git a/modules/customers/src/api/application/update-customer/assembler/update-customer.assembler.ts b/modules/customers/src/api/application/update-customer/assembler/update-customer.assembler.ts new file mode 100644 index 00000000..e3004657 --- /dev/null +++ b/modules/customers/src/api/application/update-customer/assembler/update-customer.assembler.ts @@ -0,0 +1,83 @@ +import { GetCustomerByIdResponseDTO as UpdateCustomerByIdResponseDTO } from "../../../../common/dto"; +import { Customer } from "../../../domain"; + +export class UpdateCustomerAssembler { + toDTO(customer: Customer): UpdateCustomerByIdResponseDTO { + const address = customer.address.toPrimitive(); + + return { + id: customer.id.toPrimitive(), + reference: customer.reference.match( + (value) => value.toPrimitive(), + () => "" + ), + + is_company: customer.isCompany, + name: customer.name.toPrimitive(), + trade_name: customer.tradeName.match( + (value) => value.toPrimitive(), + () => "" + ), + tin: customer.tin.match( + (value) => value.toPrimitive(), + () => "" + ), + + street: address.street.match( + (value) => value.toPrimitive(), + () => "" + ), + city: address.city.match( + (value) => value.toPrimitive(), + () => "" + ), + state: address.province.match( + (value) => value.toPrimitive(), + () => "" + ), + postal_code: address.postalCode.match( + (value) => value.toPrimitive(), + () => "" + ), + country: address.country.match( + (value) => value.toPrimitive(), + () => "" + ), + + email: customer.email.match( + (value) => value.toPrimitive(), + () => "" + ), + phone: customer.phone.match( + (value) => value.toPrimitive(), + () => "" + ), + fax: customer.fax.match( + (value) => value.toPrimitive(), + () => "" + ), + website: customer.website.match( + (value) => value.toPrimitive(), + () => "" + ), + + legal_record: customer.legalRecord.match( + (value) => value.toPrimitive(), + () => "" + ), + + default_taxes: customer.defaultTaxes.map((item) => item.toPrimitive()), + + status: customer.isActive ? "active" : "inactive", + language_code: customer.languageCode.toPrimitive(), + currency_code: customer.currencyCode.toPrimitive(), + + metadata: { + entity: "customer", + id: customer.id.toPrimitive(), + //created_at: customer.createdAt.toPrimitive(), + //updated_at: customer.updatedAt.toPrimitive() + }, + }; + } +} diff --git a/modules/customers/src/api/application/update-customer/index.ts b/modules/customers/src/api/application/update-customer/index.ts index db3e8d69..e236da9c 100644 --- a/modules/customers/src/api/application/update-customer/index.ts +++ b/modules/customers/src/api/application/update-customer/index.ts @@ -1 +1,2 @@ +export * from "./assembler"; export * from "./update-customer.use-case"; diff --git a/modules/customers/src/api/application/update-customer/map-dto-to-update-customer-props.ts b/modules/customers/src/api/application/update-customer/map-dto-to-update-customer-props.ts new file mode 100644 index 00000000..7de09ef1 --- /dev/null +++ b/modules/customers/src/api/application/update-customer/map-dto-to-update-customer-props.ts @@ -0,0 +1,245 @@ +import { + DomainError, + ValidationErrorCollection, + ValidationErrorDetail, + extractOrPushError, +} from "@erp/core/api"; +import { + City, + Country, + CurrencyCode, + EmailAddress, + LanguageCode, + Name, + PhoneNumber, + PostalAddressPatchProps, + PostalCode, + Province, + Street, + TINNumber, + TaxCode, + TextValue, + URLAddress, + maybeFromNullableVO, +} from "@repo/rdx-ddd"; +import { Collection, Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils"; +import { UpdateCustomerRequestDTO } from "../../../common/dto"; +import { CustomerPatchProps } from "../../domain"; + +/** + * mapDTOToUpdateCustomerPatchProps + * Convierte el DTO a las props validadas (CustomerProps). + * No construye directamente el agregado. + * Tri-estado: + * - campo omitido → no se cambia + * - campo con valor null/"" → set(None()), + * - campo con valor no-vacío → set(Some(VO)). + * + * @param dto - DTO con los datos a cambiar en el cliente + * @returns Cambios en las propiedades del cliente + * + */ + +export function mapDTOToUpdateCustomerPatchProps(dto: UpdateCustomerRequestDTO) { + try { + const errors: ValidationErrorDetail[] = []; + const customerPatchProps: CustomerPatchProps = {}; + + toPatchField(dto.reference).ifSet((reference) => { + customerPatchProps.reference = extractOrPushError( + maybeFromNullableVO(reference, (value) => Name.create(value)), + "reference", + errors + ); + }); + + toPatchField(dto.is_company).ifSet((is_company) => { + if (isNullishOrEmpty(is_company)) { + errors.push({ path: "is_company", message: "is_company cannot be empty" }); + return; + } + customerPatchProps.isCompany = extractOrPushError( + Result.ok(Boolean(is_company!)), + "is_company", + errors + ); + }); + + toPatchField(dto.name).ifSet((name) => { + if (isNullishOrEmpty(name)) { + errors.push({ path: "name", message: "Name cannot be empty" }); + return; + } + customerPatchProps.name = extractOrPushError(Name.create(name!), "name", errors); + }); + + toPatchField(dto.trade_name).ifSet((trade_name) => { + customerPatchProps.tradeName = extractOrPushError( + maybeFromNullableVO(trade_name, (value) => Name.create(value)), + "trade_name", + errors + ); + }); + + toPatchField(dto.tin).ifSet((tin) => { + customerPatchProps.tin = extractOrPushError( + maybeFromNullableVO(tin, (value) => TINNumber.create(value)), + "tin", + errors + ); + }); + + toPatchField(dto.email).ifSet((email) => { + customerPatchProps.email = extractOrPushError( + maybeFromNullableVO(email, (value) => EmailAddress.create(value)), + "email", + errors + ); + }); + + toPatchField(dto.phone).ifSet((phone) => { + customerPatchProps.phone = extractOrPushError( + maybeFromNullableVO(phone, (value) => PhoneNumber.create(value)), + "phone", + errors + ); + }); + + toPatchField(dto.fax).ifSet((fax) => { + customerPatchProps.fax = extractOrPushError( + maybeFromNullableVO(fax, (value) => PhoneNumber.create(value)), + "fax", + errors + ); + }); + + toPatchField(dto.website).ifSet((website) => { + customerPatchProps.website = extractOrPushError( + maybeFromNullableVO(website, (value) => URLAddress.create(value)), + "website", + errors + ); + }); + + toPatchField(dto.legal_record).ifSet((legalRecord) => { + customerPatchProps.legalRecord = extractOrPushError( + maybeFromNullableVO(legalRecord, (value) => TextValue.create(value)), + "legal_record", + errors + ); + }); + + toPatchField(dto.language_code).ifSet((languageCode) => { + if (isNullishOrEmpty(languageCode)) { + errors.push({ path: "language_code", message: "Language code cannot be empty" }); + return; + } + + customerPatchProps.languageCode = extractOrPushError( + LanguageCode.create(languageCode!), + "language_code", + errors + ); + }); + + toPatchField(dto.currency_code).ifSet((currencyCode) => { + if (isNullishOrEmpty(currencyCode)) { + errors.push({ path: "currency_code", message: "Currency code cannot be empty" }); + return; + } + + customerPatchProps.currencyCode = extractOrPushError( + CurrencyCode.create(currencyCode!), + "currency_code", + errors + ); + }); + + // Default taxes + const defaultTaxesCollection = new Collection(); + toPatchField(dto.default_taxes).ifSet((defaultTaxes) => { + customerPatchProps.defaultTaxes = defaultTaxesCollection; + + if (isNullishOrEmpty(defaultTaxes)) { + return; + } + + defaultTaxes!.map((taxCode, index) => { + const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors); + if (tax && customerPatchProps.defaultTaxes) { + customerPatchProps.defaultTaxes.add(tax); + } + }); + }); + + // PostalAddress + customerPatchProps.address = mapDTOToUpdatePostalAddressPatchProps(dto, errors); + + if (errors.length > 0) { + console.error(errors); + return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors)); + } + + return Result.ok(customerPatchProps); + } catch (err: unknown) { + console.error(err); + return Result.fail(new DomainError("Customer props mapping failed", { cause: err })); + } +} + +function mapDTOToUpdatePostalAddressPatchProps( + dto: UpdateCustomerRequestDTO, + errors: ValidationErrorDetail[] +): PostalAddressPatchProps { + const postalAddressPatchProps: PostalAddressPatchProps = {}; + + toPatchField(dto.street).ifSet((street) => { + postalAddressPatchProps.street = extractOrPushError( + maybeFromNullableVO(street, (value) => Street.create(value)), + "street", + errors + ); + }); + + toPatchField(dto.street2).ifSet((street2) => { + postalAddressPatchProps.street2 = extractOrPushError( + maybeFromNullableVO(street2, (value) => Street.create(value)), + "street2", + errors + ); + }); + + toPatchField(dto.city).ifSet((city) => { + postalAddressPatchProps.city = extractOrPushError( + maybeFromNullableVO(city, (value) => City.create(value)), + "city", + errors + ); + }); + + toPatchField(dto.province).ifSet((province) => { + postalAddressPatchProps.province = extractOrPushError( + maybeFromNullableVO(province, (value) => Province.create(value)), + "province", + errors + ); + }); + + toPatchField(dto.postal_code).ifSet((postalCode) => { + postalAddressPatchProps.postalCode = extractOrPushError( + maybeFromNullableVO(postalCode, (value) => PostalCode.create(value)), + "postal_code", + errors + ); + }); + + toPatchField(dto.country).ifSet((country) => { + postalAddressPatchProps.country = extractOrPushError( + maybeFromNullableVO(country, (value) => Country.create(value)), + "country", + errors + ); + }); + + return postalAddressPatchProps; +} diff --git a/modules/customers/src/api/application/update-customer/update-customer.use-case.ts b/modules/customers/src/api/application/update-customer/update-customer.use-case.ts index 5f2bf96d..1fefb509 100644 --- a/modules/customers/src/api/application/update-customer/update-customer.use-case.ts +++ b/modules/customers/src/api/application/update-customer/update-customer.use-case.ts @@ -1,401 +1,62 @@ -import { UniqueID } from "@/core/common/domain"; -import { ITransactionManager } from "@/core/common/infrastructure/database"; +import { ITransactionManager } from "@erp/core/api"; +import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { IUpdateCustomerRequestDTO } from "../../common/dto"; -import { Customer, ICustomerService } from "../domain"; +import { UpdateCustomerRequestDTO } from "../../../common"; +import { CustomerPatchProps, CustomerService } from "../../domain"; +import { UpdateCustomerAssembler } from "./assembler"; +import { mapDTOToUpdateCustomerPatchProps } from "./map-dto-to-update-customer-props"; -export class CreateCustomerUseCase { +type UpdateCustomerUseCaseInput = { + companyId: UniqueID; + id: string; + dto: UpdateCustomerRequestDTO; +}; + +export class UpdateCustomerUseCase { constructor( - private readonly customerService: ICustomerService, - private readonly transactionManager: ITransactionManager + private readonly service: CustomerService, + private readonly transactionManager: ITransactionManager, + private readonly assembler: UpdateCustomerAssembler ) {} - public execute( - customerID: UniqueID, - dto: Partial - ): Promise> { + public execute(params: UpdateCustomerUseCaseInput) { + const { companyId, id, dto } = params; + + const idOrError = UniqueID.create(id); + if (idOrError.isFailure) { + return Result.fail(idOrError.error); + } + + const customerId = idOrError.data; + + // Mapear DTO → props de dominio + const patchPropsResult = mapDTOToUpdateCustomerPatchProps(dto); + if (patchPropsResult.isFailure) { + return Result.fail(patchPropsResult.error); + } + + const patchProps: CustomerPatchProps = patchPropsResult.data; + return this.transactionManager.complete(async (transaction) => { - return Result.fail(new Error("No implementado")); - /* try { - const validOrErrors = this.validateCustomerData(dto); - if (validOrErrors.isFailure) { - return Result.fail(validOrErrors.error); + const updatedCustomer = await this.service.updateCustomerByIdInCompany( + companyId, + customerId, + patchProps, + transaction + ); + + if (updatedCustomer.isFailure) { + return Result.fail(updatedCustomer.error); } - const data = validOrErrors.data; + const savedCustomer = await this.service.saveCustomer(updatedCustomer.data, transaction); - // Update customer with dto - return await this.customerService.updateCustomerById(customerID, data, transaction); + const getDTO = this.assembler.toDTO(savedCustomer.data); + return Result.ok(getDTO); } catch (error: unknown) { - logger.error(error as Error); return Result.fail(error as Error); } - */ }); } - - /* private validateCustomerData( - dto: Partial - ): Result, Error> { - const errors: Error[] = []; - const validatedData: Partial = {}; - - // Create customer - let customer_status = CustomerStatus.create(customerDTO.status).object; - if (customer_status.isEmpty()) { - customer_status = CustomerStatus.createDraft(); - } - - let customer_series = CustomerSeries.create(customerDTO.customer_series).object; - if (customer_series.isEmpty()) { - customer_series = CustomerSeries.create(customerDTO.customer_series).object; - } - - let issue_date = CustomerDate.create(customerDTO.issue_date).object; - if (issue_date.isEmpty()) { - issue_date = CustomerDate.createCurrentDate().object; - } - - let operation_date = CustomerDate.create(customerDTO.operation_date).object; - if (operation_date.isEmpty()) { - operation_date = CustomerDate.createCurrentDate().object; - } - - let customerCurrency = Currency.createFromCode(customerDTO.currency).object; - - if (customerCurrency.isEmpty()) { - customerCurrency = Currency.createDefaultCode().object; - } - - let customerLanguage = Language.createFromCode(customerDTO.language_code).object; - - if (customerLanguage.isEmpty()) { - customerLanguage = Language.createDefaultCode().object; - } - - const items = new Collection( - customerDTO.items?.map( - (item) => - CustomerSimpleItem.create({ - description: Description.create(item.description).object, - quantity: Quantity.create(item.quantity).object, - unitPrice: UnitPrice.create({ - amount: item.unit_price.amount, - currencyCode: item.unit_price.currency, - precision: item.unit_price.precision, - }).object, - }).object - ) - ); - - if (!customer_status.isDraft()) { - throw Error("Error al crear una factura que no es borrador"); - } - - return DraftCustomer.create( - { - customerSeries: customer_series, - issueDate: issue_date, - operationDate: operation_date, - customerCurrency, - language: customerLanguage, - customerNumber: CustomerNumber.create(undefined).object, - //notes: Note.create(customerDTO.notes).object, - - //senderId: UniqueID.create(null).object, - recipient, - - items, - }, - customerId - ); - } */ } - -/* export type UpdateCustomerResponseOrError = - | Result // Misc errors (value objects) - | Result; // Success! - -export class UpdateCustomerUseCase2 - implements - IUseCase<{ id: UniqueID; data: IUpdateCustomer_DTO }, Promise> -{ - private _context: IInvoicingContext; - private _adapter: ISequelizeAdapter; - private _repositoryManager: IRepositoryManager; - - constructor(context: IInvoicingContext) { - this._context = context; - this._adapter = context.adapter; - this._repositoryManager = context.repositoryManager; - } - - private getRepository(name: string) { - return this._repositoryManager.getRepository(name); - } - - private handleValidationFailure( - validationError: Error, - message?: string - ): Result { - return Result.fail( - UseCaseError.create( - UseCaseError.INVALID_INPUT_DATA, - message ? message : validationError.message, - validationError - ) - ); - } - - async execute(request: { - id: UniqueID; - data: IUpdateCustomer_DTO; - }): Promise { - const { id, data: customerDTO } = request; - - // Validaciones - const customerDTOOrError = ensureUpdateCustomer_DTOIsValid(customerDTO); - if (customerDTOOrError.isFailure) { - return this.handleValidationFailure(customerDTOOrError.error); - } - - const transaction = this._adapter.startTransaction(); - - const customerRepoBuilder = this.getRepository("Customer"); - - let customer: Customer | null = null; - - try { - await transaction.complete(async (t) => { - customer = await customerRepoBuilder({ transaction: t }).getById(id); - }); - - if (customer === null) { - return Result.fail( - UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, `Customer not found`, { - id: request.id.toString(), - entity: "customer", - }) - ); - } - - return Result.ok(customer); - } catch (error: unknown) { - const _error = error as Error; - if (customerRepoBuilder().isRepositoryError(_error)) { - return this.handleRepositoryError(error as BaseError, customerRepoBuilder()); - } else { - return this.handleUnexceptedError(error); - } - } - - // Recipient validations - const recipientIdOrError = ensureParticipantIdIsValid( - customerDTO?.recipient?.id, - ); - if (recipientIdOrError.isFailure) { - return this.handleValidationFailure( - recipientIdOrError.error, - "Recipient ID not valid", - ); - } - const recipientId = recipientIdOrError.object; - - const recipientBillingIdOrError = ensureParticipantAddressIdIsValid( - customerDTO?.recipient?.billing_address_id, - ); - if (recipientBillingIdOrError.isFailure) { - return this.handleValidationFailure( - recipientBillingIdOrError.error, - "Recipient billing address ID not valid", - ); - } - const recipientBillingId = recipientBillingIdOrError.object; - - const recipientShippingIdOrError = ensureParticipantAddressIdIsValid( - customerDTO?.recipient?.shipping_address_id, - ); - if (recipientShippingIdOrError.isFailure) { - return this.handleValidationFailure( - recipientShippingIdOrError.error, - "Recipient shipping address ID not valid", - ); - } - const recipientShippingId = recipientShippingIdOrError.object; - - const recipientContact = await this.findContact( - recipientId, - recipientBillingId, - recipientShippingId, - ); - - if (!recipientContact) { - return this.handleValidationFailure( - new Error(`Recipient with ID ${recipientId.toString()} does not exist`), - ); - } - - // Crear customer - const customerOrError = await this.tryUpdateCustomerInstance( - customerDTO, - customerIdOrError.object, - //senderId, - //senderBillingId, - //senderShippingId, - recipientContact, - ); - - if (customerOrError.isFailure) { - const { error: domainError } = customerOrError; - let errorCode = ""; - let message = ""; - - switch (domainError.code) { - case Customer.ERROR_CUSTOMER_WITHOUT_NAME: - errorCode = UseCaseError.INVALID_INPUT_DATA; - message = - "El cliente debe ser una compañía o tener nombre y apellidos."; - break; - - default: - errorCode = UseCaseError.UNEXCEPTED_ERROR; - message = ""; - break; - } - - return Result.fail( - UseCaseError.create(errorCode, message, domainError), - ); - } - - return this.saveCustomer(customerOrError.object); - - } - - private async tryUpdateCustomerInstance(customerDTO, customerId, recipient) { - // Create customer - let customer_status = CustomerStatus.create(customerDTO.status).object; - if (customer_status.isEmpty()) { - customer_status = CustomerStatus.createDraft(); - } - - let customer_series = CustomerSeries.create(customerDTO.customer_series).object; - if (customer_series.isEmpty()) { - customer_series = CustomerSeries.create(customerDTO.customer_series).object; - } - - let issue_date = CustomerDate.create(customerDTO.issue_date).object; - if (issue_date.isEmpty()) { - issue_date = CustomerDate.createCurrentDate().object; - } - - let operation_date = CustomerDate.create(customerDTO.operation_date).object; - if (operation_date.isEmpty()) { - operation_date = CustomerDate.createCurrentDate().object; - } - - let customerCurrency = Currency.createFromCode(customerDTO.currency).object; - - if (customerCurrency.isEmpty()) { - customerCurrency = Currency.createDefaultCode().object; - } - - let customerLanguage = Language.createFromCode(customerDTO.language_code).object; - - if (customerLanguage.isEmpty()) { - customerLanguage = Language.createDefaultCode().object; - } - - const items = new Collection( - customerDTO.items?.map( - (item) => - CustomerSimpleItem.create({ - description: Description.create(item.description).object, - quantity: Quantity.create(item.quantity).object, - unitPrice: UnitPrice.create({ - amount: item.unit_price.amount, - currencyCode: item.unit_price.currency, - precision: item.unit_price.precision, - }).object, - }).object - ) - ); - - if (!customer_status.isDraft()) { - throw Error("Error al crear una factura que no es borrador"); - } - - return DraftCustomer.create( - { - customerSeries: customer_series, - issueDate: issue_date, - operationDate: operation_date, - customerCurrency, - language: customerLanguage, - customerNumber: CustomerNumber.create(undefined).object, - //notes: Note.create(customerDTO.notes).object, - - //senderId: UniqueID.create(null).object, - recipient, - - items, - }, - customerId - ); - } - - private async findContact( - contactId: UniqueID, - billingAddressId: UniqueID, - shippingAddressId: UniqueID - ) { - const contactRepoBuilder = this.getRepository("Contact"); - - const contact = await contactRepoBuilder().getById2( - contactId, - billingAddressId, - shippingAddressId - ); - - return contact; - } - - private async saveCustomer(customer: DraftCustomer) { - const transaction = this._adapter.startTransaction(); - const customerRepoBuilder = this.getRepository("Customer"); - - try { - await transaction.complete(async (t) => { - const customerRepo = customerRepoBuilder({ transaction: t }); - await customerRepo.save(customer); - }); - - return Result.ok(customer); - } catch (error: unknown) { - const _error = error as Error; - if (customerRepoBuilder().isRepositoryError(_error)) { - return this.handleRepositoryError(error as BaseError, customerRepoBuilder()); - } else { - return this.handleUnexceptedError(error); - } - } - } - - private handleUnexceptedError(error): Result { - return Result.fail( - UseCaseError.create(UseCaseError.UNEXCEPTED_ERROR, error.message, error) - ); - } - - private handleRepositoryError( - error: BaseError, - repository: ICustomerRepository - ): Result { - const { message, details } = repository.handleRepositoryError(error); - return Result.fail( - UseCaseError.create(UseCaseError.REPOSITORY_ERROR, message, details) - ); - } -} - */ diff --git a/modules/customers/src/api/domain/aggregates/customer.ts b/modules/customers/src/api/domain/aggregates/customer.ts index d8aa36e6..cc0e796c 100644 --- a/modules/customers/src/api/domain/aggregates/customer.ts +++ b/modules/customers/src/api/domain/aggregates/customer.ts @@ -1,66 +1,77 @@ import { AggregateRoot, + CurrencyCode, EmailAddress, + LanguageCode, + Name, PhoneNumber, PostalAddress, + PostalAddressPatchProps, + PostalAddressSnapshot, TINNumber, + TaxCode, + TextValue, + URLAddress, UniqueID, } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; +import { Collection, Maybe, Result } from "@repo/rdx-utils"; import { CustomerStatus } from "../value-objects"; export interface CustomerProps { companyId: UniqueID; status: CustomerStatus; - reference: string; + reference: Maybe; isCompany: boolean; - name: string; - tradeName: string; - tin: TINNumber; + name: Name; + tradeName: Maybe; + tin: Maybe; address: PostalAddress; - email: EmailAddress; - phone: PhoneNumber; - fax: PhoneNumber; - website: string; + email: Maybe; + phone: Maybe; + fax: Maybe; + website: Maybe; - legalRecord: string; - defaultTax: string[]; + legalRecord: Maybe; + defaultTaxes: Collection; - langCode: string; - currencyCode: string; + languageCode: LanguageCode; + currencyCode: CurrencyCode; } -export interface ICustomer { - id: UniqueID; - companyId: UniqueID; - reference: string; +export interface CustomerSnapshot { + id: string; + companyId: string; + status: string; + reference: string | null; - tin: TINNumber; - name: string; - tradeName: string; - - address: PostalAddress; - - email: EmailAddress; - phone: PhoneNumber; - fax: PhoneNumber; - website: string; - - legalRecord: string; - defaultTax: string[]; - - langCode: string; - currencyCode: string; - - isIndividual: boolean; isCompany: boolean; - isActive: boolean; + name: string; + tradeName: string | null; + + tin: string | null; + + address: PostalAddressSnapshot; // snapshot serializable del VO PostalAddress + + email: string | null; + phone: string | null; + fax: string | null; + website: string | null; + + legalRecord: string | null; + defaultTaxes: string[]; + + languageCode: string; + currencyCode: string; } -export class Customer extends AggregateRoot implements ICustomer { +export type CustomerPatchProps = Partial> & { + address?: PostalAddressPatchProps; +}; + +export class Customer extends AggregateRoot { static create(props: CustomerProps, id?: UniqueID): Result { const contact = new Customer(props, id); @@ -75,76 +86,122 @@ export class Customer extends AggregateRoot implements ICustomer return Result.ok(contact); } - update(partial: Partial>): Result { - const updatedCustomer = new Customer({ ...this.props, ...partial }, this.id); + public update(partial: CustomerPatchProps): Result { + const updatedProps = { + ...this.props, + ...partial, + } as CustomerProps; + + if (partial.address) { + const updatedAddressOrError = PostalAddress.update(this.props.address, partial.address); + if (updatedAddressOrError.isFailure) { + return Result.fail(updatedAddressOrError.error); + } + updatedProps.address = updatedAddressOrError.data; + } + + const updatedCustomer = new Customer(updatedProps, this.id); return Result.ok(updatedCustomer); } - get companyId(): UniqueID { - return this.props.companyId; + public toSnapshot(): CustomerSnapshot { + return { + id: this.id.toPrimitive(), + companyId: this.props.companyId.toPrimitive(), + status: this.props.status.toPrimitive(), + reference: this.props.reference.isSome() + ? this.props.reference.unwrap()!.toPrimitive() + : null, + + isCompany: this.props.isCompany, + name: this.props.name.toPrimitive(), + tradeName: this.props.tradeName.isSome() + ? this.props.tradeName.unwrap()!.toPrimitive() + : null, + tin: this.props.tin.isSome() ? this.props.tin.unwrap()!.toPrimitive() : null, + + address: this.props.address.toSnapshot(), + + email: this.props.email.isSome() ? this.props.email.unwrap()!.toPrimitive() : null, + phone: this.props.phone.isSome() ? this.props.phone.unwrap()!.toPrimitive() : null, + fax: this.props.fax.isSome() ? this.props.fax.unwrap()!.toPrimitive() : null, + website: this.props.website.isSome() ? this.props.website.unwrap()!.toPrimitive() : null, + + legalRecord: this.props.legalRecord.isSome() + ? this.props.legalRecord.unwrap()!.toPrimitive() + : null, + defaultTaxes: this.props.defaultTaxes.map((tax) => tax.toPrimitive()), + + languageCode: this.props.languageCode.toPrimitive(), + currencyCode: this.props.currencyCode.toPrimitive(), + }; } - get reference() { - return this.props.reference; - } - - get name() { - return this.props.name; - } - - get tradeName() { - return this.props.tradeName; - } - - get tin(): TINNumber { - return this.props.tin; - } - - get address(): PostalAddress { - return this.props.address; - } - - get email(): EmailAddress { - return this.props.email; - } - - get phone(): PhoneNumber { - return this.props.phone; - } - - get fax(): PhoneNumber { - return this.props.fax; - } - - get website(): string { - return this.props.website; - } - - get legalRecord() { - return this.props.legalRecord; - } - - get defaultTax() { - return this.props.defaultTax; - } - - get langCode() { - return this.props.langCode; - } - - get currencyCode() { - return this.props.currencyCode; - } - - get isIndividual(): boolean { + public get isIndividual(): boolean { return !this.props.isCompany; } - get isCompany(): boolean { + public get isCompany(): boolean { return this.props.isCompany; } - get isActive(): boolean { + public get isActive(): boolean { return this.props.status.isActive(); } + + public get companyId(): UniqueID { + return this.props.companyId; + } + + public get reference(): Maybe { + return this.props.reference; + } + + public get name(): Name { + return this.props.name; + } + + public get tradeName(): Maybe { + return this.props.tradeName; + } + + public get tin(): Maybe { + return this.props.tin; + } + + public get address(): PostalAddress { + return this.props.address; + } + + public get email(): Maybe { + return this.props.email; + } + + public get phone(): Maybe { + return this.props.phone; + } + + public get fax(): Maybe { + return this.props.fax; + } + + public get website(): Maybe { + return this.props.website; + } + + public get legalRecord(): Maybe { + return this.props.legalRecord; + } + + public get defaultTaxes(): Collection { + return this.props.defaultTaxes; + } + + public get languageCode(): LanguageCode { + return this.props.languageCode; + } + + public get currencyCode(): CurrencyCode { + return this.props.currencyCode; + } } diff --git a/modules/customers/src/api/domain/services/customer-service.interface.ts b/modules/customers/src/api/domain/services/customer-service.interface.ts deleted file mode 100644 index bcce2e0d..00000000 --- a/modules/customers/src/api/domain/services/customer-service.interface.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Criteria } from "@repo/rdx-criteria/server"; -import { UniqueID } from "@repo/rdx-ddd"; -import { Collection, Result } from "@repo/rdx-utils"; -import { Customer, CustomerProps } from "../aggregates"; - -export interface ICustomerService { - /** - * Construye un nuevo Customer validando todos sus value objects. - */ - buildCustomerInCompany( - companyId: UniqueID, - props: Omit, - customerId?: UniqueID - ): Result; - - /** - * Guarda un Customer (nuevo o modificado) en base de datos. - */ - saveCustomer(customer: Customer, transaction: any): Promise>; - - /** - * Comprueba si existe un Customer con ese ID en la empresa indicada. - */ - existsByIdInCompany( - companyId: UniqueID, - customerId: UniqueID, - transaction?: any - ): Promise>; - - /** - * Lista todos los customers que cumplan el criterio, dentro de una empresa. - */ - findCustomerByCriteriaInCompany( - companyId: UniqueID, - criteria: Criteria, - transaction?: any - ): Promise, Error>>; - - /** - * Recupera un Customer por su ID dentro de una empresa. - */ - getCustomerByIdInCompany( - companyId: UniqueID, - customerId: UniqueID, - transaction?: any - ): Promise>; - - /** - * Actualiza parcialmente los datos de un Customer. - */ - updateCustomerByIdInCompany( - companyId: UniqueID, - customerId: UniqueID, - partial: Partial>, - transaction?: any - ): Promise>; - - /** - * Elimina un Customer por ID dentro de una empresa. - */ - deleteCustomerByIdInCompany( - companyId: UniqueID, - customerId: UniqueID, - transaction?: any - ): Promise>; -} diff --git a/modules/customers/src/api/domain/services/customer.service.ts b/modules/customers/src/api/domain/services/customer.service.ts index 017b98b7..eefda4fc 100644 --- a/modules/customers/src/api/domain/services/customer.service.ts +++ b/modules/customers/src/api/domain/services/customer.service.ts @@ -1,11 +1,10 @@ import { Criteria } from "@repo/rdx-criteria/server"; import { UniqueID } from "@repo/rdx-ddd"; import { Collection, Result } from "@repo/rdx-utils"; -import { Customer, CustomerProps } from "../aggregates"; +import { Customer, CustomerPatchProps, CustomerProps } from "../aggregates"; import { ICustomerRepository } from "../repositories"; -import { ICustomerService } from "./customer-service.interface"; -export class CustomerService implements ICustomerService { +export class CustomerService { constructor(private readonly repository: ICustomerRepository) {} /** @@ -87,6 +86,7 @@ export class CustomerService implements ICustomerService { /** * Actualiza parcialmente un cliente existente con nuevos datos. + * No lo guarda en el repositorio. * * @param companyId - Identificador de la empresa a la que pertenece el cliente. * @param customerId - Identificador del cliente a actualizar. @@ -97,9 +97,9 @@ export class CustomerService implements ICustomerService { async updateCustomerByIdInCompany( companyId: UniqueID, customerId: UniqueID, - partial: Partial>, + partial: CustomerPatchProps, transaction?: any - ): Promise> { + ): Promise> { const customerResult = await this.getCustomerByIdInCompany(companyId, customerId, transaction); if (customerResult.isFailure) { @@ -113,7 +113,7 @@ export class CustomerService implements ICustomerService { return Result.fail(updatedCustomer.error); } - return this.saveCustomer(updatedCustomer.data, transaction); + return Result.ok(updatedCustomer.data); } /** diff --git a/modules/customers/src/api/domain/services/index.ts b/modules/customers/src/api/domain/services/index.ts index fd8abcbb..09df0d7c 100644 --- a/modules/customers/src/api/domain/services/index.ts +++ b/modules/customers/src/api/domain/services/index.ts @@ -1,2 +1 @@ -export * from "./customer-service.interface"; export * from "./customer.service"; diff --git a/modules/customers/src/api/helpers/index.ts b/modules/customers/src/api/helpers/index.ts index 2de24716..51a58083 100644 --- a/modules/customers/src/api/helpers/index.ts +++ b/modules/customers/src/api/helpers/index.ts @@ -1 +1 @@ -export * from "./map-dto-to-customer-props"; +export * from "../application/create-customer/map-dto-to-create-customer-props"; diff --git a/modules/customers/src/api/helpers/map-dto-to-customer-props.ts b/modules/customers/src/api/helpers/map-dto-to-customer-props.ts deleted file mode 100644 index 02e1dfce..00000000 --- a/modules/customers/src/api/helpers/map-dto-to-customer-props.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { - DomainError, - ValidationErrorCollection, - ValidationErrorDetail, - extractOrPushError, -} from "@erp/core/api"; -import { EmailAddress, PhoneNumber, PostalAddress, TINNumber, UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; -import { CreateCustomerRequestDTO } from "../../common/dto"; -import { CustomerProps, 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 mapDTOToCustomerProps(dto: CreateCustomerRequestDTO) { - try { - const errors: ValidationErrorDetail[] = []; - - const customerId = extractOrPushError(UniqueID.create(dto.id), "id", errors); - const companyId = extractOrPushError(UniqueID.create(dto.company_id), "company_id", errors); - const status = extractOrPushError(CustomerStatus.create(dto.status), "status", errors); - const reference = dto.reference?.trim() === "" ? undefined : dto.reference; - - const isCompany = dto.is_company ?? true; - const name = dto.name?.trim() === "" ? undefined : dto.name; - const tradeName = dto.trade_name?.trim() === "" ? undefined : dto.trade_name; - - const tinNumber = extractOrPushError(TINNumber.create(dto.tin), "tin", errors); - - const address = extractOrPushError( - PostalAddress.create({ - street: dto.street, - city: dto.city, - postalCode: dto.postal_code, - state: dto.state, - country: dto.country, - }), - "address", - errors - ); - - const emailAddress = extractOrPushError(EmailAddress.create(dto.email), "email", errors); - const phoneNumber = extractOrPushError(PhoneNumber.create(dto.phone), "phone", errors); - const faxNumber = extractOrPushError(PhoneNumber.create(dto.fax), "fax", errors); - const website = dto.website?.trim() === "" ? undefined : dto.website; - - const legalRecord = dto.legal_record?.trim() === "" ? undefined : dto.legal_record; - const langCode = dto.lang_code?.trim() === "" ? undefined : dto.lang_code; - const currencyCode = dto.currency_code?.trim() === "" ? undefined : dto.currency_code; - - if (errors.length > 0) { - console.error(errors); - return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors)); - } - - console.debug("Mapped customer props:", { - companyId, - status, - reference, - isCompany, - name, - tradeName, - tinNumber, - address, - emailAddress, - phoneNumber, - faxNumber, - website, - legalRecord, - langCode, - currencyCode, - }); - - const customerProps: CustomerProps = { - companyId: companyId!, - status: status!, - reference: reference!, - - isCompany: isCompany, - name: name!, - tradeName: tradeName!, - tin: tinNumber!, - - address: address!, - - email: emailAddress!, - phone: phoneNumber!, - fax: faxNumber!, - website: website!, - - legalRecord: legalRecord!, - defaultTax: [], - langCode: langCode!, - currencyCode: currencyCode!, - }; - - return Result.ok({ id: customerId!, props: customerProps }); - } catch (err: unknown) { - console.error(err); - return Result.fail(new DomainError("Customer props mapping failed", { cause: err })); - } -} diff --git a/modules/customers/src/api/infrastructure/dependencies.ts b/modules/customers/src/api/infrastructure/dependencies.ts index 99b6e7f8..77fa309a 100644 --- a/modules/customers/src/api/infrastructure/dependencies.ts +++ b/modules/customers/src/api/infrastructure/dependencies.ts @@ -9,8 +9,10 @@ import { GetCustomerUseCase, ListCustomersAssembler, ListCustomersUseCase, + UpdateCustomerAssembler, + UpdateCustomerUseCase, } from "../application"; -import { CustomerService, ICustomerService } from "../domain"; +import { CustomerService } from "../domain"; import { CustomerMapper } from "./mappers"; import { CustomerRepository } from "./sequelize"; @@ -18,18 +20,18 @@ type CustomerDeps = { transactionManager: SequelizeTransactionManager; repo: CustomerRepository; mapper: CustomerMapper; - service: ICustomerService; + service: CustomerService; assemblers: { list: ListCustomersAssembler; get: GetCustomerAssembler; create: CreateCustomersAssembler; - //update: UpdateCustomerAssembler; + update: UpdateCustomerAssembler; }; build: { list: () => ListCustomersUseCase; get: () => GetCustomerUseCase; create: () => CreateCustomerUseCase; - //update: () => UpdateCustomerUseCase; + update: () => UpdateCustomerUseCase; delete: () => DeleteCustomerUseCase; }; presenters: { @@ -39,7 +41,7 @@ type CustomerDeps = { let _repo: CustomerRepository | null = null; let _mapper: CustomerMapper | null = null; -let _service: ICustomerService | null = null; +let _service: CustomerService | null = null; let _assemblers: CustomerDeps["assemblers"] | null = null; export function getCustomerDependencies(params: ModuleParams): CustomerDeps { @@ -55,7 +57,7 @@ export function getCustomerDependencies(params: ModuleParams): CustomerDeps { list: new ListCustomersAssembler(), // transforma domain → ListDTO get: new GetCustomerAssembler(), // transforma domain → DetailDTO create: new CreateCustomersAssembler(), // transforma domain → CreatedDTO - //update: new UpdateCustomerAssembler(), // transforma domain -> UpdateDTO + update: new UpdateCustomerAssembler(), // transforma domain -> UpdateDTO }; } @@ -69,8 +71,7 @@ export function getCustomerDependencies(params: ModuleParams): CustomerDeps { list: () => new ListCustomersUseCase(_service!, transactionManager!, _assemblers!.list), get: () => new GetCustomerUseCase(_service!, transactionManager!, _assemblers!.get), create: () => new CreateCustomerUseCase(_service!, transactionManager!, _assemblers!.create), - /*update: () => - new UpdateCustomerUseCase(_service!, transactionManager!, _assemblers!.update),*/ + update: () => new UpdateCustomerUseCase(_service!, transactionManager!, _assemblers!.update), delete: () => new DeleteCustomerUseCase(_service!, transactionManager!), }, presenters: { diff --git a/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts index 31beb591..52262088 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts @@ -1,5 +1,5 @@ import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; -import { CreateCustomerRequestDTO } from "../../../../common/dto"; +import { UpdateCustomerRequestDTO } from "../../../../common/dto"; import { CreateCustomerUseCase } from "../../../application"; export class CreateCustomerController extends ExpressController { @@ -11,7 +11,7 @@ export class CreateCustomerController extends ExpressController { protected async executeImpl() { const companyId = this.getTenantId()!; // garantizado por tenantGuard - const dto = this.req.body as CreateCustomerRequestDTO; + const dto = this.req.body as UpdateCustomerRequestDTO; // Inyectar empresa del usuario autenticado (ownership) dto.company_id = companyId.toString(); diff --git a/modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts index b36ccc44..99c99796 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts @@ -9,10 +9,10 @@ export class DeleteCustomerController extends ExpressController { } async executeImpl(): Promise { - const tenantId = this.getTenantId()!; // garantizado por tenantGuard + const companyId = this.getTenantId()!; // garantizado por tenantGuard const { id } = this.req.params; - const result = await this.useCase.execute({ id, tenantId }); + const result = await this.useCase.execute({ id, companyId }); return result.match( (data) => this.ok(data), diff --git a/modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts index 4dc61859..f008cfbb 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts @@ -9,10 +9,12 @@ export class GetCustomerController extends ExpressController { } protected async executeImpl() { - const tenantId = this.getTenantId()!; // garantizado por tenantGuard + const companyId = this.getTenantId()!; // garantizado por tenantGuard const { id } = this.req.params; - const result = await this.useCase.execute({ id, tenantId }); + console.log(id); + + const result = await this.useCase.execute({ id, companyId }); return result.match( (data) => this.ok(data), diff --git a/modules/customers/src/api/infrastructure/express/controllers/update-customer.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/update-customer.controller.ts new file mode 100644 index 00000000..fde7ebe9 --- /dev/null +++ b/modules/customers/src/api/infrastructure/express/controllers/update-customer.controller.ts @@ -0,0 +1,24 @@ +import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { UpdateCustomerRequestDTO } from "../../../../common/dto"; +import { UpdateCustomerUseCase } from "../../../application"; + +export class UpdateCustomerController extends ExpressController { + public constructor(private readonly useCase: UpdateCustomerUseCase) { + super(); + // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query + this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); + } + + protected async executeImpl() { + const companyId = this.getTenantId()!; // garantizado por tenantGuard + const { id } = this.req.params; + const dto = this.req.body as UpdateCustomerRequestDTO; + + const result = await this.useCase.execute({ id, companyId, dto }); + + return result.match( + (data) => this.created(data), + (err) => this.handleError(err) + ); + } +} diff --git a/modules/customers/src/api/infrastructure/express/controllers/update-invoice.controller.ts.bak b/modules/customers/src/api/infrastructure/express/controllers/update-invoice.controller.ts.bak deleted file mode 100644 index 8f1a1552..00000000 --- a/modules/customers/src/api/infrastructure/express/controllers/update-invoice.controller.ts.bak +++ /dev/null @@ -1,72 +0,0 @@ -import { IInvoicingContext } from "#/server/intrastructure"; -import { ExpressController } from "@rdx/core"; -import { IUpdateCustomerPresenter } from "./presenter"; - -export class UpdateCustomerController extends ExpressController { - private useCase: UpdateCustomerUseCase2; - private presenter: IUpdateCustomerPresenter; - private context: IInvoicingContext; - - constructor( - props: { - useCase: UpdateCustomerUseCase; - presenter: IUpdateCustomerPresenter; - }, - context: IInvoicingContext - ) { - super(); - - const { useCase, presenter } = props; - this.useCase = useCase; - this.presenter = presenter; - this.context = context; - } - - async executeImpl(): Promise { - const { customerId } = this.req.params; - const request: IUpdateCustomer_DTO = this.req.body; - - if (RuleValidator.validate(RuleValidator.RULE_NOT_NULL_OR_UNDEFINED, customerId).isFailure) { - return this.invalidInputError("Customer Id param is required!"); - } - - const idOrError = UniqueID.create(customerId); - if (idOrError.isFailure) { - return this.invalidInputError("Invalid customer Id param!"); - } - - try { - const result = await this.useCase.execute({ - id: idOrError.object, - data: request, - }); - - if (result.isFailure) { - const { error } = result; - - switch (error.code) { - case UseCaseError.NOT_FOUND_ERROR: - return this.notFoundError("Customer not found", error); - - case UseCaseError.INVALID_INPUT_DATA: - return this.invalidInputError(error.message); - - case UseCaseError.UNEXCEPTED_ERROR: - return this.internalServerError(result.error.message, result.error); - - case UseCaseError.REPOSITORY_ERROR: - return this.conflictError(result.error, result.error.details); - - default: - return this.clientError(result.error.message); - } - } - - const customer = result.object; - - return this.ok(this.presenter.map(customer, this.context)); - } catch (e: unknown) { - return this.fail(e as IServerError); - } - } -} diff --git a/modules/customers/src/api/infrastructure/express/customers.routes.ts b/modules/customers/src/api/infrastructure/express/customers.routes.ts index 604f0b76..ef60013f 100644 --- a/modules/customers/src/api/infrastructure/express/customers.routes.ts +++ b/modules/customers/src/api/infrastructure/express/customers.routes.ts @@ -3,10 +3,10 @@ import { ILogger, ModuleParams, validateRequest } from "@erp/core/api"; import { Application, NextFunction, Request, Response, Router } from "express"; import { Sequelize } from "sequelize"; import { - CreateCustomerRequestSchema, CustomerListRequestSchema, DeleteCustomerByIdRequestSchema, GetCustomerByIdRequestSchema, + UpdateCustomerRequestSchema, } from "../../../common/dto"; import { getCustomerDependencies } from "../dependencies"; import { @@ -64,7 +64,7 @@ export const customersRouter = (params: ModuleParams) => { "/", //checkTabContext, - validateRequest(CreateCustomerRequestSchema), + validateRequest(UpdateCustomerRequestSchema), (req: Request, res: Response, next: NextFunction) => { const useCase = deps.build.create(); const controller = new CreateCustomerController(useCase); diff --git a/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts b/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts index 27df22fe..df219db3 100644 --- a/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts +++ b/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts @@ -6,8 +6,27 @@ import { ValidationErrorDetail, extractOrPushError, } from "@erp/core/api"; -import { EmailAddress, PhoneNumber, PostalAddress, TINNumber, UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; +import { + City, + Country, + CurrencyCode, + EmailAddress, + LanguageCode, + Name, + PhoneNumber, + PostalAddress, + PostalCode, + Province, + Street, + TINNumber, + TaxCode, + TextValue, + URLAddress, + UniqueID, + maybeFromNullableVO, + toNullable, +} from "@repo/rdx-ddd"; +import { Collection, Result, isNullishOrEmpty } from "@repo/rdx-utils"; import { Customer, CustomerProps, CustomerStatus } from "../../domain"; import { CustomerCreationAttributes, CustomerModel } from "../sequelize"; @@ -28,41 +47,140 @@ export class CustomerMapper "company_id", errors ); + + const isCompany = source.is_company; const status = extractOrPushError(CustomerStatus.create(source.status), "status", errors); - const reference = source.reference?.trim() === "" ? undefined : source.reference; - - const isCompany = source.is_company ?? true; - const name = source.name?.trim() === "" ? undefined : source.name; - const tradeName = source.trade_name?.trim() === "" ? undefined : source.trade_name; - - const tinNumber = extractOrPushError(TINNumber.create(source.tin), "tin", errors); - - const address = extractOrPushError( - PostalAddress.create({ - street: source.street, - city: source.city, - postalCode: source.postal_code, - state: source.state, - country: source.country, - }), - "address", + const reference = extractOrPushError( + maybeFromNullableVO(source.reference, (value) => Name.create(value)), + "reference", errors ); - const emailAddress = extractOrPushError(EmailAddress.create(source.email), "email", errors); - const phoneNumber = extractOrPushError(PhoneNumber.create(source.phone), "phone", errors); - const faxNumber = extractOrPushError(PhoneNumber.create(source.fax), "fax", errors); - const website = source.website?.trim() === "" ? undefined : source.website; + const name = extractOrPushError(Name.create(source.name), "name", errors); - const legalRecord = source.legal_record?.trim() === "" ? undefined : source.legal_record; - const langCode = source.lang_code?.trim() === "" ? undefined : source.lang_code; - const currencyCode = source.currency_code?.trim() === "" ? undefined : source.currency_code; + const tradeName = extractOrPushError( + maybeFromNullableVO(source.trade_name, (value) => Name.create(value)), + "trade_name", + errors + ); + + const tinNumber = extractOrPushError( + maybeFromNullableVO(source.tin, (value) => TINNumber.create(value)), + "tin", + errors + ); + + const street = extractOrPushError( + maybeFromNullableVO(source.street, (value) => Street.create(value)), + "street", + errors + ); + + const street2 = extractOrPushError( + maybeFromNullableVO(source.street2, (value) => Street.create(value)), + "street2", + errors + ); + + const city = extractOrPushError( + maybeFromNullableVO(source.city, (value) => City.create(value)), + "city", + errors + ); + + const province = extractOrPushError( + maybeFromNullableVO(source.province, (value) => Province.create(value)), + "province", + errors + ); + + const postalCode = extractOrPushError( + maybeFromNullableVO(source.postal_code, (value) => PostalCode.create(value)), + "postal_code", + errors + ); + + const country = extractOrPushError( + maybeFromNullableVO(source.country, (value) => Country.create(value)), + "country", + errors + ); + + const emailAddress = extractOrPushError( + maybeFromNullableVO(source.email, (value) => EmailAddress.create(value)), + "email", + errors + ); + + const phoneNumber = extractOrPushError( + maybeFromNullableVO(source.phone, (value) => PhoneNumber.create(value)), + "phone", + errors + ); + + const faxNumber = extractOrPushError( + maybeFromNullableVO(source.fax, (value) => PhoneNumber.create(value)), + "fax", + errors + ); + + const website = extractOrPushError( + maybeFromNullableVO(source.website, (value) => URLAddress.create(value)), + "website", + errors + ); + + const legalRecord = extractOrPushError( + maybeFromNullableVO(source.legal_record, (value) => TextValue.create(value)), + "legal_record", + errors + ); + + const languageCode = extractOrPushError( + LanguageCode.create(source.language_code), + "language_code", + errors + ); + + const currencyCode = extractOrPushError( + CurrencyCode.create(source.currency_code), + "currency_code", + errors + ); + + // source.default_taxes is stored as a comma-separated string + const defaultTaxes = new Collection(); + if (!isNullishOrEmpty(source.default_taxes)) { + source.default_taxes.split(",").map((taxCode, index) => { + const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors); + if (tax) { + defaultTaxes.add(tax!); + } + }); + } if (errors.length > 0) { console.error(errors); return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors)); } + const postalAddressProps = { + street: street!, + street2: street2!, + city: city!, + postalCode: postalCode!, + province: province!, + country: country!, + }; + + console.log(postalAddressProps); + + const postalAddress = extractOrPushError( + PostalAddress.create(postalAddressProps), + "address", + errors + ); + const customerProps: CustomerProps = { companyId: companyId!, status: status!, @@ -73,7 +191,7 @@ export class CustomerMapper tradeName: tradeName!, tin: tinNumber!, - address: address!, + address: postalAddress!, email: emailAddress!, phone: phoneNumber!, @@ -81,8 +199,8 @@ export class CustomerMapper website: website!, legalRecord: legalRecord!, - defaultTax: [], - langCode: langCode!, + defaultTaxes: defaultTaxes!, + languageCode: languageCode!, currencyCode: currencyCode!, }; @@ -96,30 +214,38 @@ export class CustomerMapper return { id: source.id.toPrimitive(), company_id: source.companyId.toPrimitive(), - reference: source.reference, + + reference: source.reference.match( + (value) => value.toPrimitive(), + () => "" + ), + is_company: source.isCompany, - name: source.name, - trade_name: source.tradeName, - tin: source.tin.toPrimitive(), + name: source.name.toPrimitive(), + trade_name: toNullable(source.tradeName, (trade_name) => trade_name.toPrimitive()), + tin: toNullable(source.tin, (tin) => tin.toPrimitive()), - email: source.email.toPrimitive(), - phone: source.phone.toPrimitive(), - fax: source.fax.toPrimitive(), - website: source.website, + street: toNullable(source.address.street, (street) => street.toPrimitive()), + street2: toNullable(source.address.street2, (street2) => street2.toPrimitive()), + city: toNullable(source.address.city, (city) => city.toPrimitive()), + province: toNullable(source.address.province, (province) => province.toPrimitive()), + postal_code: toNullable(source.address.postalCode, (postal_code) => + postal_code.toPrimitive() + ), + country: toNullable(source.address.country, (country) => country.toPrimitive()), - default_tax: source.defaultTax.toString(), - legal_record: source.legalRecord, - lang_code: source.langCode, - currency_code: source.currencyCode, + email: toNullable(source.email, (email) => email.toPrimitive()), + phone: toNullable(source.phone, (phone) => phone.toPrimitive()), + fax: toNullable(source.fax, (fax) => fax.toPrimitive()), + website: toNullable(source.website, (website) => website.toPrimitive()), + + legal_record: toNullable(source.legalRecord, (legal_record) => legal_record.toPrimitive()), + + default_taxes: source.defaultTaxes.map((item) => item.toPrimitive()).join(", "), status: source.isActive ? "active" : "inactive", - - street: source.address.street, - street2: source.address.street2, - city: source.address.city, - state: source.address.state, - postal_code: source.address.postalCode, - country: source.address.country, + language_code: source.languageCode.toPrimitive(), + currency_code: source.currencyCode.toPrimitive(), }; } } diff --git a/modules/customers/src/api/infrastructure/sequelize/customer.model.ts b/modules/customers/src/api/infrastructure/sequelize/customer.model.ts index 12d0cc68..59a0e435 100644 --- a/modules/customers/src/api/infrastructure/sequelize/customer.model.ts +++ b/modules/customers/src/api/infrastructure/sequelize/customer.model.ts @@ -23,7 +23,7 @@ export class CustomerModel extends Model< declare street: string; declare street2: string; declare city: string; - declare state: string; + declare province: string; declare postal_code: string; declare country: string; @@ -34,9 +34,9 @@ export class CustomerModel extends Model< declare legal_record: string; - declare default_tax: string; + declare default_taxes: string; declare status: string; - declare lang_code: string; + declare language_code: string; declare currency_code: string; static associate(database: Sequelize) {} @@ -95,7 +95,7 @@ export default (database: Sequelize) => { allowNull: false, defaultValue: "", }, - state: { + province: { type: DataTypes.STRING, allowNull: false, defaultValue: "", @@ -143,13 +143,13 @@ export default (database: Sequelize) => { defaultValue: "", }, - default_tax: { + default_taxes: { type: DataTypes.STRING, - allowNull: false, - defaultValue: "", + allowNull: true, + defaultValue: null, }, - lang_code: { + language_code: { type: DataTypes.STRING(2), allowNull: false, defaultValue: "es", @@ -180,6 +180,7 @@ export default (database: Sequelize) => { indexes: [ { name: "company_idx", fields: ["company_id"], unique: false }, + { name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, { name: "email_idx", fields: ["email"], unique: true }, ], diff --git a/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts b/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts index 70dcd58b..d781afd2 100644 --- a/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts +++ b/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts @@ -1,4 +1,4 @@ -import { SequelizeRepository, translateSequelizeError } from "@erp/core/api"; +import { EntityNotFoundError, SequelizeRepository, translateSequelizeError } from "@erp/core/api"; import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; import { UniqueID } from "@repo/rdx-ddd"; import { Collection, Result } from "@repo/rdx-utils"; @@ -89,10 +89,12 @@ export class CustomerRepository }); if (!row) { - return Result.fail(new Error(`Customer ${id.toString()} not found`)); + return Result.fail(new EntityNotFoundError("Customer", "id", id.toString())); } - return this.mapper.mapToDomain(row); + const customer = this.mapper.mapToDomain(row); + console.log(customer); + return customer; } catch (error: any) { return Result.fail(translateSequelizeError(error)); } diff --git a/modules/customers/src/common/dto/request/create-customer.request.dto.ts b/modules/customers/src/common/dto/request/create-customer.request.dto.ts index 0cfc7303..514ee276 100644 --- a/modules/customers/src/common/dto/request/create-customer.request.dto.ts +++ b/modules/customers/src/common/dto/request/create-customer.request.dto.ts @@ -5,14 +5,15 @@ export const CreateCustomerRequestSchema = z.object({ company_id: z.uuid(), reference: z.string().default(""), - is_company: z.boolean().default(true), + is_company: z.boolean().default(false), name: z.string().default(""), trade_name: z.string().default(""), tin: z.string().default(""), street: z.string().default(""), + street2: z.string().default(""), city: z.string().default(""), - state: z.string().default(""), + province: z.string().default(""), postal_code: z.string().default(""), country: z.string().default(""), @@ -23,9 +24,9 @@ export const CreateCustomerRequestSchema = z.object({ legal_record: z.string().default(""), - default_tax: z.array(z.string()).default([]), + default_taxes: z.array(z.string()).default([]), status: z.string().default("active"), - lang_code: z.string().default("es"), + language_code: z.string().default("es"), currency_code: z.string().default("EUR"), }); diff --git a/modules/customers/src/common/dto/request/update-customer.request.dto.ts b/modules/customers/src/common/dto/request/update-customer.request.dto.ts index e69de29b..002c729f 100644 --- a/modules/customers/src/common/dto/request/update-customer.request.dto.ts +++ b/modules/customers/src/common/dto/request/update-customer.request.dto.ts @@ -0,0 +1,30 @@ +import * as z from "zod/v4"; + +export const UpdateCustomerRequestSchema = z.object({ + reference: z.string().optional(), + + is_company: z.boolean().optional(), + name: z.string().optional(), + trade_name: z.string().optional(), + tin: z.string().optional(), + + street: z.string().optional(), + street2: z.string().optional(), + city: z.string().optional(), + province: z.string().optional(), + postal_code: z.string().optional(), + country: z.string().optional(), + + email: z.string().optional(), + phone: z.string().optional(), + fax: z.string().optional(), + website: z.string().optional(), + + legal_record: z.string().optional(), + + default_taxes: z.array(z.string()).optional(), // completo (sustituye), o null => vaciar + language_code: z.string().optional(), + currency_code: z.string().optional(), +}); + +export type UpdateCustomerRequestDTO = z.infer; diff --git a/modules/customers/src/common/dto/response/customer-creation.result.dto.ts b/modules/customers/src/common/dto/response/create-customer.result.dto.ts similarity index 70% rename from modules/customers/src/common/dto/response/customer-creation.result.dto.ts rename to modules/customers/src/common/dto/response/create-customer.result.dto.ts index 73e36595..99253b30 100644 --- a/modules/customers/src/common/dto/response/customer-creation.result.dto.ts +++ b/modules/customers/src/common/dto/response/create-customer.result.dto.ts @@ -1,12 +1,12 @@ import { MetadataSchema } from "@erp/core"; import * as z from "zod/v4"; -export const CustomerCreationResponseSchema = z.object({ +export const CreateCustomerResponseSchema = z.object({ id: z.uuid(), company_id: z.uuid(), reference: z.string(), - is_company: z.boolean(), + is_company: z.string(), name: z.string(), trade_name: z.string(), tin: z.string(), @@ -25,12 +25,12 @@ export const CustomerCreationResponseSchema = z.object({ legal_record: z.string(), - default_tax: z.array(z.string()), + default_taxes: z.array(z.string()), status: z.string(), - lang_code: z.string(), + language_code: z.string(), currency_code: z.string(), metadata: MetadataSchema.optional(), }); -export type CustomerCreationResponseDTO = z.infer; +export type CustomerCreationResponseDTO = z.infer; diff --git a/modules/customers/src/common/dto/response/customer-list.response.dto.ts b/modules/customers/src/common/dto/response/customer-list.response.dto.ts index fead2130..c0ab5824 100644 --- a/modules/customers/src/common/dto/response/customer-list.response.dto.ts +++ b/modules/customers/src/common/dto/response/customer-list.response.dto.ts @@ -24,9 +24,9 @@ export const CustomerListResponseSchema = createListViewResponseSchema( legal_record: z.string(), - default_tax: z.number(), + default_taxes: z.array(z.string()), status: z.string(), - lang_code: z.string(), + language_code: z.string(), currency_code: z.string(), metadata: MetadataSchema.optional(), diff --git a/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts b/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts index 3743a8bb..2e874101 100644 --- a/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts +++ b/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts @@ -3,14 +3,16 @@ import * as z from "zod/v4"; export const GetCustomerByIdResponseSchema = z.object({ id: z.uuid(), + company_id: z.uuid(), reference: z.string(), - is_company: z.boolean(), + is_company: z.string(), name: z.string(), trade_name: z.string(), tin: z.string(), street: z.string(), + street2: z.string(), city: z.string(), state: z.string(), postal_code: z.string(), @@ -23,9 +25,9 @@ export const GetCustomerByIdResponseSchema = z.object({ legal_record: z.string(), - default_tax: z.number(), + default_taxes: z.array(z.string()), status: z.string(), - lang_code: z.string(), + language_code: z.string(), currency_code: z.string(), metadata: MetadataSchema.optional(), diff --git a/modules/customers/src/common/dto/response/index.ts b/modules/customers/src/common/dto/response/index.ts index 14ef180c..f6fe23f1 100644 --- a/modules/customers/src/common/dto/response/index.ts +++ b/modules/customers/src/common/dto/response/index.ts @@ -1,3 +1,3 @@ -export * from "./customer-creation.result.dto"; +export * from "./create-customer.result.dto"; export * from "./customer-list.response.dto"; export * from "./get-customer-by-id.response.dto"; diff --git a/modules/customers/src/web/hooks/use-create-customer-mutation.ts b/modules/customers/src/web/hooks/use-create-customer-mutation.ts index 237b0075..c373d8f2 100644 --- a/modules/customers/src/web/hooks/use-create-customer-mutation.ts +++ b/modules/customers/src/web/hooks/use-create-customer-mutation.ts @@ -1,13 +1,13 @@ import { useDataSource, useQueryKey } from "@erp/core/hooks"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { CreateCustomerRequestDTO } from "../../common/dto"; +import { UpdateCustomerRequestDTO } from "../../common/dto"; export const useCreateCustomerMutation = () => { const queryClient = useQueryClient(); const dataSource = useDataSource(); const keys = useQueryKey(); - return useMutation>({ + return useMutation>({ mutationFn: (data) => { console.log(data); return dataSource.createOne("customers", data); diff --git a/modules/customers/src/web/pages/create/customer.schema.ts b/modules/customers/src/web/pages/create/customer.schema.ts index 33d235ae..56eea773 100644 --- a/modules/customers/src/web/pages/create/customer.schema.ts +++ b/modules/customers/src/web/pages/create/customer.schema.ts @@ -1,6 +1,6 @@ import * as z from "zod/v4"; -import { CreateCustomerRequestSchema } from "../../../common"; +import { UpdateCustomerRequestSchema } from "../../../common"; -export const CustomerDataFormSchema = CreateCustomerRequestSchema; +export const CustomerDataFormSchema = UpdateCustomerRequestSchema; export type CustomerData = z.infer; diff --git a/packages/rdx-ddd/src/helpers/index.ts b/packages/rdx-ddd/src/helpers/index.ts new file mode 100644 index 00000000..c9873891 --- /dev/null +++ b/packages/rdx-ddd/src/helpers/index.ts @@ -0,0 +1 @@ +export * from "./normalizers"; diff --git a/packages/rdx-ddd/src/helpers/normalizers.ts b/packages/rdx-ddd/src/helpers/normalizers.ts new file mode 100644 index 00000000..7ab63022 --- /dev/null +++ b/packages/rdx-ddd/src/helpers/normalizers.ts @@ -0,0 +1,35 @@ +// application/shared/normalizers.ts +// Normalizadores y adaptadores DTO -> Maybe/VO + +import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils"; + +/** any | null | undefined -> Maybe usando fábrica VO */ +export function maybeFromNullableVO( + input: any, + voFactory: (raw: any) => Result +): Result> { + if (isNullishOrEmpty(input)) return Result.ok(Maybe.none()); + const vo = voFactory(input); + return vo.isSuccess ? Result.ok(Maybe.some(vo.data)) : Result.fail(vo.error); +} + +/** string | null | undefined -> Maybe (trim, vacío => None) */ +export function maybeFromNullableString(input?: string | null): Maybe { + if (isNullishOrEmpty(input)) return Maybe.none(); + const t = (input as string).trim(); + return t ? Maybe.some(t) : Maybe.none(); +} + +/** Maybe -> null para transporte */ +export function toNullable(m: Maybe, map?: (t: T) => any): any | null { + if (m.isNone()) return null; + const v = m.unwrap() as T; + return map ? String(map(v)) : String(v); +} + +/** Maybe -> "" para transporte */ +export function toEmptyString(m: Maybe, map?: (t: T) => string): string { + if (m.isNone()) return ""; + const v = m.unwrap() as T; + return map ? map(v) : String(v); +} diff --git a/packages/rdx-ddd/src/index.ts b/packages/rdx-ddd/src/index.ts index 8f115f96..a4f4da5b 100644 --- a/packages/rdx-ddd/src/index.ts +++ b/packages/rdx-ddd/src/index.ts @@ -2,4 +2,5 @@ export * from "./aggregate-root"; export * from "./aggregate-root-repository.interface"; export * from "./domain-entity"; export * from "./events/domain-event.interface"; +export * from "./helpers"; export * from "./value-objects"; diff --git a/packages/rdx-ddd/src/value-objects/__tests__/email-address.test.ts b/packages/rdx-ddd/src/value-objects/__tests__/email-address.test.ts index a1595a41..e33600aa 100644 --- a/packages/rdx-ddd/src/value-objects/__tests__/email-address.test.ts +++ b/packages/rdx-ddd/src/value-objects/__tests__/email-address.test.ts @@ -15,12 +15,6 @@ describe("EmailAddress Value Object", () => { expect(result.error.message).toBe("Invalid email format"); }); - it("should allow null email", () => { - const result = EmailAddress.createNullable(); - expect(result.isSuccess).toBe(true); - expect(result.data.getOrUndefined()).toBeUndefined(); - }); - it("should return an error for empty string", () => { const result = EmailAddress.create(""); @@ -45,13 +39,6 @@ describe("EmailAddress Value Object", () => { expect(email1.data.equals(email2.data)).toBe(false); }); - it("should detect empty email correctly", () => { - const email = EmailAddress.createNullable(); - - expect(email.isSuccess).toBe(true); - expect(email.data.isSome()).toBe(false); - }); - it("should detect non-empty email correctly", () => { const email = EmailAddress.create("test@example.com"); diff --git a/packages/rdx-ddd/src/value-objects/__tests__/name.spec.ts b/packages/rdx-ddd/src/value-objects/__tests__/name.spec.ts index 0557f757..9c308975 100644 --- a/packages/rdx-ddd/src/value-objects/__tests__/name.spec.ts +++ b/packages/rdx-ddd/src/value-objects/__tests__/name.spec.ts @@ -14,19 +14,6 @@ describe("Name Value Object", () => { expect(nameResult.error).toBeInstanceOf(Error); }); - test("Debe permitir un Name nullable vacío", () => { - const nullableNameResult = Name.createNullable(""); - expect(nullableNameResult.isSuccess).toBe(true); - expect(nullableNameResult.data.isSome()).toBe(false); - }); - - test("Debe permitir un Name nullable con un valor válido", () => { - const nullableNameResult = Name.createNullable("Alice"); - expect(nullableNameResult.isSuccess).toBe(true); - expect(nullableNameResult.data.isSome()).toBe(true); - expect(nullableNameResult.data.getOrUndefined()?.toString()).toBe("Alice"); - }); - test("Debe generar acrónimos correctamente", () => { expect(Name.generateAcronym("John Doe")).toBe("JDXX"); expect(Name.generateAcronym("Alice Bob Charlie")).toBe("ABCX"); diff --git a/packages/rdx-ddd/src/value-objects/__tests__/phone-number.test.ts b/packages/rdx-ddd/src/value-objects/__tests__/phone-number.test.ts index 0015b971..30af29a4 100644 --- a/packages/rdx-ddd/src/value-objects/__tests__/phone-number.test.ts +++ b/packages/rdx-ddd/src/value-objects/__tests__/phone-number.test.ts @@ -1,5 +1,4 @@ import { parsePhoneNumberWithError } from "libphonenumber-js"; -import { Maybe } from "../../helpers/maybe"; import { PhoneNumber } from "../phone-number"; describe("PhoneNumber", () => { @@ -21,18 +20,6 @@ describe("PhoneNumber", () => { ); }); - test("debe devolver None para valores nulos o vacíos", () => { - const result = PhoneNumber.createNullable(nullablePhone); - expect(result.isSuccess).toBe(true); - expect(result.data).toEqual(Maybe.none()); - }); - - test("debe devolver Some con un número de teléfono válido", () => { - const result = PhoneNumber.createNullable(validPhone); - expect(result.isSuccess).toBe(true); - expect(result.data.isSome()).toBe(true); - }); - test("debe obtener el valor del número de teléfono", () => { const result = PhoneNumber.create(validPhone); expect(result.isSuccess).toBe(true); diff --git a/packages/rdx-ddd/src/value-objects/__tests__/postal-address.test.ts b/packages/rdx-ddd/src/value-objects/__tests__/postal-address.test.ts index 2b561032..27b44ad0 100644 --- a/packages/rdx-ddd/src/value-objects/__tests__/postal-address.test.ts +++ b/packages/rdx-ddd/src/value-objects/__tests__/postal-address.test.ts @@ -24,27 +24,6 @@ describe("PostalAddress Value Object", () => { expect(result.error?.message).toBe("Invalid postal code format"); }); - test("✅ `createNullable` debería devolver Maybe.none si los valores son nulos o vacíos", () => { - expect(PostalAddress.createNullable().data.isSome()).toBe(false); - expect( - PostalAddress.createNullable({ - street: "", - city: "", - postalCode: "", - state: "", - country: "", - }).data.isSome() - ).toBe(false); - }); - - test("✅ `createNullable` debería devolver Maybe.some si los valores son válidos", () => { - const result = PostalAddress.createNullable(validAddress); - - expect(result.isSuccess).toBe(true); - expect(result.data.isSome()).toBe(true); - expect(result.data.unwrap()).toBeInstanceOf(PostalAddress); - }); - test("✅ Métodos getters deberían devolver valores esperados", () => { const address = PostalAddress.create(validAddress).data; diff --git a/packages/rdx-ddd/src/value-objects/__tests__/slug.spec.ts b/packages/rdx-ddd/src/value-objects/__tests__/slug.spec.ts index f0a0e195..f5e398c7 100644 --- a/packages/rdx-ddd/src/value-objects/__tests__/slug.spec.ts +++ b/packages/rdx-ddd/src/value-objects/__tests__/slug.spec.ts @@ -25,17 +25,4 @@ describe("Slug Value Object", () => { expect(slugResult.isSuccess).toBe(false); expect(slugResult.error).toBeInstanceOf(Error); }); - - test("Debe permitir un Slug nullable vacío", () => { - const nullableSlugResult = Slug.createNullable(""); - expect(nullableSlugResult.isSuccess).toBe(true); - expect(nullableSlugResult.data.isSome()).toBe(false); - }); - - test("Debe permitir un Slug nullable con un valor válido", () => { - const nullableSlugResult = Slug.createNullable("my-slug"); - expect(nullableSlugResult.isSuccess).toBe(true); - expect(nullableSlugResult.data.isSome()).toBe(true); - expect(nullableSlugResult.data.getOrUndefined()?.toString()).toBe("my-slug"); - }); }); diff --git a/packages/rdx-ddd/src/value-objects/__tests__/tin-number.test.ts b/packages/rdx-ddd/src/value-objects/__tests__/tin-number.test.ts index 4a63d29f..250ad5d7 100644 --- a/packages/rdx-ddd/src/value-objects/__tests__/tin-number.test.ts +++ b/packages/rdx-ddd/src/value-objects/__tests__/tin-number.test.ts @@ -19,19 +19,6 @@ describe("TINNumber", () => { expect(result.error?.message).toBe("TIN must be at most 10 characters long"); }); - it("debería devolver None cuando el valor es nulo o vacío en createNullable", () => { - const result = TINNumber.createNullable(""); - expect(result.isSuccess).toBe(true); - expect(result.data.isNone()).toBe(true); - }); - - it("debería devolver Some cuando el valor es válido en createNullable", () => { - const result = TINNumber.createNullable("6789"); - expect(result.isSuccess).toBe(true); - expect(result.data.isSome()).toBe(true); - expect(result.data.unwrap()?.toString()).toBe("6789"); - }); - it("debería devolver el valor correcto en toString()", () => { const result = TINNumber.create("ABC123"); expect(result.isSuccess).toBe(true); diff --git a/packages/rdx-ddd/src/value-objects/__tests__/value-objects.test.ts b/packages/rdx-ddd/src/value-objects/__tests__/value-objects.test.ts index 6762c11a..559fe045 100644 --- a/packages/rdx-ddd/src/value-objects/__tests__/value-objects.test.ts +++ b/packages/rdx-ddd/src/value-objects/__tests__/value-objects.test.ts @@ -1,10 +1,10 @@ import { ValueObject } from "./value-object"; -interface ITestValueProps { +interface TestValueProps { value: string; } -class TestValueObject extends ValueObject { +class TestValueObject extends ValueObject { constructor(value: string) { super({ value }); } diff --git a/packages/rdx-ddd/src/value-objects/city.ts b/packages/rdx-ddd/src/value-objects/city.ts new file mode 100644 index 00000000..2ac23b1b --- /dev/null +++ b/packages/rdx-ddd/src/value-objects/city.ts @@ -0,0 +1,38 @@ +import { Result } from "@repo/rdx-utils"; +import * as z from "zod/v4"; +import { ValueObject } from "./value-object"; + +interface CityProps { + value: string; +} + +export class City extends ValueObject { + private static readonly MAX_LENGTH = 255; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .max(City.MAX_LENGTH, { + message: `City must be at most ${City.MAX_LENGTH} characters long`, + }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = City.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.issues[0].message)); + } + return Result.ok(new City({ value })); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive() { + return this.getValue(); + } +} diff --git a/packages/rdx-ddd/src/value-objects/country.ts b/packages/rdx-ddd/src/value-objects/country.ts new file mode 100644 index 00000000..90107862 --- /dev/null +++ b/packages/rdx-ddd/src/value-objects/country.ts @@ -0,0 +1,38 @@ +import { Result } from "@repo/rdx-utils"; +import * as z from "zod/v4"; +import { ValueObject } from "./value-object"; + +interface CountryProps { + value: string; +} + +export class Country extends ValueObject { + private static readonly MAX_LENGTH = 255; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .max(Country.MAX_LENGTH, { + message: `Country must be at most ${Country.MAX_LENGTH} characters long`, + }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = Country.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.issues[0].message)); + } + return Result.ok(new Country({ value })); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive() { + return this.getValue(); + } +} diff --git a/packages/rdx-ddd/src/value-objects/currency-code.ts b/packages/rdx-ddd/src/value-objects/currency-code.ts new file mode 100644 index 00000000..215ac57d --- /dev/null +++ b/packages/rdx-ddd/src/value-objects/currency-code.ts @@ -0,0 +1,48 @@ +import { Result } from "@repo/rdx-utils"; +import * as z from "zod/v4"; +import { ValueObject } from "./value-object"; + +interface CurrencyCodeProps { + value: string; +} + +/** + * ISO 4217 Currency Codes + */ + +export class CurrencyCode extends ValueObject { + private static readonly MIN_LENGTH = 3; + private static readonly MAX_LENGTH = 3; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .uppercase() + .min(CurrencyCode.MIN_LENGTH, { + message: `CurrencyCode must be at least ${CurrencyCode.MIN_LENGTH} characters long`, + }) + .max(CurrencyCode.MAX_LENGTH, { + message: `CurrencyCode must be at most ${CurrencyCode.MAX_LENGTH} characters long`, + }); + + return schema.safeParse(value); + } + + static create(value: string): Result { + const valueIsValid = CurrencyCode.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.issues[0].message)); + } + return Result.ok(new CurrencyCode({ value: valueIsValid.data })); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive(): string { + return this.props.value; + } +} diff --git a/packages/rdx-ddd/src/value-objects/email-address.ts b/packages/rdx-ddd/src/value-objects/email-address.ts index 6b6ccc4b..84262830 100644 --- a/packages/rdx-ddd/src/value-objects/email-address.ts +++ b/packages/rdx-ddd/src/value-objects/email-address.ts @@ -1,4 +1,4 @@ -import { Maybe, Result } from "@repo/rdx-utils"; +import { Result } from "@repo/rdx-utils"; import * as z from "zod/v4"; import { ValueObject } from "./value-object"; @@ -17,16 +17,8 @@ export class EmailAddress extends ValueObject { return Result.ok(new EmailAddress({ value: valueIsValid.data })); } - static createNullable(value?: string): Result, Error> { - if (!value || value.trim() === "") { - return Result.ok(Maybe.none()); - } - - return EmailAddress.create(value).map((value) => Maybe.some(value)); - } - private static validate(value: string) { - const schema = z.string().email({ message: "Invalid email format" }); + const schema = z.email({ message: "Invalid email format" }); return schema.safeParse(value); } diff --git a/packages/rdx-ddd/src/value-objects/index.ts b/packages/rdx-ddd/src/value-objects/index.ts index 5d341ab4..132d8d1c 100644 --- a/packages/rdx-ddd/src/value-objects/index.ts +++ b/packages/rdx-ddd/src/value-objects/index.ts @@ -1,12 +1,22 @@ +export * from "./city"; +export * from "./country"; +export * from "./currency-code"; export * from "./email-address"; +export * from "./language-code"; export * from "./money-value"; export * from "./name"; export * from "./percentage"; export * from "./phone-number"; export * from "./postal-address"; +export * from "./postal-code"; +export * from "./province"; export * from "./quantity"; export * from "./slug"; +export * from "./street"; +export * from "./tax-code"; +export * from "./text-value"; export * from "./tin-number"; export * from "./unique-id"; +export * from "./url-address"; export * from "./utc-date"; export * from "./value-object"; diff --git a/packages/rdx-ddd/src/value-objects/language-code.ts b/packages/rdx-ddd/src/value-objects/language-code.ts new file mode 100644 index 00000000..78efdee6 --- /dev/null +++ b/packages/rdx-ddd/src/value-objects/language-code.ts @@ -0,0 +1,48 @@ +import { Result } from "@repo/rdx-utils"; +import * as z from "zod/v4"; +import { ValueObject } from "./value-object"; + +interface LanguageCodeProps { + value: string; +} + +/** + * ISO 639-1 (2 letras) + */ + +export class LanguageCode extends ValueObject { + private static readonly MIN_LENGTH = 2; + private static readonly MAX_LENGTH = 2; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .lowercase() + .min(LanguageCode.MIN_LENGTH, { + message: `LanguageCode must be at least ${LanguageCode.MIN_LENGTH} characters long`, + }) + .max(LanguageCode.MAX_LENGTH, { + message: `LanguageCode must be at most ${LanguageCode.MAX_LENGTH} characters long`, + }); + + return schema.safeParse(value); + } + + static create(value: string): Result { + const valueIsValid = LanguageCode.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.issues[0].message)); + } + return Result.ok(new LanguageCode({ value: valueIsValid.data })); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive(): string { + return this.props.value; + } +} diff --git a/packages/rdx-ddd/src/value-objects/money-value.ts b/packages/rdx-ddd/src/value-objects/money-value.ts index 1c9595d4..74e39586 100644 --- a/packages/rdx-ddd/src/value-objects/money-value.ts +++ b/packages/rdx-ddd/src/value-objects/money-value.ts @@ -18,7 +18,7 @@ export type RoundingMode = | "HALF_AWAY_FROM_ZERO" | "DOWN"; -export interface IMoneyValueProps { +export interface MoneyValueProps { amount: number; scale?: number; currency_code?: string; @@ -29,7 +29,7 @@ export interface IMoneyValue { scale: number; currency: Dinero.Currency; - getValue(): IMoneyValueProps; + getValue(): MoneyValueProps; convertScale(newScale: number): MoneyValue; add(addend: MoneyValue): MoneyValue; subtract(subtrahend: MoneyValue): MoneyValue; @@ -47,13 +47,13 @@ export interface IMoneyValue { format(locale: string): string; } -export class MoneyValue extends ValueObject implements IMoneyValue { +export class MoneyValue extends ValueObject implements IMoneyValue { private readonly dinero: Dinero; static DEFAULT_SCALE = DEFAULT_SCALE; static DEFAULT_CURRENCY_CODE = DEFAULT_CURRENCY_CODE; - static create({ amount, currency_code, scale }: IMoneyValueProps) { + static create({ amount, currency_code, scale }: MoneyValueProps) { const props = { amount: Number(amount), scale: scale ?? MoneyValue.DEFAULT_SCALE, @@ -62,7 +62,7 @@ export class MoneyValue extends ValueObject implements IMoneyV return Result.ok(new MoneyValue(props)); } - constructor(props: IMoneyValueProps) { + constructor(props: MoneyValueProps) { super(props); const { amount, scale, currency_code } = props; this.dinero = Object.freeze( @@ -86,7 +86,7 @@ export class MoneyValue extends ValueObject implements IMoneyV return this.dinero.getPrecision(); } - getValue(): IMoneyValueProps { + getValue(): MoneyValueProps { return this.props; } diff --git a/packages/rdx-ddd/src/value-objects/name.ts b/packages/rdx-ddd/src/value-objects/name.ts index 3a81cde3..20e8b4f9 100644 --- a/packages/rdx-ddd/src/value-objects/name.ts +++ b/packages/rdx-ddd/src/value-objects/name.ts @@ -1,12 +1,12 @@ -import { Maybe, Result } from "@repo/rdx-utils"; +import { Result } from "@repo/rdx-utils"; import * as z from "zod/v4"; import { ValueObject } from "./value-object"; -interface INameProps { +interface NameProps { value: string; } -export class Name extends ValueObject { +export class Name extends ValueObject { private static readonly MAX_LENGTH = 255; protected static validate(value: string) { @@ -26,14 +26,6 @@ export class Name extends ValueObject { return Result.ok(new Name({ value })); } - static createNullable(value?: string): Result, Error> { - if (!value || value.trim() === "") { - return Result.ok(Maybe.none()); - } - - return Name.create(value).map((value) => Maybe.some(value)); - } - static generateAcronym(name: string): string { const words = name.split(" ").map((word) => word[0].toUpperCase()); let acronym = words.join(""); diff --git a/packages/rdx-ddd/src/value-objects/percentage.ts b/packages/rdx-ddd/src/value-objects/percentage.ts index 1d525abb..75a7d2d7 100644 --- a/packages/rdx-ddd/src/value-objects/percentage.ts +++ b/packages/rdx-ddd/src/value-objects/percentage.ts @@ -10,21 +10,12 @@ const DEFAULT_MAX_VALUE = 100; const DEFAULT_MIN_SCALE = 0; const DEFAULT_MAX_SCALE = 2; -export interface IPercentageProps { +export interface PercentageProps { amount: number; scale: number; } -interface IPercentage { - amount: number; - scale: number; - - getValue(): IPercentageProps; - toNumber(): number; - toString(): string; -} - -export class Percentage extends ValueObject implements IPercentage { +export class Percentage extends ValueObject { static DEFAULT_SCALE = DEFAULT_SCALE; static MIN_VALUE = DEFAULT_MIN_VALUE; static MAX_VALUE = DEFAULT_MAX_VALUE; @@ -32,7 +23,7 @@ export class Percentage extends ValueObject implements IPercen static MIN_SCALE = DEFAULT_MIN_SCALE; static MAX_SCALE = DEFAULT_MAX_SCALE; - protected static validate(values: IPercentageProps) { + protected static validate(values: PercentageProps) { const schema = z.object({ amount: z.number().int().min(Percentage.MIN_VALUE, "La cantidad no puede ser negativa."), scale: z @@ -75,7 +66,7 @@ export class Percentage extends ValueObject implements IPercen return this.props.scale; } - getValue(): IPercentageProps { + getValue(): PercentageProps { return this.props; } diff --git a/packages/rdx-ddd/src/value-objects/postal-address.ts b/packages/rdx-ddd/src/value-objects/postal-address.ts index 2fbf053f..cfd6f5a9 100644 --- a/packages/rdx-ddd/src/value-objects/postal-address.ts +++ b/packages/rdx-ddd/src/value-objects/postal-address.ts @@ -1,62 +1,45 @@ import { Maybe, Result } from "@repo/rdx-utils"; -import * as z from "zod/v4"; +import { City } from "./city"; +import { Country } from "./country"; +import { PostalCode } from "./postal-code"; +import { Province } from "./province"; +import { Street } from "./street"; import { ValueObject } from "./value-object"; -// 📌 Validaciones usando `zod` -const postalCodeSchema = z - .string() - .min(4, "Invalid postal code format") - .max(10, "Invalid postal code format") - .regex(/^\d{4,10}$/, { - message: "Invalid postal code format", - }); - -const streetSchema = z.string().max(255).default(""); -const street2Schema = z.string().default(""); -const citySchema = z.string().max(50).default(""); -const stateSchema = z.string().max(50).default(""); -const countrySchema = z.string().max(56).default(""); - -interface IPostalAddressProps { - street: string; - street2?: string; - city: string; - postalCode: string; - state: string; - country: string; +export interface PostalAddressProps { + street: Maybe; + street2: Maybe; + city: Maybe; + postalCode: Maybe; + province: Maybe; + country: Maybe; } -export class PostalAddress extends ValueObject { - protected static validate(values: IPostalAddressProps) { - return z - .object({ - street: streetSchema, - street2: street2Schema, - city: citySchema, - postalCode: postalCodeSchema, - state: stateSchema, - country: countrySchema, - }) - .safeParse(values); +export interface PostalAddressSnapshot { + street: string | null; + street2: string | null; + city: string | null; + postalCode: string | null; + province: string | null; + country: string | null; +} + +export type PostalAddressPatchProps = Partial; + +export class PostalAddress extends ValueObject { + protected static validate(values: PostalAddressProps) { + return Result.ok(values); } - static create(values: IPostalAddressProps): Result { + static create(values: PostalAddressProps): Result { const valueIsValid = PostalAddress.validate(values); - if (!valueIsValid.success) { - return Result.fail(new Error(valueIsValid.error.issues[0].message)); + if (valueIsValid.isFailure) { + return Result.fail(valueIsValid.error); } return Result.ok(new PostalAddress(values)); } - static createNullable(values?: IPostalAddressProps): Result, Error> { - if (!values || Object.values(values).every((value) => value.trim() === "")) { - return Result.ok(Maybe.none()); - } - - return PostalAddress.create(values).map((value) => Maybe.some(value)); - } - static update( oldAddress: PostalAddress, data: Partial @@ -66,37 +49,37 @@ export class PostalAddress extends ValueObject { street2: data.street2 ?? oldAddress.street2, city: data.city ?? oldAddress.city, postalCode: data.postalCode ?? oldAddress.postalCode, - state: data.state ?? oldAddress.state, + province: data.province ?? oldAddress.province, country: data.country ?? oldAddress.country, // biome-ignore lint/complexity/noThisInStatic: }).getOrElse(this); } - get street(): string { + get street(): Maybe { return this.props.street; } - get street2(): string { - return this.props.street2 ?? ""; + get street2(): Maybe { + return this.props.street2; } - get city(): string { + get city(): Maybe { return this.props.city; } - get postalCode(): string { + get postalCode(): Maybe { return this.props.postalCode; } - get state(): string { - return this.props.state; + get province(): Maybe { + return this.props.province; } - get country(): string { + get country(): Maybe { return this.props.country; } - getValue(): IPostalAddressProps { + getValue(): PostalAddressProps { return this.props; } @@ -104,7 +87,20 @@ export class PostalAddress extends ValueObject { return this.getValue(); } - toString(): string { - return `${this.props.street}, ${this.props.street2}, ${this.props.city}, ${this.props.postalCode}, ${this.props.state}, ${this.props.country}`; + toFormat(): string { + return `${this.props.street}, ${this.props.street2}, ${this.props.city}, ${this.props.postalCode}, ${this.props.province}, ${this.props.country}`; + } + + public toSnapshot(): PostalAddressSnapshot { + return { + street: this.props.street.isSome() ? this.props.street.unwrap()!.toString() : null, + street2: this.props.street2.isSome() ? this.props.street2.unwrap()!.toString() : null, + city: this.props.city.isSome() ? this.props.city.unwrap()!.toString() : null, + postalCode: this.props.postalCode.isSome() + ? this.props.postalCode.unwrap()!.toString() + : null, + province: this.props.province.isSome() ? this.props.province.unwrap()!.toString() : null, + country: this.props.country.isSome() ? this.props.country.unwrap()!.toString() : null, + }; } } diff --git a/packages/rdx-ddd/src/value-objects/postal-code.ts b/packages/rdx-ddd/src/value-objects/postal-code.ts new file mode 100644 index 00000000..f1712c9b --- /dev/null +++ b/packages/rdx-ddd/src/value-objects/postal-code.ts @@ -0,0 +1,46 @@ +import { Result } from "@repo/rdx-utils"; +import * as z from "zod/v4"; +import { ValueObject } from "./value-object"; + +interface PostalCodeProps { + value: string; +} + +export class PostalCode extends ValueObject { + private static readonly MIN_LENGTH = 5; + private static readonly MAX_LENGTH = 5; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .regex(/^[0-9]+$/, { + message: "PostalCode must contain only numbers", + }) + .min(PostalCode.MIN_LENGTH, { + message: `PostalCode must be at least ${PostalCode.MIN_LENGTH} characters long`, + }) + .max(PostalCode.MAX_LENGTH, { + message: `PostalCode must be at most ${PostalCode.MAX_LENGTH} characters long`, + }); + + return schema.safeParse(value); + } + + static create(value: string): Result { + const valueIsValid = PostalCode.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.issues[0].message)); + } + return Result.ok(new PostalCode({ value: valueIsValid.data })); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive(): string { + return this.props.value; + } +} diff --git a/packages/rdx-ddd/src/value-objects/province.ts b/packages/rdx-ddd/src/value-objects/province.ts new file mode 100644 index 00000000..6df1c814 --- /dev/null +++ b/packages/rdx-ddd/src/value-objects/province.ts @@ -0,0 +1,38 @@ +import { Result } from "@repo/rdx-utils"; +import * as z from "zod/v4"; +import { ValueObject } from "./value-object"; + +interface ProvinceProps { + value: string; +} + +export class Province extends ValueObject { + private static readonly MAX_LENGTH = 255; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .max(Province.MAX_LENGTH, { + message: `Province must be at most ${Province.MAX_LENGTH} characters long`, + }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = Province.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.issues[0].message)); + } + return Result.ok(new Province({ value })); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive() { + return this.getValue(); + } +} diff --git a/packages/rdx-ddd/src/value-objects/quantity.ts b/packages/rdx-ddd/src/value-objects/quantity.ts index fd224386..855b19a1 100644 --- a/packages/rdx-ddd/src/value-objects/quantity.ts +++ b/packages/rdx-ddd/src/value-objects/quantity.ts @@ -6,31 +6,13 @@ const DEFAULT_SCALE = 2; const DEFAULT_MIN_SCALE = 0; const DEFAULT_MAX_SCALE = 2; -export interface IQuantityProps { +export interface QuantityProps { amount: number; scale: number; } -interface IQuantity { - amount: number; - scale: number; - - getValue(): IQuantityProps; - toNumber(): number; - toString(): string; - - isZero(): boolean; - isPositive(): boolean; - isNegative(): boolean; - - increment(anotherQuantity?: Quantity): Result; - decrement(anotherQuantity?: Quantity): Result; - hasSameScale(otherQuantity: Quantity): boolean; - convertScale(newScale: number): Result; -} - -export class Quantity extends ValueObject implements IQuantity { - protected static validate(values: IQuantityProps) { +export class Quantity extends ValueObject { + protected static validate(values: QuantityProps) { const schema = z.object({ amount: z.number().int(), scale: z.number().int().min(Quantity.MIN_SCALE).max(Quantity.MAX_SCALE), @@ -43,7 +25,7 @@ export class Quantity extends ValueObject implements IQuantity { static MIN_SCALE = DEFAULT_MIN_SCALE; static MAX_SCALE = DEFAULT_MAX_SCALE; - static create({ amount, scale }: IQuantityProps) { + static create({ amount, scale }: QuantityProps) { const props = { amount: Number(amount), scale: scale ?? Quantity.DEFAULT_SCALE, @@ -51,9 +33,9 @@ export class Quantity extends ValueObject implements IQuantity { const checkProps = Quantity.validate(props); if (!checkProps.success) { - return Result.fail(new Error(checkProps.error.errors[0].message)); + return Result.fail(new Error(checkProps.error.issues[0].message)); } - return Result.ok(new Quantity({ ...(checkProps.data as IQuantityProps) })); + return Result.ok(new Quantity({ ...(checkProps.data as QuantityProps) })); } get amount(): number { @@ -64,7 +46,7 @@ export class Quantity extends ValueObject implements IQuantity { return this.props.scale; } - getValue(): IQuantityProps { + getValue(): QuantityProps { return this.props; } diff --git a/packages/rdx-ddd/src/value-objects/slug.ts b/packages/rdx-ddd/src/value-objects/slug.ts index 85a2dfcc..1112d8ea 100644 --- a/packages/rdx-ddd/src/value-objects/slug.ts +++ b/packages/rdx-ddd/src/value-objects/slug.ts @@ -1,4 +1,4 @@ -import { Maybe, Result } from "@repo/rdx-utils"; +import { Result } from "@repo/rdx-utils"; import * as z from "zod/v4"; import { ValueObject } from "./value-object"; @@ -32,14 +32,6 @@ export class Slug extends ValueObject { return Result.ok(new Slug({ value: valueIsValid.data! })); } - static createNullable(value?: string): Result, Error> { - if (!value || value.trim() === "") { - return Result.ok(Maybe.none()); - } - - return Slug.create(value).map((value: Slug) => Maybe.some(value)); - } - getValue(): string { return this.props.value; } diff --git a/packages/rdx-ddd/src/value-objects/street.ts b/packages/rdx-ddd/src/value-objects/street.ts new file mode 100644 index 00000000..6fc4c3be --- /dev/null +++ b/packages/rdx-ddd/src/value-objects/street.ts @@ -0,0 +1,38 @@ +import { Result } from "@repo/rdx-utils"; +import * as z from "zod/v4"; +import { ValueObject } from "./value-object"; + +interface StreetProps { + value: string; +} + +export class Street extends ValueObject { + private static readonly MAX_LENGTH = 255; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .max(Street.MAX_LENGTH, { + message: `Street must be at most ${Street.MAX_LENGTH} characters long`, + }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = Street.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.issues[0].message)); + } + return Result.ok(new Street({ value })); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive() { + return this.getValue(); + } +} diff --git a/packages/rdx-ddd/src/value-objects/tax-code.ts b/packages/rdx-ddd/src/value-objects/tax-code.ts new file mode 100644 index 00000000..2c6db6a5 --- /dev/null +++ b/packages/rdx-ddd/src/value-objects/tax-code.ts @@ -0,0 +1,46 @@ +import { Result } from "@repo/rdx-utils"; +import * as z from "zod/v4"; +import { ValueObject } from "./value-object"; + +interface TaxCodeProps { + value: string; +} + +export class TaxCode extends ValueObject { + protected static readonly MIN_LENGTH = 1; + protected static readonly MAX_LENGTH = 10; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .regex(/^[a-z0-9]+([_-][a-z0-9]+)*$/, { + message: "TaxCode must contain only lowercase letters, numbers, and underscores", + }) + .min(TaxCode.MIN_LENGTH, { + message: `TaxCode must be at least ${TaxCode.MIN_LENGTH} characters long`, + }) + .max(TaxCode.MAX_LENGTH, { + message: `TaxCode must be at most ${TaxCode.MAX_LENGTH} characters long`, + }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = TaxCode.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.issues[0].message)); + } + // biome-ignore lint/style/noNonNullAssertion: + return Result.ok(new TaxCode({ value: valueIsValid.data! })); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive(): string { + return this.getValue(); + } +} diff --git a/packages/rdx-ddd/src/value-objects/text-value.ts b/packages/rdx-ddd/src/value-objects/text-value.ts new file mode 100644 index 00000000..7ea1d397 --- /dev/null +++ b/packages/rdx-ddd/src/value-objects/text-value.ts @@ -0,0 +1,39 @@ +import { Result } from "@repo/rdx-utils"; +import * as z from "zod/v4"; +import { ValueObject } from "./value-object"; + +interface TextValueProps { + value: string; +} + +export class TextValue extends ValueObject { + private static readonly MAX_LENGTH = 4096; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .nonempty({ message: "Text must not be empty" }) + .max(TextValue.MAX_LENGTH, { + message: `Text must be at most ${TextValue.MAX_LENGTH} characters long`, + }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = TextValue.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.issues[0].message)); + } + return Result.ok(new TextValue({ value })); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive(): string { + return this.getValue(); + } +} diff --git a/packages/rdx-ddd/src/value-objects/tin-number.ts b/packages/rdx-ddd/src/value-objects/tin-number.ts index 5a1e6b6c..95dfa162 100644 --- a/packages/rdx-ddd/src/value-objects/tin-number.ts +++ b/packages/rdx-ddd/src/value-objects/tin-number.ts @@ -1,4 +1,4 @@ -import { Maybe, Result } from "@repo/rdx-utils"; +import { Result } from "@repo/rdx-utils"; import * as z from "zod/v4"; import { ValueObject } from "./value-object"; @@ -33,14 +33,6 @@ export class TINNumber extends ValueObject { return Result.ok(new TINNumber({ value: valueIsValid.data })); } - static createNullable(value?: string): Result, Error> { - if (!value || value.trim() === "") { - return Result.ok(Maybe.none()); - } - - return TINNumber.create(value).map((value) => Maybe.some(value)); - } - getValue(): string { return this.props.value; } diff --git a/packages/rdx-ddd/src/value-objects/url-address.ts b/packages/rdx-ddd/src/value-objects/url-address.ts new file mode 100644 index 00000000..f2f6076b --- /dev/null +++ b/packages/rdx-ddd/src/value-objects/url-address.ts @@ -0,0 +1,32 @@ +import { Result } from "@repo/rdx-utils"; +import * as z from "zod/v4"; +import { ValueObject } from "./value-object"; + +interface URLAddressProps { + value: string; +} + +export class URLAddress extends ValueObject { + static create(value: string): Result { + const valueIsValid = URLAddress.validate(value); + + if (!valueIsValid.success) { + return Result.fail(new Error(valueIsValid.error.issues[0].message)); + } + + return Result.ok(new URLAddress({ value: valueIsValid.data })); + } + + private static validate(value: string) { + const schema = z.url({ message: "Invalid URL format" }); + return schema.safeParse(value); + } + + getValue(): string { + return this.props.value; + } + + toPrimitive() { + return this.getValue(); + } +} diff --git a/packages/rdx-ddd/src/value-objects/utc-date.ts b/packages/rdx-ddd/src/value-objects/utc-date.ts index d51ed2d3..7ce4788b 100644 --- a/packages/rdx-ddd/src/value-objects/utc-date.ts +++ b/packages/rdx-ddd/src/value-objects/utc-date.ts @@ -2,14 +2,14 @@ import { Result } from "@repo/rdx-utils"; import * as z from "zod/v4"; import { ValueObject } from "./value-object"; -interface IUtcDateProps { +interface UtcDateProps { value: string; } -export class UtcDate extends ValueObject { +export class UtcDate extends ValueObject { private readonly date!: Date; - private constructor(props: IUtcDateProps) { + private constructor(props: UtcDateProps) { super(props); const { value: dateString } = props; this.date = Object.freeze(new Date(dateString)); diff --git a/packages/rdx-utils/src/helpers/index.ts b/packages/rdx-utils/src/helpers/index.ts index 1dcb86b5..aa394e8d 100644 --- a/packages/rdx-utils/src/helpers/index.ts +++ b/packages/rdx-utils/src/helpers/index.ts @@ -1,6 +1,7 @@ export * from "./collection"; export * from "./id-utils"; export * from "./maybe"; +export * from "./patch-field"; export * from "./result"; export * from "./result-collection"; export * from "./rule-validator"; diff --git a/packages/rdx-utils/src/helpers/maybe.ts b/packages/rdx-utils/src/helpers/maybe.ts index 390c090b..03560cab 100644 --- a/packages/rdx-utils/src/helpers/maybe.ts +++ b/packages/rdx-utils/src/helpers/maybe.ts @@ -43,4 +43,8 @@ export class Maybe { map(fn: (value: T) => U): Maybe { return this.isSome() ? Maybe.some(fn(this.value as T)) : Maybe.none(); } + + match(someFn: (value: T) => U, noneFn: () => U): U { + return this.isSome() ? someFn(this.value as T) : noneFn(); + } } diff --git a/packages/rdx-utils/src/helpers/patch-field.ts b/packages/rdx-utils/src/helpers/patch-field.ts new file mode 100644 index 00000000..dfee1ee8 --- /dev/null +++ b/packages/rdx-utils/src/helpers/patch-field.ts @@ -0,0 +1,38 @@ +import { isNullishOrEmpty } from "./utils"; + +// Tri-estado para PATCH: unset | set(Some) | set(None) +export class PatchField { + private constructor( + private readonly _isSet: boolean, + private readonly _value?: T | null + ) {} + + static unset(): PatchField { + return new PatchField(false); + } + + static set(value: T | null): PatchField { + return new PatchField(true, value); + } + + get isSet(): boolean { + return this._isSet; + } + + /** Devuelve el valor crudo (puede ser null) si isSet=true */ + get value(): T | null | undefined { + return this._value; + } + + /** Ejecuta una función solo si isSet=true */ + ifSet(fn: (v: T | null) => void): void { + if (this._isSet) fn(this._value ?? null); + } +} + +export function toPatchField(value: T | null | undefined): PatchField { + if (value === undefined) return PatchField.unset(); + // "" => null + if (isNullishOrEmpty(value)) return PatchField.set(null); + return PatchField.set(value as T); +} diff --git a/packages/rdx-utils/src/helpers/utils.ts b/packages/rdx-utils/src/helpers/utils.ts index 3f3d0750..7714abd3 100644 --- a/packages/rdx-utils/src/helpers/utils.ts +++ b/packages/rdx-utils/src/helpers/utils.ts @@ -1,3 +1,9 @@ +export function isNullishOrEmpty(input: unknown): boolean { + return ( + input === null || input === undefined || (typeof input === "string" && input.trim() === "") + ); +} + // Función genérica para asegurar valores básicos function ensure(value: T | undefined | null, defaultValue: T): T { return value ?? defaultValue; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ced62503..64990d5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,7 +170,7 @@ importers: version: 29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3) + version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -623,6 +623,9 @@ importers: typescript: specifier: ^5.8.3 version: 5.8.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.3)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4) packages/rdx-criteria: dependencies: @@ -2870,6 +2873,9 @@ packages: '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -2906,6 +2912,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/dinero.js@1.9.4': resolution: {integrity: sha512-mtJnan4ajy9MqvoJGVXu0tC9EAAzFjeoKc3d+8AW+H/Od9+8IiC59ymjrZF+JdTToyDvkLReacTsc50Z8eYr6Q==} @@ -3047,6 +3056,35 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -3153,6 +3191,10 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -3307,6 +3349,10 @@ packages: caniuse-lite@1.0.30001720: resolution: {integrity: sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -3329,6 +3375,10 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -3623,6 +3673,10 @@ packages: babel-plugin-macros: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -3806,6 +3860,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -3864,6 +3921,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -3883,6 +3943,10 @@ packages: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + expect@29.7.0: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4528,6 +4592,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -4732,6 +4799,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lower-case-first@1.0.2: resolution: {integrity: sha512-UuxaYakO7XeONbKrZf5FEgkantPf5DUqDayzP5VXZrtRPdH86s4kN47I8B3TW10S4QKiE3ziHNf3kRN//okHjA==} @@ -5161,6 +5231,13 @@ packages: pathe@0.2.0: resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + pause@0.0.1: resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} @@ -5689,6 +5766,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -5770,10 +5850,16 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + string-hash@1.1.3: resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} @@ -5820,6 +5906,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + styled-components@6.1.19: resolution: {integrity: sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==} engines: {node: '>= 16'} @@ -5907,6 +5996,9 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} @@ -5920,6 +6012,18 @@ packages: tinygradient@1.1.5: resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.3: + resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} + engines: {node: '>=14.0.0'} + title-case@2.1.1: resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} @@ -6230,6 +6334,11 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite-plugin-html@3.2.2: resolution: {integrity: sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==} peerDependencies: @@ -6283,6 +6392,34 @@ packages: yaml: optional: true + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -6310,6 +6447,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} @@ -8335,6 +8477,10 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 24.0.3 + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 + '@types/connect@3.4.38': dependencies: '@types/node': 24.0.3 @@ -8371,6 +8517,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/dinero.js@1.9.4': {} '@types/estree@1.0.7': {} @@ -8548,6 +8696,48 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.0.3)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 6.3.5(@types/node@24.0.3)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.0.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.3 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + abbrev@1.1.1: {} accepts@1.3.8: @@ -8639,6 +8829,8 @@ snapshots: array-union@2.1.0: {} + assertion-error@2.0.1: {} + ast-types@0.13.4: dependencies: tslib: 2.8.1 @@ -8843,6 +9035,14 @@ snapshots: caniuse-lite@1.0.30001720: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -8884,6 +9084,8 @@ snapshots: chardet@0.7.0: {} + check-error@2.1.1: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -9140,6 +9342,8 @@ snapshots: optionalDependencies: babel-plugin-macros: 3.1.0 + deep-eql@5.0.2: {} + deep-extend@0.6.0: {} deepmerge@4.3.1: {} @@ -9297,6 +9501,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -9371,6 +9577,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.7 + esutils@2.0.3: {} etag@1.8.1: {} @@ -9391,6 +9601,8 @@ snapshots: exit@0.1.2: {} + expect-type@1.2.2: {} + expect@29.7.0: dependencies: '@jest/expect-utils': 29.7.0 @@ -10230,7 +10442,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.0.3 + '@types/node': 22.15.32 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -10289,6 +10501,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@3.14.1: dependencies: argparse: 1.0.10 @@ -10469,6 +10683,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: {} + lower-case-first@1.0.2: dependencies: lower-case: 1.1.4 @@ -10883,6 +11099,10 @@ snapshots: pathe@0.2.0: {} + pathe@2.0.3: {} + + pathval@2.0.1: {} + pause@0.0.1: {} pg-connection-string@2.9.0: {} @@ -11406,6 +11626,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -11476,8 +11698,12 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + statuses@2.0.1: {} + std-env@3.9.0: {} + string-hash@1.1.3: {} string-length@4.0.2: @@ -11519,6 +11745,10 @@ snapshots: strip-json-comments@3.1.1: {} + strip-literal@3.0.0: + dependencies: + js-tokens: 9.0.1 + styled-components@6.1.19(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@emotion/is-prop-valid': 1.2.2 @@ -11629,6 +11859,8 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + tinycolor2@1.6.0: {} tinyexec@0.3.2: {} @@ -11643,6 +11875,12 @@ snapshots: '@types/tinycolor2': 1.4.6 tinycolor2: 1.6.0 + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.3: {} + title-case@2.1.1: dependencies: no-case: 2.3.2 @@ -11674,7 +11912,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3): + ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -11692,7 +11930,6 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.27.4) - esbuild: 0.25.5 jest-util: 29.7.0 ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3): @@ -11957,6 +12194,27 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite-node@3.2.4(@types/node@24.0.3)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.3.5(@types/node@24.0.3)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-plugin-html@3.2.2(vite@6.3.5(@types/node@22.15.32)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4)): dependencies: '@rollup/pluginutils': 4.2.1 @@ -12003,6 +12261,67 @@ snapshots: terser: 5.40.0 tsx: 4.19.4 + vite@6.3.5(@types/node@24.0.3)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4): + dependencies: + esbuild: 0.25.5 + fdir: 6.4.5(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.6 + rollup: 4.41.1 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 24.0.3 + fsevents: 2.3.3 + jiti: 2.4.2 + less: 4.3.0 + lightningcss: 1.30.1 + sass: 1.89.0 + stylus: 0.62.0 + terser: 5.40.0 + tsx: 4.19.4 + + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.0.3)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.0.3)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.1 + expect-type: 1.2.2 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.3.5(@types/node@24.0.3)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4) + vite-node: 3.2.4(@types/node@24.0.3)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 24.0.3 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + void-elements@3.1.0: {} walker@1.0.8: @@ -12032,6 +12351,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wide-align@1.1.5: dependencies: string-width: 4.2.3