Facturas de cliente y clientes
This commit is contained in:
parent
4b93815985
commit
f53f71760b
@ -11,7 +11,7 @@ import {
|
|||||||
|
|
||||||
import { UniqueID } from "@/core/common/domain";
|
import { UniqueID } from "@/core/common/domain";
|
||||||
import { IAuthenticatedUserRepository, JWTPayload } from "..";
|
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 { ITabContextRepository } from "../repositories/tab-context-repository.interface";
|
||||||
import { IAuthService } from "./auth-service.interface";
|
import { IAuthService } from "./auth-service.interface";
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
|
export * from "../../../../../../modules/auth/src/api/lib/passport";
|
||||||
export * from "./mappers";
|
export * from "./mappers";
|
||||||
export * from "./middleware";
|
export * from "./middleware";
|
||||||
export * from "./passport";
|
|
||||||
export * from "./sequelize";
|
export * from "./sequelize";
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import { UniqueID } from "@/core/common/domain";
|
|||||||
import { ApiError, ExpressController } from "@/core/common/presentation";
|
import { ApiError, ExpressController } from "@/core/common/presentation";
|
||||||
//import { authProvider } from "@/contexts/auth/infraestructure";
|
//import { authProvider } from "@/contexts/auth/infraestructure";
|
||||||
import { NextFunction, Response } from "express";
|
import { NextFunction, Response } from "express";
|
||||||
|
import { authProvider } from "../../../../../../../modules/auth/src/api/lib/passport";
|
||||||
import { AuthenticatedUser } from "../../domain";
|
import { AuthenticatedUser } from "../../domain";
|
||||||
import { AuthenticatedRequest } from "../express/types";
|
import { AuthenticatedRequest } from "../express/types";
|
||||||
import { authProvider } from "../passport";
|
|
||||||
|
|
||||||
// Comprueba el rol del usuario
|
// Comprueba el rol del usuario
|
||||||
const _authorizeUser = (condition: (user: AuthenticatedUser) => boolean) => {
|
const _authorizeUser = (condition: (user: AuthenticatedUser) => boolean) => {
|
||||||
@ -63,6 +63,7 @@ export const checkUserIsAdminOrOwner = [
|
|||||||
title: "Unauthorized",
|
title: "Unauthorized",
|
||||||
detail: "You are not authorized to access this resource.",
|
detail: "You are not authorized to access this resource.",
|
||||||
}),
|
}),
|
||||||
|
req,
|
||||||
res
|
res
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
import { authProvider } from "../passport";
|
import { authProvider } from "../../../../../../../modules/auth/src/api/lib/passport";
|
||||||
|
|
||||||
export const checkTabContext = [authProvider.authenticateTabId()];
|
export const checkTabContext = [authProvider.authenticateTabId()];
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export class InvoiceItemDescription extends ValueObject<IInvoiceItemDescriptionP
|
|||||||
const valueIsValid = InvoiceItemDescription.validate(value);
|
const valueIsValid = InvoiceItemDescription.validate(value);
|
||||||
|
|
||||||
if (!valueIsValid.success) {
|
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 InvoiceItemDescription({ value }));
|
return Result.ok(new InvoiceItemDescription({ value }));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export class InvoiceNumber extends ValueObject<IInvoiceNumberProps> {
|
|||||||
const valueIsValid = InvoiceNumber.validate(value);
|
const valueIsValid = InvoiceNumber.validate(value);
|
||||||
|
|
||||||
if (!valueIsValid.success) {
|
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 }));
|
return Result.ok(new InvoiceNumber({ value }));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export class InvoiceSerie extends ValueObject<IInvoiceSerieProps> {
|
|||||||
const valueIsValid = InvoiceSerie.validate(value);
|
const valueIsValid = InvoiceSerie.validate(value);
|
||||||
|
|
||||||
if (!valueIsValid.success) {
|
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 }));
|
return Result.ok(new InvoiceSerie({ value }));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,12 +9,14 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@erp/core": "workspace:*",
|
"@erp/core": "workspace:*",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"i18next": "^25.1.1",
|
||||||
"sequelize": "^6.37.5",
|
"sequelize": "^6.37.5",
|
||||||
"zod": "^3.25.67"
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.3",
|
"@types/react-dom": "^19.1.3",
|
||||||
"@types/react-i18next": "^8.1.0",
|
"@types/react-i18next": "^8.1.0",
|
||||||
@ -22,11 +24,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@erp/core": "workspace:*",
|
"@erp/core": "workspace:*",
|
||||||
|
"@repo/rdx-ddd": "workspace:*",
|
||||||
"@repo/rdx-ui": "workspace:*",
|
"@repo/rdx-ui": "workspace:*",
|
||||||
"@repo/shadcn-ui": "workspace:*",
|
"@repo/shadcn-ui": "workspace:*",
|
||||||
"@tanstack/react-query": "^5.74.11",
|
"@tanstack/react-query": "^5.74.11",
|
||||||
"express": "^4.18.2",
|
|
||||||
"i18next": "^25.1.1",
|
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hook-form": "^7.56.2",
|
"react-hook-form": "^7.56.2",
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
|
import { EmailAddress, UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
|
|
||||||
export type RequestUser = {
|
export type RequestUser = {
|
||||||
userId: string;
|
userId: UniqueID;
|
||||||
companyId: string; // tenant
|
companyId: UniqueID;
|
||||||
roles?: string[];
|
roles?: string[];
|
||||||
email?: string;
|
email?: EmailAddress;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RequestWithAuth<T = any> = Request & {
|
export type RequestWithAuth<T = any> = Request & {
|
||||||
|
|||||||
@ -1,2 +1,4 @@
|
|||||||
export * from "./auth-types";
|
export * from "./auth-types";
|
||||||
|
export * from "./mock-user.middleware";
|
||||||
export * from "./tenancy.middleware";
|
export * from "./tenancy.middleware";
|
||||||
|
export * from "./user.middleware";
|
||||||
|
|||||||
15
modules/auth/src/api/lib/express/mock-user.middleware.ts
Normal file
15
modules/auth/src/api/lib/express/mock-user.middleware.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { ExpressController, UnauthorizedApiError } from "@erp/core/api";
|
||||||
import { NextFunction, Response } from "express";
|
import { NextFunction, Response } from "express";
|
||||||
import { RequestWithAuth } from "./auth-types";
|
import { RequestWithAuth } from "./auth-types";
|
||||||
|
|
||||||
@ -9,22 +10,8 @@ export function enforceTenant() {
|
|||||||
return (req: RequestWithAuth, res: Response, next: NextFunction) => {
|
return (req: RequestWithAuth, res: Response, next: NextFunction) => {
|
||||||
// Validación básica del tenant
|
// Validación básica del tenant
|
||||||
if (!req.user || !req.user.companyId) {
|
if (!req.user || !req.user.companyId) {
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
return ExpressController.errorResponse(new UnauthorizedApiError("Unauthorized"), req, res);
|
||||||
}
|
}
|
||||||
next();
|
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<T extends Record<string, any>>(
|
|
||||||
criteria: T | undefined,
|
|
||||||
companyId: string
|
|
||||||
): T & { companyId: string } {
|
|
||||||
return {
|
|
||||||
...(criteria ?? ({} as T)),
|
|
||||||
companyId, // fuerza el scope
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
16
modules/auth/src/api/lib/express/user.middleware.ts
Normal file
16
modules/auth/src/api/lib/express/user.middleware.ts
Normal file
@ -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();
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -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";
|
import { PassportAuthProvider } from "./passport-auth-provider";
|
||||||
|
|
||||||
const database = getDatabase();
|
const database = getDatabase();
|
||||||
@ -1,14 +1,13 @@
|
|||||||
import { NextFunction, Response } from "express";
|
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 passport from "passport";
|
||||||
import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt";
|
import { ExtractJwt, Strategy as JwtStrategy } from "passport-jwt";
|
||||||
import { TabContext } from "../../domain";
|
import { TabContext } from "../../../../../../apps/server/archive/contexts/auth/domain";
|
||||||
import { IAuthService, ITabContextService } from "../../domain/services";
|
import {
|
||||||
import { TabContextRequest } from "../express/types";
|
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";
|
const SECRET_KEY = process.env.JWT_SECRET || "supersecretkey";
|
||||||
|
|
||||||
1
modules/core/src/api/helpers/index.ts
Normal file
1
modules/core/src/api/helpers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./extract-or-push-error";
|
||||||
@ -1,5 +1,6 @@
|
|||||||
export * from "./application";
|
export * from "./application";
|
||||||
export * from "./domain";
|
export * from "./domain";
|
||||||
|
export * from "./helpers";
|
||||||
export * from "./infrastructure";
|
export * from "./infrastructure";
|
||||||
export * from "./logger";
|
export * from "./logger";
|
||||||
export * from "./modules";
|
export * from "./modules";
|
||||||
|
|||||||
@ -40,6 +40,8 @@ export interface ApiErrorContext {
|
|||||||
instance?: string; // p.ej. req.originalUrl
|
instance?: string; // p.ej. req.originalUrl
|
||||||
correlationId?: string; // p.ej. header 'x-correlation-id'
|
correlationId?: string; // p.ej. header 'x-correlation-id'
|
||||||
method?: string; // GET/POST/PUT/DELETE
|
method?: string; // GET/POST/PUT/DELETE
|
||||||
|
userId?: string;
|
||||||
|
tenantId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────────────
|
||||||
@ -166,12 +168,18 @@ const defaultRules: ReadonlyArray<ErrorToApiRule> = [
|
|||||||
|
|
||||||
// Fallback genérico (500)
|
// Fallback genérico (500)
|
||||||
function defaultFallback(e: unknown): ApiError {
|
function defaultFallback(e: unknown): ApiError {
|
||||||
const message = typeof (e as any)?.message === "string" ? (e as any).message : "Unexpected error";
|
if (e instanceof ApiError) {
|
||||||
return new InternalApiError(`Unexpected error: ${message}`);
|
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) {
|
export function toProblemJson(apiError: ApiError, ctx?: ApiErrorContext) {
|
||||||
const maybeErrors = (apiError as any).errors ? { errors: (apiError as any).errors } : {};
|
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,
|
title: apiError.title,
|
||||||
status: apiError.status,
|
status: apiError.status,
|
||||||
detail: apiError.detail,
|
detail: apiError.detail,
|
||||||
|
...(ctx?.userId ? { userId: ctx.userId } : {}),
|
||||||
|
...(ctx?.tenantId ? { tenantId: ctx.tenantId } : {}),
|
||||||
...(ctx?.instance ? { instance: ctx.instance } : {}),
|
...(ctx?.instance ? { instance: ctx.instance } : {}),
|
||||||
...(ctx?.correlationId ? { correlationId: ctx.correlationId } : {}),
|
...(ctx?.correlationId ? { correlationId: ctx.correlationId } : {}),
|
||||||
...(ctx?.method ? { method: ctx.method } : {}),
|
...(ctx?.method ? { method: ctx.method } : {}),
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { Criteria, CriteriaFromUrlConverter } from "@repo/rdx-criteria/server";
|
import { Criteria, CriteriaFromUrlConverter } from "@repo/rdx-criteria/server";
|
||||||
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import httpStatus from "http-status";
|
import httpStatus from "http-status";
|
||||||
|
import { ApiErrorContext, ApiErrorMapper, toProblemJson } from "./api-error-mapper";
|
||||||
import {
|
import {
|
||||||
ApiError,
|
ApiError,
|
||||||
ConflictApiError,
|
ConflictApiError,
|
||||||
@ -11,45 +13,92 @@ import {
|
|||||||
UnavailableApiError,
|
UnavailableApiError,
|
||||||
ValidationApiError,
|
ValidationApiError,
|
||||||
} from "./errors";
|
} from "./errors";
|
||||||
|
import { GuardFn } from "./express-guards";
|
||||||
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<GuardResultLike>;
|
|
||||||
|
|
||||||
export function guardOk(): GuardResultLike {
|
|
||||||
return { isFailure: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function guardFail(error: ApiError): GuardResultLike {
|
|
||||||
return { isFailure: true, error };
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class ExpressController {
|
export abstract class ExpressController {
|
||||||
protected req!: Request;
|
protected req!: Request;
|
||||||
protected res!: Response;
|
protected res!: Response;
|
||||||
protected next!: NextFunction;
|
protected next!: NextFunction;
|
||||||
protected criteria!: Criteria;
|
protected criteria!: Criteria;
|
||||||
|
protected url!: URL;
|
||||||
|
protected errorMapper!: ApiErrorMapper;
|
||||||
|
|
||||||
// 🔹 Guards configurables por controlador
|
// 🔹 Guards configurables por controlador
|
||||||
private guards: GuardFn[] = [];
|
private guards: GuardFn[] = [];
|
||||||
|
|
||||||
static errorResponse(apiError: ApiError, res: Response) {
|
static errorResponse(apiError: ApiError, req: Request, res: Response) {
|
||||||
return res.status(apiError.status).json(apiError);
|
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<void> {
|
||||||
|
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<unknown>;
|
protected abstract executeImpl(): Promise<unknown>;
|
||||||
|
|
||||||
protected ok<T>(dto?: T) {
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
|
// Helpers de auth/tenant (opcionales para usar en executeImpl)
|
||||||
|
|
||||||
|
public getUser<T extends { userId?: UniqueID; companyId?: UniqueID } = any>(): 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<T>(dto?: T, headers?: Record<string, string>) {
|
||||||
|
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);
|
return dto ? this.res.status(httpStatus.OK).json(dto) : this.res.sendStatus(httpStatus.OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected created<T>(dto?: T) {
|
protected created<T>(dto?: T, location?: string) {
|
||||||
|
if (location) this.res.setHeader("Location", location);
|
||||||
return dto
|
return dto
|
||||||
? this.res.status(httpStatus.CREATED).json(dto)
|
? this.res.status(httpStatus.CREATED).json(dto)
|
||||||
: this.res.sendStatus(httpStatus.CREATED);
|
: this.res.sendStatus(httpStatus.CREATED);
|
||||||
@ -60,46 +109,51 @@ export abstract class ExpressController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected clientError(message: string, errors?: any[] | any) {
|
protected clientError(message: string, errors?: any[] | any) {
|
||||||
return ExpressController.errorResponse(
|
return this.handleApiError(
|
||||||
new ValidationApiError(message, Array.isArray(errors) ? errors : [errors]),
|
new ValidationApiError(message, Array.isArray(errors) ? errors : [errors])
|
||||||
this.res
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected unauthorizedError(message?: string) {
|
protected unauthorizedError(message?: string) {
|
||||||
return ExpressController.errorResponse(
|
return this.handleApiError(new UnauthorizedApiError(message ?? "Unauthorized"));
|
||||||
new UnauthorizedApiError(message ?? "Unauthorized"),
|
|
||||||
this.res
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected forbiddenError(message?: string) {
|
protected forbiddenError(message?: string) {
|
||||||
return ExpressController.errorResponse(
|
return this.handleApiError(
|
||||||
new ForbiddenApiError(message ?? "You do not have permission to perform this action."),
|
new ForbiddenApiError(message ?? "You do not have permission to perform this action.")
|
||||||
this.res
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected notFoundError(message: string) {
|
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[]) {
|
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) {
|
protected unavailableError(message?: string) {
|
||||||
return ExpressController.errorResponse(
|
return this.handleApiError(
|
||||||
new UnavailableApiError(message ?? "Service temporarily unavailable."),
|
new UnavailableApiError(message ?? "Service temporarily unavailable.")
|
||||||
this.res
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected internalServerError(message?: string) {
|
protected internalServerError(message?: string) {
|
||||||
return ExpressController.errorResponse(
|
return this.handleApiError(new InternalApiError(message ?? "Internal Server Error"));
|
||||||
new InternalApiError(message ?? "Internal Server Error"),
|
|
||||||
this.res
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleApiError(apiError: ApiError) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
|
||||||
// Helpers de auth/tenant (opcionales para usar en executeImpl)
|
|
||||||
|
|
||||||
public getUser<T extends { userId?: string; companyId?: string } = any>(): 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<void> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,26 @@
|
|||||||
import { ForbiddenApiError, UnauthorizedApiError, ValidationApiError } from "./errors";
|
import { Criteria } from "@repo/rdx-criteria/server";
|
||||||
import { GuardContext, GuardFn, guardFail, guardOk } from "./express-controller";
|
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<GuardResultLike>;
|
||||||
|
|
||||||
|
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
|
// Guards reutilizables (auth/tenancy). Si prefieres, muévelos a src/lib/http/express-guards.ts
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { ApiErrorMapper, toProblemJson } from "../api-error-mapper";
|
import { ApiErrorContext, ApiErrorMapper, toProblemJson } from "../api-error-mapper";
|
||||||
import { ApiError } from "../errors/api-error";
|
|
||||||
|
// ✅ Construye tu mapper una vez (composition root del adaptador HTTP)
|
||||||
|
export const apiErrorMapper = ApiErrorMapper.default();
|
||||||
|
|
||||||
export const globalErrorHandler = async (
|
export const globalErrorHandler = async (
|
||||||
error: Error,
|
error: Error,
|
||||||
@ -8,45 +10,23 @@ export const globalErrorHandler = async (
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) => {
|
) => {
|
||||||
|
console.error(`❌ Global unhandled error: ${error.message}`);
|
||||||
|
|
||||||
// Si ya se envió una respuesta, delegamos al siguiente error handler
|
// Si ya se envió una respuesta, delegamos al siguiente error handler
|
||||||
if (res.headersSent) {
|
if (res.headersSent) {
|
||||||
return next(error);
|
return next(error);
|
||||||
}
|
}
|
||||||
const ctx = {
|
const ctx = {
|
||||||
instance: req.originalUrl,
|
instance: req.originalUrl,
|
||||||
correlationId: (req.headers["x-correlation-id"] as string) || undefined,
|
correlationId: req.get("x-correlation-id") || undefined,
|
||||||
method: req.method,
|
method: req.method,
|
||||||
};
|
} satisfies ApiErrorContext;
|
||||||
|
|
||||||
const apiError = ApiErrorMapper.map(err, ctx);
|
const apiError = apiErrorMapper.map(error, ctx);
|
||||||
const body = toProblemJson(apiError, ctx);
|
const body = toProblemJson(apiError, ctx);
|
||||||
|
|
||||||
// 👇 Log interno con cause/traza (no lo exponemos al cliente)
|
// 👇 Log interno con cause/traza (no lo exponemos al cliente)
|
||||||
// logger.error({ err, cause: (err as any)?.cause, ...ctx }, `❌ Unhandled API error: ${error.message}`);
|
// logger.error({ err, cause: (err as any)?.cause, ...ctx }, `❌ Unhandled API error: ${error.message}`);
|
||||||
|
|
||||||
res.status(apiError.status).json(body);
|
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,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export * from "./global-error-handler";
|
export * from "./global-error-handler";
|
||||||
export * from "./validate-request";
|
export * from "./validate-request.middleware";
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export const validateRequest = <T extends "body" | "query" | "params">(
|
|||||||
|
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
console.debug("ERROR: Undefined schema!!");
|
console.debug("ERROR: Undefined schema!!");
|
||||||
return ExpressController.errorResponse(new InternalApiError("Undefined schema"), res);
|
return ExpressController.errorResponse(new InternalApiError("Undefined schema"), req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -62,6 +62,7 @@ export const validateRequest = <T extends "body" | "query" | "params">(
|
|||||||
|
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(
|
||||||
new ValidationApiError("Validation failed", validationErrors),
|
new ValidationApiError("Validation failed", validationErrors),
|
||||||
|
req,
|
||||||
res
|
res
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -71,12 +72,14 @@ export const validateRequest = <T extends "body" | "query" | "params">(
|
|||||||
req[source] = result.data;
|
req[source] = result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.debug(`Request ${source} is valid.`);
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as Error;
|
const error = err as Error;
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
return ExpressController.errorResponse(new InternalApiError(error.message), res);
|
return ExpressController.errorResponse(new InternalApiError(error.message), req, res);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
ListCustomerInvoicesAssembler,
|
ListCustomerInvoicesAssembler,
|
||||||
ListCustomerInvoicesUseCase,
|
ListCustomerInvoicesUseCase,
|
||||||
} from "../application";
|
} from "../application";
|
||||||
import { CustomerInvoiceService } from "../domain";
|
import { CustomerInvoiceService, ICustomerInvoiceService } from "../domain";
|
||||||
import { CustomerInvoiceMapper } from "./mappers";
|
import { CustomerInvoiceMapper } from "./mappers";
|
||||||
import { CustomerInvoiceRepository } from "./sequelize";
|
import { CustomerInvoiceRepository } from "./sequelize";
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ type InvoiceDeps = {
|
|||||||
transactionManager: SequelizeTransactionManager;
|
transactionManager: SequelizeTransactionManager;
|
||||||
repo: CustomerInvoiceRepository;
|
repo: CustomerInvoiceRepository;
|
||||||
mapper: CustomerInvoiceMapper;
|
mapper: CustomerInvoiceMapper;
|
||||||
service: CustomerInvoiceService;
|
service: ICustomerInvoiceService;
|
||||||
assemblers: {
|
assemblers: {
|
||||||
list: ListCustomerInvoicesAssembler;
|
list: ListCustomerInvoicesAssembler;
|
||||||
get: GetCustomerInvoiceAssembler;
|
get: GetCustomerInvoiceAssembler;
|
||||||
@ -38,7 +38,7 @@ type InvoiceDeps = {
|
|||||||
|
|
||||||
let _repo: CustomerInvoiceRepository | null = null;
|
let _repo: CustomerInvoiceRepository | null = null;
|
||||||
let _mapper: CustomerInvoiceMapper | null = null;
|
let _mapper: CustomerInvoiceMapper | null = null;
|
||||||
let _service: CustomerInvoiceService | null = null;
|
let _service: ICustomerInvoiceService | null = null;
|
||||||
let _assemblers: InvoiceDeps["assemblers"] | null = null;
|
let _assemblers: InvoiceDeps["assemblers"] | null = null;
|
||||||
|
|
||||||
export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
|
export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
|
||||||
|
|||||||
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
ExpressController,
|
|
||||||
authGuard,
|
|
||||||
errorMapper,
|
|
||||||
forbidQueryFieldGuard,
|
|
||||||
tenantGuard,
|
|
||||||
} from "@erp/core/api";
|
|
||||||
|
|
||||||
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
|
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
|
||||||
import { CreateCustomerInvoiceUseCase } from "../../../application";
|
import { CreateCustomerInvoiceUseCase } from "../../../application";
|
||||||
@ -32,7 +26,7 @@ export class CreateCustomerInvoiceController extends ExpressController {
|
|||||||
|
|
||||||
return result.match(
|
return result.match(
|
||||||
(data) => this.created(data),
|
(data) => this.created(data),
|
||||||
(err) => this.handleApiError(errorMapper.toApiError(err))
|
(err) => this.handleError(err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
ExpressController,
|
|
||||||
authGuard,
|
|
||||||
errorMapper,
|
|
||||||
forbidQueryFieldGuard,
|
|
||||||
tenantGuard,
|
|
||||||
} from "@erp/core/api";
|
|
||||||
import { DeleteCustomerInvoiceUseCase } from "../../../application";
|
import { DeleteCustomerInvoiceUseCase } from "../../../application";
|
||||||
|
|
||||||
export class DeleteCustomerInvoiceController extends ExpressController {
|
export class DeleteCustomerInvoiceController extends ExpressController {
|
||||||
@ -25,7 +19,7 @@ export class DeleteCustomerInvoiceController extends ExpressController {
|
|||||||
|
|
||||||
return result.match(
|
return result.match(
|
||||||
(data) => this.ok(data),
|
(data) => this.ok(data),
|
||||||
(error) => this.handleApiError(errorMapper.toApiError(error))
|
(err) => this.handleError(err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
ExpressController,
|
|
||||||
authGuard,
|
|
||||||
errorMapper,
|
|
||||||
forbidQueryFieldGuard,
|
|
||||||
tenantGuard,
|
|
||||||
} from "@erp/core/api";
|
|
||||||
import { GetCustomerInvoiceUseCase } from "../../../application";
|
import { GetCustomerInvoiceUseCase } from "../../../application";
|
||||||
|
|
||||||
export class GetCustomerInvoiceController extends ExpressController {
|
export class GetCustomerInvoiceController extends ExpressController {
|
||||||
@ -25,7 +19,7 @@ export class GetCustomerInvoiceController extends ExpressController {
|
|||||||
|
|
||||||
return result.match(
|
return result.match(
|
||||||
(data) => this.ok(data),
|
(data) => this.ok(data),
|
||||||
(error) => this.handleApiError(errorMapper.toApiError(error))
|
(err) => this.handleError(err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
ExpressController,
|
|
||||||
authGuard,
|
|
||||||
errorMapper,
|
|
||||||
forbidQueryFieldGuard,
|
|
||||||
tenantGuard,
|
|
||||||
} from "@erp/core/api";
|
|
||||||
import { ListCustomerInvoicesUseCase } from "../../../application";
|
import { ListCustomerInvoicesUseCase } from "../../../application";
|
||||||
|
|
||||||
export class ListCustomerInvoicesController extends ExpressController {
|
export class ListCustomerInvoicesController extends ExpressController {
|
||||||
@ -23,7 +17,7 @@ export class ListCustomerInvoicesController extends ExpressController {
|
|||||||
|
|
||||||
return result.match(
|
return result.match(
|
||||||
(data) => this.ok(data),
|
(data) => this.ok(data),
|
||||||
(err) => this.handleApiError(errorMapper.toApiError(err))
|
(err) => this.handleError(err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
import { Collection, Result } from "@repo/rdx-utils";
|
||||||
@ -58,7 +58,7 @@ export class CustomerInvoiceRepository
|
|||||||
|
|
||||||
return Result.ok(Boolean(result));
|
return Result.ok(Boolean(result));
|
||||||
} catch (err: unknown) {
|
} 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 });
|
await CustomerInvoiceModel.upsert(data, { transaction });
|
||||||
return Result.ok(invoice);
|
return Result.ok(invoice);
|
||||||
} catch (err: unknown) {
|
} 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);
|
return this.mapper.mapToDomain(rawData);
|
||||||
} catch (err: unknown) {
|
} 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);
|
return this.mapper.mapArrayToDomain(instances);
|
||||||
} catch (err: unknown) {
|
} 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);
|
await this._deleteById(CustomerInvoiceModel, id, false, transaction);
|
||||||
return Result.ok<void>();
|
return Result.ok<void>();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
return Result.fail(errorMapper.toDomainError(err));
|
return Result.fail(translateSequelizeError(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,35 @@
|
|||||||
import { Customer } from "@erp/customers/api/domain";
|
import { CustomerCreationResponseDTO } from "../../../../common";
|
||||||
import { CustomersCreationResultDTO } from "@erp/customers/common/dto";
|
import { Customer } from "../../../domain";
|
||||||
|
|
||||||
export class CreateCustomersAssembler {
|
export class CreateCustomersAssembler {
|
||||||
public toDTO(customer: Customer): CustomersCreationResultDTO {
|
public toDTO(customer: Customer): CustomerCreationResponseDTO {
|
||||||
return {
|
return {
|
||||||
id: customer.id.toPrimitive(),
|
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(),
|
email: customer.email.toPrimitive(),
|
||||||
customer_number: customer.customerNumber.toString(),
|
phone: customer.phone.toPrimitive(),
|
||||||
customer_series: customer.customerSeries.toString(),
|
fax: customer.fax.toPrimitive(),
|
||||||
issue_date: customer.issueDate.toISOString(),
|
website: customer.website,
|
||||||
operation_date: customer.operationDate.toISOString(),
|
|
||||||
language_code: "ES",
|
|
||||||
currency: "EUR",
|
|
||||||
|
|
||||||
//subtotal_price: customer.calculateSubtotal().toPrimitive(),
|
default_tax: customer.defaultTax,
|
||||||
//total_price: customer.calculateTotal().toPrimitive(),
|
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: {
|
metadata: {
|
||||||
entity: "customer",
|
entity: "customer",
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { DuplicateEntityError, ITransactionManager } from "@erp/core/api";
|
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 { Result } from "@repo/rdx-utils";
|
||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
|
import { CreateCustomerRequestDTO } from "../../../common";
|
||||||
import { ICustomerService } from "../../domain";
|
import { ICustomerService } from "../../domain";
|
||||||
import { mapDTOToCustomerProps } from "../helpers";
|
import { mapDTOToCustomerProps } from "../../helpers";
|
||||||
import { CreateCustomersAssembler } from "./assembler";
|
import { CreateCustomersAssembler } from "./assembler";
|
||||||
|
|
||||||
type CreateCustomerUseCaseInput = {
|
type CreateCustomerUseCaseInput = {
|
||||||
tenantId: string;
|
dto: CreateCustomerRequestDTO;
|
||||||
dto: CreateCustomerCommandDTO;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class CreateCustomerUseCase {
|
export class CreateCustomerUseCase {
|
||||||
@ -19,46 +19,68 @@ export class CreateCustomerUseCase {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(params: CreateCustomerUseCaseInput) {
|
public execute(params: CreateCustomerUseCaseInput) {
|
||||||
const { dto, tenantId: companyId } = params;
|
const { dto } = params;
|
||||||
|
|
||||||
const customerPropsOrError = mapDTOToCustomerProps(dto);
|
// 1) Mapear DTO → props de dominio
|
||||||
|
const dtoResult = mapDTOToCustomerProps(dto);
|
||||||
if (customerPropsOrError.isFailure) {
|
if (dtoResult.isFailure) {
|
||||||
return Result.fail(customerPropsOrError.error);
|
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) {
|
// 3) Construir entidad de dominio
|
||||||
return Result.fail(customerOrError.error);
|
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) => {
|
console.debug("Built new customer entity:", newCustomer);
|
||||||
try {
|
|
||||||
const duplicateCheck = await this.service.existsById(id, transaction);
|
|
||||||
|
|
||||||
if (duplicateCheck.isFailure) {
|
// 4) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista
|
||||||
return Result.fail(duplicateCheck.error);
|
return this.transactionManager.complete(async (tx: Transaction) => {
|
||||||
}
|
const existsGuard = await this.ensureNotExists(companyId, id, tx);
|
||||||
|
if (existsGuard.isFailure) {
|
||||||
if (duplicateCheck.data) {
|
return Result.fail(existsGuard.error);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<Result<void, Error>> {
|
||||||
|
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<void>(undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<T extends Record<string, any>>(
|
|
||||||
obj: T
|
|
||||||
): obj is { [K in keyof T]-?: Exclude<T[K], undefined> } {
|
|
||||||
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<T extends Record<string, any>>(
|
|
||||||
obj: T
|
|
||||||
): obj is { [K in keyof T]-?: Exclude<T[K], undefined> } {
|
|
||||||
return !hasNoUndefinedFields(obj);
|
|
||||||
}
|
|
||||||
@ -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<CreateCustomerCommandDTO, "items">["items"]
|
|
||||||
): Result<CustomerItem[], ValidationErrorCollection> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
@ -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" },
|
|
||||||
])
|
|
||||||
);*/
|
|
||||||
}
|
|
||||||
@ -6,46 +6,55 @@ import {
|
|||||||
TINNumber,
|
TINNumber,
|
||||||
UniqueID,
|
UniqueID,
|
||||||
} from "@repo/rdx-ddd";
|
} 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 {
|
export interface CustomerProps {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
|
status: CustomerStatus;
|
||||||
reference: string;
|
reference: string;
|
||||||
|
|
||||||
isCompany: boolean;
|
isCompany: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
|
tradeName: string;
|
||||||
tin: TINNumber;
|
tin: TINNumber;
|
||||||
|
|
||||||
address: PostalAddress;
|
address: PostalAddress;
|
||||||
|
|
||||||
email: EmailAddress;
|
email: EmailAddress;
|
||||||
phone: PhoneNumber;
|
phone: PhoneNumber;
|
||||||
|
fax: PhoneNumber;
|
||||||
|
website: string;
|
||||||
|
|
||||||
legalRecord: string;
|
legalRecord: string;
|
||||||
defaultTax: number;
|
defaultTax: string[];
|
||||||
status: string;
|
|
||||||
langCode: string;
|
langCode: string;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
|
|
||||||
tradeName: Maybe<string>;
|
|
||||||
website: Maybe<string>;
|
|
||||||
fax: Maybe<PhoneNumber>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICustomer {
|
export interface ICustomer {
|
||||||
id: UniqueID;
|
id: UniqueID;
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
reference: string;
|
reference: string;
|
||||||
name: string;
|
|
||||||
tin: TINNumber;
|
tin: TINNumber;
|
||||||
|
name: string;
|
||||||
|
tradeName: string;
|
||||||
|
|
||||||
address: PostalAddress;
|
address: PostalAddress;
|
||||||
|
|
||||||
email: EmailAddress;
|
email: EmailAddress;
|
||||||
phone: PhoneNumber;
|
phone: PhoneNumber;
|
||||||
|
fax: PhoneNumber;
|
||||||
|
website: string;
|
||||||
|
|
||||||
legalRecord: string;
|
legalRecord: string;
|
||||||
defaultTax: number;
|
defaultTax: string[];
|
||||||
|
|
||||||
langCode: string;
|
langCode: string;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
|
|
||||||
tradeName: Maybe<string>;
|
|
||||||
fax: Maybe<PhoneNumber>;
|
|
||||||
website: Maybe<string>;
|
|
||||||
|
|
||||||
isIndividual: boolean;
|
isIndividual: boolean;
|
||||||
isCompany: boolean;
|
isCompany: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@ -103,11 +112,11 @@ export class Customer extends AggregateRoot<CustomerProps> implements ICustomer
|
|||||||
return this.props.phone;
|
return this.props.phone;
|
||||||
}
|
}
|
||||||
|
|
||||||
get fax(): Maybe<PhoneNumber> {
|
get fax(): PhoneNumber {
|
||||||
return this.props.fax;
|
return this.props.fax;
|
||||||
}
|
}
|
||||||
|
|
||||||
get website() {
|
get website(): string {
|
||||||
return this.props.website;
|
return this.props.website;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +145,6 @@ export class Customer extends AggregateRoot<CustomerProps> implements ICustomer
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isActive(): boolean {
|
get isActive(): boolean {
|
||||||
return this.props.status === "active";
|
return this.props.status.isActive();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export interface ICustomerService {
|
|||||||
/**
|
/**
|
||||||
* Guarda un Customer (nuevo o modificado) en base de datos.
|
* Guarda un Customer (nuevo o modificado) en base de datos.
|
||||||
*/
|
*/
|
||||||
saveCustomerInCompany(customer: Customer, transaction: any): Promise<Result<Customer, Error>>;
|
saveCustomer(customer: Customer, transaction: any): Promise<Result<Customer, Error>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comprueba si existe un Customer con ese ID en la empresa indicada.
|
* Comprueba si existe un Customer con ese ID en la empresa indicada.
|
||||||
|
|||||||
@ -7,13 +7,6 @@ import { ICustomerService } from "./customer-service.interface";
|
|||||||
|
|
||||||
export class CustomerService implements ICustomerService {
|
export class CustomerService implements ICustomerService {
|
||||||
constructor(private readonly repository: ICustomerRepository) {}
|
constructor(private readonly repository: ICustomerRepository) {}
|
||||||
findCustomerByCriteriaInCompany(
|
|
||||||
companyId: UniqueID,
|
|
||||||
criteria: Criteria,
|
|
||||||
transaction?: any
|
|
||||||
): Promise<Result<Collection<Customer>, Error>> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construye un nuevo agregado Customer a partir de props validadas.
|
* 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.
|
* 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.
|
* @param transaction - Transacción activa para la operación.
|
||||||
* @returns Result<Customer, Error> - El agregado guardado o un error si falla la operación.
|
* @returns Result<Customer, Error> - El agregado guardado o un error si falla la operación.
|
||||||
*/
|
*/
|
||||||
async saveCustomerInCompany(
|
async saveCustomer(customer: Customer, transaction: any): Promise<Result<Customer, Error>> {
|
||||||
customer: Customer,
|
|
||||||
transaction: any
|
|
||||||
): Promise<Result<Customer, Error>> {
|
|
||||||
return this.repository.save(customer, transaction);
|
return this.repository.save(customer, transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,11 +61,11 @@ export class CustomerService implements ICustomerService {
|
|||||||
* @param transaction - Transacción activa para la operación.
|
* @param transaction - Transacción activa para la operación.
|
||||||
* @returns Result<Collection<Customer>, Error> - Colección de clientes o error.
|
* @returns Result<Collection<Customer>, Error> - Colección de clientes o error.
|
||||||
*/
|
*/
|
||||||
async findCustomersByCriteriaInCompany(
|
async findCustomerByCriteriaInCompany(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction?: any
|
transaction?: any
|
||||||
): Promise<Result<Collection<Customer>>> {
|
): Promise<Result<Collection<Customer>, Error>> {
|
||||||
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
|
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +113,7 @@ export class CustomerService implements ICustomerService {
|
|||||||
return Result.fail(updatedCustomer.error);
|
return Result.fail(updatedCustomer.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.saveCustomerInCompany(updatedCustomer.data, transaction);
|
return this.saveCustomer(updatedCustomer.data, transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,65 +6,43 @@ interface ICustomerStatusProps {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum INVOICE_STATUS {
|
export enum CUSTOMER_STATUS {
|
||||||
DRAFT = "draft",
|
ACTIVE = "active",
|
||||||
EMITTED = "emitted",
|
INACTIVE = "inactive",
|
||||||
SENT = "sent",
|
|
||||||
RECEIVED = "received",
|
|
||||||
REJECTED = "rejected",
|
|
||||||
}
|
}
|
||||||
export class CustomerStatus extends ValueObject<ICustomerStatusProps> {
|
export class CustomerStatus extends ValueObject<ICustomerStatusProps> {
|
||||||
private static readonly ALLOWED_STATUSES = ["draft", "emitted", "sent", "received", "rejected"];
|
private static readonly ALLOWED_STATUSES = ["active", "inactive"];
|
||||||
private static readonly FIELD = "invoiceStatus";
|
private static readonly FIELD = "status";
|
||||||
private static readonly ERROR_CODE = "INVALID_INVOICE_STATUS";
|
private static readonly ERROR_CODE = "INVALID_STATUS";
|
||||||
|
|
||||||
private static readonly TRANSITIONS: Record<string, string[]> = {
|
private static readonly TRANSITIONS: Record<string, string[]> = {
|
||||||
draft: [INVOICE_STATUS.EMITTED],
|
active: [CUSTOMER_STATUS.INACTIVE],
|
||||||
emitted: [INVOICE_STATUS.SENT, INVOICE_STATUS.REJECTED, INVOICE_STATUS.DRAFT],
|
inactive: [CUSTOMER_STATUS.ACTIVE],
|
||||||
sent: [INVOICE_STATUS.RECEIVED, INVOICE_STATUS.REJECTED],
|
|
||||||
received: [],
|
|
||||||
rejected: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static create(value: string): Result<CustomerStatus, Error> {
|
static create(value: string): Result<CustomerStatus, Error> {
|
||||||
if (!CustomerStatus.ALLOWED_STATUSES.includes(value)) {
|
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(
|
return Result.fail(
|
||||||
new DomainValidationError(CustomerStatus.ERROR_CODE, CustomerStatus.FIELD, detail)
|
new DomainValidationError(CustomerStatus.ERROR_CODE, CustomerStatus.FIELD, detail)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.ok(
|
return Result.ok(
|
||||||
value === "rejected"
|
value === "active" ? CustomerStatus.createActive() : CustomerStatus.createInactive()
|
||||||
? CustomerStatus.createRejected()
|
|
||||||
: value === "sent"
|
|
||||||
? CustomerStatus.createSent()
|
|
||||||
: value === "emitted"
|
|
||||||
? CustomerStatus.createSent()
|
|
||||||
: value === ""
|
|
||||||
? CustomerStatus.createReceived()
|
|
||||||
: CustomerStatus.createDraft()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createDraft(): CustomerStatus {
|
public static createActive(): CustomerStatus {
|
||||||
return new CustomerStatus({ value: INVOICE_STATUS.DRAFT });
|
return new CustomerStatus({ value: CUSTOMER_STATUS.ACTIVE });
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createEmitted(): CustomerStatus {
|
public static createInactive(): CustomerStatus {
|
||||||
return new CustomerStatus({ value: INVOICE_STATUS.EMITTED });
|
return new CustomerStatus({ value: CUSTOMER_STATUS.INACTIVE });
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createSent(): CustomerStatus {
|
isActive(): boolean {
|
||||||
return new CustomerStatus({ value: INVOICE_STATUS.SENT });
|
return this.props.value === CUSTOMER_STATUS.ACTIVE;
|
||||||
}
|
|
||||||
|
|
||||||
public static createReceived(): CustomerStatus {
|
|
||||||
return new CustomerStatus({ value: INVOICE_STATUS.RECEIVED });
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createRejected(): CustomerStatus {
|
|
||||||
return new CustomerStatus({ value: INVOICE_STATUS.REJECTED });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getValue(): string {
|
getValue(): string {
|
||||||
@ -82,7 +60,7 @@ export class CustomerStatus extends ValueObject<ICustomerStatusProps> {
|
|||||||
transitionTo(nextStatus: string): Result<CustomerStatus, Error> {
|
transitionTo(nextStatus: string): Result<CustomerStatus, Error> {
|
||||||
if (!this.canTransitionTo(nextStatus)) {
|
if (!this.canTransitionTo(nextStatus)) {
|
||||||
return Result.fail(
|
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);
|
return CustomerStatus.create(nextStatus);
|
||||||
|
|||||||
109
modules/customers/src/api/helpers/map-dto-to-customer-props.ts
Normal file
109
modules/customers/src/api/helpers/map-dto-to-customer-props.ts
Normal file
@ -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 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,7 +18,7 @@ type CustomerDeps = {
|
|||||||
transactionManager: SequelizeTransactionManager;
|
transactionManager: SequelizeTransactionManager;
|
||||||
repo: CustomerRepository;
|
repo: CustomerRepository;
|
||||||
mapper: CustomerMapper;
|
mapper: CustomerMapper;
|
||||||
service: CustomerService;
|
service: ICustomerService;
|
||||||
assemblers: {
|
assemblers: {
|
||||||
list: ListCustomersAssembler;
|
list: ListCustomersAssembler;
|
||||||
get: GetCustomerAssembler;
|
get: GetCustomerAssembler;
|
||||||
|
|||||||
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
ExpressController,
|
|
||||||
authGuard,
|
|
||||||
errorMapper,
|
|
||||||
forbidQueryFieldGuard,
|
|
||||||
tenantGuard,
|
|
||||||
} from "@erp/core/api";
|
|
||||||
import { CreateCustomerRequestDTO } from "../../../../common/dto";
|
import { CreateCustomerRequestDTO } from "../../../../common/dto";
|
||||||
import { CreateCustomerUseCase } from "../../../application";
|
import { CreateCustomerUseCase } from "../../../application";
|
||||||
|
|
||||||
@ -16,18 +10,17 @@ export class CreateCustomerController extends ExpressController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async executeImpl() {
|
protected async executeImpl() {
|
||||||
const tenantId = this.getTenantId()!; // garantizado por tenantGuard
|
const companyId = this.getTenantId()!; // garantizado por tenantGuard
|
||||||
const dto = this.req.body as CreateCustomerRequestDTO;
|
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(
|
return result.match(
|
||||||
(data) => this.created(data),
|
(data) => this.created(data),
|
||||||
(err) => this.handleApiError(errorMapper.toApiError(err))
|
(err) => this.handleError(err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
};
|
|
||||||
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
ExpressController,
|
|
||||||
authGuard,
|
|
||||||
errorMapper,
|
|
||||||
forbidQueryFieldGuard,
|
|
||||||
tenantGuard,
|
|
||||||
} from "@erp/core/api";
|
|
||||||
import { DeleteCustomerUseCase } from "../../../application";
|
import { DeleteCustomerUseCase } from "../../../application";
|
||||||
|
|
||||||
export class DeleteCustomerController extends ExpressController {
|
export class DeleteCustomerController extends ExpressController {
|
||||||
@ -22,7 +16,7 @@ export class DeleteCustomerController extends ExpressController {
|
|||||||
|
|
||||||
return result.match(
|
return result.match(
|
||||||
(data) => this.ok(data),
|
(data) => this.ok(data),
|
||||||
(error) => this.handleApiError(errorMapper.toApiError(error))
|
(err) => this.handleError(err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 */);
|
|
||||||
};
|
|
||||||
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
ExpressController,
|
|
||||||
authGuard,
|
|
||||||
errorMapper,
|
|
||||||
forbidQueryFieldGuard,
|
|
||||||
tenantGuard,
|
|
||||||
} from "@erp/core/api";
|
|
||||||
import { GetCustomerUseCase } from "../../../application";
|
import { GetCustomerUseCase } from "../../../application";
|
||||||
|
|
||||||
export class GetCustomerController extends ExpressController {
|
export class GetCustomerController extends ExpressController {
|
||||||
@ -22,7 +16,7 @@ export class GetCustomerController extends ExpressController {
|
|||||||
|
|
||||||
return result.match(
|
return result.match(
|
||||||
(data) => this.ok(data),
|
(data) => this.ok(data),
|
||||||
(error) => this.handleApiError(errorMapper.toApiError(error))
|
(err) => this.handleError(err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
};
|
|
||||||
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||||
ExpressController,
|
|
||||||
authGuard,
|
|
||||||
errorMapper,
|
|
||||||
forbidQueryFieldGuard,
|
|
||||||
tenantGuard,
|
|
||||||
} from "@erp/core/api";
|
|
||||||
import { ListCustomersUseCase } from "../../../application";
|
import { ListCustomersUseCase } from "../../../application";
|
||||||
|
|
||||||
export class ListCustomersController extends ExpressController {
|
export class ListCustomersController extends ExpressController {
|
||||||
@ -20,7 +14,7 @@ export class ListCustomersController extends ExpressController {
|
|||||||
|
|
||||||
return result.match(
|
return result.match(
|
||||||
(data) => this.ok(data),
|
(data) => this.ok(data),
|
||||||
(err) => this.handleApiError(errorMapper.toApiError(err))
|
(err) => this.handleError(err)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
};
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
|
||||||
import { ILogger, ModuleParams, validateRequest } from "@erp/core/api";
|
import { ILogger, ModuleParams, validateRequest } from "@erp/core/api";
|
||||||
import { Application, NextFunction, Request, Response, Router } from "express";
|
import { Application, NextFunction, Request, Response, Router } from "express";
|
||||||
import { Sequelize } from "sequelize";
|
import { Sequelize } from "sequelize";
|
||||||
@ -23,11 +24,17 @@ export const customersRouter = (params: ModuleParams) => {
|
|||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
};
|
};
|
||||||
|
|
||||||
const router: Router = Router({ mergeParams: true });
|
|
||||||
const deps = getCustomerDependencies(params);
|
const deps = getCustomerDependencies(params);
|
||||||
|
|
||||||
|
const router: Router = Router({ mergeParams: true });
|
||||||
|
|
||||||
// 🔐 Autenticación + Tenancy para TODO el router
|
// 🔐 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(
|
router.get(
|
||||||
"/",
|
"/",
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
import { ISequelizeMapper, MapperParamsType, SequelizeMapper } from "@erp/core/api";
|
import {
|
||||||
import { UniqueID, UtcDate } from "@repo/rdx-ddd";
|
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 { Result } from "@repo/rdx-utils";
|
||||||
import { Customer, CustomerNumber, CustomerSerie, CustomerStatus } from "../../domain";
|
import { Customer, CustomerProps, CustomerStatus } from "../../domain";
|
||||||
import { CustomerCreationAttributes, CustomerModel } from "../sequelize";
|
import { CustomerCreationAttributes, CustomerModel } from "../sequelize";
|
||||||
import { CustomerItemMapper } from "./customer-item.mapper";
|
|
||||||
|
|
||||||
export interface ICustomerMapper
|
export interface ICustomerMapper
|
||||||
extends ISequelizeMapper<CustomerModel, CustomerCreationAttributes, Customer> {}
|
extends ISequelizeMapper<CustomerModel, CustomerCreationAttributes, Customer> {}
|
||||||
@ -12,83 +18,108 @@ export class CustomerMapper
|
|||||||
extends SequelizeMapper<CustomerModel, CustomerCreationAttributes, Customer>
|
extends SequelizeMapper<CustomerModel, CustomerCreationAttributes, Customer>
|
||||||
implements ICustomerMapper
|
implements ICustomerMapper
|
||||||
{
|
{
|
||||||
private customerItemMapper: CustomerItemMapper;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.customerItemMapper = new CustomerItemMapper(); // Instanciar el mapper de items
|
|
||||||
}
|
|
||||||
|
|
||||||
public mapToDomain(source: CustomerModel, params?: MapperParamsType): Result<Customer, Error> {
|
public mapToDomain(source: CustomerModel, params?: MapperParamsType): Result<Customer, Error> {
|
||||||
const idOrError = UniqueID.create(source.id);
|
try {
|
||||||
const statusOrError = CustomerStatus.create(source.invoice_status);
|
const errors: ValidationErrorDetail[] = [];
|
||||||
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);
|
|
||||||
|
|
||||||
const result = Result.combine([
|
const customerId = extractOrPushError(UniqueID.create(source.id), "id", errors);
|
||||||
idOrError,
|
const companyId = extractOrPushError(
|
||||||
statusOrError,
|
UniqueID.create(source.company_id),
|
||||||
customerSeriesOrError,
|
"company_id",
|
||||||
customerNumberOrError,
|
errors
|
||||||
issueDateOrError,
|
);
|
||||||
operationDateOrError,
|
const status = extractOrPushError(CustomerStatus.create(source.status), "status", errors);
|
||||||
]);
|
const reference = source.reference?.trim() === "" ? undefined : source.reference;
|
||||||
|
|
||||||
if (result.isFailure) {
|
const isCompany = source.is_company ?? true;
|
||||||
return Result.fail(result.error);
|
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 {
|
public mapToPersistence(source: Customer, params?: MapperParamsType): CustomerCreationAttributes {
|
||||||
const subtotal = source.calculateSubtotal();
|
|
||||||
const total = source.calculateTotal();
|
|
||||||
|
|
||||||
const items = this.customerItemMapper.mapCollectionToPersistence(source.items, params);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: source.id.toString(),
|
id: source.id.toPrimitive(),
|
||||||
invoice_status: source.status.toPrimitive(),
|
company_id: source.companyId.toPrimitive(),
|
||||||
invoice_series: source.invoiceSeries.toPrimitive(),
|
reference: source.reference,
|
||||||
invoice_number: source.invoiceNumber.toPrimitive(),
|
is_company: source.isCompany,
|
||||||
issue_date: source.issueDate.toPrimitive(),
|
name: source.name,
|
||||||
operation_date: source.operationDate.toPrimitive(),
|
trade_name: source.tradeName,
|
||||||
invoice_language: "es",
|
tin: source.tin.toPrimitive(),
|
||||||
invoice_currency: source.currency || "EUR",
|
|
||||||
|
|
||||||
subtotal_amount: subtotal.amount,
|
email: source.email.toPrimitive(),
|
||||||
subtotal_scale: subtotal.scale,
|
phone: source.phone.toPrimitive(),
|
||||||
|
fax: source.fax.toPrimitive(),
|
||||||
|
website: source.website,
|
||||||
|
|
||||||
total_amount: total.amount,
|
default_tax: source.defaultTax.toString(),
|
||||||
total_scale: total.scale,
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,4 @@
|
|||||||
import {
|
import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize";
|
||||||
CreationOptional,
|
|
||||||
DataTypes,
|
|
||||||
InferAttributes,
|
|
||||||
InferCreationAttributes,
|
|
||||||
Model,
|
|
||||||
Sequelize,
|
|
||||||
} from "sequelize";
|
|
||||||
|
|
||||||
export type CustomerCreationAttributes = InferCreationAttributes<CustomerModel, {}> & {};
|
export type CustomerCreationAttributes = InferCreationAttributes<CustomerModel, {}> & {};
|
||||||
|
|
||||||
@ -20,14 +13,15 @@ export class CustomerModel extends Model<
|
|||||||
|
|
||||||
declare id: string;
|
declare id: string;
|
||||||
declare company_id: string;
|
declare company_id: string;
|
||||||
declare reference: CreationOptional<string>;
|
declare reference: string;
|
||||||
|
|
||||||
declare is_company: boolean;
|
declare is_company: boolean;
|
||||||
declare name: string;
|
declare name: string;
|
||||||
declare trade_name: CreationOptional<string>;
|
declare trade_name: string;
|
||||||
declare tin: string;
|
declare tin: string;
|
||||||
|
|
||||||
declare street: string;
|
declare street: string;
|
||||||
|
declare street2: string;
|
||||||
declare city: string;
|
declare city: string;
|
||||||
declare state: string;
|
declare state: string;
|
||||||
declare postal_code: string;
|
declare postal_code: string;
|
||||||
@ -35,12 +29,12 @@ export class CustomerModel extends Model<
|
|||||||
|
|
||||||
declare email: string;
|
declare email: string;
|
||||||
declare phone: string;
|
declare phone: string;
|
||||||
declare fax: CreationOptional<string>;
|
declare fax: string;
|
||||||
declare website: CreationOptional<string>;
|
declare website: string;
|
||||||
|
|
||||||
declare legal_record: string;
|
declare legal_record: string;
|
||||||
|
|
||||||
declare default_tax: number;
|
declare default_tax: string;
|
||||||
declare status: string;
|
declare status: string;
|
||||||
declare lang_code: string;
|
declare lang_code: string;
|
||||||
declare currency_code: string;
|
declare currency_code: string;
|
||||||
@ -64,10 +58,12 @@ export default (database: Sequelize) => {
|
|||||||
reference: {
|
reference: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
is_company: {
|
is_company: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
@ -75,38 +71,50 @@ export default (database: Sequelize) => {
|
|||||||
},
|
},
|
||||||
trade_name: {
|
trade_name: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: false,
|
||||||
defaultValue: null,
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
tin: {
|
tin: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
street: {
|
street: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
street2: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
city: {
|
city: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
postal_code: {
|
postal_code: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
country: {
|
country: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
email: {
|
email: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
defaultValue: "",
|
||||||
validate: {
|
validate: {
|
||||||
isEmail: true,
|
isEmail: true,
|
||||||
},
|
},
|
||||||
@ -114,16 +122,17 @@ export default (database: Sequelize) => {
|
|||||||
phone: {
|
phone: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
fax: {
|
fax: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: false,
|
||||||
defaultValue: null,
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
website: {
|
website: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: false,
|
||||||
defaultValue: null,
|
defaultValue: "",
|
||||||
validate: {
|
validate: {
|
||||||
isUrl: true,
|
isUrl: true,
|
||||||
},
|
},
|
||||||
@ -131,12 +140,13 @@ export default (database: Sequelize) => {
|
|||||||
legal_record: {
|
legal_record: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
default_tax: {
|
default_tax: {
|
||||||
type: new DataTypes.SMALLINT(),
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: 2100,
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
lang_code: {
|
lang_code: {
|
||||||
|
|||||||
@ -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 { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
import { Collection, Result } from "@repo/rdx-utils";
|
||||||
@ -30,10 +30,18 @@ export class CustomerRepository
|
|||||||
async save(customer: Customer, transaction: Transaction): Promise<Result<Customer, Error>> {
|
async save(customer: Customer, transaction: Transaction): Promise<Result<Customer, Error>> {
|
||||||
try {
|
try {
|
||||||
const data = this.mapper.mapToPersistence(customer);
|
const data = this.mapper.mapToPersistence(customer);
|
||||||
|
console.debug("Saving customer to database:", data);
|
||||||
|
|
||||||
const [instance] = await CustomerModel.upsert(data, { transaction, returning: true });
|
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) {
|
} 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));
|
return Result.ok(Boolean(count > 0));
|
||||||
} catch (error: any) {
|
} 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);
|
return this.mapper.mapToDomain(row);
|
||||||
} catch (error: any) {
|
} 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);
|
return this.mapper.mapArrayToDomain(instances);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return Result.fail(errorMapper.toDomainError(err));
|
return Result.fail(translateSequelizeError(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +162,7 @@ export class CustomerRepository
|
|||||||
return Result.ok<void>();
|
return Result.ok<void>();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
// , `Error deleting customer ${id} in company ${companyId}`
|
// , `Error deleting customer ${id} in company ${companyId}`
|
||||||
return Result.fail(errorMapper.toDomainError(err));
|
return Result.fail(translateSequelizeError(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,30 +2,31 @@ import * as z from "zod/v4";
|
|||||||
|
|
||||||
export const CreateCustomerRequestSchema = z.object({
|
export const CreateCustomerRequestSchema = z.object({
|
||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
reference: z.string().optional(),
|
company_id: z.uuid(),
|
||||||
|
reference: z.string().default(""),
|
||||||
|
|
||||||
is_company: z.boolean(),
|
is_company: z.boolean().default(true),
|
||||||
name: z.string(),
|
name: z.string().default(""),
|
||||||
trade_name: z.string(),
|
trade_name: z.string().default(""),
|
||||||
tin: z.string(),
|
tin: z.string().default(""),
|
||||||
|
|
||||||
street: z.string(),
|
street: z.string().default(""),
|
||||||
city: z.string(),
|
city: z.string().default(""),
|
||||||
state: z.string(),
|
state: z.string().default(""),
|
||||||
postal_code: z.string(),
|
postal_code: z.string().default(""),
|
||||||
country: z.string(),
|
country: z.string().default(""),
|
||||||
|
|
||||||
email: z.string(),
|
email: z.string().default(""),
|
||||||
phone: z.string(),
|
phone: z.string().default(""),
|
||||||
fax: z.string(),
|
fax: z.string().default(""),
|
||||||
website: z.string(),
|
website: z.string().default(""),
|
||||||
|
|
||||||
legal_record: z.string(),
|
legal_record: z.string().default(""),
|
||||||
|
|
||||||
default_tax: z.array(z.string()),
|
default_tax: z.array(z.string()).default([]),
|
||||||
status: z.string(),
|
status: z.string().default("active"),
|
||||||
lang_code: z.string(),
|
lang_code: z.string().default("es"),
|
||||||
currency_code: z.string(),
|
currency_code: z.string().default("EUR"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateCustomerRequestDTO = z.infer<typeof CreateCustomerRequestSchema>;
|
export type CreateCustomerRequestDTO = z.infer<typeof CreateCustomerRequestSchema>;
|
||||||
|
|||||||
@ -3,14 +3,16 @@ import * as z from "zod/v4";
|
|||||||
|
|
||||||
export const CustomerCreationResponseSchema = z.object({
|
export const CustomerCreationResponseSchema = z.object({
|
||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
|
company_id: z.uuid(),
|
||||||
reference: z.string(),
|
reference: z.string(),
|
||||||
|
|
||||||
is_companyr: z.boolean(),
|
is_company: z.boolean(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
trade_name: z.string(),
|
trade_name: z.string(),
|
||||||
tin: z.string(),
|
tin: z.string(),
|
||||||
|
|
||||||
street: z.string(),
|
street: z.string(),
|
||||||
|
street2: z.string(),
|
||||||
city: z.string(),
|
city: z.string(),
|
||||||
state: z.string(),
|
state: z.string(),
|
||||||
postal_code: z.string(),
|
postal_code: z.string(),
|
||||||
@ -23,7 +25,7 @@ export const CustomerCreationResponseSchema = z.object({
|
|||||||
|
|
||||||
legal_record: z.string(),
|
legal_record: z.string(),
|
||||||
|
|
||||||
default_tax: z.number(),
|
default_tax: z.array(z.string()),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
lang_code: z.string(),
|
lang_code: z.string(),
|
||||||
currency_code: z.string(),
|
currency_code: z.string(),
|
||||||
|
|||||||
@ -28,6 +28,6 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src", "../core/src/api/helpers/extract-or-push-error.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,7 +47,7 @@ export class Percentage extends ValueObject<IPercentageProps> implements IPercen
|
|||||||
|
|
||||||
const validationResult = Percentage.validate({ amount, scale });
|
const validationResult = Percentage.validate({ amount, scale });
|
||||||
if (!validationResult.success) {
|
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
|
// Cálculo del valor real del porcentaje
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export class Slug extends ValueObject<SlugProps> {
|
|||||||
const valueIsValid = Slug.validate(value);
|
const valueIsValid = Slug.validate(value);
|
||||||
|
|
||||||
if (!valueIsValid.success) {
|
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! }));
|
return Result.ok(new Slug({ value: valueIsValid.data! }));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export class EmailAddress extends ValueObject<EmailAddressProps> {
|
|||||||
const valueIsValid = EmailAddress.validate(value);
|
const valueIsValid = EmailAddress.validate(value);
|
||||||
|
|
||||||
if (!valueIsValid.success) {
|
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 }));
|
return Result.ok(new EmailAddress({ value: valueIsValid.data }));
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export class Name extends ValueObject<INameProps> {
|
|||||||
const valueIsValid = Name.validate(value);
|
const valueIsValid = Name.validate(value);
|
||||||
|
|
||||||
if (!valueIsValid.success) {
|
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 }));
|
return Result.ok(new Name({ value }));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export class Percentage extends ValueObject<IPercentageProps> implements IPercen
|
|||||||
|
|
||||||
const validationResult = Percentage.validate({ amount, scale });
|
const validationResult = Percentage.validate({ amount, scale });
|
||||||
if (!validationResult.success) {
|
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
|
// Cálculo del valor real del porcentaje
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { Maybe } from "@repo/rdx-utils";
|
import { isPossiblePhoneNumber, parsePhoneNumberWithError } from "libphonenumber-js";
|
||||||
import { isValidPhoneNumber, parsePhoneNumberWithError } from "libphonenumber-js";
|
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
import { ValueObject } from "./value-object";
|
import { ValueObject } from "./value-object";
|
||||||
|
|
||||||
@ -9,34 +8,49 @@ interface PhoneNumberProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PhoneNumber extends ValueObject<PhoneNumberProps> {
|
export class PhoneNumber extends ValueObject<PhoneNumberProps> {
|
||||||
|
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<PhoneNumber> {
|
static create(value: string): Result<PhoneNumber> {
|
||||||
const valueIsValid = PhoneNumber.validate(value);
|
const valueIsValid = PhoneNumber.validate(value);
|
||||||
|
|
||||||
if (!valueIsValid.success) {
|
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 }));
|
return Result.ok(new PhoneNumber({ value: valueIsValid.data }));
|
||||||
}
|
}
|
||||||
|
|
||||||
static createNullable(value?: string): Result<Maybe<PhoneNumber>, Error> {
|
|
||||||
if (!value || value.trim() === "") {
|
|
||||||
return Result.ok(Maybe.none<PhoneNumber>());
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
getValue(): string {
|
||||||
return this.props.value;
|
return this.props.value;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,11 +11,11 @@ const postalCodeSchema = z
|
|||||||
message: "Invalid postal code format",
|
message: "Invalid postal code format",
|
||||||
});
|
});
|
||||||
|
|
||||||
const streetSchema = z.string().min(2).max(255);
|
const streetSchema = z.string().max(255).default("");
|
||||||
const street2Schema = z.string().optional();
|
const street2Schema = z.string().default("");
|
||||||
const citySchema = z.string().min(2).max(50);
|
const citySchema = z.string().max(50).default("");
|
||||||
const stateSchema = z.string().min(2).max(50);
|
const stateSchema = z.string().max(50).default("");
|
||||||
const countrySchema = z.string().min(2).max(56);
|
const countrySchema = z.string().max(56).default("");
|
||||||
|
|
||||||
interface IPostalAddressProps {
|
interface IPostalAddressProps {
|
||||||
street: string;
|
street: string;
|
||||||
@ -44,7 +44,7 @@ export class PostalAddress extends ValueObject<IPostalAddressProps> {
|
|||||||
const valueIsValid = PostalAddress.validate(values);
|
const valueIsValid = PostalAddress.validate(values);
|
||||||
|
|
||||||
if (!valueIsValid.success) {
|
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));
|
return Result.ok(new PostalAddress(values));
|
||||||
}
|
}
|
||||||
@ -63,7 +63,7 @@ export class PostalAddress extends ValueObject<IPostalAddressProps> {
|
|||||||
): Result<PostalAddress, Error> {
|
): Result<PostalAddress, Error> {
|
||||||
return PostalAddress.create({
|
return PostalAddress.create({
|
||||||
street: data.street ?? oldAddress.street,
|
street: data.street ?? oldAddress.street,
|
||||||
street2: data.street2?.getOrUndefined() ?? oldAddress.street2.getOrUndefined(),
|
street2: data.street2 ?? oldAddress.street2,
|
||||||
city: data.city ?? oldAddress.city,
|
city: data.city ?? oldAddress.city,
|
||||||
postalCode: data.postalCode ?? oldAddress.postalCode,
|
postalCode: data.postalCode ?? oldAddress.postalCode,
|
||||||
state: data.state ?? oldAddress.state,
|
state: data.state ?? oldAddress.state,
|
||||||
@ -76,8 +76,8 @@ export class PostalAddress extends ValueObject<IPostalAddressProps> {
|
|||||||
return this.props.street;
|
return this.props.street;
|
||||||
}
|
}
|
||||||
|
|
||||||
get street2(): Maybe<string> {
|
get street2(): string {
|
||||||
return Maybe.fromNullable(this.props.street2);
|
return this.props.street2 ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
get city(): string {
|
get city(): string {
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export class Slug extends ValueObject<SlugProps> {
|
|||||||
const valueIsValid = Slug.validate(value);
|
const valueIsValid = Slug.validate(value);
|
||||||
|
|
||||||
if (!valueIsValid.success) {
|
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: <explanation>
|
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||||
return Result.ok(new Slug({ value: valueIsValid.data! }));
|
return Result.ok(new Slug({ value: valueIsValid.data! }));
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export class TINNumber extends ValueObject<TINNumberProps> {
|
|||||||
const valueIsValid = TINNumber.validate(value);
|
const valueIsValid = TINNumber.validate(value);
|
||||||
|
|
||||||
if (!valueIsValid.success) {
|
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 }));
|
return Result.ok(new TINNumber({ value: valueIsValid.data }));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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))
|
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:
|
ts-jest:
|
||||||
specifier: ^29.2.5
|
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:
|
tsconfig-paths:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
@ -289,6 +289,9 @@ importers:
|
|||||||
'@erp/core':
|
'@erp/core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core
|
||||||
|
'@repo/rdx-ddd':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/rdx-ddd
|
||||||
'@repo/rdx-ui':
|
'@repo/rdx-ui':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/rdx-ui
|
version: link:../../packages/rdx-ui
|
||||||
@ -332,6 +335,9 @@ importers:
|
|||||||
'@biomejs/biome':
|
'@biomejs/biome':
|
||||||
specifier: 1.9.4
|
specifier: 1.9.4
|
||||||
version: 1.9.4
|
version: 1.9.4
|
||||||
|
'@types/express':
|
||||||
|
specifier: ^4.17.21
|
||||||
|
version: 4.17.23
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19.1.2
|
specifier: ^19.1.2
|
||||||
version: 19.1.8
|
version: 19.1.8
|
||||||
@ -11668,7 +11674,7 @@ snapshots:
|
|||||||
|
|
||||||
ts-interface-checker@0.1.13: {}
|
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:
|
dependencies:
|
||||||
bs-logger: 0.2.6
|
bs-logger: 0.2.6
|
||||||
ejs: 3.1.10
|
ejs: 3.1.10
|
||||||
@ -11686,6 +11692,7 @@ snapshots:
|
|||||||
'@jest/transform': 29.7.0
|
'@jest/transform': 29.7.0
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
babel-jest: 29.7.0(@babel/core@7.27.4)
|
babel-jest: 29.7.0(@babel/core@7.27.4)
|
||||||
|
esbuild: 0.25.5
|
||||||
jest-util: 29.7.0
|
jest-util: 29.7.0
|
||||||
|
|
||||||
ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3):
|
ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user