diff --git a/apps/server/.env.example b/apps/server/.env.example index 472274bd..228811c5 100644 --- a/apps/server/.env.example +++ b/apps/server/.env.example @@ -1,8 +1,19 @@ +# ─────────────────────────────── +# Core del servidor HTTP +# ─────────────────────────────── NODE_ENV=development HOST=0.0.0.0 PORT=3002 + +# URL pública del frontend (CORS). +# En dev se puede permitir todo con '*' FRONTEND_URL=http://localhost:5173 + +# ─────────────────────────────── +# Base de datos (Sequelize / MySQL-MariaDB) +# ─────────────────────────────── + # Base de datos (opción 1: URL) # DATABASE_URL=postgres://user:pass@localhost:5432/dbname @@ -14,12 +25,39 @@ DB_NAME=uecko_erp DB_USER=rodax DB_PASSWORD=rodax +# Log de Sequelize (true|false) DB_LOGGING=false -DB_SYNC_MODE=alter -APP_TIMEZONE=Europe/Madrid -TRUST_PROXY=0 +# Si necesitas SSL/TLS en MySQL (por defecto no) +DB_SSL=false +DB_SYNC_MODE=alter # none | alter | force + + + +# ─────────────────────────────── +# Logging de la app +# ─────────────────────────────── +LOG_LEVEL=info # error | warn | info | debug +LOG_JSON=false # true para logs en JSON (entornos con agregadores) + + +# ─────────────────────────────── +# Warmup por módulo +# ─────────────────────────────── +# Tiempo máximo para cada warmup() de un módulo, en milisegundos. +WARMUP_TIMEOUT_MS=10000 + +# Si es true, un fallo de warmup aborta el arranque. Si es false, continúa con warning. +WARMUP_STRICT=false + +# ─────────────────────────────── +# Seguridad / Auth +# ─────────────────────────────── JWT_SECRET=supersecretkey JWT_ACCESS_EXPIRATION=1h -JWT_REFRESH_EXPIRATION=7d \ No newline at end of file +JWT_REFRESH_EXPIRATION=7d + +# ─────────────────────────────── +# Otros (opcional / a futuro) +# ─────────────────────────────── diff --git a/apps/server/src/lib/modules/module-loader.ts b/apps/server/src/lib/modules/module-loader.ts index 232c83ca..f9c3033e 100644 --- a/apps/server/src/lib/modules/module-loader.ts +++ b/apps/server/src/lib/modules/module-loader.ts @@ -6,10 +6,18 @@ import { registerService } from "./service-registry"; const registeredModules: Map = new Map(); const initializedModules = new Set(); const visiting = new Set(); // para detección de ciclos +const initializationOrder: string[] = []; // orden de init → para warmup + +// Config opcional para warmup (valores por defecto seguros) +const WARMUP_TIMEOUT_MS = Number(process.env.WARMUP_TIMEOUT_MS) || 10_000; +const WARMUP_STRICT = + String(process.env.WARMUP_STRICT ?? "false") + .toLowerCase() + .trim() === "true"; /** - Registra un módulo del servidor en el registry. - Lanza error si el nombre ya existe. +Registra un módulo del servidor en el registry. +Lanza error si el nombre ya existe. */ export function registerModule(pkg: IModuleServer) { if (!pkg?.name) { @@ -23,20 +31,23 @@ export function registerModule(pkg: IModuleServer) { } /** - Inicializa todos los módulos registrados (resolviendo dependencias), - y al final inicializa los modelos (Sequelize) en bloque. +Inicializa todos los módulos registrados (resolviendo dependencias), +luego inicializa los modelos (Sequelize) en bloque y, por último, ejecuta warmups opcionales. */ export async function initModules(params: ModuleParams) { for (const name of registeredModules.keys()) { await loadModule(name, params, []); // secuencial para logs deterministas } + await withPhase("global", "initModels", async () => { await initModels(params); }); + + await warmupModules(params); } /** - Carga recursivamente un módulo y sus dependencias con detección de ciclos. +Carga recursivamente un módulo y sus dependencias con detección de ciclos. */ async function loadModule(name: string, params: ModuleParams, stack: string[]) { if (initializedModules.has(name)) return; @@ -88,30 +99,95 @@ async function loadModule(name: string, params: ModuleParams, stack: string[]) { } initializedModules.add(name); + initializationOrder.push(name); // recordamos el orden para warmup + visiting.delete(name); stack.pop(); logger.info(`✅ Module "${name}" registered`, { label: "moduleRegistry" }); } -/** - Helper para anotar fase y módulo en errores y logs. +/** +Ejecuta warmup() opcional de cada módulo en orden de inicialización. +Si WARMUP_STRICT=true, un fallo aborta el arranque; si no, se avisa y continúa. +*/ +async function warmupModules(params: ModuleParams) { + logger.info("🌡️ Warmup: starting...", { label: "moduleRegistry" }); + + for (const name of initializationOrder) { + const pkg = registeredModules.get(name); + if (!pkg) continue; + + const maybeWarmup = (pkg as unknown as { warmup?: (p: ModuleParams) => Promise | void }) + .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( moduleName: string, - phase: "init" | "registerDependencies" | "registerModels" | "registerServices" | "initModels", + phase: + | "init" + | "registerDependencies" + | "registerModels" + | "registerServices" + | "initModels" + | "warmup", fn: () => Promise | T ): Promise { + const startedAt = Date.now(); + logger.info(`▶️ phase=start module=${moduleName} ${phase}`, { + label: "moduleRegistry", + module: moduleName, + phase, + }); + try { - return await fn(); + const out = await fn(); + const duration = Date.now() - startedAt; + logger.info(`✅ phase=done module=${moduleName} ${phase} durationMs=${duration}`, { + label: "moduleRegistry", + module: moduleName, + phase, + durationMs: duration, + }); + return out; } catch (error: any) { - // Log enriquecido con contexto + const duration = Date.now() - startedAt; logger.error( - `⛔️ Module "${moduleName}" failed at phase="${phase}": ${error?.message ?? error}`, + `⛔️ Module "${moduleName}" failed at phase="${phase}" after ${duration}ms: ${error?.message ?? error}`, { label: "moduleRegistry", module: moduleName, phase, + durationMs: duration, stack: error?.stack, } ); @@ -123,3 +199,18 @@ async function withPhase( throw err; } } + +/** +Promesa con timeout; rechaza si excede el tiempo dado. +*/ +function withTimeout(p: Promise, ms: number, label: string): Promise { + let timer: NodeJS.Timeout | null = null; + const timeout = new Promise((_, 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; +} diff --git a/modules/auth/package.json b/modules/auth/package.json index 9c51bc42..679bfa1d 100644 --- a/modules/auth/package.json +++ b/modules/auth/package.json @@ -6,6 +6,13 @@ "./api": "./src/api/index.ts", "./client": "./src/web/index.ts" }, + "peerDependencies": { + "@erp/core": "workspace:*", + "dinero.js": "^1.9.1", + + "sequelize": "^6.37.5", + "zod": "^3.25.67" + }, "devDependencies": { "@biomejs/biome": "1.9.4", "@types/react": "^19.1.2", @@ -18,6 +25,7 @@ "@repo/rdx-ui": "workspace:*", "@repo/shadcn-ui": "workspace:*", "@tanstack/react-query": "^5.74.11", + "express": "^4.18.2", "i18next": "^25.1.1", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/modules/auth/src/api/index.ts b/modules/auth/src/api/index.ts index 0bc59d45..723891af 100644 --- a/modules/auth/src/api/index.ts +++ b/modules/auth/src/api/index.ts @@ -1,6 +1,6 @@ -import { IModuleServer, ModuleParams } from "@erp/core/api"; +export * from "./lib"; -export const authAPIModule: IModuleServer = { +/* export const authAPIModule: IModuleServer = { name: "auth", version: "1.0.0", dependencies: [], @@ -16,10 +16,10 @@ export const authAPIModule: IModuleServer = { return { //models, services: { - /*...*/ + }, }; }, }; -export default authAPIModule; +export default authAPIModule; */ diff --git a/modules/auth/src/api/lib/express/auth-types.ts b/modules/auth/src/api/lib/express/auth-types.ts new file mode 100644 index 00000000..dd98e65b --- /dev/null +++ b/modules/auth/src/api/lib/express/auth-types.ts @@ -0,0 +1,13 @@ +import { Request } from "express"; + +export type RequestUser = { + userId: string; + companyId: string; // tenant + roles?: string[]; + email?: string; +}; + +export type RequestWithAuth = Request & { + user: RequestUser; + body: T; +}; diff --git a/modules/auth/src/api/lib/express/index.ts b/modules/auth/src/api/lib/express/index.ts new file mode 100644 index 00000000..c53251a5 --- /dev/null +++ b/modules/auth/src/api/lib/express/index.ts @@ -0,0 +1,2 @@ +export * from "./auth-types"; +export * from "./tenancy.middleware"; diff --git a/modules/auth/src/api/lib/express/tenancy.middleware.ts b/modules/auth/src/api/lib/express/tenancy.middleware.ts new file mode 100644 index 00000000..78ffc527 --- /dev/null +++ b/modules/auth/src/api/lib/express/tenancy.middleware.ts @@ -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>( + criteria: T | undefined, + companyId: string +): T & { companyId: string } { + return { + ...(criteria ?? ({} as T)), + companyId, // fuerza el scope + }; +} diff --git a/modules/auth/src/api/lib/index.ts b/modules/auth/src/api/lib/index.ts new file mode 100644 index 00000000..6b5f6511 --- /dev/null +++ b/modules/auth/src/api/lib/index.ts @@ -0,0 +1 @@ +export * from "./express"; diff --git a/modules/core/src/api/infrastructure/express/express-controller.ts b/modules/core/src/api/infrastructure/express/express-controller.ts index 0f3f5eca..cb318d22 100644 --- a/modules/core/src/api/infrastructure/express/express-controller.ts +++ b/modules/core/src/api/infrastructure/express/express-controller.ts @@ -12,12 +12,33 @@ import { ValidationApiError, } from "../../errors"; +type GuardResultLike = { isFailure: boolean; error?: ApiError }; +export type GuardContext = { + req: Request; + res: Response; + next: NextFunction; + controller: ExpressController; + criteria: Criteria; +}; +export type GuardFn = (ctx: GuardContext) => GuardResultLike | Promise; + +export function guardOk(): GuardResultLike { + return { isFailure: false }; +} + +export function guardFail(error: ApiError): GuardResultLike { + return { isFailure: true, error }; +} + export abstract class ExpressController { - protected req!: Request; //| AuthenticatedRequest | TabContextRequest; + protected req!: Request; protected res!: Response; protected next!: NextFunction; protected criteria!: Criteria; + // 🔹 Guards configurables por controlador + private guards: GuardFn[] = []; + static errorResponse(apiError: ApiError, res: Response) { return res.status(apiError.status).json(apiError); } @@ -38,100 +59,111 @@ export abstract class ExpressController { return this.res.sendStatus(httpStatus.NO_CONTENT); } - /** - * Respuesta para errores de cliente (400 Bad Request) - */ protected clientError(message: string, errors?: any[] | any) { return ExpressController.errorResponse( new ValidationApiError(message, Array.isArray(errors) ? errors : [errors]), this.res ); } - - /** - * Respuesta para errores de autenticación (401 Unauthorized) - */ protected unauthorizedError(message?: string) { return ExpressController.errorResponse( new UnauthorizedApiError(message ?? "Unauthorized"), this.res ); } - /** - * Respuesta para errores de autorización (403 Forbidden) - */ protected forbiddenError(message?: string) { return ExpressController.errorResponse( new ForbiddenApiError(message ?? "You do not have permission to perform this action."), this.res ); } - - /** - * Respuesta para recursos no encontrados (404 Not Found) - */ protected notFoundError(message: string) { return ExpressController.errorResponse(new NotFoundApiError(message), this.res); } - - /** - * Respuesta para conflictos (409 Conflict) - */ - protected conflictError(message: string, errors?: any[]) { + protected conflictError(message: string, _errors?: any[]) { return ExpressController.errorResponse(new ConflictApiError(message), this.res); } - - /** - * Respuesta para errores de validación de entrada (422 Unprocessable Entity) - */ protected invalidInputError(message: string, errors?: any[]) { return ExpressController.errorResponse(new ValidationApiError(message, errors), this.res); } - - /** - * Respuesta para errores de servidor no disponible (503 Service Unavailable) - */ protected unavailableError(message?: string) { return ExpressController.errorResponse( new UnavailableApiError(message ?? "Service temporarily unavailable."), this.res ); } - /** - * Respuesta para errores internos del servidor (500 Internal Server Error) - */ protected internalServerError(message?: string) { return ExpressController.errorResponse( new InternalApiError(message ?? "Internal Server Error"), this.res ); } - - /** - * Respuesta para cualquier error de la API - */ protected handleApiError(apiError: ApiError) { return ExpressController.errorResponse(apiError, this.res); } - /** - * Método principal que se invoca desde el router de Express. - * Maneja la conversión de la URL a criterios y llama a executeImpl. - */ - public execute(req: Request, res: Response, next: NextFunction): void { + // ─────────────────────────────────────────────────────────────────────────── + // Guards API + protected useGuards(...guards: GuardFn[]): this { + this.guards.push(...guards); + return this; + } + + private async runGuards(): Promise { + 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 | undefined { + // Si usáis un tipo RequestWithAuth real, cámbialo aquí + return (this.req as any).user as T | undefined; + } + + public getTenantId(): string | undefined { + const user = this.getUser<{ companyId?: string }>(); + return user?.companyId; + } + + // ─────────────────────────────────────────────────────────────────────────── + // Método principal + public async execute(req: Request, res: Response, next: NextFunction): Promise { this.req = req; this.res = res; this.next = next; try { - const url = new URL(req.originalUrl, `http://${req.headers.host}`); + // Construcción robusta del URL base + const host = req.get("host") ?? "localhost"; + const proto = req.protocol || "http"; + const url = new URL(req.originalUrl, `${proto}://${host}`); + this.criteria = new CriteriaFromUrlConverter().toCriteria(url); - this.executeImpl(); + // Ejecutar guards (auth/tenant/otros). Si alguno falla, se responde y no se entra al impl. + const ok = await this.runGuards(); + if (!ok) return; + + await this.executeImpl(); } catch (error: unknown) { const err = error as Error; if (err instanceof ApiError) { - ExpressController.errorResponse(err, this.res); + ExpressController.errorResponse(err as ApiError, this.res); } else { ExpressController.errorResponse(new InternalApiError(err.message), this.res); } diff --git a/modules/core/src/api/infrastructure/express/express-guards.ts b/modules/core/src/api/infrastructure/express/express-guards.ts new file mode 100644 index 00000000..21d48da0 --- /dev/null +++ b/modules/core/src/api/infrastructure/express/express-guards.ts @@ -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(); + }; +} diff --git a/modules/core/src/api/infrastructure/express/index.ts b/modules/core/src/api/infrastructure/express/index.ts index 05f51dad..49108bdf 100644 --- a/modules/core/src/api/infrastructure/express/index.ts +++ b/modules/core/src/api/infrastructure/express/index.ts @@ -1,2 +1,3 @@ export * from "./express-controller"; +export * from "./express-guards"; export * from "./middlewares"; diff --git a/modules/core/src/api/infrastructure/sequelize/sequelize-repository.ts b/modules/core/src/api/infrastructure/sequelize/sequelize-repository.ts index aac2ddb9..1426dfb8 100644 --- a/modules/core/src/api/infrastructure/sequelize/sequelize-repository.ts +++ b/modules/core/src/api/infrastructure/sequelize/sequelize-repository.ts @@ -1,15 +1,9 @@ import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; import { IAggregateRootRepository, UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { FindOptions, ModelDefined, Sequelize, Transaction } from "sequelize"; +import { FindOptions, ModelDefined, Transaction } from "sequelize"; export abstract class SequelizeRepository implements IAggregateRootRepository { - protected readonly _database!: Sequelize; - - constructor(database: Sequelize) { - this._database = database; - } - protected convertCriteria(criteria: Criteria): FindOptions { return new CriteriaToSequelizeConverter().convert(criteria); } diff --git a/modules/core/src/api/infrastructure/sequelize/sequelize-transaction-manager.ts b/modules/core/src/api/infrastructure/sequelize/sequelize-transaction-manager.ts index a081054a..49135665 100644 --- a/modules/core/src/api/infrastructure/sequelize/sequelize-transaction-manager.ts +++ b/modules/core/src/api/infrastructure/sequelize/sequelize-transaction-manager.ts @@ -2,10 +2,12 @@ import { Sequelize, Transaction } from "sequelize"; import { TransactionManager } from "../database"; export class SequelizeTransactionManager extends TransactionManager { - protected _database: any | null = null; + protected _database: Sequelize | null = null; protected async _startTransaction(): Promise { - return await this._database.transaction(); + return await this._database!.transaction({ + isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED, + }); } protected async _commitTransaction(): Promise { diff --git a/modules/customer-invoices/package.json b/modules/customer-invoices/package.json index b3e07d1c..8d1de0ff 100644 --- a/modules/customer-invoices/package.json +++ b/modules/customer-invoices/package.json @@ -10,7 +10,6 @@ "./globals.css": "./src/web/globals.css" }, "peerDependencies": { - "@erp/core": "workspace:*", "dinero.js": "^1.9.1", "express": "^4.18.2", "sequelize": "^6.37.5", @@ -32,6 +31,8 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@erp/customers": "workspace:*", + "@erp/core": "workspace:*", + "@erp/auth": "workspace:*", "@hookform/resolvers": "^5.0.1", "@repo/rdx-criteria": "workspace:*", "@repo/rdx-ddd": "workspace:*", diff --git a/modules/customer-invoices/src/api/application/create-customer-invoice/presenter/create-customer-invoices.presenter.ts b/modules/customer-invoices/src/api/application/create-customer-invoice/assembler/create-customer-invoices.assembler.ts similarity index 77% rename from modules/customer-invoices/src/api/application/create-customer-invoice/presenter/create-customer-invoices.presenter.ts rename to modules/customer-invoices/src/api/application/create-customer-invoice/assembler/create-customer-invoices.assembler.ts index 8e1f881b..6e34ab7d 100644 --- a/modules/customer-invoices/src/api/application/create-customer-invoice/presenter/create-customer-invoices.presenter.ts +++ b/modules/customer-invoices/src/api/application/create-customer-invoice/assembler/create-customer-invoices.assembler.ts @@ -1,8 +1,8 @@ import { CustomerInvoice } from "@erp/customer-invoices/api/domain"; -import { CustomerInvoicesCreationResultDTO } from "@erp/customer-invoices/common/dto"; +import { CustomerInvoicesCreationResponseDTO } from "@erp/customer-invoices/common/dto"; -export class CreateCustomerInvoicesPresenter { - public toDTO(invoice: CustomerInvoice): CustomerInvoicesCreationResultDTO { +export class CreateCustomerInvoicesAssembler { + public toDTO(invoice: CustomerInvoice): CustomerInvoicesCreationResponseDTO { return { id: invoice.id.toPrimitive(), @@ -17,7 +17,7 @@ export class CreateCustomerInvoicesPresenter { //subtotal_price: invoice.calculateSubtotal().toPrimitive(), //total_price: invoice.calculateTotal().toPrimitive(), - //recipient: CustomerInvoiceParticipantPresenter(customerInvoice.recipient), + //recipient: CustomerInvoiceParticipantAssembler(customerInvoice.recipient), metadata: { entity: "customer-invoice", diff --git a/modules/customer-invoices/src/api/application/create-customer-invoice/assembler/index.ts b/modules/customer-invoices/src/api/application/create-customer-invoice/assembler/index.ts new file mode 100644 index 00000000..330393fe --- /dev/null +++ b/modules/customer-invoices/src/api/application/create-customer-invoice/assembler/index.ts @@ -0,0 +1 @@ +export * from "./create-customer-invoices.assembler"; diff --git a/modules/customer-invoices/src/api/application/create-customer-invoice/create-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/create-customer-invoice/create-customer-invoice.use-case.ts index c994523a..c45b5d1d 100644 --- a/modules/customer-invoices/src/api/application/create-customer-invoice/create-customer-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/create-customer-invoice/create-customer-invoice.use-case.ts @@ -1,19 +1,25 @@ import { DuplicateEntityError, ITransactionManager } from "@erp/core/api"; -import { CreateCustomerInvoiceCommandDTO } from "@erp/customer-invoices/common/dto"; +import { CreateCustomerInvoiceRequestDTO } from "@erp/customer-invoices/common/dto"; import { Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; import { ICustomerInvoiceService } from "../../domain"; import { mapDTOToCustomerInvoiceProps } from "../helpers"; -import { CreateCustomerInvoicesPresenter } from "./presenter"; +import { CreateCustomerInvoicesAssembler } from "./assembler"; + +type CreateCustomerInvoiceUseCaseInput = { + tenantId: string; + dto: CreateCustomerInvoiceRequestDTO; +}; export class CreateCustomerInvoiceUseCase { constructor( private readonly service: ICustomerInvoiceService, private readonly transactionManager: ITransactionManager, - private readonly presenter: CreateCustomerInvoicesPresenter + private readonly assembler: CreateCustomerInvoicesAssembler ) {} - public execute(dto: CreateCustomerInvoiceCommandDTO) { + public execute(params: CreateCustomerInvoiceUseCaseInput) { + const { dto, tenantId } = params; const invoicePropsOrError = mapDTOToCustomerInvoiceProps(dto); if (invoicePropsOrError.isFailure) { @@ -21,7 +27,6 @@ export class CreateCustomerInvoiceUseCase { } const { props, id } = invoicePropsOrError.data; - const invoiceOrError = this.service.build(props, id); if (invoiceOrError.isFailure) { @@ -47,7 +52,7 @@ export class CreateCustomerInvoiceUseCase { return Result.fail(result.error); } - const viewDTO = this.presenter.toDTO(newInvoice); + const viewDTO = this.assembler.toDTO(newInvoice); return Result.ok(viewDTO); } catch (error: unknown) { return Result.fail(error as Error); diff --git a/modules/customer-invoices/src/api/application/create-customer-invoice/index.ts b/modules/customer-invoices/src/api/application/create-customer-invoice/index.ts index 5420fda5..29bbecc9 100644 --- a/modules/customer-invoices/src/api/application/create-customer-invoice/index.ts +++ b/modules/customer-invoices/src/api/application/create-customer-invoice/index.ts @@ -1,2 +1,2 @@ +export * from "./assembler"; export * from "./create-customer-invoice.use-case"; -export * from "./presenter"; diff --git a/modules/customer-invoices/src/api/application/create-customer-invoice/presenter/index.ts b/modules/customer-invoices/src/api/application/create-customer-invoice/presenter/index.ts deleted file mode 100644 index 2ff37706..00000000 --- a/modules/customer-invoices/src/api/application/create-customer-invoice/presenter/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./create-customer-invoices.presenter"; diff --git a/modules/customer-invoices/src/api/application/delete-customer-invoice/delete-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/delete-customer-invoice/delete-customer-invoice.use-case.ts index fe3b8d9d..2d2d38ca 100644 --- a/modules/customer-invoices/src/api/application/delete-customer-invoice/delete-customer-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/delete-customer-invoice/delete-customer-invoice.use-case.ts @@ -1,27 +1,32 @@ import { EntityNotFoundError, ITransactionManager } from "@erp/core/api"; -import { DeleteCustomerInvoiceByIdQueryDTO } from "@erp/customer-invoices/common/dto"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { ICustomerInvoiceService } from "../../domain"; +type DeleteCustomerInvoiceUseCaseInput = { + tenantId: string; + id: string; +}; + export class DeleteCustomerInvoiceUseCase { constructor( private readonly service: ICustomerInvoiceService, private readonly transactionManager: ITransactionManager ) {} - public execute(dto: DeleteCustomerInvoiceByIdQueryDTO) { - const idOrError = UniqueID.create(dto.id); + public execute(params: DeleteCustomerInvoiceUseCaseInput) { + const { id, tenantId } = params; + const idOrError = UniqueID.create(id); if (idOrError.isFailure) { return Result.fail(idOrError.error); } - const id = idOrError.data; + const invoiceId = idOrError.data; return this.transactionManager.complete(async (transaction) => { try { - const existsCheck = await this.service.existsById(id, transaction); + const existsCheck = await this.service.existsById(invoiceId, transaction); if (existsCheck.isFailure) { return Result.fail(existsCheck.error); @@ -31,7 +36,7 @@ export class DeleteCustomerInvoiceUseCase { return Result.fail(new EntityNotFoundError("CustomerInvoice", id.toString())); } - return await this.service.deleteById(id, transaction); + return await this.service.deleteById(invoiceId, transaction); } catch (error: unknown) { return Result.fail(error as Error); } diff --git a/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceItem.presenter.ts.bak b/modules/customer-invoices/src/api/application/get-customer-invoice/assembler/InvoiceItem.presenter.ts.bak similarity index 92% rename from modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceItem.presenter.ts.bak rename to modules/customer-invoices/src/api/application/get-customer-invoice/assembler/InvoiceItem.presenter.ts.bak index 2ee2211c..a208bc97 100644 --- a/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceItem.presenter.ts.bak +++ b/modules/customer-invoices/src/api/application/get-customer-invoice/assembler/InvoiceItem.presenter.ts.bak @@ -2,7 +2,7 @@ import { CustomerInvoiceItem } from "#/server/domain"; import { IInvoicingContext } from "#/server/intrastructure"; import { Collection } from "@rdx/core"; -export const customerInvoiceItemPresenter = (items: Collection, context: IInvoicingContext) => +export const customerInvoiceItemAssembler = (items: Collection, context: IInvoicingContext) => items.totalCount > 0 ? items.items.map((item: CustomerInvoiceItem) => ({ description: item.description.toString(), diff --git a/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceParticipant.presenter.ts.bak b/modules/customer-invoices/src/api/application/get-customer-invoice/assembler/InvoiceParticipant.presenter.ts.bak similarity index 71% rename from modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceParticipant.presenter.ts.bak rename to modules/customer-invoices/src/api/application/get-customer-invoice/assembler/InvoiceParticipant.presenter.ts.bak index bc6cdebe..ba67e564 100644 --- a/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceParticipant.presenter.ts.bak +++ b/modules/customer-invoices/src/api/application/get-customer-invoice/assembler/InvoiceParticipant.presenter.ts.bak @@ -1,9 +1,9 @@ import { ICustomerInvoiceParticipant } from "@/contexts/invoicing/domain"; import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; import { ICreateCustomerInvoice_Participant_Response_DTO } from "@shared/contexts"; -import { CustomerInvoiceParticipantAddressPresenter } from "./CustomerInvoiceParticipantAddress.presenter"; +import { CustomerInvoiceParticipantAddressAssembler } from "./CustomerInvoiceParticipantAddress.assembler"; -export const CustomerInvoiceParticipantPresenter = async ( +export const CustomerInvoiceParticipantAssembler = async ( participant: ICustomerInvoiceParticipant, context: IInvoicingContext, ): Promise => { @@ -14,11 +14,11 @@ export const CustomerInvoiceParticipantPresenter = async ( last_name: participant.lastName.toString(), company_name: participant.companyName.toString(), - billing_address: await CustomerInvoiceParticipantAddressPresenter( + billing_address: await CustomerInvoiceParticipantAddressAssembler( participant.billingAddress!, context, ), - shipping_address: await CustomerInvoiceParticipantAddressPresenter( + shipping_address: await CustomerInvoiceParticipantAddressAssembler( participant.shippingAddress!, context, ), diff --git a/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak b/modules/customer-invoices/src/api/application/get-customer-invoice/assembler/InvoiceParticipantAddress.presenter.ts.bak similarity index 92% rename from modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak rename to modules/customer-invoices/src/api/application/get-customer-invoice/assembler/InvoiceParticipantAddress.presenter.ts.bak index b006790b..ac630261 100644 --- a/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak +++ b/modules/customer-invoices/src/api/application/get-customer-invoice/assembler/InvoiceParticipantAddress.presenter.ts.bak @@ -2,7 +2,7 @@ import { CustomerInvoiceParticipantAddress } from "@/contexts/invoicing/domain"; import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; import { ICreateCustomerInvoice_AddressParticipant_Response_DTO } from "@shared/contexts"; -export const CustomerInvoiceParticipantAddressPresenter = async ( +export const CustomerInvoiceParticipantAddressAssembler = async ( address: CustomerInvoiceParticipantAddress, context: IInvoicingContext, ): Promise => { diff --git a/modules/customer-invoices/src/api/application/get-customer-invoice/assembler/get-invoice.assembler.ts b/modules/customer-invoices/src/api/application/get-customer-invoice/assembler/get-invoice.assembler.ts new file mode 100644 index 00000000..95fff942 --- /dev/null +++ b/modules/customer-invoices/src/api/application/get-customer-invoice/assembler/get-invoice.assembler.ts @@ -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: "", + },*/ + }; + } +} diff --git a/modules/customer-invoices/src/api/application/get-customer-invoice/assembler/index.ts b/modules/customer-invoices/src/api/application/get-customer-invoice/assembler/index.ts new file mode 100644 index 00000000..346d890f --- /dev/null +++ b/modules/customer-invoices/src/api/application/get-customer-invoice/assembler/index.ts @@ -0,0 +1 @@ +export * from "./get-invoice.assembler"; diff --git a/modules/customer-invoices/src/api/application/get-customer-invoice/get-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/get-customer-invoice/get-customer-invoice.use-case.ts index cda1bc5a..c835b6c3 100644 --- a/modules/customer-invoices/src/api/application/get-customer-invoice/get-customer-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/get-customer-invoice/get-customer-invoice.use-case.ts @@ -1,19 +1,24 @@ import { ITransactionManager } from "@erp/core/api"; -import { GetCustomerInvoiceByIdQueryDTO } from "@erp/customer-invoices/common/dto"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { ICustomerInvoiceService } from "../../domain"; -import { GetCustomerInvoicePresenter } from "./presenter"; +import { GetCustomerInvoiceAssembler } from "./assembler"; + +type GetCustomerInvoiceUseCaseInput = { + tenantId: string; + id: string; +}; export class GetCustomerInvoiceUseCase { constructor( private readonly service: ICustomerInvoiceService, private readonly transactionManager: ITransactionManager, - private readonly presenter: GetCustomerInvoicePresenter + private readonly assembler: GetCustomerInvoiceAssembler ) {} - public execute(dto: GetCustomerInvoiceByIdQueryDTO) { - const idOrError = UniqueID.create(dto.id); + public execute(params: GetCustomerInvoiceUseCaseInput) { + const { id, tenantId } = params; + const idOrError = UniqueID.create(id); if (idOrError.isFailure) { return Result.fail(idOrError.error); @@ -26,7 +31,7 @@ export class GetCustomerInvoiceUseCase { return Result.fail(invoiceOrError.error); } - const getDTO = this.presenter.toDTO(invoiceOrError.data); + const getDTO = this.assembler.toDTO(invoiceOrError.data); return Result.ok(getDTO); } catch (error: unknown) { return Result.fail(error as Error); diff --git a/modules/customer-invoices/src/api/application/get-customer-invoice/index.ts b/modules/customer-invoices/src/api/application/get-customer-invoice/index.ts index b3f84d23..c8eebea6 100644 --- a/modules/customer-invoices/src/api/application/get-customer-invoice/index.ts +++ b/modules/customer-invoices/src/api/application/get-customer-invoice/index.ts @@ -1,2 +1,2 @@ +export * from "./assembler"; export * from "./get-customer-invoice.use-case"; -export * from "./presenter"; diff --git a/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/get-invoice.presenter.ts b/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/get-invoice.presenter.ts deleted file mode 100644 index 0ea9a9d7..00000000 --- a/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/get-invoice.presenter.ts +++ /dev/null @@ -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: "", - },*/ - }), -}; diff --git a/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/index.ts b/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/index.ts deleted file mode 100644 index 60624c19..00000000 --- a/modules/customer-invoices/src/api/application/get-customer-invoice/presenter/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./get-invoice.presenter"; diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/InvoiceParticipant.presenter.ts.bak b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/InvoiceParticipant.presenter.ts.bak similarity index 67% rename from modules/customer-invoices/src/api/application/list-customer-invoices/presenter/InvoiceParticipant.presenter.ts.bak rename to modules/customer-invoices/src/api/application/list-customer-invoices/assembler/InvoiceParticipant.presenter.ts.bak index c8c7ab3e..aacc3b31 100644 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/InvoiceParticipant.presenter.ts.bak +++ b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/InvoiceParticipant.presenter.ts.bak @@ -1,8 +1,8 @@ import { ICustomerInvoiceParticipant } from "@/contexts/invoicing/domain"; import { IListCustomerInvoice_Participant_Response_DTO } from "@shared/contexts"; -import { CustomerInvoiceParticipantAddressPresenter } from "./CustomerInvoiceParticipantAddress.presenter"; +import { CustomerInvoiceParticipantAddressAssembler } from "./CustomerInvoiceParticipantAddress.assembler"; -export const CustomerInvoiceParticipantPresenter = ( +export const CustomerInvoiceParticipantAssembler = ( participant: ICustomerInvoiceParticipant, ): IListCustomerInvoice_Participant_Response_DTO => { return { @@ -12,10 +12,10 @@ export const CustomerInvoiceParticipantPresenter = ( last_name: participant?.lastName?.toString(), company_name: participant?.companyName?.toString(), - billing_address: CustomerInvoiceParticipantAddressPresenter( + billing_address: CustomerInvoiceParticipantAddressAssembler( participant?.billingAddress!, ), - shipping_address: CustomerInvoiceParticipantAddressPresenter( + shipping_address: CustomerInvoiceParticipantAddressAssembler( participant?.shippingAddress!, ), }; diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/InvoiceParticipantAddress.presenter.ts.bak b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/InvoiceParticipantAddress.presenter.ts.bak similarity index 88% rename from modules/customer-invoices/src/api/application/list-customer-invoices/presenter/InvoiceParticipantAddress.presenter.ts.bak rename to modules/customer-invoices/src/api/application/list-customer-invoices/assembler/InvoiceParticipantAddress.presenter.ts.bak index 33c1a123..1e19158d 100644 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/InvoiceParticipantAddress.presenter.ts.bak +++ b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/InvoiceParticipantAddress.presenter.ts.bak @@ -1,4 +1,4 @@ -export const CustomerInvoiceParticipantAddressPresenter = ( +export const CustomerInvoiceParticipantAddressAssembler = ( address: CustomerInvoiceParticipantAddress ): IListCustomerInvoice_AddressParticipant_Response_DTO => { return { diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/index.ts b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/index.ts new file mode 100644 index 00000000..d4824855 --- /dev/null +++ b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/index.ts @@ -0,0 +1 @@ +export * from "./list-invoices.assembler"; diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/list-invoices.presenter.ts b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices.assembler.ts similarity index 75% rename from modules/customer-invoices/src/api/application/list-customer-invoices/presenter/list-invoices.presenter.ts rename to modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices.assembler.ts index f97f03e0..5660558a 100644 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/list-invoices.presenter.ts +++ b/modules/customer-invoices/src/api/application/list-customer-invoices/assembler/list-invoices.assembler.ts @@ -1,20 +1,13 @@ +import { CustomerInvoice } from "@erp/customer-invoices/api/domain"; +import { CustomerInvoiceListResponseDTO } from "@erp/customer-invoices/common/dto"; import { Criteria } from "@repo/rdx-criteria/server"; import { Collection } from "@repo/rdx-utils"; -import { CustomerInvoiceListResponseDTO } from "../../../../common/dto"; -import { CustomerInvoice } from "../../../domain"; -export interface ListCustomerInvoicesPresenter { - toDTO: ( +export class ListCustomerInvoicesAssembler { + toDTO( customerInvoices: Collection, criteria: Criteria - ) => CustomerInvoiceListResponseDTO; -} - -export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = { - toDTO: ( - customerInvoices: Collection, - criteria: Criteria - ): CustomerInvoiceListResponseDTO => { + ): CustomerInvoiceListResponseDTO { const items = customerInvoices.map((invoice) => { return { id: invoice.id.toPrimitive(), @@ -30,7 +23,7 @@ export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = { subtotal_price: invoice.calculateSubtotal().toPrimitive(), total_price: invoice.calculateTotal().toPrimitive(), - //recipient: CustomerInvoiceParticipantPresenter(customerInvoice.recipient), + //recipient: CustomerInvoiceParticipantAssembler(customerInvoice.recipient), metadata: { entity: "customer-invoice", @@ -56,5 +49,5 @@ export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = { //}, }, }; - }, -}; + } +} diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/index.ts b/modules/customer-invoices/src/api/application/list-customer-invoices/index.ts index af0c7f6e..abe60774 100644 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/index.ts +++ b/modules/customer-invoices/src/api/application/list-customer-invoices/index.ts @@ -1 +1,2 @@ +export * from "./assembler"; export * from "./list-customer-invoices.use-case"; diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/list-customer-invoices.use-case.ts b/modules/customer-invoices/src/api/application/list-customer-invoices/list-customer-invoices.use-case.ts index dfff3f1c..52955ac7 100644 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/list-customer-invoices.use-case.ts +++ b/modules/customer-invoices/src/api/application/list-customer-invoices/list-customer-invoices.use-case.ts @@ -4,16 +4,25 @@ import { Criteria } from "@repo/rdx-criteria/server"; import { Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; import { ICustomerInvoiceService } from "../../domain"; -import { ListCustomerInvoicesPresenter } from "./presenter"; +import { ListCustomerInvoicesAssembler } from "./assembler"; + +type ListCustomerInvoicesUseCaseInput = { + tenantId: string; + criteria: Criteria; +}; export class ListCustomerInvoicesUseCase { constructor( private readonly customerInvoiceService: ICustomerInvoiceService, private readonly transactionManager: ITransactionManager, - private readonly presenter: ListCustomerInvoicesPresenter + private readonly assembler: ListCustomerInvoicesAssembler ) {} - public execute(criteria: Criteria): Promise> { + public execute( + params: ListCustomerInvoicesUseCaseInput + ): Promise> { + const { criteria, tenantId } = params; + return this.transactionManager.complete(async (transaction: Transaction) => { try { const result = await this.customerInvoiceService.findByCriteria(criteria, transaction); @@ -22,7 +31,7 @@ export class ListCustomerInvoicesUseCase { return Result.fail(result.error); } - const dto: CustomerInvoiceListResponseDTO = this.presenter.toDTO(result.data, criteria); + const dto: CustomerInvoiceListResponseDTO = this.assembler.toDTO(result.data, criteria); return Result.ok(dto); } catch (error: unknown) { return Result.fail(error as Error); diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/index.ts b/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/index.ts deleted file mode 100644 index 9ecb5c89..00000000 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/presenter/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./list-invoices.presenter"; diff --git a/modules/customer-invoices/src/api/application/update-customer-invoice/assembler/index.ts b/modules/customer-invoices/src/api/application/update-customer-invoice/assembler/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/modules/customer-invoices/src/api/application/update-customer-invoice/index.ts b/modules/customer-invoices/src/api/application/update-customer-invoice/index.ts index 002aceac..588c5eda 100644 --- a/modules/customer-invoices/src/api/application/update-customer-invoice/index.ts +++ b/modules/customer-invoices/src/api/application/update-customer-invoice/index.ts @@ -1 +1,2 @@ +export * from "./assembler"; export * from "./update-customer-invoice.use-case"; diff --git a/modules/customer-invoices/src/api/controllers/create-customer-invoice/create-customer-invoice.ts b/modules/customer-invoices/src/api/controllers/create-customer-invoice/create-customer-invoice.ts deleted file mode 100644 index 437dcef6..00000000 --- a/modules/customer-invoices/src/api/controllers/create-customer-invoice/create-customer-invoice.ts +++ /dev/null @@ -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); - } -} diff --git a/modules/customer-invoices/src/api/controllers/create-customer-invoice/index.ts b/modules/customer-invoices/src/api/controllers/create-customer-invoice/index.ts deleted file mode 100644 index 7166c0a3..00000000 --- a/modules/customer-invoices/src/api/controllers/create-customer-invoice/index.ts +++ /dev/null @@ -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); -}; diff --git a/modules/customer-invoices/src/api/controllers/delete-customer-invoice/delete-invoice.controller.ts b/modules/customer-invoices/src/api/controllers/delete-customer-invoice/delete-invoice.controller.ts deleted file mode 100644 index 92cbe23f..00000000 --- a/modules/customer-invoices/src/api/controllers/delete-customer-invoice/delete-invoice.controller.ts +++ /dev/null @@ -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 { - 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); - } -} diff --git a/modules/customer-invoices/src/api/controllers/delete-customer-invoice/index.ts b/modules/customer-invoices/src/api/controllers/delete-customer-invoice/index.ts deleted file mode 100644 index 32dc9574..00000000 --- a/modules/customer-invoices/src/api/controllers/delete-customer-invoice/index.ts +++ /dev/null @@ -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); -}; diff --git a/modules/customer-invoices/src/api/controllers/get-customer-invoice/get-invoice.controller.ts b/modules/customer-invoices/src/api/controllers/get-customer-invoice/get-invoice.controller.ts deleted file mode 100644 index fdff70f3..00000000 --- a/modules/customer-invoices/src/api/controllers/get-customer-invoice/get-invoice.controller.ts +++ /dev/null @@ -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); - } -} diff --git a/modules/customer-invoices/src/api/controllers/get-customer-invoice/index.ts b/modules/customer-invoices/src/api/controllers/get-customer-invoice/index.ts deleted file mode 100644 index 7aa6f3bd..00000000 --- a/modules/customer-invoices/src/api/controllers/get-customer-invoice/index.ts +++ /dev/null @@ -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); -}; diff --git a/modules/customer-invoices/src/api/controllers/index.ts b/modules/customer-invoices/src/api/controllers/index.ts deleted file mode 100644 index fbd5e70d..00000000 --- a/modules/customer-invoices/src/api/controllers/index.ts +++ /dev/null @@ -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"; diff --git a/modules/customer-invoices/src/api/controllers/list-customer-invoices/index.ts b/modules/customer-invoices/src/api/controllers/list-customer-invoices/index.ts deleted file mode 100644 index cc89304f..00000000 --- a/modules/customer-invoices/src/api/controllers/list-customer-invoices/index.ts +++ /dev/null @@ -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); -}; diff --git a/modules/customer-invoices/src/api/controllers/list-customer-invoices/list-customer-invoices.controller.ts b/modules/customer-invoices/src/api/controllers/list-customer-invoices/list-customer-invoices.controller.ts deleted file mode 100644 index daf9b7c0..00000000 --- a/modules/customer-invoices/src/api/controllers/list-customer-invoices/list-customer-invoices.controller.ts +++ /dev/null @@ -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); - } -} diff --git a/modules/customer-invoices/src/api/controllers/update-customer-invoice/index.ts.bak b/modules/customer-invoices/src/api/controllers/update-customer-invoice/index.ts.bak deleted file mode 100644 index 0cc6c60e..00000000 --- a/modules/customer-invoices/src/api/controllers/update-customer-invoice/index.ts.bak +++ /dev/null @@ -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 - ); -}; diff --git a/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/InvoiceItem.presenter.ts.bak b/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/InvoiceItem.presenter.ts.bak deleted file mode 100644 index d082b387..00000000 --- a/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/InvoiceItem.presenter.ts.bak +++ /dev/null @@ -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, - 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, - })) - : []; diff --git a/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/InvoiceParticipant.presenter.ts.bak b/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/InvoiceParticipant.presenter.ts.bak deleted file mode 100644 index 00457233..00000000 --- a/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/InvoiceParticipant.presenter.ts.bak +++ /dev/null @@ -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, - ), - }; -}; diff --git a/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak b/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak deleted file mode 100644 index eeede21e..00000000 --- a/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/InvoiceParticipantAddress.presenter.ts.bak +++ /dev/null @@ -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(), - }; -}; diff --git a/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/UpdateInvoice.presenter.ts.bak b/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/UpdateInvoice.presenter.ts.bak deleted file mode 100644 index eb3086ce..00000000 --- a/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/UpdateInvoice.presenter.ts.bak +++ /dev/null @@ -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), - }; - }, -}; diff --git a/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/index.ts.bak b/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/index.ts.bak deleted file mode 100644 index 392a8a88..00000000 --- a/modules/customer-invoices/src/api/controllers/update-customer-invoice/presenter/index.ts.bak +++ /dev/null @@ -1 +0,0 @@ -export * from "./UpdateCustomerInvoice.presenter"; diff --git a/modules/customer-invoices/src/api/infrastructure/Contact.repository.ts.bak b/modules/customer-invoices/src/api/infrastructure/Contact.repository.ts.bak deleted file mode 100644 index f85f57c7..00000000 --- a/modules/customer-invoices/src/api/infrastructure/Contact.repository.ts.bak +++ /dev/null @@ -1,64 +0,0 @@ -import { SequelizeRepository } from "@/core"; -import { Transaction } from "sequelize"; - -export class ContactRepository extends SequelizeRepository 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 { - 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 { - return this._exists("Customer", "id", id.toString()); - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/dependencies.ts b/modules/customer-invoices/src/api/infrastructure/dependencies.ts new file mode 100644 index 00000000..fb7d5cf6 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/dependencies.ts @@ -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: (res: Response) => ListPresenter; + }; +}; + +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: (res: Response) => createListPresenter(res), + //json: (res: Response, status: number = 200) => createJsonPresenter(res, status), + }, + }; +} diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/create-customer-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/create-customer-invoice.controller.ts new file mode 100644 index 00000000..30f76eba --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/create-customer-invoice.controller.ts @@ -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)) + ); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/delete-customer-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/delete-customer-invoice.controller.ts new file mode 100644 index 00000000..b84cd3db --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/delete-customer-invoice.controller.ts @@ -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)) + ); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/get-customer-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/get-customer-invoice.controller.ts new file mode 100644 index 00000000..41c95645 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/get-customer-invoice.controller.ts @@ -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)) + ); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/index.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/index.ts new file mode 100644 index 00000000..99bd883f --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/index.ts @@ -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"; diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/list-customer-invoices.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/list-customer-invoices.controller.ts new file mode 100644 index 00000000..ddadeaa9 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/list-customer-invoices.controller.ts @@ -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)) + ); + } +} diff --git a/modules/customer-invoices/src/api/controllers/update-customer-invoice/update-invoice.controller.ts.bak b/modules/customer-invoices/src/api/infrastructure/express/controllers/update-invoice.controller.ts.bak similarity index 100% rename from modules/customer-invoices/src/api/controllers/update-customer-invoice/update-invoice.controller.ts.bak rename to modules/customer-invoices/src/api/infrastructure/express/controllers/update-invoice.controller.ts.bak diff --git a/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts index 8616be48..c5b07685 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts @@ -1,3 +1,4 @@ +import { RequestWithAuth, enforceTenant } from "@erp/auth/api"; import { ILogger, ModuleParams, validateRequest } from "@erp/core/api"; import { Application, NextFunction, Request, Response, Router } from "express"; import { Sequelize } from "sequelize"; @@ -7,12 +8,13 @@ import { DeleteCustomerInvoiceByIdRequestSchema, GetCustomerInvoiceByIdRequestSchema, } from "../../../common/dto"; +import { getInvoiceDependencies } from "../dependencies"; import { - buildCreateCustomerInvoicesController, - buildDeleteCustomerInvoiceController, - buildGetCustomerInvoiceController, - buildListCustomerInvoicesController, -} from "../../controllers"; + CreateCustomerInvoiceController, + DeleteCustomerInvoiceController, + GetCustomerInvoiceController, + ListCustomerInvoicesController, +} from "./controllers"; export const customerInvoicesRouter = (params: ModuleParams) => { const { app, database, baseRoutePath, logger } = params as { @@ -22,35 +24,43 @@ export const customerInvoicesRouter = (params: ModuleParams) => { logger: ILogger; }; - const routes: Router = Router({ mergeParams: true }); + const router: Router = Router({ mergeParams: true }); + const deps = getInvoiceDependencies(params); - routes.get( + // 🔐 Autenticación + Tenancy para TODO el router + router.use(/* authenticateJWT(), */ enforceTenant() /*checkTabContext*/); + + router.get( "/", //checkTabContext, - //checkUser, validateRequest(CustomerInvoiceListRequestSchema, "params"), - (req: Request, res: Response, next: NextFunction) => { - buildListCustomerInvoicesController(database).execute(req, res, next); + async (req: RequestWithAuth, res: Response, next: NextFunction) => { + const useCase = deps.build.list(); + const controller = new ListCustomerInvoicesController(useCase /*, deps.presenters.list */); + return controller.execute(req, res, next); } ); - routes.get( + router.get( "/:id", //checkTabContext, - //checkUser, validateRequest(GetCustomerInvoiceByIdRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - buildGetCustomerInvoiceController(database).execute(req, res, next); + const useCase = deps.build.get(); + const controller = new GetCustomerInvoiceController(useCase); + return controller.execute(req, res, next); } ); - routes.post( + router.post( "/", //checkTabContext, - //checkUser, + validateRequest(CreateCustomerInvoiceRequestSchema), (req: Request, res: Response, next: NextFunction) => { - buildCreateCustomerInvoicesController(database).execute(req, res, next); + const useCase = deps.build.create(); + const controller = new CreateCustomerInvoiceController(useCase); + return controller.execute(req, res, next); } ); @@ -58,21 +68,23 @@ export const customerInvoicesRouter = (params: ModuleParams) => { "/:customerInvoiceId", validateAndParseBody(IUpdateCustomerInvoiceRequestSchema), checkTabContext, - //checkUser, + (req: Request, res: Response, next: NextFunction) => { buildUpdateCustomerInvoiceController().execute(req, res, next); } );*/ - routes.delete( + router.delete( "/:id", //checkTabContext, - //checkUser, + validateRequest(DeleteCustomerInvoiceByIdRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - buildDeleteCustomerInvoiceController(database).execute(req, res, next); + const useCase = deps.build.delete(); + const controller = new DeleteCustomerInvoiceController(useCase); + return controller.execute(req, res, next); } ); - app.use(`${baseRoutePath}/customer-invoices`, routes); + app.use(`${baseRoutePath}/customer-invoices`, router); }; diff --git a/modules/customer-invoices/src/api/infrastructure/express/presenter/list-customer-invoices.presenter.ts b/modules/customer-invoices/src/api/infrastructure/express/presenter/list-customer-invoices.presenter.ts new file mode 100644 index 00000000..b211feb8 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/express/presenter/list-customer-invoices.presenter.ts @@ -0,0 +1,56 @@ +import { Response } from "express"; + +export type ListResult = { + items: T[]; + total: number; + limit: number; + offset: number; +}; + +export type ListPresenterOptions = { + includeMetaInBody?: boolean; // por defecto false (solo items en body) +}; + +export class ListPresenter { + 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) { + 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(res: Response, opts?: ListPresenterOptions) { + return new ListPresenter(res, opts); +} diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts index 2f6623eb..50859ade 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts @@ -107,6 +107,3 @@ export class CustomerInvoiceMapper }; } } - -const customerInvoiceMapper: CustomerInvoiceMapper = new CustomerInvoiceMapper(); -export { customerInvoiceMapper }; diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-item.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-item.model.ts index 750db504..8fa5426e 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-item.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-item.model.ts @@ -45,13 +45,14 @@ export class CustomerInvoiceItemModel extends Model< declare invoice: NonAttribute; static associate(database: Sequelize) { - /*const { Invoice_Model, CustomerInvoiceItem_Model } = connection.models; + const { Invoice_Model, CustomerInvoiceItem_Model } = connection.models; CustomerInvoiceItem_Model.belongsTo(Invoice_Model, { as: "customerInvoice", + targetKey: "id", foreignKey: "invoice_id", onDelete: "CASCADE", - });*/ + }); } } @@ -159,6 +160,7 @@ export default (database: Sequelize) => { }, { sequelize: database, + underscored: true, tableName: "customer_invoice_items", defaultScope: {}, diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.model.ts index b1e9460a..9da987fa 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.model.ts @@ -51,7 +51,21 @@ export class CustomerInvoiceModel extends Model< CustomerInvoiceModel.hasMany(CustomerInvoiceItemModel, { as: "items", foreignKey: "invoice_id", + sourceKey: "id", onDelete: "CASCADE", + constraints: true, + }); + } + + static hooks(database: Sequelize) { + // Soft-cascade manual: al borrar una factura, marcamos items como borrados (paranoid). + CustomerInvoiceModel.addHook("afterDestroy", async (invoice, options) => { + if (!invoice?.id) return; + await CustomerInvoiceItemModel.destroy({ + where: { invoiceId: invoice.id }, + individualHooks: true, + transaction: options.transaction, + }); }); } } @@ -129,6 +143,7 @@ export default (database: Sequelize) => { sequelize: database, tableName: "customer_invoices", + underscored: true, paranoid: true, // softs deletes timestamps: true, diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts index 91fe2677..d89acbf9 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts @@ -2,7 +2,7 @@ import { SequelizeRepository, errorMapper } from "@erp/core/api"; import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; import { UniqueID } from "@repo/rdx-ddd"; import { Collection, Result } from "@repo/rdx-utils"; -import { Sequelize, Transaction } from "sequelize"; +import { Transaction } from "sequelize"; import { CustomerInvoice, ICustomerInvoiceRepository } from "../../domain"; import { ICustomerInvoiceMapper } from "../mappers/customer-invoice.mapper"; import { CustomerInvoiceModel } from "./customer-invoice.model"; @@ -14,10 +14,8 @@ export class CustomerInvoiceRepository //private readonly model: typeof CustomerInvoiceModel; private readonly mapper!: ICustomerInvoiceMapper; - constructor(database: Sequelize, mapper: ICustomerInvoiceMapper) { - super(database); - - //CustomerInvoice = database.model("CustomerInvoice") as typeof CustomerInvoiceModel; + constructor(mapper: ICustomerInvoiceMapper) { + super(); this.mapper = mapper; } diff --git a/modules/customers/src/api/application/create-customer/presenter/create-customers.presenter.ts b/modules/customers/src/api/application/create-customer/assembler/create-customers.assembler.ts similarity index 88% rename from modules/customers/src/api/application/create-customer/presenter/create-customers.presenter.ts rename to modules/customers/src/api/application/create-customer/assembler/create-customers.assembler.ts index 63fb4676..c2713d55 100644 --- a/modules/customers/src/api/application/create-customer/presenter/create-customers.presenter.ts +++ b/modules/customers/src/api/application/create-customer/assembler/create-customers.assembler.ts @@ -1,7 +1,7 @@ import { Customer } from "@erp/customers/api/domain"; import { CustomersCreationResultDTO } from "@erp/customers/common/dto"; -export class CreateCustomersPresenter { +export class CreateCustomersAssembler { public toDTO(invoice: Customer): CustomersCreationResultDTO { return { id: invoice.id.toPrimitive(), @@ -17,7 +17,7 @@ export class CreateCustomersPresenter { //subtotal_price: invoice.calculateSubtotal().toPrimitive(), //total_price: invoice.calculateTotal().toPrimitive(), - //recipient: CustomerParticipantPresenter(customer.recipient), + //recipient: CustomerParticipantAssembler(customer.recipient), metadata: { entity: "customer", diff --git a/modules/customers/src/api/application/create-customer/assembler/index.ts b/modules/customers/src/api/application/create-customer/assembler/index.ts new file mode 100644 index 00000000..b384226f --- /dev/null +++ b/modules/customers/src/api/application/create-customer/assembler/index.ts @@ -0,0 +1 @@ +export * from "./create-customers.assembler"; diff --git a/modules/customers/src/api/application/create-customer/create-customer.use-case.ts b/modules/customers/src/api/application/create-customer/create-customer.use-case.ts index 33c71e2f..400b4720 100644 --- a/modules/customers/src/api/application/create-customer/create-customer.use-case.ts +++ b/modules/customers/src/api/application/create-customer/create-customer.use-case.ts @@ -4,13 +4,13 @@ import { Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; import { ICustomerService } from "../../domain"; import { mapDTOToCustomerProps } from "../helpers"; -import { CreateCustomersPresenter } from "./presenter"; +import { CreateCustomersAssembler } from "./assembler"; export class CreateCustomerUseCase { constructor( private readonly service: ICustomerService, private readonly transactionManager: ITransactionManager, - private readonly presenter: CreateCustomersPresenter + private readonly assembler: CreateCustomersAssembler ) {} public execute(dto: CreateCustomerCommandDTO) { @@ -47,7 +47,7 @@ export class CreateCustomerUseCase { return Result.fail(result.error); } - const viewDTO = this.presenter.toDTO(newInvoice); + const viewDTO = this.assembler.toDTO(newInvoice); return Result.ok(viewDTO); } catch (error: unknown) { return Result.fail(error as Error); diff --git a/modules/customers/src/api/application/create-customer/index.ts b/modules/customers/src/api/application/create-customer/index.ts index 12c6e7bf..045226f6 100644 --- a/modules/customers/src/api/application/create-customer/index.ts +++ b/modules/customers/src/api/application/create-customer/index.ts @@ -1,2 +1,2 @@ +export * from "./assembler"; export * from "./create-customer.use-case"; -export * from "./presenter"; diff --git a/modules/customers/src/api/application/create-customer/presenter/index.ts b/modules/customers/src/api/application/create-customer/presenter/index.ts deleted file mode 100644 index e63b640f..00000000 --- a/modules/customers/src/api/application/create-customer/presenter/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./create-customers.presenter"; diff --git a/modules/customers/src/api/application/get-customer/presenter/InvoiceItem.presenter.ts.bak b/modules/customers/src/api/application/get-customer/assembler/InvoiceItem.presenter.ts.bak similarity index 91% rename from modules/customers/src/api/application/get-customer/presenter/InvoiceItem.presenter.ts.bak rename to modules/customers/src/api/application/get-customer/assembler/InvoiceItem.presenter.ts.bak index 3a6e2c6e..34672cad 100644 --- a/modules/customers/src/api/application/get-customer/presenter/InvoiceItem.presenter.ts.bak +++ b/modules/customers/src/api/application/get-customer/assembler/InvoiceItem.presenter.ts.bak @@ -2,7 +2,7 @@ import { CustomerItem } from "#/server/domain"; import { IInvoicingContext } from "#/server/intrastructure"; import { Collection } from "@rdx/core"; -export const customerItemPresenter = (items: Collection, context: IInvoicingContext) => +export const customerItemAssembler = (items: Collection, context: IInvoicingContext) => items.totalCount > 0 ? items.items.map((item: CustomerItem) => ({ description: item.description.toString(), diff --git a/modules/customers/src/api/application/get-customer/presenter/InvoiceParticipant.presenter.ts.bak b/modules/customers/src/api/application/get-customer/assembler/InvoiceParticipant.presenter.ts.bak similarity index 72% rename from modules/customers/src/api/application/get-customer/presenter/InvoiceParticipant.presenter.ts.bak rename to modules/customers/src/api/application/get-customer/assembler/InvoiceParticipant.presenter.ts.bak index d10d3001..25772cf5 100644 --- a/modules/customers/src/api/application/get-customer/presenter/InvoiceParticipant.presenter.ts.bak +++ b/modules/customers/src/api/application/get-customer/assembler/InvoiceParticipant.presenter.ts.bak @@ -1,9 +1,9 @@ import { ICustomerParticipant } from "@/contexts/invoicing/domain"; import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; import { ICreateCustomer_Participant_Response_DTO } from "@shared/contexts"; -import { CustomerParticipantAddressPresenter } from "./CustomerParticipantAddress.presenter"; +import { CustomerParticipantAddressAssembler } from "./CustomerParticipantAddress.assembler"; -export const CustomerParticipantPresenter = async ( +export const CustomerParticipantAssembler = async ( participant: ICustomerParticipant, context: IInvoicingContext, ): Promise => { @@ -14,11 +14,11 @@ export const CustomerParticipantPresenter = async ( last_name: participant.lastName.toString(), company_name: participant.companyName.toString(), - billing_address: await CustomerParticipantAddressPresenter( + billing_address: await CustomerParticipantAddressAssembler( participant.billingAddress!, context, ), - shipping_address: await CustomerParticipantAddressPresenter( + shipping_address: await CustomerParticipantAddressAssembler( participant.shippingAddress!, context, ), diff --git a/modules/customers/src/api/application/get-customer/presenter/InvoiceParticipantAddress.presenter.ts.bak b/modules/customers/src/api/application/get-customer/assembler/InvoiceParticipantAddress.presenter.ts.bak similarity index 92% rename from modules/customers/src/api/application/get-customer/presenter/InvoiceParticipantAddress.presenter.ts.bak rename to modules/customers/src/api/application/get-customer/assembler/InvoiceParticipantAddress.presenter.ts.bak index 6266fda0..1575a366 100644 --- a/modules/customers/src/api/application/get-customer/presenter/InvoiceParticipantAddress.presenter.ts.bak +++ b/modules/customers/src/api/application/get-customer/assembler/InvoiceParticipantAddress.presenter.ts.bak @@ -2,7 +2,7 @@ import { CustomerParticipantAddress } from "@/contexts/invoicing/domain"; import { IInvoicingContext } from "@/contexts/invoicing/intrastructure/InvoicingContext"; import { ICreateCustomer_AddressParticipant_Response_DTO } from "@shared/contexts"; -export const CustomerParticipantAddressPresenter = async ( +export const CustomerParticipantAddressAssembler = async ( address: CustomerParticipantAddress, context: IInvoicingContext, ): Promise => { diff --git a/modules/customers/src/api/application/get-customer/assembler/get-invoice.assembler.ts b/modules/customers/src/api/application/get-customer/assembler/get-invoice.assembler.ts new file mode 100644 index 00000000..df4c01df --- /dev/null +++ b/modules/customers/src/api/application/get-customer/assembler/get-invoice.assembler.ts @@ -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: "", + },*/ + }; + } +} diff --git a/modules/customers/src/api/application/get-customer/assembler/index.ts b/modules/customers/src/api/application/get-customer/assembler/index.ts new file mode 100644 index 00000000..346d890f --- /dev/null +++ b/modules/customers/src/api/application/get-customer/assembler/index.ts @@ -0,0 +1 @@ +export * from "./get-invoice.assembler"; diff --git a/modules/customers/src/api/application/get-customer/get-customer.use-case.ts b/modules/customers/src/api/application/get-customer/get-customer.use-case.ts index f21551a5..701f89d6 100644 --- a/modules/customers/src/api/application/get-customer/get-customer.use-case.ts +++ b/modules/customers/src/api/application/get-customer/get-customer.use-case.ts @@ -3,13 +3,13 @@ import { GetCustomerByIdQueryDTO } from "@erp/customers/common/dto"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { ICustomerService } from "../../domain"; -import { GetCustomerPresenter } from "./presenter"; +import { GetCustomerAssembler } from "./assembler"; export class GetCustomerUseCase { constructor( private readonly service: ICustomerService, private readonly transactionManager: ITransactionManager, - private readonly presenter: GetCustomerPresenter + private readonly assembler: GetCustomerAssembler ) {} public execute(dto: GetCustomerByIdQueryDTO) { @@ -26,7 +26,7 @@ export class GetCustomerUseCase { return Result.fail(invoiceOrError.error); } - const getDTO = this.presenter.toDTO(invoiceOrError.data); + const getDTO = this.assembler.toDTO(invoiceOrError.data); return Result.ok(getDTO); } catch (error: unknown) { return Result.fail(error as Error); diff --git a/modules/customers/src/api/application/get-customer/index.ts b/modules/customers/src/api/application/get-customer/index.ts index cc3e68e8..86a0415b 100644 --- a/modules/customers/src/api/application/get-customer/index.ts +++ b/modules/customers/src/api/application/get-customer/index.ts @@ -1,2 +1,2 @@ +export * from "./assembler"; export * from "./get-customer.use-case"; -export * from "./presenter"; diff --git a/modules/customers/src/api/application/get-customer/presenter/get-invoice.presenter.ts b/modules/customers/src/api/application/get-customer/presenter/get-invoice.presenter.ts deleted file mode 100644 index a312c847..00000000 --- a/modules/customers/src/api/application/get-customer/presenter/get-invoice.presenter.ts +++ /dev/null @@ -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: "", - },*/ - }), -}; diff --git a/modules/customers/src/api/application/get-customer/presenter/index.ts b/modules/customers/src/api/application/get-customer/presenter/index.ts deleted file mode 100644 index 60624c19..00000000 --- a/modules/customers/src/api/application/get-customer/presenter/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./get-invoice.presenter"; diff --git a/modules/customers/src/api/application/list-customers/assembler/index.ts b/modules/customers/src/api/application/list-customers/assembler/index.ts new file mode 100644 index 00000000..d4824855 --- /dev/null +++ b/modules/customers/src/api/application/list-customers/assembler/index.ts @@ -0,0 +1 @@ +export * from "./list-invoices.assembler"; diff --git a/modules/customers/src/api/application/list-customers/presenter/list-invoices.presenter.ts b/modules/customers/src/api/application/list-customers/assembler/list-invoices.assembler.ts similarity index 81% rename from modules/customers/src/api/application/list-customers/presenter/list-invoices.presenter.ts rename to modules/customers/src/api/application/list-customers/assembler/list-invoices.assembler.ts index 6c4770b6..0270b704 100644 --- a/modules/customers/src/api/application/list-customers/presenter/list-invoices.presenter.ts +++ b/modules/customers/src/api/application/list-customers/assembler/list-invoices.assembler.ts @@ -3,12 +3,9 @@ import { Collection } from "@repo/rdx-utils"; import { CustomerListResponsetDTO } from "../../../../common/dto"; import { Customer } from "../../../domain"; -export interface ListCustomersPresenter { - toDTO: (customers: Collection, criteria: Criteria) => CustomerListResponsetDTO; -} -export const listCustomersPresenter: ListCustomersPresenter = { - toDTO: (customers: Collection, criteria: Criteria): CustomerListResponsetDTO => { +export class ListCustomersAssembler { + toDTO(customers: Collection, criteria: Criteria): CustomerListResponsetDTO { const items = customers.map((invoice) => { return { id: invoice.id.toPrimitive(), @@ -24,7 +21,7 @@ export const listCustomersPresenter: ListCustomersPresenter = { subtotal_price: invoice.calculateSubtotal().toPrimitive(), total_price: invoice.calculateTotal().toPrimitive(), - //recipient: CustomerParticipantPresenter(customer.recipient), + //recipient: CustomerParticipantAssembler(customer.recipient), metadata: { entity: "customer", diff --git a/modules/customers/src/api/application/list-customers/index.ts b/modules/customers/src/api/application/list-customers/index.ts index 052600c9..498e9b23 100644 --- a/modules/customers/src/api/application/list-customers/index.ts +++ b/modules/customers/src/api/application/list-customers/index.ts @@ -1 +1,2 @@ +export * from "./assembler"; export * from "./list-customers.use-case"; diff --git a/modules/customers/src/api/application/list-customers/list-customers.use-case.ts b/modules/customers/src/api/application/list-customers/list-customers.use-case.ts index 48e645b0..b616a920 100644 --- a/modules/customers/src/api/application/list-customers/list-customers.use-case.ts +++ b/modules/customers/src/api/application/list-customers/list-customers.use-case.ts @@ -4,13 +4,13 @@ import { Criteria } from "@repo/rdx-criteria/server"; import { Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; import { ICustomerService } from "../../domain"; -import { ListCustomersPresenter } from "./presenter"; +import { ListCustomersAssembler } from "./assembler"; export class ListCustomersUseCase { constructor( private readonly customerService: ICustomerService, private readonly transactionManager: ITransactionManager, - private readonly presenter: ListCustomersPresenter + private readonly assembler: ListCustomersAssembler ) {} public execute(criteria: Criteria): Promise> { @@ -23,7 +23,7 @@ export class ListCustomersUseCase { return Result.fail(result.error); } - const dto: ListCustomersResultDTO = this.presenter.toDTO(result.data, criteria); + const dto: ListCustomersResultDTO = this.assembler.toDTO(result.data, criteria); return Result.ok(dto); } catch (error: unknown) { return Result.fail(error as Error); diff --git a/modules/customers/src/api/application/list-customers/presenter/index.ts b/modules/customers/src/api/application/list-customers/presenter/index.ts deleted file mode 100644 index 9ecb5c89..00000000 --- a/modules/customers/src/api/application/list-customers/presenter/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./list-invoices.presenter"; diff --git a/modules/customers/src/api/controllers/delete-customer/index.ts b/modules/customers/src/api/controllers/delete-customer/index.ts deleted file mode 100644 index b411618d..00000000 --- a/modules/customers/src/api/controllers/delete-customer/index.ts +++ /dev/null @@ -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); -}; diff --git a/modules/customers/src/api/controllers/index.ts b/modules/customers/src/api/controllers/index.ts deleted file mode 100644 index 05492882..00000000 --- a/modules/customers/src/api/controllers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./create-customer"; -export * from "./delete-customer"; -export * from "./get-customer"; -export * from "./list-customers"; -///export * from "./update-customer"; diff --git a/modules/customers/src/api/controllers/list-customers/list-customers.controller.ts b/modules/customers/src/api/controllers/list-customers/list-customers.controller.ts deleted file mode 100644 index 1168ddb3..00000000 --- a/modules/customers/src/api/controllers/list-customers/list-customers.controller.ts +++ /dev/null @@ -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); - } -} diff --git a/modules/customers/src/api/infrastructure/dependencies.ts b/modules/customers/src/api/infrastructure/dependencies.ts new file mode 100644 index 00000000..5ef9df65 --- /dev/null +++ b/modules/customers/src/api/infrastructure/dependencies.ts @@ -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: (res: Response) => ListPresenter; + }; +}; + +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: (res: Response) => createListPresenter(res), + //json: (res: Response, status: number = 200) => createJsonPresenter(res, status), + }, + }; +} diff --git a/modules/customers/src/api/controllers/create-customer/create-customer.ts b/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts similarity index 76% rename from modules/customers/src/api/controllers/create-customer/create-customer.ts rename to modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts index 735c2933..20a2132e 100644 --- a/modules/customers/src/api/controllers/create-customer/create-customer.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/create-customer.controller.ts @@ -1,10 +1,12 @@ import { ExpressController, errorMapper } from "@erp/core/api"; -import { CreateCustomerCommandDTO } from "../../../common/dto"; -import { CreateCustomerUseCase } from "../../application"; +import { CreateCustomerCommandDTO } from "../../../../../common/dto"; +import { CreateCustomerUseCase } from "../../../../application"; export class CreateCustomerController extends ExpressController { public constructor(private readonly createCustomer: CreateCustomerUseCase) { super(); + // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query + this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); } protected async executeImpl() { diff --git a/modules/customers/src/api/controllers/create-customer/index.ts b/modules/customers/src/api/infrastructure/express/controllers/create-customer/index.ts similarity index 77% rename from modules/customers/src/api/controllers/create-customer/index.ts rename to modules/customers/src/api/infrastructure/express/controllers/create-customer/index.ts index e668b006..8d372703 100644 --- a/modules/customers/src/api/controllers/create-customer/index.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/create-customer/index.ts @@ -1,9 +1,9 @@ import { SequelizeTransactionManager } from "@erp/core/api"; import { Sequelize } from "sequelize"; -import { CreateCustomerUseCase, CreateCustomersPresenter } from "../../application/"; -import { CustomerService } from "../../domain"; -import { CustomerMapper } from "../../infrastructure"; -import { CreateCustomerController } from "./create-customer"; +import { CustomerMapper } from "../../.."; +import { CreateCustomerUseCase, CreateCustomersPresenter } from "../../../../application"; +import { CustomerService } from "../../../../domain"; +import { CreateCustomerController } from "./create-customer.controller"; export const buildCreateCustomersController = (database: Sequelize) => { const transactionManager = new SequelizeTransactionManager(database); diff --git a/modules/customers/src/api/controllers/delete-customer/delete-invoice.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts similarity index 77% rename from modules/customers/src/api/controllers/delete-customer/delete-invoice.controller.ts rename to modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts index 388484e6..25171e8f 100644 --- a/modules/customers/src/api/controllers/delete-customer/delete-invoice.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/delete-customer.controller.ts @@ -1,9 +1,11 @@ import { ExpressController, errorMapper } from "@erp/core/api"; -import { DeleteCustomerUseCase } from "../../application"; +import { DeleteCustomerUseCase } from "../../../../application"; export class DeleteCustomerController extends ExpressController { public constructor(private readonly deleteCustomer: DeleteCustomerUseCase) { super(); + // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query + this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); } async executeImpl(): Promise { diff --git a/modules/customers/src/api/infrastructure/express/controllers/delete-customer/index.ts b/modules/customers/src/api/infrastructure/express/controllers/delete-customer/index.ts new file mode 100644 index 00000000..a550c469 --- /dev/null +++ b/modules/customers/src/api/infrastructure/express/controllers/delete-customer/index.ts @@ -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 */); +}; diff --git a/modules/customers/src/api/controllers/get-customer/get-invoice.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts similarity index 77% rename from modules/customers/src/api/controllers/get-customer/get-invoice.controller.ts rename to modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts index 2d6c051d..6d2726e0 100644 --- a/modules/customers/src/api/controllers/get-customer/get-invoice.controller.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/get-customer.controller.ts @@ -1,9 +1,11 @@ import { ExpressController, errorMapper } from "@erp/core/api"; -import { GetCustomerUseCase } from "../../application"; +import { GetCustomerUseCase } from "../../../application"; export class GetCustomerController extends ExpressController { public constructor(private readonly getCustomer: GetCustomerUseCase) { super(); + // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query + this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); } protected async executeImpl() { diff --git a/modules/customers/src/api/controllers/get-customer/index.ts b/modules/customers/src/api/infrastructure/express/controllers/get-customer/index.ts similarity index 75% rename from modules/customers/src/api/controllers/get-customer/index.ts rename to modules/customers/src/api/infrastructure/express/controllers/get-customer/index.ts index 66906698..b6bf6819 100644 --- a/modules/customers/src/api/controllers/get-customer/index.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/get-customer/index.ts @@ -1,9 +1,9 @@ import { SequelizeTransactionManager } from "@erp/core/api"; import { Sequelize } from "sequelize"; -import { GetCustomerUseCase, getCustomerPresenter } from "../../application"; -import { CustomerService } from "../../domain"; -import { CustomerRepository, customerMapper } from "../../infrastructure"; -import { GetCustomerController } from "./get-invoice.controller"; +import { CustomerRepository, customerMapper } from "../../.."; +import { GetCustomerUseCase, getCustomerPresenter } from "../../../../application"; +import { CustomerService } from "../../../../domain"; +import { GetCustomerController } from "../get-customer.controller"; export const buildGetCustomerController = (database: Sequelize) => { const transactionManager = new SequelizeTransactionManager(database); diff --git a/modules/customers/src/api/infrastructure/express/controllers/index.ts b/modules/customers/src/api/infrastructure/express/controllers/index.ts new file mode 100644 index 00000000..60ce3f1f --- /dev/null +++ b/modules/customers/src/api/infrastructure/express/controllers/index.ts @@ -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"; diff --git a/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts b/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts new file mode 100644 index 00000000..7ed3e40d --- /dev/null +++ b/modules/customers/src/api/infrastructure/express/controllers/list-customers.controller.ts @@ -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)) + ); + } +} diff --git a/modules/customers/src/api/controllers/list-customers/index.ts b/modules/customers/src/api/infrastructure/express/controllers/list-customers/index.ts similarity index 69% rename from modules/customers/src/api/controllers/list-customers/index.ts rename to modules/customers/src/api/infrastructure/express/controllers/list-customers/index.ts index 72f75675..0c0a261d 100644 --- a/modules/customers/src/api/controllers/list-customers/index.ts +++ b/modules/customers/src/api/infrastructure/express/controllers/list-customers/index.ts @@ -1,9 +1,9 @@ import { SequelizeTransactionManager } from "@erp/core/api"; import { Sequelize } from "sequelize"; -import { ListCustomersUseCase } from "../../application"; -import { listCustomersPresenter } from "../../application/list-customers/presenter"; -import { CustomerService } from "../../domain"; -import { CustomerRepository, customerMapper } from "../../infrastructure"; +import { CustomerRepository, customerMapper } from "../../.."; +import { ListCustomersUseCase } from "../../../../application"; +import { listCustomersPresenter } from "../../../../application/list-customers/assembler"; +import { CustomerService } from "../../../../domain"; import { ListCustomersController } from "./list-customers.controller"; export const buildListCustomersController = (database: Sequelize) => { diff --git a/modules/customers/src/api/controllers/update-customer/index.ts.bak b/modules/customers/src/api/infrastructure/express/controllers/update-customer/index.ts.bak similarity index 100% rename from modules/customers/src/api/controllers/update-customer/index.ts.bak rename to modules/customers/src/api/infrastructure/express/controllers/update-customer/index.ts.bak diff --git a/modules/customers/src/api/controllers/update-customer/presenter/InvoiceItem.presenter.ts.bak b/modules/customers/src/api/infrastructure/express/controllers/update-customer/presenter/InvoiceItem.presenter.ts.bak similarity index 100% rename from modules/customers/src/api/controllers/update-customer/presenter/InvoiceItem.presenter.ts.bak rename to modules/customers/src/api/infrastructure/express/controllers/update-customer/presenter/InvoiceItem.presenter.ts.bak diff --git a/modules/customers/src/api/controllers/update-customer/presenter/InvoiceParticipant.presenter.ts.bak b/modules/customers/src/api/infrastructure/express/controllers/update-customer/presenter/InvoiceParticipant.presenter.ts.bak similarity index 100% rename from modules/customers/src/api/controllers/update-customer/presenter/InvoiceParticipant.presenter.ts.bak rename to modules/customers/src/api/infrastructure/express/controllers/update-customer/presenter/InvoiceParticipant.presenter.ts.bak diff --git a/modules/customers/src/api/controllers/update-customer/presenter/InvoiceParticipantAddress.presenter.ts.bak b/modules/customers/src/api/infrastructure/express/controllers/update-customer/presenter/InvoiceParticipantAddress.presenter.ts.bak similarity index 100% rename from modules/customers/src/api/controllers/update-customer/presenter/InvoiceParticipantAddress.presenter.ts.bak rename to modules/customers/src/api/infrastructure/express/controllers/update-customer/presenter/InvoiceParticipantAddress.presenter.ts.bak diff --git a/modules/customers/src/api/controllers/update-customer/presenter/UpdateInvoice.presenter.ts.bak b/modules/customers/src/api/infrastructure/express/controllers/update-customer/presenter/UpdateInvoice.presenter.ts.bak similarity index 100% rename from modules/customers/src/api/controllers/update-customer/presenter/UpdateInvoice.presenter.ts.bak rename to modules/customers/src/api/infrastructure/express/controllers/update-customer/presenter/UpdateInvoice.presenter.ts.bak diff --git a/modules/customers/src/api/controllers/update-customer/presenter/index.ts.bak b/modules/customers/src/api/infrastructure/express/controllers/update-customer/presenter/index.ts.bak similarity index 100% rename from modules/customers/src/api/controllers/update-customer/presenter/index.ts.bak rename to modules/customers/src/api/infrastructure/express/controllers/update-customer/presenter/index.ts.bak diff --git a/modules/customers/src/api/controllers/update-customer/update-invoice.controller.ts.bak b/modules/customers/src/api/infrastructure/express/controllers/update-invoice.controller.ts.bak similarity index 100% rename from modules/customers/src/api/controllers/update-customer/update-invoice.controller.ts.bak rename to modules/customers/src/api/infrastructure/express/controllers/update-invoice.controller.ts.bak diff --git a/modules/customers/src/api/infrastructure/express/customers.routes.ts b/modules/customers/src/api/infrastructure/express/customers.routes.ts index a73ea7ec..b703b066 100644 --- a/modules/customers/src/api/infrastructure/express/customers.routes.ts +++ b/modules/customers/src/api/infrastructure/express/customers.routes.ts @@ -7,12 +7,13 @@ import { DeleteCustomerByIdRequestSchema, GetCustomerByIdRequestSchema, } from "../../../common/dto"; +import { getCustomerDependencies } from "../dependencies"; import { - buildCreateCustomersController, - buildDeleteCustomerController, - buildGetCustomerController, - buildListCustomersController, -} from "../../controllers"; + CreateCustomerController, + DeleteCustomerController, + GetCustomerController, + ListCustomersController, +} from "./controllers"; export const customersRouter = (params: ModuleParams) => { const { app, database, baseRoutePath, logger } = params as { @@ -22,35 +23,45 @@ export const customersRouter = (params: ModuleParams) => { logger: ILogger; }; - const routes: Router = Router({ mergeParams: true }); + const router: Router = Router({ mergeParams: true }); + const deps = getCustomerDependencies(params); - routes.get( + // 🔐 Autenticación + Tenancy para TODO el router + router.use(/* authenticateJWT(), */ enforceTenant() /*checkTabContext*/); + + router.get( "/", //checkTabContext, - //checkUser, + validateRequest(CustomerListRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - buildListCustomersController(database).execute(req, res, next); + const useCase = deps.build.list(); + const controller = new ListCustomersController(useCase /*, deps.presenters.list */); + return controller.execute(req, res, next); } ); - routes.get( + router.get( "/:id", //checkTabContext, - //checkUser, + validateRequest(GetCustomerByIdRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - buildGetCustomerController(database).execute(req, res, next); + const useCase = deps.build.get(); + const controller = new GetCustomerController(useCase); + return controller.execute(req, res, next); } ); - routes.post( + router.post( "/", //checkTabContext, - //checkUser, + validateRequest(CreateCustomerRequestSchema), (req: Request, res: Response, next: NextFunction) => { - buildCreateCustomersController(database).execute(req, res, next); + const useCase = deps.build.create(); + const controller = new CreateCustomerController(useCase); + return controller.execute(req, res, next); } ); @@ -58,21 +69,23 @@ export const customersRouter = (params: ModuleParams) => { "/:customerId", validateAndParseBody(IUpdateCustomerRequestSchema), checkTabContext, - //checkUser, + (req: Request, res: Response, next: NextFunction) => { buildUpdateCustomerController().execute(req, res, next); } );*/ - routes.delete( + router.delete( "/:id", //checkTabContext, - //checkUser, + validateRequest(DeleteCustomerByIdRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - buildDeleteCustomerController(database).execute(req, res, next); + const useCase = deps.build.delete(); + const controller = new DeleteCustomerController(useCase); + return controller.execute(req, res, next); } ); - app.use(`${baseRoutePath}/customers`, routes); + app.use(`${baseRoutePath}/customers`, router); }; diff --git a/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts b/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts index 335e541c..06b5fc09 100644 --- a/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts +++ b/modules/customers/src/api/infrastructure/mappers/customer.mapper.ts @@ -92,6 +92,3 @@ export class CustomerMapper }; } } - -const customerMapper: CustomerMapper = new CustomerMapper(); -export { customerMapper }; diff --git a/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts b/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts index ed4fc3d7..fe880cd2 100644 --- a/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts +++ b/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts @@ -2,7 +2,7 @@ import { SequelizeRepository, errorMapper } from "@erp/core/api"; import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; import { UniqueID } from "@repo/rdx-ddd"; import { Collection, Result } from "@repo/rdx-utils"; -import { Sequelize, Transaction } from "sequelize"; +import { Transaction } from "sequelize"; import { Customer, ICustomerRepository } from "../../domain"; import { ICustomerMapper } from "../mappers/customer.mapper"; import { CustomerModel } from "./customer.model"; @@ -14,10 +14,8 @@ export class CustomerRepository //private readonly model: typeof CustomerModel; private readonly mapper!: ICustomerMapper; - constructor(database: Sequelize, mapper: ICustomerMapper) { - super(database); - - //Customer = database.model("Customer") as typeof CustomerModel; + constructor(mapper: ICustomerMapper) { + super(); this.mapper = mapper; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a7056f7..ad86d07e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -325,6 +325,12 @@ importers: '@tanstack/react-query': specifier: ^5.74.11 version: 5.81.2(react@19.1.0) + dinero.js: + specifier: ^1.9.1 + version: 1.9.1 + express: + specifier: ^4.18.2 + version: 4.21.2 i18next: specifier: ^25.1.1 version: 25.2.1(typescript@5.8.3) @@ -343,6 +349,12 @@ importers: react-secure-storage: specifier: ^1.3.2 version: 1.3.2 + sequelize: + specifier: ^6.37.5 + version: 6.37.7(mysql2@3.14.1) + zod: + specifier: ^3.25.67 + version: 3.25.67 devDependencies: '@biomejs/biome': specifier: 1.9.4 @@ -444,6 +456,9 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@19.1.0) + '@erp/auth': + specifier: workspace:* + version: link:../auth '@erp/core': specifier: workspace:* version: link:../core