feat(audit): integrate audit functionality for payment methods

- Added @repo/rdx-audit as a dependency in server and catalogs package.json.
- Enhanced payment method services (creator, updater, deleter, status changer, and default setter) to include audit logging.
- Implemented audit actions for create, update, enable, disable, delete, and default set/ unset operations.
- Created payment-method-audit.ts for managing audit actions and state projections.
- Developed integration tests for payment method audit logging in the HTTP context.
This commit is contained in:
David Arranz 2026-08-25 00:28:23 +02:00
parent 01d0ff67f8
commit f1c0df685b
19 changed files with 562 additions and 31 deletions

View File

@ -16,6 +16,7 @@
},
"devDependencies": {
"@biomejs/biome": "^2.4.11",
"@repo/rdx-ddd": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/bcrypt": "^6.0.0",
"@types/cors": "^2.8.19",

View File

@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
import {
AsyncLocalStorageOperationContextStore,
type IModuleServer,
type IOperationContextStore,
type ISequelizeTransactionContext,
OperationAuditContextProvider,
SequelizeTransactionContext,
@ -53,6 +54,7 @@ export interface IHttpTestRuntime {
readonly config: ConfigType;
readonly database: Sequelize;
readonly transactionContext: ISequelizeTransactionContext;
readonly operationContext: IOperationContextStore;
readonly audit: IAudit;
request(
path: string,
@ -136,6 +138,7 @@ export async function createHttpTestRuntime(
baseUrl,
config,
database,
operationContext,
transactionContext,
audit,
request: async (path: string, init?: RequestInit) => {

View File

@ -0,0 +1,237 @@
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import test from "node:test";
import { SequelizeTransactionManager } from "@erp/core/api";
import { Name, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { AuditChangeModel, AuditEventModel } from "@/infrastructure/audit/sequelize";
import { PaymentMethodUpdater } from "../../../../../modules/catalogs/src/api/application/payment-methods/services/payment-method-updater";
import { SequelizePaymentMethodDomainMapper } from "../../../../../modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/mappers/sequelize-payment-method-domain.mapper";
import { SequelizePaymentMethodSummaryMapper } from "../../../../../modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/mappers/sequelize-payment-method-summary.mapper";
import { PaymentMethodModel } from "../../../../../modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/models/sequelize-payment-method.model";
import { SequelizePaymentMethodRepository } from "../../../../../modules/catalogs/src/api/infrastructure/payment-methods/persistence/sequelize/repositories/sequelize-payment-method.repository";
import {
buildCompanyHeaders,
createTestAccount,
createTestCompany,
createTestMembership,
loginByApi,
} from "./http-test-fixtures";
import { createHttpTestRuntime } from "./http-test-runtime";
test("payment methods registra create y update con el contexto HTTP en MariaDB", async (t) => {
const runtime = await createHttpTestRuntime();
t.after(async () => runtime.stop());
const account = await createTestAccount(runtime.database, {
email: `payment-method-audit-${randomUUID()}@example.test`,
password: "Password123!",
});
const company = await createTestCompany(runtime.database, {
legalName: "Payment method audit company",
});
await createTestMembership(runtime.database, {
accountId: account.id,
companyId: company.id,
});
const loginResponse = await loginByApi({
request: runtime.request,
email: account.email,
password: account.password,
});
assert.equal(loginResponse.status, 200);
const accessToken = (loginResponse.body as { access_token: string }).access_token;
const paymentMethodId = randomUUID();
const createResponse = await runtime.request("/api/v1/catalogs/payment-methods", {
method: "POST",
headers: {
...buildCompanyHeaders({ accessToken, companyId: company.id }),
"content-type": "application/json",
"x-correlation-id": "payment-method-create-correlation",
},
body: JSON.stringify({
id: paymentMethodId,
name: "Cash",
description: "Counter payment",
is_active: true,
}),
});
assert.equal(createResponse.status, 201);
const createdAudit = await AuditEventModel.findOne({
where: { action: "payment_method.created", target_id: paymentMethodId },
});
assert.ok(createdAudit);
assert.equal(createdAudit.source_type, "application");
assert.equal(createdAudit.source_id, "uecko-erp");
assert.equal(createdAudit.origin, "http");
assert.equal(createdAudit.operation_id, createResponse.headers.get("x-request-id"));
assert.equal(createdAudit.correlation_id, "payment-method-create-correlation");
assert.equal(createdAudit.actor_type, "user");
assert.equal(createdAudit.actor_id, account.id);
assert.equal(createdAudit.scope_type, "company");
assert.equal(createdAudit.scope_id, company.id);
assert.equal(createdAudit.target_type, "payment_method");
assert.equal(createdAudit.target_label, "Cash");
const updateResponse = await runtime.request(
`/api/v1/catalogs/payment-methods/${paymentMethodId}`,
{
method: "PUT",
headers: {
...buildCompanyHeaders({ accessToken, companyId: company.id }),
"content-type": "application/json",
"x-correlation-id": "payment-method-update-correlation",
},
body: JSON.stringify({ name: "Cash updated" }),
}
);
assert.equal(updateResponse.status, 200);
const updatedAudit = await AuditEventModel.findOne({
where: { action: "payment_method.updated", target_id: paymentMethodId },
});
assert.ok(updatedAudit);
assert.equal(updatedAudit.target_label, "Cash updated");
assert.equal(updatedAudit.actor_id, account.id);
assert.equal(updatedAudit.scope_id, company.id);
const changes = await AuditChangeModel.findAll({
where: { audit_event_id: updatedAudit.id },
order: [["position", "ASC"]],
});
assert.deepEqual(
changes.map((change) => ({
path: change.path,
kind: change.kind,
before: change.before_value,
after: change.after_value,
})),
[{ path: "name", kind: "change", before: "Cash", after: "Cash updated" }]
);
const secondPaymentMethodId = randomUUID();
const secondCreateResponse = await runtime.request("/api/v1/catalogs/payment-methods", {
method: "POST",
headers: {
...buildCompanyHeaders({ accessToken, companyId: company.id }),
"content-type": "application/json",
},
body: JSON.stringify({
id: secondPaymentMethodId,
name: "Card",
is_active: true,
}),
});
assert.equal(secondCreateResponse.status, 201);
const firstDefaultResponse = await runtime.request(
`/api/v1/catalogs/payment-methods/${paymentMethodId}/default`,
{
method: "PATCH",
headers: buildCompanyHeaders({ accessToken, companyId: company.id }),
}
);
assert.equal(firstDefaultResponse.status, 200);
const defaultResponse = await runtime.request(
`/api/v1/catalogs/payment-methods/${secondPaymentMethodId}/default`,
{
method: "PATCH",
headers: {
...buildCompanyHeaders({ accessToken, companyId: company.id }),
"x-correlation-id": "payment-method-default-correlation",
},
}
);
assert.equal(defaultResponse.status, 200);
const defaultEvents = await AuditEventModel.findAll({
where: {
action: ["payment_method.default_unset", "payment_method.default_set"],
operation_id: defaultResponse.headers.get("x-request-id") ?? "",
},
order: [["action", "ASC"]],
});
assert.equal(defaultEvents.length, 2);
const unsetEvent = defaultEvents.find((entry) => entry.action === "payment_method.default_unset");
const setEvent = defaultEvents.find((entry) => entry.action === "payment_method.default_set");
assert.equal(unsetEvent?.target_id, paymentMethodId);
assert.equal(setEvent?.target_id, secondPaymentMethodId);
assert.equal(unsetEvent?.correlation_id, "payment-method-default-correlation");
assert.equal(setEvent?.correlation_id, "payment-method-default-correlation");
assert.equal(unsetEvent?.actor_id, account.id);
assert.equal(setEvent?.scope_id, company.id);
const [unsetChange] = await AuditChangeModel.findAll({
where: { audit_event_id: unsetEvent?.id ?? "" },
});
const [setChange] = await AuditChangeModel.findAll({
where: { audit_event_id: setEvent?.id ?? "" },
});
assert.deepEqual(
{ path: unsetChange?.path, before: unsetChange?.before_value, after: unsetChange?.after_value },
{ path: "isDefault", before: true, after: false }
);
assert.deepEqual(
{ path: setChange?.path, before: setChange?.before_value, after: setChange?.after_value },
{ path: "isDefault", before: false, after: true }
);
const originalName = "Cash updated";
const rollbackName = "Cash rollback attempt";
const repository = new SequelizePaymentMethodRepository(
new SequelizePaymentMethodDomainMapper(),
new SequelizePaymentMethodSummaryMapper(),
runtime.database
);
const updater = new PaymentMethodUpdater(repository, runtime.audit);
const transactionManager = new SequelizeTransactionManager(
runtime.database,
runtime.transactionContext
);
const paymentMethodIdResult = UniqueID.create(paymentMethodId);
const rollbackNameResult = Name.create(rollbackName);
assert.equal(paymentMethodIdResult.isSuccess, true);
assert.equal(rollbackNameResult.isSuccess, true);
const changesBeforeRollback = await AuditChangeModel.count();
await assert.rejects(
() =>
runtime.operationContext.run(
{
operationId: "payment-method-rollback-operation",
correlationId: "payment-method-rollback-correlation",
origin: "http",
actor: { type: "user", id: account.id, label: account.email },
scope: { type: "company", id: company.id, label: null },
},
() =>
transactionManager.complete(async (transaction) => {
const updateResult = await updater.update({
companyId: UniqueID.create(company.id).data,
id: paymentMethodIdResult.data,
patchProps: { name: rollbackNameResult.data },
transaction,
});
assert.equal(updateResult.isSuccess, true);
return Result.fail(new Error("forced payment method rollback"));
})
),
/forced payment method rollback/
);
const rolledBackPaymentMethod = await PaymentMethodModel.findByPk(paymentMethodId);
assert.equal(rolledBackPaymentMethod?.name, originalName);
assert.equal(
await AuditEventModel.count({
where: { operation_id: "payment-method-rollback-operation" },
}),
0
);
assert.equal(await AuditChangeModel.count(), changesBeforeRollback);
});

View File

@ -37,6 +37,7 @@
"@erp/identity": "workspace:*",
"@repo/i18next": "workspace:*",
"@repo/rdx-criteria": "workspace:*",
"@repo/rdx-audit": "workspace:*",
"@repo/rdx-ddd": "workspace:*",
"@repo/rdx-logger": "workspace:*",
"@repo/rdx-ui": "workspace:*",

View File

@ -0,0 +1,33 @@
import type { AuditMutationTarget, AuditState } from "@repo/rdx-audit";
import type { PaymentMethod } from "../../../domain";
export const PAYMENT_METHOD_AUDIT_ACTIONS = {
CREATED: "payment_method.created",
UPDATED: "payment_method.updated",
ENABLED: "payment_method.enabled",
DISABLED: "payment_method.disabled",
DELETED: "payment_method.deleted",
DEFAULT_SET: "payment_method.default_set",
DEFAULT_UNSET: "payment_method.default_unset",
} as const;
/** Proyección estable del estado funcional, sin detalles ORM ni de infraestructura. */
export const toPaymentMethodAuditState = (paymentMethod: PaymentMethod) =>
({
name: paymentMethod.name.toPrimitive(),
description: paymentMethod.description.match(
(description) => description.toPrimitive(),
() => null
),
isActive: paymentMethod.isActive,
isSystem: paymentMethod.isSystem,
isDefault: paymentMethod.isDefault,
}) satisfies AuditState;
/** El label conserva el nombre visible del recurso en el momento de la mutación. */
export const toPaymentMethodAuditTarget = (paymentMethod: PaymentMethod): AuditMutationTarget => ({
type: "payment_method",
id: paymentMethod.id.toPrimitive(),
label: paymentMethod.name.toPrimitive(),
});

View File

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

View File

@ -1,15 +1,19 @@
import type { IAudit } from "@repo/rdx-audit";
import type { IPaymentMethodRepository } from "../repositories";
import { type IPaymentMethodDeleter, PaymentMethodDeleter } from "../services";
import type { IPaymentMethodUsageChecker } from "../services";
import { type IPaymentMethodDeleter, PaymentMethodDeleter } from "../services";
export const buildPaymentMethodDeleter = (params: {
repository: IPaymentMethodRepository;
usageChecker: IPaymentMethodUsageChecker;
audit: IAudit;
}): IPaymentMethodDeleter => {
const { repository, usageChecker } = params;
const { repository, usageChecker, audit } = params;
return new PaymentMethodDeleter({
repository,
usageChecker,
audit,
});
};

View File

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

View File

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

View File

@ -1,15 +1,37 @@
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { describe, test } from "node:test";
import { EntityNotFoundError } from "@erp/core/api";
import type { AuditError, IAudit, RecordAuditMutationShortcutInput } from "@repo/rdx-audit";
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 { PaymentMethodDeleter } from "../payment-method-deleter";
import { SetDefaultPaymentMethodService } from "../set-default-payment-method.service";
import type { IPaymentMethodRepository } from "../../repositories";
import type { PaymentMethodSummary } from "../../models";
import type { IPaymentMethodRepository } from "../../repositories";
import { PaymentMethodDeleter } from "../payment-method-deleter";
import { PaymentMethodStatusChanger } from "../payment-method-status-changer";
import { PaymentMethodUpdater } from "../payment-method-updater";
import { SetDefaultPaymentMethodService } from "../set-default-payment-method.service";
class AuditSpy implements IAudit {
public readonly mutations: RecordAuditMutationShortcutInput[] = [];
async record(): Promise<Result<void, AuditError>> {
return Result.ok();
}
async recordEvent(): Promise<Result<void, AuditError>> {
return Result.ok();
}
async recordMutation(input: RecordAuditMutationShortcutInput): Promise<Result<void, AuditError>> {
this.mutations.push(input);
return Result.ok();
}
}
const audit = new AuditSpy();
class InMemoryPaymentMethodRepository implements IPaymentMethodRepository {
private readonly items = new Map<string, PaymentMethod>();
@ -126,9 +148,113 @@ const buildPaymentMethod = (params: {
};
describe("Payment method services", () => {
test("enable registra la transición activa", async () => {
const repository = new InMemoryPaymentMethodRepository();
const audit = new AuditSpy();
const companyId = UniqueID.generateNewID();
const paymentMethod = buildPaymentMethod({ companyId, name: "Card", isActive: false });
await repository.create(paymentMethod);
const result = await new PaymentMethodStatusChanger(repository, audit).changeStatus({
paymentMethod,
action: "enable",
});
assert.equal(result.isSuccess, true);
assert.equal(audit.mutations.length, 1);
assert.deepEqual(audit.mutations[0], {
action: "payment_method.enabled",
target: { type: "payment_method", id: paymentMethod.id.toPrimitive(), label: "Card" },
before: {
name: "Card",
description: null,
isActive: false,
isSystem: false,
isDefault: false,
},
after: { name: "Card", description: null, isActive: true, isSystem: false, isDefault: false },
});
});
test("disable rechaza métodos de sistema sin auditar", async () => {
const repository = new InMemoryPaymentMethodRepository();
const audit = new AuditSpy();
const paymentMethod = buildPaymentMethod({
companyId: UniqueID.generateNewID(),
name: "System",
isSystem: true,
});
const result = await new PaymentMethodStatusChanger(repository, audit).changeStatus({
paymentMethod,
action: "disable",
});
assert.equal(result.isFailure, true);
assert.equal(audit.mutations.length, 0);
});
test("disable registra la transición inactiva", async () => {
const repository = new InMemoryPaymentMethodRepository();
const audit = new AuditSpy();
const paymentMethod = buildPaymentMethod({ companyId: UniqueID.generateNewID(), name: "Cash" });
const result = await new PaymentMethodStatusChanger(repository, audit).changeStatus({
paymentMethod,
action: "disable",
});
assert.equal(result.isSuccess, true);
assert.equal(audit.mutations.length, 1);
assert.equal(audit.mutations[0]?.action, "payment_method.disabled");
assert.equal(audit.mutations[0]?.before.isActive, true);
assert.equal(audit.mutations[0]?.after.isActive, false);
});
test("update rechaza métodos de sistema sin auditar", async () => {
const repository = new InMemoryPaymentMethodRepository();
const audit = new AuditSpy();
const companyId = UniqueID.generateNewID();
const paymentMethod = buildPaymentMethod({ companyId, name: "System", isSystem: true });
await repository.create(paymentMethod);
const name = Name.create("Changed");
assert.equal(name.isSuccess, true);
const result = await new PaymentMethodUpdater(repository, audit).update({
companyId,
id: paymentMethod.id,
patchProps: { name: name.data },
});
assert.equal(result.isFailure, true);
assert.equal(audit.mutations.length, 0);
});
test("delete registra estado completo y no audita rechazos", async () => {
const repository = new InMemoryPaymentMethodRepository();
const audit = new AuditSpy();
const paymentMethod = buildPaymentMethod({ companyId: UniqueID.generateNewID(), name: "Cash" });
await repository.create(paymentMethod);
const deleter = new PaymentMethodDeleter({
repository,
audit,
usageChecker: { findUsage: async () => Result.ok([]) },
});
const result = await deleter.delete({ paymentMethod });
assert.equal(result.isSuccess, true);
assert.equal(audit.mutations.length, 1);
assert.deepEqual(audit.mutations[0]?.target, {
type: "payment_method",
id: paymentMethod.id.toPrimitive(),
label: "Cash",
});
assert.deepEqual(audit.mutations[0]?.before, {
name: "Cash",
description: null,
isActive: true,
isSystem: false,
isDefault: false,
});
assert.deepEqual(audit.mutations[0]?.after, {});
assert.equal(audit.mutations[0]?.action, "payment_method.deleted");
});
test("set default swaps previous default atomically in repository state", async () => {
const repository = new InMemoryPaymentMethodRepository();
const service = new SetDefaultPaymentMethodService(repository);
const service = new SetDefaultPaymentMethodService(repository, audit);
const companyId = UniqueID.generateNewID();
const cash = buildPaymentMethod({
@ -162,7 +288,7 @@ describe("Payment method services", () => {
test("set default is idempotent when method is already default", async () => {
const repository = new InMemoryPaymentMethodRepository();
const service = new SetDefaultPaymentMethodService(repository);
const service = new SetDefaultPaymentMethodService(repository, audit);
const companyId = UniqueID.generateNewID();
const cash = buildPaymentMethod({
@ -184,7 +310,7 @@ describe("Payment method services", () => {
test("set default rejects inactive methods", async () => {
const repository = new InMemoryPaymentMethodRepository();
const service = new SetDefaultPaymentMethodService(repository);
const service = new SetDefaultPaymentMethodService(repository, audit);
const companyId = UniqueID.generateNewID();
const inactive = buildPaymentMethod({
@ -205,7 +331,7 @@ describe("Payment method services", () => {
test("set default preserves company isolation", async () => {
const repository = new InMemoryPaymentMethodRepository();
const service = new SetDefaultPaymentMethodService(repository);
const service = new SetDefaultPaymentMethodService(repository, audit);
const companyA = UniqueID.generateNewID();
const companyB = UniqueID.generateNewID();
@ -226,11 +352,13 @@ describe("Payment method services", () => {
test("delete blocks referenced payment methods", async () => {
const repository = new InMemoryPaymentMethodRepository();
const audit = new AuditSpy();
const deleter = new PaymentMethodDeleter({
repository,
usageChecker: {
findUsage: async () => Result.ok([{ source: "customers" }]),
},
audit,
});
const companyId = UniqueID.generateNewID();
const paymentMethod = buildPaymentMethod({
@ -245,15 +373,21 @@ describe("Payment method services", () => {
});
assert.equal(result.isFailure, true);
assert.equal(audit.mutations.length, 0);
const persistedPaymentMethod = await repository.getByIdInCompany(companyId, paymentMethod.id);
assert.equal(persistedPaymentMethod.isSuccess, true);
});
test("delete blocks system payment methods", async () => {
const repository = new InMemoryPaymentMethodRepository();
const audit = new AuditSpy();
const deleter = new PaymentMethodDeleter({
repository,
usageChecker: {
findUsage: async () => Result.ok([]),
},
audit,
});
const companyId = UniqueID.generateNewID();
const paymentMethod = buildPaymentMethod({
@ -269,5 +403,9 @@ describe("Payment method services", () => {
});
assert.equal(result.isFailure, true);
assert.equal(audit.mutations.length, 0);
const persistedPaymentMethod = await repository.getByIdInCompany(companyId, paymentMethod.id);
assert.equal(persistedPaymentMethod.isSuccess, true);
});
});

View File

@ -4,7 +4,7 @@ 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-usage-checker";
export * from "./payment-method-status-changer";
export * from "./payment-method-updater";
export * from "./payment-method-usage-checker";
export * from "./set-default-payment-method.service";

View File

@ -1,7 +1,13 @@
import type { IAudit } from "@repo/rdx-audit";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { type IPaymentMethodCreateProps, PaymentMethod } from "../../../domain";
import {
PAYMENT_METHOD_AUDIT_ACTIONS,
toPaymentMethodAuditState,
toPaymentMethodAuditTarget,
} from "../audit/payment-method-audit";
import type { IPaymentMethodRepository } from "../repositories";
export interface IPaymentMethodCreatorParams {
@ -16,7 +22,10 @@ export interface IPaymentMethodCreator {
}
export class PaymentMethodCreator implements IPaymentMethodCreator {
constructor(private readonly repository: IPaymentMethodRepository) {}
constructor(
private readonly repository: IPaymentMethodRepository,
private readonly audit: IAudit
) {}
async create(params: IPaymentMethodCreatorParams): Promise<Result<PaymentMethod, Error>> {
const { companyId, id, props, transaction } = params;
@ -36,6 +45,14 @@ export class PaymentMethodCreator implements IPaymentMethodCreator {
return Result.fail(saveResult.error);
}
const auditResult = await this.audit.recordMutation({
action: PAYMENT_METHOD_AUDIT_ACTIONS.CREATED,
target: toPaymentMethodAuditTarget(paymentMethod),
before: {},
after: toPaymentMethodAuditState(paymentMethod),
});
if (auditResult.isFailure) return Result.fail(auditResult.error);
return Result.ok(paymentMethod);
}
}

View File

@ -1,8 +1,15 @@
import type { IAudit } from "@repo/rdx-audit";
import { Result } from "@repo/rdx-utils";
import type { PaymentMethod } from "../../../domain";
import { PaymentMethodInUseError } from "../../../domain";
import {
PAYMENT_METHOD_AUDIT_ACTIONS,
toPaymentMethodAuditState,
toPaymentMethodAuditTarget,
} from "../audit/payment-method-audit";
import type { IPaymentMethodRepository } from "../repositories";
import type { IPaymentMethodUsageChecker } from "./payment-method-usage-checker";
export interface IPaymentMethodDeleter {
@ -17,6 +24,7 @@ export class PaymentMethodDeleter implements IPaymentMethodDeleter {
private readonly deps: {
repository: IPaymentMethodRepository;
usageChecker: IPaymentMethodUsageChecker;
audit: IAudit;
}
) {}
@ -25,6 +33,8 @@ export class PaymentMethodDeleter implements IPaymentMethodDeleter {
transaction?: unknown;
}): Promise<Result<PaymentMethod, Error>> {
const { paymentMethod, transaction } = params;
const before = toPaymentMethodAuditState(paymentMethod);
const target = toPaymentMethodAuditTarget(paymentMethod);
const invariantResult = paymentMethod.ensureCanBeDeleted();
if (invariantResult.isFailure) {
@ -60,6 +70,14 @@ export class PaymentMethodDeleter implements IPaymentMethodDeleter {
return Result.fail(deleteResult.error);
}
const auditResult = await this.deps.audit.recordMutation({
action: PAYMENT_METHOD_AUDIT_ACTIONS.DELETED,
target,
before,
after: {},
});
if (auditResult.isFailure) return Result.fail(auditResult.error);
return Result.ok(paymentMethod);
}
}

View File

@ -1,6 +1,12 @@
import type { IAudit } from "@repo/rdx-audit";
import { Result } from "@repo/rdx-utils";
import type { PaymentMethod } from "../../../domain";
import {
PAYMENT_METHOD_AUDIT_ACTIONS,
toPaymentMethodAuditState,
toPaymentMethodAuditTarget,
} from "../audit/payment-method-audit";
import type { IPaymentMethodRepository } from "../repositories";
export type PaymentMethodStatusChangeAction = "enable" | "disable";
@ -14,7 +20,10 @@ export interface IPaymentMethodStatusChanger {
}
export class PaymentMethodStatusChanger implements IPaymentMethodStatusChanger {
public constructor(private readonly repository: IPaymentMethodRepository) {}
public constructor(
private readonly repository: IPaymentMethodRepository,
private readonly audit: IAudit
) {}
public async changeStatus(params: {
paymentMethod: PaymentMethod;
@ -22,6 +31,7 @@ export class PaymentMethodStatusChanger implements IPaymentMethodStatusChanger {
transaction?: unknown;
}): Promise<Result<PaymentMethod, Error>> {
const { paymentMethod, action, transaction } = params;
const before = toPaymentMethodAuditState(paymentMethod);
const statusResult = action === "enable" ? paymentMethod.enable() : paymentMethod.disable();
@ -39,6 +49,17 @@ export class PaymentMethodStatusChanger implements IPaymentMethodStatusChanger {
return Result.fail(persistenceResult.error);
}
const auditResult = await this.audit.recordMutation({
action:
action === "enable"
? PAYMENT_METHOD_AUDIT_ACTIONS.ENABLED
: PAYMENT_METHOD_AUDIT_ACTIONS.DISABLED,
target: toPaymentMethodAuditTarget(paymentMethod),
before,
after: toPaymentMethodAuditState(paymentMethod),
});
if (auditResult.isFailure) return Result.fail(auditResult.error);
return Result.ok(paymentMethod);
}
}

View File

@ -1,7 +1,13 @@
import type { IAudit } from "@repo/rdx-audit";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { PaymentMethod, PaymentMethodPatchProps } from "../../../domain";
import {
PAYMENT_METHOD_AUDIT_ACTIONS,
toPaymentMethodAuditState,
toPaymentMethodAuditTarget,
} from "../audit/payment-method-audit";
import type { IPaymentMethodRepository } from "../repositories";
export interface IPaymentMethodUpdater {
@ -14,7 +20,10 @@ export interface IPaymentMethodUpdater {
}
export class PaymentMethodUpdater implements IPaymentMethodUpdater {
constructor(private readonly repository: IPaymentMethodRepository) {}
constructor(
private readonly repository: IPaymentMethodRepository,
private readonly audit: IAudit
) {}
async update(params: {
companyId: UniqueID;
@ -32,6 +41,7 @@ export class PaymentMethodUpdater implements IPaymentMethodUpdater {
}
const paymentMethod = existingResult.data;
const before = toPaymentMethodAuditState(paymentMethod);
// Aplicar cambios en el agregado
const updateResult = paymentMethod.update(patchProps);
@ -54,6 +64,14 @@ export class PaymentMethodUpdater implements IPaymentMethodUpdater {
return Result.fail(saveResult.error);
}
const auditResult = await this.audit.recordMutation({
action: PAYMENT_METHOD_AUDIT_ACTIONS.UPDATED,
target: toPaymentMethodAuditTarget(paymentMethod),
before,
after: toPaymentMethodAuditState(paymentMethod),
});
if (auditResult.isFailure) return Result.fail(auditResult.error);
return Result.ok(paymentMethod);
}
}

View File

@ -1,7 +1,13 @@
import type { IAudit } from "@repo/rdx-audit";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { PaymentMethodCannotBeDefaultError, type PaymentMethod } from "../../../domain";
import { type PaymentMethod, PaymentMethodCannotBeDefaultError } from "../../../domain";
import {
PAYMENT_METHOD_AUDIT_ACTIONS,
toPaymentMethodAuditState,
toPaymentMethodAuditTarget,
} from "../audit/payment-method-audit";
import type { IPaymentMethodRepository } from "../repositories";
export interface ISetDefaultPaymentMethodService {
@ -13,7 +19,10 @@ export interface ISetDefaultPaymentMethodService {
}
export class SetDefaultPaymentMethodService implements ISetDefaultPaymentMethodService {
public constructor(private readonly repository: IPaymentMethodRepository) {}
public constructor(
private readonly repository: IPaymentMethodRepository,
private readonly audit: IAudit
) {}
public async setDefault(params: {
companyId: UniqueID;
@ -56,6 +65,7 @@ export class SetDefaultPaymentMethodService implements ISetDefaultPaymentMethodS
const currentDefault = currentDefaultResult.data.getOrUndefined() ?? null;
if (currentDefault && currentDefault.id.toString() !== paymentMethod.id.toString()) {
const previousDefaultState = toPaymentMethodAuditState(currentDefault);
const unmarkResult = currentDefault.unmarkAsDefault();
if (unmarkResult.isFailure) {
return Result.fail(unmarkResult.error);
@ -66,9 +76,17 @@ export class SetDefaultPaymentMethodService implements ISetDefaultPaymentMethodS
if (saveCurrentResult.isFailure) {
return Result.fail(saveCurrentResult.error);
}
const auditResult = await this.audit.recordMutation({
action: PAYMENT_METHOD_AUDIT_ACTIONS.DEFAULT_UNSET,
target: toPaymentMethodAuditTarget(currentDefault),
before: previousDefaultState,
after: toPaymentMethodAuditState(currentDefault),
});
if (auditResult.isFailure) return Result.fail(auditResult.error);
}
}
const previousPaymentMethodState = toPaymentMethodAuditState(paymentMethod);
const markResult = paymentMethod.markAsDefault();
if (markResult.isFailure) {
return Result.fail(markResult.error);
@ -79,6 +97,13 @@ export class SetDefaultPaymentMethodService implements ISetDefaultPaymentMethodS
if (savePaymentMethodResult.isFailure) {
return Result.fail(savePaymentMethodResult.error);
}
const auditResult = await this.audit.recordMutation({
action: PAYMENT_METHOD_AUDIT_ACTIONS.DEFAULT_SET,
target: toPaymentMethodAuditTarget(paymentMethod),
before: previousPaymentMethodState,
after: toPaymentMethodAuditState(paymentMethod),
});
if (auditResult.isFailure) return Result.fail(auditResult.error);
}
return Result.ok(paymentMethod);

View File

@ -2,8 +2,8 @@ import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IPaymentMethodFullSnapshotBuilder } from "../snapshot-builders";
import type { ISetDefaultPaymentMethodService } from "../services";
import type { IPaymentMethodFullSnapshotBuilder } from "../snapshot-builders";
export type SetDefaultPaymentMethodByIdUseCaseInput = {
companyId: UniqueID;

View File

@ -25,10 +25,10 @@ import {
ListPaymentMethodsUseCase,
UpdatePaymentMethodByIdUseCase,
} from "../../../application/payment-methods/use-cases";
import { buildPaymentMethodReferenceUsageChecker } from "../payment-method-reference-usage-checker";
import { buildPaymentMethodPersistenceMappers } from "./payment-method-persistence-mappers.di";
import { buildPaymentMethodRepository } from "./payment-method-repositories.di";
import { buildPaymentMethodReferenceUsageChecker } from "../payment-method-reference-usage-checker";
export type PaymentMethodsInternalDeps = {
repository: IPaymentMethodRepository;
@ -47,7 +47,7 @@ export type PaymentMethodsInternalDeps = {
export const buildPaymentMethodsDependencies = (
params: ModuleParams
): PaymentMethodsInternalDeps => {
const { database, transactionContext } = params;
const { audit, database, transactionContext } = params;
// Infrastructure
const transactionManager = buildTransactionManager(database as Sequelize, transactionContext);
@ -59,11 +59,11 @@ export const buildPaymentMethodsDependencies = (
// Application helpers
const inputMappers = buildPaymentMethodInputMappers();
const finder = buildPaymentMethodFinder({ repository });
const creator = buildPaymentMethodCreator({ repository });
const updater = buildPaymentMethodUpdater({ repository });
const deleter = buildPaymentMethodDeleter({ repository, usageChecker });
const statusChanger = buildPaymentMethodStatusChanger({ repository });
const setDefaultService = new SetDefaultPaymentMethodService(repository);
const creator = buildPaymentMethodCreator({ repository, audit });
const updater = buildPaymentMethodUpdater({ repository, audit });
const deleter = buildPaymentMethodDeleter({ repository, usageChecker, audit });
const statusChanger = buildPaymentMethodStatusChanger({ repository, audit });
const setDefaultService = new SetDefaultPaymentMethodService(repository, audit);
const snapshotBuilders = buildPaymentMethodSnapshotBuilders();

View File

@ -180,6 +180,9 @@ importers:
'@biomejs/biome':
specifier: ^2.4.11
version: 2.4.11
'@repo/rdx-ddd':
specifier: workspace:*
version: link:../../packages/rdx-ddd
'@repo/typescript-config':
specifier: workspace:*
version: link:../../packages/typescript-config
@ -428,6 +431,9 @@ importers:
'@repo/i18next':
specifier: workspace:*
version: link:../../packages/i18n
'@repo/rdx-audit':
specifier: workspace:*
version: link:../../packages/rdx-audit
'@repo/rdx-criteria':
specifier: workspace:*
version: link:../../packages/rdx-criteria