Reestructurar servidor y configuración
This commit is contained in:
parent
d1b7731b90
commit
b3c5e650fc
@ -1,8 +1,19 @@
|
|||||||
|
# ───────────────────────────────
|
||||||
|
# Core del servidor HTTP
|
||||||
|
# ───────────────────────────────
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
PORT=3002
|
PORT=3002
|
||||||
|
|
||||||
|
# URL pública del frontend (CORS).
|
||||||
|
# En dev se puede permitir todo con '*'
|
||||||
FRONTEND_URL=http://localhost:5173
|
FRONTEND_URL=http://localhost:5173
|
||||||
|
|
||||||
|
|
||||||
|
# ───────────────────────────────
|
||||||
|
# Base de datos (Sequelize / MySQL-MariaDB)
|
||||||
|
# ───────────────────────────────
|
||||||
|
|
||||||
# Base de datos (opción 1: URL)
|
# Base de datos (opción 1: URL)
|
||||||
# DATABASE_URL=postgres://user:pass@localhost:5432/dbname
|
# DATABASE_URL=postgres://user:pass@localhost:5432/dbname
|
||||||
|
|
||||||
@ -14,12 +25,39 @@ DB_NAME=uecko_erp
|
|||||||
DB_USER=rodax
|
DB_USER=rodax
|
||||||
DB_PASSWORD=rodax
|
DB_PASSWORD=rodax
|
||||||
|
|
||||||
|
# Log de Sequelize (true|false)
|
||||||
DB_LOGGING=false
|
DB_LOGGING=false
|
||||||
DB_SYNC_MODE=alter
|
|
||||||
|
|
||||||
APP_TIMEZONE=Europe/Madrid
|
# Si necesitas SSL/TLS en MySQL (por defecto no)
|
||||||
TRUST_PROXY=0
|
DB_SSL=false
|
||||||
|
|
||||||
|
DB_SYNC_MODE=alter # none | alter | force
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ───────────────────────────────
|
||||||
|
# Logging de la app
|
||||||
|
# ───────────────────────────────
|
||||||
|
LOG_LEVEL=info # error | warn | info | debug
|
||||||
|
LOG_JSON=false # true para logs en JSON (entornos con agregadores)
|
||||||
|
|
||||||
|
|
||||||
|
# ───────────────────────────────
|
||||||
|
# Warmup por módulo
|
||||||
|
# ───────────────────────────────
|
||||||
|
# Tiempo máximo para cada warmup() de un módulo, en milisegundos.
|
||||||
|
WARMUP_TIMEOUT_MS=10000
|
||||||
|
|
||||||
|
# Si es true, un fallo de warmup aborta el arranque. Si es false, continúa con warning.
|
||||||
|
WARMUP_STRICT=false
|
||||||
|
|
||||||
|
# ───────────────────────────────
|
||||||
|
# Seguridad / Auth
|
||||||
|
# ───────────────────────────────
|
||||||
JWT_SECRET=supersecretkey
|
JWT_SECRET=supersecretkey
|
||||||
JWT_ACCESS_EXPIRATION=1h
|
JWT_ACCESS_EXPIRATION=1h
|
||||||
JWT_REFRESH_EXPIRATION=7d
|
JWT_REFRESH_EXPIRATION=7d
|
||||||
|
|
||||||
|
# ───────────────────────────────
|
||||||
|
# Otros (opcional / a futuro)
|
||||||
|
# ───────────────────────────────
|
||||||
|
|||||||
@ -6,10 +6,18 @@ import { registerService } from "./service-registry";
|
|||||||
const registeredModules: Map<string, IModuleServer> = new Map();
|
const registeredModules: Map<string, IModuleServer> = new Map();
|
||||||
const initializedModules = new Set<string>();
|
const initializedModules = new Set<string>();
|
||||||
const visiting = new Set<string>(); // para detección de ciclos
|
const visiting = new Set<string>(); // para detección de ciclos
|
||||||
|
const initializationOrder: string[] = []; // orden de init → para warmup
|
||||||
|
|
||||||
|
// Config opcional para warmup (valores por defecto seguros)
|
||||||
|
const WARMUP_TIMEOUT_MS = Number(process.env.WARMUP_TIMEOUT_MS) || 10_000;
|
||||||
|
const WARMUP_STRICT =
|
||||||
|
String(process.env.WARMUP_STRICT ?? "false")
|
||||||
|
.toLowerCase()
|
||||||
|
.trim() === "true";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Registra un módulo del servidor en el registry.
|
Registra un módulo del servidor en el registry.
|
||||||
Lanza error si el nombre ya existe.
|
Lanza error si el nombre ya existe.
|
||||||
*/
|
*/
|
||||||
export function registerModule(pkg: IModuleServer) {
|
export function registerModule(pkg: IModuleServer) {
|
||||||
if (!pkg?.name) {
|
if (!pkg?.name) {
|
||||||
@ -23,20 +31,23 @@ export function registerModule(pkg: IModuleServer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Inicializa todos los módulos registrados (resolviendo dependencias),
|
Inicializa todos los módulos registrados (resolviendo dependencias),
|
||||||
y al final inicializa los modelos (Sequelize) en bloque.
|
luego inicializa los modelos (Sequelize) en bloque y, por último, ejecuta warmups opcionales.
|
||||||
*/
|
*/
|
||||||
export async function initModules(params: ModuleParams) {
|
export async function initModules(params: ModuleParams) {
|
||||||
for (const name of registeredModules.keys()) {
|
for (const name of registeredModules.keys()) {
|
||||||
await loadModule(name, params, []); // secuencial para logs deterministas
|
await loadModule(name, params, []); // secuencial para logs deterministas
|
||||||
}
|
}
|
||||||
|
|
||||||
await withPhase("global", "initModels", async () => {
|
await withPhase("global", "initModels", async () => {
|
||||||
await initModels(params);
|
await initModels(params);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await warmupModules(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Carga recursivamente un módulo y sus dependencias con detección de ciclos.
|
Carga recursivamente un módulo y sus dependencias con detección de ciclos.
|
||||||
*/
|
*/
|
||||||
async function loadModule(name: string, params: ModuleParams, stack: string[]) {
|
async function loadModule(name: string, params: ModuleParams, stack: string[]) {
|
||||||
if (initializedModules.has(name)) return;
|
if (initializedModules.has(name)) return;
|
||||||
@ -88,30 +99,95 @@ async function loadModule(name: string, params: ModuleParams, stack: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initializedModules.add(name);
|
initializedModules.add(name);
|
||||||
|
initializationOrder.push(name); // recordamos el orden para warmup
|
||||||
|
|
||||||
visiting.delete(name);
|
visiting.delete(name);
|
||||||
stack.pop();
|
stack.pop();
|
||||||
|
|
||||||
logger.info(`✅ Module "${name}" registered`, { label: "moduleRegistry" });
|
logger.info(`✅ Module "${name}" registered`, { label: "moduleRegistry" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Helper para anotar fase y módulo en errores y logs.
|
Ejecuta warmup() opcional de cada módulo en orden de inicialización.
|
||||||
|
Si WARMUP_STRICT=true, un fallo aborta el arranque; si no, se avisa y continúa.
|
||||||
|
*/
|
||||||
|
async function warmupModules(params: ModuleParams) {
|
||||||
|
logger.info("🌡️ Warmup: starting...", { label: "moduleRegistry" });
|
||||||
|
|
||||||
|
for (const name of initializationOrder) {
|
||||||
|
const pkg = registeredModules.get(name);
|
||||||
|
if (!pkg) continue;
|
||||||
|
|
||||||
|
const maybeWarmup = (pkg as unknown as { warmup?: (p: ModuleParams) => Promise<void> | void })
|
||||||
|
.warmup;
|
||||||
|
|
||||||
|
if (typeof maybeWarmup === "function") {
|
||||||
|
try {
|
||||||
|
await withPhase(name, "warmup", () =>
|
||||||
|
withTimeout(Promise.resolve(maybeWarmup(params)), WARMUP_TIMEOUT_MS, `${name}.warmup`)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (WARMUP_STRICT) {
|
||||||
|
logger.error("⛔️ Warmup failed (strict=true). Aborting.", {
|
||||||
|
label: "moduleRegistry",
|
||||||
|
module: name,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn("⚠️ Warmup failed but continuing (strict=false)", {
|
||||||
|
label: "moduleRegistry",
|
||||||
|
module: name,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("🌡️ Warmup: done.", { label: "moduleRegistry" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Helper para anotar fase y módulo en logs (inicio/fin/duración) y errores.
|
||||||
*/
|
*/
|
||||||
async function withPhase<T>(
|
async function withPhase<T>(
|
||||||
moduleName: string,
|
moduleName: string,
|
||||||
phase: "init" | "registerDependencies" | "registerModels" | "registerServices" | "initModels",
|
phase:
|
||||||
|
| "init"
|
||||||
|
| "registerDependencies"
|
||||||
|
| "registerModels"
|
||||||
|
| "registerServices"
|
||||||
|
| "initModels"
|
||||||
|
| "warmup",
|
||||||
fn: () => Promise<T> | T
|
fn: () => Promise<T> | T
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
logger.info(`▶️ phase=start module=${moduleName} ${phase}`, {
|
||||||
|
label: "moduleRegistry",
|
||||||
|
module: moduleName,
|
||||||
|
phase,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await fn();
|
const out = await fn();
|
||||||
|
const duration = Date.now() - startedAt;
|
||||||
|
logger.info(`✅ phase=done module=${moduleName} ${phase} durationMs=${duration}`, {
|
||||||
|
label: "moduleRegistry",
|
||||||
|
module: moduleName,
|
||||||
|
phase,
|
||||||
|
durationMs: duration,
|
||||||
|
});
|
||||||
|
return out;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Log enriquecido con contexto
|
const duration = Date.now() - startedAt;
|
||||||
logger.error(
|
logger.error(
|
||||||
`⛔️ Module "${moduleName}" failed at phase="${phase}": ${error?.message ?? error}`,
|
`⛔️ Module "${moduleName}" failed at phase="${phase}" after ${duration}ms: ${error?.message ?? error}`,
|
||||||
{
|
{
|
||||||
label: "moduleRegistry",
|
label: "moduleRegistry",
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
phase,
|
phase,
|
||||||
|
durationMs: duration,
|
||||||
stack: error?.stack,
|
stack: error?.stack,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -123,3 +199,18 @@ async function withPhase<T>(
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Promesa con timeout; rechaza si excede el tiempo dado.
|
||||||
|
*/
|
||||||
|
function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
|
||||||
|
let timer: NodeJS.Timeout | null = null;
|
||||||
|
const timeout = new Promise<T>((_, reject) => {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
reject(new Error(`Timeout after ${ms}ms (${label})`));
|
||||||
|
}, ms).unref();
|
||||||
|
});
|
||||||
|
return Promise.race([p, timeout]).finally(() => {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
}) as Promise<T>;
|
||||||
|
}
|
||||||
|
|||||||
@ -6,6 +6,13 @@
|
|||||||
"./api": "./src/api/index.ts",
|
"./api": "./src/api/index.ts",
|
||||||
"./client": "./src/web/index.ts"
|
"./client": "./src/web/index.ts"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@erp/core": "workspace:*",
|
||||||
|
"dinero.js": "^1.9.1",
|
||||||
|
|
||||||
|
"sequelize": "^6.37.5",
|
||||||
|
"zod": "^3.25.67"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
@ -18,6 +25,7 @@
|
|||||||
"@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",
|
"i18next": "^25.1.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { IModuleServer, ModuleParams } from "@erp/core/api";
|
export * from "./lib";
|
||||||
|
|
||||||
export const authAPIModule: IModuleServer = {
|
/* export const authAPIModule: IModuleServer = {
|
||||||
name: "auth",
|
name: "auth",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
@ -16,10 +16,10 @@ export const authAPIModule: IModuleServer = {
|
|||||||
return {
|
return {
|
||||||
//models,
|
//models,
|
||||||
services: {
|
services: {
|
||||||
/*...*/
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default authAPIModule;
|
export default authAPIModule; */
|
||||||
|
|||||||
13
modules/auth/src/api/lib/express/auth-types.ts
Normal file
13
modules/auth/src/api/lib/express/auth-types.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
|
export type RequestUser = {
|
||||||
|
userId: string;
|
||||||
|
companyId: string; // tenant
|
||||||
|
roles?: string[];
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RequestWithAuth<T = any> = Request & {
|
||||||
|
user: RequestUser;
|
||||||
|
body: T;
|
||||||
|
};
|
||||||
2
modules/auth/src/api/lib/express/index.ts
Normal file
2
modules/auth/src/api/lib/express/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./auth-types";
|
||||||
|
export * from "./tenancy.middleware";
|
||||||
30
modules/auth/src/api/lib/express/tenancy.middleware.ts
Normal file
30
modules/auth/src/api/lib/express/tenancy.middleware.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { NextFunction, Response } from "express";
|
||||||
|
import { RequestWithAuth } from "./auth-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware que exige presencia de usuario y companyId.
|
||||||
|
* Debe ir DESPUÉS del middleware de autenticación.
|
||||||
|
*/
|
||||||
|
export function enforceTenant() {
|
||||||
|
return (req: RequestWithAuth, res: Response, next: NextFunction) => {
|
||||||
|
// Validación básica del tenant
|
||||||
|
if (!req.user || !req.user.companyId) {
|
||||||
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
|
}
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
1
modules/auth/src/api/lib/index.ts
Normal file
1
modules/auth/src/api/lib/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./express";
|
||||||
@ -12,12 +12,33 @@ import {
|
|||||||
ValidationApiError,
|
ValidationApiError,
|
||||||
} from "../../errors";
|
} from "../../errors";
|
||||||
|
|
||||||
|
type GuardResultLike = { isFailure: boolean; error?: ApiError };
|
||||||
|
export type GuardContext = {
|
||||||
|
req: Request;
|
||||||
|
res: Response;
|
||||||
|
next: NextFunction;
|
||||||
|
controller: ExpressController;
|
||||||
|
criteria: Criteria;
|
||||||
|
};
|
||||||
|
export type GuardFn = (ctx: GuardContext) => GuardResultLike | Promise<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; //| AuthenticatedRequest | TabContextRequest;
|
protected req!: Request;
|
||||||
protected res!: Response;
|
protected res!: Response;
|
||||||
protected next!: NextFunction;
|
protected next!: NextFunction;
|
||||||
protected criteria!: Criteria;
|
protected criteria!: Criteria;
|
||||||
|
|
||||||
|
// 🔹 Guards configurables por controlador
|
||||||
|
private guards: GuardFn[] = [];
|
||||||
|
|
||||||
static errorResponse(apiError: ApiError, res: Response) {
|
static errorResponse(apiError: ApiError, res: Response) {
|
||||||
return res.status(apiError.status).json(apiError);
|
return res.status(apiError.status).json(apiError);
|
||||||
}
|
}
|
||||||
@ -38,100 +59,111 @@ export abstract class ExpressController {
|
|||||||
return this.res.sendStatus(httpStatus.NO_CONTENT);
|
return this.res.sendStatus(httpStatus.NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Respuesta para errores de cliente (400 Bad Request)
|
|
||||||
*/
|
|
||||||
protected clientError(message: string, errors?: any[] | any) {
|
protected clientError(message: string, errors?: any[] | any) {
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(
|
||||||
new ValidationApiError(message, Array.isArray(errors) ? errors : [errors]),
|
new ValidationApiError(message, Array.isArray(errors) ? errors : [errors]),
|
||||||
this.res
|
this.res
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Respuesta para errores de autenticación (401 Unauthorized)
|
|
||||||
*/
|
|
||||||
protected unauthorizedError(message?: string) {
|
protected unauthorizedError(message?: string) {
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(
|
||||||
new UnauthorizedApiError(message ?? "Unauthorized"),
|
new UnauthorizedApiError(message ?? "Unauthorized"),
|
||||||
this.res
|
this.res
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Respuesta para errores de autorización (403 Forbidden)
|
|
||||||
*/
|
|
||||||
protected forbiddenError(message?: string) {
|
protected forbiddenError(message?: string) {
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(
|
||||||
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
|
this.res
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Respuesta para recursos no encontrados (404 Not Found)
|
|
||||||
*/
|
|
||||||
protected notFoundError(message: string) {
|
protected notFoundError(message: string) {
|
||||||
return ExpressController.errorResponse(new NotFoundApiError(message), this.res);
|
return ExpressController.errorResponse(new NotFoundApiError(message), this.res);
|
||||||
}
|
}
|
||||||
|
protected conflictError(message: string, _errors?: any[]) {
|
||||||
/**
|
|
||||||
* Respuesta para conflictos (409 Conflict)
|
|
||||||
*/
|
|
||||||
protected conflictError(message: string, errors?: any[]) {
|
|
||||||
return ExpressController.errorResponse(new ConflictApiError(message), this.res);
|
return ExpressController.errorResponse(new ConflictApiError(message), this.res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Respuesta para errores de validación de entrada (422 Unprocessable Entity)
|
|
||||||
*/
|
|
||||||
protected invalidInputError(message: string, errors?: any[]) {
|
protected invalidInputError(message: string, errors?: any[]) {
|
||||||
return ExpressController.errorResponse(new ValidationApiError(message, errors), this.res);
|
return ExpressController.errorResponse(new ValidationApiError(message, errors), this.res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Respuesta para errores de servidor no disponible (503 Service Unavailable)
|
|
||||||
*/
|
|
||||||
protected unavailableError(message?: string) {
|
protected unavailableError(message?: string) {
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(
|
||||||
new UnavailableApiError(message ?? "Service temporarily unavailable."),
|
new UnavailableApiError(message ?? "Service temporarily unavailable."),
|
||||||
this.res
|
this.res
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Respuesta para errores internos del servidor (500 Internal Server Error)
|
|
||||||
*/
|
|
||||||
protected internalServerError(message?: string) {
|
protected internalServerError(message?: string) {
|
||||||
return ExpressController.errorResponse(
|
return ExpressController.errorResponse(
|
||||||
new InternalApiError(message ?? "Internal Server Error"),
|
new InternalApiError(message ?? "Internal Server Error"),
|
||||||
this.res
|
this.res
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Respuesta para cualquier error de la API
|
|
||||||
*/
|
|
||||||
protected handleApiError(apiError: ApiError) {
|
protected handleApiError(apiError: ApiError) {
|
||||||
return ExpressController.errorResponse(apiError, this.res);
|
return ExpressController.errorResponse(apiError, this.res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
* Método principal que se invoca desde el router de Express.
|
// Guards API
|
||||||
* Maneja la conversión de la URL a criterios y llama a executeImpl.
|
protected useGuards(...guards: GuardFn[]): this {
|
||||||
*/
|
this.guards.push(...guards);
|
||||||
public execute(req: Request, res: Response, next: NextFunction): void {
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runGuards(): Promise<boolean> {
|
||||||
|
for (const guard of this.guards) {
|
||||||
|
const result = await guard({
|
||||||
|
req: this.req,
|
||||||
|
res: this.res,
|
||||||
|
next: this.next,
|
||||||
|
controller: this,
|
||||||
|
criteria: this.criteria,
|
||||||
|
});
|
||||||
|
if (result?.isFailure) {
|
||||||
|
const err = result.error ?? new InternalApiError("Guard failed without error");
|
||||||
|
this.handleApiError(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.req = req;
|
||||||
this.res = res;
|
this.res = res;
|
||||||
this.next = next;
|
this.next = next;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL(req.originalUrl, `http://${req.headers.host}`);
|
// 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);
|
this.criteria = new CriteriaFromUrlConverter().toCriteria(url);
|
||||||
|
|
||||||
this.executeImpl();
|
// 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) {
|
} catch (error: unknown) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
if (err instanceof ApiError) {
|
if (err instanceof ApiError) {
|
||||||
ExpressController.errorResponse(err, this.res);
|
ExpressController.errorResponse(err as ApiError, this.res);
|
||||||
} else {
|
} else {
|
||||||
ExpressController.errorResponse(new InternalApiError(err.message), this.res);
|
ExpressController.errorResponse(new InternalApiError(err.message), this.res);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { ForbiddenApiError, UnauthorizedApiError, ValidationApiError } from "../../errors";
|
||||||
|
import { GuardContext, GuardFn, guardFail, guardOk } from "./express-controller";
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
|
// Guards reutilizables (auth/tenancy). Si prefieres, muévelos a src/lib/http/express-guards.ts
|
||||||
|
|
||||||
|
export function authGuard(): GuardFn {
|
||||||
|
return ({ controller }: GuardContext) => {
|
||||||
|
const user = controller.getUser();
|
||||||
|
if (!user) return guardFail(new UnauthorizedApiError("Unauthorized"));
|
||||||
|
return guardOk();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tenantGuard(): GuardFn {
|
||||||
|
return ({ controller }: GuardContext) => {
|
||||||
|
const tenantId = controller.getTenantId();
|
||||||
|
if (!tenantId) return guardFail(new ForbiddenApiError("Tenant not found for user"));
|
||||||
|
return guardOk();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Impide que el cliente intente fijar el 'companyId' vía query (?companyId=...).
|
||||||
|
La política de scoping real debe hacerse en repos/servicios; esto es defensa adicional.
|
||||||
|
*/
|
||||||
|
export function forbidQueryFieldGuard(field = "companyId"): GuardFn {
|
||||||
|
return ({ req }: GuardContext) => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(req.query, field)) {
|
||||||
|
return guardFail(new ValidationApiError(`Query param "${field}" is not allowed`));
|
||||||
|
}
|
||||||
|
return guardOk();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Inyecta el tenant del usuario en el body (create/update).
|
||||||
|
Ignora cualquier valor que venga del cliente.
|
||||||
|
*/
|
||||||
|
export function injectTenantIntoBodyGuard(field = "companyId"): GuardFn {
|
||||||
|
return ({ controller }: GuardContext) => {
|
||||||
|
const tenantId = controller.getTenantId();
|
||||||
|
if (!tenantId) return guardFail(new ForbiddenApiError("Tenant not found for user"));
|
||||||
|
|
||||||
|
const body = (controller as any).req.body ?? {};
|
||||||
|
(controller as any).req.body = { ...body, [field]: tenantId };
|
||||||
|
return guardOk();
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./express-controller";
|
export * from "./express-controller";
|
||||||
|
export * from "./express-guards";
|
||||||
export * from "./middlewares";
|
export * from "./middlewares";
|
||||||
|
|||||||
@ -1,15 +1,9 @@
|
|||||||
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
||||||
import { IAggregateRootRepository, UniqueID } from "@repo/rdx-ddd";
|
import { IAggregateRootRepository, UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { FindOptions, ModelDefined, Sequelize, Transaction } from "sequelize";
|
import { FindOptions, ModelDefined, Transaction } from "sequelize";
|
||||||
|
|
||||||
export abstract class SequelizeRepository<T> implements IAggregateRootRepository<T> {
|
export abstract class SequelizeRepository<T> implements IAggregateRootRepository<T> {
|
||||||
protected readonly _database!: Sequelize;
|
|
||||||
|
|
||||||
constructor(database: Sequelize) {
|
|
||||||
this._database = database;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected convertCriteria(criteria: Criteria): FindOptions {
|
protected convertCriteria(criteria: Criteria): FindOptions {
|
||||||
return new CriteriaToSequelizeConverter().convert(criteria);
|
return new CriteriaToSequelizeConverter().convert(criteria);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,12 @@ import { Sequelize, Transaction } from "sequelize";
|
|||||||
import { TransactionManager } from "../database";
|
import { TransactionManager } from "../database";
|
||||||
|
|
||||||
export class SequelizeTransactionManager extends TransactionManager {
|
export class SequelizeTransactionManager extends TransactionManager {
|
||||||
protected _database: any | null = null;
|
protected _database: Sequelize | null = null;
|
||||||
|
|
||||||
protected async _startTransaction(): Promise<Transaction> {
|
protected async _startTransaction(): Promise<Transaction> {
|
||||||
return await this._database.transaction();
|
return await this._database!.transaction({
|
||||||
|
isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _commitTransaction(): Promise<void> {
|
protected async _commitTransaction(): Promise<void> {
|
||||||
|
|||||||
@ -10,7 +10,6 @@
|
|||||||
"./globals.css": "./src/web/globals.css"
|
"./globals.css": "./src/web/globals.css"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@erp/core": "workspace:*",
|
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"sequelize": "^6.37.5",
|
"sequelize": "^6.37.5",
|
||||||
@ -32,6 +31,8 @@
|
|||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@erp/customers": "workspace:*",
|
"@erp/customers": "workspace:*",
|
||||||
|
"@erp/core": "workspace:*",
|
||||||
|
"@erp/auth": "workspace:*",
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@repo/rdx-criteria": "workspace:*",
|
"@repo/rdx-criteria": "workspace:*",
|
||||||
"@repo/rdx-ddd": "workspace:*",
|
"@repo/rdx-ddd": "workspace:*",
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { CustomerInvoice } from "@erp/customer-invoices/api/domain";
|
import { CustomerInvoice } from "@erp/customer-invoices/api/domain";
|
||||||
import { CustomerInvoicesCreationResultDTO } from "@erp/customer-invoices/common/dto";
|
import { CustomerInvoicesCreationResponseDTO } from "@erp/customer-invoices/common/dto";
|
||||||
|
|
||||||
export class CreateCustomerInvoicesPresenter {
|
export class CreateCustomerInvoicesAssembler {
|
||||||
public toDTO(invoice: CustomerInvoice): CustomerInvoicesCreationResultDTO {
|
public toDTO(invoice: CustomerInvoice): CustomerInvoicesCreationResponseDTO {
|
||||||
return {
|
return {
|
||||||
id: invoice.id.toPrimitive(),
|
id: invoice.id.toPrimitive(),
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ export class CreateCustomerInvoicesPresenter {
|
|||||||
//subtotal_price: invoice.calculateSubtotal().toPrimitive(),
|
//subtotal_price: invoice.calculateSubtotal().toPrimitive(),
|
||||||
//total_price: invoice.calculateTotal().toPrimitive(),
|
//total_price: invoice.calculateTotal().toPrimitive(),
|
||||||
|
|
||||||
//recipient: CustomerInvoiceParticipantPresenter(customerInvoice.recipient),
|
//recipient: CustomerInvoiceParticipantAssembler(customerInvoice.recipient),
|
||||||
|
|
||||||
metadata: {
|
metadata: {
|
||||||
entity: "customer-invoice",
|
entity: "customer-invoice",
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./create-customer-invoices.assembler";
|
||||||
@ -1,19 +1,25 @@
|
|||||||
import { DuplicateEntityError, ITransactionManager } from "@erp/core/api";
|
import { DuplicateEntityError, ITransactionManager } from "@erp/core/api";
|
||||||
import { CreateCustomerInvoiceCommandDTO } from "@erp/customer-invoices/common/dto";
|
import { CreateCustomerInvoiceRequestDTO } from "@erp/customer-invoices/common/dto";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { ICustomerInvoiceService } from "../../domain";
|
import { ICustomerInvoiceService } from "../../domain";
|
||||||
import { mapDTOToCustomerInvoiceProps } from "../helpers";
|
import { mapDTOToCustomerInvoiceProps } from "../helpers";
|
||||||
import { CreateCustomerInvoicesPresenter } from "./presenter";
|
import { CreateCustomerInvoicesAssembler } from "./assembler";
|
||||||
|
|
||||||
|
type CreateCustomerInvoiceUseCaseInput = {
|
||||||
|
tenantId: string;
|
||||||
|
dto: CreateCustomerInvoiceRequestDTO;
|
||||||
|
};
|
||||||
|
|
||||||
export class CreateCustomerInvoiceUseCase {
|
export class CreateCustomerInvoiceUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly service: ICustomerInvoiceService,
|
private readonly service: ICustomerInvoiceService,
|
||||||
private readonly transactionManager: ITransactionManager,
|
private readonly transactionManager: ITransactionManager,
|
||||||
private readonly presenter: CreateCustomerInvoicesPresenter
|
private readonly assembler: CreateCustomerInvoicesAssembler
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(dto: CreateCustomerInvoiceCommandDTO) {
|
public execute(params: CreateCustomerInvoiceUseCaseInput) {
|
||||||
|
const { dto, tenantId } = params;
|
||||||
const invoicePropsOrError = mapDTOToCustomerInvoiceProps(dto);
|
const invoicePropsOrError = mapDTOToCustomerInvoiceProps(dto);
|
||||||
|
|
||||||
if (invoicePropsOrError.isFailure) {
|
if (invoicePropsOrError.isFailure) {
|
||||||
@ -21,7 +27,6 @@ export class CreateCustomerInvoiceUseCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { props, id } = invoicePropsOrError.data;
|
const { props, id } = invoicePropsOrError.data;
|
||||||
|
|
||||||
const invoiceOrError = this.service.build(props, id);
|
const invoiceOrError = this.service.build(props, id);
|
||||||
|
|
||||||
if (invoiceOrError.isFailure) {
|
if (invoiceOrError.isFailure) {
|
||||||
@ -47,7 +52,7 @@ export class CreateCustomerInvoiceUseCase {
|
|||||||
return Result.fail(result.error);
|
return Result.fail(result.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewDTO = this.presenter.toDTO(newInvoice);
|
const viewDTO = this.assembler.toDTO(newInvoice);
|
||||||
return Result.ok(viewDTO);
|
return Result.ok(viewDTO);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
return Result.fail(error as Error);
|
return Result.fail(error as Error);
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
|
export * from "./assembler";
|
||||||
export * from "./create-customer-invoice.use-case";
|
export * from "./create-customer-invoice.use-case";
|
||||||
export * from "./presenter";
|
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./create-customer-invoices.presenter";
|
|
||||||
@ -1,27 +1,32 @@
|
|||||||
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
|
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
|
||||||
import { DeleteCustomerInvoiceByIdQueryDTO } from "@erp/customer-invoices/common/dto";
|
|
||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { ICustomerInvoiceService } from "../../domain";
|
import { ICustomerInvoiceService } from "../../domain";
|
||||||
|
|
||||||
|
type DeleteCustomerInvoiceUseCaseInput = {
|
||||||
|
tenantId: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class DeleteCustomerInvoiceUseCase {
|
export class DeleteCustomerInvoiceUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly service: ICustomerInvoiceService,
|
private readonly service: ICustomerInvoiceService,
|
||||||
private readonly transactionManager: ITransactionManager
|
private readonly transactionManager: ITransactionManager
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(dto: DeleteCustomerInvoiceByIdQueryDTO) {
|
public execute(params: DeleteCustomerInvoiceUseCaseInput) {
|
||||||
const idOrError = UniqueID.create(dto.id);
|
const { id, tenantId } = params;
|
||||||
|
const idOrError = UniqueID.create(id);
|
||||||
|
|
||||||
if (idOrError.isFailure) {
|
if (idOrError.isFailure) {
|
||||||
return Result.fail(idOrError.error);
|
return Result.fail(idOrError.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = idOrError.data;
|
const invoiceId = idOrError.data;
|
||||||
|
|
||||||
return this.transactionManager.complete(async (transaction) => {
|
return this.transactionManager.complete(async (transaction) => {
|
||||||
try {
|
try {
|
||||||
const existsCheck = await this.service.existsById(id, transaction);
|
const existsCheck = await this.service.existsById(invoiceId, transaction);
|
||||||
|
|
||||||
if (existsCheck.isFailure) {
|
if (existsCheck.isFailure) {
|
||||||
return Result.fail(existsCheck.error);
|
return Result.fail(existsCheck.error);
|
||||||
@ -31,7 +36,7 @@ export class DeleteCustomerInvoiceUseCase {
|
|||||||
return Result.fail(new EntityNotFoundError("CustomerInvoice", id.toString()));
|
return Result.fail(new EntityNotFoundError("CustomerInvoice", id.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.service.deleteById(id, transaction);
|
return await this.service.deleteById(invoiceId, transaction);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
return Result.fail(error as Error);
|
return Result.fail(error as Error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { CustomerInvoiceItem } from "#/server/domain";
|
|||||||
import { IInvoicingContext } from "#/server/intrastructure";
|
import { IInvoicingContext } from "#/server/intrastructure";
|
||||||
import { Collection } from "@rdx/core";
|
import { Collection } from "@rdx/core";
|
||||||
|
|
||||||
export const customerInvoiceItemPresenter = (items: Collection<CustomerInvoiceItem>, context: IInvoicingContext) =>
|
export const customerInvoiceItemAssembler = (items: Collection<CustomerInvoiceItem>, context: IInvoicingContext) =>
|
||||||
items.totalCount > 0
|
items.totalCount > 0
|
||||||
? items.items.map((item: CustomerInvoiceItem) => ({
|
? items.items.map((item: CustomerInvoiceItem) => ({
|
||||||
description: item.description.toString(),
|
description: item.description.toString(),
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { ICustomerInvoiceParticipant } from "@/contexts/invoicing/domain";
|
import { ICustomerInvoiceParticipant } from "@/contexts/invoicing/domain";
|
||||||
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
||||||
import { ICreateCustomerInvoice_Participant_Response_DTO } from "@shared/contexts";
|
import { ICreateCustomerInvoice_Participant_Response_DTO } from "@shared/contexts";
|
||||||
import { CustomerInvoiceParticipantAddressPresenter } from "./CustomerInvoiceParticipantAddress.presenter";
|
import { CustomerInvoiceParticipantAddressAssembler } from "./CustomerInvoiceParticipantAddress.assembler";
|
||||||
|
|
||||||
export const CustomerInvoiceParticipantPresenter = async (
|
export const CustomerInvoiceParticipantAssembler = async (
|
||||||
participant: ICustomerInvoiceParticipant,
|
participant: ICustomerInvoiceParticipant,
|
||||||
context: IInvoicingContext,
|
context: IInvoicingContext,
|
||||||
): Promise<ICreateCustomerInvoice_Participant_Response_DTO | undefined> => {
|
): Promise<ICreateCustomerInvoice_Participant_Response_DTO | undefined> => {
|
||||||
@ -14,11 +14,11 @@ export const CustomerInvoiceParticipantPresenter = async (
|
|||||||
last_name: participant.lastName.toString(),
|
last_name: participant.lastName.toString(),
|
||||||
company_name: participant.companyName.toString(),
|
company_name: participant.companyName.toString(),
|
||||||
|
|
||||||
billing_address: await CustomerInvoiceParticipantAddressPresenter(
|
billing_address: await CustomerInvoiceParticipantAddressAssembler(
|
||||||
participant.billingAddress!,
|
participant.billingAddress!,
|
||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
shipping_address: await CustomerInvoiceParticipantAddressPresenter(
|
shipping_address: await CustomerInvoiceParticipantAddressAssembler(
|
||||||
participant.shippingAddress!,
|
participant.shippingAddress!,
|
||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
@ -2,7 +2,7 @@ import { CustomerInvoiceParticipantAddress } from "@/contexts/invoicing/domain";
|
|||||||
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
||||||
import { ICreateCustomerInvoice_AddressParticipant_Response_DTO } from "@shared/contexts";
|
import { ICreateCustomerInvoice_AddressParticipant_Response_DTO } from "@shared/contexts";
|
||||||
|
|
||||||
export const CustomerInvoiceParticipantAddressPresenter = async (
|
export const CustomerInvoiceParticipantAddressAssembler = async (
|
||||||
address: CustomerInvoiceParticipantAddress,
|
address: CustomerInvoiceParticipantAddress,
|
||||||
context: IInvoicingContext,
|
context: IInvoicingContext,
|
||||||
): Promise<ICreateCustomerInvoice_AddressParticipant_Response_DTO> => {
|
): Promise<ICreateCustomerInvoice_AddressParticipant_Response_DTO> => {
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import { GetCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common/dto";
|
||||||
|
import { CustomerInvoice } from "../../../domain";
|
||||||
|
|
||||||
|
export class GetCustomerInvoiceAssembler {
|
||||||
|
toDTO(customerInvoice: CustomerInvoice): GetCustomerInvoiceByIdResponseDTO {
|
||||||
|
return {
|
||||||
|
id: customerInvoice.id.toPrimitive(),
|
||||||
|
|
||||||
|
invoice_status: customerInvoice.status.toString(),
|
||||||
|
invoice_number: customerInvoice.invoiceNumber.toString(),
|
||||||
|
invoice_series: customerInvoice.invoiceSeries.toString(),
|
||||||
|
issue_date: customerInvoice.issueDate.toDateString(),
|
||||||
|
operation_date: customerInvoice.operationDate.toDateString(),
|
||||||
|
language_code: "ES",
|
||||||
|
currency: customerInvoice.currency,
|
||||||
|
|
||||||
|
metadata: {
|
||||||
|
entity: "customer-invoices",
|
||||||
|
},
|
||||||
|
|
||||||
|
//subtotal: customerInvoice.calculateSubtotal().toPrimitive(),
|
||||||
|
|
||||||
|
//total: customerInvoice.calculateTotal().toPrimitive(),
|
||||||
|
|
||||||
|
/*items:
|
||||||
|
customerInvoice.items.size() > 0
|
||||||
|
? customerInvoice.items.map((item: CustomerInvoiceItem) => ({
|
||||||
|
description: item.description.toString(),
|
||||||
|
quantity: item.quantity.toPrimitive(),
|
||||||
|
unit_measure: "",
|
||||||
|
unit_price: item.unitPrice.toPrimitive(),
|
||||||
|
subtotal: item.calculateSubtotal().toPrimitive(),
|
||||||
|
//tax_amount: item.calculateTaxAmount().toPrimitive(),
|
||||||
|
total: item.calculateTotal().toPrimitive(),
|
||||||
|
}))
|
||||||
|
: [],*/
|
||||||
|
|
||||||
|
//sender: {}, //await CustomerInvoiceParticipantAssembler(customerInvoice.senderId, context),
|
||||||
|
|
||||||
|
/*recipient: await CustomerInvoiceParticipantAssembler(customerInvoice.recipient, context),
|
||||||
|
items: customerInvoiceItemAssembler(customerInvoice.items, context),
|
||||||
|
|
||||||
|
payment_term: {
|
||||||
|
payment_type: "",
|
||||||
|
due_date: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
due_amount: {
|
||||||
|
currency: customerInvoice.currency.toString(),
|
||||||
|
precision: 2,
|
||||||
|
amount: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
custom_fields: [],
|
||||||
|
|
||||||
|
metadata: {
|
||||||
|
create_time: "",
|
||||||
|
last_updated_time: "",
|
||||||
|
delete_time: "",
|
||||||
|
},*/
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./get-invoice.assembler";
|
||||||
@ -1,19 +1,24 @@
|
|||||||
import { ITransactionManager } from "@erp/core/api";
|
import { ITransactionManager } from "@erp/core/api";
|
||||||
import { GetCustomerInvoiceByIdQueryDTO } from "@erp/customer-invoices/common/dto";
|
|
||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { ICustomerInvoiceService } from "../../domain";
|
import { ICustomerInvoiceService } from "../../domain";
|
||||||
import { GetCustomerInvoicePresenter } from "./presenter";
|
import { GetCustomerInvoiceAssembler } from "./assembler";
|
||||||
|
|
||||||
|
type GetCustomerInvoiceUseCaseInput = {
|
||||||
|
tenantId: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class GetCustomerInvoiceUseCase {
|
export class GetCustomerInvoiceUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly service: ICustomerInvoiceService,
|
private readonly service: ICustomerInvoiceService,
|
||||||
private readonly transactionManager: ITransactionManager,
|
private readonly transactionManager: ITransactionManager,
|
||||||
private readonly presenter: GetCustomerInvoicePresenter
|
private readonly assembler: GetCustomerInvoiceAssembler
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(dto: GetCustomerInvoiceByIdQueryDTO) {
|
public execute(params: GetCustomerInvoiceUseCaseInput) {
|
||||||
const idOrError = UniqueID.create(dto.id);
|
const { id, tenantId } = params;
|
||||||
|
const idOrError = UniqueID.create(id);
|
||||||
|
|
||||||
if (idOrError.isFailure) {
|
if (idOrError.isFailure) {
|
||||||
return Result.fail(idOrError.error);
|
return Result.fail(idOrError.error);
|
||||||
@ -26,7 +31,7 @@ export class GetCustomerInvoiceUseCase {
|
|||||||
return Result.fail(invoiceOrError.error);
|
return Result.fail(invoiceOrError.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDTO = this.presenter.toDTO(invoiceOrError.data);
|
const getDTO = this.assembler.toDTO(invoiceOrError.data);
|
||||||
return Result.ok(getDTO);
|
return Result.ok(getDTO);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
return Result.fail(error as Error);
|
return Result.fail(error as Error);
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
|
export * from "./assembler";
|
||||||
export * from "./get-customer-invoice.use-case";
|
export * from "./get-customer-invoice.use-case";
|
||||||
export * from "./presenter";
|
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
import { GetCustomerInvoiceByIdResultDTO } from "../../../../common/dto";
|
|
||||||
import { CustomerInvoice } from "../../../domain";
|
|
||||||
|
|
||||||
export interface GetCustomerInvoicePresenter {
|
|
||||||
toDTO: (customerInvoice: CustomerInvoice) => GetCustomerInvoiceByIdResultDTO;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getCustomerInvoicePresenter: GetCustomerInvoicePresenter = {
|
|
||||||
toDTO: (customerInvoice: CustomerInvoice): GetCustomerInvoiceByIdResultDTO => ({
|
|
||||||
id: customerInvoice.id.toPrimitive(),
|
|
||||||
|
|
||||||
invoice_status: customerInvoice.status.toString(),
|
|
||||||
invoice_number: customerInvoice.invoiceNumber.toString(),
|
|
||||||
invoice_series: customerInvoice.invoiceSeries.toString(),
|
|
||||||
issue_date: customerInvoice.issueDate.toDateString(),
|
|
||||||
operation_date: customerInvoice.operationDate.toDateString(),
|
|
||||||
language_code: "ES",
|
|
||||||
currency: customerInvoice.currency,
|
|
||||||
|
|
||||||
metadata: {
|
|
||||||
entity: "customer-invoices",
|
|
||||||
},
|
|
||||||
|
|
||||||
//subtotal: customerInvoice.calculateSubtotal().toPrimitive(),
|
|
||||||
|
|
||||||
//total: customerInvoice.calculateTotal().toPrimitive(),
|
|
||||||
|
|
||||||
/*items:
|
|
||||||
customerInvoice.items.size() > 0
|
|
||||||
? customerInvoice.items.map((item: CustomerInvoiceItem) => ({
|
|
||||||
description: item.description.toString(),
|
|
||||||
quantity: item.quantity.toPrimitive(),
|
|
||||||
unit_measure: "",
|
|
||||||
unit_price: item.unitPrice.toPrimitive(),
|
|
||||||
subtotal: item.calculateSubtotal().toPrimitive(),
|
|
||||||
//tax_amount: item.calculateTaxAmount().toPrimitive(),
|
|
||||||
total: item.calculateTotal().toPrimitive(),
|
|
||||||
}))
|
|
||||||
: [],*/
|
|
||||||
|
|
||||||
//sender: {}, //await CustomerInvoiceParticipantPresenter(customerInvoice.senderId, context),
|
|
||||||
|
|
||||||
/*recipient: await CustomerInvoiceParticipantPresenter(customerInvoice.recipient, context),
|
|
||||||
items: customerInvoiceItemPresenter(customerInvoice.items, context),
|
|
||||||
|
|
||||||
payment_term: {
|
|
||||||
payment_type: "",
|
|
||||||
due_date: "",
|
|
||||||
},
|
|
||||||
|
|
||||||
due_amount: {
|
|
||||||
currency: customerInvoice.currency.toString(),
|
|
||||||
precision: 2,
|
|
||||||
amount: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
custom_fields: [],
|
|
||||||
|
|
||||||
metadata: {
|
|
||||||
create_time: "",
|
|
||||||
last_updated_time: "",
|
|
||||||
delete_time: "",
|
|
||||||
},*/
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./get-invoice.presenter";
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { ICustomerInvoiceParticipant } from "@/contexts/invoicing/domain";
|
import { ICustomerInvoiceParticipant } from "@/contexts/invoicing/domain";
|
||||||
import { IListCustomerInvoice_Participant_Response_DTO } from "@shared/contexts";
|
import { IListCustomerInvoice_Participant_Response_DTO } from "@shared/contexts";
|
||||||
import { CustomerInvoiceParticipantAddressPresenter } from "./CustomerInvoiceParticipantAddress.presenter";
|
import { CustomerInvoiceParticipantAddressAssembler } from "./CustomerInvoiceParticipantAddress.assembler";
|
||||||
|
|
||||||
export const CustomerInvoiceParticipantPresenter = (
|
export const CustomerInvoiceParticipantAssembler = (
|
||||||
participant: ICustomerInvoiceParticipant,
|
participant: ICustomerInvoiceParticipant,
|
||||||
): IListCustomerInvoice_Participant_Response_DTO => {
|
): IListCustomerInvoice_Participant_Response_DTO => {
|
||||||
return {
|
return {
|
||||||
@ -12,10 +12,10 @@ export const CustomerInvoiceParticipantPresenter = (
|
|||||||
last_name: participant?.lastName?.toString(),
|
last_name: participant?.lastName?.toString(),
|
||||||
company_name: participant?.companyName?.toString(),
|
company_name: participant?.companyName?.toString(),
|
||||||
|
|
||||||
billing_address: CustomerInvoiceParticipantAddressPresenter(
|
billing_address: CustomerInvoiceParticipantAddressAssembler(
|
||||||
participant?.billingAddress!,
|
participant?.billingAddress!,
|
||||||
),
|
),
|
||||||
shipping_address: CustomerInvoiceParticipantAddressPresenter(
|
shipping_address: CustomerInvoiceParticipantAddressAssembler(
|
||||||
participant?.shippingAddress!,
|
participant?.shippingAddress!,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
export const CustomerInvoiceParticipantAddressPresenter = (
|
export const CustomerInvoiceParticipantAddressAssembler = (
|
||||||
address: CustomerInvoiceParticipantAddress
|
address: CustomerInvoiceParticipantAddress
|
||||||
): IListCustomerInvoice_AddressParticipant_Response_DTO => {
|
): IListCustomerInvoice_AddressParticipant_Response_DTO => {
|
||||||
return {
|
return {
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./list-invoices.assembler";
|
||||||
@ -1,20 +1,13 @@
|
|||||||
|
import { CustomerInvoice } from "@erp/customer-invoices/api/domain";
|
||||||
|
import { CustomerInvoiceListResponseDTO } from "@erp/customer-invoices/common/dto";
|
||||||
import { Criteria } from "@repo/rdx-criteria/server";
|
import { Criteria } from "@repo/rdx-criteria/server";
|
||||||
import { Collection } from "@repo/rdx-utils";
|
import { Collection } from "@repo/rdx-utils";
|
||||||
import { CustomerInvoiceListResponseDTO } from "../../../../common/dto";
|
|
||||||
import { CustomerInvoice } from "../../../domain";
|
|
||||||
|
|
||||||
export interface ListCustomerInvoicesPresenter {
|
export class ListCustomerInvoicesAssembler {
|
||||||
toDTO: (
|
toDTO(
|
||||||
customerInvoices: Collection<CustomerInvoice>,
|
customerInvoices: Collection<CustomerInvoice>,
|
||||||
criteria: Criteria
|
criteria: Criteria
|
||||||
) => CustomerInvoiceListResponseDTO;
|
): CustomerInvoiceListResponseDTO {
|
||||||
}
|
|
||||||
|
|
||||||
export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
|
|
||||||
toDTO: (
|
|
||||||
customerInvoices: Collection<CustomerInvoice>,
|
|
||||||
criteria: Criteria
|
|
||||||
): CustomerInvoiceListResponseDTO => {
|
|
||||||
const items = customerInvoices.map((invoice) => {
|
const items = customerInvoices.map((invoice) => {
|
||||||
return {
|
return {
|
||||||
id: invoice.id.toPrimitive(),
|
id: invoice.id.toPrimitive(),
|
||||||
@ -30,7 +23,7 @@ export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
|
|||||||
subtotal_price: invoice.calculateSubtotal().toPrimitive(),
|
subtotal_price: invoice.calculateSubtotal().toPrimitive(),
|
||||||
total_price: invoice.calculateTotal().toPrimitive(),
|
total_price: invoice.calculateTotal().toPrimitive(),
|
||||||
|
|
||||||
//recipient: CustomerInvoiceParticipantPresenter(customerInvoice.recipient),
|
//recipient: CustomerInvoiceParticipantAssembler(customerInvoice.recipient),
|
||||||
|
|
||||||
metadata: {
|
metadata: {
|
||||||
entity: "customer-invoice",
|
entity: "customer-invoice",
|
||||||
@ -56,5 +49,5 @@ export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
|
|||||||
//},
|
//},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
@ -1 +1,2 @@
|
|||||||
|
export * from "./assembler";
|
||||||
export * from "./list-customer-invoices.use-case";
|
export * from "./list-customer-invoices.use-case";
|
||||||
|
|||||||
@ -4,16 +4,25 @@ import { Criteria } from "@repo/rdx-criteria/server";
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { ICustomerInvoiceService } from "../../domain";
|
import { ICustomerInvoiceService } from "../../domain";
|
||||||
import { ListCustomerInvoicesPresenter } from "./presenter";
|
import { ListCustomerInvoicesAssembler } from "./assembler";
|
||||||
|
|
||||||
|
type ListCustomerInvoicesUseCaseInput = {
|
||||||
|
tenantId: string;
|
||||||
|
criteria: Criteria;
|
||||||
|
};
|
||||||
|
|
||||||
export class ListCustomerInvoicesUseCase {
|
export class ListCustomerInvoicesUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly customerInvoiceService: ICustomerInvoiceService,
|
private readonly customerInvoiceService: ICustomerInvoiceService,
|
||||||
private readonly transactionManager: ITransactionManager,
|
private readonly transactionManager: ITransactionManager,
|
||||||
private readonly presenter: ListCustomerInvoicesPresenter
|
private readonly assembler: ListCustomerInvoicesAssembler
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(criteria: Criteria): Promise<Result<CustomerInvoiceListResponseDTO, Error>> {
|
public execute(
|
||||||
|
params: ListCustomerInvoicesUseCaseInput
|
||||||
|
): Promise<Result<CustomerInvoiceListResponseDTO, Error>> {
|
||||||
|
const { criteria, tenantId } = params;
|
||||||
|
|
||||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||||
try {
|
try {
|
||||||
const result = await this.customerInvoiceService.findByCriteria(criteria, transaction);
|
const result = await this.customerInvoiceService.findByCriteria(criteria, transaction);
|
||||||
@ -22,7 +31,7 @@ export class ListCustomerInvoicesUseCase {
|
|||||||
return Result.fail(result.error);
|
return Result.fail(result.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dto: CustomerInvoiceListResponseDTO = this.presenter.toDTO(result.data, criteria);
|
const dto: CustomerInvoiceListResponseDTO = this.assembler.toDTO(result.data, criteria);
|
||||||
return Result.ok(dto);
|
return Result.ok(dto);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
return Result.fail(error as Error);
|
return Result.fail(error as Error);
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./list-invoices.presenter";
|
|
||||||
@ -1 +1,2 @@
|
|||||||
|
export * from "./assembler";
|
||||||
export * from "./update-customer-invoice.use-case";
|
export * from "./update-customer-invoice.use-case";
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
import { ExpressController, errorMapper } from "@erp/core/api";
|
|
||||||
import { CreateCustomerInvoiceCommandDTO } from "../../../common/dto";
|
|
||||||
import { CreateCustomerInvoiceUseCase } from "../../application";
|
|
||||||
|
|
||||||
export class CreateCustomerInvoiceController extends ExpressController {
|
|
||||||
public constructor(private readonly createCustomerInvoice: CreateCustomerInvoiceUseCase) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async executeImpl() {
|
|
||||||
const dto = this.req.body as CreateCustomerInvoiceCommandDTO;
|
|
||||||
/*
|
|
||||||
const user = this.req.user; // asumimos middleware authenticateJWT inyecta user
|
|
||||||
|
|
||||||
if (!user || !user.companyId) {
|
|
||||||
this.unauthorized(res, "Unauthorized: user or company not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inyectar empresa del usuario autenticado (ownership)
|
|
||||||
dto.customerCompanyId = user.companyId;
|
|
||||||
*/
|
|
||||||
|
|
||||||
const result = await this.createCustomerInvoice.execute(dto);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
console.log(result.error);
|
|
||||||
const apiError = errorMapper.toApiError(result.error);
|
|
||||||
return this.handleApiError(apiError);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.created(result.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import { SequelizeTransactionManager } from "@erp/core/api";
|
|
||||||
import { Sequelize } from "sequelize";
|
|
||||||
import { CreateCustomerInvoiceUseCase, CreateCustomerInvoicesPresenter } from "../../application/";
|
|
||||||
import { CustomerInvoiceService } from "../../domain";
|
|
||||||
import { CustomerInvoiceMapper, CustomerInvoiceRepository } from "../../infrastructure";
|
|
||||||
import { CreateCustomerInvoiceController } from "./create-customer-invoice";
|
|
||||||
|
|
||||||
export const buildCreateCustomerInvoicesController = (database: Sequelize) => {
|
|
||||||
const transactionManager = new SequelizeTransactionManager(database);
|
|
||||||
const customerInvoiceRepository = new CustomerInvoiceRepository(
|
|
||||||
database,
|
|
||||||
new CustomerInvoiceMapper()
|
|
||||||
);
|
|
||||||
const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository);
|
|
||||||
const presenter = new CreateCustomerInvoicesPresenter();
|
|
||||||
|
|
||||||
const useCase = new CreateCustomerInvoiceUseCase(
|
|
||||||
customerInvoiceService,
|
|
||||||
transactionManager,
|
|
||||||
presenter
|
|
||||||
);
|
|
||||||
|
|
||||||
return new CreateCustomerInvoiceController(useCase);
|
|
||||||
};
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import { ExpressController, errorMapper } from "@erp/core/api";
|
|
||||||
import { DeleteCustomerInvoiceUseCase } from "../../application";
|
|
||||||
|
|
||||||
export class DeleteCustomerInvoiceController extends ExpressController {
|
|
||||||
public constructor(private readonly deleteCustomerInvoice: DeleteCustomerInvoiceUseCase) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async executeImpl(): Promise<any> {
|
|
||||||
const { id } = this.req.params;
|
|
||||||
|
|
||||||
/*
|
|
||||||
const user = this.req.user; // asumimos middleware authenticateJWT inyecta user
|
|
||||||
|
|
||||||
if (!user || !user.companyId) {
|
|
||||||
this.unauthorized(res, "Unauthorized: user or company not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const result = await this.deleteCustomerInvoice.execute({ id });
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
const apiError = errorMapper.toApiError(result.error);
|
|
||||||
return this.handleApiError(apiError);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.ok(result.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import { SequelizeTransactionManager } from "@erp/core/api";
|
|
||||||
import { Sequelize } from "sequelize";
|
|
||||||
import { DeleteCustomerInvoiceUseCase } from "../../application";
|
|
||||||
import { CustomerInvoiceService } from "../../domain";
|
|
||||||
import { CustomerInvoiceMapper, CustomerInvoiceRepository } from "../../infrastructure";
|
|
||||||
import { DeleteCustomerInvoiceController } from "./delete-invoice.controller";
|
|
||||||
|
|
||||||
export const buildDeleteCustomerInvoiceController = (database: Sequelize) => {
|
|
||||||
const transactionManager = new SequelizeTransactionManager(database);
|
|
||||||
const customerInvoiceRepository = new CustomerInvoiceRepository(
|
|
||||||
database,
|
|
||||||
new CustomerInvoiceMapper()
|
|
||||||
);
|
|
||||||
const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository);
|
|
||||||
|
|
||||||
const useCase = new DeleteCustomerInvoiceUseCase(customerInvoiceService, transactionManager);
|
|
||||||
|
|
||||||
return new DeleteCustomerInvoiceController(useCase);
|
|
||||||
};
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import { ExpressController, errorMapper } from "@erp/core/api";
|
|
||||||
import { GetCustomerInvoiceUseCase } from "../../application";
|
|
||||||
|
|
||||||
export class GetCustomerInvoiceController extends ExpressController {
|
|
||||||
public constructor(private readonly getCustomerInvoice: GetCustomerInvoiceUseCase) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async executeImpl() {
|
|
||||||
const { id } = this.req.params;
|
|
||||||
|
|
||||||
/*
|
|
||||||
const user = this.req.user; // asumimos middleware authenticateJWT inyecta user
|
|
||||||
|
|
||||||
if (!user || !user.companyId) {
|
|
||||||
this.unauthorized(res, "Unauthorized: user or company not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const result = await this.getCustomerInvoice.execute({ id });
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
const apiError = errorMapper.toApiError(result.error);
|
|
||||||
return this.handleApiError(apiError);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.ok(result.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { SequelizeTransactionManager } from "@erp/core/api";
|
|
||||||
import { Sequelize } from "sequelize";
|
|
||||||
import { GetCustomerInvoiceUseCase, getCustomerInvoicePresenter } from "../../application";
|
|
||||||
import { CustomerInvoiceService } from "../../domain";
|
|
||||||
import { CustomerInvoiceRepository, customerInvoiceMapper } from "../../infrastructure";
|
|
||||||
import { GetCustomerInvoiceController } from "./get-invoice.controller";
|
|
||||||
|
|
||||||
export const buildGetCustomerInvoiceController = (database: Sequelize) => {
|
|
||||||
const transactionManager = new SequelizeTransactionManager(database);
|
|
||||||
const customerInvoiceRepository = new CustomerInvoiceRepository(database, customerInvoiceMapper);
|
|
||||||
const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository);
|
|
||||||
const presenter = getCustomerInvoicePresenter;
|
|
||||||
|
|
||||||
const useCase = new GetCustomerInvoiceUseCase(
|
|
||||||
customerInvoiceService,
|
|
||||||
transactionManager,
|
|
||||||
presenter
|
|
||||||
);
|
|
||||||
|
|
||||||
return new GetCustomerInvoiceController(useCase);
|
|
||||||
};
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export * from "./create-customer-invoice";
|
|
||||||
export * from "./delete-customer-invoice";
|
|
||||||
export * from "./get-customer-invoice";
|
|
||||||
export * from "./list-customer-invoices";
|
|
||||||
///export * from "./update-customer-invoice";
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import { SequelizeTransactionManager } from "@erp/core/api";
|
|
||||||
import { Sequelize } from "sequelize";
|
|
||||||
import { ListCustomerInvoicesUseCase } from "../../application";
|
|
||||||
import { listCustomerInvoicesPresenter } from "../../application/list-customer-invoices/presenter";
|
|
||||||
import { CustomerInvoiceService } from "../../domain";
|
|
||||||
import { CustomerInvoiceRepository, customerInvoiceMapper } from "../../infrastructure";
|
|
||||||
import { ListCustomerInvoicesController } from "./list-customer-invoices.controller";
|
|
||||||
|
|
||||||
export const buildListCustomerInvoicesController = (database: Sequelize) => {
|
|
||||||
const transactionManager = new SequelizeTransactionManager(database);
|
|
||||||
const customerInvoiceRepository = new CustomerInvoiceRepository(database, customerInvoiceMapper);
|
|
||||||
const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository);
|
|
||||||
const presenter = listCustomerInvoicesPresenter;
|
|
||||||
|
|
||||||
const useCase = new ListCustomerInvoicesUseCase(
|
|
||||||
customerInvoiceService,
|
|
||||||
transactionManager,
|
|
||||||
presenter
|
|
||||||
);
|
|
||||||
|
|
||||||
return new ListCustomerInvoicesController(useCase);
|
|
||||||
};
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import { ExpressController, errorMapper } from "@erp/core/api";
|
|
||||||
import { ListCustomerInvoicesUseCase } from "../../application";
|
|
||||||
|
|
||||||
export class ListCustomerInvoicesController extends ExpressController {
|
|
||||||
public constructor(private readonly listCustomerInvoices: ListCustomerInvoicesUseCase) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async executeImpl() {
|
|
||||||
const criteria = this.criteria;
|
|
||||||
|
|
||||||
/*
|
|
||||||
const user = this.req.user; // asumimos middleware authenticateJWT inyecta user
|
|
||||||
|
|
||||||
if (!user || !user.companyId) {
|
|
||||||
this.unauthorized(res, "Unauthorized: user or company not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inyectar empresa del usuario autenticado (ownership)
|
|
||||||
this.criteria.addFilter("companyId", "=", companyId);
|
|
||||||
*/
|
|
||||||
|
|
||||||
const result = await this.listCustomerInvoices.execute(criteria);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
const apiError = errorMapper.toApiError(result.error);
|
|
||||||
return this.handleApiError(apiError);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.ok(result.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import { IInvoicingContext } from "#/server/intrastructure";
|
|
||||||
import { CustomerInvoiceRepository } from "#/server/intrastructure/CustomerInvoice.repository";
|
|
||||||
|
|
||||||
export const updateCustomerInvoiceController = (context: IInvoicingContext) => {
|
|
||||||
const adapter = context.adapter;
|
|
||||||
const repoManager = context.repositoryManager;
|
|
||||||
|
|
||||||
repoManager.registerRepository("CustomerInvoice", (params = { transaction: null }) => {
|
|
||||||
const { transaction } = params;
|
|
||||||
|
|
||||||
return new CustomerInvoiceRepository({
|
|
||||||
transaction,
|
|
||||||
adapter,
|
|
||||||
mapper: createCustomerInvoiceMapper(context),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
repoManager.registerRepository("Participant", (params = { transaction: null }) => {
|
|
||||||
const { transaction } = params;
|
|
||||||
|
|
||||||
return new CustomerInvoiceParticipantRepository({
|
|
||||||
transaction,
|
|
||||||
adapter,
|
|
||||||
mapper: createCustomerInvoiceParticipantMapper(context),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
repoManager.registerRepository("ParticipantAddress", (params = { transaction: null }) => {
|
|
||||||
const { transaction } = params;
|
|
||||||
|
|
||||||
return new CustomerInvoiceParticipantAddressRepository({
|
|
||||||
transaction,
|
|
||||||
adapter,
|
|
||||||
mapper: createCustomerInvoiceParticipantAddressMapper(context),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
repoManager.registerRepository("Contact", (params = { transaction: null }) => {
|
|
||||||
const { transaction } = params;
|
|
||||||
|
|
||||||
return new ContactRepository({
|
|
||||||
transaction,
|
|
||||||
adapter,
|
|
||||||
mapper: createContactMapper(context),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateCustomerInvoiceUseCase = new UpdateCustomerInvoiceUseCase(context);
|
|
||||||
|
|
||||||
return new UpdateCustomerInvoiceController(
|
|
||||||
{
|
|
||||||
useCase: updateCustomerInvoiceUseCase,
|
|
||||||
presenter: updateCustomerInvoicePresenter,
|
|
||||||
},
|
|
||||||
context
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import { CustomerInvoiceItem } from "@/contexts/invoicing/domain/CustomerInvoiceItems";
|
|
||||||
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
|
||||||
import { ICollection, IMoney_Response_DTO } from "@shared/contexts";
|
|
||||||
|
|
||||||
export const customerInvoiceItemPresenter = (
|
|
||||||
items: ICollection<CustomerInvoiceItem>,
|
|
||||||
context: IInvoicingContext
|
|
||||||
) =>
|
|
||||||
items.totalCount > 0
|
|
||||||
? items.items.map((item: CustomerInvoiceItem) => ({
|
|
||||||
description: item.description.toString(),
|
|
||||||
quantity: item.quantity.toString(),
|
|
||||||
unit_measure: "",
|
|
||||||
unit_price: item.unitPrice.toPrimitive() as IMoney_Response_DTO,
|
|
||||||
subtotal: item.calculateSubtotal().toPrimitive() as IMoney_Response_DTO,
|
|
||||||
tax_amount: item.calculateTaxAmount().toPrimitive() as IMoney_Response_DTO,
|
|
||||||
total: item.calculateTotal().toPrimitive() as IMoney_Response_DTO,
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import { ICustomerInvoiceParticipant } from "@/contexts/invoicing/domain";
|
|
||||||
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
|
||||||
import { IUpdateCustomerInvoice_Participant_Response_DTO } from "@shared/contexts";
|
|
||||||
import { CustomerInvoiceParticipantAddressPresenter } from "./CustomerInvoiceParticipantAddress.presenter";
|
|
||||||
|
|
||||||
export const CustomerInvoiceParticipantPresenter = (
|
|
||||||
participant: ICustomerInvoiceParticipant,
|
|
||||||
context: IInvoicingContext,
|
|
||||||
): IUpdateCustomerInvoice_Participant_Response_DTO | undefined => {
|
|
||||||
return {
|
|
||||||
id: participant.id.toString(),
|
|
||||||
tin: participant.tin.toString(),
|
|
||||||
first_name: participant.firstName.toString(),
|
|
||||||
last_name: participant.lastName.toString(),
|
|
||||||
company_name: participant.companyName.toString(),
|
|
||||||
|
|
||||||
billing_address: CustomerInvoiceParticipantAddressPresenter(
|
|
||||||
participant.billingAddress!,
|
|
||||||
context,
|
|
||||||
),
|
|
||||||
shipping_address: CustomerInvoiceParticipantAddressPresenter(
|
|
||||||
participant.shippingAddress!,
|
|
||||||
context,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import { CustomerInvoiceParticipantAddress } from "@/contexts/invoicing/domain";
|
|
||||||
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
|
||||||
import { IUpdateCustomerInvoice_AddressParticipant_Response_DTO } from "@shared/contexts";
|
|
||||||
|
|
||||||
export const CustomerInvoiceParticipantAddressPresenter = (
|
|
||||||
address: CustomerInvoiceParticipantAddress,
|
|
||||||
context: IInvoicingContext,
|
|
||||||
): IUpdateCustomerInvoice_AddressParticipant_Response_DTO => {
|
|
||||||
return {
|
|
||||||
id: address.id.toString(),
|
|
||||||
street: address.street.toString(),
|
|
||||||
city: address.city.toString(),
|
|
||||||
postal_code: address.postalCode.toString(),
|
|
||||||
province: address.province.toString(),
|
|
||||||
country: address.country.toString(),
|
|
||||||
email: address.email.toString(),
|
|
||||||
phone: address.phone.toString(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import { CustomerInvoice } from "@/contexts/invoicing/domain";
|
|
||||||
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
|
||||||
import { IUpdateCustomerInvoice_Response_DTO } from "@shared/contexts";
|
|
||||||
import { customerInvoiceItemPresenter } from "./CustomerInvoiceItem.presenter";
|
|
||||||
import { CustomerInvoiceParticipantPresenter } from "./CustomerInvoiceParticipant.presenter";
|
|
||||||
|
|
||||||
export interface IUpdateCustomerInvoicePresenter {
|
|
||||||
map: (customerInvoice: CustomerInvoice, context: IInvoicingContext) => IUpdateCustomerInvoice_Response_DTO;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateCustomerInvoicePresenter: IUpdateCustomerInvoicePresenter = {
|
|
||||||
map: (customerInvoice: CustomerInvoice, context: IInvoicingContext): IUpdateCustomerInvoice_Response_DTO => {
|
|
||||||
return {
|
|
||||||
id: customerInvoice.id.toString(),
|
|
||||||
|
|
||||||
customerInvoice_status: customerInvoice.status.toString(),
|
|
||||||
customerInvoice_number: customerInvoice.customerInvoiceNumber.toString(),
|
|
||||||
customerInvoice_series: customerInvoice.customerInvoiceSeries.toString(),
|
|
||||||
issue_date: customerInvoice.issueDate.toISO8601(),
|
|
||||||
operation_date: customerInvoice.operationDate.toISO8601(),
|
|
||||||
language_code: customerInvoice.language.toString(),
|
|
||||||
currency: customerInvoice.currency.toString(),
|
|
||||||
subtotal: customerInvoice.calculateSubtotal().toPrimitive(),
|
|
||||||
total: customerInvoice.calculateTotal().toPrimitive(),
|
|
||||||
|
|
||||||
//sender: {}, //await CustomerInvoiceParticipantPresenter(customerInvoice.senderId, context),
|
|
||||||
|
|
||||||
recipient: CustomerInvoiceParticipantPresenter(customerInvoice.recipient, context),
|
|
||||||
|
|
||||||
items: customerInvoiceItemPresenter(customerInvoice.items, context),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./UpdateCustomerInvoice.presenter";
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
import { SequelizeRepository } from "@/core";
|
|
||||||
import { Transaction } from "sequelize";
|
|
||||||
|
|
||||||
export class ContactRepository extends SequelizeRepository<Contact> implements IContactRepository {
|
|
||||||
protected mapper: IContactMapper;
|
|
||||||
|
|
||||||
public constructor(props: {
|
|
||||||
mapper: IContactMapper;
|
|
||||||
adapter: ISequelizeAdapter;
|
|
||||||
transaction: Transaction;
|
|
||||||
}) {
|
|
||||||
const { adapter, mapper, transaction } = props;
|
|
||||||
super({ adapter, transaction });
|
|
||||||
this.mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getById2(id: UniqueID, billingAddressId: UniqueID, shippingAddressId: UniqueID) {
|
|
||||||
const Contact_Model = this.adapter.getModel("Contact_Model");
|
|
||||||
const ContactAddress_Model = this.adapter.getModel("ContactAddress_Model");
|
|
||||||
|
|
||||||
const rawContact: any = await Contact_Model.findOne({
|
|
||||||
where: { id: id.toString() },
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: ContactAddress_Model,
|
|
||||||
as: "billingAddress",
|
|
||||||
where: {
|
|
||||||
id: billingAddressId.toString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: ContactAddress_Model,
|
|
||||||
as: "shippingAddress",
|
|
||||||
where: {
|
|
||||||
id: shippingAddressId.toString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
transaction: this.transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!rawContact === true) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.mapper.mapToDomain(rawContact);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getById(id: UniqueID): Promise<Contact | null> {
|
|
||||||
const rawContact: any = await this._getById("Contact_Model", id, {
|
|
||||||
include: [{ all: true }],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!rawContact === true) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.mapper.mapToDomain(rawContact);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async exists(id: UniqueID): Promise<boolean> {
|
|
||||||
return this._exists("Customer", "id", id.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import type { ModuleParams } from "@erp/core/api";
|
||||||
|
import { SequelizeTransactionManager } from "@erp/core/api";
|
||||||
|
import {
|
||||||
|
CreateCustomerInvoiceUseCase,
|
||||||
|
CreateCustomerInvoicesAssembler,
|
||||||
|
DeleteCustomerInvoiceUseCase,
|
||||||
|
GetCustomerInvoiceAssembler,
|
||||||
|
GetCustomerInvoiceUseCase,
|
||||||
|
ListCustomerInvoicesAssembler,
|
||||||
|
ListCustomerInvoicesUseCase,
|
||||||
|
} from "../application";
|
||||||
|
import { CustomerInvoiceService } from "../domain";
|
||||||
|
import { CustomerInvoiceMapper } from "./mappers";
|
||||||
|
import { CustomerInvoiceRepository } from "./sequelize";
|
||||||
|
|
||||||
|
type InvoiceDeps = {
|
||||||
|
transactionManager: SequelizeTransactionManager;
|
||||||
|
repo: CustomerInvoiceRepository;
|
||||||
|
mapper: CustomerInvoiceMapper;
|
||||||
|
service: CustomerInvoiceService;
|
||||||
|
assemblers: {
|
||||||
|
list: ListCustomerInvoicesAssembler;
|
||||||
|
get: GetCustomerInvoiceAssembler;
|
||||||
|
create: CreateCustomerInvoicesAssembler;
|
||||||
|
//update: UpdateCustomerInvoiceAssembler;
|
||||||
|
};
|
||||||
|
build: {
|
||||||
|
list: () => ListCustomerInvoicesUseCase;
|
||||||
|
get: () => GetCustomerInvoiceUseCase;
|
||||||
|
create: () => CreateCustomerInvoiceUseCase;
|
||||||
|
//update: () => UpdateCustomerInvoiceUseCase;
|
||||||
|
delete: () => DeleteCustomerInvoiceUseCase;
|
||||||
|
};
|
||||||
|
presenters: {
|
||||||
|
// list: <T>(res: Response) => ListPresenter<T>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let _repo: CustomerInvoiceRepository | null = null;
|
||||||
|
let _mapper: CustomerInvoiceMapper | null = null;
|
||||||
|
let _service: CustomerInvoiceService | null = null;
|
||||||
|
let _assemblers: InvoiceDeps["assemblers"] | null = null;
|
||||||
|
|
||||||
|
export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
|
||||||
|
const { database } = params;
|
||||||
|
const transactionManager = new SequelizeTransactionManager(database);
|
||||||
|
|
||||||
|
if (!_mapper) _mapper = new CustomerInvoiceMapper();
|
||||||
|
if (!_repo) _repo = new CustomerInvoiceRepository(_mapper);
|
||||||
|
if (!_service) _service = new CustomerInvoiceService(_repo);
|
||||||
|
|
||||||
|
if (!_assemblers) {
|
||||||
|
_assemblers = {
|
||||||
|
list: new ListCustomerInvoicesAssembler(), // transforma domain → ListDTO
|
||||||
|
get: new GetCustomerInvoiceAssembler(), // transforma domain → DetailDTO
|
||||||
|
create: new CreateCustomerInvoicesAssembler(), // transforma domain → CreatedDTO
|
||||||
|
//update: new UpdateCustomerInvoiceAssembler(), // transforma domain -> UpdateDTO
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactionManager,
|
||||||
|
repo: _repo,
|
||||||
|
mapper: _mapper,
|
||||||
|
service: _service,
|
||||||
|
assemblers: _assemblers,
|
||||||
|
build: {
|
||||||
|
list: () =>
|
||||||
|
new ListCustomerInvoicesUseCase(_service!, transactionManager!, _assemblers!.list),
|
||||||
|
get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.get),
|
||||||
|
create: () =>
|
||||||
|
new CreateCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.create),
|
||||||
|
/*update: () =>
|
||||||
|
new UpdateCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.update),*/
|
||||||
|
delete: () => new DeleteCustomerInvoiceUseCase(_service!, transactionManager!),
|
||||||
|
},
|
||||||
|
presenters: {
|
||||||
|
//list: <T>(res: Response) => createListPresenter<T>(res),
|
||||||
|
//json: <T>(res: Response, status: number = 200) => createJsonPresenter<T>(res, status),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
ExpressController,
|
||||||
|
authGuard,
|
||||||
|
errorMapper,
|
||||||
|
forbidQueryFieldGuard,
|
||||||
|
tenantGuard,
|
||||||
|
} from "@erp/core/api";
|
||||||
|
|
||||||
|
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
|
||||||
|
import { CreateCustomerInvoiceUseCase } from "../../../application";
|
||||||
|
|
||||||
|
export class CreateCustomerInvoiceController extends ExpressController {
|
||||||
|
public constructor(
|
||||||
|
private readonly useCase: CreateCustomerInvoiceUseCase
|
||||||
|
/* private readonly presenter: any */
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
|
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl() {
|
||||||
|
const tenantId = this.getTenantId()!; // garantizado por tenantGuard
|
||||||
|
const dto = this.req.body as CreateCustomerInvoiceRequestDTO;
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Inyectar empresa del usuario autenticado (ownership)
|
||||||
|
dto.customerCompanyId = user.companyId;
|
||||||
|
*/
|
||||||
|
|
||||||
|
const result = await this.useCase.execute({ tenantId, dto });
|
||||||
|
|
||||||
|
return result.match(
|
||||||
|
(data) => this.created(data),
|
||||||
|
(err) => this.handleApiError(errorMapper.toApiError(err))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
ExpressController,
|
||||||
|
authGuard,
|
||||||
|
errorMapper,
|
||||||
|
forbidQueryFieldGuard,
|
||||||
|
tenantGuard,
|
||||||
|
} from "@erp/core/api";
|
||||||
|
import { DeleteCustomerInvoiceUseCase } from "../../../application";
|
||||||
|
|
||||||
|
export class DeleteCustomerInvoiceController extends ExpressController {
|
||||||
|
public constructor(
|
||||||
|
private readonly useCase: DeleteCustomerInvoiceUseCase
|
||||||
|
/* private readonly presenter: any */
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
|
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeImpl() {
|
||||||
|
const tenantId = this.getTenantId()!; // garantizado por tenantGuard
|
||||||
|
const { id } = this.req.params;
|
||||||
|
|
||||||
|
const result = await this.useCase.execute({ id, tenantId });
|
||||||
|
|
||||||
|
return result.match(
|
||||||
|
(data) => this.ok(data),
|
||||||
|
(error) => this.handleApiError(errorMapper.toApiError(error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
ExpressController,
|
||||||
|
authGuard,
|
||||||
|
errorMapper,
|
||||||
|
forbidQueryFieldGuard,
|
||||||
|
tenantGuard,
|
||||||
|
} from "@erp/core/api";
|
||||||
|
import { GetCustomerInvoiceUseCase } from "../../../application";
|
||||||
|
|
||||||
|
export class GetCustomerInvoiceController extends ExpressController {
|
||||||
|
public constructor(
|
||||||
|
private readonly useCase: GetCustomerInvoiceUseCase
|
||||||
|
/* private readonly presenter: any */
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
|
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl() {
|
||||||
|
const tenantId = this.getTenantId()!; // garantizado por tenantGuard
|
||||||
|
const { id } = this.req.params;
|
||||||
|
|
||||||
|
const result = await this.useCase.execute({ id, tenantId });
|
||||||
|
|
||||||
|
return result.match(
|
||||||
|
(data) => this.ok(data),
|
||||||
|
(error) => this.handleApiError(errorMapper.toApiError(error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./create-customer-invoice.controller";
|
||||||
|
export * from "./delete-customer-invoice.controller";
|
||||||
|
export * from "./get-customer-invoice.controller";
|
||||||
|
export * from "./list-customer-invoices.controller";
|
||||||
|
///export * from "./update-customer-invoice.controller";
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import {
|
||||||
|
ExpressController,
|
||||||
|
authGuard,
|
||||||
|
errorMapper,
|
||||||
|
forbidQueryFieldGuard,
|
||||||
|
tenantGuard,
|
||||||
|
} from "@erp/core/api";
|
||||||
|
import { ListCustomerInvoicesUseCase } from "../../../application";
|
||||||
|
|
||||||
|
export class ListCustomerInvoicesController extends ExpressController {
|
||||||
|
public constructor(
|
||||||
|
private readonly useCase: ListCustomerInvoicesUseCase
|
||||||
|
/* private readonly presenter: any */
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
|
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl() {
|
||||||
|
const tenantId = this.getTenantId()!; // garantizado por tenantGuard
|
||||||
|
const result = await this.useCase.execute({ criteria: this.criteria, tenantId });
|
||||||
|
|
||||||
|
return result.match(
|
||||||
|
(data) => this.ok(data),
|
||||||
|
(err) => this.handleApiError(errorMapper.toApiError(err))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { RequestWithAuth, enforceTenant } 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";
|
||||||
@ -7,12 +8,13 @@ import {
|
|||||||
DeleteCustomerInvoiceByIdRequestSchema,
|
DeleteCustomerInvoiceByIdRequestSchema,
|
||||||
GetCustomerInvoiceByIdRequestSchema,
|
GetCustomerInvoiceByIdRequestSchema,
|
||||||
} from "../../../common/dto";
|
} from "../../../common/dto";
|
||||||
|
import { getInvoiceDependencies } from "../dependencies";
|
||||||
import {
|
import {
|
||||||
buildCreateCustomerInvoicesController,
|
CreateCustomerInvoiceController,
|
||||||
buildDeleteCustomerInvoiceController,
|
DeleteCustomerInvoiceController,
|
||||||
buildGetCustomerInvoiceController,
|
GetCustomerInvoiceController,
|
||||||
buildListCustomerInvoicesController,
|
ListCustomerInvoicesController,
|
||||||
} from "../../controllers";
|
} from "./controllers";
|
||||||
|
|
||||||
export const customerInvoicesRouter = (params: ModuleParams) => {
|
export const customerInvoicesRouter = (params: ModuleParams) => {
|
||||||
const { app, database, baseRoutePath, logger } = params as {
|
const { app, database, baseRoutePath, logger } = params as {
|
||||||
@ -22,35 +24,43 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
|||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
};
|
};
|
||||||
|
|
||||||
const routes: Router = Router({ mergeParams: true });
|
const router: Router = Router({ mergeParams: true });
|
||||||
|
const deps = getInvoiceDependencies(params);
|
||||||
|
|
||||||
routes.get(
|
// 🔐 Autenticación + Tenancy para TODO el router
|
||||||
|
router.use(/* authenticateJWT(), */ enforceTenant() /*checkTabContext*/);
|
||||||
|
|
||||||
|
router.get(
|
||||||
"/",
|
"/",
|
||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
//checkUser,
|
|
||||||
validateRequest(CustomerInvoiceListRequestSchema, "params"),
|
validateRequest(CustomerInvoiceListRequestSchema, "params"),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
async (req: RequestWithAuth, res: Response, next: NextFunction) => {
|
||||||
buildListCustomerInvoicesController(database).execute(req, res, next);
|
const useCase = deps.build.list();
|
||||||
|
const controller = new ListCustomerInvoicesController(useCase /*, deps.presenters.list */);
|
||||||
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
routes.get(
|
router.get(
|
||||||
"/:id",
|
"/:id",
|
||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
//checkUser,
|
|
||||||
validateRequest(GetCustomerInvoiceByIdRequestSchema, "params"),
|
validateRequest(GetCustomerInvoiceByIdRequestSchema, "params"),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
buildGetCustomerInvoiceController(database).execute(req, res, next);
|
const useCase = deps.build.get();
|
||||||
|
const controller = new GetCustomerInvoiceController(useCase);
|
||||||
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
routes.post(
|
router.post(
|
||||||
"/",
|
"/",
|
||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
//checkUser,
|
|
||||||
validateRequest(CreateCustomerInvoiceRequestSchema),
|
validateRequest(CreateCustomerInvoiceRequestSchema),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
buildCreateCustomerInvoicesController(database).execute(req, res, next);
|
const useCase = deps.build.create();
|
||||||
|
const controller = new CreateCustomerInvoiceController(useCase);
|
||||||
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -58,21 +68,23 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
|||||||
"/:customerInvoiceId",
|
"/:customerInvoiceId",
|
||||||
validateAndParseBody(IUpdateCustomerInvoiceRequestSchema),
|
validateAndParseBody(IUpdateCustomerInvoiceRequestSchema),
|
||||||
checkTabContext,
|
checkTabContext,
|
||||||
//checkUser,
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
buildUpdateCustomerInvoiceController().execute(req, res, next);
|
buildUpdateCustomerInvoiceController().execute(req, res, next);
|
||||||
}
|
}
|
||||||
);*/
|
);*/
|
||||||
|
|
||||||
routes.delete(
|
router.delete(
|
||||||
"/:id",
|
"/:id",
|
||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
//checkUser,
|
|
||||||
validateRequest(DeleteCustomerInvoiceByIdRequestSchema, "params"),
|
validateRequest(DeleteCustomerInvoiceByIdRequestSchema, "params"),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
buildDeleteCustomerInvoiceController(database).execute(req, res, next);
|
const useCase = deps.build.delete();
|
||||||
|
const controller = new DeleteCustomerInvoiceController(useCase);
|
||||||
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use(`${baseRoutePath}/customer-invoices`, routes);
|
app.use(`${baseRoutePath}/customer-invoices`, router);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,56 @@
|
|||||||
|
import { Response } from "express";
|
||||||
|
|
||||||
|
export type ListResult<T> = {
|
||||||
|
items: T[];
|
||||||
|
total: number;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListPresenterOptions = {
|
||||||
|
includeMetaInBody?: boolean; // por defecto false (solo items en body)
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ListPresenter<T> {
|
||||||
|
constructor(
|
||||||
|
private readonly res: Response,
|
||||||
|
private readonly opts?: ListPresenterOptions
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Envía cabeceras de paginación y devuelve el cuerpo según la opción:
|
||||||
|
por defecto: items[]
|
||||||
|
includeMetaInBody: objeto con { items, total, limit, offset, page }
|
||||||
|
*/
|
||||||
|
present(result: ListResult<T>) {
|
||||||
|
const { total, limit } = result;
|
||||||
|
const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 25;
|
||||||
|
const page = Math.floor(result.offset / (safeLimit || 1)) + 1;
|
||||||
|
|
||||||
|
// Cabeceras de paginación (ya expuestas por CORS en app.ts)
|
||||||
|
this.res.setHeader("X-Total-Count", String(total));
|
||||||
|
this.res.setHeader("Pagination-Count", String(total));
|
||||||
|
this.res.setHeader("Pagination-Page", String(page));
|
||||||
|
this.res.setHeader("Pagination-Limit", String(safeLimit));
|
||||||
|
|
||||||
|
if (this.opts?.includeMetaInBody) {
|
||||||
|
return this.res.status(200).json({
|
||||||
|
items: result.items,
|
||||||
|
total,
|
||||||
|
limit: safeLimit,
|
||||||
|
offset: result.offset,
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contrato clásico: solo items en el body
|
||||||
|
return this.res.status(200).json(result.items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Factoría simple para integrarla en dependencies.ts
|
||||||
|
*/
|
||||||
|
export function createListPresenter<T>(res: Response, opts?: ListPresenterOptions) {
|
||||||
|
return new ListPresenter<T>(res, opts);
|
||||||
|
}
|
||||||
@ -107,6 +107,3 @@ export class CustomerInvoiceMapper
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const customerInvoiceMapper: CustomerInvoiceMapper = new CustomerInvoiceMapper();
|
|
||||||
export { customerInvoiceMapper };
|
|
||||||
|
|||||||
@ -45,13 +45,14 @@ export class CustomerInvoiceItemModel extends Model<
|
|||||||
declare invoice: NonAttribute<CustomerInvoiceModel>;
|
declare invoice: NonAttribute<CustomerInvoiceModel>;
|
||||||
|
|
||||||
static associate(database: Sequelize) {
|
static associate(database: Sequelize) {
|
||||||
/*const { Invoice_Model, CustomerInvoiceItem_Model } = connection.models;
|
const { Invoice_Model, CustomerInvoiceItem_Model } = connection.models;
|
||||||
|
|
||||||
CustomerInvoiceItem_Model.belongsTo(Invoice_Model, {
|
CustomerInvoiceItem_Model.belongsTo(Invoice_Model, {
|
||||||
as: "customerInvoice",
|
as: "customerInvoice",
|
||||||
|
targetKey: "id",
|
||||||
foreignKey: "invoice_id",
|
foreignKey: "invoice_id",
|
||||||
onDelete: "CASCADE",
|
onDelete: "CASCADE",
|
||||||
});*/
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,6 +160,7 @@ export default (database: Sequelize) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize: database,
|
sequelize: database,
|
||||||
|
underscored: true,
|
||||||
tableName: "customer_invoice_items",
|
tableName: "customer_invoice_items",
|
||||||
|
|
||||||
defaultScope: {},
|
defaultScope: {},
|
||||||
|
|||||||
@ -51,7 +51,21 @@ export class CustomerInvoiceModel extends Model<
|
|||||||
CustomerInvoiceModel.hasMany(CustomerInvoiceItemModel, {
|
CustomerInvoiceModel.hasMany(CustomerInvoiceItemModel, {
|
||||||
as: "items",
|
as: "items",
|
||||||
foreignKey: "invoice_id",
|
foreignKey: "invoice_id",
|
||||||
|
sourceKey: "id",
|
||||||
onDelete: "CASCADE",
|
onDelete: "CASCADE",
|
||||||
|
constraints: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static hooks(database: Sequelize) {
|
||||||
|
// Soft-cascade manual: al borrar una factura, marcamos items como borrados (paranoid).
|
||||||
|
CustomerInvoiceModel.addHook("afterDestroy", async (invoice, options) => {
|
||||||
|
if (!invoice?.id) return;
|
||||||
|
await CustomerInvoiceItemModel.destroy({
|
||||||
|
where: { invoiceId: invoice.id },
|
||||||
|
individualHooks: true,
|
||||||
|
transaction: options.transaction,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,6 +143,7 @@ export default (database: Sequelize) => {
|
|||||||
sequelize: database,
|
sequelize: database,
|
||||||
tableName: "customer_invoices",
|
tableName: "customer_invoices",
|
||||||
|
|
||||||
|
underscored: true,
|
||||||
paranoid: true, // softs deletes
|
paranoid: true, // softs deletes
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { SequelizeRepository, errorMapper } 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";
|
||||||
import { Sequelize, Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { CustomerInvoice, ICustomerInvoiceRepository } from "../../domain";
|
import { CustomerInvoice, ICustomerInvoiceRepository } from "../../domain";
|
||||||
import { ICustomerInvoiceMapper } from "../mappers/customer-invoice.mapper";
|
import { ICustomerInvoiceMapper } from "../mappers/customer-invoice.mapper";
|
||||||
import { CustomerInvoiceModel } from "./customer-invoice.model";
|
import { CustomerInvoiceModel } from "./customer-invoice.model";
|
||||||
@ -14,10 +14,8 @@ export class CustomerInvoiceRepository
|
|||||||
//private readonly model: typeof CustomerInvoiceModel;
|
//private readonly model: typeof CustomerInvoiceModel;
|
||||||
private readonly mapper!: ICustomerInvoiceMapper;
|
private readonly mapper!: ICustomerInvoiceMapper;
|
||||||
|
|
||||||
constructor(database: Sequelize, mapper: ICustomerInvoiceMapper) {
|
constructor(mapper: ICustomerInvoiceMapper) {
|
||||||
super(database);
|
super();
|
||||||
|
|
||||||
//CustomerInvoice = database.model("CustomerInvoice") as typeof CustomerInvoiceModel;
|
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Customer } from "@erp/customers/api/domain";
|
import { Customer } from "@erp/customers/api/domain";
|
||||||
import { CustomersCreationResultDTO } from "@erp/customers/common/dto";
|
import { CustomersCreationResultDTO } from "@erp/customers/common/dto";
|
||||||
|
|
||||||
export class CreateCustomersPresenter {
|
export class CreateCustomersAssembler {
|
||||||
public toDTO(invoice: Customer): CustomersCreationResultDTO {
|
public toDTO(invoice: Customer): CustomersCreationResultDTO {
|
||||||
return {
|
return {
|
||||||
id: invoice.id.toPrimitive(),
|
id: invoice.id.toPrimitive(),
|
||||||
@ -17,7 +17,7 @@ export class CreateCustomersPresenter {
|
|||||||
//subtotal_price: invoice.calculateSubtotal().toPrimitive(),
|
//subtotal_price: invoice.calculateSubtotal().toPrimitive(),
|
||||||
//total_price: invoice.calculateTotal().toPrimitive(),
|
//total_price: invoice.calculateTotal().toPrimitive(),
|
||||||
|
|
||||||
//recipient: CustomerParticipantPresenter(customer.recipient),
|
//recipient: CustomerParticipantAssembler(customer.recipient),
|
||||||
|
|
||||||
metadata: {
|
metadata: {
|
||||||
entity: "customer",
|
entity: "customer",
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./create-customers.assembler";
|
||||||
@ -4,13 +4,13 @@ import { Result } from "@repo/rdx-utils";
|
|||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { ICustomerService } from "../../domain";
|
import { ICustomerService } from "../../domain";
|
||||||
import { mapDTOToCustomerProps } from "../helpers";
|
import { mapDTOToCustomerProps } from "../helpers";
|
||||||
import { CreateCustomersPresenter } from "./presenter";
|
import { CreateCustomersAssembler } from "./assembler";
|
||||||
|
|
||||||
export class CreateCustomerUseCase {
|
export class CreateCustomerUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly service: ICustomerService,
|
private readonly service: ICustomerService,
|
||||||
private readonly transactionManager: ITransactionManager,
|
private readonly transactionManager: ITransactionManager,
|
||||||
private readonly presenter: CreateCustomersPresenter
|
private readonly assembler: CreateCustomersAssembler
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(dto: CreateCustomerCommandDTO) {
|
public execute(dto: CreateCustomerCommandDTO) {
|
||||||
@ -47,7 +47,7 @@ export class CreateCustomerUseCase {
|
|||||||
return Result.fail(result.error);
|
return Result.fail(result.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewDTO = this.presenter.toDTO(newInvoice);
|
const viewDTO = this.assembler.toDTO(newInvoice);
|
||||||
return Result.ok(viewDTO);
|
return Result.ok(viewDTO);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
return Result.fail(error as Error);
|
return Result.fail(error as Error);
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
|
export * from "./assembler";
|
||||||
export * from "./create-customer.use-case";
|
export * from "./create-customer.use-case";
|
||||||
export * from "./presenter";
|
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./create-customers.presenter";
|
|
||||||
@ -2,7 +2,7 @@ import { CustomerItem } from "#/server/domain";
|
|||||||
import { IInvoicingContext } from "#/server/intrastructure";
|
import { IInvoicingContext } from "#/server/intrastructure";
|
||||||
import { Collection } from "@rdx/core";
|
import { Collection } from "@rdx/core";
|
||||||
|
|
||||||
export const customerItemPresenter = (items: Collection<CustomerItem>, context: IInvoicingContext) =>
|
export const customerItemAssembler = (items: Collection<CustomerItem>, context: IInvoicingContext) =>
|
||||||
items.totalCount > 0
|
items.totalCount > 0
|
||||||
? items.items.map((item: CustomerItem) => ({
|
? items.items.map((item: CustomerItem) => ({
|
||||||
description: item.description.toString(),
|
description: item.description.toString(),
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { ICustomerParticipant } from "@/contexts/invoicing/domain";
|
import { ICustomerParticipant } from "@/contexts/invoicing/domain";
|
||||||
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
||||||
import { ICreateCustomer_Participant_Response_DTO } from "@shared/contexts";
|
import { ICreateCustomer_Participant_Response_DTO } from "@shared/contexts";
|
||||||
import { CustomerParticipantAddressPresenter } from "./CustomerParticipantAddress.presenter";
|
import { CustomerParticipantAddressAssembler } from "./CustomerParticipantAddress.assembler";
|
||||||
|
|
||||||
export const CustomerParticipantPresenter = async (
|
export const CustomerParticipantAssembler = async (
|
||||||
participant: ICustomerParticipant,
|
participant: ICustomerParticipant,
|
||||||
context: IInvoicingContext,
|
context: IInvoicingContext,
|
||||||
): Promise<ICreateCustomer_Participant_Response_DTO | undefined> => {
|
): Promise<ICreateCustomer_Participant_Response_DTO | undefined> => {
|
||||||
@ -14,11 +14,11 @@ export const CustomerParticipantPresenter = async (
|
|||||||
last_name: participant.lastName.toString(),
|
last_name: participant.lastName.toString(),
|
||||||
company_name: participant.companyName.toString(),
|
company_name: participant.companyName.toString(),
|
||||||
|
|
||||||
billing_address: await CustomerParticipantAddressPresenter(
|
billing_address: await CustomerParticipantAddressAssembler(
|
||||||
participant.billingAddress!,
|
participant.billingAddress!,
|
||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
shipping_address: await CustomerParticipantAddressPresenter(
|
shipping_address: await CustomerParticipantAddressAssembler(
|
||||||
participant.shippingAddress!,
|
participant.shippingAddress!,
|
||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
@ -2,7 +2,7 @@ import { CustomerParticipantAddress } from "@/contexts/invoicing/domain";
|
|||||||
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
||||||
import { ICreateCustomer_AddressParticipant_Response_DTO } from "@shared/contexts";
|
import { ICreateCustomer_AddressParticipant_Response_DTO } from "@shared/contexts";
|
||||||
|
|
||||||
export const CustomerParticipantAddressPresenter = async (
|
export const CustomerParticipantAddressAssembler = async (
|
||||||
address: CustomerParticipantAddress,
|
address: CustomerParticipantAddress,
|
||||||
context: IInvoicingContext,
|
context: IInvoicingContext,
|
||||||
): Promise<ICreateCustomer_AddressParticipant_Response_DTO> => {
|
): Promise<ICreateCustomer_AddressParticipant_Response_DTO> => {
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import { GetCustomerByIdResultDTO } from "../../../../common/dto";
|
||||||
|
import { Customer } from "../../../domain";
|
||||||
|
|
||||||
|
export class GetCustomerAssembler {
|
||||||
|
toDTO(customer: Customer): GetCustomerByIdResultDTO {
|
||||||
|
return {
|
||||||
|
id: customer.id.toPrimitive(),
|
||||||
|
|
||||||
|
invoice_status: customer.status.toString(),
|
||||||
|
invoice_number: customer.invoiceNumber.toString(),
|
||||||
|
invoice_series: customer.invoiceSeries.toString(),
|
||||||
|
issue_date: customer.issueDate.toDateString(),
|
||||||
|
operation_date: customer.operationDate.toDateString(),
|
||||||
|
language_code: "ES",
|
||||||
|
currency: customer.currency,
|
||||||
|
|
||||||
|
metadata: {
|
||||||
|
entity: "customers",
|
||||||
|
},
|
||||||
|
|
||||||
|
//subtotal: customer.calculateSubtotal().toPrimitive(),
|
||||||
|
|
||||||
|
//total: customer.calculateTotal().toPrimitive(),
|
||||||
|
|
||||||
|
/*items:
|
||||||
|
customer.items.size() > 0
|
||||||
|
? customer.items.map((item: CustomerItem) => ({
|
||||||
|
description: item.description.toString(),
|
||||||
|
quantity: item.quantity.toPrimitive(),
|
||||||
|
unit_measure: "",
|
||||||
|
unit_price: item.unitPrice.toPrimitive(),
|
||||||
|
subtotal: item.calculateSubtotal().toPrimitive(),
|
||||||
|
//tax_amount: item.calculateTaxAmount().toPrimitive(),
|
||||||
|
total: item.calculateTotal().toPrimitive(),
|
||||||
|
}))
|
||||||
|
: [],*/
|
||||||
|
|
||||||
|
//sender: {}, //await CustomerParticipantAssembler(customer.senderId, context),
|
||||||
|
|
||||||
|
/*recipient: await CustomerParticipantAssembler(customer.recipient, context),
|
||||||
|
items: customerItemAssembler(customer.items, context),
|
||||||
|
|
||||||
|
payment_term: {
|
||||||
|
payment_type: "",
|
||||||
|
due_date: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
due_amount: {
|
||||||
|
currency: customer.currency.toString(),
|
||||||
|
precision: 2,
|
||||||
|
amount: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
custom_fields: [],
|
||||||
|
|
||||||
|
metadata: {
|
||||||
|
create_time: "",
|
||||||
|
last_updated_time: "",
|
||||||
|
delete_time: "",
|
||||||
|
},*/
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./get-invoice.assembler";
|
||||||
@ -3,13 +3,13 @@ import { GetCustomerByIdQueryDTO } from "@erp/customers/common/dto";
|
|||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { ICustomerService } from "../../domain";
|
import { ICustomerService } from "../../domain";
|
||||||
import { GetCustomerPresenter } from "./presenter";
|
import { GetCustomerAssembler } from "./assembler";
|
||||||
|
|
||||||
export class GetCustomerUseCase {
|
export class GetCustomerUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly service: ICustomerService,
|
private readonly service: ICustomerService,
|
||||||
private readonly transactionManager: ITransactionManager,
|
private readonly transactionManager: ITransactionManager,
|
||||||
private readonly presenter: GetCustomerPresenter
|
private readonly assembler: GetCustomerAssembler
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(dto: GetCustomerByIdQueryDTO) {
|
public execute(dto: GetCustomerByIdQueryDTO) {
|
||||||
@ -26,7 +26,7 @@ export class GetCustomerUseCase {
|
|||||||
return Result.fail(invoiceOrError.error);
|
return Result.fail(invoiceOrError.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDTO = this.presenter.toDTO(invoiceOrError.data);
|
const getDTO = this.assembler.toDTO(invoiceOrError.data);
|
||||||
return Result.ok(getDTO);
|
return Result.ok(getDTO);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
return Result.fail(error as Error);
|
return Result.fail(error as Error);
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
|
export * from "./assembler";
|
||||||
export * from "./get-customer.use-case";
|
export * from "./get-customer.use-case";
|
||||||
export * from "./presenter";
|
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
import { GetCustomerByIdResultDTO } from "../../../../common/dto";
|
|
||||||
import { Customer } from "../../../domain";
|
|
||||||
|
|
||||||
export interface GetCustomerPresenter {
|
|
||||||
toDTO: (customer: Customer) => GetCustomerByIdResultDTO;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getCustomerPresenter: GetCustomerPresenter = {
|
|
||||||
toDTO: (customer: Customer): GetCustomerByIdResultDTO => ({
|
|
||||||
id: customer.id.toPrimitive(),
|
|
||||||
|
|
||||||
invoice_status: customer.status.toString(),
|
|
||||||
invoice_number: customer.invoiceNumber.toString(),
|
|
||||||
invoice_series: customer.invoiceSeries.toString(),
|
|
||||||
issue_date: customer.issueDate.toDateString(),
|
|
||||||
operation_date: customer.operationDate.toDateString(),
|
|
||||||
language_code: "ES",
|
|
||||||
currency: customer.currency,
|
|
||||||
|
|
||||||
metadata: {
|
|
||||||
entity: "customers",
|
|
||||||
},
|
|
||||||
|
|
||||||
//subtotal: customer.calculateSubtotal().toPrimitive(),
|
|
||||||
|
|
||||||
//total: customer.calculateTotal().toPrimitive(),
|
|
||||||
|
|
||||||
/*items:
|
|
||||||
customer.items.size() > 0
|
|
||||||
? customer.items.map((item: CustomerItem) => ({
|
|
||||||
description: item.description.toString(),
|
|
||||||
quantity: item.quantity.toPrimitive(),
|
|
||||||
unit_measure: "",
|
|
||||||
unit_price: item.unitPrice.toPrimitive(),
|
|
||||||
subtotal: item.calculateSubtotal().toPrimitive(),
|
|
||||||
//tax_amount: item.calculateTaxAmount().toPrimitive(),
|
|
||||||
total: item.calculateTotal().toPrimitive(),
|
|
||||||
}))
|
|
||||||
: [],*/
|
|
||||||
|
|
||||||
//sender: {}, //await CustomerParticipantPresenter(customer.senderId, context),
|
|
||||||
|
|
||||||
/*recipient: await CustomerParticipantPresenter(customer.recipient, context),
|
|
||||||
items: customerItemPresenter(customer.items, context),
|
|
||||||
|
|
||||||
payment_term: {
|
|
||||||
payment_type: "",
|
|
||||||
due_date: "",
|
|
||||||
},
|
|
||||||
|
|
||||||
due_amount: {
|
|
||||||
currency: customer.currency.toString(),
|
|
||||||
precision: 2,
|
|
||||||
amount: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
custom_fields: [],
|
|
||||||
|
|
||||||
metadata: {
|
|
||||||
create_time: "",
|
|
||||||
last_updated_time: "",
|
|
||||||
delete_time: "",
|
|
||||||
},*/
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./get-invoice.presenter";
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./list-invoices.assembler";
|
||||||
@ -3,12 +3,9 @@ import { Collection } from "@repo/rdx-utils";
|
|||||||
import { CustomerListResponsetDTO } from "../../../../common/dto";
|
import { CustomerListResponsetDTO } from "../../../../common/dto";
|
||||||
import { Customer } from "../../../domain";
|
import { Customer } from "../../../domain";
|
||||||
|
|
||||||
export interface ListCustomersPresenter {
|
|
||||||
toDTO: (customers: Collection<Customer>, criteria: Criteria) => CustomerListResponsetDTO;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const listCustomersPresenter: ListCustomersPresenter = {
|
export class ListCustomersAssembler {
|
||||||
toDTO: (customers: Collection<Customer>, criteria: Criteria): CustomerListResponsetDTO => {
|
toDTO(customers: Collection<Customer>, criteria: Criteria): CustomerListResponsetDTO {
|
||||||
const items = customers.map((invoice) => {
|
const items = customers.map((invoice) => {
|
||||||
return {
|
return {
|
||||||
id: invoice.id.toPrimitive(),
|
id: invoice.id.toPrimitive(),
|
||||||
@ -24,7 +21,7 @@ export const listCustomersPresenter: ListCustomersPresenter = {
|
|||||||
subtotal_price: invoice.calculateSubtotal().toPrimitive(),
|
subtotal_price: invoice.calculateSubtotal().toPrimitive(),
|
||||||
total_price: invoice.calculateTotal().toPrimitive(),
|
total_price: invoice.calculateTotal().toPrimitive(),
|
||||||
|
|
||||||
//recipient: CustomerParticipantPresenter(customer.recipient),
|
//recipient: CustomerParticipantAssembler(customer.recipient),
|
||||||
|
|
||||||
metadata: {
|
metadata: {
|
||||||
entity: "customer",
|
entity: "customer",
|
||||||
@ -1 +1,2 @@
|
|||||||
|
export * from "./assembler";
|
||||||
export * from "./list-customers.use-case";
|
export * from "./list-customers.use-case";
|
||||||
|
|||||||
@ -4,13 +4,13 @@ import { Criteria } from "@repo/rdx-criteria/server";
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { ICustomerService } from "../../domain";
|
import { ICustomerService } from "../../domain";
|
||||||
import { ListCustomersPresenter } from "./presenter";
|
import { ListCustomersAssembler } from "./assembler";
|
||||||
|
|
||||||
export class ListCustomersUseCase {
|
export class ListCustomersUseCase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly customerService: ICustomerService,
|
private readonly customerService: ICustomerService,
|
||||||
private readonly transactionManager: ITransactionManager,
|
private readonly transactionManager: ITransactionManager,
|
||||||
private readonly presenter: ListCustomersPresenter
|
private readonly assembler: ListCustomersAssembler
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(criteria: Criteria): Promise<Result<ListCustomersResultDTO, Error>> {
|
public execute(criteria: Criteria): Promise<Result<ListCustomersResultDTO, Error>> {
|
||||||
@ -23,7 +23,7 @@ export class ListCustomersUseCase {
|
|||||||
return Result.fail(result.error);
|
return Result.fail(result.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dto: ListCustomersResultDTO = this.presenter.toDTO(result.data, criteria);
|
const dto: ListCustomersResultDTO = this.assembler.toDTO(result.data, criteria);
|
||||||
return Result.ok(dto);
|
return Result.ok(dto);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
return Result.fail(error as Error);
|
return Result.fail(error as Error);
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./list-invoices.presenter";
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { SequelizeTransactionManager } from "@erp/core/api";
|
|
||||||
import { Sequelize } from "sequelize";
|
|
||||||
import { DeleteCustomerUseCase } from "../../application";
|
|
||||||
import { CustomerService } from "../../domain";
|
|
||||||
import { CustomerMapper } from "../../infrastructure";
|
|
||||||
import { DeleteCustomerController } from "./delete-invoice.controller";
|
|
||||||
|
|
||||||
export const buildDeleteCustomerController = (database: Sequelize) => {
|
|
||||||
const transactionManager = new SequelizeTransactionManager(database);
|
|
||||||
const customerRepository = new customerRepository(database, new CustomerMapper());
|
|
||||||
const customerService = new CustomerService(customerRepository);
|
|
||||||
|
|
||||||
const useCase = new DeleteCustomerUseCase(customerService, transactionManager);
|
|
||||||
|
|
||||||
return new DeleteCustomerController(useCase);
|
|
||||||
};
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export * from "./create-customer";
|
|
||||||
export * from "./delete-customer";
|
|
||||||
export * from "./get-customer";
|
|
||||||
export * from "./list-customers";
|
|
||||||
///export * from "./update-customer";
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import { ExpressController, errorMapper } from "@erp/core/api";
|
|
||||||
import { ListCustomersUseCase } from "../../application";
|
|
||||||
|
|
||||||
export class ListCustomersController extends ExpressController {
|
|
||||||
public constructor(private readonly listCustomers: ListCustomersUseCase) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async executeImpl() {
|
|
||||||
const criteria = this.criteria;
|
|
||||||
|
|
||||||
/*
|
|
||||||
const user = this.req.user; // asumimos middleware authenticateJWT inyecta user
|
|
||||||
|
|
||||||
if (!user || !user.companyId) {
|
|
||||||
this.unauthorized(res, "Unauthorized: user or company not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inyectar empresa del usuario autenticado (ownership)
|
|
||||||
this.criteria.addFilter("companyId", "=", companyId);
|
|
||||||
*/
|
|
||||||
|
|
||||||
const result = await this.listCustomers.execute(criteria);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
const apiError = errorMapper.toApiError(result.error);
|
|
||||||
return this.handleApiError(apiError);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.ok(result.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
81
modules/customers/src/api/infrastructure/dependencies.ts
Normal file
81
modules/customers/src/api/infrastructure/dependencies.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import type { ModuleParams } from "@erp/core/api";
|
||||||
|
import { SequelizeTransactionManager } from "@erp/core/api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateCustomerUseCase,
|
||||||
|
CreateCustomersAssembler,
|
||||||
|
DeleteCustomerUseCase,
|
||||||
|
GetCustomerAssembler,
|
||||||
|
GetCustomerUseCase,
|
||||||
|
ListCustomersAssembler,
|
||||||
|
ListCustomersUseCase,
|
||||||
|
} from "../application";
|
||||||
|
import { CustomerService } from "../domain";
|
||||||
|
import { CustomerMapper } from "./mappers";
|
||||||
|
import { CustomerRepository } from "./sequelize";
|
||||||
|
|
||||||
|
type CustomerDeps = {
|
||||||
|
transactionManager: SequelizeTransactionManager;
|
||||||
|
repo: CustomerRepository;
|
||||||
|
mapper: CustomerMapper;
|
||||||
|
service: CustomerService;
|
||||||
|
assemblers: {
|
||||||
|
list: ListCustomersAssembler;
|
||||||
|
get: GetCustomerAssembler;
|
||||||
|
create: CreateCustomersAssembler;
|
||||||
|
//update: UpdateCustomerAssembler;
|
||||||
|
};
|
||||||
|
build: {
|
||||||
|
list: () => ListCustomersUseCase;
|
||||||
|
get: () => GetCustomerUseCase;
|
||||||
|
create: () => CreateCustomerUseCase;
|
||||||
|
//update: () => UpdateCustomerUseCase;
|
||||||
|
delete: () => DeleteCustomerUseCase;
|
||||||
|
};
|
||||||
|
presenters: {
|
||||||
|
// list: <T>(res: Response) => ListPresenter<T>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let _repo: CustomerRepository | null = null;
|
||||||
|
let _mapper: CustomerMapper | null = null;
|
||||||
|
let _service: CustomerService | null = null;
|
||||||
|
let _assemblers: CustomerDeps["assemblers"] | null = null;
|
||||||
|
|
||||||
|
export function getCustomerDependencies(params: ModuleParams): CustomerDeps {
|
||||||
|
const { database } = params;
|
||||||
|
const transactionManager = new SequelizeTransactionManager(database);
|
||||||
|
|
||||||
|
if (!_mapper) _mapper = new CustomerMapper();
|
||||||
|
if (!_repo) _repo = new CustomerRepository(_mapper);
|
||||||
|
if (!_service) _service = new CustomerService(_repo);
|
||||||
|
|
||||||
|
if (!_assemblers) {
|
||||||
|
_assemblers = {
|
||||||
|
list: new ListCustomersAssembler(), // transforma domain → ListDTO
|
||||||
|
get: new GetCustomerAssembler(), // transforma domain → DetailDTO
|
||||||
|
create: new CreateCustomersAssembler(), // transforma domain → CreatedDTO
|
||||||
|
//update: new UpdateCustomerAssembler(), // transforma domain -> UpdateDTO
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
transactionManager,
|
||||||
|
repo: _repo,
|
||||||
|
mapper: _mapper,
|
||||||
|
service: _service,
|
||||||
|
assemblers: _assemblers,
|
||||||
|
build: {
|
||||||
|
list: () => new ListCustomersUseCase(_service!, transactionManager!, _assemblers!.list),
|
||||||
|
get: () => new GetCustomerUseCase(_service!, transactionManager!, _assemblers!.get),
|
||||||
|
create: () => new CreateCustomerUseCase(_service!, transactionManager!, _assemblers!.create),
|
||||||
|
/*update: () =>
|
||||||
|
new UpdateCustomerUseCase(_service!, transactionManager!, _assemblers!.update),*/
|
||||||
|
delete: () => new DeleteCustomerUseCase(_service!, transactionManager!),
|
||||||
|
},
|
||||||
|
presenters: {
|
||||||
|
//list: <T>(res: Response) => createListPresenter<T>(res),
|
||||||
|
//json: <T>(res: Response, status: number = 200) => createJsonPresenter<T>(res, status),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import { ExpressController, errorMapper } from "@erp/core/api";
|
import { ExpressController, errorMapper } from "@erp/core/api";
|
||||||
import { CreateCustomerCommandDTO } from "../../../common/dto";
|
import { CreateCustomerCommandDTO } from "../../../../../common/dto";
|
||||||
import { CreateCustomerUseCase } from "../../application";
|
import { CreateCustomerUseCase } from "../../../../application";
|
||||||
|
|
||||||
export class CreateCustomerController extends ExpressController {
|
export class CreateCustomerController extends ExpressController {
|
||||||
public constructor(private readonly createCustomer: CreateCustomerUseCase) {
|
public constructor(private readonly createCustomer: CreateCustomerUseCase) {
|
||||||
super();
|
super();
|
||||||
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
|
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async executeImpl() {
|
protected async executeImpl() {
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { SequelizeTransactionManager } from "@erp/core/api";
|
import { SequelizeTransactionManager } from "@erp/core/api";
|
||||||
import { Sequelize } from "sequelize";
|
import { Sequelize } from "sequelize";
|
||||||
import { CreateCustomerUseCase, CreateCustomersPresenter } from "../../application/";
|
import { CustomerMapper } from "../../..";
|
||||||
import { CustomerService } from "../../domain";
|
import { CreateCustomerUseCase, CreateCustomersPresenter } from "../../../../application";
|
||||||
import { CustomerMapper } from "../../infrastructure";
|
import { CustomerService } from "../../../../domain";
|
||||||
import { CreateCustomerController } from "./create-customer";
|
import { CreateCustomerController } from "./create-customer.controller";
|
||||||
|
|
||||||
export const buildCreateCustomersController = (database: Sequelize) => {
|
export const buildCreateCustomersController = (database: Sequelize) => {
|
||||||
const transactionManager = new SequelizeTransactionManager(database);
|
const transactionManager = new SequelizeTransactionManager(database);
|
||||||
@ -1,9 +1,11 @@
|
|||||||
import { ExpressController, errorMapper } from "@erp/core/api";
|
import { ExpressController, errorMapper } from "@erp/core/api";
|
||||||
import { DeleteCustomerUseCase } from "../../application";
|
import { DeleteCustomerUseCase } from "../../../../application";
|
||||||
|
|
||||||
export class DeleteCustomerController extends ExpressController {
|
export class DeleteCustomerController extends ExpressController {
|
||||||
public constructor(private readonly deleteCustomer: DeleteCustomerUseCase) {
|
public constructor(private readonly deleteCustomer: DeleteCustomerUseCase) {
|
||||||
super();
|
super();
|
||||||
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
|
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeImpl(): Promise<any> {
|
async executeImpl(): Promise<any> {
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
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,9 +1,11 @@
|
|||||||
import { ExpressController, errorMapper } from "@erp/core/api";
|
import { ExpressController, errorMapper } from "@erp/core/api";
|
||||||
import { GetCustomerUseCase } from "../../application";
|
import { GetCustomerUseCase } from "../../../application";
|
||||||
|
|
||||||
export class GetCustomerController extends ExpressController {
|
export class GetCustomerController extends ExpressController {
|
||||||
public constructor(private readonly getCustomer: GetCustomerUseCase) {
|
public constructor(private readonly getCustomer: GetCustomerUseCase) {
|
||||||
super();
|
super();
|
||||||
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
|
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async executeImpl() {
|
protected async executeImpl() {
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { SequelizeTransactionManager } from "@erp/core/api";
|
import { SequelizeTransactionManager } from "@erp/core/api";
|
||||||
import { Sequelize } from "sequelize";
|
import { Sequelize } from "sequelize";
|
||||||
import { GetCustomerUseCase, getCustomerPresenter } from "../../application";
|
import { CustomerRepository, customerMapper } from "../../..";
|
||||||
import { CustomerService } from "../../domain";
|
import { GetCustomerUseCase, getCustomerPresenter } from "../../../../application";
|
||||||
import { CustomerRepository, customerMapper } from "../../infrastructure";
|
import { CustomerService } from "../../../../domain";
|
||||||
import { GetCustomerController } from "./get-invoice.controller";
|
import { GetCustomerController } from "../get-customer.controller";
|
||||||
|
|
||||||
export const buildGetCustomerController = (database: Sequelize) => {
|
export const buildGetCustomerController = (database: Sequelize) => {
|
||||||
const transactionManager = new SequelizeTransactionManager(database);
|
const transactionManager = new SequelizeTransactionManager(database);
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./create-customer.controller";
|
||||||
|
export * from "./delete-customer.controller";
|
||||||
|
export * from "./get-customer.controller";
|
||||||
|
export * from "./list-customers.controller";
|
||||||
|
///export * from "./update-customer.controller";
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
ExpressController,
|
||||||
|
authGuard,
|
||||||
|
errorMapper,
|
||||||
|
forbidQueryFieldGuard,
|
||||||
|
tenantGuard,
|
||||||
|
} from "@erp/core/api";
|
||||||
|
import { ListCustomersUseCase } from "../../../../application";
|
||||||
|
|
||||||
|
export class ListCustomersController extends ExpressController {
|
||||||
|
public constructor(private readonly listCustomers: ListCustomersUseCase) {
|
||||||
|
super();
|
||||||
|
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||||
|
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl() {
|
||||||
|
const tenantId = this.getTenantId()!; // garantizado por tenantGuard
|
||||||
|
const result = await this.listCustomers.execute({ criteria: this.criteria, tenantId });
|
||||||
|
|
||||||
|
return result.match(
|
||||||
|
(data) => this.ok(data),
|
||||||
|
(err) => this.handleApiError(errorMapper.toApiError(err))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { SequelizeTransactionManager } from "@erp/core/api";
|
import { SequelizeTransactionManager } from "@erp/core/api";
|
||||||
import { Sequelize } from "sequelize";
|
import { Sequelize } from "sequelize";
|
||||||
import { ListCustomersUseCase } from "../../application";
|
import { CustomerRepository, customerMapper } from "../../..";
|
||||||
import { listCustomersPresenter } from "../../application/list-customers/presenter";
|
import { ListCustomersUseCase } from "../../../../application";
|
||||||
import { CustomerService } from "../../domain";
|
import { listCustomersPresenter } from "../../../../application/list-customers/assembler";
|
||||||
import { CustomerRepository, customerMapper } from "../../infrastructure";
|
import { CustomerService } from "../../../../domain";
|
||||||
import { ListCustomersController } from "./list-customers.controller";
|
import { ListCustomersController } from "./list-customers.controller";
|
||||||
|
|
||||||
export const buildListCustomersController = (database: Sequelize) => {
|
export const buildListCustomersController = (database: Sequelize) => {
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user