diff --git a/apps/server/archive/contexts/auth/domain/services/auth.service.ts b/apps/server/archive/contexts/auth/domain/services/auth.service.ts index 472038a6..91bf4d9f 100644 --- a/apps/server/archive/contexts/auth/domain/services/auth.service.ts +++ b/apps/server/archive/contexts/auth/domain/services/auth.service.ts @@ -11,7 +11,7 @@ import { import { UniqueID } from "@/core/common/domain"; import { IAuthenticatedUserRepository, JWTPayload } from ".."; -import { JwtHelper } from "../../infraestructure/passport/jwt.helper"; +import { JwtHelper } from "../../../../../../../modules/auth/src/api/lib/passport/jwt.helper"; import { ITabContextRepository } from "../repositories/tab-context-repository.interface"; import { IAuthService } from "./auth-service.interface"; diff --git a/apps/server/archive/contexts/auth/infraestructure/index.ts b/apps/server/archive/contexts/auth/infraestructure/index.ts index f512c2c3..7e7c2a62 100644 --- a/apps/server/archive/contexts/auth/infraestructure/index.ts +++ b/apps/server/archive/contexts/auth/infraestructure/index.ts @@ -1,4 +1,4 @@ +export * from "../../../../../../modules/auth/src/api/lib/passport"; export * from "./mappers"; export * from "./middleware"; -export * from "./passport"; export * from "./sequelize"; diff --git a/apps/server/archive/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts b/apps/server/archive/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts index 09859bdf..d1425975 100644 --- a/apps/server/archive/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts +++ b/apps/server/archive/contexts/auth/infraestructure/middleware/passport-auth.middleware.ts @@ -2,9 +2,9 @@ import { UniqueID } from "@/core/common/domain"; import { ApiError, ExpressController } from "@/core/common/presentation"; //import { authProvider } from "@/contexts/auth/infraestructure"; import { NextFunction, Response } from "express"; +import { authProvider } from "../../../../../../../modules/auth/src/api/lib/passport"; import { AuthenticatedUser } from "../../domain"; import { AuthenticatedRequest } from "../express/types"; -import { authProvider } from "../passport"; // Comprueba el rol del usuario const _authorizeUser = (condition: (user: AuthenticatedUser) => boolean) => { @@ -63,6 +63,7 @@ export const checkUserIsAdminOrOwner = [ title: "Unauthorized", detail: "You are not authorized to access this resource.", }), + req, res ); }, diff --git a/apps/server/archive/contexts/auth/infraestructure/middleware/tab-context.middleware.ts b/apps/server/archive/contexts/auth/infraestructure/middleware/tab-context.middleware.ts index 9de9cfee..2642ce61 100644 --- a/apps/server/archive/contexts/auth/infraestructure/middleware/tab-context.middleware.ts +++ b/apps/server/archive/contexts/auth/infraestructure/middleware/tab-context.middleware.ts @@ -1,3 +1,3 @@ -import { authProvider } from "../passport"; +import { authProvider } from "../../../../../../../modules/auth/src/api/lib/passport"; export const checkTabContext = [authProvider.authenticateTabId()]; diff --git a/modules.bak/invoices/src/server/domain/value-objects/invoice-item-description.ts b/modules.bak/invoices/src/server/domain/value-objects/invoice-item-description.ts index 0e3aae95..a0e3c952 100644 --- a/modules.bak/invoices/src/server/domain/value-objects/invoice-item-description.ts +++ b/modules.bak/invoices/src/server/domain/value-objects/invoice-item-description.ts @@ -23,7 +23,7 @@ export class InvoiceItemDescription extends ValueObject { const valueIsValid = InvoiceNumber.validate(value); if (!valueIsValid.success) { - return Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.issues[0].message)); } return Result.ok(new InvoiceNumber({ value })); } diff --git a/modules.bak/invoices/src/server/domain/value-objects/invoice-serie.ts b/modules.bak/invoices/src/server/domain/value-objects/invoice-serie.ts index f0ead545..9b6ab0f2 100644 --- a/modules.bak/invoices/src/server/domain/value-objects/invoice-serie.ts +++ b/modules.bak/invoices/src/server/domain/value-objects/invoice-serie.ts @@ -23,7 +23,7 @@ export class InvoiceSerie extends ValueObject { const valueIsValid = InvoiceSerie.validate(value); if (!valueIsValid.success) { - return Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.issues[0].message)); } return Result.ok(new InvoiceSerie({ value })); } diff --git a/modules/auth/package.json b/modules/auth/package.json index 679bfa1d..fe3ae1c8 100644 --- a/modules/auth/package.json +++ b/modules/auth/package.json @@ -9,12 +9,14 @@ "peerDependencies": { "@erp/core": "workspace:*", "dinero.js": "^1.9.1", - + "express": "^4.18.2", + "i18next": "^25.1.1", "sequelize": "^6.37.5", "zod": "^3.25.67" }, "devDependencies": { "@biomejs/biome": "1.9.4", + "@types/express": "^4.17.21", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.3", "@types/react-i18next": "^8.1.0", @@ -22,11 +24,10 @@ }, "dependencies": { "@erp/core": "workspace:*", + "@repo/rdx-ddd": "workspace:*", "@repo/rdx-ui": "workspace:*", "@repo/shadcn-ui": "workspace:*", "@tanstack/react-query": "^5.74.11", - "express": "^4.18.2", - "i18next": "^25.1.1", "react": "^19.1.0", "react-dom": "^19.1.0", "react-hook-form": "^7.56.2", diff --git a/modules/auth/src/api/lib/express/auth-types.ts b/modules/auth/src/api/lib/express/auth-types.ts index dd98e65b..c868d3c9 100644 --- a/modules/auth/src/api/lib/express/auth-types.ts +++ b/modules/auth/src/api/lib/express/auth-types.ts @@ -1,10 +1,11 @@ +import { EmailAddress, UniqueID } from "@repo/rdx-ddd"; import { Request } from "express"; export type RequestUser = { - userId: string; - companyId: string; // tenant + userId: UniqueID; + companyId: UniqueID; roles?: string[]; - email?: string; + email?: EmailAddress; }; export type RequestWithAuth = Request & { diff --git a/modules/auth/src/api/lib/express/index.ts b/modules/auth/src/api/lib/express/index.ts index c53251a5..c4ccac27 100644 --- a/modules/auth/src/api/lib/express/index.ts +++ b/modules/auth/src/api/lib/express/index.ts @@ -1,2 +1,4 @@ export * from "./auth-types"; +export * from "./mock-user.middleware"; export * from "./tenancy.middleware"; +export * from "./user.middleware"; diff --git a/modules/auth/src/api/lib/express/mock-user.middleware.ts b/modules/auth/src/api/lib/express/mock-user.middleware.ts new file mode 100644 index 00000000..d9291eb7 --- /dev/null +++ b/modules/auth/src/api/lib/express/mock-user.middleware.ts @@ -0,0 +1,15 @@ +import { EmailAddress, UniqueID } from "@repo/rdx-ddd"; +import { NextFunction, Response } from "express"; +import { RequestWithAuth } from "./auth-types"; + +export function mockUser(req: RequestWithAuth, res: Response, next: NextFunction) { + if (process.env.NODE_ENV === "development") { + req.user = { + id: UniqueID.create("9e4dc5b3-96b9-4968-9490-14bd032fec5f").data, + email: EmailAddress.create("dev@example.com").data, + companyId: UniqueID.create("1e4dc5b3-96b9-4968-9490-14bd032fec5f").data, + roles: ["admin"], + }; + } + next(); +} diff --git a/modules/auth/src/api/lib/express/tenancy.middleware.ts b/modules/auth/src/api/lib/express/tenancy.middleware.ts index 78ffc527..130019b6 100644 --- a/modules/auth/src/api/lib/express/tenancy.middleware.ts +++ b/modules/auth/src/api/lib/express/tenancy.middleware.ts @@ -1,3 +1,4 @@ +import { ExpressController, UnauthorizedApiError } from "@erp/core/api"; import { NextFunction, Response } from "express"; import { RequestWithAuth } from "./auth-types"; @@ -9,22 +10,8 @@ export function enforceTenant() { return (req: RequestWithAuth, res: Response, next: NextFunction) => { // Validación básica del tenant if (!req.user || !req.user.companyId) { - return res.status(401).json({ error: "Unauthorized" }); + return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); } next(); }; } - -/** - * Mezcla el companyId del usuario en el criteria de listados, ignorando cualquier companyId entrante. - * Evita evasión de tenant por parámetros manipulados. - */ -export function scopeCriteriaWithCompany>( - criteria: T | undefined, - companyId: string -): T & { companyId: string } { - return { - ...(criteria ?? ({} as T)), - companyId, // fuerza el scope - }; -} diff --git a/modules/auth/src/api/lib/express/user.middleware.ts b/modules/auth/src/api/lib/express/user.middleware.ts new file mode 100644 index 00000000..5700e3b5 --- /dev/null +++ b/modules/auth/src/api/lib/express/user.middleware.ts @@ -0,0 +1,16 @@ +import { ExpressController, UnauthorizedApiError } from "@erp/core/api"; +import { NextFunction, Response } from "express"; +import { RequestWithAuth } from "./auth-types"; + +/** + * Middleware que exige presencia de usuario (sin validar companyId). + * Debe ir DESPUÉS del middleware de autenticación. + */ +export function enforceUser() { + return (req: RequestWithAuth, res: Response, next: NextFunction) => { + if (!req.user) { + return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res); + } + next(); + }; +} diff --git a/apps/server/archive/contexts/auth/infraestructure/passport/index.ts b/modules/auth/src/api/lib/passport/index.ts similarity index 65% rename from apps/server/archive/contexts/auth/infraestructure/passport/index.ts rename to modules/auth/src/api/lib/passport/index.ts index b92534d5..de1f64b4 100644 --- a/apps/server/archive/contexts/auth/infraestructure/passport/index.ts +++ b/modules/auth/src/api/lib/passport/index.ts @@ -1,8 +1,3 @@ -import { getDatabase } from "@/config"; -import { SequelizeTransactionManager } from "@/core/common/infrastructure"; -import { AuthService, TabContextService } from "../../domain/services"; -import { authenticatedUserMapper, tabContextMapper } from "../mappers"; -import { AuthenticatedUserRepository, TabContextRepository } from "../sequelize"; import { PassportAuthProvider } from "./passport-auth-provider"; const database = getDatabase(); diff --git a/apps/server/archive/contexts/auth/infraestructure/passport/jwt.helper.ts b/modules/auth/src/api/lib/passport/jwt.helper.ts similarity index 100% rename from apps/server/archive/contexts/auth/infraestructure/passport/jwt.helper.ts rename to modules/auth/src/api/lib/passport/jwt.helper.ts diff --git a/apps/server/archive/contexts/auth/infraestructure/passport/passport-auth-provider.ts b/modules/auth/src/api/lib/passport/passport-auth-provider.ts similarity index 88% rename from apps/server/archive/contexts/auth/infraestructure/passport/passport-auth-provider.ts rename to modules/auth/src/api/lib/passport/passport-auth-provider.ts index 262c40d5..ee3bd93f 100644 --- a/apps/server/archive/contexts/auth/infraestructure/passport/passport-auth-provider.ts +++ b/modules/auth/src/api/lib/passport/passport-auth-provider.ts @@ -1,14 +1,13 @@ import { NextFunction, Response } from "express"; -import { EmailAddress, UniqueID } from "@/core/common/domain"; -import { ITransactionManager } from "@/core/common/infrastructure/database"; -import { logger } from "@/core/logger"; -import { Result } from "@repo/rdx-utils"; import passport from "passport"; import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt"; -import { TabContext } from "../../domain"; -import { IAuthService, ITabContextService } from "../../domain/services"; -import { TabContextRequest } from "../express/types"; +import { TabContext } from "../../../../../../apps/server/archive/contexts/auth/domain"; +import { + IAuthService, + ITabContextService, +} from "../../../../../../apps/server/archive/contexts/auth/domain/services"; +import { TabContextRequest } from "../../../../../../apps/server/archive/contexts/auth/infraestructure/express/types"; const SECRET_KEY = process.env.JWT_SECRET || "supersecretkey"; diff --git a/modules/customers/src/api/application/helpers/extract-or-push-error.ts b/modules/core/src/api/helpers/extract-or-push-error.ts similarity index 100% rename from modules/customers/src/api/application/helpers/extract-or-push-error.ts rename to modules/core/src/api/helpers/extract-or-push-error.ts diff --git a/modules/core/src/api/helpers/index.ts b/modules/core/src/api/helpers/index.ts new file mode 100644 index 00000000..349bd8b5 --- /dev/null +++ b/modules/core/src/api/helpers/index.ts @@ -0,0 +1 @@ +export * from "./extract-or-push-error"; diff --git a/modules/core/src/api/index.ts b/modules/core/src/api/index.ts index e61c4ebe..a7952f34 100644 --- a/modules/core/src/api/index.ts +++ b/modules/core/src/api/index.ts @@ -1,5 +1,6 @@ export * from "./application"; export * from "./domain"; +export * from "./helpers"; export * from "./infrastructure"; export * from "./logger"; export * from "./modules"; diff --git a/modules/core/src/api/infrastructure/express/api-error-mapper.ts b/modules/core/src/api/infrastructure/express/api-error-mapper.ts index d61802b2..04931d43 100644 --- a/modules/core/src/api/infrastructure/express/api-error-mapper.ts +++ b/modules/core/src/api/infrastructure/express/api-error-mapper.ts @@ -40,6 +40,8 @@ export interface ApiErrorContext { instance?: string; // p.ej. req.originalUrl correlationId?: string; // p.ej. header 'x-correlation-id' method?: string; // GET/POST/PUT/DELETE + userId?: string; + tenantId?: string; } // ──────────────────────────────────────────────────────────────────────────────── @@ -166,12 +168,18 @@ const defaultRules: ReadonlyArray = [ // Fallback genérico (500) function defaultFallback(e: unknown): ApiError { - const message = typeof (e as any)?.message === "string" ? (e as any).message : "Unexpected error"; - return new InternalApiError(`Unexpected error: ${message}`); + if (e instanceof ApiError) { + return e; // ya es un ApiError + } + + const message = typeof (e as any)?.message === "string" ? (e as any).message : ""; + const detail = typeof (e as any)?.detail === "string" ? (e as any).detail : ""; + + return new InternalApiError(`${message} ${detail}`); } // ──────────────────────────────────────────────────────────────────────────────── -// Serializador opcional a Problem+JSON (si tu ApiError no lo trae ya) +// Serializador opcional a Problem+JSON // ──────────────────────────────────────────────────────────────────────────────── export function toProblemJson(apiError: ApiError, ctx?: ApiErrorContext) { const maybeErrors = (apiError as any).errors ? { errors: (apiError as any).errors } : {}; @@ -180,6 +188,8 @@ export function toProblemJson(apiError: ApiError, ctx?: ApiErrorContext) { title: apiError.title, status: apiError.status, detail: apiError.detail, + ...(ctx?.userId ? { userId: ctx.userId } : {}), + ...(ctx?.tenantId ? { tenantId: ctx.tenantId } : {}), ...(ctx?.instance ? { instance: ctx.instance } : {}), ...(ctx?.correlationId ? { correlationId: ctx.correlationId } : {}), ...(ctx?.method ? { method: ctx.method } : {}), diff --git a/modules/core/src/api/infrastructure/express/express-controller.ts b/modules/core/src/api/infrastructure/express/express-controller.ts index c99d57d2..eb30dc26 100644 --- a/modules/core/src/api/infrastructure/express/express-controller.ts +++ b/modules/core/src/api/infrastructure/express/express-controller.ts @@ -1,6 +1,8 @@ import { Criteria, CriteriaFromUrlConverter } from "@repo/rdx-criteria/server"; +import { UniqueID } from "@repo/rdx-ddd"; import { NextFunction, Request, Response } from "express"; import httpStatus from "http-status"; +import { ApiErrorContext, ApiErrorMapper, toProblemJson } from "./api-error-mapper"; import { ApiError, ConflictApiError, @@ -11,45 +13,92 @@ import { UnavailableApiError, ValidationApiError, } from "./errors"; - -type GuardResultLike = { isFailure: boolean; error?: ApiError }; -export type GuardContext = { - req: Request; - res: Response; - next: NextFunction; - controller: ExpressController; - criteria: Criteria; -}; -export type GuardFn = (ctx: GuardContext) => GuardResultLike | Promise; - -export function guardOk(): GuardResultLike { - return { isFailure: false }; -} - -export function guardFail(error: ApiError): GuardResultLike { - return { isFailure: true, error }; -} +import { GuardFn } from "./express-guards"; export abstract class ExpressController { protected req!: Request; protected res!: Response; protected next!: NextFunction; protected criteria!: Criteria; + protected url!: URL; + protected errorMapper!: ApiErrorMapper; // 🔹 Guards configurables por controlador private guards: GuardFn[] = []; - static errorResponse(apiError: ApiError, res: Response) { - return res.status(apiError.status).json(apiError); + static errorResponse(apiError: ApiError, req: Request, res: Response) { + const ctx = { + instance: req.originalUrl, + correlationId: req.get("x-correlation-id") || undefined, + method: req.method, + } satisfies ApiErrorContext; + + const body = toProblemJson(apiError, ctx); + return res.type("application/problem+json").status(apiError.status).json(body); + } + + public constructor() { + this.errorMapper = ApiErrorMapper.default(); + } + + // ─────────────────────────────────────────────────────────────────────────── + // Método principal + public async execute(req: Request, res: Response, next: NextFunction): Promise { + this.req = req; + this.res = res; + this.next = next; + this.url = this.buildUrlFromRequest(); + + try { + this.criteria = this.buildCriteriaFromURL(); + + // Ejecutar guards (auth/tenant/otros). Si alguno falla, se responde y no se entra al impl. + const ok = await this.runGuards(); + if (!ok) return; + + await this.executeImpl(); + } catch (error: unknown) { + const err = error as Error; + console.debug("❌ Unhandled error executing controller:", err.message); + this.handleError(new InternalApiError(err.message)); + } } protected abstract executeImpl(): Promise; - protected ok(dto?: T) { + // ─────────────────────────────────────────────────────────────────────────── + // Helpers de auth/tenant (opcionales para usar en executeImpl) + + public getUser(): T | undefined { + // Si usáis un tipo RequestWithAuth real, cámbialo aquí + return (this.req as any).user as T | undefined; + } + + public getTenantId(): UniqueID | undefined { + const user = this.getUser<{ companyId?: UniqueID }>(); + return user?.companyId; + } + + // ─────────────────────────────────────────────────────────────────────────── + + protected buildUrlFromRequest(): URL { + // Si Express está tras un proxy y trust proxy=true, estos headers son fiables + const host = this.req.get("x-forwarded-host") ?? this.req.get("host") ?? "localhost"; + const proto = this.req.get("x-forwarded-proto") ?? this.req.protocol ?? "http"; + return new URL(this.req.originalUrl || "/", `${proto}://${host}`); + } + + protected buildCriteriaFromURL() { + return new CriteriaFromUrlConverter().toCriteria(this.url); + } + + protected ok(dto?: T, headers?: Record) { + if (headers) Object.entries(headers).forEach(([k, v]) => this.res.setHeader(k, v)); return dto ? this.res.status(httpStatus.OK).json(dto) : this.res.sendStatus(httpStatus.OK); } - protected created(dto?: T) { + protected created(dto?: T, location?: string) { + if (location) this.res.setHeader("Location", location); return dto ? this.res.status(httpStatus.CREATED).json(dto) : this.res.sendStatus(httpStatus.CREATED); @@ -60,46 +109,51 @@ export abstract class ExpressController { } protected clientError(message: string, errors?: any[] | any) { - return ExpressController.errorResponse( - new ValidationApiError(message, Array.isArray(errors) ? errors : [errors]), - this.res + return this.handleApiError( + new ValidationApiError(message, Array.isArray(errors) ? errors : [errors]) ); } + protected unauthorizedError(message?: string) { - return ExpressController.errorResponse( - new UnauthorizedApiError(message ?? "Unauthorized"), - this.res - ); + return this.handleApiError(new UnauthorizedApiError(message ?? "Unauthorized")); } + protected forbiddenError(message?: string) { - return ExpressController.errorResponse( - new ForbiddenApiError(message ?? "You do not have permission to perform this action."), - this.res + return this.handleApiError( + new ForbiddenApiError(message ?? "You do not have permission to perform this action.") ); } + protected notFoundError(message: string) { - return ExpressController.errorResponse(new NotFoundApiError(message), this.res); + return this.handleApiError(new NotFoundApiError(message)); } - protected conflictError(message: string, _errors?: any[]) { - return ExpressController.errorResponse(new ConflictApiError(message), this.res); + + protected conflictError(message: string) { + return this.handleApiError(new ConflictApiError(message)); } + protected invalidInputError(message: string, errors?: any[]) { - return ExpressController.errorResponse(new ValidationApiError(message, errors), this.res); + return this.handleApiError(new ValidationApiError(message, errors)); } + protected unavailableError(message?: string) { - return ExpressController.errorResponse( - new UnavailableApiError(message ?? "Service temporarily unavailable."), - this.res + return this.handleApiError( + new UnavailableApiError(message ?? "Service temporarily unavailable.") ); } + protected internalServerError(message?: string) { - return ExpressController.errorResponse( - new InternalApiError(message ?? "Internal Server Error"), - this.res - ); + return this.handleApiError(new InternalApiError(message ?? "Internal Server Error")); } + protected handleApiError(apiError: ApiError) { - return ExpressController.errorResponse(apiError, this.res); + return ExpressController.errorResponse(apiError, this.req, this.res); + } + + protected handleError(error: unknown, ctx?: ApiErrorContext) { + const err = error instanceof Error ? error : new Error("Unknown error"); + const apiError = this.errorMapper.map(err, ctx); + return this.handleApiError(apiError); } // ─────────────────────────────────────────────────────────────────────────── @@ -126,47 +180,4 @@ export abstract class ExpressController { } return true; } - - // ─────────────────────────────────────────────────────────────────────────── - // Helpers de auth/tenant (opcionales para usar en executeImpl) - - public getUser(): T | undefined { - // Si usáis un tipo RequestWithAuth real, cámbialo aquí - return (this.req as any).user as T | undefined; - } - - public getTenantId(): string | undefined { - const user = this.getUser<{ companyId?: string }>(); - return user?.companyId; - } - - // ─────────────────────────────────────────────────────────────────────────── - // Método principal - public async execute(req: Request, res: Response, next: NextFunction): Promise { - this.req = req; - this.res = res; - this.next = next; - - try { - // Construcción robusta del URL base - const host = req.get("host") ?? "localhost"; - const proto = req.protocol || "http"; - const url = new URL(req.originalUrl, `${proto}://${host}`); - - this.criteria = new CriteriaFromUrlConverter().toCriteria(url); - - // Ejecutar guards (auth/tenant/otros). Si alguno falla, se responde y no se entra al impl. - const ok = await this.runGuards(); - if (!ok) return; - - await this.executeImpl(); - } catch (error: unknown) { - const err = error as Error; - if (err instanceof ApiError) { - ExpressController.errorResponse(err as ApiError, this.res); - } else { - ExpressController.errorResponse(new InternalApiError(err.message), this.res); - } - } - } } diff --git a/modules/core/src/api/infrastructure/express/express-guards.ts b/modules/core/src/api/infrastructure/express/express-guards.ts index e80adcc7..69833177 100644 --- a/modules/core/src/api/infrastructure/express/express-guards.ts +++ b/modules/core/src/api/infrastructure/express/express-guards.ts @@ -1,5 +1,26 @@ -import { ForbiddenApiError, UnauthorizedApiError, ValidationApiError } from "./errors"; -import { GuardContext, GuardFn, guardFail, guardOk } from "./express-controller"; +import { Criteria } from "@repo/rdx-criteria/server"; +import { NextFunction, Request, Response } from "express"; +import { ApiError, ForbiddenApiError, UnauthorizedApiError, ValidationApiError } from "./errors"; +import { ExpressController } from "./express-controller"; + +export type GuardResultLike = { isFailure: boolean; error?: ApiError }; + +export type GuardContext = { + req: Request; + res: Response; + next: NextFunction; + controller: ExpressController; + criteria: Criteria; +}; +export type GuardFn = (ctx: GuardContext) => GuardResultLike | Promise; + +export function guardOk(): GuardResultLike { + return { isFailure: false }; +} + +export function guardFail(error: ApiError): GuardResultLike { + return { isFailure: true, error }; +} // ─────────────────────────────────────────────────────────────────────────── // Guards reutilizables (auth/tenancy). Si prefieres, muévelos a src/lib/http/express-guards.ts diff --git a/modules/core/src/api/infrastructure/express/middlewares/global-error-handler.ts b/modules/core/src/api/infrastructure/express/middlewares/global-error-handler.ts index 8386f6be..f67e0709 100644 --- a/modules/core/src/api/infrastructure/express/middlewares/global-error-handler.ts +++ b/modules/core/src/api/infrastructure/express/middlewares/global-error-handler.ts @@ -1,6 +1,8 @@ import { NextFunction, Request, Response } from "express"; -import { ApiErrorMapper, toProblemJson } from "../api-error-mapper"; -import { ApiError } from "../errors/api-error"; +import { ApiErrorContext, ApiErrorMapper, toProblemJson } from "../api-error-mapper"; + +// ✅ Construye tu mapper una vez (composition root del adaptador HTTP) +export const apiErrorMapper = ApiErrorMapper.default(); export const globalErrorHandler = async ( error: Error, @@ -8,45 +10,23 @@ export const globalErrorHandler = async ( res: Response, next: NextFunction ) => { + console.error(`❌ Global unhandled error: ${error.message}`); + // Si ya se envió una respuesta, delegamos al siguiente error handler if (res.headersSent) { return next(error); } const ctx = { instance: req.originalUrl, - correlationId: (req.headers["x-correlation-id"] as string) || undefined, + correlationId: req.get("x-correlation-id") || undefined, method: req.method, - }; + } satisfies ApiErrorContext; - const apiError = ApiErrorMapper.map(err, ctx); + const apiError = apiErrorMapper.map(error, ctx); const body = toProblemJson(apiError, ctx); // 👇 Log interno con cause/traza (no lo exponemos al cliente) // logger.error({ err, cause: (err as any)?.cause, ...ctx }, `❌ Unhandled API error: ${error.message}`); res.status(apiError.status).json(body); - - //logger.error(`❌ Unhandled API error: ${error.message}`); - - // Verifica si el error es una instancia de ApiError - if (error instanceof ApiError) { - // Respuesta con formato RFC 7807 - return res.status(error.status).json({ - type: error.type, - title: error.title, - status: error.status, - detail: error.detail, - instance: error.instance ?? req.originalUrl, - errors: error.errors ?? [], // Aquí puedes almacenar validaciones, etc. - }); - } - - // Si no es un ApiError, lo tratamos como un error interno (500) - return res.status(500).json({ - type: "https://example.com/probs/internal-server-error", - title: "Internal Server Error", - status: 500, - detail: error.message || "Ha ocurrido un error inesperado.", - instance: req.originalUrl, - }); }; diff --git a/modules/core/src/api/infrastructure/express/middlewares/index.ts b/modules/core/src/api/infrastructure/express/middlewares/index.ts index 0d5d108a..b15e3be9 100644 --- a/modules/core/src/api/infrastructure/express/middlewares/index.ts +++ b/modules/core/src/api/infrastructure/express/middlewares/index.ts @@ -1,2 +1,2 @@ export * from "./global-error-handler"; -export * from "./validate-request"; +export * from "./validate-request.middleware"; diff --git a/modules/core/src/api/infrastructure/express/middlewares/validate-request.ts b/modules/core/src/api/infrastructure/express/middlewares/validate-request.middleware.ts similarity index 94% rename from modules/core/src/api/infrastructure/express/middlewares/validate-request.ts rename to modules/core/src/api/infrastructure/express/middlewares/validate-request.middleware.ts index 7e4cfd63..149079a9 100644 --- a/modules/core/src/api/infrastructure/express/middlewares/validate-request.ts +++ b/modules/core/src/api/infrastructure/express/middlewares/validate-request.middleware.ts @@ -45,7 +45,7 @@ export const validateRequest = ( if (!schema) { console.debug("ERROR: Undefined schema!!"); - return ExpressController.errorResponse(new InternalApiError("Undefined schema"), res); + return ExpressController.errorResponse(new InternalApiError("Undefined schema"), req, res); } try { @@ -62,6 +62,7 @@ export const validateRequest = ( return ExpressController.errorResponse( new ValidationApiError("Validation failed", validationErrors), + req, res ); } @@ -71,12 +72,14 @@ export const validateRequest = ( req[source] = result.data; } + console.debug(`Request ${source} is valid.`); + next(); } catch (err) { const error = err as Error; console.error(error); - return ExpressController.errorResponse(new InternalApiError(error.message), res); + return ExpressController.errorResponse(new InternalApiError(error.message), req, res); } }; }; diff --git a/modules/customer-invoices/src/api/infrastructure/dependencies.ts b/modules/customer-invoices/src/api/infrastructure/dependencies.ts index fb7d5cf6..0949b729 100644 --- a/modules/customer-invoices/src/api/infrastructure/dependencies.ts +++ b/modules/customer-invoices/src/api/infrastructure/dependencies.ts @@ -9,7 +9,7 @@ import { ListCustomerInvoicesAssembler, ListCustomerInvoicesUseCase, } from "../application"; -import { CustomerInvoiceService } from "../domain"; +import { CustomerInvoiceService, ICustomerInvoiceService } from "../domain"; import { CustomerInvoiceMapper } from "./mappers"; import { CustomerInvoiceRepository } from "./sequelize"; @@ -17,7 +17,7 @@ type InvoiceDeps = { transactionManager: SequelizeTransactionManager; repo: CustomerInvoiceRepository; mapper: CustomerInvoiceMapper; - service: CustomerInvoiceService; + service: ICustomerInvoiceService; assemblers: { list: ListCustomerInvoicesAssembler; get: GetCustomerInvoiceAssembler; @@ -38,7 +38,7 @@ type InvoiceDeps = { let _repo: CustomerInvoiceRepository | null = null; let _mapper: CustomerInvoiceMapper | null = null; -let _service: CustomerInvoiceService | null = null; +let _service: ICustomerInvoiceService | null = null; let _assemblers: InvoiceDeps["assemblers"] | null = null; export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/create-customer-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/create-customer-invoice.controller.ts index 30f76eba..fe2d1311 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/create-customer-invoice.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/create-customer-invoice.controller.ts @@ -1,10 +1,4 @@ -import { - ExpressController, - authGuard, - errorMapper, - forbidQueryFieldGuard, - tenantGuard, -} from "@erp/core/api"; +import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto"; import { CreateCustomerInvoiceUseCase } from "../../../application"; @@ -32,7 +26,7 @@ export class CreateCustomerInvoiceController extends ExpressController { return result.match( (data) => this.created(data), - (err) => this.handleApiError(errorMapper.toApiError(err)) + (err) => this.handleError(err) ); } } diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/delete-customer-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/delete-customer-invoice.controller.ts index b84cd3db..259b8560 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/delete-customer-invoice.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/delete-customer-invoice.controller.ts @@ -1,10 +1,4 @@ -import { - ExpressController, - authGuard, - errorMapper, - forbidQueryFieldGuard, - tenantGuard, -} from "@erp/core/api"; +import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { DeleteCustomerInvoiceUseCase } from "../../../application"; export class DeleteCustomerInvoiceController extends ExpressController { @@ -25,7 +19,7 @@ export class DeleteCustomerInvoiceController extends ExpressController { return result.match( (data) => this.ok(data), - (error) => this.handleApiError(errorMapper.toApiError(error)) + (err) => this.handleError(err) ); } } diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/get-customer-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/get-customer-invoice.controller.ts index 41c95645..2fc50f3b 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/get-customer-invoice.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/get-customer-invoice.controller.ts @@ -1,10 +1,4 @@ -import { - ExpressController, - authGuard, - errorMapper, - forbidQueryFieldGuard, - tenantGuard, -} from "@erp/core/api"; +import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { GetCustomerInvoiceUseCase } from "../../../application"; export class GetCustomerInvoiceController extends ExpressController { @@ -25,7 +19,7 @@ export class GetCustomerInvoiceController extends ExpressController { return result.match( (data) => this.ok(data), - (error) => this.handleApiError(errorMapper.toApiError(error)) + (err) => this.handleError(err) ); } } diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/list-customer-invoices.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/list-customer-invoices.controller.ts index ddadeaa9..26433456 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/list-customer-invoices.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/list-customer-invoices.controller.ts @@ -1,10 +1,4 @@ -import { - ExpressController, - authGuard, - errorMapper, - forbidQueryFieldGuard, - tenantGuard, -} from "@erp/core/api"; +import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { ListCustomerInvoicesUseCase } from "../../../application"; export class ListCustomerInvoicesController extends ExpressController { @@ -23,7 +17,7 @@ export class ListCustomerInvoicesController extends ExpressController { return result.match( (data) => this.ok(data), - (err) => this.handleApiError(errorMapper.toApiError(err)) + (err) => this.handleError(err) ); } } diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts index f66e62d8..d0c40194 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts @@ -1,4 +1,4 @@ -import { SequelizeRepository, errorMapper } from "@erp/core/api"; +import { SequelizeRepository } 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"; @@ -58,7 +58,7 @@ export class CustomerInvoiceRepository return Result.ok(Boolean(result)); } catch (err: unknown) { - return Result.fail(errorMapper.toDomainError(err)); + return Result.fail(translateSequelizeError(err)); } } @@ -79,7 +79,7 @@ export class CustomerInvoiceRepository await CustomerInvoiceModel.upsert(data, { transaction }); return Result.ok(invoice); } catch (err: unknown) { - return Result.fail(errorMapper.toDomainError(err)); + return Result.fail(translateSequelizeError(err)); } } @@ -100,7 +100,7 @@ export class CustomerInvoiceRepository return this.mapper.mapToDomain(rawData); } catch (err: unknown) { - return Result.fail(errorMapper.toDomainError(err)); + return Result.fail(translateSequelizeError(err)); } } @@ -128,7 +128,7 @@ export class CustomerInvoiceRepository return this.mapper.mapArrayToDomain(instances); } catch (err: unknown) { - return Result.fail(errorMapper.toDomainError(err)); + return Result.fail(translateSequelizeError(err)); } } @@ -144,7 +144,7 @@ export class CustomerInvoiceRepository await this._deleteById(CustomerInvoiceModel, id, false, transaction); return Result.ok(); } catch (err: unknown) { - return Result.fail(errorMapper.toDomainError(err)); + return Result.fail(translateSequelizeError(err)); } } } diff --git a/modules/customers/src/api/application/create-customer/assembler/create-customers.assembler.ts b/modules/customers/src/api/application/create-customer/assembler/create-customers.assembler.ts index afc9fc7c..8d7ffdc2 100644 --- a/modules/customers/src/api/application/create-customer/assembler/create-customers.assembler.ts +++ b/modules/customers/src/api/application/create-customer/assembler/create-customers.assembler.ts @@ -1,23 +1,35 @@ -import { Customer } from "@erp/customers/api/domain"; -import { CustomersCreationResultDTO } from "@erp/customers/common/dto"; +import { CustomerCreationResponseDTO } from "../../../../common"; +import { Customer } from "../../../domain"; export class CreateCustomersAssembler { - public toDTO(customer: Customer): CustomersCreationResultDTO { + public toDTO(customer: Customer): CustomerCreationResponseDTO { 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(), - customer_status: customer.status.toString(), - customer_number: customer.customerNumber.toString(), - customer_series: customer.customerSeries.toString(), - issue_date: customer.issueDate.toISOString(), - operation_date: customer.operationDate.toISOString(), - language_code: "ES", - currency: "EUR", + email: customer.email.toPrimitive(), + phone: customer.phone.toPrimitive(), + fax: customer.fax.toPrimitive(), + website: customer.website, - //subtotal_price: customer.calculateSubtotal().toPrimitive(), - //total_price: customer.calculateTotal().toPrimitive(), + default_tax: customer.defaultTax, + legal_record: customer.legalRecord, + lang_code: customer.langCode, + currency_code: customer.currencyCode, - //recipient: CustomerParticipantAssembler(customer.recipient), + 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, metadata: { entity: "customer", diff --git a/modules/customers/src/api/application/create-customer/create-customer.use-case.ts b/modules/customers/src/api/application/create-customer/create-customer.use-case.ts index 65da7e1d..9c15183e 100644 --- a/modules/customers/src/api/application/create-customer/create-customer.use-case.ts +++ b/modules/customers/src/api/application/create-customer/create-customer.use-case.ts @@ -1,14 +1,14 @@ import { DuplicateEntityError, ITransactionManager } from "@erp/core/api"; -import { CreateCustomerCommandDTO } from "@erp/customers/common/dto"; +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 { mapDTOToCustomerProps } from "../../helpers"; import { CreateCustomersAssembler } from "./assembler"; type CreateCustomerUseCaseInput = { - tenantId: string; - dto: CreateCustomerCommandDTO; + dto: CreateCustomerRequestDTO; }; export class CreateCustomerUseCase { @@ -19,46 +19,68 @@ export class CreateCustomerUseCase { ) {} public execute(params: CreateCustomerUseCaseInput) { - const { dto, tenantId: companyId } = params; + const { dto } = params; - const customerPropsOrError = mapDTOToCustomerProps(dto); - - if (customerPropsOrError.isFailure) { - return Result.fail(customerPropsOrError.error); + // 1) Mapear DTO → props de dominio + const dtoResult = mapDTOToCustomerProps(dto); + if (dtoResult.isFailure) { + return Result.fail(dtoResult.error); } - const { props, id } = customerPropsOrError.data; + const mapped = dtoResult.data; + const id = mapped.id; + const { companyId, ...customerProps } = mapped.props; - const customerOrError = this.service.build(props, id); + console.debug("Creating customer with props:", customerProps); - if (customerOrError.isFailure) { - return Result.fail(customerOrError.error); + // 3) Construir entidad de dominio + const buildResult = this.service.buildCustomerInCompany(companyId, customerProps, id); + if (buildResult.isFailure) { + return Result.fail(buildResult.error); } - const newCustomer = customerOrError.data; + const newCustomer = buildResult.data; - return this.transactionManager.complete(async (transaction: Transaction) => { - try { - const duplicateCheck = await this.service.existsById(id, transaction); + console.debug("Built new customer entity:", newCustomer); - if (duplicateCheck.isFailure) { - return Result.fail(duplicateCheck.error); - } - - if (duplicateCheck.data) { - return Result.fail(new DuplicateEntityError("Customer", id.toString())); - } - - const result = await this.service.save(newCustomer, idCompany, transaction); - if (result.isFailure) { - return Result.fail(result.error); - } - - const viewDTO = this.assembler.toDTO(newCustomer); - return Result.ok(viewDTO); - } catch (error: unknown) { - return Result.fail(error as Error); + // 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); + 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); + 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); }); } + + /** + Verifica que no exista un Customer con el mismo id en la companyId. + */ + private async ensureNotExists( + companyId: UniqueID, + id: UniqueID, + transaction: Transaction + ): Promise> { + const existsResult = await this.service.existsByIdInCompany(companyId, id, transaction); + if (existsResult.isFailure) { + return Result.fail(existsResult.error); + } + + if (existsResult.data) { + return Result.fail(new DuplicateEntityError("Customer", "id", String(id))); + } + + return Result.ok(undefined); + } } diff --git a/modules/customers/src/api/application/helpers/has-no-undefined-fields.ts b/modules/customers/src/api/application/helpers/has-no-undefined-fields.ts deleted file mode 100644 index beabd781..00000000 --- a/modules/customers/src/api/application/helpers/has-no-undefined-fields.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * - * @param obj - El objeto a evaluar. - * @template T - El tipo del objeto. - * @description Verifica si un objeto no tiene campos con valor undefined. - * - * Esta función recorre los valores del objeto y devuelve true si todos los valores son diferentes de undefined. - * Si al menos un valor es undefined, devuelve false. - * - * @example - * const obj = { a: 1, b: 'test', c: null }; - * console.log(hasNoUndefinedFields(obj)); // true - * - * const objWithUndefined = { a: 1, b: undefined, c: null }; - * console.log(hasNoUndefinedFields(objWithUndefined)); // false - * - * @template T - El tipo del objeto. - * @param obj - El objeto a evaluar. - * @returns true si el objeto no tiene campos undefined, false en caso contrario. - */ - -export function hasNoUndefinedFields>( - obj: T -): obj is { [K in keyof T]-?: Exclude } { - return Object.values(obj).every((value) => value !== undefined); -} - -/** - * - * @description Verifica si un objeto tiene campos con valor undefined. - * Esta función es el complemento de `hasNoUndefinedFields`. - * - * @example - * const obj = { a: 1, b: 'test', c: null }; - * console.log(hasUndefinedFields(obj)); // false - * - * const objWithUndefined = { a: 1, b: undefined, c: null }; - * console.log(hasUndefinedFields(objWithUndefined)); // true - * - * @template T - El tipo del objeto. - * @param obj - El objeto a evaluar. - * @returns true si el objeto tiene al menos un campo undefined, false en caso contrario. - * - */ - -export function hasUndefinedFields>( - obj: T -): obj is { [K in keyof T]-?: Exclude } { - return !hasNoUndefinedFields(obj); -} diff --git a/modules/customers/src/api/application/helpers/map-dto-to-customer-items-props.ts b/modules/customers/src/api/application/helpers/map-dto-to-customer-items-props.ts deleted file mode 100644 index 0f4e76cf..00000000 --- a/modules/customers/src/api/application/helpers/map-dto-to-customer-items-props.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ValidationErrorCollection, ValidationErrorDetail } from "@erp/core/api"; -import { CreateCustomerCommandDTO } from "@erp/customers/common/dto"; -import { Result } from "@repo/rdx-utils"; -import { - CustomerItem, - CustomerItemDescription, - CustomerItemDiscount, - CustomerItemQuantity, - CustomerItemUnitPrice, -} from "../../domain"; -import { extractOrPushError } from "./extract-or-push-error"; -import { hasNoUndefinedFields } from "./has-no-undefined-fields"; - -export function mapDTOToCustomerItemsProps( - dtoItems: Pick["items"] -): Result { - const errors: ValidationErrorDetail[] = []; - const items: CustomerItem[] = []; - - dtoItems.forEach((item, index) => { - const path = (field: string) => `items[${index}].${field}`; - - const description = extractOrPushError( - CustomerItemDescription.create(item.description), - path("description"), - errors - ); - - const quantity = extractOrPushError( - CustomerItemQuantity.create({ - amount: item.quantity.amount, - scale: item.quantity.scale, - }), - path("quantity"), - errors - ); - - const unitPrice = extractOrPushError( - CustomerItemUnitPrice.create({ - amount: item.unitPrice.amount, - scale: item.unitPrice.scale, - currency_code: item.unitPrice.currency, - }), - path("unit_price"), - errors - ); - - const discount = extractOrPushError( - CustomerItemDiscount.create({ - amount: item.discount.amount, - scale: item.discount.scale, - }), - path("discount"), - errors - ); - - if (errors.length === 0) { - const itemProps = { - description: description, - quantity: quantity, - unitPrice: unitPrice, - discount: discount, - }; - - if (hasNoUndefinedFields(itemProps)) { - // Validar y crear el item de factura - const itemOrError = CustomerItem.create(itemProps); - - if (itemOrError.isSuccess) { - items.push(itemOrError.data); - } else { - errors.push({ path: `items[${index}]`, message: itemOrError.error.message }); - } - } - } - - if (errors.length > 0) { - return Result.fail(new ValidationErrorCollection(errors)); - } - }); - - return Result.ok(items); -} diff --git a/modules/customers/src/api/application/helpers/map-dto-to-customer-props.ts b/modules/customers/src/api/application/helpers/map-dto-to-customer-props.ts deleted file mode 100644 index b446541b..00000000 --- a/modules/customers/src/api/application/helpers/map-dto-to-customer-props.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { ValidationErrorCollection, ValidationErrorDetail } from "@erp/core/api"; -import { UniqueID, UtcDate } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; -import { CreateCustomerCommandDTO } from "../../../common/dto"; -import { CustomerNumber, CustomerProps, CustomerSerie, CustomerStatus } from "../../domain"; -import { extractOrPushError } from "./extract-or-push-error"; -import { mapDTOToCustomerItemsProps } from "./map-dto-to-customer-items-props"; - -/** - * 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: CreateCustomerCommandDTO) { - const errors: ValidationErrorDetail[] = []; - - const customerId = extractOrPushError(UniqueID.create(dto.id), "id", errors); - - const customerNumber = extractOrPushError( - CustomerNumber.create(dto.customer_number), - "customer_number", - errors - ); - const customerSeries = extractOrPushError( - CustomerSerie.create(dto.customer_series), - "customer_series", - errors - ); - const issueDate = extractOrPushError(UtcDate.createFromISO(dto.issue_date), "issue_date", errors); - const operationDate = extractOrPushError( - UtcDate.createFromISO(dto.operation_date), - "operation_date", - errors - ); - - //const currency = extractOrPushError(Currency.(dto.currency), "currency", errors); - const currency = dto.currency; - - // 🔄 Validar y construir los items de factura con helper especializado - const itemsResult = mapDTOToCustomerItemsProps(dto.items); - if (itemsResult.isFailure) { - return Result.fail(itemsResult.error); - } - - if (errors.length > 0) { - return Result.fail(new ValidationErrorCollection(errors)); - } - - const customerProps: CustomerProps = { - customerNumber: customerNumber!, - customerSeries: customerSeries!, - issueDate: issueDate!, - operationDate: operationDate!, - status: CustomerStatus.createDraft(), - currency, - }; - - return Result.ok({ id: customerId!, props: customerProps }); - - /*if (hasNoUndefinedFields(customerProps)) { - const customerOrError = Customer.create(customerProps, customerId); - if (customerOrError.isFailure) { - return Result.fail(customerOrError.error); - } - return Result.ok(customerOrError.data); - } - - return Result.fail( - new ValidationErrorCollection([ - { path: "", message: "Error building from DTO: Some fields are undefined" }, - ]) - );*/ -} diff --git a/modules/customers/src/api/domain/aggregates/customer.ts b/modules/customers/src/api/domain/aggregates/customer.ts index e522f345..d8aa36e6 100644 --- a/modules/customers/src/api/domain/aggregates/customer.ts +++ b/modules/customers/src/api/domain/aggregates/customer.ts @@ -6,46 +6,55 @@ import { TINNumber, UniqueID, } from "@repo/rdx-ddd"; -import { Maybe, Result } from "@repo/rdx-utils"; +import { Result } from "@repo/rdx-utils"; +import { CustomerStatus } from "../value-objects"; export interface CustomerProps { companyId: UniqueID; + status: CustomerStatus; reference: string; + isCompany: boolean; name: string; + tradeName: string; tin: TINNumber; + address: PostalAddress; + email: EmailAddress; phone: PhoneNumber; + fax: PhoneNumber; + website: string; + legalRecord: string; - defaultTax: number; - status: string; + defaultTax: string[]; + langCode: string; currencyCode: string; - - tradeName: Maybe; - website: Maybe; - fax: Maybe; } export interface ICustomer { id: UniqueID; companyId: UniqueID; reference: string; - name: string; + tin: TINNumber; + name: string; + tradeName: string; + address: PostalAddress; + email: EmailAddress; phone: PhoneNumber; + fax: PhoneNumber; + website: string; + legalRecord: string; - defaultTax: number; + defaultTax: string[]; + langCode: string; currencyCode: string; - tradeName: Maybe; - fax: Maybe; - website: Maybe; - isIndividual: boolean; isCompany: boolean; isActive: boolean; @@ -103,11 +112,11 @@ export class Customer extends AggregateRoot implements ICustomer return this.props.phone; } - get fax(): Maybe { + get fax(): PhoneNumber { return this.props.fax; } - get website() { + get website(): string { return this.props.website; } @@ -136,6 +145,6 @@ export class Customer extends AggregateRoot implements ICustomer } get isActive(): boolean { - return this.props.status === "active"; + return this.props.status.isActive(); } } diff --git a/modules/customers/src/api/domain/services/customer-service.interface.ts b/modules/customers/src/api/domain/services/customer-service.interface.ts index 073f4f78..bcce2e0d 100644 --- a/modules/customers/src/api/domain/services/customer-service.interface.ts +++ b/modules/customers/src/api/domain/services/customer-service.interface.ts @@ -16,7 +16,7 @@ export interface ICustomerService { /** * Guarda un Customer (nuevo o modificado) en base de datos. */ - saveCustomerInCompany(customer: Customer, transaction: any): Promise>; + saveCustomer(customer: Customer, transaction: any): Promise>; /** * Comprueba si existe un Customer con ese ID en la empresa indicada. diff --git a/modules/customers/src/api/domain/services/customer.service.ts b/modules/customers/src/api/domain/services/customer.service.ts index 99f56723..017b98b7 100644 --- a/modules/customers/src/api/domain/services/customer.service.ts +++ b/modules/customers/src/api/domain/services/customer.service.ts @@ -7,13 +7,6 @@ import { ICustomerService } from "./customer-service.interface"; export class CustomerService implements ICustomerService { constructor(private readonly repository: ICustomerRepository) {} - findCustomerByCriteriaInCompany( - companyId: UniqueID, - criteria: Criteria, - transaction?: any - ): Promise, Error>> { - throw new Error("Method not implemented."); - } /** * Construye un nuevo agregado Customer a partir de props validadas. @@ -34,14 +27,11 @@ export class CustomerService implements ICustomerService { /** * Guarda una instancia de Customer en persistencia. * - * @param invoice - El agregado a guardar. + * @param customer - El agregado a guardar. * @param transaction - Transacción activa para la operación. * @returns Result - El agregado guardado o un error si falla la operación. */ - async saveCustomerInCompany( - customer: Customer, - transaction: any - ): Promise> { + async saveCustomer(customer: Customer, transaction: any): Promise> { return this.repository.save(customer, transaction); } @@ -71,11 +61,11 @@ export class CustomerService implements ICustomerService { * @param transaction - Transacción activa para la operación. * @returns Result, Error> - Colección de clientes o error. */ - async findCustomersByCriteriaInCompany( + async findCustomerByCriteriaInCompany( companyId: UniqueID, criteria: Criteria, transaction?: any - ): Promise>> { + ): Promise, Error>> { return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); } @@ -123,7 +113,7 @@ export class CustomerService implements ICustomerService { return Result.fail(updatedCustomer.error); } - return this.saveCustomerInCompany(updatedCustomer.data, transaction); + return this.saveCustomer(updatedCustomer.data, transaction); } /** diff --git a/modules/customers/src/api/domain/value-objects/customer-status.ts b/modules/customers/src/api/domain/value-objects/customer-status.ts index ca424353..f66d4a7f 100644 --- a/modules/customers/src/api/domain/value-objects/customer-status.ts +++ b/modules/customers/src/api/domain/value-objects/customer-status.ts @@ -6,65 +6,43 @@ interface ICustomerStatusProps { value: string; } -export enum INVOICE_STATUS { - DRAFT = "draft", - EMITTED = "emitted", - SENT = "sent", - RECEIVED = "received", - REJECTED = "rejected", +export enum CUSTOMER_STATUS { + ACTIVE = "active", + INACTIVE = "inactive", } export class CustomerStatus extends ValueObject { - private static readonly ALLOWED_STATUSES = ["draft", "emitted", "sent", "received", "rejected"]; - private static readonly FIELD = "invoiceStatus"; - private static readonly ERROR_CODE = "INVALID_INVOICE_STATUS"; + private static readonly ALLOWED_STATUSES = ["active", "inactive"]; + private static readonly FIELD = "status"; + private static readonly ERROR_CODE = "INVALID_STATUS"; private static readonly TRANSITIONS: Record = { - draft: [INVOICE_STATUS.EMITTED], - emitted: [INVOICE_STATUS.SENT, INVOICE_STATUS.REJECTED, INVOICE_STATUS.DRAFT], - sent: [INVOICE_STATUS.RECEIVED, INVOICE_STATUS.REJECTED], - received: [], - rejected: [], + active: [CUSTOMER_STATUS.INACTIVE], + inactive: [CUSTOMER_STATUS.ACTIVE], }; static create(value: string): Result { if (!CustomerStatus.ALLOWED_STATUSES.includes(value)) { - const detail = `Estado de la factura no válido: ${value}`; + const detail = `Status value not valid: ${value}`; return Result.fail( new DomainValidationError(CustomerStatus.ERROR_CODE, CustomerStatus.FIELD, detail) ); } return Result.ok( - value === "rejected" - ? CustomerStatus.createRejected() - : value === "sent" - ? CustomerStatus.createSent() - : value === "emitted" - ? CustomerStatus.createSent() - : value === "" - ? CustomerStatus.createReceived() - : CustomerStatus.createDraft() + value === "active" ? CustomerStatus.createActive() : CustomerStatus.createInactive() ); } - public static createDraft(): CustomerStatus { - return new CustomerStatus({ value: INVOICE_STATUS.DRAFT }); + public static createActive(): CustomerStatus { + return new CustomerStatus({ value: CUSTOMER_STATUS.ACTIVE }); } - public static createEmitted(): CustomerStatus { - return new CustomerStatus({ value: INVOICE_STATUS.EMITTED }); + public static createInactive(): CustomerStatus { + return new CustomerStatus({ value: CUSTOMER_STATUS.INACTIVE }); } - public static createSent(): CustomerStatus { - return new CustomerStatus({ value: INVOICE_STATUS.SENT }); - } - - public static createReceived(): CustomerStatus { - return new CustomerStatus({ value: INVOICE_STATUS.RECEIVED }); - } - - public static createRejected(): CustomerStatus { - return new CustomerStatus({ value: INVOICE_STATUS.REJECTED }); + isActive(): boolean { + return this.props.value === CUSTOMER_STATUS.ACTIVE; } getValue(): string { @@ -82,7 +60,7 @@ export class CustomerStatus extends ValueObject { transitionTo(nextStatus: string): Result { if (!this.canTransitionTo(nextStatus)) { return Result.fail( - new Error(`Transición no permitida de ${this.props.value} a ${nextStatus}`) + new Error(`Transition not allowed from ${this.props.value} to ${nextStatus}`) ); } return CustomerStatus.create(nextStatus); diff --git a/modules/customers/src/api/application/helpers/index.ts b/modules/customers/src/api/helpers/index.ts similarity index 100% rename from modules/customers/src/api/application/helpers/index.ts rename to modules/customers/src/api/helpers/index.ts diff --git a/modules/customers/src/api/helpers/map-dto-to-customer-props.ts b/modules/customers/src/api/helpers/map-dto-to-customer-props.ts new file mode 100644 index 00000000..02e1dfce --- /dev/null +++ b/modules/customers/src/api/helpers/map-dto-to-customer-props.ts @@ -0,0 +1,109 @@ +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 })); + } +} diff --git a/modules/customers/src/api/infrastructure/dependencies.ts b/modules/customers/src/api/infrastructure/dependencies.ts index 1a3d9ddc..99b6e7f8 100644 --- a/modules/customers/src/api/infrastructure/dependencies.ts +++ b/modules/customers/src/api/infrastructure/dependencies.ts @@ -18,7 +18,7 @@ type CustomerDeps = { transactionManager: SequelizeTransactionManager; repo: CustomerRepository; mapper: CustomerMapper; - service: CustomerService; + service: ICustomerService; assemblers: { list: ListCustomersAssembler; get: GetCustomerAssembler; diff --git a/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts index eba5887b..31beb591 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts @@ -1,10 +1,4 @@ -import { - ExpressController, - authGuard, - errorMapper, - forbidQueryFieldGuard, - tenantGuard, -} from "@erp/core/api"; +import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { CreateCustomerRequestDTO } from "../../../../common/dto"; import { CreateCustomerUseCase } from "../../../application"; @@ -16,18 +10,17 @@ export class CreateCustomerController extends ExpressController { } protected async executeImpl() { - const tenantId = this.getTenantId()!; // garantizado por tenantGuard + const companyId = this.getTenantId()!; // garantizado por tenantGuard const dto = this.req.body as CreateCustomerRequestDTO; - /* - // Inyectar empresa del usuario autenticado (ownership) - dto.customerCompanyId = user.companyId; - */ - const result = await this.useCase.execute({ tenantId, dto }); + // Inyectar empresa del usuario autenticado (ownership) + dto.company_id = companyId.toString(); + + const result = await this.useCase.execute({ dto }); return result.match( (data) => this.created(data), - (err) => this.handleApiError(errorMapper.toApiError(err)) + (err) => this.handleError(err) ); } } diff --git a/modules/customers/src/api/infrastructure/express/controllers/create-customer/index.ts b/modules/customers/src/api/infrastructure/express/controllers/create-customer/index.ts deleted file mode 100644 index 8d372703..00000000 --- a/modules/customers/src/api/infrastructure/express/controllers/create-customer/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { SequelizeTransactionManager } from "@erp/core/api"; -import { Sequelize } from "sequelize"; -import { CustomerMapper } from "../../.."; -import { CreateCustomerUseCase, CreateCustomersPresenter } from "../../../../application"; -import { CustomerService } from "../../../../domain"; -import { CreateCustomerController } from "./create-customer.controller"; - -export const buildCreateCustomersController = (database: Sequelize) => { - const transactionManager = new SequelizeTransactionManager(database); - const customerRepository = new customerRepository(database, new CustomerMapper()); - const customerService = new CustomerService(customerRepository); - const presenter = new CreateCustomersPresenter(); - - const useCase = new CreateCustomerUseCase(customerService, transactionManager, presenter); - - return new CreateCustomerController(useCase); -}; diff --git a/modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts index 7c0aa232..b36ccc44 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts @@ -1,10 +1,4 @@ -import { - ExpressController, - authGuard, - errorMapper, - forbidQueryFieldGuard, - tenantGuard, -} from "@erp/core/api"; +import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { DeleteCustomerUseCase } from "../../../application"; export class DeleteCustomerController extends ExpressController { @@ -22,7 +16,7 @@ export class DeleteCustomerController extends ExpressController { return result.match( (data) => this.ok(data), - (error) => this.handleApiError(errorMapper.toApiError(error)) + (err) => this.handleError(err) ); } } diff --git a/modules/customers/src/api/infrastructure/express/controllers/delete-customer/index.ts b/modules/customers/src/api/infrastructure/express/controllers/delete-customer/index.ts deleted file mode 100644 index a550c469..00000000 --- a/modules/customers/src/api/infrastructure/express/controllers/delete-customer/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ModuleParams } from "@erp/core/api"; -import { DeleteCustomerController } from "./delete-customer.controller"; - -export const buildDeleteCustomerController = (params: ModuleParams) => { - const deps = getCustomerDependencies(params); - - const useCase = deps.build.delete(); - return new DeleteCustomerController(useCase /*, deps.presenters.delete */); -}; diff --git a/modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts index 52dcb017..4dc61859 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts @@ -1,10 +1,4 @@ -import { - ExpressController, - authGuard, - errorMapper, - forbidQueryFieldGuard, - tenantGuard, -} from "@erp/core/api"; +import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { GetCustomerUseCase } from "../../../application"; export class GetCustomerController extends ExpressController { @@ -22,7 +16,7 @@ export class GetCustomerController extends ExpressController { return result.match( (data) => this.ok(data), - (error) => this.handleApiError(errorMapper.toApiError(error)) + (err) => this.handleError(err) ); } } diff --git a/modules/customers/src/api/infrastructure/express/controllers/get-customer/index.ts b/modules/customers/src/api/infrastructure/express/controllers/get-customer/index.ts deleted file mode 100644 index b6bf6819..00000000 --- a/modules/customers/src/api/infrastructure/express/controllers/get-customer/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { SequelizeTransactionManager } from "@erp/core/api"; -import { Sequelize } from "sequelize"; -import { CustomerRepository, customerMapper } from "../../.."; -import { GetCustomerUseCase, getCustomerPresenter } from "../../../../application"; -import { CustomerService } from "../../../../domain"; -import { GetCustomerController } from "../get-customer.controller"; - -export const buildGetCustomerController = (database: Sequelize) => { - const transactionManager = new SequelizeTransactionManager(database); - const repository = new CustomerRepository(database, customerMapper); - const customerService = new CustomerService(repository); - const presenter = getCustomerPresenter; - - const useCase = new GetCustomerUseCase(customerService, transactionManager, presenter); - - return new GetCustomerController(useCase); -}; diff --git a/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts index 7815d615..1bf2c364 100644 --- a/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts @@ -1,10 +1,4 @@ -import { - ExpressController, - authGuard, - errorMapper, - forbidQueryFieldGuard, - tenantGuard, -} from "@erp/core/api"; +import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { ListCustomersUseCase } from "../../../application"; export class ListCustomersController extends ExpressController { @@ -20,7 +14,7 @@ export class ListCustomersController extends ExpressController { return result.match( (data) => this.ok(data), - (err) => this.handleApiError(errorMapper.toApiError(err)) + (err) => this.handleError(err) ); } } diff --git a/modules/customers/src/api/infrastructure/express/controllers/list-customers/index.ts b/modules/customers/src/api/infrastructure/express/controllers/list-customers/index.ts deleted file mode 100644 index 0c0a261d..00000000 --- a/modules/customers/src/api/infrastructure/express/controllers/list-customers/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { SequelizeTransactionManager } from "@erp/core/api"; -import { Sequelize } from "sequelize"; -import { CustomerRepository, customerMapper } from "../../.."; -import { ListCustomersUseCase } from "../../../../application"; -import { listCustomersPresenter } from "../../../../application/list-customers/assembler"; -import { CustomerService } from "../../../../domain"; -import { ListCustomersController } from "./list-customers.controller"; - -export const buildListCustomersController = (database: Sequelize) => { - const transactionManager = new SequelizeTransactionManager(database); - const repository = new CustomerRepository(database, customerMapper); - const customerService = new CustomerService(repository); - const presenter = listCustomersPresenter; - - const useCase = new ListCustomersUseCase(customerService, transactionManager, presenter); - - return new ListCustomersController(useCase); -}; diff --git a/modules/customers/src/api/infrastructure/express/customers.routes.ts b/modules/customers/src/api/infrastructure/express/customers.routes.ts index edef179d..604f0b76 100644 --- a/modules/customers/src/api/infrastructure/express/customers.routes.ts +++ b/modules/customers/src/api/infrastructure/express/customers.routes.ts @@ -1,3 +1,4 @@ +import { enforceTenant, enforceUser, mockUser } from "@erp/auth/api"; import { ILogger, ModuleParams, validateRequest } from "@erp/core/api"; import { Application, NextFunction, Request, Response, Router } from "express"; import { Sequelize } from "sequelize"; @@ -23,11 +24,17 @@ export const customersRouter = (params: ModuleParams) => { logger: ILogger; }; - const router: Router = Router({ mergeParams: true }); const deps = getCustomerDependencies(params); + const router: Router = Router({ mergeParams: true }); + // 🔐 Autenticación + Tenancy para TODO el router - //router.use(/* authenticateJWT(), */ enforceTenant() /*checkTabContext*/); + if (process.env.NODE_ENV === "development") { + router.use(mockUser); // Debe ir antes de las rutas protegidas + } + + //router.use(/*authenticateJWT(),*/ enforceTenant() /*checkTabContext*/); + router.use([enforceUser(), enforceTenant()]); router.get( "/", diff --git a/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts b/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts index 06b5fc09..27df22fe 100644 --- a/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts +++ b/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts @@ -1,9 +1,15 @@ -import { ISequelizeMapper, MapperParamsType, SequelizeMapper } from "@erp/core/api"; -import { UniqueID, UtcDate } from "@repo/rdx-ddd"; +import { + ISequelizeMapper, + MapperParamsType, + SequelizeMapper, + ValidationErrorCollection, + ValidationErrorDetail, + extractOrPushError, +} from "@erp/core/api"; +import { EmailAddress, PhoneNumber, PostalAddress, TINNumber, UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { Customer, CustomerNumber, CustomerSerie, CustomerStatus } from "../../domain"; +import { Customer, CustomerProps, CustomerStatus } from "../../domain"; import { CustomerCreationAttributes, CustomerModel } from "../sequelize"; -import { CustomerItemMapper } from "./customer-item.mapper"; export interface ICustomerMapper extends ISequelizeMapper {} @@ -12,83 +18,108 @@ export class CustomerMapper extends SequelizeMapper implements ICustomerMapper { - private customerItemMapper: CustomerItemMapper; - - constructor() { - super(); - this.customerItemMapper = new CustomerItemMapper(); // Instanciar el mapper de items - } - public mapToDomain(source: CustomerModel, params?: MapperParamsType): Result { - const idOrError = UniqueID.create(source.id); - const statusOrError = CustomerStatus.create(source.invoice_status); - const customerSeriesOrError = CustomerSerie.create(source.invoice_series); - const customerNumberOrError = CustomerNumber.create(source.invoice_number); - const issueDateOrError = UtcDate.createFromISO(source.issue_date); - const operationDateOrError = UtcDate.createFromISO(source.operation_date); + try { + const errors: ValidationErrorDetail[] = []; - const result = Result.combine([ - idOrError, - statusOrError, - customerSeriesOrError, - customerNumberOrError, - issueDateOrError, - operationDateOrError, - ]); + const customerId = extractOrPushError(UniqueID.create(source.id), "id", errors); + const companyId = extractOrPushError( + UniqueID.create(source.company_id), + "company_id", + errors + ); + const status = extractOrPushError(CustomerStatus.create(source.status), "status", errors); + const reference = source.reference?.trim() === "" ? undefined : source.reference; - if (result.isFailure) { - return Result.fail(result.error); + 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", + 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 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; + + if (errors.length > 0) { + console.error(errors); + return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors)); + } + + 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 Customer.create(customerProps, customerId); + } catch (err: unknown) { + return Result.fail(err as Error); } - - // Mapear los items de la factura - const itemsOrErrors = this.customerItemMapper.mapArrayToDomain(source.items, { - sourceParent: source, - ...params, - }); - - if (itemsOrErrors.isFailure) { - return Result.fail(itemsOrErrors.error); - } - - const customerCurrency = source.invoice_currency || "EUR"; - - return Customer.create( - { - status: statusOrError.data, - invoiceSeries: customerSeriesOrError.data, - invoiceNumber: customerNumberOrError.data, - issueDate: issueDateOrError.data, - operationDate: operationDateOrError.data, - currency: customerCurrency, - items: itemsOrErrors.data, - }, - idOrError.data - ); } public mapToPersistence(source: Customer, params?: MapperParamsType): CustomerCreationAttributes { - const subtotal = source.calculateSubtotal(); - const total = source.calculateTotal(); - - const items = this.customerItemMapper.mapCollectionToPersistence(source.items, params); - return { - id: source.id.toString(), - invoice_status: source.status.toPrimitive(), - invoice_series: source.invoiceSeries.toPrimitive(), - invoice_number: source.invoiceNumber.toPrimitive(), - issue_date: source.issueDate.toPrimitive(), - operation_date: source.operationDate.toPrimitive(), - invoice_language: "es", - invoice_currency: source.currency || "EUR", + id: source.id.toPrimitive(), + company_id: source.companyId.toPrimitive(), + reference: source.reference, + is_company: source.isCompany, + name: source.name, + trade_name: source.tradeName, + tin: source.tin.toPrimitive(), - subtotal_amount: subtotal.amount, - subtotal_scale: subtotal.scale, + email: source.email.toPrimitive(), + phone: source.phone.toPrimitive(), + fax: source.fax.toPrimitive(), + website: source.website, - total_amount: total.amount, - total_scale: total.scale, + default_tax: source.defaultTax.toString(), + legal_record: source.legalRecord, + lang_code: source.langCode, + currency_code: source.currencyCode, - items, + 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, }; } } diff --git a/modules/customers/src/api/infrastructure/sequelize/customer.model.ts b/modules/customers/src/api/infrastructure/sequelize/customer.model.ts index ed0792c7..12d0cc68 100644 --- a/modules/customers/src/api/infrastructure/sequelize/customer.model.ts +++ b/modules/customers/src/api/infrastructure/sequelize/customer.model.ts @@ -1,11 +1,4 @@ -import { - CreationOptional, - DataTypes, - InferAttributes, - InferCreationAttributes, - Model, - Sequelize, -} from "sequelize"; +import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize"; export type CustomerCreationAttributes = InferCreationAttributes & {}; @@ -20,14 +13,15 @@ export class CustomerModel extends Model< declare id: string; declare company_id: string; - declare reference: CreationOptional; + declare reference: string; declare is_company: boolean; declare name: string; - declare trade_name: CreationOptional; + declare trade_name: string; declare tin: string; declare street: string; + declare street2: string; declare city: string; declare state: string; declare postal_code: string; @@ -35,12 +29,12 @@ export class CustomerModel extends Model< declare email: string; declare phone: string; - declare fax: CreationOptional; - declare website: CreationOptional; + declare fax: string; + declare website: string; declare legal_record: string; - declare default_tax: number; + declare default_tax: string; declare status: string; declare lang_code: string; declare currency_code: string; @@ -64,10 +58,12 @@ export default (database: Sequelize) => { reference: { type: DataTypes.STRING, allowNull: false, + defaultValue: "", }, is_company: { type: DataTypes.BOOLEAN, allowNull: false, + defaultValue: false, }, name: { type: DataTypes.STRING, @@ -75,38 +71,50 @@ export default (database: Sequelize) => { }, trade_name: { type: DataTypes.STRING, - allowNull: true, - defaultValue: null, + allowNull: false, + defaultValue: "", }, tin: { type: DataTypes.STRING, allowNull: false, + defaultValue: "", }, street: { type: DataTypes.STRING, allowNull: false, + defaultValue: "", + }, + street2: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: "", }, city: { type: DataTypes.STRING, allowNull: false, + defaultValue: "", }, state: { type: DataTypes.STRING, allowNull: false, + defaultValue: "", }, postal_code: { type: DataTypes.STRING, allowNull: false, + defaultValue: "", }, country: { type: DataTypes.STRING, allowNull: false, + defaultValue: "", }, email: { type: DataTypes.STRING, allowNull: false, + defaultValue: "", validate: { isEmail: true, }, @@ -114,16 +122,17 @@ export default (database: Sequelize) => { phone: { type: DataTypes.STRING, allowNull: false, + defaultValue: "", }, fax: { type: DataTypes.STRING, - allowNull: true, - defaultValue: null, + allowNull: false, + defaultValue: "", }, website: { type: DataTypes.STRING, - allowNull: true, - defaultValue: null, + allowNull: false, + defaultValue: "", validate: { isUrl: true, }, @@ -131,12 +140,13 @@ export default (database: Sequelize) => { legal_record: { type: DataTypes.TEXT, allowNull: false, + defaultValue: "", }, default_tax: { - type: new DataTypes.SMALLINT(), + type: DataTypes.STRING, allowNull: false, - defaultValue: 2100, + defaultValue: "", }, lang_code: { diff --git a/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts b/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts index 00e30820..70dcd58b 100644 --- a/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts +++ b/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts @@ -1,4 +1,4 @@ -import { SequelizeRepository, errorMapper } from "@erp/core/api"; +import { 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"; @@ -30,10 +30,18 @@ export class CustomerRepository async save(customer: Customer, transaction: Transaction): Promise> { try { const data = this.mapper.mapToPersistence(customer); + console.debug("Saving customer to database:", data); + const [instance] = await CustomerModel.upsert(data, { transaction, returning: true }); - return this.mapper.mapToDomain(instance); + + 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) { - return Result.fail(errorMapper.toDomainError(err)); + console.error("Error saving customer:", err); + return Result.fail(translateSequelizeError(err)); } } @@ -57,7 +65,7 @@ export class CustomerRepository }); return Result.ok(Boolean(count > 0)); } catch (error: any) { - return Result.fail(errorMapper.toDomainError(error)); + return Result.fail(translateSequelizeError(error)); } } @@ -86,7 +94,7 @@ export class CustomerRepository return this.mapper.mapToDomain(row); } catch (error: any) { - return Result.fail(errorMapper.toDomainError(error)); + return Result.fail(translateSequelizeError(error)); } } @@ -124,7 +132,7 @@ export class CustomerRepository return this.mapper.mapArrayToDomain(instances); } catch (err: unknown) { console.error(err); - return Result.fail(errorMapper.toDomainError(err)); + return Result.fail(translateSequelizeError(err)); } } @@ -154,7 +162,7 @@ export class CustomerRepository return Result.ok(); } catch (err: unknown) { // , `Error deleting customer ${id} in company ${companyId}` - return Result.fail(errorMapper.toDomainError(err)); + return Result.fail(translateSequelizeError(err)); } } } diff --git a/modules/customers/src/common/dto/request/create-customer.request.dto.ts b/modules/customers/src/common/dto/request/create-customer.request.dto.ts index a6e4679e..0cfc7303 100644 --- a/modules/customers/src/common/dto/request/create-customer.request.dto.ts +++ b/modules/customers/src/common/dto/request/create-customer.request.dto.ts @@ -2,30 +2,31 @@ import * as z from "zod/v4"; export const CreateCustomerRequestSchema = z.object({ id: z.uuid(), - reference: z.string().optional(), + company_id: z.uuid(), + reference: z.string().default(""), - is_company: z.boolean(), - name: z.string(), - trade_name: z.string(), - tin: z.string(), + is_company: z.boolean().default(true), + name: z.string().default(""), + trade_name: z.string().default(""), + tin: z.string().default(""), - street: z.string(), - city: z.string(), - state: z.string(), - postal_code: z.string(), - country: z.string(), + street: z.string().default(""), + city: z.string().default(""), + state: z.string().default(""), + postal_code: z.string().default(""), + country: z.string().default(""), - email: z.string(), - phone: z.string(), - fax: z.string(), - website: z.string(), + email: z.string().default(""), + phone: z.string().default(""), + fax: z.string().default(""), + website: z.string().default(""), - legal_record: z.string(), + legal_record: z.string().default(""), - default_tax: z.array(z.string()), - status: z.string(), - lang_code: z.string(), - currency_code: z.string(), + default_tax: z.array(z.string()).default([]), + status: z.string().default("active"), + lang_code: z.string().default("es"), + currency_code: z.string().default("EUR"), }); export type CreateCustomerRequestDTO = z.infer; diff --git a/modules/customers/src/common/dto/response/customer-creation.result.dto.ts b/modules/customers/src/common/dto/response/customer-creation.result.dto.ts index 104588f3..73e36595 100644 --- a/modules/customers/src/common/dto/response/customer-creation.result.dto.ts +++ b/modules/customers/src/common/dto/response/customer-creation.result.dto.ts @@ -3,14 +3,16 @@ import * as z from "zod/v4"; export const CustomerCreationResponseSchema = z.object({ id: z.uuid(), + company_id: z.uuid(), reference: z.string(), - is_companyr: z.boolean(), + is_company: z.boolean(), 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,7 +25,7 @@ export const CustomerCreationResponseSchema = z.object({ legal_record: z.string(), - default_tax: z.number(), + default_tax: z.array(z.string()), status: z.string(), lang_code: z.string(), currency_code: z.string(), diff --git a/modules/customers/tsconfig.json b/modules/customers/tsconfig.json index b4a95fde..5f79ccf1 100644 --- a/modules/customers/tsconfig.json +++ b/modules/customers/tsconfig.json @@ -28,6 +28,6 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src"], + "include": ["src", "../core/src/api/helpers/extract-or-push-error.ts"], "exclude": ["node_modules"] } diff --git a/packages.bak/rdx-ddd-domain/src/value-objects/percentage.ts b/packages.bak/rdx-ddd-domain/src/value-objects/percentage.ts index b95a45cb..4aac4435 100644 --- a/packages.bak/rdx-ddd-domain/src/value-objects/percentage.ts +++ b/packages.bak/rdx-ddd-domain/src/value-objects/percentage.ts @@ -47,7 +47,7 @@ export class Percentage extends ValueObject implements IPercen const validationResult = Percentage.validate({ amount, scale }); if (!validationResult.success) { - return Result.fail(new Error(validationResult.error.errors.map((e) => e.message).join(", "))); + return Result.fail(new Error(validationResult.error.issues.map((e) => e.message).join(", "))); } // Cálculo del valor real del porcentaje diff --git a/packages.bak/rdx-ddd-domain/src/value-objects/slug.ts b/packages.bak/rdx-ddd-domain/src/value-objects/slug.ts index 997beb69..daab1772 100644 --- a/packages.bak/rdx-ddd-domain/src/value-objects/slug.ts +++ b/packages.bak/rdx-ddd-domain/src/value-objects/slug.ts @@ -26,7 +26,7 @@ export class Slug extends ValueObject { const valueIsValid = Slug.validate(value); if (!valueIsValid.success) { - return Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.issues[0].message)); } return Result.ok(new Slug({ value: valueIsValid.data! })); } diff --git a/packages/rdx-ddd/src/value-objects/email-address.ts b/packages/rdx-ddd/src/value-objects/email-address.ts index aa8495d0..6b6ccc4b 100644 --- a/packages/rdx-ddd/src/value-objects/email-address.ts +++ b/packages/rdx-ddd/src/value-objects/email-address.ts @@ -11,7 +11,7 @@ export class EmailAddress extends ValueObject { const valueIsValid = EmailAddress.validate(value); if (!valueIsValid.success) { - return Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.issues[0].message)); } return Result.ok(new EmailAddress({ value: valueIsValid.data })); diff --git a/packages/rdx-ddd/src/value-objects/name.ts b/packages/rdx-ddd/src/value-objects/name.ts index b80971b3..3a81cde3 100644 --- a/packages/rdx-ddd/src/value-objects/name.ts +++ b/packages/rdx-ddd/src/value-objects/name.ts @@ -21,7 +21,7 @@ export class Name extends ValueObject { const valueIsValid = Name.validate(value); if (!valueIsValid.success) { - return Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.issues[0].message)); } return Result.ok(new Name({ value })); } diff --git a/packages/rdx-ddd/src/value-objects/percentage.ts b/packages/rdx-ddd/src/value-objects/percentage.ts index 8e03b150..1d525abb 100644 --- a/packages/rdx-ddd/src/value-objects/percentage.ts +++ b/packages/rdx-ddd/src/value-objects/percentage.ts @@ -53,7 +53,7 @@ export class Percentage extends ValueObject implements IPercen const validationResult = Percentage.validate({ amount, scale }); if (!validationResult.success) { - return Result.fail(new Error(validationResult.error.errors.map((e) => e.message).join(", "))); + return Result.fail(new Error(validationResult.error.issues.map((e) => e.message).join(", "))); } // Cálculo del valor real del porcentaje diff --git a/packages/rdx-ddd/src/value-objects/phone-number.ts b/packages/rdx-ddd/src/value-objects/phone-number.ts index 3f60acf0..78b35c6c 100644 --- a/packages/rdx-ddd/src/value-objects/phone-number.ts +++ b/packages/rdx-ddd/src/value-objects/phone-number.ts @@ -1,6 +1,5 @@ import { Result } from "@repo/rdx-utils"; -import { Maybe } from "@repo/rdx-utils"; -import { isValidPhoneNumber, parsePhoneNumberWithError } from "libphonenumber-js"; +import { isPossiblePhoneNumber, parsePhoneNumberWithError } from "libphonenumber-js"; import * as z from "zod/v4"; import { ValueObject } from "./value-object"; @@ -9,34 +8,49 @@ interface PhoneNumberProps { } export class PhoneNumber extends ValueObject { + static validate(value: string) { + const schema = z + .string() + .optional() + .default("") + .refine( + (value: string) => (value === "" ? true : isPossiblePhoneNumber(value, "ES")), + "Please specify a valid phone number (include the international prefix)." + ); + /*.transform((value: string) => { + console.log("transforming value", value); + + if (value === "") return ""; + + try { + const phoneNumber = parsePhoneNumberWithError(value, "ES"); + return phoneNumber.formatInternational(); + } catch (error) { + console.log(error); + if (error instanceof ParseError) { + // Not a phone number, non-existent country, etc. + console.log(error.message); + } else { + return error; + } + + return phoneNumber; + } + })*/ + + return schema.safeParse(value); + } + static create(value: string): Result { const valueIsValid = PhoneNumber.validate(value); if (!valueIsValid.success) { - return Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.issues[0].message)); } + return Result.ok(new PhoneNumber({ value: valueIsValid.data })); } - static createNullable(value?: string): Result, Error> { - if (!value || value.trim() === "") { - return Result.ok(Maybe.none()); - } - - return PhoneNumber.create(value).map((value) => Maybe.some(value)); - } - - static validate(value: string) { - const schema = z - .string() - .refine( - isValidPhoneNumber, - "Please specify a valid phone number (include the international prefix)." - ) - .transform((value: string) => parsePhoneNumberWithError(value).number.toString()); - return schema.safeParse(value); - } - getValue(): string { return this.props.value; } diff --git a/packages/rdx-ddd/src/value-objects/postal-address.ts b/packages/rdx-ddd/src/value-objects/postal-address.ts index 083b1d61..2fbf053f 100644 --- a/packages/rdx-ddd/src/value-objects/postal-address.ts +++ b/packages/rdx-ddd/src/value-objects/postal-address.ts @@ -11,11 +11,11 @@ const postalCodeSchema = z message: "Invalid postal code format", }); -const streetSchema = z.string().min(2).max(255); -const street2Schema = z.string().optional(); -const citySchema = z.string().min(2).max(50); -const stateSchema = z.string().min(2).max(50); -const countrySchema = z.string().min(2).max(56); +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; @@ -44,7 +44,7 @@ export class PostalAddress extends ValueObject { const valueIsValid = PostalAddress.validate(values); if (!valueIsValid.success) { - return Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.issues[0].message)); } return Result.ok(new PostalAddress(values)); } @@ -63,7 +63,7 @@ export class PostalAddress extends ValueObject { ): Result { return PostalAddress.create({ street: data.street ?? oldAddress.street, - street2: data.street2?.getOrUndefined() ?? oldAddress.street2.getOrUndefined(), + street2: data.street2 ?? oldAddress.street2, city: data.city ?? oldAddress.city, postalCode: data.postalCode ?? oldAddress.postalCode, state: data.state ?? oldAddress.state, @@ -76,8 +76,8 @@ export class PostalAddress extends ValueObject { return this.props.street; } - get street2(): Maybe { - return Maybe.fromNullable(this.props.street2); + get street2(): string { + return this.props.street2 ?? ""; } get city(): string { diff --git a/packages/rdx-ddd/src/value-objects/slug.ts b/packages/rdx-ddd/src/value-objects/slug.ts index b94a390f..85a2dfcc 100644 --- a/packages/rdx-ddd/src/value-objects/slug.ts +++ b/packages/rdx-ddd/src/value-objects/slug.ts @@ -26,7 +26,7 @@ export class Slug extends ValueObject { const valueIsValid = Slug.validate(value); if (!valueIsValid.success) { - return Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.issues[0].message)); } // biome-ignore lint/style/noNonNullAssertion: return Result.ok(new Slug({ value: valueIsValid.data! })); diff --git a/packages/rdx-ddd/src/value-objects/tin-number.ts b/packages/rdx-ddd/src/value-objects/tin-number.ts index d815b7c9..5a1e6b6c 100644 --- a/packages/rdx-ddd/src/value-objects/tin-number.ts +++ b/packages/rdx-ddd/src/value-objects/tin-number.ts @@ -28,7 +28,7 @@ export class TINNumber extends ValueObject { const valueIsValid = TINNumber.validate(value); if (!valueIsValid.success) { - return Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.issues[0].message)); } return Result.ok(new TINNumber({ value: valueIsValid.data })); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 880806f6..ced62503 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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))(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))(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) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -289,6 +289,9 @@ importers: '@erp/core': specifier: workspace:* version: link:../core + '@repo/rdx-ddd': + specifier: workspace:* + version: link:../../packages/rdx-ddd '@repo/rdx-ui': specifier: workspace:* version: link:../../packages/rdx-ui @@ -332,6 +335,9 @@ importers: '@biomejs/biome': specifier: 1.9.4 version: 1.9.4 + '@types/express': + specifier: ^4.17.21 + version: 4.17.23 '@types/react': specifier: ^19.1.2 version: 19.1.8 @@ -11668,7 +11674,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))(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))(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): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -11686,6 +11692,7 @@ 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):