Formas de pago

This commit is contained in:
David Arranz 2026-05-21 11:37:50 +02:00
parent cd4b8975c3
commit 32dbd7d31f
37 changed files with 726 additions and 75 deletions

View File

@ -50,7 +50,10 @@
"noInferrableTypes": "error",
"noNamespace": "error",
"noNegationElse": "warn",
"noNonNullAssertion": "info",
"noNonNullAssertion": {
"level": "info",
"fix": "none"
},
"noParameterAssign": "error",
"noUnusedTemplateLiteral": "error",
"noUselessElse": "warn",

View File

@ -1,5 +1,7 @@
export * from "./payment-method-creator.di";
export * from "./payment-method-deleter.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-status-changer.di";
export * from "./payment-method-updater.di";

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
export * from "./payment-method-creator";
export * from "./payment-method-deleter";
export * from "./payment-method-disabler";
export * from "./payment-method-enabler";
export * from "./payment-method-finder";
export * from "./payment-method-public-services";
export * from "./payment-method-status-changer";
export * from "./payment-method-updater";

View File

@ -0,0 +1,41 @@
import { Result } from "@repo/rdx-utils";
import type { PaymentMethod } from "../../domain";
import { PaymentMethodCannotBeDeletedError } from "../../domain";
import type { IPaymentMethodRepository } from "../repositories";
export interface IPaymentMethodDeleter {
delete(params: {
paymentMethod: PaymentMethod;
transaction?: unknown;
}): Promise<Result<PaymentMethod, Error>>;
}
export class PaymentMethodDeleter implements IPaymentMethodDeleter {
public constructor(private readonly repository: IPaymentMethodRepository) {}
public async delete(params: {
paymentMethod: PaymentMethod;
transaction?: unknown;
}): Promise<Result<PaymentMethod, Error>> {
const { paymentMethod, transaction } = params;
if (paymentMethod.isSystem) {
return Result.fail(
new PaymentMethodCannotBeDeletedError("System payment methods cannot be deleted.")
);
}
const deleteResult = await this.repository.deleteByIdInCompany(
paymentMethod.companyId,
paymentMethod.id,
transaction
);
if (deleteResult.isFailure) {
return Result.fail(deleteResult.error);
}
return Result.ok(paymentMethod);
}
}

View File

@ -1,5 +1,4 @@
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { PaymentMethod } from "../../domain";
import type { IPaymentMethodRepository } from "../repositories";
@ -7,7 +6,7 @@ import type { IPaymentMethodRepository } from "../repositories";
export interface IPaymentMethodDisabler {
disable(params: {
paymentMethod: PaymentMethod;
transaction?: Transaction;
transaction?: unknown;
}): Promise<Result<PaymentMethod, Error>>;
}
@ -16,16 +15,22 @@ export class PaymentMethodDisabler implements IPaymentMethodDisabler {
async disable(params: {
paymentMethod: PaymentMethod;
transaction?: Transaction;
transaction?: unknown;
}): Promise<Result<PaymentMethod, Error>> {
const { paymentMethod, transaction } = params;
const disableResult = paymentMethod.disable();
if (disableResult.isFailure) {
return Result.fail(disableResult.error);
}
if (!disableResult.data) {
return Result.ok(paymentMethod);
}
const persistenceResult = await this.repository.update(paymentMethod, transaction);
if (persistenceResult.isFailure) {
return Result.fail(persistenceResult.error);
}

View File

@ -0,0 +1,40 @@
import { Result } from "@repo/rdx-utils";
import type { PaymentMethod } from "../../domain";
import type { IPaymentMethodRepository } from "../repositories";
export interface IPaymentMethodEnabler {
enable(params: {
paymentMethod: PaymentMethod;
transaction?: unknown;
}): Promise<Result<PaymentMethod, Error>>;
}
export class PaymentMethodEnabler implements IPaymentMethodEnabler {
constructor(private readonly repository: IPaymentMethodRepository) {}
async enable(params: {
paymentMethod: PaymentMethod;
transaction?: unknown;
}): Promise<Result<PaymentMethod, Error>> {
const { paymentMethod, transaction } = params;
const enableResult = paymentMethod.enable();
if (enableResult.isFailure) {
return Result.fail(enableResult.error);
}
if (!enableResult.data) {
return Result.ok(paymentMethod);
}
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,44 @@
import { Result } from "@repo/rdx-utils";
import type { PaymentMethod } from "../../domain";
import type { IPaymentMethodRepository } from "../repositories";
export type PaymentMethodStatusChangeAction = "enable" | "disable";
export interface IPaymentMethodStatusChanger {
changeStatus(params: {
paymentMethod: PaymentMethod;
action: PaymentMethodStatusChangeAction;
transaction?: unknown;
}): Promise<Result<PaymentMethod, Error>>;
}
export class PaymentMethodStatusChanger implements IPaymentMethodStatusChanger {
public constructor(private readonly repository: IPaymentMethodRepository) {}
public async changeStatus(params: {
paymentMethod: PaymentMethod;
action: PaymentMethodStatusChangeAction;
transaction?: unknown;
}): Promise<Result<PaymentMethod, Error>> {
const { paymentMethod, action, transaction } = params;
const statusResult = action === "enable" ? paymentMethod.enable() : paymentMethod.disable();
if (statusResult.isFailure) {
return Result.fail(statusResult.error);
}
if (!statusResult.data) {
return Result.ok(paymentMethod);
}
const persistenceResult = await this.repository.update(paymentMethod, transaction);
if (persistenceResult.isFailure) {
return Result.fail(persistenceResult.error);
}
return Result.ok(paymentMethod);
}
}

View File

@ -40,6 +40,13 @@ export class PaymentMethodUpdater implements IPaymentMethodUpdater {
return Result.fail(updateResult.error);
}
const hasChanges = updateResult.data;
if (!hasChanges) {
// No hay cambios, retornar el agregado original
return Result.ok(paymentMethod);
}
// Persistir cambios
const saveResult = await this.repository.update(paymentMethod, transaction);

View File

@ -11,6 +11,7 @@ export class PaymentMethodFullSnapshotBuilder implements IPaymentMethodFullSnaps
public toOutput(paymentMethod: PaymentMethod): GetPaymentMethodByIdResponseDTO {
return {
id: paymentMethod.id.toPrimitive(),
company_id: paymentMethod.companyId.toPrimitive(),
name: paymentMethod.name.toPrimitive(),
description: toNullable(paymentMethod.description, (value) => value.toPrimitive()),
is_active: paymentMethod.isActive,

View File

@ -0,0 +1,57 @@
import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IPaymentMethodDeleter, IPaymentMethodFinder } from "../services";
import type { IPaymentMethodFullSnapshotBuilder } from "../snapshot-builders";
export type DeletePaymentMethodByIdUseCaseInput = {
companyId: UniqueID;
payment_method_id: string;
};
export class DeletePaymentMethodByIdUseCase {
constructor(
private readonly deps: {
finder: IPaymentMethodFinder;
deleter: IPaymentMethodDeleter;
fullSnapshotBuilder: IPaymentMethodFullSnapshotBuilder;
transactionManager: ITransactionManager;
}
) {}
public execute(params: DeletePaymentMethodByIdUseCaseInput) {
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.deps.transactionManager.complete(async (transaction: unknown) => {
try {
const findResult = await this.deps.finder.findPaymentMethodById(
companyId,
paymentMethodId,
transaction
);
if (findResult.isFailure) {
return Result.fail(findResult.error);
}
const deleteResult = await this.deps.deleter.delete({
paymentMethod: findResult.data,
transaction,
});
if (deleteResult.isFailure) {
return Result.fail(deleteResult.error);
}
return Result.ok(this.deps.fullSnapshotBuilder.toOutput(deleteResult.data));
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -1,39 +1,50 @@
import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { IPaymentMethodDisabler, IPaymentMethodFinder } from "../services";
import type { IPaymentMethodFinder, IPaymentMethodStatusChanger } from "../services";
import type { IPaymentMethodFullSnapshotBuilder } from "../snapshot-builders";
export type DisablePaymentMethodByIdUseCaseInput = {
id: string;
companyId: UniqueID;
payment_method_id: string;
};
export class DisablePaymentMethodByIdUseCase {
constructor(
private readonly deps: {
finder: IPaymentMethodFinder;
disabler: IPaymentMethodDisabler;
changer: IPaymentMethodStatusChanger;
fullSnapshotBuilder: IPaymentMethodFullSnapshotBuilder;
transactionManager: ITransactionManager;
}
) {}
public execute(params: DisablePaymentMethodByIdUseCaseInput) {
const { id } = params;
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.deps.transactionManager.complete(async (transaction: unknown) => {
const tx = transaction as Transaction;
try {
const findResult = await this.deps.finder.getById(id, tx);
const findResult = await this.deps.finder.findPaymentMethodById(
companyId,
paymentMethodId,
transaction
);
if (findResult.isFailure) {
return Result.fail(findResult.error);
}
const disableResult = await this.deps.disabler.disable({
const disableResult = await this.deps.changer.changeStatus({
paymentMethod: findResult.data,
transaction: tx,
action: "disable",
transaction,
});
if (disableResult.isFailure) {
return Result.fail(disableResult.error);
}

View File

@ -0,0 +1,58 @@
import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IPaymentMethodFinder, IPaymentMethodStatusChanger } from "../services";
import type { IPaymentMethodFullSnapshotBuilder } from "../snapshot-builders";
export type EnablePaymentMethodByIdUseCaseInput = {
companyId: UniqueID;
payment_method_id: string;
};
export class EnablePaymentMethodByIdUseCase {
constructor(
private readonly deps: {
finder: IPaymentMethodFinder;
changer: IPaymentMethodStatusChanger;
fullSnapshotBuilder: IPaymentMethodFullSnapshotBuilder;
transactionManager: ITransactionManager;
}
) {}
public execute(params: EnablePaymentMethodByIdUseCaseInput) {
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.deps.transactionManager.complete(async (transaction: unknown) => {
try {
const findResult = await this.deps.finder.findPaymentMethodById(
companyId,
paymentMethodId,
transaction
);
if (findResult.isFailure) {
return Result.fail(findResult.error);
}
const enableResult = await this.deps.changer.changeStatus({
paymentMethod: findResult.data,
action: "enable",
transaction,
});
if (enableResult.isFailure) {
return Result.fail(enableResult.error);
}
return Result.ok(this.deps.fullSnapshotBuilder.toOutput(enableResult.data));
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -1,5 +1,7 @@
export * from "./create-payment-method.use-case";
export * from "./delete-payment-method-by-id.use-case";
export * from "./disable-payment-method-by-id.use-case";
export * from "./enable-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

@ -7,6 +7,8 @@ export class InvalidPaymentMethodIdError extends DomainError {
export const isInvalidPaymentMethodIdError = (e: unknown): e is InvalidPaymentMethodIdError =>
e instanceof InvalidPaymentMethodIdError;
//
export class InvalidPaymentMethodNameError extends DomainError {
public readonly code = "PAYMENT_METHOD_NAME" as const;
}
@ -14,10 +16,17 @@ export class InvalidPaymentMethodNameError extends DomainError {
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 const isPaymentMethodNotFoundError = (e: unknown): e is PaymentMethodNotFoundError =>
e instanceof PaymentMethodNotFoundError;
//
export class PaymentMethodCannotBeDeletedError extends DomainError {
public readonly code = "PAYMENT_METHOD_CANNOT_BE_DELETED" as const;
}
@ -25,3 +34,33 @@ export class PaymentMethodCannotBeDeletedError extends DomainError {
export const isPaymentMethodCannotBeDeletedError = (
e: unknown
): e is PaymentMethodCannotBeDeletedError => e instanceof PaymentMethodCannotBeDeletedError;
//
export class PaymentMethodCannotBeDisabledError extends DomainError {
public readonly code = "PAYMENT_METHOD_CANNOT_BE_DISABLED" as const;
}
export const isPaymentMethodCannotBeDisabledError = (
e: unknown
): e is PaymentMethodCannotBeDisabledError => e instanceof PaymentMethodCannotBeDisabledError;
//
export class PaymentMethodCannotBeEnabledError extends DomainError {
public readonly code = "PAYMENT_METHOD_CANNOT_BE_ENABLED" as const;
}
export const isPaymentMethodCannotBeEnabledError = (
e: unknown
): e is PaymentMethodCannotBeEnabledError => e instanceof PaymentMethodCannotBeEnabledError;
//
export class PaymentMethodCannotBeUpdatedError extends DomainError {
public readonly code = "PAYMENT_METHOD_CANNOT_BE_UPDATED" as const;
}
export const isPaymentMethodCannotBeUpdatedError = (
e: unknown
): e is PaymentMethodCannotBeUpdatedError => e instanceof PaymentMethodCannotBeUpdatedError;

View File

@ -1,6 +1,8 @@
import { AggregateRoot, type Name, type TextValue, type UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils";
import { PaymentMethodCannotBeDisabledError, PaymentMethodCannotBeUpdatedError } from "./errors";
export interface IPaymentMethodCreateProps {
companyId: UniqueID;
name: Name;
@ -9,14 +11,14 @@ export interface IPaymentMethodCreateProps {
isSystem: boolean;
}
export type PaymentMethodPatchProps = Partial<
Omit<IPaymentMethodCreateProps, "companyId" | "isSystem">
>;
export type PaymentMethodPatchProps = Partial<{
name: Name;
description: Maybe<TextValue>;
isActive: boolean;
}>;
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
@ -46,7 +48,23 @@ export class PaymentMethod extends AggregateRoot<PaymentMethodInternalProps> {
return new PaymentMethod(props, id);
}
private static validateCreateProps(_props: IPaymentMethodCreateProps): Result<void, Error> {
private static validateCreateProps(props: IPaymentMethodCreateProps): Result<void, Error> {
if (!props.companyId) {
return Result.fail(new Error("Payment method company ID is required"));
}
if (!props.name) {
return Result.fail(new Error("Payment method name is required"));
}
return Result.ok();
}
private static validatePatchProps(patchProps: PaymentMethodPatchProps): Result<void, Error> {
if (Object.keys(patchProps).length === 0) {
return Result.ok();
}
return Result.ok();
}
@ -70,38 +88,96 @@ export class PaymentMethod extends AggregateRoot<PaymentMethodInternalProps> {
return this.props.isSystem;
}
public update(props: Partial<PaymentMethodPatchProps>): Result<void, Error> {
if (props.name !== undefined) {
this.props.name = props.name;
public update(patchProps: PaymentMethodPatchProps): Result<boolean, Error> {
if (this.isSystem) {
return Result.fail(
new PaymentMethodCannotBeUpdatedError("System payment methods cannot be updated.")
);
}
const validationResult = PaymentMethod.validatePatchProps(patchProps);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
if (props.description !== undefined) {
this.props.description = props.description;
let hasChanges = false;
if (
patchProps.name !== undefined &&
this.props.name.toPrimitive() !== patchProps.name.toPrimitive()
) {
this.props.name = patchProps.name;
hasChanges = true;
}
if (props.isActive !== undefined) {
this.props.isActive = props.isActive;
if (
patchProps.description !== undefined &&
!PaymentMethod.sameDescription(this.props.description, patchProps.description)
) {
this.props.description = patchProps.description;
hasChanges = true;
}
return Result.ok();
if (patchProps.isActive !== undefined && this.props.isActive !== patchProps.isActive) {
this.props.isActive = patchProps.isActive;
hasChanges = true;
}
return Result.ok(hasChanges);
}
public disable(): Result<void, Error> {
public disable(): Result<boolean, Error> {
if (this.isSystem) {
return Result.fail(
new PaymentMethodCannotBeDisabledError("System payment methods cannot be disabled.")
);
}
if (!this.isActive) {
return Result.ok();
return Result.ok(false);
}
this.props.isActive = false;
return Result.ok();
return Result.ok(true);
}
public enable(): Result<boolean, Error> {
if (this.isSystem) {
return Result.ok(false);
}
if (this.isActive) {
return Result.ok(false);
}
this.props.isActive = true;
return Result.ok(true);
}
public toJSON() {
return {
id: this.id.toPrimitive(),
name: this.props.name.toPrimitive(),
description: this.props.description,
company_id: this.companyId.toPrimitive(),
name: this.name.toPrimitive(),
description: this.description.match(
(value) => value.toPrimitive(),
() => null
),
is_active: this.isActive,
is_system: this.isSystem,
};
}
private static sameDescription(current: Maybe<TextValue>, next: Maybe<TextValue>): boolean {
return current.match(
(currentValue) =>
next.match(
(nextValue) => currentValue.toPrimitive() === nextValue.toPrimitive(),
() => false
),
() => next.isNone()
);
}
}

View File

@ -3,16 +3,21 @@ import type { Sequelize } from "sequelize";
import {
buildPaymentMethodCreator,
buildPaymentMethodDeleter,
buildPaymentMethodFinder,
buildPaymentMethodInputMappers,
buildPaymentMethodSnapshotBuilders,
buildPaymentMethodStatusChanger,
buildPaymentMethodUpdater,
} from "../../application";
import type { IPaymentMethodRepository } from "../../application/repositories";
import type { IPaymentMethodFinder } from "../../application/services";
import { PaymentMethodFinder, PaymentMethodUpdater } from "../../application/services";
import { PaymentMethodFinder } from "../../application/services";
import {
CreatePaymentMethodUseCase,
type DisablePaymentMethodByIdUseCase,
DeletePaymentMethodByIdUseCase,
DisablePaymentMethodByIdUseCase,
EnablePaymentMethodByIdUseCase,
GetPaymentMethodByIdUseCase,
ListPaymentMethodsUseCase,
UpdatePaymentMethodByIdUseCase,
@ -28,7 +33,9 @@ export type PaymentMethodsInternalDeps = {
getPaymentMethodById: () => GetPaymentMethodByIdUseCase;
createPaymentMethod: () => CreatePaymentMethodUseCase;
updatePaymentMethodById: () => UpdatePaymentMethodByIdUseCase;
deletePaymentMethodById: () => DeletePaymentMethodByIdUseCase;
disablePaymentMethodById: () => DisablePaymentMethodByIdUseCase;
enablePaymentMethodById: () => EnablePaymentMethodByIdUseCase;
};
};
@ -45,10 +52,11 @@ export const buildPaymentMethodsDependencies = (
// Application helpers
const inputMappers = buildPaymentMethodInputMappers();
const finder = buildPaymentMethodFinder(repository);
const finder = buildPaymentMethodFinder({ repository });
const creator = buildPaymentMethodCreator({ repository });
const updater = new PaymentMethodUpdater(repository);
//const disabler = new PaymentMethodDisabler(repository);
const updater = buildPaymentMethodUpdater({ repository });
const deleter = buildPaymentMethodDeleter({ repository });
const statusChanger = buildPaymentMethodStatusChanger({ repository });
const snapshotBuilders = buildPaymentMethodSnapshotBuilders();
@ -57,8 +65,10 @@ export const buildPaymentMethodsDependencies = (
useCases: {
listPaymentMethods: () =>
new ListPaymentMethodsUseCase(finder, snapshotBuilders.summary, transactionManager),
getPaymentMethodById: () =>
new GetPaymentMethodByIdUseCase(finder, snapshotBuilders.full, transactionManager),
createPaymentMethod: () =>
new CreatePaymentMethodUseCase({
dtoMapper: inputMappers.createInputMapper,
@ -75,13 +85,30 @@ export const buildPaymentMethodsDependencies = (
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
/*disablePaymentMethodById: () =>
deletePaymentMethodById: () =>
new DeletePaymentMethodByIdUseCase({
deleter,
finder,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
disablePaymentMethodById: () =>
new DisablePaymentMethodByIdUseCase({
finder,
disabler,
fullSnapshotBuilder,
changer: statusChanger,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),*/
}),
enablePaymentMethodById: () =>
new EnablePaymentMethodByIdUseCase({
finder,
changer: statusChanger,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
},
};
};

View File

@ -7,13 +7,13 @@ import {
import type { CreatePaymentMethodRequestDTO } from "../../../../../common";
import type { CreatePaymentMethodUseCase } from "../../../../application";
import { paymentmethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class CreatePaymentMethodController extends ExpressController {
constructor(private readonly useCase: CreatePaymentMethodUseCase) {
super();
this.errorMapper = paymentmethodsApiErrorMapper;
this.errorMapper = paymentMethodsApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards(

View File

@ -0,0 +1,39 @@
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { DeletePaymentMethodByIdUseCase } from "../../../../application";
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class DeletePaymentMethodByIdController extends ExpressController {
constructor(private readonly useCase: DeletePaymentMethodByIdUseCase) {
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

@ -1,15 +1,35 @@
import { ExpressController } from "@erp/core/api";
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { DisablePaymentMethodByIdUseCase } from "../../../../application";
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class DisablePaymentMethodByIdController extends ExpressController {
constructor(private readonly useCase: DisablePaymentMethodByIdUseCase) {
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 id = this.req.params.payment_method_id;
const result = await this.useCase.execute({ id });
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),

View File

@ -0,0 +1,39 @@
import {
ExpressController,
forbidQueryFieldGuard,
requireAuthenticatedGuard,
requireCompanyContextGuard,
} from "@erp/core/api";
import type { EnablePaymentMethodByIdUseCase } from "../../../../application";
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class EnablePaymentMethodByIdController extends ExpressController {
constructor(private readonly useCase: EnablePaymentMethodByIdUseCase) {
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

@ -6,13 +6,13 @@ import {
} from "@erp/core/api";
import type { GetPaymentMethodByIdUseCase } from "../../../../application";
import { paymentmethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class GetPaymentMethodByIdController extends ExpressController {
constructor(private readonly useCase: GetPaymentMethodByIdUseCase) {
super();
this.errorMapper = paymentmethodsApiErrorMapper;
this.errorMapper = paymentMethodsApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards(

View File

@ -1,5 +1,7 @@
export * from "./create-payment-method.controller";
export * from "./delete-payment-method-by-id.controller";
export * from "./disable-payment-method-by-id.controller";
export * from "./enable-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

@ -7,12 +7,12 @@ import {
import { Criteria } from "@repo/rdx-criteria/server";
import type { ListPaymentMethodsUseCase } from "../../../../application";
import { paymentmethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class ListPaymentMethodsController extends ExpressController {
constructor(private readonly useCase: ListPaymentMethodsUseCase) {
super();
this.errorMapper = paymentmethodsApiErrorMapper;
this.errorMapper = paymentMethodsApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards(

View File

@ -7,13 +7,13 @@ import {
} from "@erp/core/api";
import type { UpdatePaymentMethodByIdUseCase } from "../../../../application";
import { paymentmethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
export class UpdatePaymentMethodByIdController extends ExpressController {
constructor(private readonly useCase: UpdatePaymentMethodByIdUseCase) {
super();
this.errorMapper = paymentmethodsApiErrorMapper;
this.errorMapper = paymentMethodsApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards(

View File

@ -2,37 +2,96 @@ import {
ApiErrorMapper,
ConflictApiError,
type ErrorToApiRule,
NotFoundApiError,
ValidationApiError,
} from "@erp/core/api";
import {
type InvalidPaymentMethodIdError,
type InvalidPaymentMethodNameError,
type PaymentMethodCannotBeDeletedError,
type PaymentMethodCannotBeDisabledError,
type PaymentMethodCannotBeEnabledError,
type PaymentMethodCannotBeUpdatedError,
type PaymentMethodNotFoundError,
isInvalidPaymentMethodIdError,
isInvalidPaymentMethodNameError,
isPaymentMethodCannotBeDeletedError,
isPaymentMethodCannotBeDisabledError,
isPaymentMethodCannotBeEnabledError,
isPaymentMethodCannotBeUpdatedError,
isPaymentMethodNotFoundError,
} from "../../../domain/payment-methods";
// Crea una regla específica (prioridad alta para sobreescribir mensajes)
const paymentmethodDuplicateRule: ErrorToApiRule = {
const invalidPaymentMethodIdRule: ErrorToApiRule = {
priority: 120,
matches: (e) => isInvalidPaymentMethodIdError(e),
build: (e) =>
matches: isInvalidPaymentMethodIdError,
build: (error) =>
new ConflictApiError(
(e as InvalidPaymentMethodIdError).message ||
(error as InvalidPaymentMethodIdError).message ||
"Payment method with the provided id already exists."
),
};
const paymentmethodCannotBeDeletedRule: ErrorToApiRule = {
const invalidPaymentMethodNameRule: ErrorToApiRule = {
priority: 120,
matches: (e) => isPaymentMethodCannotBeDeletedError(e),
build: (e) =>
matches: isInvalidPaymentMethodNameError,
build: (error) =>
new ValidationApiError(
(e as PaymentMethodCannotBeDeletedError).message || "Payment method cannot be deleted."
(error as InvalidPaymentMethodNameError).message || "Payment method name is invalid."
),
};
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
export const paymentmethodsApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
.register(paymentmethodDuplicateRule)
.register(paymentmethodCannotBeDeletedRule);
const paymentMethodNotFoundRule: ErrorToApiRule = {
priority: 120,
matches: isPaymentMethodNotFoundError,
build: (error) =>
new NotFoundApiError(
(error as PaymentMethodNotFoundError).message || "Payment method not found."
),
};
const paymentMethodCannotBeDeletedRule: ErrorToApiRule = {
priority: 120,
matches: isPaymentMethodCannotBeDeletedError,
build: (error) =>
new ValidationApiError(
(error as PaymentMethodCannotBeDeletedError).message || "Payment method cannot be deleted."
),
};
const paymentMethodCannotBeDisabledRule: ErrorToApiRule = {
priority: 120,
matches: isPaymentMethodCannotBeDisabledError,
build: (error) =>
new ValidationApiError(
(error as PaymentMethodCannotBeDisabledError).message || "Payment method cannot be disabled."
),
};
const paymentMethodCannotBeEnabledRule: ErrorToApiRule = {
priority: 120,
matches: isPaymentMethodCannotBeEnabledError,
build: (error) =>
new ValidationApiError(
(error as PaymentMethodCannotBeEnabledError).message || "Payment method cannot be enabled."
),
};
const paymentMethodCannotBeUpdatedRule: ErrorToApiRule = {
priority: 120,
matches: isPaymentMethodCannotBeUpdatedError,
build: (error) =>
new ValidationApiError(
(error as PaymentMethodCannotBeUpdatedError).message || "Payment method cannot be updated."
),
};
export const paymentMethodsApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
.register(invalidPaymentMethodIdRule)
.register(invalidPaymentMethodNameRule)
.register(paymentMethodNotFoundRule)
.register(paymentMethodCannotBeDeletedRule)
.register(paymentMethodCannotBeDisabledRule)
.register(paymentMethodCannotBeEnabledRule)
.register(paymentMethodCannotBeUpdatedRule);

View File

@ -4,16 +4,19 @@ import { type NextFunction, type Request, type Response, Router } from "express"
import {
CreatePaymentMethodRequestSchema,
DeletePaymentMethodByIdRequestSchema,
GetPaymentMethodByIdRequestSchema,
ListPaymentMethodsRequestSchema,
UpdatePaymentMethodByIdParamsRequestSchema,
UpdatePaymentMethodByIdRequestSchema,
} from "../../../../common/dto/payment-methods/request";
} from "../../../../common";
import type { CatalogsInternalDeps } from "../../di/catalogs.di";
import {
CreatePaymentMethodController,
DeletePaymentMethodByIdController,
DisablePaymentMethodByIdController,
EnablePaymentMethodByIdController,
GetPaymentMethodByIdController,
ListPaymentMethodsController,
UpdatePaymentMethodByIdController,
@ -64,7 +67,18 @@ export const paymentMethodsRouter = (params: StartParams) => {
}
);
router.patch(
router.delete(
"/:payment_method_id",
validateRequest(DeletePaymentMethodByIdRequestSchema, "params"),
(req, res, next) => {
const controller = new DeletePaymentMethodByIdController(
deps.useCases.deletePaymentMethodById()
);
return controller.execute(req, res, next);
}
);
router.put(
"/:payment_method_id",
validateRequest(UpdatePaymentMethodByIdParamsRequestSchema, "params"),
validateRequest(UpdatePaymentMethodByIdRequestSchema, "body"),
@ -87,5 +101,16 @@ export const paymentMethodsRouter = (params: StartParams) => {
}
);
router.patch(
"/:payment_method_id/enable",
validateRequest(GetPaymentMethodByIdRequestSchema, "params"),
(req, res, next) => {
const controller = new EnablePaymentMethodByIdController(
deps.useCases.enablePaymentMethodById()
);
return controller.execute(req, res, next);
}
);
app.use(`${config.server.apiBasePath}/catalogs/payment-methods`, router);
};

View File

@ -21,7 +21,7 @@ export class SequelizePaymentMethodDomainMapper extends SequelizeDomainMapper<
> {
public mapToDomain(
source: PaymentMethodModel,
params?: MapperParamsType
_params?: MapperParamsType
): Result<PaymentMethod, Error> {
try {
const errors: ValidationErrorDetail[] = [];
@ -71,7 +71,7 @@ export class SequelizePaymentMethodDomainMapper extends SequelizeDomainMapper<
public mapToPersistence(
source: PaymentMethod,
params?: MapperParamsType
_params?: MapperParamsType
): Result<PaymentMethodCreationAttributes, Error> {
return Result.ok<PaymentMethodCreationAttributes>({
id: source.id.toPrimitive(),

View File

@ -17,7 +17,7 @@ export class SequelizePaymentMethodSummaryMapper extends SequelizeQueryMapper<
> {
public mapToReadModel(
raw: PaymentMethodModel,
params?: MapperParamsType
_params?: MapperParamsType
): Result<PaymentMethodSummary, Error> {
const errors: ValidationErrorDetail[] = [];

View File

@ -71,12 +71,14 @@ export class SequelizePaymentMethodRepository
}
const { id, ...payload } = dtoResult.data;
const [affected] = await PaymentMethodModel.update(payload, {
const [affected, updated] = await PaymentMethodModel.update(payload, {
where: { id },
transaction,
individualHooks: true,
});
console.log("Update result:", { affected, updated });
if (affected === 0) {
return Result.fail(
new InfrastructureRepositoryError("Concurrency conflict or payment method not found")

View File

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

View File

@ -1,4 +1,5 @@
export * from "./create-payment-method.request.dto";
export * from "./delete-payment-method-by-id.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

@ -1,6 +1,8 @@
import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4";
import { translateZodValidationError } from "../helpers";
import { ValueObject } from "./value-object";
interface NameProps {
@ -24,7 +26,7 @@ export class Name extends ValueObject<NameProps> {
if (!valueIsValid.success) {
return Result.fail(translateZodValidationError("Name creation failed", valueIsValid.error));
}
return Result.ok(new Name({ value }));
return Result.ok(new Name({ value: valueIsValid.data }));
}
static generateAcronym(name: string): string {

View File

@ -1,6 +1,8 @@
import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4";
import { translateZodValidationError } from "../helpers";
import { ValueObject } from "./value-object";
interface TextValueProps {

View File

@ -16,6 +16,7 @@ export interface IMaybe<T> {
getOrUndefined(): T | undefined;
map<U>(fn: (value: T) => U): IMaybe<U>;
match<U>(someFn: (value: T) => U, noneFn: () => U): U;
equals(other: IMaybe<T>): boolean;
}
export function isMaybe<T = unknown>(input: unknown): input is Maybe<T> {
@ -40,6 +41,18 @@ export class Maybe<T> implements IMaybe<T> {
return new Maybe<T>();
}
equals(other: IMaybe<T>): boolean {
if (this.isNone() && other.isNone()) {
return true;
}
if (this.isSome() && other.isSome()) {
return this.unwrap() === other.unwrap();
}
return false;
}
isSome(): boolean {
return this.value !== undefined;
}