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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ export class CustomerService {
/** /**
* Guarda una instancia de Customer en persistencia. * 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. * @param transaction - Transacción activa para la operación.
* @returns Result<Customer, Error> - El agregado guardado o un error si falla 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> { async executeImpl(): Promise<any> {
const companyId = this.getTenantId()!; // garantizado por tenantGuard 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( return result.match(
(data) => this.ok(data), (data) => this.ok(data),

View File

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

View File

@ -11,10 +11,10 @@ export class UpdateCustomerController extends ExpressController {
protected async executeImpl() { protected async executeImpl() {
const companyId = this.getTenantId()!; // garantizado por tenantGuard 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 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( return result.match(
(data) => this.created(data), (data) => this.created(data),

View File

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

View File

@ -211,41 +211,42 @@ export class CustomerMapper
} }
public mapToPersistence(source: Customer, params?: MapperParamsType): CustomerCreationAttributes { public mapToPersistence(source: Customer, params?: MapperParamsType): CustomerCreationAttributes {
return { const customerValues: Partial<CustomerCreationAttributes> = {
id: source.id.toPrimitive(), id: source.id.toPrimitive(),
company_id: source.companyId.toPrimitive(), company_id: source.companyId.toPrimitive(),
reference: source.reference.match( reference: toNullable(source.reference, (reference) => reference.toPrimitive()),
(value) => value.toPrimitive(),
() => ""
),
is_company: source.isCompany, is_company: source.isCompany,
name: source.name.toPrimitive(), 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()), 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()), email: toNullable(source.email, (email) => email.toPrimitive()),
phone: toNullable(source.phone, (phone) => phone.toPrimitive()), phone: toNullable(source.phone, (phone) => phone.toPrimitive()),
fax: toNullable(source.fax, (fax) => fax.toPrimitive()), fax: toNullable(source.fax, (fax) => fax.toPrimitive()),
website: toNullable(source.website, (website) => website.toPrimitive()), website: toNullable(source.website, (website) => website.toPrimitive()),
legal_record: toNullable(source.legalRecord, (legal_record) => legal_record.toPrimitive()), legal_record: toNullable(source.legalRecord, (legalRecord) => legalRecord.toPrimitive()),
default_taxes: source.defaultTaxes.map((taxItem) => taxItem.toPrimitive()).join(", "),
default_taxes: source.defaultTaxes.map((item) => item.toPrimitive()).join(", "),
status: source.isActive ? "active" : "inactive", status: source.isActive ? "active" : "inactive",
language_code: source.languageCode.toPrimitive(), language_code: source.languageCode.toPrimitive(),
currency_code: source.currencyCode.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: { reference: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: "", defaultValue: null,
}, },
is_company: { is_company: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
@ -71,82 +71,82 @@ export default (database: Sequelize) => {
}, },
trade_name: { trade_name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: "", defaultValue: null,
}, },
tin: { tin: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: "", defaultValue: null,
}, },
street: { street: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: "", defaultValue: null,
}, },
street2: { street2: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: "", defaultValue: null,
}, },
city: { city: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: "", defaultValue: null,
}, },
province: { province: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: "", defaultValue: null,
}, },
postal_code: { postal_code: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: "", defaultValue: null,
}, },
country: { country: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: "", defaultValue: null,
}, },
email: { email: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: "", defaultValue: null,
validate: { validate: {
isEmail: true, isEmail: true,
}, },
}, },
phone: { phone: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: "", defaultValue: null,
}, },
fax: { fax: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: "", defaultValue: null,
}, },
website: { website: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: true,
defaultValue: "", defaultValue: null,
validate: { validate: {
isUrl: true, isUrl: true,
}, },
}, },
legal_record: { legal_record: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: false, defaultValue: null,
defaultValue: "", allowNull: true,
}, },
default_taxes: { default_taxes: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true,
defaultValue: null, defaultValue: null,
allowNull: true,
}, },
language_code: { language_code: {

View File

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

View File

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