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:
parent
01d0ff67f8
commit
f1c0df685b
@ -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",
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
@ -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:*",
|
||||
@ -52,4 +53,4 @@
|
||||
"use-debounce": "^10.1.1",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user