Facturas de cliente y clientes

This commit is contained in:
David Arranz 2025-09-01 16:07:59 +02:00
parent 4a36097290
commit ad66580d85
82 changed files with 2121 additions and 1268 deletions

View File

@ -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);
}

View File

@ -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 {

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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",

View File

@ -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",

View File

@ -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);
}

View File

@ -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<TaxCode>();
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 }));
}
}

View File

@ -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);
}

View File

@ -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(),
};
}
}

View File

@ -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);

View File

@ -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";

View File

@ -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",

View File

@ -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
) {}

View File

@ -1,2 +0,0 @@
//export * from "./participantAddressFinder";
//export * from "./participantFinder";

View File

@ -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<ICustomerParticipantAddressRepository>
) => {
if (addressId.isNull()) {
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(
ApplicationServiceError.INVALID_REQUEST_PARAM,
`Participant address ID required`
)
);
}
const transaction = adapter.startTransaction();
let address: NullOr<ICustomerParticipantAddress> = null;
try {
await transaction.complete(async (t) => {
address = await repository({ transaction: t }).getById(addressId);
});
if (address === null) {
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(ApplicationServiceError.NOT_FOUND_ERROR, "", {
id: addressId.toString(),
entity: "participant address",
})
);
}
return Result.ok<ICustomerParticipantAddress>(address);
} catch (error: unknown) {
const _error = error as Error;
if (repository().isRepositoryError(_error)) {
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(
ApplicationServiceError.REPOSITORY_ERROR,
_error.message,
_error
)
);
}
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(
ApplicationServiceError.UNEXCEPTED_ERROR,
_error.message,
_error
)
);
}
};
*/

View File

@ -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<ICustomerParticipantRepository>
): Promise<CustomerCustomer | undefined> => {
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);
};
*/

View File

@ -0,0 +1 @@
export * from "./update-customer.assembler";

View File

@ -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()
},
};
}
}

View File

@ -1 +1,2 @@
export * from "./assembler";
export * from "./update-customer.use-case";

View File

@ -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<TaxCode>();
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;
}

View File

@ -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<IUpdateCustomerRequestDTO>
): Promise<Result<Customer, Error>> {
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<IUpdateCustomerRequestDTO>
): Result<Partial<ICustomerProps>, Error> {
const errors: Error[] = [];
const validatedData: Partial<ICustomerProps> = {};
// 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<CustomerItem>(
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<never, IUseCaseError> // Misc errors (value objects)
| Result<Customer, never>; // Success!
export class UpdateCustomerUseCase2
implements
IUseCase<{ id: UniqueID; data: IUpdateCustomer_DTO }, Promise<UpdateCustomerResponseOrError>>
{
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<T>(name: string) {
return this._repositoryManager.getRepository<T>(name);
}
private handleValidationFailure(
validationError: Error,
message?: string
): Result<never, IUseCaseError> {
return Result.fail<IUseCaseError>(
UseCaseError.create(
UseCaseError.INVALID_INPUT_DATA,
message ? message : validationError.message,
validationError
)
);
}
async execute(request: {
id: UniqueID;
data: IUpdateCustomer_DTO;
}): Promise<UpdateCustomerResponseOrError> {
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<ICustomerRepository>("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<IUseCaseError>(
UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, `Customer not found`, {
id: request.id.toString(),
entity: "customer",
})
);
}
return Result.ok<Customer>(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<IUseCaseError>(
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<CustomerItem>(
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<IContactRepository>("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<ICustomerRepository>("Customer");
try {
await transaction.complete(async (t) => {
const customerRepo = customerRepoBuilder({ transaction: t });
await customerRepo.save(customer);
});
return Result.ok<DraftCustomer>(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<never, IUseCaseError> {
return Result.fail<IUseCaseError>(
UseCaseError.create(UseCaseError.UNEXCEPTED_ERROR, error.message, error)
);
}
private handleRepositoryError(
error: BaseError,
repository: ICustomerRepository
): Result<never, IUseCaseError> {
const { message, details } = repository.handleRepositoryError(error);
return Result.fail<IUseCaseError>(
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, message, details)
);
}
}
*/

View File

@ -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<Name>;
isCompany: boolean;
name: string;
tradeName: string;
tin: TINNumber;
name: Name;
tradeName: Maybe<Name>;
tin: Maybe<TINNumber>;
address: PostalAddress;
email: EmailAddress;
phone: PhoneNumber;
fax: PhoneNumber;
website: string;
email: Maybe<EmailAddress>;
phone: Maybe<PhoneNumber>;
fax: Maybe<PhoneNumber>;
website: Maybe<URLAddress>;
legalRecord: string;
defaultTax: string[];
legalRecord: Maybe<TextValue>;
defaultTaxes: Collection<TaxCode>;
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<CustomerProps> implements ICustomer {
export type CustomerPatchProps = Partial<Omit<CustomerProps, "companyId" | "address">> & {
address?: PostalAddressPatchProps;
};
export class Customer extends AggregateRoot<CustomerProps> {
static create(props: CustomerProps, id?: UniqueID): Result<Customer, Error> {
const contact = new Customer(props, id);
@ -75,76 +86,122 @@ export class Customer extends AggregateRoot<CustomerProps> implements ICustomer
return Result.ok(contact);
}
update(partial: Partial<Omit<CustomerProps, "companyId">>): Result<Customer, Error> {
const updatedCustomer = new Customer({ ...this.props, ...partial }, this.id);
public update(partial: CustomerPatchProps): Result<Customer, Error> {
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<Name> {
return this.props.reference;
}
public get name(): Name {
return this.props.name;
}
public get tradeName(): Maybe<Name> {
return this.props.tradeName;
}
public get tin(): Maybe<TINNumber> {
return this.props.tin;
}
public get address(): PostalAddress {
return this.props.address;
}
public get email(): Maybe<EmailAddress> {
return this.props.email;
}
public get phone(): Maybe<PhoneNumber> {
return this.props.phone;
}
public get fax(): Maybe<PhoneNumber> {
return this.props.fax;
}
public get website(): Maybe<URLAddress> {
return this.props.website;
}
public get legalRecord(): Maybe<TextValue> {
return this.props.legalRecord;
}
public get defaultTaxes(): Collection<TaxCode> {
return this.props.defaultTaxes;
}
public get languageCode(): LanguageCode {
return this.props.languageCode;
}
public get currencyCode(): CurrencyCode {
return this.props.currencyCode;
}
}

View File

@ -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<CustomerProps, "companyId">,
customerId?: UniqueID
): Result<Customer, Error>;
/**
* Guarda un Customer (nuevo o modificado) en base de datos.
*/
saveCustomer(customer: Customer, transaction: any): Promise<Result<Customer, Error>>;
/**
* Comprueba si existe un Customer con ese ID en la empresa indicada.
*/
existsByIdInCompany(
companyId: UniqueID,
customerId: UniqueID,
transaction?: any
): Promise<Result<boolean, Error>>;
/**
* Lista todos los customers que cumplan el criterio, dentro de una empresa.
*/
findCustomerByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction?: any
): Promise<Result<Collection<Customer>, Error>>;
/**
* Recupera un Customer por su ID dentro de una empresa.
*/
getCustomerByIdInCompany(
companyId: UniqueID,
customerId: UniqueID,
transaction?: any
): Promise<Result<Customer>>;
/**
* Actualiza parcialmente los datos de un Customer.
*/
updateCustomerByIdInCompany(
companyId: UniqueID,
customerId: UniqueID,
partial: Partial<Omit<CustomerProps, "companyId">>,
transaction?: any
): Promise<Result<Customer, Error>>;
/**
* Elimina un Customer por ID dentro de una empresa.
*/
deleteCustomerByIdInCompany(
companyId: UniqueID,
customerId: UniqueID,
transaction?: any
): Promise<Result<void, Error>>;
}

View File

@ -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<Omit<CustomerProps, "companyId">>,
partial: CustomerPatchProps,
transaction?: any
): Promise<Result<Customer>> {
): Promise<Result<Customer, Error>> {
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);
}
/**

View File

@ -1,2 +1 @@
export * from "./customer-service.interface";
export * from "./customer.service";

View File

@ -1 +1 @@
export * from "./map-dto-to-customer-props";
export * from "../application/create-customer/map-dto-to-create-customer-props";

View File

@ -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 }));
}
}

View File

@ -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: {

View File

@ -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();

View File

@ -9,10 +9,10 @@ export class DeleteCustomerController extends ExpressController {
}
async executeImpl(): Promise<any> {
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),

View File

@ -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),

View File

@ -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)
);
}
}

View File

@ -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<any> {
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 = <Customer>result.object;
return this.ok<IUpdateCustomer_Response_DTO>(this.presenter.map(customer, this.context));
} catch (e: unknown) {
return this.fail(e as IServerError);
}
}
}

View File

@ -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);

View File

@ -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<TaxCode>();
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(),
};
}
}

View File

@ -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 },
],

View File

@ -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));
}

View File

@ -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"),
});

View File

@ -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<typeof UpdateCustomerRequestSchema>;

View File

@ -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<typeof CustomerCreationResponseSchema>;
export type CustomerCreationResponseDTO = z.infer<typeof CreateCustomerResponseSchema>;

View File

@ -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(),

View File

@ -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(),

View File

@ -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";

View File

@ -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<CreateCustomerRequestDTO, Error, Partial<CreateCustomerRequestDTO>>({
return useMutation<UpdateCustomerRequestDTO, Error, Partial<UpdateCustomerRequestDTO>>({
mutationFn: (data) => {
console.log(data);
return dataSource.createOne("customers", data);

View File

@ -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<typeof CustomerDataFormSchema>;

View File

@ -0,0 +1 @@
export * from "./normalizers";

View File

@ -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<T> usando fábrica VO */
export function maybeFromNullableVO<T>(
input: any,
voFactory: (raw: any) => Result<T>
): Result<Maybe<T>> {
if (isNullishOrEmpty(input)) return Result.ok(Maybe.none<T>());
const vo = voFactory(input);
return vo.isSuccess ? Result.ok(Maybe.some(vo.data)) : Result.fail(vo.error);
}
/** string | null | undefined -> Maybe<string> (trim, vacío => None) */
export function maybeFromNullableString(input?: string | null): Maybe<string> {
if (isNullishOrEmpty(input)) return Maybe.none<string>();
const t = (input as string).trim();
return t ? Maybe.some(t) : Maybe.none<string>();
}
/** Maybe<T> -> null para transporte */
export function toNullable<T>(m: Maybe<T>, 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<T> -> "" para transporte */
export function toEmptyString<T>(m: Maybe<T>, map?: (t: T) => string): string {
if (m.isNone()) return "";
const v = m.unwrap() as T;
return map ? map(v) : String(v);
}

View File

@ -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";

View File

@ -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");

View File

@ -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");

View File

@ -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);

View File

@ -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;

View File

@ -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");
});
});

View File

@ -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);

View File

@ -1,10 +1,10 @@
import { ValueObject } from "./value-object";
interface ITestValueProps {
interface TestValueProps {
value: string;
}
class TestValueObject extends ValueObject<ITestValueProps> {
class TestValueObject extends ValueObject<TestValueProps> {
constructor(value: string) {
super({ value });
}

View File

@ -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<CityProps> {
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();
}
}

View File

@ -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<CountryProps> {
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();
}
}

View File

@ -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<CurrencyCodeProps> {
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<CurrencyCode, Error> {
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;
}
}

View File

@ -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<EmailAddressProps> {
return Result.ok(new EmailAddress({ value: valueIsValid.data }));
}
static createNullable(value?: string): Result<Maybe<EmailAddress>, Error> {
if (!value || value.trim() === "") {
return Result.ok(Maybe.none<EmailAddress>());
}
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);
}

View File

@ -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";

View File

@ -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<LanguageCodeProps> {
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<LanguageCode, Error> {
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;
}
}

View File

@ -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<IMoneyValueProps> implements IMoneyValue {
export class MoneyValue extends ValueObject<MoneyValueProps> 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<IMoneyValueProps> 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<IMoneyValueProps> implements IMoneyV
return this.dinero.getPrecision();
}
getValue(): IMoneyValueProps {
getValue(): MoneyValueProps {
return this.props;
}

View File

@ -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<INameProps> {
export class Name extends ValueObject<NameProps> {
private static readonly MAX_LENGTH = 255;
protected static validate(value: string) {
@ -26,14 +26,6 @@ export class Name extends ValueObject<INameProps> {
return Result.ok(new Name({ value }));
}
static createNullable(value?: string): Result<Maybe<Name>, Error> {
if (!value || value.trim() === "") {
return Result.ok(Maybe.none<Name>());
}
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("");

View File

@ -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<IPercentageProps> implements IPercentage {
export class Percentage extends ValueObject<PercentageProps> {
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<IPercentageProps> 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<IPercentageProps> implements IPercen
return this.props.scale;
}
getValue(): IPercentageProps {
getValue(): PercentageProps {
return this.props;
}

View File

@ -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<Street>;
street2: Maybe<Street>;
city: Maybe<City>;
postalCode: Maybe<PostalCode>;
province: Maybe<Province>;
country: Maybe<Country>;
}
export class PostalAddress extends ValueObject<IPostalAddressProps> {
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<PostalAddressProps>;
export class PostalAddress extends ValueObject<PostalAddressProps> {
protected static validate(values: PostalAddressProps) {
return Result.ok(values);
}
static create(values: IPostalAddressProps): Result<PostalAddress, Error> {
static create(values: PostalAddressProps): Result<PostalAddress, Error> {
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<Maybe<PostalAddress>, Error> {
if (!values || Object.values(values).every((value) => value.trim() === "")) {
return Result.ok(Maybe.none<PostalAddress>());
}
return PostalAddress.create(values).map((value) => Maybe.some(value));
}
static update(
oldAddress: PostalAddress,
data: Partial<PostalAddress>
@ -66,37 +49,37 @@ export class PostalAddress extends ValueObject<IPostalAddressProps> {
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: <explanation>
}).getOrElse(this);
}
get street(): string {
get street(): Maybe<Street> {
return this.props.street;
}
get street2(): string {
return this.props.street2 ?? "";
get street2(): Maybe<Street> {
return this.props.street2;
}
get city(): string {
get city(): Maybe<City> {
return this.props.city;
}
get postalCode(): string {
get postalCode(): Maybe<PostalCode> {
return this.props.postalCode;
}
get state(): string {
return this.props.state;
get province(): Maybe<Province> {
return this.props.province;
}
get country(): string {
get country(): Maybe<Country> {
return this.props.country;
}
getValue(): IPostalAddressProps {
getValue(): PostalAddressProps {
return this.props;
}
@ -104,7 +87,20 @@ export class PostalAddress extends ValueObject<IPostalAddressProps> {
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,
};
}
}

View File

@ -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<PostalCodeProps> {
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<PostalCode, Error> {
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;
}
}

View File

@ -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<ProvinceProps> {
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();
}
}

View File

@ -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<Quantity, Error>;
decrement(anotherQuantity?: Quantity): Result<Quantity, Error>;
hasSameScale(otherQuantity: Quantity): boolean;
convertScale(newScale: number): Result<Quantity, Error>;
}
export class Quantity extends ValueObject<IQuantityProps> implements IQuantity {
protected static validate(values: IQuantityProps) {
export class Quantity extends ValueObject<QuantityProps> {
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<IQuantityProps> 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<IQuantityProps> 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<IQuantityProps> implements IQuantity {
return this.props.scale;
}
getValue(): IQuantityProps {
getValue(): QuantityProps {
return this.props;
}

View File

@ -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<SlugProps> {
return Result.ok(new Slug({ value: valueIsValid.data! }));
}
static createNullable(value?: string): Result<Maybe<Slug>, Error> {
if (!value || value.trim() === "") {
return Result.ok(Maybe.none<Slug>());
}
return Slug.create(value).map((value: Slug) => Maybe.some(value));
}
getValue(): string {
return this.props.value;
}

View File

@ -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<StreetProps> {
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();
}
}

View File

@ -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<TaxCodeProps> {
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: <explanation>
return Result.ok(new TaxCode({ value: valueIsValid.data! }));
}
getValue(): string {
return this.props.value;
}
toPrimitive(): string {
return this.getValue();
}
}

View File

@ -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<TextValueProps> {
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();
}
}

View File

@ -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<TINNumberProps> {
return Result.ok(new TINNumber({ value: valueIsValid.data }));
}
static createNullable(value?: string): Result<Maybe<TINNumber>, Error> {
if (!value || value.trim() === "") {
return Result.ok(Maybe.none<TINNumber>());
}
return TINNumber.create(value).map((value) => Maybe.some(value));
}
getValue(): string {
return this.props.value;
}

View File

@ -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<URLAddressProps> {
static create(value: string): Result<URLAddress, Error> {
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();
}
}

View File

@ -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<IUtcDateProps> {
export class UtcDate extends ValueObject<UtcDateProps> {
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));

View File

@ -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";

View File

@ -43,4 +43,8 @@ export class Maybe<T> {
map<U>(fn: (value: T) => U): Maybe<U> {
return this.isSome() ? Maybe.some(fn(this.value as T)) : Maybe.none();
}
match<U>(someFn: (value: T) => U, noneFn: () => U): U {
return this.isSome() ? someFn(this.value as T) : noneFn();
}
}

View File

@ -0,0 +1,38 @@
import { isNullishOrEmpty } from "./utils";
// Tri-estado para PATCH: unset | set(Some) | set(None)
export class PatchField<T> {
private constructor(
private readonly _isSet: boolean,
private readonly _value?: T | null
) {}
static unset<T>(): PatchField<T> {
return new PatchField<T>(false);
}
static set<T>(value: T | null): PatchField<T> {
return new PatchField<T>(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<T>(value: T | null | undefined): PatchField<T> {
if (value === undefined) return PatchField.unset<T>();
// "" => null
if (isNullishOrEmpty(value)) return PatchField.set<T>(null);
return PatchField.set<T>(value as T);
}

View File

@ -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<T>(value: T | undefined | null, defaultValue: T): T {
return value ?? defaultValue;

View File

@ -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