Reestructurar servidor y configuración

This commit is contained in:
David Arranz 2025-08-14 16:58:13 +02:00
parent d1b7731b90
commit b3c5e650fc
110 changed files with 1083 additions and 853 deletions

View File

@ -1,8 +1,19 @@
# ───────────────────────────────
# Core del servidor HTTP
# ───────────────────────────────
NODE_ENV=development NODE_ENV=development
HOST=0.0.0.0 HOST=0.0.0.0
PORT=3002 PORT=3002
# URL pública del frontend (CORS).
# En dev se puede permitir todo con '*'
FRONTEND_URL=http://localhost:5173 FRONTEND_URL=http://localhost:5173
# ───────────────────────────────
# Base de datos (Sequelize / MySQL-MariaDB)
# ───────────────────────────────
# Base de datos (opción 1: URL) # Base de datos (opción 1: URL)
# DATABASE_URL=postgres://user:pass@localhost:5432/dbname # DATABASE_URL=postgres://user:pass@localhost:5432/dbname
@ -14,12 +25,39 @@ DB_NAME=uecko_erp
DB_USER=rodax DB_USER=rodax
DB_PASSWORD=rodax DB_PASSWORD=rodax
# Log de Sequelize (true|false)
DB_LOGGING=false DB_LOGGING=false
DB_SYNC_MODE=alter
APP_TIMEZONE=Europe/Madrid # Si necesitas SSL/TLS en MySQL (por defecto no)
TRUST_PROXY=0 DB_SSL=false
DB_SYNC_MODE=alter # none | alter | force
# ───────────────────────────────
# Logging de la app
# ───────────────────────────────
LOG_LEVEL=info # error | warn | info | debug
LOG_JSON=false # true para logs en JSON (entornos con agregadores)
# ───────────────────────────────
# Warmup por módulo
# ───────────────────────────────
# Tiempo máximo para cada warmup() de un módulo, en milisegundos.
WARMUP_TIMEOUT_MS=10000
# Si es true, un fallo de warmup aborta el arranque. Si es false, continúa con warning.
WARMUP_STRICT=false
# ───────────────────────────────
# Seguridad / Auth
# ───────────────────────────────
JWT_SECRET=supersecretkey JWT_SECRET=supersecretkey
JWT_ACCESS_EXPIRATION=1h JWT_ACCESS_EXPIRATION=1h
JWT_REFRESH_EXPIRATION=7d JWT_REFRESH_EXPIRATION=7d
# ───────────────────────────────
# Otros (opcional / a futuro)
# ───────────────────────────────

View File

@ -6,10 +6,18 @@ import { registerService } from "./service-registry";
const registeredModules: Map<string, IModuleServer> = new Map(); const registeredModules: Map<string, IModuleServer> = new Map();
const initializedModules = new Set<string>(); const initializedModules = new Set<string>();
const visiting = new Set<string>(); // para detección de ciclos const visiting = new Set<string>(); // para detección de ciclos
const initializationOrder: string[] = []; // orden de init → para warmup
// Config opcional para warmup (valores por defecto seguros)
const WARMUP_TIMEOUT_MS = Number(process.env.WARMUP_TIMEOUT_MS) || 10_000;
const WARMUP_STRICT =
String(process.env.WARMUP_STRICT ?? "false")
.toLowerCase()
.trim() === "true";
/** /**
Registra un módulo del servidor en el registry. Registra un módulo del servidor en el registry.
Lanza error si el nombre ya existe. Lanza error si el nombre ya existe.
*/ */
export function registerModule(pkg: IModuleServer) { export function registerModule(pkg: IModuleServer) {
if (!pkg?.name) { if (!pkg?.name) {
@ -23,20 +31,23 @@ export function registerModule(pkg: IModuleServer) {
} }
/** /**
Inicializa todos los módulos registrados (resolviendo dependencias), Inicializa todos los módulos registrados (resolviendo dependencias),
y al final inicializa los modelos (Sequelize) en bloque. luego inicializa los modelos (Sequelize) en bloque y, por último, ejecuta warmups opcionales.
*/ */
export async function initModules(params: ModuleParams) { export async function initModules(params: ModuleParams) {
for (const name of registeredModules.keys()) { for (const name of registeredModules.keys()) {
await loadModule(name, params, []); // secuencial para logs deterministas await loadModule(name, params, []); // secuencial para logs deterministas
} }
await withPhase("global", "initModels", async () => { await withPhase("global", "initModels", async () => {
await initModels(params); await initModels(params);
}); });
await warmupModules(params);
} }
/** /**
Carga recursivamente un módulo y sus dependencias con detección de ciclos. Carga recursivamente un módulo y sus dependencias con detección de ciclos.
*/ */
async function loadModule(name: string, params: ModuleParams, stack: string[]) { async function loadModule(name: string, params: ModuleParams, stack: string[]) {
if (initializedModules.has(name)) return; if (initializedModules.has(name)) return;
@ -88,30 +99,95 @@ async function loadModule(name: string, params: ModuleParams, stack: string[]) {
} }
initializedModules.add(name); initializedModules.add(name);
initializationOrder.push(name); // recordamos el orden para warmup
visiting.delete(name); visiting.delete(name);
stack.pop(); stack.pop();
logger.info(`✅ Module "${name}" registered`, { label: "moduleRegistry" }); logger.info(`✅ Module "${name}" registered`, { label: "moduleRegistry" });
} }
/** /**
Helper para anotar fase y módulo en errores y logs. Ejecuta warmup() opcional de cada módulo en orden de inicialización.
Si WARMUP_STRICT=true, un fallo aborta el arranque; si no, se avisa y continúa.
*/
async function warmupModules(params: ModuleParams) {
logger.info("🌡️ Warmup: starting...", { label: "moduleRegistry" });
for (const name of initializationOrder) {
const pkg = registeredModules.get(name);
if (!pkg) continue;
const maybeWarmup = (pkg as unknown as { warmup?: (p: ModuleParams) => Promise<void> | void })
.warmup;
if (typeof maybeWarmup === "function") {
try {
await withPhase(name, "warmup", () =>
withTimeout(Promise.resolve(maybeWarmup(params)), WARMUP_TIMEOUT_MS, `${name}.warmup`)
);
} catch (error) {
if (WARMUP_STRICT) {
logger.error("⛔️ Warmup failed (strict=true). Aborting.", {
label: "moduleRegistry",
module: name,
error,
});
throw error;
}
logger.warn("⚠️ Warmup failed but continuing (strict=false)", {
label: "moduleRegistry",
module: name,
error,
});
}
}
}
logger.info("🌡️ Warmup: done.", { label: "moduleRegistry" });
}
/**
Helper para anotar fase y módulo en logs (inicio/fin/duración) y errores.
*/ */
async function withPhase<T>( async function withPhase<T>(
moduleName: string, moduleName: string,
phase: "init" | "registerDependencies" | "registerModels" | "registerServices" | "initModels", phase:
| "init"
| "registerDependencies"
| "registerModels"
| "registerServices"
| "initModels"
| "warmup",
fn: () => Promise<T> | T fn: () => Promise<T> | T
): Promise<T> { ): Promise<T> {
const startedAt = Date.now();
logger.info(`▶️ phase=start module=${moduleName} ${phase}`, {
label: "moduleRegistry",
module: moduleName,
phase,
});
try { try {
return await fn(); const out = await fn();
const duration = Date.now() - startedAt;
logger.info(`✅ phase=done module=${moduleName} ${phase} durationMs=${duration}`, {
label: "moduleRegistry",
module: moduleName,
phase,
durationMs: duration,
});
return out;
} catch (error: any) { } catch (error: any) {
// Log enriquecido con contexto const duration = Date.now() - startedAt;
logger.error( logger.error(
`⛔️ Module "${moduleName}" failed at phase="${phase}": ${error?.message ?? error}`, `⛔️ Module "${moduleName}" failed at phase="${phase}" after ${duration}ms: ${error?.message ?? error}`,
{ {
label: "moduleRegistry", label: "moduleRegistry",
module: moduleName, module: moduleName,
phase, phase,
durationMs: duration,
stack: error?.stack, stack: error?.stack,
} }
); );
@ -123,3 +199,18 @@ async function withPhase<T>(
throw err; throw err;
} }
} }
/**
Promesa con timeout; rechaza si excede el tiempo dado.
*/
function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
let timer: NodeJS.Timeout | null = null;
const timeout = new Promise<T>((_, reject) => {
timer = setTimeout(() => {
reject(new Error(`Timeout after ${ms}ms (${label})`));
}, ms).unref();
});
return Promise.race([p, timeout]).finally(() => {
if (timer) clearTimeout(timer);
}) as Promise<T>;
}

View File

@ -6,6 +6,13 @@
"./api": "./src/api/index.ts", "./api": "./src/api/index.ts",
"./client": "./src/web/index.ts" "./client": "./src/web/index.ts"
}, },
"peerDependencies": {
"@erp/core": "workspace:*",
"dinero.js": "^1.9.1",
"sequelize": "^6.37.5",
"zod": "^3.25.67"
},
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@types/react": "^19.1.2", "@types/react": "^19.1.2",
@ -18,6 +25,7 @@
"@repo/rdx-ui": "workspace:*", "@repo/rdx-ui": "workspace:*",
"@repo/shadcn-ui": "workspace:*", "@repo/shadcn-ui": "workspace:*",
"@tanstack/react-query": "^5.74.11", "@tanstack/react-query": "^5.74.11",
"express": "^4.18.2",
"i18next": "^25.1.1", "i18next": "^25.1.1",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",

View File

@ -1,6 +1,6 @@
import { IModuleServer, ModuleParams } from "@erp/core/api"; export * from "./lib";
export const authAPIModule: IModuleServer = { /* export const authAPIModule: IModuleServer = {
name: "auth", name: "auth",
version: "1.0.0", version: "1.0.0",
dependencies: [], dependencies: [],
@ -16,10 +16,10 @@ export const authAPIModule: IModuleServer = {
return { return {
//models, //models,
services: { services: {
/*...*/
}, },
}; };
}, },
}; };
export default authAPIModule; export default authAPIModule; */

View 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;
};

View File

@ -0,0 +1,2 @@
export * from "./auth-types";
export * from "./tenancy.middleware";

View 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
};
}

View File

@ -0,0 +1 @@
export * from "./express";

View File

@ -12,12 +12,33 @@ import {
ValidationApiError, ValidationApiError,
} from "../../errors"; } from "../../errors";
type GuardResultLike = { isFailure: boolean; error?: ApiError };
export type GuardContext = {
req: Request;
res: Response;
next: NextFunction;
controller: ExpressController;
criteria: Criteria;
};
export type GuardFn = (ctx: GuardContext) => GuardResultLike | Promise<GuardResultLike>;
export function guardOk(): GuardResultLike {
return { isFailure: false };
}
export function guardFail(error: ApiError): GuardResultLike {
return { isFailure: true, error };
}
export abstract class ExpressController { export abstract class ExpressController {
protected req!: Request; //| AuthenticatedRequest | TabContextRequest; protected req!: Request;
protected res!: Response; protected res!: Response;
protected next!: NextFunction; protected next!: NextFunction;
protected criteria!: Criteria; protected criteria!: Criteria;
// 🔹 Guards configurables por controlador
private guards: GuardFn[] = [];
static errorResponse(apiError: ApiError, res: Response) { static errorResponse(apiError: ApiError, res: Response) {
return res.status(apiError.status).json(apiError); return res.status(apiError.status).json(apiError);
} }
@ -38,100 +59,111 @@ export abstract class ExpressController {
return this.res.sendStatus(httpStatus.NO_CONTENT); return this.res.sendStatus(httpStatus.NO_CONTENT);
} }
/**
* Respuesta para errores de cliente (400 Bad Request)
*/
protected clientError(message: string, errors?: any[] | any) { protected clientError(message: string, errors?: any[] | any) {
return ExpressController.errorResponse( return ExpressController.errorResponse(
new ValidationApiError(message, Array.isArray(errors) ? errors : [errors]), new ValidationApiError(message, Array.isArray(errors) ? errors : [errors]),
this.res this.res
); );
} }
/**
* Respuesta para errores de autenticación (401 Unauthorized)
*/
protected unauthorizedError(message?: string) { protected unauthorizedError(message?: string) {
return ExpressController.errorResponse( return ExpressController.errorResponse(
new UnauthorizedApiError(message ?? "Unauthorized"), new UnauthorizedApiError(message ?? "Unauthorized"),
this.res this.res
); );
} }
/**
* Respuesta para errores de autorización (403 Forbidden)
*/
protected forbiddenError(message?: string) { protected forbiddenError(message?: string) {
return ExpressController.errorResponse( return ExpressController.errorResponse(
new ForbiddenApiError(message ?? "You do not have permission to perform this action."), new ForbiddenApiError(message ?? "You do not have permission to perform this action."),
this.res this.res
); );
} }
/**
* Respuesta para recursos no encontrados (404 Not Found)
*/
protected notFoundError(message: string) { protected notFoundError(message: string) {
return ExpressController.errorResponse(new NotFoundApiError(message), this.res); return ExpressController.errorResponse(new NotFoundApiError(message), this.res);
} }
protected conflictError(message: string, _errors?: any[]) {
/**
* Respuesta para conflictos (409 Conflict)
*/
protected conflictError(message: string, errors?: any[]) {
return ExpressController.errorResponse(new ConflictApiError(message), this.res); return ExpressController.errorResponse(new ConflictApiError(message), this.res);
} }
/**
* Respuesta para errores de validación de entrada (422 Unprocessable Entity)
*/
protected invalidInputError(message: string, errors?: any[]) { protected invalidInputError(message: string, errors?: any[]) {
return ExpressController.errorResponse(new ValidationApiError(message, errors), this.res); return ExpressController.errorResponse(new ValidationApiError(message, errors), this.res);
} }
/**
* Respuesta para errores de servidor no disponible (503 Service Unavailable)
*/
protected unavailableError(message?: string) { protected unavailableError(message?: string) {
return ExpressController.errorResponse( return ExpressController.errorResponse(
new UnavailableApiError(message ?? "Service temporarily unavailable."), new UnavailableApiError(message ?? "Service temporarily unavailable."),
this.res this.res
); );
} }
/**
* Respuesta para errores internos del servidor (500 Internal Server Error)
*/
protected internalServerError(message?: string) { protected internalServerError(message?: string) {
return ExpressController.errorResponse( return ExpressController.errorResponse(
new InternalApiError(message ?? "Internal Server Error"), new InternalApiError(message ?? "Internal Server Error"),
this.res this.res
); );
} }
/**
* Respuesta para cualquier error de la API
*/
protected handleApiError(apiError: ApiError) { protected handleApiError(apiError: ApiError) {
return ExpressController.errorResponse(apiError, this.res); return ExpressController.errorResponse(apiError, this.res);
} }
/** // ───────────────────────────────────────────────────────────────────────────
* Método principal que se invoca desde el router de Express. // Guards API
* Maneja la conversión de la URL a criterios y llama a executeImpl. protected useGuards(...guards: GuardFn[]): this {
*/ this.guards.push(...guards);
public execute(req: Request, res: Response, next: NextFunction): void { return this;
}
private async runGuards(): Promise<boolean> {
for (const guard of this.guards) {
const result = await guard({
req: this.req,
res: this.res,
next: this.next,
controller: this,
criteria: this.criteria,
});
if (result?.isFailure) {
const err = result.error ?? new InternalApiError("Guard failed without error");
this.handleApiError(err);
return false;
}
}
return true;
}
// ───────────────────────────────────────────────────────────────────────────
// Helpers de auth/tenant (opcionales para usar en executeImpl)
public getUser<T extends { userId?: string; companyId?: string } = any>(): T | undefined {
// Si usáis un tipo RequestWithAuth real, cámbialo aquí
return (this.req as any).user as T | undefined;
}
public getTenantId(): string | undefined {
const user = this.getUser<{ companyId?: string }>();
return user?.companyId;
}
// ───────────────────────────────────────────────────────────────────────────
// Método principal
public async execute(req: Request, res: Response, next: NextFunction): Promise<void> {
this.req = req; this.req = req;
this.res = res; this.res = res;
this.next = next; this.next = next;
try { try {
const url = new URL(req.originalUrl, `http://${req.headers.host}`); // Construcción robusta del URL base
const host = req.get("host") ?? "localhost";
const proto = req.protocol || "http";
const url = new URL(req.originalUrl, `${proto}://${host}`);
this.criteria = new CriteriaFromUrlConverter().toCriteria(url); this.criteria = new CriteriaFromUrlConverter().toCriteria(url);
this.executeImpl(); // Ejecutar guards (auth/tenant/otros). Si alguno falla, se responde y no se entra al impl.
const ok = await this.runGuards();
if (!ok) return;
await this.executeImpl();
} catch (error: unknown) { } catch (error: unknown) {
const err = error as Error; const err = error as Error;
if (err instanceof ApiError) { if (err instanceof ApiError) {
ExpressController.errorResponse(err, this.res); ExpressController.errorResponse(err as ApiError, this.res);
} else { } else {
ExpressController.errorResponse(new InternalApiError(err.message), this.res); ExpressController.errorResponse(new InternalApiError(err.message), this.res);
} }

View File

@ -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();
};
}

View File

@ -1,2 +1,3 @@
export * from "./express-controller"; export * from "./express-controller";
export * from "./express-guards";
export * from "./middlewares"; export * from "./middlewares";

View File

@ -1,15 +1,9 @@
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import { IAggregateRootRepository, UniqueID } from "@repo/rdx-ddd"; import { IAggregateRootRepository, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { FindOptions, ModelDefined, Sequelize, Transaction } from "sequelize"; import { FindOptions, ModelDefined, Transaction } from "sequelize";
export abstract class SequelizeRepository<T> implements IAggregateRootRepository<T> { export abstract class SequelizeRepository<T> implements IAggregateRootRepository<T> {
protected readonly _database!: Sequelize;
constructor(database: Sequelize) {
this._database = database;
}
protected convertCriteria(criteria: Criteria): FindOptions { protected convertCriteria(criteria: Criteria): FindOptions {
return new CriteriaToSequelizeConverter().convert(criteria); return new CriteriaToSequelizeConverter().convert(criteria);
} }

View File

@ -2,10 +2,12 @@ import { Sequelize, Transaction } from "sequelize";
import { TransactionManager } from "../database"; import { TransactionManager } from "../database";
export class SequelizeTransactionManager extends TransactionManager { export class SequelizeTransactionManager extends TransactionManager {
protected _database: any | null = null; protected _database: Sequelize | null = null;
protected async _startTransaction(): Promise<Transaction> { protected async _startTransaction(): Promise<Transaction> {
return await this._database.transaction(); return await this._database!.transaction({
isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED,
});
} }
protected async _commitTransaction(): Promise<void> { protected async _commitTransaction(): Promise<void> {

View File

@ -10,7 +10,6 @@
"./globals.css": "./src/web/globals.css" "./globals.css": "./src/web/globals.css"
}, },
"peerDependencies": { "peerDependencies": {
"@erp/core": "workspace:*",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"express": "^4.18.2", "express": "^4.18.2",
"sequelize": "^6.37.5", "sequelize": "^6.37.5",
@ -32,6 +31,8 @@
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@erp/customers": "workspace:*", "@erp/customers": "workspace:*",
"@erp/core": "workspace:*",
"@erp/auth": "workspace:*",
"@hookform/resolvers": "^5.0.1", "@hookform/resolvers": "^5.0.1",
"@repo/rdx-criteria": "workspace:*", "@repo/rdx-criteria": "workspace:*",
"@repo/rdx-ddd": "workspace:*", "@repo/rdx-ddd": "workspace:*",

View File

@ -1,8 +1,8 @@
import { CustomerInvoice } from "@erp/customer-invoices/api/domain"; import { CustomerInvoice } from "@erp/customer-invoices/api/domain";
import { CustomerInvoicesCreationResultDTO } from "@erp/customer-invoices/common/dto"; import { CustomerInvoicesCreationResponseDTO } from "@erp/customer-invoices/common/dto";
export class CreateCustomerInvoicesPresenter { export class CreateCustomerInvoicesAssembler {
public toDTO(invoice: CustomerInvoice): CustomerInvoicesCreationResultDTO { public toDTO(invoice: CustomerInvoice): CustomerInvoicesCreationResponseDTO {
return { return {
id: invoice.id.toPrimitive(), id: invoice.id.toPrimitive(),
@ -17,7 +17,7 @@ export class CreateCustomerInvoicesPresenter {
//subtotal_price: invoice.calculateSubtotal().toPrimitive(), //subtotal_price: invoice.calculateSubtotal().toPrimitive(),
//total_price: invoice.calculateTotal().toPrimitive(), //total_price: invoice.calculateTotal().toPrimitive(),
//recipient: CustomerInvoiceParticipantPresenter(customerInvoice.recipient), //recipient: CustomerInvoiceParticipantAssembler(customerInvoice.recipient),
metadata: { metadata: {
entity: "customer-invoice", entity: "customer-invoice",

View File

@ -0,0 +1 @@
export * from "./create-customer-invoices.assembler";

View File

@ -1,19 +1,25 @@
import { DuplicateEntityError, ITransactionManager } from "@erp/core/api"; import { DuplicateEntityError, ITransactionManager } from "@erp/core/api";
import { CreateCustomerInvoiceCommandDTO } from "@erp/customer-invoices/common/dto"; import { CreateCustomerInvoiceRequestDTO } from "@erp/customer-invoices/common/dto";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { ICustomerInvoiceService } from "../../domain"; import { ICustomerInvoiceService } from "../../domain";
import { mapDTOToCustomerInvoiceProps } from "../helpers"; import { mapDTOToCustomerInvoiceProps } from "../helpers";
import { CreateCustomerInvoicesPresenter } from "./presenter"; import { CreateCustomerInvoicesAssembler } from "./assembler";
type CreateCustomerInvoiceUseCaseInput = {
tenantId: string;
dto: CreateCustomerInvoiceRequestDTO;
};
export class CreateCustomerInvoiceUseCase { export class CreateCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: ICustomerInvoiceService, private readonly service: ICustomerInvoiceService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenter: CreateCustomerInvoicesPresenter private readonly assembler: CreateCustomerInvoicesAssembler
) {} ) {}
public execute(dto: CreateCustomerInvoiceCommandDTO) { public execute(params: CreateCustomerInvoiceUseCaseInput) {
const { dto, tenantId } = params;
const invoicePropsOrError = mapDTOToCustomerInvoiceProps(dto); const invoicePropsOrError = mapDTOToCustomerInvoiceProps(dto);
if (invoicePropsOrError.isFailure) { if (invoicePropsOrError.isFailure) {
@ -21,7 +27,6 @@ export class CreateCustomerInvoiceUseCase {
} }
const { props, id } = invoicePropsOrError.data; const { props, id } = invoicePropsOrError.data;
const invoiceOrError = this.service.build(props, id); const invoiceOrError = this.service.build(props, id);
if (invoiceOrError.isFailure) { if (invoiceOrError.isFailure) {
@ -47,7 +52,7 @@ export class CreateCustomerInvoiceUseCase {
return Result.fail(result.error); return Result.fail(result.error);
} }
const viewDTO = this.presenter.toDTO(newInvoice); const viewDTO = this.assembler.toDTO(newInvoice);
return Result.ok(viewDTO); return Result.ok(viewDTO);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);

View File

@ -1,2 +1,2 @@
export * from "./assembler";
export * from "./create-customer-invoice.use-case"; export * from "./create-customer-invoice.use-case";
export * from "./presenter";

View File

@ -1 +0,0 @@
export * from "./create-customer-invoices.presenter";

View File

@ -1,27 +1,32 @@
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api"; import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
import { DeleteCustomerInvoiceByIdQueryDTO } from "@erp/customer-invoices/common/dto";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { ICustomerInvoiceService } from "../../domain"; import { ICustomerInvoiceService } from "../../domain";
type DeleteCustomerInvoiceUseCaseInput = {
tenantId: string;
id: string;
};
export class DeleteCustomerInvoiceUseCase { export class DeleteCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: ICustomerInvoiceService, private readonly service: ICustomerInvoiceService,
private readonly transactionManager: ITransactionManager private readonly transactionManager: ITransactionManager
) {} ) {}
public execute(dto: DeleteCustomerInvoiceByIdQueryDTO) { public execute(params: DeleteCustomerInvoiceUseCaseInput) {
const idOrError = UniqueID.create(dto.id); const { id, tenantId } = params;
const idOrError = UniqueID.create(id);
if (idOrError.isFailure) { if (idOrError.isFailure) {
return Result.fail(idOrError.error); return Result.fail(idOrError.error);
} }
const id = idOrError.data; const invoiceId = idOrError.data;
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
const existsCheck = await this.service.existsById(id, transaction); const existsCheck = await this.service.existsById(invoiceId, transaction);
if (existsCheck.isFailure) { if (existsCheck.isFailure) {
return Result.fail(existsCheck.error); return Result.fail(existsCheck.error);
@ -31,7 +36,7 @@ export class DeleteCustomerInvoiceUseCase {
return Result.fail(new EntityNotFoundError("CustomerInvoice", id.toString())); return Result.fail(new EntityNotFoundError("CustomerInvoice", id.toString()));
} }
return await this.service.deleteById(id, transaction); return await this.service.deleteById(invoiceId, transaction);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);
} }

View File

@ -2,7 +2,7 @@ import { CustomerInvoiceItem } from "#/server/domain";
import { IInvoicingContext } from "#/server/intrastructure"; import { IInvoicingContext } from "#/server/intrastructure";
import { Collection } from "@rdx/core"; import { Collection } from "@rdx/core";
export const customerInvoiceItemPresenter = (items: Collection<CustomerInvoiceItem>, context: IInvoicingContext) => export const customerInvoiceItemAssembler = (items: Collection<CustomerInvoiceItem>, context: IInvoicingContext) =>
items.totalCount > 0 items.totalCount > 0
? items.items.map((item: CustomerInvoiceItem) => ({ ? items.items.map((item: CustomerInvoiceItem) => ({
description: item.description.toString(), description: item.description.toString(),

View File

@ -1,9 +1,9 @@
import { ICustomerInvoiceParticipant } from "@/contexts/invoicing/domain"; import { ICustomerInvoiceParticipant } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { ICreateCustomerInvoice_Participant_Response_DTO } from "@shared/contexts"; import { ICreateCustomerInvoice_Participant_Response_DTO } from "@shared/contexts";
import { CustomerInvoiceParticipantAddressPresenter } from "./CustomerInvoiceParticipantAddress.presenter"; import { CustomerInvoiceParticipantAddressAssembler } from "./CustomerInvoiceParticipantAddress.assembler";
export const CustomerInvoiceParticipantPresenter = async ( export const CustomerInvoiceParticipantAssembler = async (
participant: ICustomerInvoiceParticipant, participant: ICustomerInvoiceParticipant,
context: IInvoicingContext, context: IInvoicingContext,
): Promise<ICreateCustomerInvoice_Participant_Response_DTO | undefined> => { ): Promise<ICreateCustomerInvoice_Participant_Response_DTO | undefined> => {
@ -14,11 +14,11 @@ export const CustomerInvoiceParticipantPresenter = async (
last_name: participant.lastName.toString(), last_name: participant.lastName.toString(),
company_name: participant.companyName.toString(), company_name: participant.companyName.toString(),
billing_address: await CustomerInvoiceParticipantAddressPresenter( billing_address: await CustomerInvoiceParticipantAddressAssembler(
participant.billingAddress!, participant.billingAddress!,
context, context,
), ),
shipping_address: await CustomerInvoiceParticipantAddressPresenter( shipping_address: await CustomerInvoiceParticipantAddressAssembler(
participant.shippingAddress!, participant.shippingAddress!,
context, context,
), ),

View File

@ -2,7 +2,7 @@ import { CustomerInvoiceParticipantAddress } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { ICreateCustomerInvoice_AddressParticipant_Response_DTO } from "@shared/contexts"; import { ICreateCustomerInvoice_AddressParticipant_Response_DTO } from "@shared/contexts";
export const CustomerInvoiceParticipantAddressPresenter = async ( export const CustomerInvoiceParticipantAddressAssembler = async (
address: CustomerInvoiceParticipantAddress, address: CustomerInvoiceParticipantAddress,
context: IInvoicingContext, context: IInvoicingContext,
): Promise<ICreateCustomerInvoice_AddressParticipant_Response_DTO> => { ): Promise<ICreateCustomerInvoice_AddressParticipant_Response_DTO> => {

View File

@ -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: "",
},*/
};
}
}

View File

@ -0,0 +1 @@
export * from "./get-invoice.assembler";

View File

@ -1,19 +1,24 @@
import { ITransactionManager } from "@erp/core/api"; import { ITransactionManager } from "@erp/core/api";
import { GetCustomerInvoiceByIdQueryDTO } from "@erp/customer-invoices/common/dto";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { ICustomerInvoiceService } from "../../domain"; import { ICustomerInvoiceService } from "../../domain";
import { GetCustomerInvoicePresenter } from "./presenter"; import { GetCustomerInvoiceAssembler } from "./assembler";
type GetCustomerInvoiceUseCaseInput = {
tenantId: string;
id: string;
};
export class GetCustomerInvoiceUseCase { export class GetCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: ICustomerInvoiceService, private readonly service: ICustomerInvoiceService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenter: GetCustomerInvoicePresenter private readonly assembler: GetCustomerInvoiceAssembler
) {} ) {}
public execute(dto: GetCustomerInvoiceByIdQueryDTO) { public execute(params: GetCustomerInvoiceUseCaseInput) {
const idOrError = UniqueID.create(dto.id); const { id, tenantId } = params;
const idOrError = UniqueID.create(id);
if (idOrError.isFailure) { if (idOrError.isFailure) {
return Result.fail(idOrError.error); return Result.fail(idOrError.error);
@ -26,7 +31,7 @@ export class GetCustomerInvoiceUseCase {
return Result.fail(invoiceOrError.error); return Result.fail(invoiceOrError.error);
} }
const getDTO = this.presenter.toDTO(invoiceOrError.data); const getDTO = this.assembler.toDTO(invoiceOrError.data);
return Result.ok(getDTO); return Result.ok(getDTO);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);

View File

@ -1,2 +1,2 @@
export * from "./assembler";
export * from "./get-customer-invoice.use-case"; export * from "./get-customer-invoice.use-case";
export * from "./presenter";

View File

@ -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: "",
},*/
}),
};

View File

@ -1 +0,0 @@
export * from "./get-invoice.presenter";

View File

@ -1,8 +1,8 @@
import { ICustomerInvoiceParticipant } from "@/contexts/invoicing/domain"; import { ICustomerInvoiceParticipant } from "@/contexts/invoicing/domain";
import { IListCustomerInvoice_Participant_Response_DTO } from "@shared/contexts"; import { IListCustomerInvoice_Participant_Response_DTO } from "@shared/contexts";
import { CustomerInvoiceParticipantAddressPresenter } from "./CustomerInvoiceParticipantAddress.presenter"; import { CustomerInvoiceParticipantAddressAssembler } from "./CustomerInvoiceParticipantAddress.assembler";
export const CustomerInvoiceParticipantPresenter = ( export const CustomerInvoiceParticipantAssembler = (
participant: ICustomerInvoiceParticipant, participant: ICustomerInvoiceParticipant,
): IListCustomerInvoice_Participant_Response_DTO => { ): IListCustomerInvoice_Participant_Response_DTO => {
return { return {
@ -12,10 +12,10 @@ export const CustomerInvoiceParticipantPresenter = (
last_name: participant?.lastName?.toString(), last_name: participant?.lastName?.toString(),
company_name: participant?.companyName?.toString(), company_name: participant?.companyName?.toString(),
billing_address: CustomerInvoiceParticipantAddressPresenter( billing_address: CustomerInvoiceParticipantAddressAssembler(
participant?.billingAddress!, participant?.billingAddress!,
), ),
shipping_address: CustomerInvoiceParticipantAddressPresenter( shipping_address: CustomerInvoiceParticipantAddressAssembler(
participant?.shippingAddress!, participant?.shippingAddress!,
), ),
}; };

View File

@ -1,4 +1,4 @@
export const CustomerInvoiceParticipantAddressPresenter = ( export const CustomerInvoiceParticipantAddressAssembler = (
address: CustomerInvoiceParticipantAddress address: CustomerInvoiceParticipantAddress
): IListCustomerInvoice_AddressParticipant_Response_DTO => { ): IListCustomerInvoice_AddressParticipant_Response_DTO => {
return { return {

View File

@ -0,0 +1 @@
export * from "./list-invoices.assembler";

View File

@ -1,20 +1,13 @@
import { CustomerInvoice } from "@erp/customer-invoices/api/domain";
import { CustomerInvoiceListResponseDTO } from "@erp/customer-invoices/common/dto";
import { Criteria } from "@repo/rdx-criteria/server"; import { Criteria } from "@repo/rdx-criteria/server";
import { Collection } from "@repo/rdx-utils"; import { Collection } from "@repo/rdx-utils";
import { CustomerInvoiceListResponseDTO } from "../../../../common/dto";
import { CustomerInvoice } from "../../../domain";
export interface ListCustomerInvoicesPresenter { export class ListCustomerInvoicesAssembler {
toDTO: ( toDTO(
customerInvoices: Collection<CustomerInvoice>, customerInvoices: Collection<CustomerInvoice>,
criteria: Criteria criteria: Criteria
) => CustomerInvoiceListResponseDTO; ): CustomerInvoiceListResponseDTO {
}
export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
toDTO: (
customerInvoices: Collection<CustomerInvoice>,
criteria: Criteria
): CustomerInvoiceListResponseDTO => {
const items = customerInvoices.map((invoice) => { const items = customerInvoices.map((invoice) => {
return { return {
id: invoice.id.toPrimitive(), id: invoice.id.toPrimitive(),
@ -30,7 +23,7 @@ export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
subtotal_price: invoice.calculateSubtotal().toPrimitive(), subtotal_price: invoice.calculateSubtotal().toPrimitive(),
total_price: invoice.calculateTotal().toPrimitive(), total_price: invoice.calculateTotal().toPrimitive(),
//recipient: CustomerInvoiceParticipantPresenter(customerInvoice.recipient), //recipient: CustomerInvoiceParticipantAssembler(customerInvoice.recipient),
metadata: { metadata: {
entity: "customer-invoice", entity: "customer-invoice",
@ -56,5 +49,5 @@ export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
//}, //},
}, },
}; };
}, }
}; }

View File

@ -1 +1,2 @@
export * from "./assembler";
export * from "./list-customer-invoices.use-case"; export * from "./list-customer-invoices.use-case";

View File

@ -4,16 +4,25 @@ import { Criteria } from "@repo/rdx-criteria/server";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { ICustomerInvoiceService } from "../../domain"; import { ICustomerInvoiceService } from "../../domain";
import { ListCustomerInvoicesPresenter } from "./presenter"; import { ListCustomerInvoicesAssembler } from "./assembler";
type ListCustomerInvoicesUseCaseInput = {
tenantId: string;
criteria: Criteria;
};
export class ListCustomerInvoicesUseCase { export class ListCustomerInvoicesUseCase {
constructor( constructor(
private readonly customerInvoiceService: ICustomerInvoiceService, private readonly customerInvoiceService: ICustomerInvoiceService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenter: ListCustomerInvoicesPresenter private readonly assembler: ListCustomerInvoicesAssembler
) {} ) {}
public execute(criteria: Criteria): Promise<Result<CustomerInvoiceListResponseDTO, Error>> { public execute(
params: ListCustomerInvoicesUseCaseInput
): Promise<Result<CustomerInvoiceListResponseDTO, Error>> {
const { criteria, tenantId } = params;
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: Transaction) => {
try { try {
const result = await this.customerInvoiceService.findByCriteria(criteria, transaction); const result = await this.customerInvoiceService.findByCriteria(criteria, transaction);
@ -22,7 +31,7 @@ export class ListCustomerInvoicesUseCase {
return Result.fail(result.error); return Result.fail(result.error);
} }
const dto: CustomerInvoiceListResponseDTO = this.presenter.toDTO(result.data, criteria); const dto: CustomerInvoiceListResponseDTO = this.assembler.toDTO(result.data, criteria);
return Result.ok(dto); return Result.ok(dto);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);

View File

@ -1 +0,0 @@
export * from "./list-invoices.presenter";

View File

@ -1 +1,2 @@
export * from "./assembler";
export * from "./update-customer-invoice.use-case"; export * from "./update-customer-invoice.use-case";

View File

@ -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);
}
}

View File

@ -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);
};

View File

@ -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);
}
}

View File

@ -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);
};

View File

@ -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);
}
}

View File

@ -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);
};

View File

@ -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";

View File

@ -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);
};

View File

@ -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);
}
}

View File

@ -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
);
};

View File

@ -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,
}))
: [];

View File

@ -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,
),
};
};

View File

@ -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(),
};
};

View File

@ -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),
};
},
};

View File

@ -1 +0,0 @@
export * from "./UpdateCustomerInvoice.presenter";

View File

@ -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());
}
}

View File

@ -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),
},
};
}

View File

@ -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))
);
}
}

View File

@ -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))
);
}
}

View File

@ -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))
);
}
}

View File

@ -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";

View File

@ -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))
);
}
}

View File

@ -1,3 +1,4 @@
import { RequestWithAuth, enforceTenant } from "@erp/auth/api";
import { ILogger, ModuleParams, validateRequest } from "@erp/core/api"; import { ILogger, ModuleParams, validateRequest } from "@erp/core/api";
import { Application, NextFunction, Request, Response, Router } from "express"; import { Application, NextFunction, Request, Response, Router } from "express";
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
@ -7,12 +8,13 @@ import {
DeleteCustomerInvoiceByIdRequestSchema, DeleteCustomerInvoiceByIdRequestSchema,
GetCustomerInvoiceByIdRequestSchema, GetCustomerInvoiceByIdRequestSchema,
} from "../../../common/dto"; } from "../../../common/dto";
import { getInvoiceDependencies } from "../dependencies";
import { import {
buildCreateCustomerInvoicesController, CreateCustomerInvoiceController,
buildDeleteCustomerInvoiceController, DeleteCustomerInvoiceController,
buildGetCustomerInvoiceController, GetCustomerInvoiceController,
buildListCustomerInvoicesController, ListCustomerInvoicesController,
} from "../../controllers"; } from "./controllers";
export const customerInvoicesRouter = (params: ModuleParams) => { export const customerInvoicesRouter = (params: ModuleParams) => {
const { app, database, baseRoutePath, logger } = params as { const { app, database, baseRoutePath, logger } = params as {
@ -22,35 +24,43 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
logger: ILogger; logger: ILogger;
}; };
const routes: Router = Router({ mergeParams: true }); const router: Router = Router({ mergeParams: true });
const deps = getInvoiceDependencies(params);
routes.get( // 🔐 Autenticación + Tenancy para TODO el router
router.use(/* authenticateJWT(), */ enforceTenant() /*checkTabContext*/);
router.get(
"/", "/",
//checkTabContext, //checkTabContext,
//checkUser,
validateRequest(CustomerInvoiceListRequestSchema, "params"), validateRequest(CustomerInvoiceListRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => { async (req: RequestWithAuth, res: Response, next: NextFunction) => {
buildListCustomerInvoicesController(database).execute(req, res, next); const useCase = deps.build.list();
const controller = new ListCustomerInvoicesController(useCase /*, deps.presenters.list */);
return controller.execute(req, res, next);
} }
); );
routes.get( router.get(
"/:id", "/:id",
//checkTabContext, //checkTabContext,
//checkUser,
validateRequest(GetCustomerInvoiceByIdRequestSchema, "params"), validateRequest(GetCustomerInvoiceByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
buildGetCustomerInvoiceController(database).execute(req, res, next); const useCase = deps.build.get();
const controller = new GetCustomerInvoiceController(useCase);
return controller.execute(req, res, next);
} }
); );
routes.post( router.post(
"/", "/",
//checkTabContext, //checkTabContext,
//checkUser,
validateRequest(CreateCustomerInvoiceRequestSchema), validateRequest(CreateCustomerInvoiceRequestSchema),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
buildCreateCustomerInvoicesController(database).execute(req, res, next); const useCase = deps.build.create();
const controller = new CreateCustomerInvoiceController(useCase);
return controller.execute(req, res, next);
} }
); );
@ -58,21 +68,23 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
"/:customerInvoiceId", "/:customerInvoiceId",
validateAndParseBody(IUpdateCustomerInvoiceRequestSchema), validateAndParseBody(IUpdateCustomerInvoiceRequestSchema),
checkTabContext, checkTabContext,
//checkUser,
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
buildUpdateCustomerInvoiceController().execute(req, res, next); buildUpdateCustomerInvoiceController().execute(req, res, next);
} }
);*/ );*/
routes.delete( router.delete(
"/:id", "/:id",
//checkTabContext, //checkTabContext,
//checkUser,
validateRequest(DeleteCustomerInvoiceByIdRequestSchema, "params"), validateRequest(DeleteCustomerInvoiceByIdRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
buildDeleteCustomerInvoiceController(database).execute(req, res, next); const useCase = deps.build.delete();
const controller = new DeleteCustomerInvoiceController(useCase);
return controller.execute(req, res, next);
} }
); );
app.use(`${baseRoutePath}/customer-invoices`, routes); app.use(`${baseRoutePath}/customer-invoices`, router);
}; };

View File

@ -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);
}

View File

@ -107,6 +107,3 @@ export class CustomerInvoiceMapper
}; };
} }
} }
const customerInvoiceMapper: CustomerInvoiceMapper = new CustomerInvoiceMapper();
export { customerInvoiceMapper };

View File

@ -45,13 +45,14 @@ export class CustomerInvoiceItemModel extends Model<
declare invoice: NonAttribute<CustomerInvoiceModel>; declare invoice: NonAttribute<CustomerInvoiceModel>;
static associate(database: Sequelize) { static associate(database: Sequelize) {
/*const { Invoice_Model, CustomerInvoiceItem_Model } = connection.models; const { Invoice_Model, CustomerInvoiceItem_Model } = connection.models;
CustomerInvoiceItem_Model.belongsTo(Invoice_Model, { CustomerInvoiceItem_Model.belongsTo(Invoice_Model, {
as: "customerInvoice", as: "customerInvoice",
targetKey: "id",
foreignKey: "invoice_id", foreignKey: "invoice_id",
onDelete: "CASCADE", onDelete: "CASCADE",
});*/ });
} }
} }
@ -159,6 +160,7 @@ export default (database: Sequelize) => {
}, },
{ {
sequelize: database, sequelize: database,
underscored: true,
tableName: "customer_invoice_items", tableName: "customer_invoice_items",
defaultScope: {}, defaultScope: {},

View File

@ -51,7 +51,21 @@ export class CustomerInvoiceModel extends Model<
CustomerInvoiceModel.hasMany(CustomerInvoiceItemModel, { CustomerInvoiceModel.hasMany(CustomerInvoiceItemModel, {
as: "items", as: "items",
foreignKey: "invoice_id", foreignKey: "invoice_id",
sourceKey: "id",
onDelete: "CASCADE", onDelete: "CASCADE",
constraints: true,
});
}
static hooks(database: Sequelize) {
// Soft-cascade manual: al borrar una factura, marcamos items como borrados (paranoid).
CustomerInvoiceModel.addHook("afterDestroy", async (invoice, options) => {
if (!invoice?.id) return;
await CustomerInvoiceItemModel.destroy({
where: { invoiceId: invoice.id },
individualHooks: true,
transaction: options.transaction,
});
}); });
} }
} }
@ -129,6 +143,7 @@ export default (database: Sequelize) => {
sequelize: database, sequelize: database,
tableName: "customer_invoices", tableName: "customer_invoices",
underscored: true,
paranoid: true, // softs deletes paranoid: true, // softs deletes
timestamps: true, timestamps: true,

View File

@ -2,7 +2,7 @@ import { SequelizeRepository, errorMapper } from "@erp/core/api";
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import { Sequelize, Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { CustomerInvoice, ICustomerInvoiceRepository } from "../../domain"; import { CustomerInvoice, ICustomerInvoiceRepository } from "../../domain";
import { ICustomerInvoiceMapper } from "../mappers/customer-invoice.mapper"; import { ICustomerInvoiceMapper } from "../mappers/customer-invoice.mapper";
import { CustomerInvoiceModel } from "./customer-invoice.model"; import { CustomerInvoiceModel } from "./customer-invoice.model";
@ -14,10 +14,8 @@ export class CustomerInvoiceRepository
//private readonly model: typeof CustomerInvoiceModel; //private readonly model: typeof CustomerInvoiceModel;
private readonly mapper!: ICustomerInvoiceMapper; private readonly mapper!: ICustomerInvoiceMapper;
constructor(database: Sequelize, mapper: ICustomerInvoiceMapper) { constructor(mapper: ICustomerInvoiceMapper) {
super(database); super();
//CustomerInvoice = database.model("CustomerInvoice") as typeof CustomerInvoiceModel;
this.mapper = mapper; this.mapper = mapper;
} }

View File

@ -1,7 +1,7 @@
import { Customer } from "@erp/customers/api/domain"; import { Customer } from "@erp/customers/api/domain";
import { CustomersCreationResultDTO } from "@erp/customers/common/dto"; import { CustomersCreationResultDTO } from "@erp/customers/common/dto";
export class CreateCustomersPresenter { export class CreateCustomersAssembler {
public toDTO(invoice: Customer): CustomersCreationResultDTO { public toDTO(invoice: Customer): CustomersCreationResultDTO {
return { return {
id: invoice.id.toPrimitive(), id: invoice.id.toPrimitive(),
@ -17,7 +17,7 @@ export class CreateCustomersPresenter {
//subtotal_price: invoice.calculateSubtotal().toPrimitive(), //subtotal_price: invoice.calculateSubtotal().toPrimitive(),
//total_price: invoice.calculateTotal().toPrimitive(), //total_price: invoice.calculateTotal().toPrimitive(),
//recipient: CustomerParticipantPresenter(customer.recipient), //recipient: CustomerParticipantAssembler(customer.recipient),
metadata: { metadata: {
entity: "customer", entity: "customer",

View File

@ -0,0 +1 @@
export * from "./create-customers.assembler";

View File

@ -4,13 +4,13 @@ import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { ICustomerService } from "../../domain"; import { ICustomerService } from "../../domain";
import { mapDTOToCustomerProps } from "../helpers"; import { mapDTOToCustomerProps } from "../helpers";
import { CreateCustomersPresenter } from "./presenter"; import { CreateCustomersAssembler } from "./assembler";
export class CreateCustomerUseCase { export class CreateCustomerUseCase {
constructor( constructor(
private readonly service: ICustomerService, private readonly service: ICustomerService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenter: CreateCustomersPresenter private readonly assembler: CreateCustomersAssembler
) {} ) {}
public execute(dto: CreateCustomerCommandDTO) { public execute(dto: CreateCustomerCommandDTO) {
@ -47,7 +47,7 @@ export class CreateCustomerUseCase {
return Result.fail(result.error); return Result.fail(result.error);
} }
const viewDTO = this.presenter.toDTO(newInvoice); const viewDTO = this.assembler.toDTO(newInvoice);
return Result.ok(viewDTO); return Result.ok(viewDTO);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);

View File

@ -1,2 +1,2 @@
export * from "./assembler";
export * from "./create-customer.use-case"; export * from "./create-customer.use-case";
export * from "./presenter";

View File

@ -1 +0,0 @@
export * from "./create-customers.presenter";

View File

@ -2,7 +2,7 @@ import { CustomerItem } from "#/server/domain";
import { IInvoicingContext } from "#/server/intrastructure"; import { IInvoicingContext } from "#/server/intrastructure";
import { Collection } from "@rdx/core"; import { Collection } from "@rdx/core";
export const customerItemPresenter = (items: Collection<CustomerItem>, context: IInvoicingContext) => export const customerItemAssembler = (items: Collection<CustomerItem>, context: IInvoicingContext) =>
items.totalCount > 0 items.totalCount > 0
? items.items.map((item: CustomerItem) => ({ ? items.items.map((item: CustomerItem) => ({
description: item.description.toString(), description: item.description.toString(),

View File

@ -1,9 +1,9 @@
import { ICustomerParticipant } from "@/contexts/invoicing/domain"; import { ICustomerParticipant } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { ICreateCustomer_Participant_Response_DTO } from "@shared/contexts"; import { ICreateCustomer_Participant_Response_DTO } from "@shared/contexts";
import { CustomerParticipantAddressPresenter } from "./CustomerParticipantAddress.presenter"; import { CustomerParticipantAddressAssembler } from "./CustomerParticipantAddress.assembler";
export const CustomerParticipantPresenter = async ( export const CustomerParticipantAssembler = async (
participant: ICustomerParticipant, participant: ICustomerParticipant,
context: IInvoicingContext, context: IInvoicingContext,
): Promise<ICreateCustomer_Participant_Response_DTO | undefined> => { ): Promise<ICreateCustomer_Participant_Response_DTO | undefined> => {
@ -14,11 +14,11 @@ export const CustomerParticipantPresenter = async (
last_name: participant.lastName.toString(), last_name: participant.lastName.toString(),
company_name: participant.companyName.toString(), company_name: participant.companyName.toString(),
billing_address: await CustomerParticipantAddressPresenter( billing_address: await CustomerParticipantAddressAssembler(
participant.billingAddress!, participant.billingAddress!,
context, context,
), ),
shipping_address: await CustomerParticipantAddressPresenter( shipping_address: await CustomerParticipantAddressAssembler(
participant.shippingAddress!, participant.shippingAddress!,
context, context,
), ),

View File

@ -2,7 +2,7 @@ import { CustomerParticipantAddress } from "@/contexts/invoicing/domain";
import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext";
import { ICreateCustomer_AddressParticipant_Response_DTO } from "@shared/contexts"; import { ICreateCustomer_AddressParticipant_Response_DTO } from "@shared/contexts";
export const CustomerParticipantAddressPresenter = async ( export const CustomerParticipantAddressAssembler = async (
address: CustomerParticipantAddress, address: CustomerParticipantAddress,
context: IInvoicingContext, context: IInvoicingContext,
): Promise<ICreateCustomer_AddressParticipant_Response_DTO> => { ): Promise<ICreateCustomer_AddressParticipant_Response_DTO> => {

View File

@ -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: "",
},*/
};
}
}

View File

@ -0,0 +1 @@
export * from "./get-invoice.assembler";

View File

@ -3,13 +3,13 @@ import { GetCustomerByIdQueryDTO } from "@erp/customers/common/dto";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { ICustomerService } from "../../domain"; import { ICustomerService } from "../../domain";
import { GetCustomerPresenter } from "./presenter"; import { GetCustomerAssembler } from "./assembler";
export class GetCustomerUseCase { export class GetCustomerUseCase {
constructor( constructor(
private readonly service: ICustomerService, private readonly service: ICustomerService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenter: GetCustomerPresenter private readonly assembler: GetCustomerAssembler
) {} ) {}
public execute(dto: GetCustomerByIdQueryDTO) { public execute(dto: GetCustomerByIdQueryDTO) {
@ -26,7 +26,7 @@ export class GetCustomerUseCase {
return Result.fail(invoiceOrError.error); return Result.fail(invoiceOrError.error);
} }
const getDTO = this.presenter.toDTO(invoiceOrError.data); const getDTO = this.assembler.toDTO(invoiceOrError.data);
return Result.ok(getDTO); return Result.ok(getDTO);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);

View File

@ -1,2 +1,2 @@
export * from "./assembler";
export * from "./get-customer.use-case"; export * from "./get-customer.use-case";
export * from "./presenter";

View File

@ -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: "",
},*/
}),
};

View File

@ -1 +0,0 @@
export * from "./get-invoice.presenter";

View File

@ -0,0 +1 @@
export * from "./list-invoices.assembler";

View File

@ -3,12 +3,9 @@ import { Collection } from "@repo/rdx-utils";
import { CustomerListResponsetDTO } from "../../../../common/dto"; import { CustomerListResponsetDTO } from "../../../../common/dto";
import { Customer } from "../../../domain"; import { Customer } from "../../../domain";
export interface ListCustomersPresenter {
toDTO: (customers: Collection<Customer>, criteria: Criteria) => CustomerListResponsetDTO;
}
export const listCustomersPresenter: ListCustomersPresenter = { export class ListCustomersAssembler {
toDTO: (customers: Collection<Customer>, criteria: Criteria): CustomerListResponsetDTO => { toDTO(customers: Collection<Customer>, criteria: Criteria): CustomerListResponsetDTO {
const items = customers.map((invoice) => { const items = customers.map((invoice) => {
return { return {
id: invoice.id.toPrimitive(), id: invoice.id.toPrimitive(),
@ -24,7 +21,7 @@ export const listCustomersPresenter: ListCustomersPresenter = {
subtotal_price: invoice.calculateSubtotal().toPrimitive(), subtotal_price: invoice.calculateSubtotal().toPrimitive(),
total_price: invoice.calculateTotal().toPrimitive(), total_price: invoice.calculateTotal().toPrimitive(),
//recipient: CustomerParticipantPresenter(customer.recipient), //recipient: CustomerParticipantAssembler(customer.recipient),
metadata: { metadata: {
entity: "customer", entity: "customer",

View File

@ -1 +1,2 @@
export * from "./assembler";
export * from "./list-customers.use-case"; export * from "./list-customers.use-case";

View File

@ -4,13 +4,13 @@ import { Criteria } from "@repo/rdx-criteria/server";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { ICustomerService } from "../../domain"; import { ICustomerService } from "../../domain";
import { ListCustomersPresenter } from "./presenter"; import { ListCustomersAssembler } from "./assembler";
export class ListCustomersUseCase { export class ListCustomersUseCase {
constructor( constructor(
private readonly customerService: ICustomerService, private readonly customerService: ICustomerService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenter: ListCustomersPresenter private readonly assembler: ListCustomersAssembler
) {} ) {}
public execute(criteria: Criteria): Promise<Result<ListCustomersResultDTO, Error>> { public execute(criteria: Criteria): Promise<Result<ListCustomersResultDTO, Error>> {
@ -23,7 +23,7 @@ export class ListCustomersUseCase {
return Result.fail(result.error); return Result.fail(result.error);
} }
const dto: ListCustomersResultDTO = this.presenter.toDTO(result.data, criteria); const dto: ListCustomersResultDTO = this.assembler.toDTO(result.data, criteria);
return Result.ok(dto); return Result.ok(dto);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);

View File

@ -1 +0,0 @@
export * from "./list-invoices.presenter";

View File

@ -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);
};

View File

@ -1,5 +0,0 @@
export * from "./create-customer";
export * from "./delete-customer";
export * from "./get-customer";
export * from "./list-customers";
///export * from "./update-customer";

View File

@ -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);
}
}

View 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),
},
};
}

View File

@ -1,10 +1,12 @@
import { ExpressController, errorMapper } from "@erp/core/api"; import { ExpressController, errorMapper } from "@erp/core/api";
import { CreateCustomerCommandDTO } from "../../../common/dto"; import { CreateCustomerCommandDTO } from "../../../../../common/dto";
import { CreateCustomerUseCase } from "../../application"; import { CreateCustomerUseCase } from "../../../../application";
export class CreateCustomerController extends ExpressController { export class CreateCustomerController extends ExpressController {
public constructor(private readonly createCustomer: CreateCustomerUseCase) { public constructor(private readonly createCustomer: CreateCustomerUseCase) {
super(); super();
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
} }
protected async executeImpl() { protected async executeImpl() {

View File

@ -1,9 +1,9 @@
import { SequelizeTransactionManager } from "@erp/core/api"; import { SequelizeTransactionManager } from "@erp/core/api";
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
import { CreateCustomerUseCase, CreateCustomersPresenter } from "../../application/"; import { CustomerMapper } from "../../..";
import { CustomerService } from "../../domain"; import { CreateCustomerUseCase, CreateCustomersPresenter } from "../../../../application";
import { CustomerMapper } from "../../infrastructure"; import { CustomerService } from "../../../../domain";
import { CreateCustomerController } from "./create-customer"; import { CreateCustomerController } from "./create-customer.controller";
export const buildCreateCustomersController = (database: Sequelize) => { export const buildCreateCustomersController = (database: Sequelize) => {
const transactionManager = new SequelizeTransactionManager(database); const transactionManager = new SequelizeTransactionManager(database);

View File

@ -1,9 +1,11 @@
import { ExpressController, errorMapper } from "@erp/core/api"; import { ExpressController, errorMapper } from "@erp/core/api";
import { DeleteCustomerUseCase } from "../../application"; import { DeleteCustomerUseCase } from "../../../../application";
export class DeleteCustomerController extends ExpressController { export class DeleteCustomerController extends ExpressController {
public constructor(private readonly deleteCustomer: DeleteCustomerUseCase) { public constructor(private readonly deleteCustomer: DeleteCustomerUseCase) {
super(); super();
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
} }
async executeImpl(): Promise<any> { async executeImpl(): Promise<any> {

View File

@ -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 */);
};

View File

@ -1,9 +1,11 @@
import { ExpressController, errorMapper } from "@erp/core/api"; import { ExpressController, errorMapper } from "@erp/core/api";
import { GetCustomerUseCase } from "../../application"; import { GetCustomerUseCase } from "../../../application";
export class GetCustomerController extends ExpressController { export class GetCustomerController extends ExpressController {
public constructor(private readonly getCustomer: GetCustomerUseCase) { public constructor(private readonly getCustomer: GetCustomerUseCase) {
super(); super();
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
} }
protected async executeImpl() { protected async executeImpl() {

View File

@ -1,9 +1,9 @@
import { SequelizeTransactionManager } from "@erp/core/api"; import { SequelizeTransactionManager } from "@erp/core/api";
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
import { GetCustomerUseCase, getCustomerPresenter } from "../../application"; import { CustomerRepository, customerMapper } from "../../..";
import { CustomerService } from "../../domain"; import { GetCustomerUseCase, getCustomerPresenter } from "../../../../application";
import { CustomerRepository, customerMapper } from "../../infrastructure"; import { CustomerService } from "../../../../domain";
import { GetCustomerController } from "./get-invoice.controller"; import { GetCustomerController } from "../get-customer.controller";
export const buildGetCustomerController = (database: Sequelize) => { export const buildGetCustomerController = (database: Sequelize) => {
const transactionManager = new SequelizeTransactionManager(database); const transactionManager = new SequelizeTransactionManager(database);

View File

@ -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";

View File

@ -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))
);
}
}

View File

@ -1,9 +1,9 @@
import { SequelizeTransactionManager } from "@erp/core/api"; import { SequelizeTransactionManager } from "@erp/core/api";
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
import { ListCustomersUseCase } from "../../application"; import { CustomerRepository, customerMapper } from "../../..";
import { listCustomersPresenter } from "../../application/list-customers/presenter"; import { ListCustomersUseCase } from "../../../../application";
import { CustomerService } from "../../domain"; import { listCustomersPresenter } from "../../../../application/list-customers/assembler";
import { CustomerRepository, customerMapper } from "../../infrastructure"; import { CustomerService } from "../../../../domain";
import { ListCustomersController } from "./list-customers.controller"; import { ListCustomersController } from "./list-customers.controller";
export const buildListCustomersController = (database: Sequelize) => { export const buildListCustomersController = (database: Sequelize) => {

Some files were not shown because too many files have changed in this diff Show More