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
|
||||
HOST=0.0.0.0
|
||||
PORT=3002
|
||||
|
||||
# URL pública del frontend (CORS).
|
||||
# En dev se puede permitir todo con '*'
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
|
||||
|
||||
# ───────────────────────────────
|
||||
# Base de datos (Sequelize / MySQL-MariaDB)
|
||||
# ───────────────────────────────
|
||||
|
||||
# Base de datos (opción 1: URL)
|
||||
# DATABASE_URL=postgres://user:pass@localhost:5432/dbname
|
||||
|
||||
@ -14,12 +25,39 @@ DB_NAME=uecko_erp
|
||||
DB_USER=rodax
|
||||
DB_PASSWORD=rodax
|
||||
|
||||
# Log de Sequelize (true|false)
|
||||
DB_LOGGING=false
|
||||
DB_SYNC_MODE=alter
|
||||
|
||||
APP_TIMEZONE=Europe/Madrid
|
||||
TRUST_PROXY=0
|
||||
# Si necesitas SSL/TLS en MySQL (por defecto no)
|
||||
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_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 initializedModules = new Set<string>();
|
||||
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.
|
||||
Lanza error si el nombre ya existe.
|
||||
Registra un módulo del servidor en el registry.
|
||||
Lanza error si el nombre ya existe.
|
||||
*/
|
||||
export function registerModule(pkg: IModuleServer) {
|
||||
if (!pkg?.name) {
|
||||
@ -23,20 +31,23 @@ export function registerModule(pkg: IModuleServer) {
|
||||
}
|
||||
|
||||
/**
|
||||
Inicializa todos los módulos registrados (resolviendo dependencias),
|
||||
y al final inicializa los modelos (Sequelize) en bloque.
|
||||
Inicializa todos los módulos registrados (resolviendo dependencias),
|
||||
luego inicializa los modelos (Sequelize) en bloque y, por último, ejecuta warmups opcionales.
|
||||
*/
|
||||
export async function initModules(params: ModuleParams) {
|
||||
for (const name of registeredModules.keys()) {
|
||||
await loadModule(name, params, []); // secuencial para logs deterministas
|
||||
}
|
||||
|
||||
await withPhase("global", "initModels", async () => {
|
||||
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[]) {
|
||||
if (initializedModules.has(name)) return;
|
||||
@ -88,30 +99,95 @@ async function loadModule(name: string, params: ModuleParams, stack: string[]) {
|
||||
}
|
||||
|
||||
initializedModules.add(name);
|
||||
initializationOrder.push(name); // recordamos el orden para warmup
|
||||
|
||||
visiting.delete(name);
|
||||
stack.pop();
|
||||
|
||||
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>(
|
||||
moduleName: string,
|
||||
phase: "init" | "registerDependencies" | "registerModels" | "registerServices" | "initModels",
|
||||
phase:
|
||||
| "init"
|
||||
| "registerDependencies"
|
||||
| "registerModels"
|
||||
| "registerServices"
|
||||
| "initModels"
|
||||
| "warmup",
|
||||
fn: () => Promise<T> | T
|
||||
): Promise<T> {
|
||||
const startedAt = Date.now();
|
||||
logger.info(`▶️ phase=start module=${moduleName} ${phase}`, {
|
||||
label: "moduleRegistry",
|
||||
module: moduleName,
|
||||
phase,
|
||||
});
|
||||
|
||||
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) {
|
||||
// Log enriquecido con contexto
|
||||
const duration = Date.now() - startedAt;
|
||||
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",
|
||||
module: moduleName,
|
||||
phase,
|
||||
durationMs: duration,
|
||||
stack: error?.stack,
|
||||
}
|
||||
);
|
||||
@ -123,3 +199,18 @@ async function withPhase<T>(
|
||||
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",
|
||||
"./client": "./src/web/index.ts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@erp/core": "workspace:*",
|
||||
"dinero.js": "^1.9.1",
|
||||
|
||||
"sequelize": "^6.37.5",
|
||||
"zod": "^3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/react": "^19.1.2",
|
||||
@ -18,6 +25,7 @@
|
||||
"@repo/rdx-ui": "workspace:*",
|
||||
"@repo/shadcn-ui": "workspace:*",
|
||||
"@tanstack/react-query": "^5.74.11",
|
||||
"express": "^4.18.2",
|
||||
"i18next": "^25.1.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { IModuleServer, ModuleParams } from "@erp/core/api";
|
||||
export * from "./lib";
|
||||
|
||||
export const authAPIModule: IModuleServer = {
|
||||
/* export const authAPIModule: IModuleServer = {
|
||||
name: "auth",
|
||||
version: "1.0.0",
|
||||
dependencies: [],
|
||||
@ -16,10 +16,10 @@ export const authAPIModule: IModuleServer = {
|
||||
return {
|
||||
//models,
|
||||
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,
|
||||
} 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 {
|
||||
protected req!: Request; //| AuthenticatedRequest | TabContextRequest;
|
||||
protected req!: Request;
|
||||
protected res!: Response;
|
||||
protected next!: NextFunction;
|
||||
protected criteria!: Criteria;
|
||||
|
||||
// 🔹 Guards configurables por controlador
|
||||
private guards: GuardFn[] = [];
|
||||
|
||||
static errorResponse(apiError: ApiError, res: Response) {
|
||||
return res.status(apiError.status).json(apiError);
|
||||
}
|
||||
@ -38,100 +59,111 @@ export abstract class ExpressController {
|
||||
return this.res.sendStatus(httpStatus.NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Respuesta para errores de cliente (400 Bad Request)
|
||||
*/
|
||||
protected clientError(message: string, errors?: any[] | any) {
|
||||
return ExpressController.errorResponse(
|
||||
new ValidationApiError(message, Array.isArray(errors) ? errors : [errors]),
|
||||
this.res
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Respuesta para errores de autenticación (401 Unauthorized)
|
||||
*/
|
||||
protected unauthorizedError(message?: string) {
|
||||
return ExpressController.errorResponse(
|
||||
new UnauthorizedApiError(message ?? "Unauthorized"),
|
||||
this.res
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Respuesta para errores de autorización (403 Forbidden)
|
||||
*/
|
||||
protected forbiddenError(message?: string) {
|
||||
return ExpressController.errorResponse(
|
||||
new ForbiddenApiError(message ?? "You do not have permission to perform this action."),
|
||||
this.res
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Respuesta para recursos no encontrados (404 Not Found)
|
||||
*/
|
||||
protected notFoundError(message: string) {
|
||||
return ExpressController.errorResponse(new NotFoundApiError(message), this.res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Respuesta para conflictos (409 Conflict)
|
||||
*/
|
||||
protected conflictError(message: string, errors?: any[]) {
|
||||
protected conflictError(message: string, _errors?: any[]) {
|
||||
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[]) {
|
||||
return ExpressController.errorResponse(new ValidationApiError(message, errors), this.res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Respuesta para errores de servidor no disponible (503 Service Unavailable)
|
||||
*/
|
||||
protected unavailableError(message?: string) {
|
||||
return ExpressController.errorResponse(
|
||||
new UnavailableApiError(message ?? "Service temporarily unavailable."),
|
||||
this.res
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Respuesta para errores internos del servidor (500 Internal Server Error)
|
||||
*/
|
||||
protected internalServerError(message?: string) {
|
||||
return ExpressController.errorResponse(
|
||||
new InternalApiError(message ?? "Internal Server Error"),
|
||||
this.res
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Respuesta para cualquier error de la API
|
||||
*/
|
||||
protected handleApiError(apiError: ApiError) {
|
||||
return ExpressController.errorResponse(apiError, this.res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Método principal que se invoca desde el router de Express.
|
||||
* Maneja la conversión de la URL a criterios y llama a executeImpl.
|
||||
*/
|
||||
public execute(req: Request, res: Response, next: NextFunction): void {
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Guards API
|
||||
protected useGuards(...guards: GuardFn[]): this {
|
||||
this.guards.push(...guards);
|
||||
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.res = res;
|
||||
this.next = next;
|
||||
|
||||
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.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) {
|
||||
const err = error as Error;
|
||||
if (err instanceof ApiError) {
|
||||
ExpressController.errorResponse(err, this.res);
|
||||
ExpressController.errorResponse(err as ApiError, this.res);
|
||||
} else {
|
||||
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-guards";
|
||||
export * from "./middlewares";
|
||||
|
||||
@ -1,15 +1,9 @@
|
||||
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
||||
import { IAggregateRootRepository, UniqueID } from "@repo/rdx-ddd";
|
||||
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> {
|
||||
protected readonly _database!: Sequelize;
|
||||
|
||||
constructor(database: Sequelize) {
|
||||
this._database = database;
|
||||
}
|
||||
|
||||
protected convertCriteria(criteria: Criteria): FindOptions {
|
||||
return new CriteriaToSequelizeConverter().convert(criteria);
|
||||
}
|
||||
|
||||
@ -2,10 +2,12 @@ import { Sequelize, Transaction } from "sequelize";
|
||||
import { TransactionManager } from "../database";
|
||||
|
||||
export class SequelizeTransactionManager extends TransactionManager {
|
||||
protected _database: any | null = null;
|
||||
protected _database: Sequelize | null = null;
|
||||
|
||||
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> {
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
"./globals.css": "./src/web/globals.css"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@erp/core": "workspace:*",
|
||||
"dinero.js": "^1.9.1",
|
||||
"express": "^4.18.2",
|
||||
"sequelize": "^6.37.5",
|
||||
@ -32,6 +31,8 @@
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@erp/customers": "workspace:*",
|
||||
"@erp/core": "workspace:*",
|
||||
"@erp/auth": "workspace:*",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@repo/rdx-criteria": "workspace:*",
|
||||
"@repo/rdx-ddd": "workspace:*",
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
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 {
|
||||
public toDTO(invoice: CustomerInvoice): CustomerInvoicesCreationResultDTO {
|
||||
export class CreateCustomerInvoicesAssembler {
|
||||
public toDTO(invoice: CustomerInvoice): CustomerInvoicesCreationResponseDTO {
|
||||
return {
|
||||
id: invoice.id.toPrimitive(),
|
||||
|
||||
@ -17,7 +17,7 @@ export class CreateCustomerInvoicesPresenter {
|
||||
//subtotal_price: invoice.calculateSubtotal().toPrimitive(),
|
||||
//total_price: invoice.calculateTotal().toPrimitive(),
|
||||
|
||||
//recipient: CustomerInvoiceParticipantPresenter(customerInvoice.recipient),
|
||||
//recipient: CustomerInvoiceParticipantAssembler(customerInvoice.recipient),
|
||||
|
||||
metadata: {
|
||||
entity: "customer-invoice",
|
||||
@ -0,0 +1 @@
|
||||
export * from "./create-customer-invoices.assembler";
|
||||
@ -1,19 +1,25 @@
|
||||
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 { Transaction } from "sequelize";
|
||||
import { ICustomerInvoiceService } from "../../domain";
|
||||
import { mapDTOToCustomerInvoiceProps } from "../helpers";
|
||||
import { CreateCustomerInvoicesPresenter } from "./presenter";
|
||||
import { CreateCustomerInvoicesAssembler } from "./assembler";
|
||||
|
||||
type CreateCustomerInvoiceUseCaseInput = {
|
||||
tenantId: string;
|
||||
dto: CreateCustomerInvoiceRequestDTO;
|
||||
};
|
||||
|
||||
export class CreateCustomerInvoiceUseCase {
|
||||
constructor(
|
||||
private readonly service: ICustomerInvoiceService,
|
||||
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);
|
||||
|
||||
if (invoicePropsOrError.isFailure) {
|
||||
@ -21,7 +27,6 @@ export class CreateCustomerInvoiceUseCase {
|
||||
}
|
||||
|
||||
const { props, id } = invoicePropsOrError.data;
|
||||
|
||||
const invoiceOrError = this.service.build(props, id);
|
||||
|
||||
if (invoiceOrError.isFailure) {
|
||||
@ -47,7 +52,7 @@ export class CreateCustomerInvoiceUseCase {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
const viewDTO = this.presenter.toDTO(newInvoice);
|
||||
const viewDTO = this.assembler.toDTO(newInvoice);
|
||||
return Result.ok(viewDTO);
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./assembler";
|
||||
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 { DeleteCustomerInvoiceByIdQueryDTO } from "@erp/customer-invoices/common/dto";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { ICustomerInvoiceService } from "../../domain";
|
||||
|
||||
type DeleteCustomerInvoiceUseCaseInput = {
|
||||
tenantId: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export class DeleteCustomerInvoiceUseCase {
|
||||
constructor(
|
||||
private readonly service: ICustomerInvoiceService,
|
||||
private readonly transactionManager: ITransactionManager
|
||||
) {}
|
||||
|
||||
public execute(dto: DeleteCustomerInvoiceByIdQueryDTO) {
|
||||
const idOrError = UniqueID.create(dto.id);
|
||||
public execute(params: DeleteCustomerInvoiceUseCaseInput) {
|
||||
const { id, tenantId } = params;
|
||||
const idOrError = UniqueID.create(id);
|
||||
|
||||
if (idOrError.isFailure) {
|
||||
return Result.fail(idOrError.error);
|
||||
}
|
||||
|
||||
const id = idOrError.data;
|
||||
const invoiceId = idOrError.data;
|
||||
|
||||
return this.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
const existsCheck = await this.service.existsById(id, transaction);
|
||||
const existsCheck = await this.service.existsById(invoiceId, transaction);
|
||||
|
||||
if (existsCheck.isFailure) {
|
||||
return Result.fail(existsCheck.error);
|
||||
@ -31,7 +36,7 @@ export class DeleteCustomerInvoiceUseCase {
|
||||
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) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { CustomerInvoiceItem } from "#/server/domain";
|
||||
import { IInvoicingContext } from "#/server/intrastructure";
|
||||
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.items.map((item: CustomerInvoiceItem) => ({
|
||||
description: item.description.toString(),
|
||||
@ -1,9 +1,9 @@
|
||||
import { ICustomerInvoiceParticipant } from "@/contexts/invoicing/domain";
|
||||
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
||||
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,
|
||||
context: IInvoicingContext,
|
||||
): Promise<ICreateCustomerInvoice_Participant_Response_DTO | undefined> => {
|
||||
@ -14,11 +14,11 @@ export const CustomerInvoiceParticipantPresenter = async (
|
||||
last_name: participant.lastName.toString(),
|
||||
company_name: participant.companyName.toString(),
|
||||
|
||||
billing_address: await CustomerInvoiceParticipantAddressPresenter(
|
||||
billing_address: await CustomerInvoiceParticipantAddressAssembler(
|
||||
participant.billingAddress!,
|
||||
context,
|
||||
),
|
||||
shipping_address: await CustomerInvoiceParticipantAddressPresenter(
|
||||
shipping_address: await CustomerInvoiceParticipantAddressAssembler(
|
||||
participant.shippingAddress!,
|
||||
context,
|
||||
),
|
||||
@ -2,7 +2,7 @@ import { CustomerInvoiceParticipantAddress } from "@/contexts/invoicing/domain";
|
||||
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
||||
import { ICreateCustomerInvoice_AddressParticipant_Response_DTO } from "@shared/contexts";
|
||||
|
||||
export const CustomerInvoiceParticipantAddressPresenter = async (
|
||||
export const CustomerInvoiceParticipantAddressAssembler = async (
|
||||
address: CustomerInvoiceParticipantAddress,
|
||||
context: IInvoicingContext,
|
||||
): 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 { GetCustomerInvoiceByIdQueryDTO } from "@erp/customer-invoices/common/dto";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { ICustomerInvoiceService } from "../../domain";
|
||||
import { GetCustomerInvoicePresenter } from "./presenter";
|
||||
import { GetCustomerInvoiceAssembler } from "./assembler";
|
||||
|
||||
type GetCustomerInvoiceUseCaseInput = {
|
||||
tenantId: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export class GetCustomerInvoiceUseCase {
|
||||
constructor(
|
||||
private readonly service: ICustomerInvoiceService,
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
private readonly presenter: GetCustomerInvoicePresenter
|
||||
private readonly assembler: GetCustomerInvoiceAssembler
|
||||
) {}
|
||||
|
||||
public execute(dto: GetCustomerInvoiceByIdQueryDTO) {
|
||||
const idOrError = UniqueID.create(dto.id);
|
||||
public execute(params: GetCustomerInvoiceUseCaseInput) {
|
||||
const { id, tenantId } = params;
|
||||
const idOrError = UniqueID.create(id);
|
||||
|
||||
if (idOrError.isFailure) {
|
||||
return Result.fail(idOrError.error);
|
||||
@ -26,7 +31,7 @@ export class GetCustomerInvoiceUseCase {
|
||||
return Result.fail(invoiceOrError.error);
|
||||
}
|
||||
|
||||
const getDTO = this.presenter.toDTO(invoiceOrError.data);
|
||||
const getDTO = this.assembler.toDTO(invoiceOrError.data);
|
||||
return Result.ok(getDTO);
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./assembler";
|
||||
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 { 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,
|
||||
): IListCustomerInvoice_Participant_Response_DTO => {
|
||||
return {
|
||||
@ -12,10 +12,10 @@ export const CustomerInvoiceParticipantPresenter = (
|
||||
last_name: participant?.lastName?.toString(),
|
||||
company_name: participant?.companyName?.toString(),
|
||||
|
||||
billing_address: CustomerInvoiceParticipantAddressPresenter(
|
||||
billing_address: CustomerInvoiceParticipantAddressAssembler(
|
||||
participant?.billingAddress!,
|
||||
),
|
||||
shipping_address: CustomerInvoiceParticipantAddressPresenter(
|
||||
shipping_address: CustomerInvoiceParticipantAddressAssembler(
|
||||
participant?.shippingAddress!,
|
||||
),
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
export const CustomerInvoiceParticipantAddressPresenter = (
|
||||
export const CustomerInvoiceParticipantAddressAssembler = (
|
||||
address: CustomerInvoiceParticipantAddress
|
||||
): IListCustomerInvoice_AddressParticipant_Response_DTO => {
|
||||
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 { Collection } from "@repo/rdx-utils";
|
||||
import { CustomerInvoiceListResponseDTO } from "../../../../common/dto";
|
||||
import { CustomerInvoice } from "../../../domain";
|
||||
|
||||
export interface ListCustomerInvoicesPresenter {
|
||||
toDTO: (
|
||||
export class ListCustomerInvoicesAssembler {
|
||||
toDTO(
|
||||
customerInvoices: Collection<CustomerInvoice>,
|
||||
criteria: Criteria
|
||||
) => CustomerInvoiceListResponseDTO;
|
||||
}
|
||||
|
||||
export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
|
||||
toDTO: (
|
||||
customerInvoices: Collection<CustomerInvoice>,
|
||||
criteria: Criteria
|
||||
): CustomerInvoiceListResponseDTO => {
|
||||
): CustomerInvoiceListResponseDTO {
|
||||
const items = customerInvoices.map((invoice) => {
|
||||
return {
|
||||
id: invoice.id.toPrimitive(),
|
||||
@ -30,7 +23,7 @@ export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
|
||||
subtotal_price: invoice.calculateSubtotal().toPrimitive(),
|
||||
total_price: invoice.calculateTotal().toPrimitive(),
|
||||
|
||||
//recipient: CustomerInvoiceParticipantPresenter(customerInvoice.recipient),
|
||||
//recipient: CustomerInvoiceParticipantAssembler(customerInvoice.recipient),
|
||||
|
||||
metadata: {
|
||||
entity: "customer-invoice",
|
||||
@ -56,5 +49,5 @@ export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
|
||||
//},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1 +1,2 @@
|
||||
export * from "./assembler";
|
||||
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 { Transaction } from "sequelize";
|
||||
import { ICustomerInvoiceService } from "../../domain";
|
||||
import { ListCustomerInvoicesPresenter } from "./presenter";
|
||||
import { ListCustomerInvoicesAssembler } from "./assembler";
|
||||
|
||||
type ListCustomerInvoicesUseCaseInput = {
|
||||
tenantId: string;
|
||||
criteria: Criteria;
|
||||
};
|
||||
|
||||
export class ListCustomerInvoicesUseCase {
|
||||
constructor(
|
||||
private readonly customerInvoiceService: ICustomerInvoiceService,
|
||||
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) => {
|
||||
try {
|
||||
const result = await this.customerInvoiceService.findByCriteria(criteria, transaction);
|
||||
@ -22,7 +31,7 @@ export class ListCustomerInvoicesUseCase {
|
||||
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);
|
||||
} catch (error: unknown) {
|
||||
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";
|
||||
|
||||
@ -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 { Application, NextFunction, Request, Response, Router } from "express";
|
||||
import { Sequelize } from "sequelize";
|
||||
@ -7,12 +8,13 @@ import {
|
||||
DeleteCustomerInvoiceByIdRequestSchema,
|
||||
GetCustomerInvoiceByIdRequestSchema,
|
||||
} from "../../../common/dto";
|
||||
import { getInvoiceDependencies } from "../dependencies";
|
||||
import {
|
||||
buildCreateCustomerInvoicesController,
|
||||
buildDeleteCustomerInvoiceController,
|
||||
buildGetCustomerInvoiceController,
|
||||
buildListCustomerInvoicesController,
|
||||
} from "../../controllers";
|
||||
CreateCustomerInvoiceController,
|
||||
DeleteCustomerInvoiceController,
|
||||
GetCustomerInvoiceController,
|
||||
ListCustomerInvoicesController,
|
||||
} from "./controllers";
|
||||
|
||||
export const customerInvoicesRouter = (params: ModuleParams) => {
|
||||
const { app, database, baseRoutePath, logger } = params as {
|
||||
@ -22,35 +24,43 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
||||
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,
|
||||
//checkUser,
|
||||
validateRequest(CustomerInvoiceListRequestSchema, "params"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
buildListCustomerInvoicesController(database).execute(req, res, next);
|
||||
async (req: RequestWithAuth, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.build.list();
|
||||
const controller = new ListCustomerInvoicesController(useCase /*, deps.presenters.list */);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
routes.get(
|
||||
router.get(
|
||||
"/:id",
|
||||
//checkTabContext,
|
||||
//checkUser,
|
||||
validateRequest(GetCustomerInvoiceByIdRequestSchema, "params"),
|
||||
(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,
|
||||
//checkUser,
|
||||
|
||||
validateRequest(CreateCustomerInvoiceRequestSchema),
|
||||
(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",
|
||||
validateAndParseBody(IUpdateCustomerInvoiceRequestSchema),
|
||||
checkTabContext,
|
||||
//checkUser,
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
buildUpdateCustomerInvoiceController().execute(req, res, next);
|
||||
}
|
||||
);*/
|
||||
|
||||
routes.delete(
|
||||
router.delete(
|
||||
"/:id",
|
||||
//checkTabContext,
|
||||
//checkUser,
|
||||
|
||||
validateRequest(DeleteCustomerInvoiceByIdRequestSchema, "params"),
|
||||
(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>;
|
||||
|
||||
static associate(database: Sequelize) {
|
||||
/*const { Invoice_Model, CustomerInvoiceItem_Model } = connection.models;
|
||||
const { Invoice_Model, CustomerInvoiceItem_Model } = connection.models;
|
||||
|
||||
CustomerInvoiceItem_Model.belongsTo(Invoice_Model, {
|
||||
as: "customerInvoice",
|
||||
targetKey: "id",
|
||||
foreignKey: "invoice_id",
|
||||
onDelete: "CASCADE",
|
||||
});*/
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,6 +160,7 @@ export default (database: Sequelize) => {
|
||||
},
|
||||
{
|
||||
sequelize: database,
|
||||
underscored: true,
|
||||
tableName: "customer_invoice_items",
|
||||
|
||||
defaultScope: {},
|
||||
|
||||
@ -51,7 +51,21 @@ export class CustomerInvoiceModel extends Model<
|
||||
CustomerInvoiceModel.hasMany(CustomerInvoiceItemModel, {
|
||||
as: "items",
|
||||
foreignKey: "invoice_id",
|
||||
sourceKey: "id",
|
||||
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,
|
||||
tableName: "customer_invoices",
|
||||
|
||||
underscored: true,
|
||||
paranoid: true, // softs deletes
|
||||
timestamps: true,
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { SequelizeRepository, errorMapper } from "@erp/core/api";
|
||||
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
import { Sequelize, Transaction } from "sequelize";
|
||||
import { Transaction } from "sequelize";
|
||||
import { CustomerInvoice, ICustomerInvoiceRepository } from "../../domain";
|
||||
import { ICustomerInvoiceMapper } from "../mappers/customer-invoice.mapper";
|
||||
import { CustomerInvoiceModel } from "./customer-invoice.model";
|
||||
@ -14,10 +14,8 @@ export class CustomerInvoiceRepository
|
||||
//private readonly model: typeof CustomerInvoiceModel;
|
||||
private readonly mapper!: ICustomerInvoiceMapper;
|
||||
|
||||
constructor(database: Sequelize, mapper: ICustomerInvoiceMapper) {
|
||||
super(database);
|
||||
|
||||
//CustomerInvoice = database.model("CustomerInvoice") as typeof CustomerInvoiceModel;
|
||||
constructor(mapper: ICustomerInvoiceMapper) {
|
||||
super();
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Customer } from "@erp/customers/api/domain";
|
||||
import { CustomersCreationResultDTO } from "@erp/customers/common/dto";
|
||||
|
||||
export class CreateCustomersPresenter {
|
||||
export class CreateCustomersAssembler {
|
||||
public toDTO(invoice: Customer): CustomersCreationResultDTO {
|
||||
return {
|
||||
id: invoice.id.toPrimitive(),
|
||||
@ -17,7 +17,7 @@ export class CreateCustomersPresenter {
|
||||
//subtotal_price: invoice.calculateSubtotal().toPrimitive(),
|
||||
//total_price: invoice.calculateTotal().toPrimitive(),
|
||||
|
||||
//recipient: CustomerParticipantPresenter(customer.recipient),
|
||||
//recipient: CustomerParticipantAssembler(customer.recipient),
|
||||
|
||||
metadata: {
|
||||
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 { ICustomerService } from "../../domain";
|
||||
import { mapDTOToCustomerProps } from "../helpers";
|
||||
import { CreateCustomersPresenter } from "./presenter";
|
||||
import { CreateCustomersAssembler } from "./assembler";
|
||||
|
||||
export class CreateCustomerUseCase {
|
||||
constructor(
|
||||
private readonly service: ICustomerService,
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
private readonly presenter: CreateCustomersPresenter
|
||||
private readonly assembler: CreateCustomersAssembler
|
||||
) {}
|
||||
|
||||
public execute(dto: CreateCustomerCommandDTO) {
|
||||
@ -47,7 +47,7 @@ export class CreateCustomerUseCase {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
const viewDTO = this.presenter.toDTO(newInvoice);
|
||||
const viewDTO = this.assembler.toDTO(newInvoice);
|
||||
return Result.ok(viewDTO);
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./assembler";
|
||||
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 { Collection } from "@rdx/core";
|
||||
|
||||
export const customerItemPresenter = (items: Collection<CustomerItem>, context: IInvoicingContext) =>
|
||||
export const customerItemAssembler = (items: Collection<CustomerItem>, context: IInvoicingContext) =>
|
||||
items.totalCount > 0
|
||||
? items.items.map((item: CustomerItem) => ({
|
||||
description: item.description.toString(),
|
||||
@ -1,9 +1,9 @@
|
||||
import { ICustomerParticipant } from "@/contexts/invoicing/domain";
|
||||
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
||||
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,
|
||||
context: IInvoicingContext,
|
||||
): Promise<ICreateCustomer_Participant_Response_DTO | undefined> => {
|
||||
@ -14,11 +14,11 @@ export const CustomerParticipantPresenter = async (
|
||||
last_name: participant.lastName.toString(),
|
||||
company_name: participant.companyName.toString(),
|
||||
|
||||
billing_address: await CustomerParticipantAddressPresenter(
|
||||
billing_address: await CustomerParticipantAddressAssembler(
|
||||
participant.billingAddress!,
|
||||
context,
|
||||
),
|
||||
shipping_address: await CustomerParticipantAddressPresenter(
|
||||
shipping_address: await CustomerParticipantAddressAssembler(
|
||||
participant.shippingAddress!,
|
||||
context,
|
||||
),
|
||||
@ -2,7 +2,7 @@ import { CustomerParticipantAddress } from "@/contexts/invoicing/domain";
|
||||
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
|
||||
import { ICreateCustomer_AddressParticipant_Response_DTO } from "@shared/contexts";
|
||||
|
||||
export const CustomerParticipantAddressPresenter = async (
|
||||
export const CustomerParticipantAddressAssembler = async (
|
||||
address: CustomerParticipantAddress,
|
||||
context: IInvoicingContext,
|
||||
): 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 { Result } from "@repo/rdx-utils";
|
||||
import { ICustomerService } from "../../domain";
|
||||
import { GetCustomerPresenter } from "./presenter";
|
||||
import { GetCustomerAssembler } from "./assembler";
|
||||
|
||||
export class GetCustomerUseCase {
|
||||
constructor(
|
||||
private readonly service: ICustomerService,
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
private readonly presenter: GetCustomerPresenter
|
||||
private readonly assembler: GetCustomerAssembler
|
||||
) {}
|
||||
|
||||
public execute(dto: GetCustomerByIdQueryDTO) {
|
||||
@ -26,7 +26,7 @@ export class GetCustomerUseCase {
|
||||
return Result.fail(invoiceOrError.error);
|
||||
}
|
||||
|
||||
const getDTO = this.presenter.toDTO(invoiceOrError.data);
|
||||
const getDTO = this.assembler.toDTO(invoiceOrError.data);
|
||||
return Result.ok(getDTO);
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./assembler";
|
||||
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 { Customer } from "../../../domain";
|
||||
|
||||
export interface ListCustomersPresenter {
|
||||
toDTO: (customers: Collection<Customer>, criteria: Criteria) => CustomerListResponsetDTO;
|
||||
}
|
||||
|
||||
export const listCustomersPresenter: ListCustomersPresenter = {
|
||||
toDTO: (customers: Collection<Customer>, criteria: Criteria): CustomerListResponsetDTO => {
|
||||
export class ListCustomersAssembler {
|
||||
toDTO(customers: Collection<Customer>, criteria: Criteria): CustomerListResponsetDTO {
|
||||
const items = customers.map((invoice) => {
|
||||
return {
|
||||
id: invoice.id.toPrimitive(),
|
||||
@ -24,7 +21,7 @@ export const listCustomersPresenter: ListCustomersPresenter = {
|
||||
subtotal_price: invoice.calculateSubtotal().toPrimitive(),
|
||||
total_price: invoice.calculateTotal().toPrimitive(),
|
||||
|
||||
//recipient: CustomerParticipantPresenter(customer.recipient),
|
||||
//recipient: CustomerParticipantAssembler(customer.recipient),
|
||||
|
||||
metadata: {
|
||||
entity: "customer",
|
||||
@ -1 +1,2 @@
|
||||
export * from "./assembler";
|
||||
export * from "./list-customers.use-case";
|
||||
|
||||
@ -4,13 +4,13 @@ import { Criteria } from "@repo/rdx-criteria/server";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { Transaction } from "sequelize";
|
||||
import { ICustomerService } from "../../domain";
|
||||
import { ListCustomersPresenter } from "./presenter";
|
||||
import { ListCustomersAssembler } from "./assembler";
|
||||
|
||||
export class ListCustomersUseCase {
|
||||
constructor(
|
||||
private readonly customerService: ICustomerService,
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
private readonly presenter: ListCustomersPresenter
|
||||
private readonly assembler: ListCustomersAssembler
|
||||
) {}
|
||||
|
||||
public execute(criteria: Criteria): Promise<Result<ListCustomersResultDTO, Error>> {
|
||||
@ -23,7 +23,7 @@ export class ListCustomersUseCase {
|
||||
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);
|
||||
} catch (error: unknown) {
|
||||
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 { CreateCustomerCommandDTO } from "../../../common/dto";
|
||||
import { CreateCustomerUseCase } from "../../application";
|
||||
import { CreateCustomerCommandDTO } from "../../../../../common/dto";
|
||||
import { CreateCustomerUseCase } from "../../../../application";
|
||||
|
||||
export class CreateCustomerController extends ExpressController {
|
||||
public constructor(private readonly createCustomer: CreateCustomerUseCase) {
|
||||
super();
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||
}
|
||||
|
||||
protected async executeImpl() {
|
||||
@ -1,9 +1,9 @@
|
||||
import { SequelizeTransactionManager } from "@erp/core/api";
|
||||
import { Sequelize } from "sequelize";
|
||||
import { CreateCustomerUseCase, CreateCustomersPresenter } from "../../application/";
|
||||
import { CustomerService } from "../../domain";
|
||||
import { CustomerMapper } from "../../infrastructure";
|
||||
import { CreateCustomerController } from "./create-customer";
|
||||
import { CustomerMapper } from "../../..";
|
||||
import { CreateCustomerUseCase, CreateCustomersPresenter } from "../../../../application";
|
||||
import { CustomerService } from "../../../../domain";
|
||||
import { CreateCustomerController } from "./create-customer.controller";
|
||||
|
||||
export const buildCreateCustomersController = (database: Sequelize) => {
|
||||
const transactionManager = new SequelizeTransactionManager(database);
|
||||
@ -1,9 +1,11 @@
|
||||
import { ExpressController, errorMapper } from "@erp/core/api";
|
||||
import { DeleteCustomerUseCase } from "../../application";
|
||||
import { DeleteCustomerUseCase } from "../../../../application";
|
||||
|
||||
export class DeleteCustomerController extends ExpressController {
|
||||
public constructor(private readonly deleteCustomer: DeleteCustomerUseCase) {
|
||||
super();
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||
}
|
||||
|
||||
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 { GetCustomerUseCase } from "../../application";
|
||||
import { GetCustomerUseCase } from "../../../application";
|
||||
|
||||
export class GetCustomerController extends ExpressController {
|
||||
public constructor(private readonly getCustomer: GetCustomerUseCase) {
|
||||
super();
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||
}
|
||||
|
||||
protected async executeImpl() {
|
||||
@ -1,9 +1,9 @@
|
||||
import { SequelizeTransactionManager } from "@erp/core/api";
|
||||
import { Sequelize } from "sequelize";
|
||||
import { GetCustomerUseCase, getCustomerPresenter } from "../../application";
|
||||
import { CustomerService } from "../../domain";
|
||||
import { CustomerRepository, customerMapper } from "../../infrastructure";
|
||||
import { GetCustomerController } from "./get-invoice.controller";
|
||||
import { CustomerRepository, customerMapper } from "../../..";
|
||||
import { GetCustomerUseCase, getCustomerPresenter } from "../../../../application";
|
||||
import { CustomerService } from "../../../../domain";
|
||||
import { GetCustomerController } from "../get-customer.controller";
|
||||
|
||||
export const buildGetCustomerController = (database: Sequelize) => {
|
||||
const transactionManager = new SequelizeTransactionManager(database);
|
||||
@ -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 { Sequelize } from "sequelize";
|
||||
import { ListCustomersUseCase } from "../../application";
|
||||
import { listCustomersPresenter } from "../../application/list-customers/presenter";
|
||||
import { CustomerService } from "../../domain";
|
||||
import { CustomerRepository, customerMapper } from "../../infrastructure";
|
||||
import { CustomerRepository, customerMapper } from "../../..";
|
||||
import { ListCustomersUseCase } from "../../../../application";
|
||||
import { listCustomersPresenter } from "../../../../application/list-customers/assembler";
|
||||
import { CustomerService } from "../../../../domain";
|
||||
import { ListCustomersController } from "./list-customers.controller";
|
||||
|
||||
export const buildListCustomersController = (database: Sequelize) => {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user