Refactor payment methods: Remove system_key and related logic
- Removed `system_key` from payment method model and related files. - Updated payment method repository and services to eliminate references to `system_key`. - Deleted the BootstrapSystemPaymentMethodsService and its use case, as they are no longer needed. - Adjusted tests to reflect the removal of system payment methods and their associated logic. - Added SQL script to drop the `system_key` column and its unique index from the database.
This commit is contained in:
parent
de204fbcc9
commit
f1f329fa0d
@ -15,7 +15,6 @@ Campos persistidos y expuestos para administración:
|
||||
- `is_active`
|
||||
- `is_system`
|
||||
- `is_default`
|
||||
- `system_key`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `deleted_at`
|
||||
@ -24,17 +23,8 @@ Campos persistidos y expuestos para administración:
|
||||
|
||||
Reglas:
|
||||
|
||||
- `is_system = true` implica `system_key != null`
|
||||
- `is_system = false` implica `system_key = null`
|
||||
- `is_system = true` no se puede editar, desactivar ni eliminar
|
||||
|
||||
Identidad estable de métodos del sistema:
|
||||
|
||||
- `cash`
|
||||
- `bank_transfer`
|
||||
- `card`
|
||||
- `direct_debit`
|
||||
|
||||
## Active and default
|
||||
|
||||
Reglas:
|
||||
@ -46,26 +36,6 @@ Reglas:
|
||||
|
||||
La unicidad del default se garantiza mediante servicio transaccional y bloqueo por empresa. No se fuerza con constraint SQL adicional para evitar una solución opaca en MariaDB.
|
||||
|
||||
## Bootstrap
|
||||
|
||||
Endpoint explícito e idempotente:
|
||||
|
||||
- `POST /catalogs/payment-methods/bootstrap-system`
|
||||
|
||||
Comportamiento:
|
||||
|
||||
- crea los cuatro métodos del sistema que falten
|
||||
- restaura un system method soft-deleted si ya existía por `system_key`
|
||||
- resincroniza `name`, `is_system`, `is_active`, `is_default = false` y `system_key`
|
||||
- si la empresa no tiene default, establece `cash` como default inicial
|
||||
|
||||
Métodos creados:
|
||||
|
||||
- `Efectivo`
|
||||
- `Transferencia bancaria`
|
||||
- `Tarjeta`
|
||||
- `Domiciliación bancaria`
|
||||
|
||||
## Delete policy
|
||||
|
||||
Un método solo puede eliminarse si:
|
||||
|
||||
35
docs/catalogs/sql/remove-payment-method-system-key.sql
Normal file
35
docs/catalogs/sql/remove-payment-method-system-key.sql
Normal file
@ -0,0 +1,35 @@
|
||||
SET @index_exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'payment_methods'
|
||||
AND index_name = 'uq_payment_methods_company_system_key'
|
||||
);
|
||||
|
||||
SET @drop_index_sql := IF(
|
||||
@index_exists > 0,
|
||||
'DROP INDEX `uq_payment_methods_company_system_key` ON `payment_methods`',
|
||||
'SELECT 1'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @drop_index_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @column_exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'payment_methods'
|
||||
AND column_name = 'system_key'
|
||||
);
|
||||
|
||||
SET @drop_column_sql := IF(
|
||||
@column_exists > 0,
|
||||
'ALTER TABLE `payment_methods` DROP COLUMN `system_key`',
|
||||
'SELECT 1'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @drop_column_sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -8,7 +8,7 @@ import {
|
||||
extractOrPushError,
|
||||
maybeFromNullableResult,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CreatePaymentMethodRequestDTO } from "../../../../common";
|
||||
import type { IPaymentMethodCreateProps } from "../../../domain";
|
||||
@ -49,7 +49,6 @@ export class CreatePaymentMethodInputMapper implements ICreatePaymentMethodInput
|
||||
isActive: isActive!,
|
||||
isSystem: false,
|
||||
isDefault: false,
|
||||
systemKey: Maybe.none(),
|
||||
};
|
||||
|
||||
return Result.ok({
|
||||
|
||||
@ -2,7 +2,7 @@ import type { Criteria } from "@repo/rdx-criteria/server";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Collection, Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { PaymentMethod, PaymentMethodSystemKey } from "../../../domain";
|
||||
import type { PaymentMethod } from "../../../domain";
|
||||
import type { PaymentMethodSummary } from "../models";
|
||||
|
||||
export interface IPaymentMethodRepository {
|
||||
@ -32,29 +32,7 @@ export interface IPaymentMethodRepository {
|
||||
companyId: UniqueID,
|
||||
transaction?: unknown
|
||||
): Promise<Result<Maybe<PaymentMethod>, Error>>;
|
||||
findBySystemKeyInCompany(
|
||||
companyId: UniqueID,
|
||||
systemKey: PaymentMethodSystemKey,
|
||||
transaction?: unknown,
|
||||
options?: { includeDeleted?: boolean }
|
||||
): Promise<Result<Maybe<PaymentMethod>, Error>>;
|
||||
restoreByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
transaction?: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
syncSystemPaymentMethodState(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
systemKey: PaymentMethodSystemKey;
|
||||
transaction?: unknown;
|
||||
}): Promise<Result<void, Error>>;
|
||||
lockByCompanyId(
|
||||
companyId: UniqueID,
|
||||
transaction?: unknown
|
||||
): Promise<Result<void, Error>>;
|
||||
lockByCompanyId(companyId: UniqueID, transaction?: unknown): Promise<Result<void, Error>>;
|
||||
deleteByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { describe, test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||
import { EntityNotFoundError } from "@erp/core/api";
|
||||
import { Name, UniqueID } from "@repo/rdx-ddd";
|
||||
import { Collection, Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { PaymentMethod } from "../../../../domain/payment-methods/payment-method.aggregate";
|
||||
import { BootstrapSystemPaymentMethodsService } from "../bootstrap-system-payment-methods.service";
|
||||
import { PaymentMethodDeleter } from "../payment-method-deleter";
|
||||
import { SetDefaultPaymentMethodService } from "../set-default-payment-method.service";
|
||||
import type { IPaymentMethodRepository } from "../../repositories";
|
||||
@ -66,7 +64,7 @@ class InMemoryPaymentMethodRepository implements IPaymentMethodRepository {
|
||||
}
|
||||
|
||||
async findByCriteriaInCompany(): Promise<Result<Collection<PaymentMethodSummary>, Error>> {
|
||||
return Result.ok(Collection.create<PaymentMethodSummary>([]));
|
||||
return Result.ok(new Collection<PaymentMethodSummary>([]));
|
||||
}
|
||||
|
||||
async findDefaultInCompany(companyId: UniqueID): Promise<Result<Maybe<PaymentMethod>, Error>> {
|
||||
@ -81,80 +79,6 @@ class InMemoryPaymentMethodRepository implements IPaymentMethodRepository {
|
||||
return paymentMethod ? Result.ok(Maybe.some(paymentMethod)) : Result.ok(Maybe.none());
|
||||
}
|
||||
|
||||
async findBySystemKeyInCompany(
|
||||
companyId: UniqueID,
|
||||
systemKey: "cash" | "bank_transfer" | "card" | "direct_debit",
|
||||
_transaction?: unknown,
|
||||
options?: { includeDeleted?: boolean }
|
||||
): Promise<Result<Maybe<PaymentMethod>, Error>> {
|
||||
const paymentMethod =
|
||||
[...this.items.values()].find((item) => {
|
||||
const matchesCompany = item.companyId.toString() === companyId.toString();
|
||||
const matchesKey = item.systemKey.match(
|
||||
(value) => value === systemKey,
|
||||
() => false
|
||||
);
|
||||
const includeDeleted = options?.includeDeleted === true;
|
||||
|
||||
return (
|
||||
matchesCompany &&
|
||||
matchesKey &&
|
||||
(includeDeleted || !this.deleted.has(item.id.toString()))
|
||||
);
|
||||
}) ?? null;
|
||||
|
||||
return paymentMethod ? Result.ok(Maybe.some(paymentMethod)) : Result.ok(Maybe.none());
|
||||
}
|
||||
|
||||
async restoreByIdInCompany(companyId: UniqueID, id: UniqueID): Promise<Result<boolean, Error>> {
|
||||
const result = await this.existsByIdInCompany(companyId, id);
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
if (!result.data) {
|
||||
return Result.ok(false);
|
||||
}
|
||||
|
||||
this.deleted.delete(id.toString());
|
||||
return Result.ok(true);
|
||||
}
|
||||
|
||||
async syncSystemPaymentMethodState(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
systemKey: "cash" | "bank_transfer" | "card" | "direct_debit";
|
||||
}): Promise<Result<void, Error>> {
|
||||
const paymentMethodResult = await this.getByIdInCompany(params.companyId, params.id);
|
||||
if (paymentMethodResult.isFailure) {
|
||||
return Result.fail(paymentMethodResult.error);
|
||||
}
|
||||
|
||||
const current = paymentMethodResult.data;
|
||||
const nameResult = Name.create(params.name);
|
||||
assert.equal(nameResult.isSuccess, true);
|
||||
|
||||
const updatedResult = PaymentMethod.create(
|
||||
{
|
||||
companyId: current.companyId,
|
||||
name: nameResult.data,
|
||||
description: current.description,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isDefault: params.isDefault,
|
||||
systemKey: Maybe.some(params.systemKey),
|
||||
},
|
||||
current.id
|
||||
);
|
||||
|
||||
assert.equal(updatedResult.isSuccess, true);
|
||||
this.items.set(current.id.toString(), updatedResult.data);
|
||||
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
async lockByCompanyId(): Promise<Result<void, Error>> {
|
||||
return Result.ok();
|
||||
}
|
||||
@ -180,7 +104,6 @@ const buildPaymentMethod = (params: {
|
||||
isActive?: boolean;
|
||||
isSystem?: boolean;
|
||||
isDefault?: boolean;
|
||||
systemKey?: "cash" | "bank_transfer" | "card" | "direct_debit";
|
||||
}) => {
|
||||
const nameResult = Name.create(params.name);
|
||||
assert.equal(nameResult.isSuccess, true);
|
||||
@ -193,7 +116,6 @@ const buildPaymentMethod = (params: {
|
||||
isActive: params.isActive ?? true,
|
||||
isSystem: params.isSystem ?? false,
|
||||
isDefault: params.isDefault ?? false,
|
||||
systemKey: params.systemKey ? Maybe.some(params.systemKey) : Maybe.none(),
|
||||
},
|
||||
UniqueID.generateNewID()
|
||||
);
|
||||
@ -302,46 +224,12 @@ describe("Payment method services", () => {
|
||||
assert.equal(result.isFailure, true);
|
||||
});
|
||||
|
||||
test("bootstrap creates four system methods and exactly one default", async () => {
|
||||
const repository = new InMemoryPaymentMethodRepository();
|
||||
const setDefaultService = new SetDefaultPaymentMethodService(repository);
|
||||
const service = new BootstrapSystemPaymentMethodsService({
|
||||
repository,
|
||||
setDefaultService,
|
||||
});
|
||||
const companyId = UniqueID.generateNewID();
|
||||
|
||||
const result = await service.bootstrap({ companyId });
|
||||
|
||||
assert.equal(result.isSuccess, true);
|
||||
assert.equal(result.data.length, 4);
|
||||
assert.equal(result.data.filter((item) => item.isDefault).length, 1);
|
||||
assert.equal(result.data.some((item) => item.systemKey.match((key) => key === "cash", () => false) && item.isDefault), true);
|
||||
});
|
||||
|
||||
test("bootstrap is idempotent", async () => {
|
||||
const repository = new InMemoryPaymentMethodRepository();
|
||||
const setDefaultService = new SetDefaultPaymentMethodService(repository);
|
||||
const service = new BootstrapSystemPaymentMethodsService({
|
||||
repository,
|
||||
setDefaultService,
|
||||
});
|
||||
const companyId = UniqueID.generateNewID();
|
||||
|
||||
const first = await service.bootstrap({ companyId });
|
||||
const second = await service.bootstrap({ companyId });
|
||||
|
||||
assert.equal(first.isSuccess, true);
|
||||
assert.equal(second.isSuccess, true);
|
||||
assert.equal(second.data.length, 4);
|
||||
});
|
||||
|
||||
test("delete blocks referenced payment methods", async () => {
|
||||
const repository = new InMemoryPaymentMethodRepository();
|
||||
const deleter = new PaymentMethodDeleter({
|
||||
repository,
|
||||
usageChecker: {
|
||||
findUsage: async () => Result.ok(["customers"]),
|
||||
findUsage: async () => Result.ok([{ source: "customers" }]),
|
||||
},
|
||||
});
|
||||
const companyId = UniqueID.generateNewID();
|
||||
@ -352,9 +240,32 @@ describe("Payment method services", () => {
|
||||
|
||||
await repository.create(paymentMethod);
|
||||
|
||||
const result = await deleter.deleteById({
|
||||
const result = await deleter.delete({
|
||||
paymentMethod,
|
||||
});
|
||||
|
||||
assert.equal(result.isFailure, true);
|
||||
});
|
||||
|
||||
test("delete blocks system payment methods", async () => {
|
||||
const repository = new InMemoryPaymentMethodRepository();
|
||||
const deleter = new PaymentMethodDeleter({
|
||||
repository,
|
||||
usageChecker: {
|
||||
findUsage: async () => Result.ok([]),
|
||||
},
|
||||
});
|
||||
const companyId = UniqueID.generateNewID();
|
||||
const paymentMethod = buildPaymentMethod({
|
||||
companyId,
|
||||
id: paymentMethod.id,
|
||||
name: "Transferencia",
|
||||
isSystem: true,
|
||||
});
|
||||
|
||||
await repository.create(paymentMethod);
|
||||
|
||||
const result = await deleter.delete({
|
||||
paymentMethod,
|
||||
});
|
||||
|
||||
assert.equal(result.isFailure, true);
|
||||
|
||||
@ -1,208 +0,0 @@
|
||||
import { Name, UniqueID } from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { PaymentMethod, type PaymentMethodSystemKey } from "../../../domain";
|
||||
import type { IPaymentMethodRepository } from "../repositories";
|
||||
import type { ISetDefaultPaymentMethodService } from "./set-default-payment-method.service";
|
||||
|
||||
type SystemPaymentMethodDefinition = {
|
||||
systemKey: PaymentMethodSystemKey;
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
const SYSTEM_PAYMENT_METHOD_DEFINITIONS: SystemPaymentMethodDefinition[] = [
|
||||
{
|
||||
systemKey: "cash",
|
||||
name: "Efectivo",
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
systemKey: "bank_transfer",
|
||||
name: "Transferencia bancaria",
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
systemKey: "card",
|
||||
name: "Tarjeta",
|
||||
isDefault: false,
|
||||
},
|
||||
{
|
||||
systemKey: "direct_debit",
|
||||
name: "Domiciliación bancaria",
|
||||
isDefault: false,
|
||||
},
|
||||
];
|
||||
|
||||
export interface IBootstrapSystemPaymentMethodsService {
|
||||
bootstrap(params: {
|
||||
companyId: UniqueID;
|
||||
transaction?: unknown;
|
||||
}): Promise<Result<PaymentMethod[], Error>>;
|
||||
}
|
||||
|
||||
export class BootstrapSystemPaymentMethodsService implements IBootstrapSystemPaymentMethodsService {
|
||||
public constructor(
|
||||
private readonly deps: {
|
||||
repository: IPaymentMethodRepository;
|
||||
setDefaultService: ISetDefaultPaymentMethodService;
|
||||
}
|
||||
) {}
|
||||
|
||||
public async bootstrap(params: {
|
||||
companyId: UniqueID;
|
||||
transaction?: unknown;
|
||||
}): Promise<Result<PaymentMethod[], Error>> {
|
||||
const { companyId, transaction } = params;
|
||||
|
||||
const lockResult = await this.deps.repository.lockByCompanyId(companyId, transaction);
|
||||
if (lockResult.isFailure) {
|
||||
return Result.fail(lockResult.error);
|
||||
}
|
||||
|
||||
const bootstrappedPaymentMethods: PaymentMethod[] = [];
|
||||
|
||||
for (const definition of SYSTEM_PAYMENT_METHOD_DEFINITIONS) {
|
||||
const paymentMethodResult = await this.ensureSystemPaymentMethod({
|
||||
companyId,
|
||||
definition,
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (paymentMethodResult.isFailure) {
|
||||
return Result.fail(paymentMethodResult.error);
|
||||
}
|
||||
|
||||
bootstrappedPaymentMethods.push(paymentMethodResult.data);
|
||||
}
|
||||
|
||||
const currentDefaultResult = await this.deps.repository.findDefaultInCompany(
|
||||
companyId,
|
||||
transaction
|
||||
);
|
||||
if (currentDefaultResult.isFailure) {
|
||||
return Result.fail(currentDefaultResult.error);
|
||||
}
|
||||
|
||||
if (currentDefaultResult.data.isNone()) {
|
||||
const cashPaymentMethod = bootstrappedPaymentMethods.find(
|
||||
(paymentMethod) =>
|
||||
paymentMethod.systemKey.match(
|
||||
(systemKey) => systemKey === "cash",
|
||||
() => false
|
||||
)
|
||||
);
|
||||
|
||||
if (!cashPaymentMethod) {
|
||||
return Result.fail(new Error("Cash system payment method bootstrap failed."));
|
||||
}
|
||||
|
||||
const setDefaultResult = await this.deps.setDefaultService.setDefault({
|
||||
companyId,
|
||||
paymentMethodId: cashPaymentMethod.id,
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (setDefaultResult.isFailure) {
|
||||
return Result.fail(setDefaultResult.error);
|
||||
}
|
||||
|
||||
const updatedItems = bootstrappedPaymentMethods.map((paymentMethod) =>
|
||||
paymentMethod.id.toString() === setDefaultResult.data.id.toString()
|
||||
? setDefaultResult.data
|
||||
: paymentMethod
|
||||
);
|
||||
|
||||
return Result.ok(updatedItems);
|
||||
}
|
||||
|
||||
return Result.ok(bootstrappedPaymentMethods);
|
||||
}
|
||||
|
||||
private async ensureSystemPaymentMethod(params: {
|
||||
companyId: UniqueID;
|
||||
definition: SystemPaymentMethodDefinition;
|
||||
transaction?: unknown;
|
||||
}): Promise<Result<PaymentMethod, Error>> {
|
||||
const { companyId, definition, transaction } = params;
|
||||
|
||||
const existingResult = await this.deps.repository.findBySystemKeyInCompany(
|
||||
companyId,
|
||||
definition.systemKey,
|
||||
transaction,
|
||||
{ includeDeleted: true }
|
||||
);
|
||||
|
||||
if (existingResult.isFailure) {
|
||||
return Result.fail(existingResult.error);
|
||||
}
|
||||
|
||||
if (existingResult.data.isSome()) {
|
||||
const existingPaymentMethod = existingResult.data.unwrap();
|
||||
|
||||
const restoreResult = await this.deps.repository.restoreByIdInCompany(
|
||||
companyId,
|
||||
existingPaymentMethod.id,
|
||||
transaction
|
||||
);
|
||||
|
||||
if (restoreResult.isFailure) {
|
||||
return Result.fail(restoreResult.error);
|
||||
}
|
||||
|
||||
const syncResult = await this.deps.repository.syncSystemPaymentMethodState({
|
||||
companyId,
|
||||
id: existingPaymentMethod.id,
|
||||
name: definition.name,
|
||||
isDefault: false,
|
||||
systemKey: definition.systemKey,
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (syncResult.isFailure) {
|
||||
return Result.fail(syncResult.error);
|
||||
}
|
||||
|
||||
const refreshedResult = await this.deps.repository.getByIdInCompany(
|
||||
companyId,
|
||||
existingPaymentMethod.id,
|
||||
transaction
|
||||
);
|
||||
|
||||
if (refreshedResult.isFailure) {
|
||||
return Result.fail(refreshedResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(refreshedResult.data);
|
||||
}
|
||||
|
||||
const nameResult = Name.create(definition.name);
|
||||
if (nameResult.isFailure) {
|
||||
return Result.fail(nameResult.error);
|
||||
}
|
||||
|
||||
const paymentMethodResult = PaymentMethod.create(
|
||||
{
|
||||
companyId,
|
||||
name: nameResult.data,
|
||||
description: Maybe.none(),
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isDefault: false,
|
||||
systemKey: Maybe.some(definition.systemKey),
|
||||
},
|
||||
UniqueID.generateNewID()
|
||||
);
|
||||
|
||||
if (paymentMethodResult.isFailure) {
|
||||
return Result.fail(paymentMethodResult.error);
|
||||
}
|
||||
|
||||
const createResult = await this.deps.repository.create(paymentMethodResult.data, transaction);
|
||||
if (createResult.isFailure) {
|
||||
return Result.fail(createResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(paymentMethodResult.data);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
export * from "./payment-method-creator";
|
||||
export * from "./bootstrap-system-payment-methods.service";
|
||||
export * from "./payment-method-deleter";
|
||||
export * from "./payment-method-disabler";
|
||||
export * from "./payment-method-enabler";
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
import type { BootstrapSystemPaymentMethodsResponseDTO } 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 { IBootstrapSystemPaymentMethodsService } from "../services";
|
||||
import type { IPaymentMethodFullSnapshotBuilder } from "../snapshot-builders";
|
||||
|
||||
export type BootstrapSystemPaymentMethodsUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
};
|
||||
|
||||
export class BootstrapSystemPaymentMethodsUseCase {
|
||||
public constructor(
|
||||
private readonly deps: {
|
||||
bootstrapService: IBootstrapSystemPaymentMethodsService;
|
||||
fullSnapshotBuilder: IPaymentMethodFullSnapshotBuilder;
|
||||
transactionManager: ITransactionManager;
|
||||
}
|
||||
) {}
|
||||
|
||||
public execute(
|
||||
params: BootstrapSystemPaymentMethodsUseCaseInput
|
||||
): Promise<Result<BootstrapSystemPaymentMethodsResponseDTO, Error>> {
|
||||
const { companyId } = params;
|
||||
|
||||
return this.deps.transactionManager.complete(async (transaction: unknown) => {
|
||||
try {
|
||||
const result = await this.deps.bootstrapService.bootstrap({
|
||||
companyId,
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return Result.ok({
|
||||
items: result.data.map((paymentMethod) => this.deps.fullSnapshotBuilder.toOutput(paymentMethod)),
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
export * from "./bootstrap-system-payment-methods.use-case";
|
||||
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";
|
||||
|
||||
@ -22,7 +22,6 @@ const createPaymentMethod = (params?: {
|
||||
isActive: params?.isActive ?? true,
|
||||
isSystem: params?.isSystem ?? false,
|
||||
isDefault: params?.isDefault ?? false,
|
||||
systemKey: Maybe.none(),
|
||||
},
|
||||
UniqueID.generateNewID()
|
||||
);
|
||||
@ -45,7 +44,6 @@ describe("PaymentMethod aggregate", () => {
|
||||
isActive: false,
|
||||
isSystem: false,
|
||||
isDefault: true,
|
||||
systemKey: Maybe.none(),
|
||||
},
|
||||
UniqueID.generateNewID()
|
||||
);
|
||||
@ -81,7 +79,6 @@ describe("PaymentMethod aggregate", () => {
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isDefault: false,
|
||||
systemKey: Maybe.some("cash"),
|
||||
},
|
||||
UniqueID.generateNewID()
|
||||
);
|
||||
@ -105,4 +102,29 @@ describe("PaymentMethod aggregate", () => {
|
||||
|
||||
assert.equal(result.isSuccess, true);
|
||||
});
|
||||
|
||||
test("blocks deleting system payment methods", () => {
|
||||
const paymentMethod = createPaymentMethod({ isSystem: true });
|
||||
|
||||
const result = paymentMethod.ensureCanBeDeleted();
|
||||
|
||||
assert.equal(result.isFailure, true);
|
||||
});
|
||||
|
||||
test("blocks disabling system payment methods", () => {
|
||||
const paymentMethod = createPaymentMethod({ isSystem: true });
|
||||
|
||||
const result = paymentMethod.disable();
|
||||
|
||||
assert.equal(result.isFailure, true);
|
||||
});
|
||||
|
||||
test("keeps enable idempotent for system payment methods", () => {
|
||||
const paymentMethod = createPaymentMethod({ isSystem: true });
|
||||
|
||||
const result = paymentMethod.enable();
|
||||
|
||||
assert.equal(result.isSuccess, true);
|
||||
assert.equal(result.data, false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
export * from "./errors";
|
||||
export * from "./payment-method.aggregate";
|
||||
export * from "./payment-method-name";
|
||||
export * from "./payment-method-system-key";
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
export const PAYMENT_METHOD_SYSTEM_KEYS = [
|
||||
"cash",
|
||||
"bank_transfer",
|
||||
"card",
|
||||
"direct_debit",
|
||||
] as const;
|
||||
|
||||
export type PaymentMethodSystemKey = (typeof PAYMENT_METHOD_SYSTEM_KEYS)[number];
|
||||
|
||||
export const isPaymentMethodSystemKey = (value: string): value is PaymentMethodSystemKey => {
|
||||
return PAYMENT_METHOD_SYSTEM_KEYS.includes(value as PaymentMethodSystemKey);
|
||||
};
|
||||
@ -7,7 +7,6 @@ import {
|
||||
PaymentMethodCannotBeDeletedError,
|
||||
PaymentMethodCannotBeUpdatedError,
|
||||
} from "./errors";
|
||||
import type { PaymentMethodSystemKey } from "./payment-method-system-key";
|
||||
|
||||
export interface IPaymentMethodCreateProps {
|
||||
companyId: UniqueID;
|
||||
@ -16,7 +15,6 @@ export interface IPaymentMethodCreateProps {
|
||||
isActive: boolean;
|
||||
isSystem: boolean;
|
||||
isDefault: boolean;
|
||||
systemKey: Maybe<PaymentMethodSystemKey>;
|
||||
}
|
||||
|
||||
export type PaymentMethodPatchProps = Partial<{
|
||||
@ -71,14 +69,6 @@ export class PaymentMethod extends AggregateRoot<PaymentMethodInternalProps> {
|
||||
);
|
||||
}
|
||||
|
||||
if (props.isSystem && props.systemKey.isNone()) {
|
||||
return Result.fail(new Error("System payment methods require a system key."));
|
||||
}
|
||||
|
||||
if (!props.isSystem && props.systemKey.isSome()) {
|
||||
return Result.fail(new Error("Custom payment methods cannot define a system key."));
|
||||
}
|
||||
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@ -114,10 +104,6 @@ export class PaymentMethod extends AggregateRoot<PaymentMethodInternalProps> {
|
||||
return this.props.isDefault;
|
||||
}
|
||||
|
||||
public get systemKey(): Maybe<PaymentMethodSystemKey> {
|
||||
return this.props.systemKey;
|
||||
}
|
||||
|
||||
public update(patchProps: PaymentMethodPatchProps): Result<boolean, Error> {
|
||||
if (this.isSystem) {
|
||||
return Result.fail(
|
||||
|
||||
@ -3,8 +3,6 @@ import type { Sequelize } from "sequelize";
|
||||
|
||||
import {
|
||||
type IPaymentMethodPublicFinder,
|
||||
BootstrapSystemPaymentMethodsService,
|
||||
BootstrapSystemPaymentMethodsUseCase,
|
||||
PaymentMethodPublicFinder,
|
||||
PaymentMethodPublicModelMapper,
|
||||
SetDefaultPaymentMethodByIdUseCase,
|
||||
@ -43,7 +41,6 @@ export type PaymentMethodsInternalDeps = {
|
||||
disablePaymentMethodById: () => DisablePaymentMethodByIdUseCase;
|
||||
enablePaymentMethodById: () => EnablePaymentMethodByIdUseCase;
|
||||
setDefaultPaymentMethodById: () => SetDefaultPaymentMethodByIdUseCase;
|
||||
bootstrapSystemPaymentMethods: () => BootstrapSystemPaymentMethodsUseCase;
|
||||
};
|
||||
};
|
||||
|
||||
@ -67,10 +64,6 @@ export const buildPaymentMethodsDependencies = (
|
||||
const deleter = buildPaymentMethodDeleter({ repository, usageChecker });
|
||||
const statusChanger = buildPaymentMethodStatusChanger({ repository });
|
||||
const setDefaultService = new SetDefaultPaymentMethodService(repository);
|
||||
const bootstrapService = new BootstrapSystemPaymentMethodsService({
|
||||
repository,
|
||||
setDefaultService,
|
||||
});
|
||||
|
||||
const snapshotBuilders = buildPaymentMethodSnapshotBuilders();
|
||||
|
||||
@ -130,13 +123,6 @@ export const buildPaymentMethodsDependencies = (
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
transactionManager,
|
||||
}),
|
||||
|
||||
bootstrapSystemPaymentMethods: () =>
|
||||
new BootstrapSystemPaymentMethodsUseCase({
|
||||
bootstrapService,
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
transactionManager,
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
import {
|
||||
ExpressController,
|
||||
forbidQueryFieldGuard,
|
||||
requireAuthenticatedGuard,
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { BootstrapSystemPaymentMethodsUseCase } from "../../../../application";
|
||||
import { paymentMethodsApiErrorMapper } from "../payment-methods-api-error-mapper";
|
||||
|
||||
export class BootstrapSystemPaymentMethodsController extends ExpressController {
|
||||
constructor(private readonly useCase: BootstrapSystemPaymentMethodsUseCase) {
|
||||
super();
|
||||
|
||||
this.errorMapper = paymentMethodsApiErrorMapper;
|
||||
|
||||
this.registerGuards(
|
||||
requireAuthenticatedGuard(),
|
||||
requireCompanyContextGuard(),
|
||||
forbidQueryFieldGuard("companyId")
|
||||
);
|
||||
}
|
||||
|
||||
protected async executeImpl() {
|
||||
const companyId = this.getTenantId();
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const result = await this.useCase.execute({ companyId });
|
||||
|
||||
return result.match(
|
||||
(data) => this.ok(data),
|
||||
(err) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,5 @@ 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 "./bootstrap-system-payment-methods.controller";
|
||||
export * from "./set-default-payment-method-by-id.controller";
|
||||
export * from "./update-payment-method-by-id.controller";
|
||||
|
||||
@ -3,7 +3,6 @@ import { requireIdentityTenantMembership } from "@erp/identity/api";
|
||||
import { type NextFunction, type Request, type Response, Router } from "express";
|
||||
|
||||
import {
|
||||
BootstrapSystemPaymentMethodsRequestSchema,
|
||||
CreatePaymentMethodRequestSchema,
|
||||
DeletePaymentMethodByIdRequestSchema,
|
||||
GetPaymentMethodByIdRequestSchema,
|
||||
@ -15,7 +14,6 @@ import {
|
||||
import type { CatalogsInternalDeps } from "../../di/catalogs.di";
|
||||
|
||||
import {
|
||||
BootstrapSystemPaymentMethodsController,
|
||||
CreatePaymentMethodController,
|
||||
DeletePaymentMethodByIdController,
|
||||
DisablePaymentMethodByIdController,
|
||||
@ -47,17 +45,6 @@ export const paymentMethodsRouter = (params: StartParams) => {
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/bootstrap-system",
|
||||
validateRequest(BootstrapSystemPaymentMethodsRequestSchema, "body"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const controller = new BootstrapSystemPaymentMethodsController(
|
||||
deps.useCases.bootstrapSystemPaymentMethods()
|
||||
);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
validateRequest(CreatePaymentMethodRequestSchema, "body"),
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import { PaymentMethod, isPaymentMethodSystemKey } from "../../../../../domain";
|
||||
import { PaymentMethod } from "../../../../../domain";
|
||||
import type { PaymentMethodCreationAttributes, PaymentMethodModel } from "../models";
|
||||
|
||||
export class SequelizePaymentMethodDomainMapper extends SequelizeDomainMapper<
|
||||
@ -44,15 +44,6 @@ export class SequelizePaymentMethodDomainMapper extends SequelizeDomainMapper<
|
||||
"description",
|
||||
errors
|
||||
);
|
||||
const systemKey = extractOrPushError(
|
||||
maybeFromNullableResult(source.system_key, (value) =>
|
||||
isPaymentMethodSystemKey(value)
|
||||
? Result.ok(value)
|
||||
: Result.fail(new Error(`Invalid payment method system key: ${value}`))
|
||||
),
|
||||
"system_key",
|
||||
errors
|
||||
);
|
||||
|
||||
// Si hubo errores de mapeo, devolvemos colección de validación
|
||||
if (errors.length > 0) {
|
||||
@ -69,7 +60,6 @@ export class SequelizePaymentMethodDomainMapper extends SequelizeDomainMapper<
|
||||
isActive: source.is_active,
|
||||
isSystem: source.is_system,
|
||||
isDefault: source.is_default,
|
||||
systemKey: systemKey!,
|
||||
},
|
||||
idResult.data
|
||||
);
|
||||
@ -92,10 +82,6 @@ export class SequelizePaymentMethodDomainMapper extends SequelizeDomainMapper<
|
||||
is_active: source.isActive,
|
||||
is_system: source.isSystem,
|
||||
is_default: source.isDefault,
|
||||
system_key: source.systemKey.match(
|
||||
(systemKey) => systemKey,
|
||||
() => null
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,6 @@ export class PaymentMethodModel extends Model<
|
||||
declare is_active: boolean;
|
||||
declare is_system: boolean;
|
||||
declare is_default: boolean;
|
||||
declare system_key: CreationOptional<string | null>;
|
||||
|
||||
static associate(_database: Sequelize) {}
|
||||
|
||||
@ -65,11 +64,6 @@ export default (database: Sequelize) => {
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
system_key: {
|
||||
type: DataTypes.STRING(40),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize: database,
|
||||
@ -91,13 +85,6 @@ export default (database: Sequelize) => {
|
||||
defaultScope: {},
|
||||
|
||||
scopes: {},
|
||||
indexes: [
|
||||
{
|
||||
name: "uq_payment_methods_company_system_key",
|
||||
fields: ["company_id", "system_key"],
|
||||
unique: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import type { Sequelize, Transaction } from "sequelize";
|
||||
|
||||
import type { IPaymentMethodRepository } from "../../../../../application";
|
||||
import type { PaymentMethodSummary } from "../../../../../application/payment-methods/models";
|
||||
import type { PaymentMethod, PaymentMethodSystemKey } from "../../../../../domain";
|
||||
import type { PaymentMethod } from "../../../../../domain";
|
||||
import type { SequelizePaymentMethodSummaryMapper } from "../mappers";
|
||||
import type { SequelizePaymentMethodDomainMapper } from "../mappers/sequelize-payment-method-domain.mapper";
|
||||
import { PaymentMethodModel } from "../models";
|
||||
@ -202,90 +202,6 @@ export class SequelizePaymentMethodRepository
|
||||
}
|
||||
}
|
||||
|
||||
public async findBySystemKeyInCompany(
|
||||
companyId: UniqueID,
|
||||
systemKey: PaymentMethodSystemKey,
|
||||
transaction?: Transaction,
|
||||
options?: { includeDeleted?: boolean }
|
||||
): Promise<Result<Maybe<PaymentMethod>, Error>> {
|
||||
try {
|
||||
const row = await PaymentMethodModel.findOne({
|
||||
where: {
|
||||
company_id: companyId.toString(),
|
||||
system_key: systemKey,
|
||||
},
|
||||
paranoid: !(options?.includeDeleted ?? false),
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (!row) {
|
||||
return Result.ok(Maybe.none<PaymentMethod>());
|
||||
}
|
||||
|
||||
const mappedResult = this.domainMapper.mapToDomain(row);
|
||||
if (mappedResult.isFailure) {
|
||||
return Result.fail(mappedResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(Maybe.some(mappedResult.data));
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(translateSequelizeError(error));
|
||||
}
|
||||
}
|
||||
|
||||
public async restoreByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
transaction?: Transaction
|
||||
): Promise<Result<boolean, Error>> {
|
||||
try {
|
||||
const restored = await PaymentMethodModel.restore({
|
||||
where: {
|
||||
id: id.toString(),
|
||||
company_id: companyId.toString(),
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
|
||||
return Result.ok(restored > 0);
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(translateSequelizeError(error));
|
||||
}
|
||||
}
|
||||
|
||||
public async syncSystemPaymentMethodState(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
systemKey: PaymentMethodSystemKey;
|
||||
transaction?: Transaction;
|
||||
}): Promise<Result<void, Error>> {
|
||||
try {
|
||||
await PaymentMethodModel.update(
|
||||
{
|
||||
name: params.name,
|
||||
is_active: true,
|
||||
is_system: true,
|
||||
is_default: params.isDefault,
|
||||
system_key: params.systemKey,
|
||||
},
|
||||
{
|
||||
where: {
|
||||
id: params.id.toString(),
|
||||
company_id: params.companyId.toString(),
|
||||
},
|
||||
paranoid: false,
|
||||
transaction: params.transaction,
|
||||
}
|
||||
);
|
||||
|
||||
return Result.ok();
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(translateSequelizeError(error));
|
||||
}
|
||||
}
|
||||
|
||||
public async lockByCompanyId(
|
||||
companyId: UniqueID,
|
||||
transaction?: Transaction
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const BootstrapSystemPaymentMethodsRequestSchema = z.object({});
|
||||
|
||||
export type BootstrapSystemPaymentMethodsRequestDTO = z.infer<
|
||||
typeof BootstrapSystemPaymentMethodsRequestSchema
|
||||
>;
|
||||
@ -1,5 +1,4 @@
|
||||
export * from "./create-payment-method.request.dto";
|
||||
export * from "./bootstrap-system-payment-methods.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";
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { PaymentMethodDetailSchema } from "../shared";
|
||||
|
||||
export const BootstrapSystemPaymentMethodsResponseSchema = z.object({
|
||||
items: z.array(PaymentMethodDetailSchema),
|
||||
});
|
||||
|
||||
export type BootstrapSystemPaymentMethodsResponseDTO = z.infer<
|
||||
typeof BootstrapSystemPaymentMethodsResponseSchema
|
||||
>;
|
||||
@ -1,5 +1,4 @@
|
||||
export * from "./create-payment-method.response.dto";
|
||||
export * from "./bootstrap-system-payment-methods.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";
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user