This commit is contained in:
David Arranz 2025-09-01 18:38:00 +02:00
parent 36e215ad9c
commit 7e63288e12
12 changed files with 46 additions and 44 deletions

View File

@ -59,7 +59,9 @@ function buildSequelize(): Sequelize {
idle: 10_000, idle: 10_000,
acquire: 30_000, acquire: 30_000,
}, },
// dialectOptions: { /* según dialecto (ssl, etc.) */ }, dialectOptions: {
timezone: ENV.APP_TIMEZONE,
},
} as const; } as const;
if (ENV.DATABASE_URL && ENV.DATABASE_URL.trim() !== "") { if (ENV.DATABASE_URL && ENV.DATABASE_URL.trim() !== "") {

View File

@ -5,9 +5,9 @@ import { RequestWithAuth } from "./auth-types";
export function mockUser(req: RequestWithAuth, res: Response, next: NextFunction) { export function mockUser(req: RequestWithAuth, res: Response, next: NextFunction) {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
req.user = { req.user = {
id: UniqueID.create("9e4dc5b3-96b9-4968-9490-14bd032fec5f").data, userId: UniqueID.create("9e4dc5b3-96b9-4968-9490-14bd032fec5f").data,
email: EmailAddress.create("dev@example.com").data, email: EmailAddress.create("dev@example.com").data,
companyId: UniqueID.create("1e4dc5b3-96b9-4968-9490-14bd032fec5f").data, companyId: UniqueID.create("5e4dc5b3-96b9-4968-9490-14bd032fec5f").data,
roles: ["admin"], roles: ["admin"],
}; };
} }

View File

@ -1,7 +1,7 @@
import { DomainError } from "./domain-error"; import { DomainError } from "./domain-error";
export class EntityNotFoundError extends DomainError { export class EntityNotFoundError extends DomainError {
constructor(entity: string, field: string, value: string, options?: ErrorOptions) { constructor(entity: string, field: string, value: any, options?: ErrorOptions) {
super(`Entity '${entity}' with ${field} '${value}' was not found.`, options); super(`Entity '${entity}' with ${field} '${value}' was not found.`, options);
this.name = "EntityNotFoundError"; this.name = "EntityNotFoundError";
} }

View File

@ -9,6 +9,7 @@ import { mapDTOToCreateCustomerProps } from "./map-dto-to-create-customer-props"
type CreateCustomerUseCaseInput = { type CreateCustomerUseCaseInput = {
dto: CreateCustomerRequestDTO; dto: CreateCustomerRequestDTO;
companyId: UniqueID;
}; };
export class CreateCustomerUseCase { export class CreateCustomerUseCase {
@ -19,7 +20,7 @@ export class CreateCustomerUseCase {
) {} ) {}
public execute(params: CreateCustomerUseCaseInput) { public execute(params: CreateCustomerUseCaseInput) {
const { dto } = params; const { dto, companyId } = params;
// 1) Mapear DTO → props de dominio // 1) Mapear DTO → props de dominio
const dtoResult = mapDTOToCreateCustomerProps(dto); const dtoResult = mapDTOToCreateCustomerProps(dto);
@ -27,14 +28,12 @@ export class CreateCustomerUseCase {
return Result.fail(dtoResult.error); return Result.fail(dtoResult.error);
} }
const mapped = dtoResult.data; const { props, id } = dtoResult.data;
const id = mapped.id;
const { companyId, ...customerProps } = mapped.props;
console.debug("Creating customer with props:", customerProps); console.debug("Creating customer with props:", props);
// 3) Construir entidad de dominio // 3) Construir entidad de dominio
const buildResult = this.service.buildCustomerInCompany(companyId, customerProps, id); const buildResult = this.service.buildCustomerInCompany(companyId, props, id);
if (buildResult.isFailure) { if (buildResult.isFailure) {
return Result.fail(buildResult.error); return Result.fail(buildResult.error);
} }

View File

@ -44,7 +44,7 @@ export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) {
const customerId = extractOrPushError(UniqueID.create(dto.id), "id", errors); const customerId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const companyId = extractOrPushError(UniqueID.create(dto.company_id), "company_id", errors); const companyId = extractOrPushError(UniqueID.create(dto.company_id), "company_id", errors);
const isCompany = dto.is_company; const isCompany = dto.is_company === "true";
const status = extractOrPushError(CustomerStatus.create(dto.status), "status", errors); const status = extractOrPushError(CustomerStatus.create(dto.status), "status", errors);
const reference = extractOrPushError( const reference = extractOrPushError(
maybeFromNullableVO(dto.reference, (value) => Name.create(value)), maybeFromNullableVO(dto.reference, (value) => Name.create(value)),
@ -146,7 +146,7 @@ export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) {
const defaultTaxes = new Collection<TaxCode>(); const defaultTaxes = new Collection<TaxCode>();
dto.default_taxes.map((taxCode, index) => { dto.default_taxes.split(",").map((taxCode, index) => {
const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors); const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors);
if (tax) { if (tax) {
defaultTaxes.add(tax!); defaultTaxes.add(tax!);

View File

@ -33,11 +33,13 @@ export class DeleteCustomerUseCase {
return Result.fail(existsCheck.error); return Result.fail(existsCheck.error);
} }
if (!existsCheck.data) { const customerExists = existsCheck.data;
if (!customerExists) {
return Result.fail(new EntityNotFoundError("Customer", "id", validId.toString())); return Result.fail(new EntityNotFoundError("Customer", "id", validId.toString()));
} }
return await this.service.deleteCustomerByIdInCompany(validId, transaction); return await this.service.deleteCustomerByIdInCompany(validId, companyId, transaction);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);
} }

View File

@ -13,7 +13,7 @@ export interface ICustomerRepository {
* Guarda (crea o actualiza) un Customer en la base de datos. * Guarda (crea o actualiza) un Customer en la base de datos.
* Retorna el objeto actualizado tras la operación. * Retorna el objeto actualizado tras la operación.
*/ */
save(customer: Customer, transaction?: any): Promise<Result<Customer, Error>>; save(customer: Customer, transaction: any): Promise<Result<Customer, Error>>;
/** /**
* Comprueba si existe un Customer con un `id` dentro de una `company`. * Comprueba si existe un Customer con un `id` dentro de una `company`.
@ -48,5 +48,5 @@ export interface ICustomerRepository {
* Elimina un Customer por su ID, dentro de una empresa. * Elimina un Customer por su ID, dentro de una empresa.
* Retorna `void` si se elimina correctamente, o `NotFoundError` si no existía. * Retorna `void` si se elimina correctamente, o `NotFoundError` si no existía.
*/ */
deleteByIdInCompany(companyId: UniqueID, id: UniqueID, transaction?: any): Promise<Result<void>>; deleteByIdInCompany(companyId: UniqueID, id: UniqueID, transaction: any): Promise<Result<void>>;
} }

View File

@ -1,5 +1,5 @@
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { UpdateCustomerRequestDTO } from "../../../../common/dto"; import { CreateCustomerRequestDTO } from "../../../../common/dto";
import { CreateCustomerUseCase } from "../../../application"; import { CreateCustomerUseCase } from "../../../application";
export class CreateCustomerController extends ExpressController { export class CreateCustomerController extends ExpressController {
@ -11,12 +11,9 @@ export class CreateCustomerController extends ExpressController {
protected async executeImpl() { protected async executeImpl() {
const companyId = this.getTenantId()!; // garantizado por tenantGuard const companyId = this.getTenantId()!; // garantizado por tenantGuard
const dto = this.req.body as UpdateCustomerRequestDTO; const dto = this.req.body as CreateCustomerRequestDTO;
// Inyectar empresa del usuario autenticado (ownership) const result = await this.useCase.execute({ dto, companyId });
dto.company_id = companyId.toString();
const result = await this.useCase.execute({ dto });
return result.match( return result.match(
(data) => this.created(data), (data) => this.created(data),

View File

@ -3,6 +3,7 @@ import { ILogger, ModuleParams, validateRequest } from "@erp/core/api";
import { Application, NextFunction, Request, Response, Router } from "express"; import { Application, NextFunction, Request, Response, Router } from "express";
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
import { import {
CreateCustomerRequestSchema,
CustomerListRequestSchema, CustomerListRequestSchema,
DeleteCustomerByIdRequestSchema, DeleteCustomerByIdRequestSchema,
GetCustomerByIdRequestSchema, GetCustomerByIdRequestSchema,
@ -15,6 +16,7 @@ import {
GetCustomerController, GetCustomerController,
ListCustomersController, ListCustomersController,
} from "./controllers"; } from "./controllers";
import { UpdateCustomerController } from "./controllers/update-customer.controller";
export const customersRouter = (params: ModuleParams) => { export const customersRouter = (params: ModuleParams) => {
const { app, database, baseRoutePath, logger } = params as { const { app, database, baseRoutePath, logger } = params as {
@ -64,7 +66,7 @@ export const customersRouter = (params: ModuleParams) => {
"/", "/",
//checkTabContext, //checkTabContext,
validateRequest(UpdateCustomerRequestSchema), validateRequest(CreateCustomerRequestSchema),
(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);
@ -72,15 +74,17 @@ export const customersRouter = (params: ModuleParams) => {
} }
); );
/*routes.put( router.put(
"/:customerId", "/:customerId",
validateAndParseBody(IUpdateCustomerRequestSchema), //checkTabContext,
checkTabContext,
validateRequest(UpdateCustomerRequestSchema),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
buildUpdateCustomerController().execute(req, res, next); const useCase = deps.build.update();
const controller = new UpdateCustomerController(useCase);
return controller.execute(req, res, next);
} }
);*/ );
router.delete( router.delete(
"/:id", "/:id",

View File

@ -56,7 +56,7 @@ export class CustomerRepository
async existsByIdInCompany( async existsByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
id: UniqueID, id: UniqueID,
transaction?: any transaction?: Transaction
): Promise<Result<boolean, Error>> { ): Promise<Result<boolean, Error>> {
try { try {
const count = await CustomerModel.count({ const count = await CustomerModel.count({
@ -80,7 +80,7 @@ export class CustomerRepository
async getByIdInCompany( async getByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
id: UniqueID, id: UniqueID,
transaction?: any transaction?: Transaction
): Promise<Result<Customer, Error>> { ): Promise<Result<Customer, Error>> {
try { try {
const row = await CustomerModel.findOne({ const row = await CustomerModel.findOne({
@ -113,7 +113,7 @@ export class CustomerRepository
async findByCriteriaInCompany( async findByCriteriaInCompany(
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: any transaction?: Transaction
): Promise<Result<Collection<Customer>>> { ): Promise<Result<Collection<Customer>>> {
try { try {
const converter = new CriteriaToSequelizeConverter(); const converter = new CriteriaToSequelizeConverter();
@ -124,8 +124,6 @@ export class CustomerRepository
company_id: companyId.toString(), company_id: companyId.toString(),
}; };
console.debug({ model: "CustomerModel", criteria, query });
const instances = await CustomerModel.findAll({ const instances = await CustomerModel.findAll({
...query, ...query,
transaction, transaction,
@ -150,17 +148,17 @@ export class CustomerRepository
async deleteByIdInCompany( async deleteByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
id: UniqueID, id: UniqueID,
transaction?: any transaction: Transaction
): Promise<Result<void>> { ): Promise<Result<void>> {
try { try {
console.log(id, companyId);
const deleted = await CustomerModel.destroy({ const deleted = await CustomerModel.destroy({
where: { id: id.toString(), company_id: companyId.toString() }, where: { id: id.toString(), company_id: companyId.toString() },
transaction, transaction,
}); });
if (deleted === 0) { console.log(deleted);
return Result.fail(new Error(`Customer with id ${id} not found in company ${companyId}.`));
}
return Result.ok<void>(); return Result.ok<void>();
} catch (err: unknown) { } catch (err: unknown) {
// , `Error deleting customer ${id} in company ${companyId}` // , `Error deleting customer ${id} in company ${companyId}`

View File

@ -5,7 +5,7 @@ export const CreateCustomerRequestSchema = z.object({
company_id: z.uuid(), company_id: z.uuid(),
reference: z.string().default(""), reference: z.string().default(""),
is_company: z.boolean().default(false), is_company: z.string().toLowerCase().default("false"),
name: z.string().default(""), name: z.string().default(""),
trade_name: z.string().default(""), trade_name: z.string().default(""),
tin: z.string().default(""), tin: z.string().default(""),
@ -24,10 +24,10 @@ export const CreateCustomerRequestSchema = z.object({
legal_record: z.string().default(""), legal_record: z.string().default(""),
default_taxes: z.array(z.string()).default([]), default_taxes: z.string().default(""),
status: z.string().default("active"), status: z.string().toLowerCase().default("active"),
language_code: z.string().default("es"), language_code: z.string().toLowerCase().default("es"),
currency_code: z.string().default("EUR"), currency_code: z.string().toUpperCase().default("EUR"),
}); });
export type CreateCustomerRequestDTO = z.infer<typeof CreateCustomerRequestSchema>; export type CreateCustomerRequestDTO = z.infer<typeof CreateCustomerRequestSchema>;

View File

@ -3,7 +3,7 @@ import * as z from "zod/v4";
export const UpdateCustomerRequestSchema = z.object({ export const UpdateCustomerRequestSchema = z.object({
reference: z.string().optional(), reference: z.string().optional(),
is_company: z.boolean().optional(), is_company: z.string().optional(),
name: z.string().optional(), name: z.string().optional(),
trade_name: z.string().optional(), trade_name: z.string().optional(),
tin: z.string().optional(), tin: z.string().optional(),
@ -22,7 +22,7 @@ export const UpdateCustomerRequestSchema = z.object({
legal_record: z.string().optional(), legal_record: z.string().optional(),
default_taxes: z.array(z.string()).optional(), // completo (sustituye), o null => vaciar default_taxes: z.string().optional(), // completo (sustituye), o null => vaciar
language_code: z.string().optional(), language_code: z.string().optional(),
currency_code: z.string().optional(), currency_code: z.string().optional(),
}); });