This commit is contained in:
David Arranz 2025-09-02 12:55:45 +02:00
parent a0f75a4a8f
commit 0420b8e090
25 changed files with 130 additions and 262 deletions

View File

@ -22,7 +22,7 @@ export class CreateCustomersAssembler {
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()),
province: toEmptyString(address.province, (value) => value.toPrimitive()),
postal_code: toEmptyString(address.postalCode, (value) => value.toPrimitive()),
country: toEmptyString(address.country, (value) => value.toPrimitive()),

View File

@ -38,8 +38,6 @@ export class CreateCustomerUseCase {
const newCustomer = buildResult.data;
console.debug("Built new customer entity:", id, newCustomer);
// 4) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista
return this.transactionManager.complete(async (transaction: Transaction) => {
const existsGuard = await this.ensureNotExists(companyId, id, transaction);
@ -47,15 +45,12 @@ export class CreateCustomerUseCase {
return Result.fail(existsGuard.error);
}
console.debug("No existing customer with same ID found, proceeding to save.");
const saveResult = await this.service.saveCustomer(newCustomer, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
const viewDTO = this.assembler.toDTO(saveResult.data);
console.debug("Assembled view DTO:", viewDTO);
return Result.ok(viewDTO);
});

View File

@ -156,7 +156,6 @@ export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) {
}
if (errors.length > 0) {
console.error(errors);
return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors));
}
@ -175,30 +174,6 @@ export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) {
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!,
@ -224,7 +199,6 @@ export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) {
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

@ -22,7 +22,7 @@ export class GetCustomerAssembler {
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()),
province: toEmptyString(address.province, (value) => value.toPrimitive()),
postal_code: toEmptyString(address.postalCode, (value) => value.toPrimitive()),
country: toEmptyString(address.country, (value) => value.toPrimitive()),

View File

@ -17,7 +17,6 @@ export class GetCustomerUseCase {
) {}
public execute(params: GetCustomerUseCaseInput) {
console.log(params);
const { customer_id, companyId } = params;
const idOrError = UniqueID.create(customer_id);
@ -36,14 +35,11 @@ export class GetCustomerUseCase {
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

@ -1,4 +1,5 @@
import { Criteria } from "@repo/rdx-criteria/server";
import { toEmptyString } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils";
import { CustomerListResponsetDTO } from "../../../../common/dto";
import { Customer } from "../../../domain";
@ -12,64 +13,28 @@ export class ListCustomersAssembler {
id: customer.id.toPrimitive(),
company_id: customer.companyId.toPrimitive(),
reference: customer.reference.match(
(value) => value.toPrimitive(),
() => ""
),
reference: toEmptyString(customer.reference, (value) => value.toPrimitive()),
is_company: String(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(),
() => ""
),
trade_name: toEmptyString(customer.tradeName, (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(),
() => ""
),
tin: toEmptyString(customer.tin, (value) => value.toPrimitive()),
legal_record: customer.legalRecord.match(
(value) => value.toPrimitive(),
() => ""
),
street: toEmptyString(address.street, (value) => value.toPrimitive()),
street2: toEmptyString(address.street2, (value) => value.toPrimitive()),
city: toEmptyString(address.city, (value) => value.toPrimitive()),
province: 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.getAll().join(", "),
@ -79,7 +44,6 @@ export class ListCustomersAssembler {
metadata: {
entity: "customer",
id: customer.id.toPrimitive(),
//created_at: customer.createdAt.toPrimitive(),
//updated_at: customer.updatedAt.toPrimitive()
},

View File

@ -1,4 +1,5 @@
import { GetCustomerByIdResponseDTO as UpdateCustomerByIdResponseDTO } from "../../../../common/dto";
import { toEmptyString } from "@repo/rdx-ddd";
import { UpdateCustomerByIdResponseDTO } from "../../../../common/dto";
import { Customer } from "../../../domain";
export class UpdateCustomerAssembler {
@ -7,66 +8,32 @@ export class UpdateCustomerAssembler {
return {
id: customer.id.toPrimitive(),
reference: customer.reference.match(
(value) => value.toPrimitive(),
() => ""
),
company_id: customer.companyId.toPrimitive(),
is_company: customer.isCompany,
reference: toEmptyString(customer.reference, (value) => value.toPrimitive()),
is_company: String(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(),
() => ""
),
trade_name: toEmptyString(customer.tradeName, (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(),
() => ""
),
tin: toEmptyString(customer.tin, (value) => value.toPrimitive()),
legal_record: customer.legalRecord.match(
(value) => value.toPrimitive(),
() => ""
),
street: toEmptyString(address.street, (value) => value.toPrimitive()),
street2: toEmptyString(address.street2, (value) => value.toPrimitive()),
city: toEmptyString(address.city, (value) => value.toPrimitive()),
province: toEmptyString(address.province, (value) => value.toPrimitive()),
postal_code: toEmptyString(address.postalCode, (value) => value.toPrimitive()),
country: toEmptyString(address.country, (value) => value.toPrimitive()),
default_taxes: customer.defaultTaxes.map((item) => item.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.getAll().join(", "),
status: customer.isActive ? "active" : "inactive",
language_code: customer.languageCode.toPrimitive(),
@ -74,7 +41,7 @@ export class UpdateCustomerAssembler {
metadata: {
entity: "customer",
id: customer.id.toPrimitive(),
//id: customer.id.toPrimitive(),
//created_at: customer.createdAt.toPrimitive(),
//updated_at: customer.updatedAt.toPrimitive()
},

View File

@ -32,8 +32,8 @@ import { CustomerPatchProps } from "../../domain";
* 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)).
* - campo con valor null/"" se quita el valor -> set(None()),
* - campo con valor no-vacío se pone el nuevo valor -> set(Some(VO)).
*
* @param dto - DTO con los datos a cambiar en el cliente
* @returns Cambios en las propiedades del cliente
@ -173,16 +173,17 @@ export function mapDTOToUpdateCustomerPatchProps(dto: UpdateCustomerRequestDTO)
});
// PostalAddress
customerPatchProps.address = mapDTOToUpdatePostalAddressPatchProps(dto, errors);
const addressPatchProps = mapDTOToUpdatePostalAddressPatchProps(dto, errors);
if (addressPatchProps) {
customerPatchProps.address = addressPatchProps;
}
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 }));
}
}
@ -190,7 +191,7 @@ export function mapDTOToUpdateCustomerPatchProps(dto: UpdateCustomerRequestDTO)
function mapDTOToUpdatePostalAddressPatchProps(
dto: UpdateCustomerRequestDTO,
errors: ValidationErrorDetail[]
): PostalAddressPatchProps {
): PostalAddressPatchProps | undefined {
const postalAddressPatchProps: PostalAddressPatchProps = {};
toPatchField(dto.street).ifSet((street) => {
@ -241,5 +242,5 @@ function mapDTOToUpdatePostalAddressPatchProps(
);
});
return postalAddressPatchProps;
return Object.keys(postalAddressPatchProps).length > 0 ? postalAddressPatchProps : undefined;
}

View File

@ -86,55 +86,23 @@ export class Customer extends AggregateRoot<CustomerProps> {
return Result.ok(contact);
}
public update(partial: CustomerPatchProps): Result<Customer, Error> {
public update(partialCustomer: CustomerPatchProps): Result<Customer, Error> {
const { address: partialAddress, ...rest } = partialCustomer;
const updatedProps = {
...this.props,
...partial,
...rest,
} as CustomerProps;
if (partial.address) {
const updatedAddressOrError = PostalAddress.update(this.props.address, partial.address);
if (partialAddress) {
const updatedAddressOrError = this.address.update(partialAddress);
if (updatedAddressOrError.isFailure) {
return Result.fail(updatedAddressOrError.error);
}
updatedProps.address = updatedAddressOrError.data;
}
const updatedCustomer = new Customer(updatedProps, this.id);
return Result.ok(updatedCustomer);
}
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(),
};
return Customer.create(updatedProps, this.id);
}
public get isIndividual(): boolean {

View File

@ -7,8 +7,8 @@ import {
CustomerListRequestSchema,
DeleteCustomerByIdRequestSchema,
GetCustomerByIdRequestSchema,
UpdateCustomerParamsRequestSchema,
UpdateCustomerRequestSchema,
UpdateCustomerByIdParamsRequestSchema,
UpdateCustomerByIdRequestSchema,
} from "../../../common/dto";
import { getCustomerDependencies } from "../dependencies";
import {
@ -78,8 +78,8 @@ export const customersRouter = (params: ModuleParams) => {
router.put(
"/:customer_id",
//checkTabContext,
validateRequest(UpdateCustomerParamsRequestSchema, "params"),
validateRequest(UpdateCustomerRequestSchema, "body"),
validateRequest(UpdateCustomerByIdParamsRequestSchema, "params"),
validateRequest(UpdateCustomerByIdRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.build.update();
const controller = new UpdateCustomerController(useCase);

View File

@ -160,7 +160,6 @@ export class CustomerMapper
}
if (errors.length > 0) {
console.error(errors);
return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors));
}
@ -173,8 +172,6 @@ export class CustomerMapper
country: country!,
};
console.log(postalAddressProps);
const postalAddress = extractOrPushError(
PostalAddress.create(postalAddressProps),
"address",

View File

@ -30,17 +30,10 @@ export class CustomerRepository
async save(customer: Customer, transaction: Transaction): Promise<Result<Customer, Error>> {
try {
const data = this.mapper.mapToPersistence(customer);
console.debug("Saving customer to database:", data);
const [instance] = await CustomerModel.upsert(data, { transaction, returning: true });
console.debug("Customer saved successfully:", instance.toJSON());
const savedCustomer = this.mapper.mapToDomain(instance);
console.debug("Mapped customer to domain:", savedCustomer);
return savedCustomer;
} catch (err: unknown) {
console.error("Error saving customer:", err);
return Result.fail(translateSequelizeError(err));
}
}
@ -93,7 +86,6 @@ export class CustomerRepository
}
const customer = this.mapper.mapToDomain(row);
console.log(customer);
return customer;
} catch (error: any) {
return Result.fail(translateSequelizeError(error));
@ -131,7 +123,6 @@ export class CustomerRepository
return this.mapper.mapArrayToDomain(instances);
} catch (err: unknown) {
console.error(err);
return Result.fail(translateSequelizeError(err));
}
}
@ -151,14 +142,11 @@ export class CustomerRepository
transaction: Transaction
): Promise<Result<void>> {
try {
console.log(id, companyId);
const deleted = await CustomerModel.destroy({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
console.log(deleted);
return Result.ok<void>();
} catch (err: unknown) {
// , `Error deleting customer ${id} in company ${companyId}`

View File

@ -2,4 +2,4 @@ export * from "./create-customer.request.dto";
export * from "./customer-list.request.dto";
export * from "./delete-customer-by-id.request.dto";
export * from "./get-customer-by-id.request.dto";
export * from "./update-customer.request.dto";
export * from "./update-customer-by-id.request.dto";

View File

@ -1,10 +1,10 @@
import * as z from "zod/v4";
export const UpdateCustomerParamsRequestSchema = z.object({
export const UpdateCustomerByIdParamsRequestSchema = z.object({
customer_id: z.string(),
});
export const UpdateCustomerRequestSchema = z.object({
export const UpdateCustomerByIdRequestSchema = z.object({
reference: z.string().optional(),
is_company: z.string().optional(),
@ -31,4 +31,4 @@ export const UpdateCustomerRequestSchema = z.object({
currency_code: z.string().optional(),
});
export type UpdateCustomerRequestDTO = z.infer<typeof UpdateCustomerRequestSchema>;
export type UpdateCustomerRequestDTO = z.infer<typeof UpdateCustomerByIdRequestSchema>;

View File

@ -14,7 +14,7 @@ export const CreateCustomerResponseSchema = z.object({
street: z.string(),
street2: z.string(),
city: z.string(),
state: z.string(),
province: z.string(),
postal_code: z.string(),
country: z.string(),

View File

@ -14,7 +14,7 @@ export const CustomerListResponseSchema = createListViewResponseSchema(
street: z.string(),
city: z.string(),
state: z.string(),
province: z.string(),
postal_code: z.string(),
country: z.string(),

View File

@ -14,7 +14,7 @@ export const GetCustomerByIdResponseSchema = z.object({
street: z.string(),
street2: z.string(),
city: z.string(),
state: z.string(),
province: z.string(),
postal_code: z.string(),
country: z.string(),

View File

@ -1,3 +1,4 @@
export * from "./create-customer.result.dto";
export * from "./customer-list.response.dto";
export * from "./get-customer-by-id.response.dto";
export * from "./update-customer-by-id.response.dto";

View File

@ -0,0 +1,36 @@
import { MetadataSchema } from "@erp/core";
import * as z from "zod/v4";
export const UpdateCustomerByIdResponseSchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
reference: z.string(),
is_company: z.string(),
name: z.string(),
trade_name: z.string(),
tin: z.string(),
street: z.string(),
street2: z.string(),
city: z.string(),
province: z.string(),
postal_code: z.string(),
country: z.string(),
email: z.string(),
phone: z.string(),
fax: z.string(),
website: z.string(),
legal_record: z.string(),
default_taxes: z.string(),
status: z.string(),
language_code: z.string(),
currency_code: z.string(),
metadata: MetadataSchema.optional(),
});
export type UpdateCustomerByIdResponseDTO = z.infer<typeof UpdateCustomerByIdResponseSchema>;

View File

@ -61,10 +61,10 @@
"placeholder": "Enter postal code",
"description": "The postal code of the customer"
},
"state": {
"label": "State",
"placeholder": "Enter state",
"description": "The state of the customer"
"province": {
"label": "Province",
"placeholder": "Enter province",
"description": "The province of the customer"
},
"country": {
"label": "Country",

View File

@ -61,10 +61,10 @@
"placeholder": "Ingrese el código postal",
"description": "El código postal del cliente"
},
"state": {
"label": "Estado",
"placeholder": "Ingrese el estado",
"description": "El estado del cliente"
"province": {
"label": "Provincia",
"placeholder": "Ingrese la provincia",
"description": "La provincia del cliente"
},
"country": {
"label": "País",

View File

@ -39,7 +39,7 @@ const defaultCustomerData = {
city: "Madrid",
country: "ES",
postal_code: "28080",
state: "Madrid",
province: "Madrid",
lang_code: "es",
currency_code: "EUR",
legal_record: "Registro Mercantil de Madrid, Tomo 12345, Folio 67, Hoja M-123456",
@ -208,11 +208,11 @@ export const CustomerEditForm = ({
<TextField
control={form.control}
name='state'
name='province'
required
label={t("form_fields.state.label")}
placeholder={t("form_fields.state.placeholder")}
description={t("form_fields.state.description")}
label={t("form_fields.province.label")}
placeholder={t("form_fields.province.placeholder")}
description={t("form_fields.province.description")}
/>
<SelectField

View File

@ -1,6 +1,6 @@
import * as z from "zod/v4";
import { UpdateCustomerRequestSchema } from "../../../common";
import { UpdateCustomerByIdRequestSchema } from "../../../common";
export const CustomerDataFormSchema = UpdateCustomerRequestSchema;
export const CustomerDataFormSchema = UpdateCustomerByIdRequestSchema;
export type CustomerData = z.infer<typeof CustomerDataFormSchema>;

View File

@ -22,14 +22,14 @@ export function maybeFromNullableString(input?: string | null): Maybe<string> {
/** Maybe<T> -> null para transporte */
export function toNullable<T>(m: Maybe<T>, map?: (t: T) => any): any | null {
if (m.isNone()) return null;
if (!m || 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 "";
if (!m || m.isNone()) return "";
const v = m.unwrap() as T;
return map ? map(v) : String(v);
}

View File

@ -40,19 +40,13 @@ export class PostalAddress extends ValueObject<PostalAddressProps> {
return Result.ok(new PostalAddress(values));
}
static update(
oldAddress: PostalAddress,
data: Partial<PostalAddress>
): Result<PostalAddress, Error> {
return PostalAddress.create({
street: data.street ?? oldAddress.street,
street2: data.street2 ?? oldAddress.street2,
city: data.city ?? oldAddress.city,
postalCode: data.postalCode ?? oldAddress.postalCode,
province: data.province ?? oldAddress.province,
country: data.country ?? oldAddress.country,
// biome-ignore lint/complexity/noThisInStatic: <explanation>
}).getOrElse(this);
public update(partial: Partial<PostalAddressPatchProps>): Result<PostalAddress, Error> {
const updatedProps = {
...this.props,
...partial,
} as PostalAddressProps;
return PostalAddress.create(updatedProps);
}
get street(): Maybe<Street> {
@ -90,17 +84,4 @@ export class PostalAddress extends ValueObject<PostalAddressProps> {
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,
};
}
}