This commit is contained in:
David Arranz 2025-09-02 10:57:41 +02:00
parent 7e63288e12
commit a0f75a4a8f
16 changed files with 104 additions and 94 deletions

View File

@ -30,8 +30,6 @@ export class CreateCustomerUseCase {
const { props, id } = dtoResult.data;
console.debug("Creating customer with props:", props);
// 3) Construir entidad de dominio
const buildResult = this.service.buildCustomerInCompany(companyId, props, id);
if (buildResult.isFailure) {
@ -40,18 +38,18 @@ export class CreateCustomerUseCase {
const newCustomer = buildResult.data;
console.debug("Built new customer entity:", newCustomer);
console.debug("Built new customer entity:", id, newCustomer);
// 4) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista
return this.transactionManager.complete(async (tx: Transaction) => {
const existsGuard = await this.ensureNotExists(companyId, id, tx);
return this.transactionManager.complete(async (transaction: Transaction) => {
const existsGuard = await this.ensureNotExists(companyId, id, transaction);
if (existsGuard.isFailure) {
return Result.fail(existsGuard.error);
}
console.debug("No existing customer with same ID found, proceeding to save.");
const saveResult = await this.service.saveCustomer(newCustomer, tx);
const saveResult = await this.service.saveCustomer(newCustomer, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}

View File

@ -23,7 +23,7 @@ import {
UniqueID,
maybeFromNullableVO,
} from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils";
import { Collection, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import { CreateCustomerRequestDTO } from "../../../common/dto";
import { CustomerProps, CustomerStatus } from "../../domain";
@ -146,12 +146,14 @@ export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) {
const defaultTaxes = new Collection<TaxCode>();
dto.default_taxes.split(",").map((taxCode, index) => {
const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors);
if (tax) {
defaultTaxes.add(tax!);
}
});
if (!isNullishOrEmpty(dto.default_taxes)) {
dto.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);

View File

@ -5,7 +5,7 @@ import { CustomerService } from "../../domain";
type DeleteCustomerUseCaseInput = {
companyId: UniqueID;
id: string;
customer_id: string;
};
export class DeleteCustomerUseCase {
@ -15,19 +15,23 @@ export class DeleteCustomerUseCase {
) {}
public execute(params: DeleteCustomerUseCaseInput) {
const { companyId, id } = params;
const { companyId, customer_id } = params;
const idOrError = UniqueID.create(id);
const idOrError = UniqueID.create(customer_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const validId = idOrError.data;
const customerId = idOrError.data;
return this.transactionManager.complete(async (transaction) => {
try {
const existsCheck = await this.service.existsByIdInCompany(companyId, validId, transaction);
const existsCheck = await this.service.existsByIdInCompany(
companyId,
customerId,
transaction
);
if (existsCheck.isFailure) {
return Result.fail(existsCheck.error);
@ -36,10 +40,10 @@ export class DeleteCustomerUseCase {
const customerExists = existsCheck.data;
if (!customerExists) {
return Result.fail(new EntityNotFoundError("Customer", "id", validId.toString()));
return Result.fail(new EntityNotFoundError("Customer", "id", customerId.toString()));
}
return await this.service.deleteCustomerByIdInCompany(validId, companyId, transaction);
return await this.service.deleteCustomerByIdInCompany(customerId, companyId, transaction);
} catch (error: unknown) {
return Result.fail(error as Error);
}

View File

@ -6,7 +6,7 @@ import { GetCustomerAssembler } from "./assembler";
type GetCustomerUseCaseInput = {
companyId: UniqueID;
id: string;
customer_id: string;
};
export class GetCustomerUseCase {
@ -18,19 +18,21 @@ export class GetCustomerUseCase {
public execute(params: GetCustomerUseCaseInput) {
console.log(params);
const { id, companyId } = params;
const { customer_id, companyId } = params;
const idOrError = UniqueID.create(id);
const idOrError = UniqueID.create(customer_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const customerId = idOrError.data;
return this.transactionManager.complete(async (transaction) => {
try {
const customerOrError = await this.service.getCustomerByIdInCompany(
companyId,
idOrError.data,
customerId,
transaction
);

View File

@ -164,7 +164,7 @@ export function mapDTOToUpdateCustomerPatchProps(dto: UpdateCustomerRequestDTO)
return;
}
defaultTaxes!.map((taxCode, index) => {
defaultTaxes!.split(",").forEach((taxCode, index) => {
const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors);
if (tax && customerPatchProps.defaultTaxes) {
customerPatchProps.defaultTaxes.add(tax);

View File

@ -8,7 +8,7 @@ import { mapDTOToUpdateCustomerPatchProps } from "./map-dto-to-update-customer-p
type UpdateCustomerUseCaseInput = {
companyId: UniqueID;
id: string;
customer_id: string;
dto: UpdateCustomerRequestDTO;
};
@ -20,9 +20,9 @@ export class UpdateCustomerUseCase {
) {}
public execute(params: UpdateCustomerUseCaseInput) {
const { companyId, id, dto } = params;
const { companyId, customer_id, dto } = params;
const idOrError = UniqueID.create(id);
const idOrError = UniqueID.create(customer_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}

View File

@ -26,7 +26,7 @@ export class CustomerService {
/**
* Guarda una instancia de Customer en persistencia.
*
* @param customer - El agregado a guardar.
* @param customer - El agregado a guardar (con el companyId ya asignado)
* @param transaction - Transacción activa para la operación.
* @returns Result<Customer, Error> - El agregado guardado o un error si falla la operación.
*/

View File

@ -10,9 +10,9 @@ export class DeleteCustomerController extends ExpressController {
async executeImpl(): Promise<any> {
const companyId = this.getTenantId()!; // garantizado por tenantGuard
const { id } = this.req.params;
const { customer_id } = this.req.params;
const result = await this.useCase.execute({ id, companyId });
const result = await this.useCase.execute({ customer_id, companyId });
return result.match(
(data) => this.ok(data),

View File

@ -10,11 +10,9 @@ export class GetCustomerController extends ExpressController {
protected async executeImpl() {
const companyId = this.getTenantId()!; // garantizado por tenantGuard
const { id } = this.req.params;
const { customer_id } = this.req.params;
console.log(id);
const result = await this.useCase.execute({ id, companyId });
const result = await this.useCase.execute({ customer_id, companyId });
return result.match(
(data) => this.ok(data),

View File

@ -11,10 +11,10 @@ export class UpdateCustomerController extends ExpressController {
protected async executeImpl() {
const companyId = this.getTenantId()!; // garantizado por tenantGuard
const { id } = this.req.params;
const { customer_id } = this.req.params;
const dto = this.req.body as UpdateCustomerRequestDTO;
const result = await this.useCase.execute({ id, companyId, dto });
const result = await this.useCase.execute({ customer_id, companyId, dto });
return result.match(
(data) => this.created(data),

View File

@ -7,6 +7,7 @@ import {
CustomerListRequestSchema,
DeleteCustomerByIdRequestSchema,
GetCustomerByIdRequestSchema,
UpdateCustomerParamsRequestSchema,
UpdateCustomerRequestSchema,
} from "../../../common/dto";
import { getCustomerDependencies } from "../dependencies";
@ -51,7 +52,7 @@ export const customersRouter = (params: ModuleParams) => {
);
router.get(
"/:id",
"/:customer_id",
//checkTabContext,
validateRequest(GetCustomerByIdRequestSchema, "params"),
@ -66,7 +67,7 @@ export const customersRouter = (params: ModuleParams) => {
"/",
//checkTabContext,
validateRequest(CreateCustomerRequestSchema),
validateRequest(CreateCustomerRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.build.create();
const controller = new CreateCustomerController(useCase);
@ -75,10 +76,10 @@ export const customersRouter = (params: ModuleParams) => {
);
router.put(
"/:customerId",
"/:customer_id",
//checkTabContext,
validateRequest(UpdateCustomerRequestSchema),
validateRequest(UpdateCustomerParamsRequestSchema, "params"),
validateRequest(UpdateCustomerRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.build.update();
const controller = new UpdateCustomerController(useCase);
@ -87,7 +88,7 @@ export const customersRouter = (params: ModuleParams) => {
);
router.delete(
"/:id",
"/:customer_id",
//checkTabContext,
validateRequest(DeleteCustomerByIdRequestSchema, "params"),

View File

@ -211,41 +211,42 @@ export class CustomerMapper
}
public mapToPersistence(source: Customer, params?: MapperParamsType): CustomerCreationAttributes {
return {
const customerValues: Partial<CustomerCreationAttributes> = {
id: source.id.toPrimitive(),
company_id: source.companyId.toPrimitive(),
reference: source.reference.match(
(value) => value.toPrimitive(),
() => ""
),
reference: toNullable(source.reference, (reference) => reference.toPrimitive()),
is_company: source.isCompany,
name: source.name.toPrimitive(),
trade_name: toNullable(source.tradeName, (trade_name) => trade_name.toPrimitive()),
trade_name: toNullable(source.tradeName, (tradeName) => tradeName.toPrimitive()),
tin: toNullable(source.tin, (tin) => tin.toPrimitive()),
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()),
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(", "),
legal_record: toNullable(source.legalRecord, (legalRecord) => legalRecord.toPrimitive()),
default_taxes: source.defaultTaxes.map((taxItem) => taxItem.toPrimitive()).join(", "),
status: source.isActive ? "active" : "inactive",
language_code: source.languageCode.toPrimitive(),
currency_code: source.currencyCode.toPrimitive(),
};
if (source.address) {
Object.assign(customerValues, {
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, (postalCode) =>
postalCode.toPrimitive()
),
country: toNullable(source.address.country, (country) => country.toPrimitive()),
});
}
return customerValues as CustomerCreationAttributes;
}
}

View File

@ -57,8 +57,8 @@ export default (database: Sequelize) => {
},
reference: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "",
allowNull: true,
defaultValue: null,
},
is_company: {
type: DataTypes.BOOLEAN,
@ -71,82 +71,82 @@ export default (database: Sequelize) => {
},
trade_name: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "",
allowNull: true,
defaultValue: null,
},
tin: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "",
allowNull: true,
defaultValue: null,
},
street: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "",
allowNull: true,
defaultValue: null,
},
street2: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "",
allowNull: true,
defaultValue: null,
},
city: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "",
allowNull: true,
defaultValue: null,
},
province: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "",
allowNull: true,
defaultValue: null,
},
postal_code: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "",
allowNull: true,
defaultValue: null,
},
country: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "",
allowNull: true,
defaultValue: null,
},
email: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "",
allowNull: true,
defaultValue: null,
validate: {
isEmail: true,
},
},
phone: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "",
allowNull: true,
defaultValue: null,
},
fax: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "",
allowNull: true,
defaultValue: null,
},
website: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "",
allowNull: true,
defaultValue: null,
validate: {
isUrl: true,
},
},
legal_record: {
type: DataTypes.TEXT,
allowNull: false,
defaultValue: "",
defaultValue: null,
allowNull: true,
},
default_taxes: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
allowNull: true,
},
language_code: {

View File

@ -7,7 +7,7 @@ import * as z from "zod/v4";
*/
export const DeleteCustomerByIdRequestSchema = z.object({
id: z.string(),
customer_id: z.string(),
});
export type DeleteCustomerByIdRequestDTO = z.infer<typeof DeleteCustomerByIdRequestSchema>;

View File

@ -2,12 +2,12 @@ import * as z from "zod/v4";
/**
* Este DTO es utilizado por el endpoint:
* `GET /customers/:id` (consultar una factura por ID).
* `GET /customers/:customer_id` (consultar una factura por ID).
*
*/
export const GetCustomerByIdRequestSchema = z.object({
id: z.string(),
customer_id: z.string(),
});
export type GetCustomerByIdRequestDTO = z.infer<typeof GetCustomerByIdRequestSchema>;

View File

@ -1,5 +1,9 @@
import * as z from "zod/v4";
export const UpdateCustomerParamsRequestSchema = z.object({
customer_id: z.string(),
});
export const UpdateCustomerRequestSchema = z.object({
reference: z.string().optional(),