Métodos de pago

This commit is contained in:
David Arranz 2026-05-20 19:53:23 +02:00
parent 996cf4ceb4
commit cd4b8975c3
95 changed files with 2286 additions and 10 deletions

15
.ai/backend/agent.md Normal file
View File

@ -0,0 +1,15 @@
Especialización backend.
Contiene:
- Clean Architecture
- DDD
- módulos
- Sequelize
- DTOs
- repositorios
- fiscalidad
- use cases
- Express
- snapshot builders

32
.ai/frontend/agent.md Normal file
View File

@ -0,0 +1,32 @@
# Frontend ERP Agent
## Objetivo
Construir frontend ERP modular, tipado, mantenible y desacoplado.
---
# Stack
- React
- TypeScript
- Vite
- TanStack Query
- React Hook Form
- Zod
- Zustand (si aplica)
- Tailwind/Shadcn/etc
---
# Principios
- UI desacoplada de transporte
- SSR/CSR explícito
- Estado mínimo global
- Formularios tipados
- Server state != UI state
- Componentes pequeños
- Evitar smart components gigantes
- Accesibilidad obligatoria
- Diseño orientado a features

10
.ai/index.md Normal file
View File

@ -0,0 +1,10 @@
# ERP AI Context
## Shared => filosofía + convenciones
- ./shared/agent.md
## Backend => dominio + arquitectura + persistencia
- ./backend/agent.md
## Frontend => interacción + UX + estado + rendering
- ./frontend/agent.md

View File

@ -0,0 +1,5 @@
- Mantener código en inglés.
- Comentarios solo si aportan, en español.
- No usar any.
- No crear abstracciones genéricas prematuras.

View File

@ -1,3 +1,4 @@
import catalogsAPIModule from "@erp/catalogs/api";
import customerInvoicesAPIModule from "@erp/customer-invoices/api";
import customersAPIModule from "@erp/customers/api";
import factuGESAPIModule from "@erp/factuges/api";
@ -8,6 +9,7 @@ import { registerModule } from "./lib";
export const registerModules = () => {
//registerModule(authAPIModule);
registerModule(catalogsAPIModule);
registerModule(customersAPIModule);
registerModule(customerInvoicesAPIModule);
registerModule(factuGESAPIModule);

View File

@ -0,0 +1,32 @@
{
"name": "@erp/catalogs",
"description": "Catalogs module",
"version": "0.1.0",
"private": true,
"type": "module",
"sideEffects": false,
"scripts": {
"typecheck": "tsc -p tsconfig.json --noEmit",
"check": "biome check .",
"lint": "biome lint .",
"clean": "rimraf .turbo node_modules dist"
},
"exports": {
".": "./src/api/index.ts",
"./api": "./src/api/index.ts"
},
"dependencies": {
"@erp/core": "workspace:*",
"@erp/auth": "workspace:*",
"@repo/rdx-ddd": "workspace:*",
"@repo/rdx-utils": "workspace:*",
"@repo/rdx-criteria": "workspace:*",
"express": "^4.22.1",
"sequelize": "^6.37.8",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/express": "^4.17.21",
"typescript": "^6.0.2"
}
}

View File

@ -0,0 +1,5 @@
export * from "./payment-method-creator.di";
export * from "./payment-method-finder.di";
export * from "./payment-method-input-mappers.di";
export * from "./payment-method-snapshot-builders.di";
export * from "./payment-method-updater.di";

View File

@ -0,0 +1,10 @@
import type { IPaymentMethodRepository } from "../repositories";
import { type IPaymentMethodCreator, PaymentMethodCreator } from "../services";
export const buildPaymentMethodCreator = (params: {
repository: IPaymentMethodRepository;
}): IPaymentMethodCreator => {
const { repository } = params;
return new PaymentMethodCreator(repository);
};

View File

@ -0,0 +1,8 @@
import type { IPaymentMethodRepository } from "../repositories";
import { type IPaymentMethodFinder, PaymentMethodFinder } from "../services";
export function buildPaymentMethodFinder(
repository: IPaymentMethodRepository
): IPaymentMethodFinder {
return new PaymentMethodFinder(repository);
}

View File

@ -0,0 +1,21 @@
import {
CreatePaymentMethodInputMapper,
type ICreatePaymentMethodInputMapper,
UpdatePaymentMethodInputMapper,
} from "../mappers";
export interface IPaymentMethodInputMappers {
createInputMapper: ICreatePaymentMethodInputMapper;
updateInputMapper: UpdatePaymentMethodInputMapper;
}
export const buildPaymentMethodInputMappers = (): IPaymentMethodInputMappers => {
// Mappers el DTO a las props validadas (PaymentMethodProps) y luego construir agregado
const createInputMapper = new CreatePaymentMethodInputMapper();
const updateInputMapper = new UpdatePaymentMethodInputMapper();
return {
createInputMapper,
updateInputMapper,
};
};

View File

@ -0,0 +1,16 @@
// application/issued-invoices/di/snapshot-builders.di.ts
import {
PaymentMethodFullSnapshotBuilder,
PaymentMethodSummarySnapshotBuilder,
} from "../snapshot-builders";
export function buildPaymentMethodSnapshotBuilders() {
const fullSnapshotBuilder = new PaymentMethodFullSnapshotBuilder();
const summarySnapshotBuilder = new PaymentMethodSummarySnapshotBuilder();
return {
full: fullSnapshotBuilder,
summary: summarySnapshotBuilder,
};
}

View File

@ -0,0 +1,10 @@
import type { IPaymentMethodRepository } from "../repositories";
import { type IPaymentMethodUpdater, PaymentMethodUpdater } from "../services";
export const buildPaymentMethodUpdater = (params: {
repository: IPaymentMethodRepository;
}): IPaymentMethodUpdater => {
const { repository } = params;
return new PaymentMethodUpdater(repository);
};

View File

@ -0,0 +1,7 @@
export * from "./di";
export * from "./mappers";
export * from "./models";
export * from "./repositories";
export * from "./services";
export * from "./snapshot-builders";
export * from "./use-cases";

View File

@ -0,0 +1,67 @@
import {
DomainError,
Name,
TextValue,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { CreatePaymentMethodRequestDTO } from "../../../common";
import type { IPaymentMethodCreateProps } from "../../domain";
export interface ICreatePaymentMethodInputMapper {
map(
dto: CreatePaymentMethodRequestDTO,
params: { companyId: UniqueID }
): Result<{ id: UniqueID; props: IPaymentMethodCreateProps }, Error>;
}
export class CreatePaymentMethodInputMapper implements ICreatePaymentMethodInputMapper {
public map(
dto: CreatePaymentMethodRequestDTO,
params: { companyId: UniqueID }
): Result<{ id: UniqueID; props: IPaymentMethodCreateProps }, Error> {
const errors: ValidationErrorDetail[] = [];
try {
const paymentMethodId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const name = extractOrPushError(Name.create(dto.name), "name", errors);
const description = extractOrPushError(
maybeFromNullableResult(dto.description, (value) => TextValue.create(value)),
"description",
errors
);
const isActive = extractOrPushError(Result.ok(dto.is_active), "is_active", errors);
this.throwIfValidationErrors(errors);
const props = {
companyId: params.companyId,
name: name!,
description: description!,
isActive: isActive!,
isSystem: false,
};
return Result.ok({
id: paymentMethodId!,
props,
});
} catch (err: unknown) {
return Result.fail(new DomainError("Payment method props mapping failed", { cause: err }));
}
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
if (errors.length > 0) {
throw new ValidationErrorCollection("Payment method props mapping failed", errors);
}
}
}

View File

@ -0,0 +1,2 @@
export * from "./create-payment-method-input.mapper";
export * from "./update-payment-method-by-id-input.mapper";

View File

@ -0,0 +1,86 @@
import {
DomainError,
Name,
TextValue,
type UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
import type { UpdatePaymentMethodByIdRequestDTO } from "../../../common";
import type { PaymentMethodPatchProps } from "../../domain";
export interface IUpdatePaymentMethodInputMapper {
map(
dto: UpdatePaymentMethodByIdRequestDTO,
params: { companyId: UniqueID }
): Result<PaymentMethodPatchProps>;
}
/**
* @summary Convierte el DTO de update de forma de pago en props de dominio.
* @remarks
* Respeta semántica PATCH en cabecera:
* - omitido: no modificar
* - null: limpiar valor cuando el campo lo permite
* - valor: asignar nuevo valor
*
* Para `items`, no aplica patch granular:
* - undefined: no tocar líneas
* - []: borrar todas las líneas
* - [...]: reemplazar colección completa
*/
export class UpdatePaymentMethodInputMapper implements IUpdatePaymentMethodInputMapper {
public map(
dto: UpdatePaymentMethodByIdRequestDTO,
_params: { companyId: UniqueID }
): Result<PaymentMethodPatchProps> {
try {
const errors: ValidationErrorDetail[] = [];
const paymentmethodPatchProps: PaymentMethodPatchProps = {};
toPatchField(dto.name).ifSet((name) => {
if (isNullishOrEmpty(name)) {
errors.push({
path: "name",
message: "Name cannot be empty",
});
return;
}
paymentmethodPatchProps.name = extractOrPushError(Name.create(name), "name", errors);
});
toPatchField(dto.description).ifSet((description) => {
paymentmethodPatchProps.description = extractOrPushError(
maybeFromNullableResult(description, (value) => TextValue.create(value)),
"description",
errors
);
});
toPatchField(dto.is_active).ifSet((isActive) => {
paymentmethodPatchProps.isActive = isActive;
});
this.throwIfValidationErrors(errors);
return Result.ok(paymentmethodPatchProps);
} catch (err: unknown) {
console.error(err);
return Result.fail(
new DomainError("PaymentMethod props mapping failed (update)", { cause: err })
);
}
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
if (errors.length > 0) {
throw new ValidationErrorCollection("PaymentMethod props mapping failed", errors);
}
}
}

View File

@ -0,0 +1,2 @@
export * from "./payment-method-detail.model";
export * from "./payment-method-summary.model";

View File

@ -0,0 +1,8 @@
export type PaymentMethodDetail = {
id: string;
name: string;
type: string;
is_active: boolean;
created_at: string;
updated_at: string;
};

View File

@ -0,0 +1,9 @@
import type { Name, UniqueID } from "@repo/rdx-ddd";
export type PaymentMethodSummary = {
id: UniqueID;
companyId: UniqueID;
name: Name;
isActive: boolean;
isSystem: boolean;
};

View File

@ -0,0 +1 @@
export * from "./payment-method-repository.interface";

View File

@ -0,0 +1,31 @@
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils";
import type { PaymentMethod } from "../../domain";
import type { PaymentMethodSummary } from "../models";
export interface IPaymentMethodRepository {
create(paymentMethod: PaymentMethod, transaction?: unknown): Promise<Result<void, Error>>;
update(paymentMethod: PaymentMethod, transaction?: unknown): Promise<Result<void, Error>>;
existsByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: unknown
): Promise<Result<boolean, Error>>;
getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: unknown
): Promise<Result<PaymentMethod, Error>>;
findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction?: unknown
): Promise<Result<Collection<PaymentMethodSummary>, Error>>;
deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: unknown
): Promise<Result<boolean, Error>>;
}

View File

@ -0,0 +1,5 @@
export * from "./payment-method-creator";
export * from "./payment-method-disabler";
export * from "./payment-method-finder";
export * from "./payment-method-public-services";
export * from "./payment-method-updater";

View File

@ -0,0 +1,41 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { type IPaymentMethodCreateProps, PaymentMethod } from "../../domain";
import type { IPaymentMethodRepository } from "../repositories";
export interface IPaymentMethodCreatorParams {
companyId: UniqueID;
id: UniqueID;
props: IPaymentMethodCreateProps;
transaction: unknown;
}
export interface IPaymentMethodCreator {
create(params: IPaymentMethodCreatorParams): Promise<Result<PaymentMethod, Error>>;
}
export class PaymentMethodCreator implements IPaymentMethodCreator {
constructor(private readonly repository: IPaymentMethodRepository) {}
async create(params: IPaymentMethodCreatorParams): Promise<Result<PaymentMethod, Error>> {
const { companyId, id, props, transaction } = params;
// 1. Crear agregado
const paymentMethodResult = PaymentMethod.create({ ...props, companyId }, id);
if (paymentMethodResult.isFailure) {
return Result.fail(paymentMethodResult.error);
}
const paymentMethod = paymentMethodResult.data;
// 2. Persistir
const saveResult = await this.repository.create(paymentMethod, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
return Result.ok(paymentMethod);
}
}

View File

@ -0,0 +1,35 @@
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { PaymentMethod } from "../../domain";
import type { IPaymentMethodRepository } from "../repositories";
export interface IPaymentMethodDisabler {
disable(params: {
paymentMethod: PaymentMethod;
transaction?: Transaction;
}): Promise<Result<PaymentMethod, Error>>;
}
export class PaymentMethodDisabler implements IPaymentMethodDisabler {
constructor(private readonly repository: IPaymentMethodRepository) {}
async disable(params: {
paymentMethod: PaymentMethod;
transaction?: Transaction;
}): Promise<Result<PaymentMethod, Error>> {
const { paymentMethod, transaction } = params;
const disableResult = paymentMethod.disable();
if (disableResult.isFailure) {
return Result.fail(disableResult.error);
}
const persistenceResult = await this.repository.update(paymentMethod, transaction);
if (persistenceResult.isFailure) {
return Result.fail(persistenceResult.error);
}
return Result.ok(paymentMethod);
}
}

View File

@ -0,0 +1,55 @@
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils";
import type { PaymentMethod } from "../../domain";
import type { PaymentMethodSummary } from "../models";
import type { IPaymentMethodRepository } from "../repositories";
export interface IPaymentMethodFinder {
findPaymentMethodById(
companyId: UniqueID,
invoiceId: UniqueID,
transaction?: unknown
): Promise<Result<PaymentMethod, Error>>;
paymentmethodExists(
companyId: UniqueID,
invoiceId: UniqueID,
transaction?: unknown
): Promise<Result<boolean, Error>>;
findPaymentMethodsByCriteria(
companyId: UniqueID,
criteria: Criteria,
transaction?: unknown
): Promise<Result<Collection<PaymentMethodSummary>, Error>>;
}
export class PaymentMethodFinder implements IPaymentMethodFinder {
constructor(private readonly repository: IPaymentMethodRepository) {}
async findPaymentMethodById(
companyId: UniqueID,
paymentmethodId: UniqueID,
transaction?: unknown
): Promise<Result<PaymentMethod, Error>> {
return this.repository.getByIdInCompany(companyId, paymentmethodId, transaction);
}
async paymentmethodExists(
companyId: UniqueID,
paymentmethodId: UniqueID,
transaction?: unknown
): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, paymentmethodId, transaction);
}
async findPaymentMethodsByCriteria(
companyId: UniqueID,
criteria: Criteria,
transaction?: unknown
): Promise<Result<Collection<PaymentMethodSummary>, Error>> {
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
}
}

View File

@ -0,0 +1,5 @@
import type { IPaymentMethodFinder } from "./payment-method-finder";
export type IPaymentMethodPublicServices = {
finder: IPaymentMethodFinder;
};

View File

@ -0,0 +1,52 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { PaymentMethod, PaymentMethodPatchProps } from "../../domain";
import type { IPaymentMethodRepository } from "../repositories";
export interface IPaymentMethodUpdater {
update(params: {
companyId: UniqueID;
id: UniqueID;
patchProps: PaymentMethodPatchProps;
transaction?: unknown;
}): Promise<Result<PaymentMethod, Error>>;
}
export class PaymentMethodUpdater implements IPaymentMethodUpdater {
constructor(private readonly repository: IPaymentMethodRepository) {}
async update(params: {
companyId: UniqueID;
id: UniqueID;
patchProps: PaymentMethodPatchProps;
transaction?: unknown;
}): Promise<Result<PaymentMethod, Error>> {
const { companyId, id, patchProps, transaction } = params;
// Recuperar agregado existente
const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction);
if (existingResult.isFailure) {
return Result.fail(existingResult.error);
}
const paymentMethod = existingResult.data;
// Aplicar cambios en el agregado
const updateResult = paymentMethod.update(patchProps);
if (updateResult.isFailure) {
return Result.fail(updateResult.error);
}
// Persistir cambios
const saveResult = await this.repository.update(paymentMethod, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
return Result.ok(paymentMethod);
}
}

View File

@ -0,0 +1 @@
export * from './payment-method-full-snapshot-builder';

View File

@ -0,0 +1,20 @@
import type { GetPaymentMethodByIdResponseDTO } from "@erp/catalogs/common";
import type { ISnapshotBuilder } from "@erp/core/api";
import { toNullable } from "@repo/rdx-ddd";
import type { PaymentMethod } from "../../../domain";
export interface IPaymentMethodFullSnapshotBuilder
extends ISnapshotBuilder<PaymentMethod, GetPaymentMethodByIdResponseDTO> {}
export class PaymentMethodFullSnapshotBuilder implements IPaymentMethodFullSnapshotBuilder {
public toOutput(paymentMethod: PaymentMethod): GetPaymentMethodByIdResponseDTO {
return {
id: paymentMethod.id.toPrimitive(),
name: paymentMethod.name.toPrimitive(),
description: toNullable(paymentMethod.description, (value) => value.toPrimitive()),
is_active: paymentMethod.isActive,
is_system: paymentMethod.isSystem,
};
}
}

View File

@ -0,0 +1,2 @@
export * from "./full";
export * from "./summary";

View File

@ -0,0 +1 @@
export * from "./payment-method-summary-snapshot-builder";

View File

@ -0,0 +1,19 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import type { PaymentMethodSummaryDTO } from "../../../../common/";
import type { PaymentMethodSummary } from "../../models";
export interface IPaymentMethodSummarySnapshotBuilder
extends ISnapshotBuilder<PaymentMethodSummary, PaymentMethodSummaryDTO> {}
export class PaymentMethodSummarySnapshotBuilder implements IPaymentMethodSummarySnapshotBuilder {
public toOutput(paymentMethod: PaymentMethodSummary): PaymentMethodSummaryDTO {
return {
id: paymentMethod.id.toString(),
company_id: paymentMethod.companyId.toString(),
name: paymentMethod.name.toString(),
is_system: paymentMethod.isSystem,
is_active: paymentMethod.isActive,
};
}
}

View File

@ -0,0 +1,51 @@
import type { CreatePaymentMethodRequestDTO } from "@erp/catalogs/common";
import type { ITransactionManager } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { ICreatePaymentMethodInputMapper } from "../mappers";
import type { IPaymentMethodCreator } from "../services";
import type { IPaymentMethodFullSnapshotBuilder } from "../snapshot-builders";
export type CreatePaymentMethodUseCaseInput = {
companyId: UniqueID;
dto: CreatePaymentMethodRequestDTO;
};
type CreatePaymentMethodUseCaseDeps = {
dtoMapper: ICreatePaymentMethodInputMapper;
creator: IPaymentMethodCreator;
fullSnapshotBuilder: IPaymentMethodFullSnapshotBuilder;
transactionManager: ITransactionManager;
};
export class CreatePaymentMethodUseCase {
constructor(private readonly deps: CreatePaymentMethodUseCaseDeps) {}
public execute(params: CreatePaymentMethodUseCaseInput) {
const { dto, companyId } = params;
console.log("Executing CreatePaymentMethodUseCase with params:", params);
const mappedPropsResult = this.deps.dtoMapper.map(dto, { companyId });
if (mappedPropsResult.isFailure) {
return mappedPropsResult;
}
const { props, id } = mappedPropsResult.data;
return this.deps.transactionManager.complete(async (transaction: unknown) => {
try {
const createResult = await this.deps.creator.create({ companyId, id, props, transaction });
if (createResult.isFailure) {
return createResult;
}
const snapshot = this.deps.fullSnapshotBuilder.toOutput(createResult.data);
return Result.ok(snapshot);
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,47 @@
import type { ITransactionManager } from "@erp/core/api";
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { IPaymentMethodDisabler, IPaymentMethodFinder } from "../services";
import type { IPaymentMethodFullSnapshotBuilder } from "../snapshot-builders";
export type DisablePaymentMethodByIdUseCaseInput = {
id: string;
};
export class DisablePaymentMethodByIdUseCase {
constructor(
private readonly deps: {
finder: IPaymentMethodFinder;
disabler: IPaymentMethodDisabler;
fullSnapshotBuilder: IPaymentMethodFullSnapshotBuilder;
transactionManager: ITransactionManager;
}
) {}
public execute(params: DisablePaymentMethodByIdUseCaseInput) {
const { id } = params;
return this.deps.transactionManager.complete(async (transaction: unknown) => {
const tx = transaction as Transaction;
try {
const findResult = await this.deps.finder.getById(id, tx);
if (findResult.isFailure) {
return Result.fail(findResult.error);
}
const disableResult = await this.deps.disabler.disable({
paymentMethod: findResult.data,
transaction: tx,
});
if (disableResult.isFailure) {
return Result.fail(disableResult.error);
}
return Result.ok(this.deps.fullSnapshotBuilder.toOutput(disableResult.data));
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,47 @@
import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IPaymentMethodFinder } from "../services";
import type { IPaymentMethodFullSnapshotBuilder } from "../snapshot-builders";
export type GetPaymentMethodByIdUseCaseInput = {
companyId: UniqueID;
payment_method_id: string;
};
export class GetPaymentMethodByIdUseCase {
constructor(
private readonly finder: IPaymentMethodFinder,
private readonly fullSnapshotBuilder: IPaymentMethodFullSnapshotBuilder,
private readonly transactionManager: ITransactionManager
) {}
public execute(params: GetPaymentMethodByIdUseCaseInput) {
const { payment_method_id, companyId } = params;
const idOrError = UniqueID.create(payment_method_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const paymentMethodId = idOrError.data;
return this.transactionManager.complete(async (transaction: unknown) => {
try {
const result = await this.finder.findPaymentMethodById(
companyId,
paymentMethodId,
transaction
);
if (result.isFailure) {
return Result.fail(result.error);
}
return Result.ok(this.fullSnapshotBuilder.toOutput(result.data));
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,5 @@
export * from "./create-payment-method.use-case";
export * from "./disable-payment-method-by-id.use-case";
export * from "./get-payment-method-by-id.use-case";
export * from "./list-payment-methods.use-case";
export * from "./update-payment-method-by-id.use-case";

View File

@ -0,0 +1,59 @@
import type { ITransactionManager } from "@erp/core/api";
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IPaymentMethodFinder } from "../services";
import type { IPaymentMethodSummarySnapshotBuilder } from "../snapshot-builders";
type ListPaymentMethodsUseCaseInput = {
companyId: UniqueID;
criteria: Criteria;
};
export class ListPaymentMethodsUseCase {
constructor(
private readonly finder: IPaymentMethodFinder,
private readonly summarySnapshotBuilder: IPaymentMethodSummarySnapshotBuilder,
private readonly transactionManager: ITransactionManager
) {}
public execute(params: ListPaymentMethodsUseCaseInput) {
const { criteria, companyId } = params;
return this.transactionManager.complete(async (transaction: unknown) => {
try {
const result = await this.finder.findPaymentMethodsByCriteria(
companyId,
criteria,
transaction
);
if (result.isFailure) {
return Result.fail(result.error);
}
const paymentMethods = result.data;
const totalItems = paymentMethods.total();
const items = paymentMethods.map((item) => this.summarySnapshotBuilder.toOutput(item));
const snapshot = {
page: criteria.pageNumber,
per_page: criteria.pageSize,
total_pages: Math.ceil(totalItems / criteria.pageSize),
total_items: totalItems,
items: items,
metadata: {
entity: "payment_methods",
criteria: criteria.toJSON(),
},
};
return Result.ok(snapshot);
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,64 @@
import type { UpdatePaymentMethodByIdRequestDTO } from "@erp/catalogs/common";
import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IUpdatePaymentMethodInputMapper } from "../mappers";
import type { IPaymentMethodFinder, IPaymentMethodUpdater } from "../services";
import type { IPaymentMethodFullSnapshotBuilder } from "../snapshot-builders";
export type UpdatePaymentMethodByIdUseCaseInput = {
companyId: UniqueID;
payment_method_id: string;
dto: UpdatePaymentMethodByIdRequestDTO;
};
export class UpdatePaymentMethodByIdUseCase {
constructor(
private readonly deps: {
updater: IPaymentMethodUpdater;
finder: IPaymentMethodFinder;
dtoMapper: IUpdatePaymentMethodInputMapper;
fullSnapshotBuilder: IPaymentMethodFullSnapshotBuilder;
transactionManager: ITransactionManager;
}
) {}
public execute(params: UpdatePaymentMethodByIdUseCaseInput) {
const { companyId, payment_method_id, dto } = params;
const idOrError = UniqueID.create(payment_method_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const paymentMethodId = idOrError.data;
// Mapear DTO → props de dominio
const patchPropsResult = this.deps.dtoMapper.map(dto, { companyId });
if (patchPropsResult.isFailure) {
return patchPropsResult;
}
const patchProps = patchPropsResult.data;
return this.deps.transactionManager.complete(async (transaction: unknown) => {
try {
const updateResult = await this.deps.updater.update({
companyId,
id: paymentMethodId,
patchProps,
transaction,
});
if (updateResult.isFailure) {
return Result.fail(updateResult.error);
}
return Result.ok(this.deps.fullSnapshotBuilder.toOutput(updateResult.data));
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

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

View File

@ -0,0 +1,27 @@
import { DomainError } from "@repo/rdx-ddd";
export class InvalidPaymentMethodIdError extends DomainError {
public readonly code = "PAYMENT_METHOD_INVALID_ID" as const;
}
export const isInvalidPaymentMethodIdError = (e: unknown): e is InvalidPaymentMethodIdError =>
e instanceof InvalidPaymentMethodIdError;
export class InvalidPaymentMethodNameError extends DomainError {
public readonly code = "PAYMENT_METHOD_NAME" as const;
}
export const isInvalidPaymentMethodNameError = (e: unknown): e is InvalidPaymentMethodNameError =>
e instanceof InvalidPaymentMethodNameError;
export class PaymentMethodNotFoundError extends DomainError {
public readonly code = "PAYMENT_METHOD_NOT_FOUND" as const;
}
export class PaymentMethodCannotBeDeletedError extends DomainError {
public readonly code = "PAYMENT_METHOD_CANNOT_BE_DELETED" as const;
}
export const isPaymentMethodCannotBeDeletedError = (
e: unknown
): e is PaymentMethodCannotBeDeletedError => e instanceof PaymentMethodCannotBeDeletedError;

View File

@ -0,0 +1,4 @@
export * from "./errors";
export * from "./payment-method.aggregate";
export * from "./payment-method-name";
export * from "./payment-method-type";

View File

@ -0,0 +1,28 @@
import { Result } from "@repo/rdx-utils";
import { InvalidPaymentMethodNameError } from "./errors";
export class PaymentMethodName {
private constructor(private readonly value: string) {}
public static create(name: string): Result<PaymentMethodName, Error> {
const trimmed = name?.trim() ?? "";
if (trimmed.length === 0) {
return Result.fail(new InvalidPaymentMethodNameError("Payment method name cannot be empty"));
}
return Result.ok(new PaymentMethodName(trimmed));
}
public static fromPersistence(name: string): PaymentMethodName {
return new PaymentMethodName(name);
}
public toString(): string {
return this.value;
}
public toPrimitive(): string {
return this.value;
}
}

View File

@ -0,0 +1,43 @@
import { Result } from "@repo/rdx-utils";
import { InvalidPaymentMethodTypeError } from "./errors";
export const PAYMENT_METHOD_TYPES = [
"cash",
"bank_transfer",
"card",
"direct_debit",
"other",
] as const;
export type PaymentMethodTypeValue = (typeof PAYMENT_METHOD_TYPES)[number];
export class PaymentMethodType {
private constructor(private readonly value: PaymentMethodTypeValue) {}
public static create(type: string): Result<PaymentMethodType, Error> {
const normalized = String(type).trim() as PaymentMethodTypeValue;
if (!PAYMENT_METHOD_TYPES.includes(normalized)) {
return Result.fail(
new InvalidPaymentMethodTypeError(
`Payment method type must be one of: ${PAYMENT_METHOD_TYPES.join(", ")}`
)
);
}
return Result.ok(new PaymentMethodType(normalized));
}
public static fromPersistence(type: PaymentMethodTypeValue): PaymentMethodType {
return new PaymentMethodType(type);
}
public toString(): PaymentMethodTypeValue {
return this.value;
}
public toPrimitive(): PaymentMethodTypeValue {
return this.value;
}
}

View File

@ -0,0 +1,107 @@
import { AggregateRoot, type Name, type TextValue, type UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils";
export interface IPaymentMethodCreateProps {
companyId: UniqueID;
name: Name;
description: Maybe<TextValue>;
isActive: boolean;
isSystem: boolean;
}
export type PaymentMethodPatchProps = Partial<
Omit<IPaymentMethodCreateProps, "companyId" | "isSystem">
>;
export type PaymentMethodInternalProps = IPaymentMethodCreateProps;
export type PaymentMethodProps = PaymentMethodPatchProps;
export class PaymentMethod extends AggregateRoot<PaymentMethodInternalProps> {
protected constructor(props: PaymentMethodInternalProps, id?: UniqueID) {
super(props, id); // eslint-disable-line @typescript-eslint/no-unused-vars
}
public static create(
props: IPaymentMethodCreateProps,
id?: UniqueID
): Result<PaymentMethod, Error> {
const validationResult = PaymentMethod.validateCreateProps(props);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
// Crear instancia
const paymentMethod = new PaymentMethod(props, id);
// Disparar eventos de dominio
// ...
// ...
return Result.ok(paymentMethod);
}
public static rehydrate(props: PaymentMethodInternalProps, id: UniqueID): PaymentMethod {
return new PaymentMethod(props, id);
}
private static validateCreateProps(_props: IPaymentMethodCreateProps): Result<void, Error> {
return Result.ok();
}
public get companyId(): UniqueID {
return this.props.companyId;
}
public get name(): Name {
return this.props.name;
}
public get description(): Maybe<TextValue> {
return this.props.description;
}
public get isActive(): boolean {
return this.props.isActive;
}
public get isSystem(): boolean {
return this.props.isSystem;
}
public update(props: Partial<PaymentMethodPatchProps>): Result<void, Error> {
if (props.name !== undefined) {
this.props.name = props.name;
}
if (props.description !== undefined) {
this.props.description = props.description;
}
if (props.isActive !== undefined) {
this.props.isActive = props.isActive;
}
return Result.ok();
}
public disable(): Result<void, Error> {
if (!this.isActive) {
return Result.ok();
}
this.props.isActive = false;
return Result.ok();
}
public toJSON() {
return {
id: this.id.toPrimitive(),
name: this.props.name.toPrimitive(),
description: this.props.description,
is_active: this.isActive,
is_system: this.isSystem,
};
}
}

View File

@ -0,0 +1,44 @@
import type { IModuleServer } from "@erp/core/api";
import { models, paymentMethodsRouter } from "./infrastructure";
import {
buildCatalogsDependencies,
buildCatalogsPublicServices,
} from "./infrastructure/di/catalogs.di";
export * from "./infrastructure/persistence/sequelize";
export const catalogsAPIModule: IModuleServer = {
name: "catalogs",
version: "1.0.0",
dependencies: [],
async setup(params) {
const internal = buildCatalogsDependencies(params);
const publicServices = buildCatalogsPublicServices(params, internal);
params.logger.info("🚀 Catalogs module dependencies registered", {
label: this.name,
});
return {
models,
services: {
paymentMethod: publicServices,
},
internal,
};
},
async start(params) {
const { logger } = params;
paymentMethodsRouter(params);
logger.info("🚀 Catalogs module started", {
label: this.name,
});
},
};
export default catalogsAPIModule;

View File

@ -0,0 +1,23 @@
import type { ModuleParams, SetupParams } from "@erp/core/api";
import {
type PaymentMethodsInternalDeps,
buildPaymentMethodsDependencies,
buildPaymentMethodsPublicServices,
} from "./payment-methods.di";
export type CatalogsInternalDeps = {
paymentMethods: PaymentMethodsInternalDeps;
};
export const buildCatalogsDependencies = (params: ModuleParams): CatalogsInternalDeps => {
return {
paymentMethods: buildPaymentMethodsDependencies(params),
};
};
export const buildCatalogsPublicServices = (params: SetupParams, deps: CatalogsInternalDeps) => {
return {
paymentMethods: buildPaymentMethodsPublicServices(params, deps.paymentMethods),
};
};

View File

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

View File

@ -0,0 +1,27 @@
import {
SequelizePaymentMethodDomainMapper,
SequelizePaymentMethodSummaryMapper,
} from "../persistence/index";
export interface IPaymentMethodPersistenceMappers {
domainMapper: SequelizePaymentMethodDomainMapper;
listMapper: SequelizePaymentMethodSummaryMapper;
//createMapper: CreatePaymentMethodRequestMapper;
}
export const buildPaymentMethodPersistenceMappers = (): IPaymentMethodPersistenceMappers => {
// Mappers para el repositorio
const domainMapper = new SequelizePaymentMethodDomainMapper();
const listMapper = new SequelizePaymentMethodSummaryMapper();
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
//const createMapper = new CreatePaymentMethodRequestMapper(catalogs);
return {
domainMapper,
listMapper,
//createMapper,
};
};

View File

@ -0,0 +1,14 @@
import type { Sequelize } from "sequelize";
import { SequelizePaymentMethodRepository } from "../persistence";
import type { IPaymentMethodPersistenceMappers } from "./payment-method-persistence-mappers.di";
export const buildPaymentMethodRepository = (params: {
database: Sequelize;
mappers: IPaymentMethodPersistenceMappers;
}) => {
const { database, mappers } = params;
return new SequelizePaymentMethodRepository(mappers.domainMapper, mappers.listMapper, database);
};

View File

@ -0,0 +1,96 @@
import { type ModuleParams, type SetupParams, buildTransactionManager } from "@erp/core/api";
import type { Sequelize } from "sequelize";
import {
buildPaymentMethodCreator,
buildPaymentMethodFinder,
buildPaymentMethodInputMappers,
buildPaymentMethodSnapshotBuilders,
} from "../../application";
import type { IPaymentMethodRepository } from "../../application/repositories";
import type { IPaymentMethodFinder } from "../../application/services";
import { PaymentMethodFinder, PaymentMethodUpdater } from "../../application/services";
import {
CreatePaymentMethodUseCase,
type DisablePaymentMethodByIdUseCase,
GetPaymentMethodByIdUseCase,
ListPaymentMethodsUseCase,
UpdatePaymentMethodByIdUseCase,
} from "../../application/use-cases";
import { buildPaymentMethodPersistenceMappers } from "./payment-method-persistence-mappers.di";
import { buildPaymentMethodRepository } from "./payment-method-repositories.di";
export type PaymentMethodsInternalDeps = {
repository: IPaymentMethodRepository;
useCases: {
listPaymentMethods: () => ListPaymentMethodsUseCase;
getPaymentMethodById: () => GetPaymentMethodByIdUseCase;
createPaymentMethod: () => CreatePaymentMethodUseCase;
updatePaymentMethodById: () => UpdatePaymentMethodByIdUseCase;
disablePaymentMethodById: () => DisablePaymentMethodByIdUseCase;
};
};
export const buildPaymentMethodsDependencies = (
params: ModuleParams
): PaymentMethodsInternalDeps => {
const { database } = params;
// Infrastructure
const transactionManager = buildTransactionManager(database as Sequelize);
const persistenceMappers = buildPaymentMethodPersistenceMappers();
const repository = buildPaymentMethodRepository({ database, mappers: persistenceMappers });
// Application helpers
const inputMappers = buildPaymentMethodInputMappers();
const finder = buildPaymentMethodFinder(repository);
const creator = buildPaymentMethodCreator({ repository });
const updater = new PaymentMethodUpdater(repository);
//const disabler = new PaymentMethodDisabler(repository);
const snapshotBuilders = buildPaymentMethodSnapshotBuilders();
return {
repository,
useCases: {
listPaymentMethods: () =>
new ListPaymentMethodsUseCase(finder, snapshotBuilders.summary, transactionManager),
getPaymentMethodById: () =>
new GetPaymentMethodByIdUseCase(finder, snapshotBuilders.full, transactionManager),
createPaymentMethod: () =>
new CreatePaymentMethodUseCase({
dtoMapper: inputMappers.createInputMapper,
creator,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
updatePaymentMethodById: () =>
new UpdatePaymentMethodByIdUseCase({
updater,
finder,
dtoMapper: inputMappers.updateInputMapper,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
/*disablePaymentMethodById: () =>
new DisablePaymentMethodByIdUseCase({
finder,
disabler,
fullSnapshotBuilder,
transactionManager,
}),*/
},
};
};
export const buildPaymentMethodsPublicServices = (
_params: SetupParams,
deps: PaymentMethodsInternalDeps
): { finder: IPaymentMethodFinder } => {
return {
finder: new PaymentMethodFinder(deps.repository),
};
};

View File

@ -0,0 +1 @@
export * from "./payment-methods/payment-methods.routes";

View File

@ -0,0 +1,40 @@
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { CreatePaymentMethodRequestDTO } from "../../../../../common";
import type { CreatePaymentMethodUseCase } from "../../../../application";
import { paymentmethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class CreatePaymentMethodController extends ExpressController {
constructor(private readonly useCase: CreatePaymentMethodUseCase) {
super();
this.errorMapper = paymentmethodsApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards(
requireAuthenticatedGuard(),
requireCompanyContextGuard(),
forbidQueryFieldGuard("companyId")
);
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const dto = this.req.body satisfies CreatePaymentMethodRequestDTO;
const result = await this.useCase.execute({ dto, companyId });
return result.match(
(data) => this.created(data),
(err) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,19 @@
import { ExpressController } from "@erp/core/api";
import type { DisablePaymentMethodByIdUseCase } from "../../../../application";
export class DisablePaymentMethodByIdController extends ExpressController {
constructor(private readonly useCase: DisablePaymentMethodByIdUseCase) {
super();
}
protected async executeImpl() {
const id = this.req.params.payment_method_id;
const result = await this.useCase.execute({ id });
return result.match(
(data) => this.ok(data),
(err) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,39 @@
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { GetPaymentMethodByIdUseCase } from "../../../../application";
import { paymentmethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class GetPaymentMethodByIdController extends ExpressController {
constructor(private readonly useCase: GetPaymentMethodByIdUseCase) {
super();
this.errorMapper = paymentmethodsApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards(
requireAuthenticatedGuard(),
requireCompanyContextGuard(),
forbidQueryFieldGuard("companyId")
);
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const { payment_method_id } = this.req.params;
const result = await this.useCase.execute({ payment_method_id, companyId });
return result.match(
(data) => this.ok(data),
(err) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,5 @@
export * from "./create-payment-method.controller";
export * from "./disable-payment-method-by-id.controller";
export * from "./get-payment-method-by-id.controller";
export * from "./list-payment-methods.controller";
export * from "./update-payment-method-by-id.controller";

View File

@ -0,0 +1,54 @@
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server";
import type { ListPaymentMethodsUseCase } from "../../../../application";
import { paymentmethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class ListPaymentMethodsController extends ExpressController {
constructor(private readonly useCase: ListPaymentMethodsUseCase) {
super();
this.errorMapper = paymentmethodsApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards(
requireAuthenticatedGuard(),
requireCompanyContextGuard(),
forbidQueryFieldGuard("companyId")
);
}
private getCriteriaWithDefaultOrder() {
if (this.criteria.hasOrder()) {
return this.criteria;
}
const { q: quicksearch, filters, pageSize, pageNumber } = this.criteria.toPrimitives();
return Criteria.fromPrimitives(filters, "name", "ASC", pageSize, pageNumber, quicksearch);
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const criteria = this.getCriteriaWithDefaultOrder();
const result = await this.useCase.execute({ criteria, companyId });
return result.match(
(data) =>
this.ok(data, {
"X-Total-Count": String(data.total_items),
"Pagination-Count": String(data.total_pages),
"Pagination-Page": String(data.page),
"Pagination-Limit": String(data.per_page),
}),
(err) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,45 @@
import type { UpdatePaymentMethodByIdRequestDTO } from "@erp/catalogs/common";
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { UpdatePaymentMethodByIdUseCase } from "../../../../application";
import { paymentmethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class UpdatePaymentMethodByIdController extends ExpressController {
constructor(private readonly useCase: UpdatePaymentMethodByIdUseCase) {
super();
this.errorMapper = paymentmethodsApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards(
requireAuthenticatedGuard(),
requireCompanyContextGuard(),
forbidQueryFieldGuard("companyId")
);
}
protected async executeImpl() {
const companyId = this.getTenantId();
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const { payment_method_id } = this.req.params;
if (!payment_method_id) {
return this.invalidInputError("Payment method ID missing");
}
const dto = this.req.body as UpdatePaymentMethodByIdRequestDTO;
const result = await this.useCase.execute({ payment_method_id, companyId, dto });
return result.match(
(data) => this.ok(data),
(err) => this.handleError(err)
);
}
}

View File

@ -0,0 +1,38 @@
import {
ApiErrorMapper,
ConflictApiError,
type ErrorToApiRule,
ValidationApiError,
} from "@erp/core/api";
import {
type InvalidPaymentMethodIdError,
type PaymentMethodCannotBeDeletedError,
isInvalidPaymentMethodIdError,
isPaymentMethodCannotBeDeletedError,
} from "../../../domain/payment-methods";
// Crea una regla específica (prioridad alta para sobreescribir mensajes)
const paymentmethodDuplicateRule: ErrorToApiRule = {
priority: 120,
matches: (e) => isInvalidPaymentMethodIdError(e),
build: (e) =>
new ConflictApiError(
(e as InvalidPaymentMethodIdError).message ||
"Payment method with the provided id already exists."
),
};
const paymentmethodCannotBeDeletedRule: ErrorToApiRule = {
priority: 120,
matches: (e) => isPaymentMethodCannotBeDeletedError(e),
build: (e) =>
new ValidationApiError(
(e as PaymentMethodCannotBeDeletedError).message || "Payment method cannot be deleted."
),
};
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
export const paymentmethodsApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
.register(paymentmethodDuplicateRule)
.register(paymentmethodCannotBeDeletedRule);

View File

@ -0,0 +1,91 @@
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
import { type RequestWithAuth, type StartParams, validateRequest } from "@erp/core/api";
import { type NextFunction, type Request, type Response, Router } from "express";
import {
CreatePaymentMethodRequestSchema,
GetPaymentMethodByIdRequestSchema,
ListPaymentMethodsRequestSchema,
UpdatePaymentMethodByIdParamsRequestSchema,
UpdatePaymentMethodByIdRequestSchema,
} from "../../../../common/dto/payment-methods/request";
import type { CatalogsInternalDeps } from "../../di/catalogs.di";
import {
CreatePaymentMethodController,
DisablePaymentMethodByIdController,
GetPaymentMethodByIdController,
ListPaymentMethodsController,
UpdatePaymentMethodByIdController,
} from "./controllers";
export const paymentMethodsRouter = (params: StartParams) => {
const { app, config, getInternal } = params;
const deps = getInternal<CatalogsInternalDeps>("catalogs").paymentMethods;
const router = Router({ mergeParams: true });
// ----------------------------------------------
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
// 🔐 Autenticación + Tenancy para TODO el router
router.use(
(req: Request, res: Response, next: NextFunction) =>
mockUser(req as RequestWithAuth, res, next) // Debe ir antes de las rutas protegidas
);
}
router.use([
(req: Request, res: Response, next: NextFunction) =>
requireAuthenticated()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas
(req: Request, res: Response, next: NextFunction) =>
requireCompanyContext()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas
]);
// ----------------------------------------------
router.get("/", validateRequest(ListPaymentMethodsRequestSchema, "query"), (req, res, next) => {
const controller = new ListPaymentMethodsController(deps.useCases.listPaymentMethods());
return controller.execute(req, res, next);
});
router.post("/", validateRequest(CreatePaymentMethodRequestSchema, "body"), (req, res, next) => {
const controller = new CreatePaymentMethodController(deps.useCases.createPaymentMethod());
return controller.execute(req, res, next);
});
router.get(
"/:payment_method_id",
validateRequest(GetPaymentMethodByIdRequestSchema, "params"),
(req, res, next) => {
const controller = new GetPaymentMethodByIdController(deps.useCases.getPaymentMethodById());
return controller.execute(req, res, next);
}
);
router.patch(
"/:payment_method_id",
validateRequest(UpdatePaymentMethodByIdParamsRequestSchema, "params"),
validateRequest(UpdatePaymentMethodByIdRequestSchema, "body"),
(req, res, next) => {
const controller = new UpdatePaymentMethodByIdController(
deps.useCases.updatePaymentMethodById()
);
return controller.execute(req, res, next);
}
);
router.patch(
"/:payment_method_id/disable",
validateRequest(GetPaymentMethodByIdRequestSchema, "params"),
(req, res, next) => {
const controller = new DisablePaymentMethodByIdController(
deps.useCases.disablePaymentMethodById()
);
return controller.execute(req, res, next);
}
);
app.use(`${config.server.apiBasePath}/catalogs/payment-methods`, router);
};

View File

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

View File

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

View File

@ -0,0 +1,7 @@
export * from "./mappers";
export * from "./models";
export * from "./repositories";
import paymentMethodModelInit from "./models/sequelize-payment-method.model";
export const models = [paymentMethodModelInit];

View File

@ -0,0 +1,2 @@
export * from "./sequelize-payment-method-domain.mapper";
export * from "./sequelize-payment-method-summary.mapper";

View File

@ -0,0 +1,85 @@
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import {
Name,
TextValue,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
maybeToNullable,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { PaymentMethod } from "../../../../domain";
import type { PaymentMethodCreationAttributes, PaymentMethodModel } from "../models";
export class SequelizePaymentMethodDomainMapper extends SequelizeDomainMapper<
PaymentMethodModel,
PaymentMethodCreationAttributes,
PaymentMethod
> {
public mapToDomain(
source: PaymentMethodModel,
params?: MapperParamsType
): Result<PaymentMethod, Error> {
try {
const errors: ValidationErrorDetail[] = [];
const idResult = UniqueID.create(source.id, true);
if (idResult.isFailure) {
return Result.fail(idResult.error);
}
const companyId = extractOrPushError(
UniqueID.create(source.company_id),
"company_id",
errors
);
const name = extractOrPushError(Name.create(source.name), "name", errors);
const description = extractOrPushError(
maybeFromNullableResult(source.description, (value) => TextValue.create(value)),
"description",
errors
);
// Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Payment method props mapping failed", errors)
);
}
const paymentMethod = PaymentMethod.rehydrate(
{
companyId: companyId!,
name: name!,
description: description!,
isActive: source.is_active,
isSystem: source.is_system,
},
idResult.data
);
return Result.ok(paymentMethod);
} catch (error: unknown) {
return Result.fail(error as Error);
}
}
public mapToPersistence(
source: PaymentMethod,
params?: MapperParamsType
): Result<PaymentMethodCreationAttributes, Error> {
return Result.ok<PaymentMethodCreationAttributes>({
id: source.id.toPrimitive(),
company_id: source.companyId.toPrimitive(),
name: source.name.toPrimitive(),
description: maybeToNullable(source.description, (description) => description.toPrimitive()),
is_active: source.isActive,
is_system: source.isSystem,
});
}
}

View File

@ -0,0 +1,46 @@
import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api";
import {
Name,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { PaymentMethodSummary } from "../../../../application/models";
import type { PaymentMethodModel } from "../models";
export class SequelizePaymentMethodSummaryMapper extends SequelizeQueryMapper<
PaymentMethodModel,
PaymentMethodSummary
> {
public mapToReadModel(
raw: PaymentMethodModel,
params?: MapperParamsType
): Result<PaymentMethodSummary, Error> {
const errors: ValidationErrorDetail[] = [];
// 1) Valores escalares (atributos generales)
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
const id = extractOrPushError(UniqueID.create(raw.id), "id", errors);
const name = extractOrPushError(Name.create(raw.name), "name", errors);
const isActive = raw.is_active;
const isSystem = raw.is_system;
// Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("PaymentMethod mapping failed [mapToDTO]", errors)
);
}
return Result.ok<PaymentMethodSummary>({
id: id!,
companyId: companyId!,
name: name!,
isActive: isActive,
isSystem: isSystem,
});
}
}

View File

@ -0,0 +1 @@
export * from "./sequelize-payment-method.model";

View File

@ -0,0 +1,87 @@
import {
type CreationOptional,
DataTypes,
type InferAttributes,
type InferCreationAttributes,
Model,
type Sequelize,
} from "sequelize";
export type PaymentMethodCreationAttributes = InferCreationAttributes<PaymentMethodModel, {}> & {};
export class PaymentMethodModel extends Model<
InferAttributes<PaymentMethodModel>,
InferCreationAttributes<PaymentMethodModel>
> {
declare id: string;
declare company_id: string;
declare name: string;
declare description: CreationOptional<string | null>;
declare is_active: boolean;
declare is_system: boolean;
static associate(_database: Sequelize) {}
static hooks(_database: Sequelize) {}
}
export default (database: Sequelize) => {
PaymentMethodModel.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
},
company_id: {
type: DataTypes.UUID,
allowNull: false,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
description: {
type: DataTypes.STRING,
allowNull: true,
},
is_active: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
is_system: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
},
{
sequelize: database,
modelName: "PaymentMethodModel",
tableName: "payment_methods",
underscored: true,
paranoid: true, // softs deletes
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at",
deletedAt: "deleted_at",
indexes: [
{
name: "idx_payment_methods_company",
fields: ["company_id", "deleted_at", "name"],
},
],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
defaultScope: {},
scopes: {},
}
);
return PaymentMethodModel;
};

View File

@ -0,0 +1 @@
export * from "./sequelize-payment-method.repository";

View File

@ -0,0 +1,226 @@
import {
EntityNotFoundError,
InfrastructureRepositoryError,
SequelizeRepository,
translateSequelizeError,
} from "@erp/core/api";
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import { type Collection, Result } from "@repo/rdx-utils";
import type { Sequelize, Transaction } from "sequelize";
import type { IPaymentMethodRepository } from "../../../../application";
import type { PaymentMethodSummary } from "../../../../application/models";
import type { PaymentMethod } from "../../../../domain";
import type { SequelizePaymentMethodSummaryMapper } from "../mappers";
import type { SequelizePaymentMethodDomainMapper } from "../mappers/sequelize-payment-method-domain.mapper";
import { PaymentMethodModel } from "../models";
export class SequelizePaymentMethodRepository
extends SequelizeRepository<PaymentMethod>
implements IPaymentMethodRepository
{
constructor(
private readonly domainMapper: SequelizePaymentMethodDomainMapper,
private readonly summaryMapper: SequelizePaymentMethodSummaryMapper,
database: Sequelize
) {
super({ database });
}
/**
*
* Crea un nuevo método de pago
*
* @param paymentMethod - El método de pago nuevo a guardar.
* @param transaction - Transacción activa para la operación.
* @returns Result<void, Error>
*/
async create(
paymentMethod: PaymentMethod,
transaction?: Transaction
): Promise<Result<void, Error>> {
try {
const dtoResult = this.domainMapper.mapToPersistence(paymentMethod);
if (dtoResult.isFailure) {
return Result.fail(dtoResult.error);
}
await PaymentMethodModel.create(dtoResult.data, { transaction });
return Result.ok();
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
/**
* Actualiza un método de pago existente.
*
* @param paymentMethod - El método de pago a actualizar.
* @param transaction - Transacción activa para la operación.
* @returns Result<void, Error>
*/
async update(
paymentMethod: PaymentMethod,
transaction?: Transaction
): Promise<Result<void, Error>> {
try {
const dtoResult = this.domainMapper.mapToPersistence(paymentMethod);
if (dtoResult.isFailure) {
return Result.fail(dtoResult.error);
}
const { id, ...payload } = dtoResult.data;
const [affected] = await PaymentMethodModel.update(payload, {
where: { id },
transaction,
individualHooks: true,
});
if (affected === 0) {
return Result.fail(
new InfrastructureRepositoryError("Concurrency conflict or payment method not found")
);
}
return Result.ok();
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
/**
* Comprueba si existe un PaymentMethod con un `id` dentro de una `company`.
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el método de pago.
* @param id - Identificador UUID del método de pago.
* @param transaction - Transacción activa para la operación.
* @returns Result<boolean, Error>
*/
async existsByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
try {
const count = await PaymentMethodModel.count({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
return Result.ok(Boolean(count > 0));
} catch (error: unknown) {
return Result.fail(translateSequelizeError(error));
}
}
/**
* Recupera un método de pago por su ID y companyId.
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el método de pago.
* @param id - Identificador UUID del método de pago.
* @param transaction - Transacción activa para la operación.
* @returns Result<PaymentMethod, Error>
*/
async getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: Transaction
): Promise<Result<PaymentMethod, Error>> {
try {
const row = await PaymentMethodModel.findOne({
where: {
id: id.toString(),
company_id: companyId.toString(),
},
transaction,
});
if (!row) {
return Result.fail(new EntityNotFoundError("PaymentMethod", "id", id.toString()));
}
return this.domainMapper.mapToDomain(row);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
/**
* Recupera múltiples customers dentro de una empresa según un criterio dinámico (búsqueda, paginación, etc.).
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param criteria - Criterios de búsqueda.
* @param transaction - Transacción activa para la operación.
* @returns Result<Collection<CustomerListDTO>, Error>
*
* @see Criteria
*/
async findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<PaymentMethodSummary>, Error>> {
try {
const criteriaConverter = new CriteriaToSequelizeConverter();
const query = criteriaConverter.convert(criteria, {
searchableFields: [],
sortableFields: ["name"],
enableFullText: true,
database: this.database,
strictMode: true, // fuerza error si ORDER BY no permitido
});
query.where = {
...query.where,
company_id: companyId.toString(),
deleted_at: null,
};
const [rows, count] = await Promise.all([
PaymentMethodModel.findAll({
...query,
transaction,
}),
PaymentMethodModel.count({
where: query.where,
distinct: true, // evita duplicados por LEFT JOIN
transaction,
}),
]);
return this.summaryMapper.mapToReadModelCollection(rows, count);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
/**
*
* Elimina o marca como eliminado una forma de pago.
*
* @param companyId - Identificador UUID de la empresa a la que pertenece la forma de pago.
* @param id - UUID de la forma de pago a eliminar.
* @param transaction - Transacción activa para la operación.
* @returns Result<boolean, Error>
*/
async deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction
): Promise<Result<boolean, Error>> {
try {
const deleted = await PaymentMethodModel.destroy({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
if (deleted === 0) {
return Result.fail(new EntityNotFoundError("PaymentMethod", "id", id.toString()));
}
return Result.ok(true);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
}

View File

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

View File

@ -0,0 +1,2 @@
export * from "./request";
export * from "./response";

View File

@ -0,0 +1,12 @@
import { z } from "zod/v4";
export const CreatePaymentMethodRequestSchema = z.object({
id: z.uuid(),
name: z.string(),
description: z.string().nullable().optional(),
is_active: z.boolean(),
});
export type CreatePaymentMethodRequestDTO = z.infer<typeof CreatePaymentMethodRequestSchema>;

View File

@ -0,0 +1,7 @@
import { z } from "zod/v4";
export const GetPaymentMethodByIdRequestSchema = z.object({
payment_method_id: z.uuid(),
});
export type GetPaymentMethodByIdRequestDTO = z.infer<typeof GetPaymentMethodByIdRequestSchema>;

View File

@ -0,0 +1,4 @@
export * from "./create-payment-method.request.dto";
export * from "./get-payment-method-by-id.request.dto";
export * from "./list-payment-methods.request.dto";
export * from "./update-payment-method-by-id.request.dto";

View File

@ -0,0 +1,5 @@
import { CriteriaSchema } from "@erp/core";
import type { z } from "zod/v4";
export const ListPaymentMethodsRequestSchema = CriteriaSchema;
export type ListPaymentMethodsRequestDTO = z.infer<typeof ListPaymentMethodsRequestSchema>;

View File

@ -0,0 +1,18 @@
import { z } from "zod/v4";
export const UpdatePaymentMethodByIdParamsRequestSchema = z.object({
payment_method_id: z.uuid(),
});
export const UpdatePaymentMethodByIdRequestSchema = z.object({
name: z.string().optional(),
description: z.string().nullable().optional(),
is_active: z.boolean().optional(),
});
export type UpdatePaymentMethodByIdParamsRequestDTO = z.infer<
typeof UpdatePaymentMethodByIdParamsRequestSchema
>;
export type UpdatePaymentMethodByIdRequestDTO = z.infer<
typeof UpdatePaymentMethodByIdRequestSchema
>;

View File

@ -0,0 +1,6 @@
import type { z } from "zod/v4";
import { PaymentMethodDetailSchema } from "../shared";
export const CreatePaymentMethodResponseSchema = PaymentMethodDetailSchema;
export type CreatePaymentMethodResponseDTO = z.infer<typeof CreatePaymentMethodResponseSchema>;

View File

@ -0,0 +1,8 @@
import type { z } from "zod/v4";
import { PaymentMethodDetailSchema } from "../shared";
export const DisablePaymentMethodByIdResponseSchema = PaymentMethodDetailSchema;
export type DisablePaymentMethodByIdResponseDTO = z.infer<
typeof DisablePaymentMethodByIdResponseSchema
>;

View File

@ -0,0 +1,6 @@
import type { z } from "zod/v4";
import { PaymentMethodDetailSchema } from "../shared";
export const GetPaymentMethodByIdResponseSchema = PaymentMethodDetailSchema;
export type GetPaymentMethodByIdResponseDTO = z.infer<typeof GetPaymentMethodByIdResponseSchema>;

View File

@ -0,0 +1,5 @@
export * from "./create-payment-method.response.dto";
export * from "./disable-payment-method-by-id.response.dto";
export * from "./get-payment-method-by-id.response.dto";
export * from "./list-payment-methods.response.dto";
export * from "./update-payment-method-by-id.response.dto";

View File

@ -0,0 +1,6 @@
import { z } from "zod/v4";
import { PaymentMethodSummarySchema } from "../shared";
export const ListPaymentMethodsResponseSchema = z.array(PaymentMethodSummarySchema);
export type ListPaymentMethodsResponseDTO = z.infer<typeof ListPaymentMethodsResponseSchema>;

View File

@ -0,0 +1,8 @@
import type { z } from "zod/v4";
import { PaymentMethodDetailSchema } from "../shared";
export const UpdatePaymentMethodByIdResponseSchema = PaymentMethodDetailSchema;
export type UpdatePaymentMethodByIdResponseDTO = z.infer<
typeof UpdatePaymentMethodByIdResponseSchema
>;

View File

@ -0,0 +1,2 @@
export * from "./payment-method-detail.dto";
export * from "./payment-method-summary.dto";

View File

@ -0,0 +1,13 @@
import { z } from "zod/v4";
export const PaymentMethodDetailSchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
name: z.string(),
description: z.string().nullable(),
is_active: z.boolean(),
is_system: z.boolean(),
});
export type PaymentMethodDetailDTO = z.infer<typeof PaymentMethodDetailSchema>;

View File

@ -0,0 +1,12 @@
import { z } from "zod/v4";
export const PaymentMethodSummarySchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
name: z.string(),
is_active: z.boolean(),
is_system: z.boolean(),
});
export type PaymentMethodSummaryDTO = z.infer<typeof PaymentMethodSummarySchema>;

View File

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

View File

@ -0,0 +1,26 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"paths": {
"@erp/catalogs/*": ["./src/*"]
},
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@ -21,7 +21,7 @@ import {
ListProformasUseCase,
ReportProformaUseCase,
} from "../use-cases";
import { UpdateProformaUseCase } from "../use-cases/update-proforma.use-case";
import { UpdateProformaByIdUseCase } from "../use-cases/update-proforma-by-id.use-case";
export function buildGetProformaByIdUseCase(deps: {
finder: IProformaFinder;
@ -101,7 +101,7 @@ export function buildUpdateProformaUseCase(deps: {
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new UpdateProformaUseCase({
return new UpdateProformaByIdUseCase({
dtoMapper: deps.dtoMapper,
updater: deps.updater,
fullSnapshotBuilder: deps.fullSnapshotBuilder,

View File

@ -5,4 +5,4 @@ export * from "./get-proforma-by-id.use-case";
export * from "./issue-proforma.use-case";
export * from "./list-proformas.use-case";
export * from "./report-proforma.use-case";
export * from "./update-proforma.use-case";
export * from "./update-proforma-by-id.use-case";

View File

@ -21,7 +21,7 @@ type UpdateProformaUseCaseDeps = {
transactionManager: ITransactionManager;
};
export class UpdateProformaUseCase {
export class UpdateProformaByIdUseCase {
private readonly dtoMapper: IUpdateProformaInputMapper;
private readonly updater: IProformaUpdater;
private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder;

View File

@ -7,7 +7,7 @@ import {
type IssueProformaUseCase,
type ListProformasUseCase,
type ReportProformaUseCase,
type UpdateProformaUseCase,
type UpdateProformaByIdUseCase,
buildCreateProformaUseCase,
buildGetProformaByIdUseCase,
buildIssueProformaUseCase,
@ -37,7 +37,7 @@ export type ProformasInternalDeps = {
issueProforma: (publicServices: {
issuedInvoiceServices: IIssuedInvoicePublicServices;
}) => IssueProformaUseCase;
updateProforma: () => UpdateProformaUseCase;
updateProforma: () => UpdateProformaByIdUseCase;
/*
deleteProforma: () => DeleteProformaUseCase;

View File

@ -27,7 +27,7 @@ export class CreateProformaController extends ExpressController {
if (!companyId) {
return this.forbiddenError("Tenant ID not found");
}
const dto = this.req.body as CreateProformaRequestDTO;
const dto = this.req.body satisfies CreateProformaRequestDTO;
const result = await this.useCase.execute({ dto, companyId });

View File

@ -6,11 +6,11 @@ import {
} from "@erp/core/api";
import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto/index.ts";
import type { UpdateProformaUseCase } from "../../../../application/index.ts";
import type { UpdateProformaByIdUseCase } from "../../../../application/index.ts";
import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
export class UpdateProformaController extends ExpressController {
public constructor(private readonly useCase: UpdateProformaUseCase) {
public constructor(private readonly useCase: UpdateProformaByIdUseCase) {
super();
this.errorMapper = proformasApiErrorMapper;

View File

@ -56,6 +56,9 @@ importers:
'@erp/auth':
specifier: workspace:*
version: link:../../modules/auth
'@erp/catalogs':
specifier: workspace:*
version: link:../../modules/catalogs
'@erp/core':
specifier: workspace:*
version: link:../../modules/core
@ -375,6 +378,40 @@ importers:
specifier: ^6.0.2
version: 6.0.2
modules/catalogs:
dependencies:
'@erp/auth':
specifier: workspace:*
version: link:../auth
'@erp/core':
specifier: workspace:*
version: link:../core
'@repo/rdx-criteria':
specifier: workspace:*
version: link:../../packages/rdx-criteria
'@repo/rdx-ddd':
specifier: workspace:*
version: link:../../packages/rdx-ddd
'@repo/rdx-utils':
specifier: workspace:*
version: link:../../packages/rdx-utils
express:
specifier: ^4.22.1
version: 4.22.1
sequelize:
specifier: ^6.37.8
version: 6.37.8(mysql2@3.22.0(@types/node@25.6.0))(pg-hstore@2.3.4)
zod:
specifier: ^4.3.6
version: 4.3.6
devDependencies:
'@types/express':
specifier: ^4.17.21
version: 4.17.25
typescript:
specifier: ^6.0.2
version: 6.0.2
modules/core:
dependencies:
'@hookform/resolvers':

View File

@ -7,7 +7,11 @@
"settings": {
"chatgpt.openOnStartup": true,
"chat.tools.terminal.autoApprove": {
"pnpm": true
"pnpm": true,
"/^cd /home/rodax/Documentos/uecko-erp && ls -l node_modules/@repo/typescript-config/root\\.json && node -p \"require\\.resolve\\('@repo/typescript-config/root\\.json'\\)\"$/": {
"approve": true,
"matchCommandLine": true
}
}
}
}