feat(audit): integrate audit functionality for payment terms management

This commit is contained in:
David Arranz 2026-08-25 00:47:29 +02:00
parent f1c0df685b
commit 71b79cc4b1
12 changed files with 610 additions and 35 deletions

View File

@ -0,0 +1,201 @@
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import test from "node:test";
import { SequelizeTransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { AuditChangeModel, AuditEventModel } from "@/infrastructure/audit/sequelize";
import { PaymentTermUpdater } from "../../../../../modules/catalogs/src/api/application/payment-terms/services/payment-term-updater.service";
import {
PaymentTermDueDays,
PaymentTermPercentage,
} from "../../../../../modules/catalogs/src/api/domain/payment-terms";
import { SequelizePaymentTermDomainMapper } from "../../../../../modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/mappers/sequelize-payment-term-domain.mapper";
import { SequelizePaymentTermSummaryMapper } from "../../../../../modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/mappers/sequelize-payment-term-summary.mapper";
import { PaymentTermDueRuleModel } from "../../../../../modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/models/sequelize-payment-term-due-rule.model";
import { SequelizePaymentTermRepository } from "../../../../../modules/catalogs/src/api/infrastructure/payment-terms/persistence/sequelize/repositories/sequelize-payment-term.repository";
import {
buildCompanyHeaders,
createTestAccount,
createTestCompany,
createTestMembership,
loginByApi,
} from "./http-test-fixtures";
import { createHttpTestRuntime } from "./http-test-runtime";
test("payment terms registra dueRules atómicas y revierte junto con el agregado en MariaDB", async (t) => {
const runtime = await createHttpTestRuntime();
t.after(async () => runtime.stop());
const account = await createTestAccount(runtime.database, {
email: `payment-term-audit-${randomUUID()}@example.test`,
password: "Password123!",
});
const company = await createTestCompany(runtime.database, {
legalName: "Payment term 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 paymentTermId = randomUUID();
const createResponse = await runtime.request("/api/v1/catalogs/payment-terms", {
method: "POST",
headers: {
...buildCompanyHeaders({ accessToken, companyId: company.id }),
"content-type": "application/json",
},
body: JSON.stringify({
id: paymentTermId,
name: "Net 30",
is_active: true,
due_rules: [{ due_days: "30", percentage: { value: "10000", scale: "2" } }],
}),
});
assert.equal(createResponse.status, 201);
const updateResponse = await runtime.request(`/api/v1/catalogs/payment-terms/${paymentTermId}`, {
method: "PUT",
headers: {
...buildCompanyHeaders({ accessToken, companyId: company.id }),
"content-type": "application/json",
"x-correlation-id": "payment-term-due-rules-correlation",
},
body: JSON.stringify({
due_rules: [
{ due_days: "30", percentage: { value: "5000", scale: "2" } },
{ due_days: "60", percentage: { value: "5000", scale: "2" } },
],
}),
});
assert.equal(updateResponse.status, 200);
const auditEvent = await AuditEventModel.findOne({
where: { action: "payment_term.updated", target_id: paymentTermId },
});
assert.ok(auditEvent);
assert.equal(auditEvent.source_type, "application");
assert.equal(auditEvent.source_id, "uecko-erp");
assert.equal(auditEvent.origin, "http");
assert.equal(auditEvent.operation_id, updateResponse.headers.get("x-request-id"));
assert.equal(auditEvent.correlation_id, "payment-term-due-rules-correlation");
assert.equal(auditEvent.actor_type, "user");
assert.equal(auditEvent.actor_id, account.id);
assert.equal(auditEvent.scope_type, "company");
assert.equal(auditEvent.scope_id, company.id);
assert.equal(auditEvent.target_type, "payment_term");
assert.equal(auditEvent.target_label, "Net 30");
const auditChanges = await AuditChangeModel.findAll({
where: { audit_event_id: auditEvent.id },
order: [["position", "ASC"]],
});
assert.deepEqual(
auditChanges.map((change) => ({
path: change.path,
kind: change.kind,
before: change.before_value,
after: change.after_value,
})),
[
{
path: "dueRules",
kind: "change",
before: [{ dueDays: 30, percentage: "100.00" }],
after: [
{ dueDays: 30, percentage: "50.00" },
{ dueDays: 60, percentage: "50.00" },
],
},
]
);
assert.equal(
auditChanges.some((change) => change.path.startsWith("dueRules.")),
false
);
const dueDays30 = PaymentTermDueDays.create(30);
const dueDays60 = PaymentTermDueDays.create(60);
const dueDays90 = PaymentTermDueDays.create(90);
const percentage25 = PaymentTermPercentage.create({ value: 2500, scale: 2 });
const percentage50 = PaymentTermPercentage.create({ value: 5000, scale: 2 });
assert.equal(dueDays30.isSuccess, true);
assert.equal(dueDays60.isSuccess, true);
assert.equal(dueDays90.isSuccess, true);
assert.equal(percentage25.isSuccess, true);
assert.equal(percentage50.isSuccess, true);
const repository = new SequelizePaymentTermRepository(
new SequelizePaymentTermDomainMapper(),
new SequelizePaymentTermSummaryMapper(),
runtime.database
);
const updater = new PaymentTermUpdater(repository, runtime.audit);
const transactionManager = new SequelizeTransactionManager(
runtime.database,
runtime.transactionContext
);
const paymentTermIdResult = UniqueID.create(paymentTermId);
assert.equal(paymentTermIdResult.isSuccess, true);
const changesBeforeRollback = await AuditChangeModel.count();
await assert.rejects(
() =>
runtime.operationContext.run(
{
operationId: "payment-term-rollback-operation",
correlationId: "payment-term-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: paymentTermIdResult.data,
patchProps: {
dueRules: [
{ dueDays: dueDays30.data, percentage: percentage25.data },
{ dueDays: dueDays60.data, percentage: percentage25.data },
{ dueDays: dueDays90.data, percentage: percentage50.data },
],
},
transaction,
});
assert.equal(updateResult.isSuccess, true);
return Result.fail(new Error("forced payment term rollback"));
})
),
/forced payment term rollback/
);
const persistedRules = await PaymentTermDueRuleModel.findAll({
where: { payment_term_id: paymentTermId },
order: [["due_days", "ASC"]],
});
assert.deepEqual(
persistedRules.map((rule) => ({ dueDays: rule.due_days, percentage: rule.percentage_value })),
[
{ dueDays: 30, percentage: 5000 },
{ dueDays: 60, percentage: 5000 },
]
);
assert.equal(
await AuditEventModel.count({ where: { operation_id: "payment-term-rollback-operation" } }),
0
);
assert.equal(await AuditChangeModel.count(), changesBeforeRollback);
});

View File

@ -0,0 +1,42 @@
import type { AuditMutationTarget, AuditState } from "@repo/rdx-audit";
import type { PaymentTerm } from "../../../domain";
export const PAYMENT_TERM_AUDIT_ACTIONS = {
CREATED: "payment_term.created",
UPDATED: "payment_term.updated",
ENABLED: "payment_term.enabled",
DISABLED: "payment_term.disabled",
DELETED: "payment_term.deleted",
} as const;
const toPercentageString = (params: { value: number; scale: number }): string => {
const divisor = 10 ** params.scale;
const integer = Math.floor(params.value / divisor);
const decimal = String(params.value % divisor).padStart(params.scale, "0");
return `${integer}.${decimal}`;
};
/** Proyección histórica estable; las reglas ya llegan ordenadas por dueDays desde el dominio. */
export const toPaymentTermAuditState = (paymentTerm: PaymentTerm) =>
({
name: paymentTerm.name.toPrimitive(),
description: paymentTerm.description.match(
(description) => description.toPrimitive(),
() => null
),
isActive: paymentTerm.isActive,
isSystem: paymentTerm.isSystem,
dueRules: paymentTerm.dueRules.getAll().map((rule) => ({
dueDays: rule.dueDays.toPrimitive(),
percentage: toPercentageString(rule.percentage),
})),
}) satisfies AuditState;
/** El nombre se conserva como label del recurso en el momento de la mutación. */
export const toPaymentTermAuditTarget = (paymentTerm: PaymentTerm): AuditMutationTarget => ({
type: "payment_term",
id: paymentTerm.id.toPrimitive(),
label: paymentTerm.name.toPrimitive(),
});

View File

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

View File

@ -1,3 +1,5 @@
import type { IAudit } from "@repo/rdx-audit";
import type { IPaymentTermRepository } from "../repositories";
import type { IPaymentTermUsageChecker } from "../services";
import { type IPaymentTermDeleter, PaymentTermDeleter } from "../services";
@ -5,8 +7,9 @@ import { type IPaymentTermDeleter, PaymentTermDeleter } from "../services";
export const buildPaymentTermDeleter = (params: {
repository: IPaymentTermRepository;
usageChecker: IPaymentTermUsageChecker;
audit: IAudit;
}): IPaymentTermDeleter => {
const { repository, usageChecker } = params;
const { repository, usageChecker, audit } = params;
return new PaymentTermDeleter({ repository, usageChecker });
return new PaymentTermDeleter({ repository, usageChecker, audit });
};

View File

@ -1,3 +1,4 @@
import type { IAudit } from "@repo/rdx-audit";
import type { UniqueID } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
@ -6,6 +7,7 @@ import { type IPaymentTermStatusChanger, PaymentTermStatusChanger } from "../ser
export const buildPaymentTermStatusChanger = (params: {
repository: IPaymentTermRepository;
audit: IAudit;
companyDefaultUsageChecker?: {
isUsedAsCompanyDefault(params: {
companyId: UniqueID;
@ -14,7 +16,7 @@ export const buildPaymentTermStatusChanger = (params: {
}): Promise<Result<boolean, Error>>;
};
}): IPaymentTermStatusChanger => {
const { repository, companyDefaultUsageChecker } = params;
const { repository, audit, companyDefaultUsageChecker } = params;
return new PaymentTermStatusChanger({ repository, companyDefaultUsageChecker });
return new PaymentTermStatusChanger({ repository, companyDefaultUsageChecker, audit });
};

View File

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

View File

@ -2,6 +2,7 @@ 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 { UniqueID } from "@repo/rdx-ddd";
import { Collection, Maybe, Result } from "@repo/rdx-utils";
@ -11,8 +12,29 @@ import { PaymentTermName } from "../../../../domain/payment-terms/payment-term-n
import { PaymentTermPercentage } from "../../../../domain/payment-terms/payment-term-percentage";
import type { PaymentTermSummary } from "../../models/payment-term-summary.model";
import type { IPaymentTermRepository } from "../../repositories/payment-term-repository.interface";
import { PaymentTermCreator } from "../payment-term-creator.service";
import { PaymentTermDeleter } from "../payment-term-deleter.service";
import { PaymentTermStatusChanger } from "../payment-term-status-changer.service";
import { PaymentTermUpdater } from "../payment-term-updater.service";
class AuditSpy implements IAudit {
public readonly mutations: RecordAuditMutationShortcutInput[] = [];
public constructor(private readonly failure?: Error) {}
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 this.failure ? Result.fail(this.failure) : Result.ok();
}
}
class InMemoryPaymentTermRepository implements IPaymentTermRepository {
private readonly items = new Map<string, PaymentTerm>();
@ -124,9 +146,174 @@ const createPaymentTerm = (companyId = UniqueID.generateNewID()) => {
return paymentTermResult.data;
};
const createSystemPaymentTerm = () => {
const companyId = UniqueID.generateNewID();
const nameResult = PaymentTermName.create("System term");
assert.equal(nameResult.isSuccess, true);
const paymentTermResult = PaymentTerm.create(
{
companyId,
name: nameResult.data,
description: Maybe.none(),
isActive: true,
isSystem: true,
dueRules: [createRuleProps({ dueDays: 0, percentageValue: 10000 })],
},
UniqueID.generateNewID()
);
assert.equal(paymentTermResult.isSuccess, true);
return paymentTermResult.data;
};
describe("Payment term services", () => {
test("create registra el estado completo", async () => {
const repository = new InMemoryPaymentTermRepository();
const audit = new AuditSpy();
const companyId = UniqueID.generateNewID();
const paymentTerm = createPaymentTerm(companyId);
const result = await new PaymentTermCreator(repository, audit).create({
companyId,
id: paymentTerm.id,
props: {
companyId,
name: paymentTerm.name,
description: paymentTerm.description,
isActive: paymentTerm.isActive,
isSystem: paymentTerm.isSystem,
dueRules: paymentTerm.dueRules.getAll().map((rule) => ({
dueDays: rule.dueDays,
percentage: rule.percentage,
})),
},
transaction: undefined,
});
assert.equal(result.isSuccess, true);
assert.deepEqual(audit.mutations, [
{
action: "payment_term.created",
target: { type: "payment_term", id: paymentTerm.id.toPrimitive(), label: "30/60" },
before: {},
after: {
name: "30/60",
description: null,
isActive: true,
isSystem: false,
dueRules: [
{ dueDays: 30, percentage: "50.00" },
{ dueDays: 60, percentage: "50.00" },
],
},
},
]);
});
test("update registra reglas de vencimiento como un único estado ordenado", async () => {
const repository = new InMemoryPaymentTermRepository();
const audit = new AuditSpy();
const paymentTerm = createPaymentTerm();
await repository.create(paymentTerm);
const result = await new PaymentTermUpdater(repository, audit).update({
companyId: paymentTerm.companyId,
id: paymentTerm.id,
patchProps: {
dueRules: [
createRuleProps({ dueDays: 60, percentageValue: 5000 }),
createRuleProps({ dueDays: 30, percentageValue: 5000 }),
],
},
});
assert.equal(result.isSuccess, true);
assert.equal(audit.mutations.length, 0);
const changedRulesResult = await new PaymentTermUpdater(repository, audit).update({
companyId: paymentTerm.companyId,
id: paymentTerm.id,
patchProps: {
dueRules: [
createRuleProps({ dueDays: 30, percentageValue: 5000 }),
createRuleProps({ dueDays: 60, percentageValue: 2500 }),
createRuleProps({ dueDays: 90, percentageValue: 2500 }),
],
},
});
assert.equal(changedRulesResult.isSuccess, true);
assert.deepEqual(audit.mutations, [
{
action: "payment_term.updated",
target: { type: "payment_term", id: paymentTerm.id.toPrimitive(), label: "30/60" },
before: {
name: "30/60",
description: null,
isActive: true,
isSystem: false,
dueRules: [
{ dueDays: 30, percentage: "50.00" },
{ dueDays: 60, percentage: "50.00" },
],
},
after: {
name: "30/60",
description: null,
isActive: true,
isSystem: false,
dueRules: [
{ dueDays: 30, percentage: "50.00" },
{ dueDays: 60, percentage: "25.00" },
{ dueDays: 90, percentage: "25.00" },
],
},
},
]);
});
test("propaga el fallo de auditoría al actualizar", async () => {
const repository = new InMemoryPaymentTermRepository();
const paymentTerm = createPaymentTerm();
await repository.create(paymentTerm);
const auditFailure = new Error("audit unavailable");
const result = await new PaymentTermUpdater(repository, new AuditSpy(auditFailure)).update({
companyId: paymentTerm.companyId,
id: paymentTerm.id,
patchProps: { isActive: false },
});
assert.equal(result.isFailure, true);
assert.equal(result.error, auditFailure);
});
test("update rechaza plazos de sistema y not found sin auditar", async () => {
const repository = new InMemoryPaymentTermRepository();
const audit = new AuditSpy();
const paymentTerm = createSystemPaymentTerm();
await repository.create(paymentTerm);
const updater = new PaymentTermUpdater(repository, audit);
const systemResult = await updater.update({
companyId: paymentTerm.companyId,
id: paymentTerm.id,
patchProps: { isActive: false },
});
const notFoundResult = await updater.update({
companyId: paymentTerm.companyId,
id: UniqueID.generateNewID(),
patchProps: { isActive: false },
});
assert.equal(systemResult.isFailure, true);
assert.equal(notFoundResult.isFailure, true);
assert.equal(audit.mutations.length, 0);
});
test("delete allows removing unreferenced custom payment terms", async () => {
const repository = new InMemoryPaymentTermRepository();
const audit = new AuditSpy();
const paymentTerm = createPaymentTerm();
await repository.create(paymentTerm);
@ -135,6 +322,7 @@ describe("Payment term services", () => {
usageChecker: {
findUsage: async () => Result.ok([]),
},
audit,
});
const result = await deleter.delete({
@ -142,10 +330,28 @@ describe("Payment term services", () => {
});
assert.equal(result.isSuccess, true);
assert.deepEqual(audit.mutations, [
{
action: "payment_term.deleted",
target: { type: "payment_term", id: paymentTerm.id.toPrimitive(), label: "30/60" },
before: {
name: "30/60",
description: null,
isActive: true,
isSystem: false,
dueRules: [
{ dueDays: 30, percentage: "50.00" },
{ dueDays: 60, percentage: "50.00" },
],
},
after: {},
},
]);
});
test("delete blocks referenced payment terms", async () => {
const repository = new InMemoryPaymentTermRepository();
const audit = new AuditSpy();
const paymentTerm = createPaymentTerm();
await repository.create(paymentTerm);
@ -154,6 +360,7 @@ describe("Payment term services", () => {
usageChecker: {
findUsage: async () => Result.ok([{ source: "customers" }]),
},
audit,
});
const result = await deleter.delete({
@ -161,43 +368,34 @@ describe("Payment term services", () => {
});
assert.equal(result.isFailure, true);
assert.equal(audit.mutations.length, 0);
assert.equal(
(await repository.getByIdInCompany(paymentTerm.companyId, paymentTerm.id)).isSuccess,
true
);
});
test("delete blocks system payment terms", async () => {
const companyId = UniqueID.generateNewID();
const nameResult = PaymentTermName.create("Contado sistema");
assert.equal(nameResult.isSuccess, true);
const paymentTermResult = PaymentTerm.create(
{
companyId,
name: nameResult.data,
description: Maybe.none(),
isActive: true,
isSystem: true,
dueRules: [createRuleProps({ dueDays: 0, percentageValue: 10000 })],
},
UniqueID.generateNewID()
);
assert.equal(paymentTermResult.isSuccess, true);
const audit = new AuditSpy();
const deleter = new PaymentTermDeleter({
repository: new InMemoryPaymentTermRepository(),
usageChecker: {
findUsage: async () => Result.ok([]),
},
audit,
});
const result = await deleter.delete({
paymentTerm: paymentTermResult.data,
paymentTerm: createSystemPaymentTerm(),
});
assert.equal(result.isFailure, true);
assert.equal(audit.mutations.length, 0);
});
test("disable permite desactivar un payment term que no es default de company", async () => {
const repository = new InMemoryPaymentTermRepository();
const audit = new AuditSpy();
const paymentTerm = createPaymentTerm();
await repository.create(paymentTerm);
@ -206,6 +404,7 @@ describe("Payment term services", () => {
companyDefaultUsageChecker: {
isUsedAsCompanyDefault: async () => Result.ok(false),
},
audit,
});
const result = await changer.changeStatus({
@ -215,10 +414,14 @@ describe("Payment term services", () => {
assert.equal(result.isSuccess, true);
assert.equal(result.data.isActive, false);
assert.equal(audit.mutations[0]?.action, "payment_term.disabled");
assert.equal(audit.mutations[0]?.before.isActive, true);
assert.equal(audit.mutations[0]?.after.isActive, false);
});
test("disable rechaza desactivar un payment term usado como default de company", async () => {
const repository = new InMemoryPaymentTermRepository();
const audit = new AuditSpy();
const paymentTerm = createPaymentTerm();
await repository.create(paymentTerm);
@ -227,6 +430,7 @@ describe("Payment term services", () => {
companyDefaultUsageChecker: {
isUsedAsCompanyDefault: async () => Result.ok(true),
},
audit,
});
const result = await changer.changeStatus({
@ -236,5 +440,44 @@ describe("Payment term services", () => {
assert.equal(result.isFailure, true);
assert.match(result.error.message, /plazo por defecto/i);
assert.equal(audit.mutations.length, 0);
});
test("enable registra la transición activa", async () => {
const repository = new InMemoryPaymentTermRepository();
const audit = new AuditSpy();
const paymentTerm = createPaymentTerm();
const disableResult = paymentTerm.disable();
assert.equal(disableResult.isSuccess, true);
await repository.create(paymentTerm);
const result = await new PaymentTermStatusChanger({ repository, audit }).changeStatus({
paymentTerm,
action: "enable",
});
assert.equal(result.isSuccess, true);
assert.equal(audit.mutations[0]?.action, "payment_term.enabled");
assert.equal(audit.mutations[0]?.before.isActive, false);
assert.equal(audit.mutations[0]?.after.isActive, true);
});
test("disable rechaza plazos de sistema sin auditar", async () => {
const audit = new AuditSpy();
const paymentTerm = createSystemPaymentTerm();
const result = await new PaymentTermStatusChanger({
repository: new InMemoryPaymentTermRepository(),
audit,
companyDefaultUsageChecker: {
isUsedAsCompanyDefault: async () => Result.ok(false),
},
}).changeStatus({
paymentTerm,
action: "disable",
});
assert.equal(result.isFailure, true);
assert.equal(audit.mutations.length, 0);
});
});

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 IPaymentTermCreateProps, PaymentTerm } from "../../../domain";
import {
PAYMENT_TERM_AUDIT_ACTIONS,
toPaymentTermAuditState,
toPaymentTermAuditTarget,
} from "../audit/payment-term-audit";
import type { IPaymentTermRepository } from "../repositories/payment-term-repository.interface";
export interface IPaymentTermCreatorParams {
@ -16,7 +22,10 @@ export interface IPaymentTermCreator {
}
export class PaymentTermCreator implements IPaymentTermCreator {
constructor(private readonly repository: IPaymentTermRepository) {}
constructor(
private readonly repository: IPaymentTermRepository,
private readonly audit: IAudit
) {}
public async create(params: IPaymentTermCreatorParams): Promise<Result<PaymentTerm, Error>> {
const { companyId, id, props, transaction } = params;
@ -32,6 +41,16 @@ export class PaymentTermCreator implements IPaymentTermCreator {
return Result.fail(saveResult.error);
}
const auditResult = await this.audit.recordMutation({
action: PAYMENT_TERM_AUDIT_ACTIONS.CREATED,
target: toPaymentTermAuditTarget(paymentTerm),
before: {},
after: toPaymentTermAuditState(paymentTerm),
});
if (auditResult.isFailure) {
return Result.fail(auditResult.error);
}
return Result.ok(paymentTerm);
}
}

View File

@ -1,7 +1,13 @@
import type { IAudit } from "@repo/rdx-audit";
import { Result } from "@repo/rdx-utils";
import type { PaymentTerm } from "../../../domain";
import { PaymentTermCannotBeDeletedError } from "../../../domain";
import {
PAYMENT_TERM_AUDIT_ACTIONS,
toPaymentTermAuditState,
toPaymentTermAuditTarget,
} from "../audit/payment-term-audit";
import type { IPaymentTermRepository } from "../repositories/payment-term-repository.interface";
import type { IPaymentTermUsageChecker } from "./payment-term-usage-checker";
@ -18,6 +24,7 @@ export class PaymentTermDeleter implements IPaymentTermDeleter {
private readonly deps: {
repository: IPaymentTermRepository;
usageChecker: IPaymentTermUsageChecker;
audit: IAudit;
}
) {}
@ -26,6 +33,8 @@ export class PaymentTermDeleter implements IPaymentTermDeleter {
transaction?: unknown;
}): Promise<Result<PaymentTerm, Error>> {
const { paymentTerm, transaction } = params;
const before = toPaymentTermAuditState(paymentTerm);
const target = toPaymentTermAuditTarget(paymentTerm);
const invariantResult = paymentTerm.ensureCanBeDeleted();
if (invariantResult.isFailure) {
@ -61,6 +70,16 @@ export class PaymentTermDeleter implements IPaymentTermDeleter {
return Result.fail(deleteResult.error);
}
const auditResult = await this.deps.audit.recordMutation({
action: PAYMENT_TERM_AUDIT_ACTIONS.DELETED,
target,
before,
after: {},
});
if (auditResult.isFailure) {
return Result.fail(auditResult.error);
}
return Result.ok(paymentTerm);
}
}

View File

@ -1,7 +1,13 @@
import type { IAudit } from "@repo/rdx-audit";
import { Result } from "@repo/rdx-utils";
import type { PaymentTerm } from "../../../domain";
import { PaymentTermCannotBeDisabledError } from "../../../domain";
import {
PAYMENT_TERM_AUDIT_ACTIONS,
toPaymentTermAuditState,
toPaymentTermAuditTarget,
} from "../audit/payment-term-audit";
import type { IPaymentTermRepository } from "../repositories";
export type PaymentTermStatusChangeAction = "enable" | "disable";
@ -25,6 +31,7 @@ export class PaymentTermStatusChanger implements IPaymentTermStatusChanger {
transaction?: unknown;
}): Promise<Result<boolean, Error>>;
};
audit: IAudit;
}
) {}
@ -34,6 +41,7 @@ export class PaymentTermStatusChanger implements IPaymentTermStatusChanger {
transaction?: unknown;
}): Promise<Result<PaymentTerm, Error>> {
const { paymentTerm, action, transaction } = params;
const before = toPaymentTermAuditState(paymentTerm);
if (action === "disable" && this.deps.companyDefaultUsageChecker) {
const companyDefaultUsageResult =
@ -70,6 +78,19 @@ export class PaymentTermStatusChanger implements IPaymentTermStatusChanger {
return Result.fail(persistenceResult.error);
}
const auditResult = await this.deps.audit.recordMutation({
action:
action === "enable"
? PAYMENT_TERM_AUDIT_ACTIONS.ENABLED
: PAYMENT_TERM_AUDIT_ACTIONS.DISABLED,
target: toPaymentTermAuditTarget(paymentTerm),
before,
after: toPaymentTermAuditState(paymentTerm),
});
if (auditResult.isFailure) {
return Result.fail(auditResult.error);
}
return Result.ok(paymentTerm);
}
}

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 { PaymentTerm, PaymentTermPatchProps } from "../../../domain";
import {
PAYMENT_TERM_AUDIT_ACTIONS,
toPaymentTermAuditState,
toPaymentTermAuditTarget,
} from "../audit/payment-term-audit";
import type { IPaymentTermRepository } from "../repositories/payment-term-repository.interface";
export interface IPaymentTermUpdater {
@ -14,7 +20,10 @@ export interface IPaymentTermUpdater {
}
export class PaymentTermUpdater implements IPaymentTermUpdater {
constructor(private readonly repository: IPaymentTermRepository) {}
constructor(
private readonly repository: IPaymentTermRepository,
private readonly audit: IAudit
) {}
public async update(params: {
companyId: UniqueID;
@ -30,8 +39,8 @@ export class PaymentTermUpdater implements IPaymentTermUpdater {
}
const paymentTerm = existingResult.data;
const before = toPaymentTermAuditState(paymentTerm);
// Aplicar cambios en el agregado
const updateResult = paymentTerm.update(patchProps);
if (updateResult.isFailure) {
return Result.fail(updateResult.error);
@ -41,12 +50,21 @@ export class PaymentTermUpdater implements IPaymentTermUpdater {
return Result.ok(paymentTerm);
}
// Persistir cambios
const saveResult = await this.repository.update(paymentTerm, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
const auditResult = await this.audit.recordMutation({
action: PAYMENT_TERM_AUDIT_ACTIONS.UPDATED,
target: toPaymentTermAuditTarget(paymentTerm),
before,
after: toPaymentTermAuditState(paymentTerm),
});
if (auditResult.isFailure) {
return Result.fail(auditResult.error);
}
return Result.ok(paymentTerm);
}
}

View File

@ -43,7 +43,7 @@ export type PaymentTermsInternalDeps = {
};
export const buildPaymentTermsDependencies = (params: ModuleParams): PaymentTermsInternalDeps => {
const { database, transactionContext } = params;
const { audit, database, transactionContext } = params;
const transactionManager = buildTransactionManager(database as Sequelize, transactionContext);
const persistenceMappers = buildPaymentTermPersistenceMappers();
@ -57,11 +57,12 @@ export const buildPaymentTermsDependencies = (params: ModuleParams): PaymentTerm
const inputMappers = buildPaymentTermInputMappers();
const finder = buildPaymentTermFinder({ repository });
const creator = buildPaymentTermCreator({ repository });
const updater = buildPaymentTermUpdater({ repository });
const deleter = buildPaymentTermDeleter({ repository, usageChecker });
const creator = buildPaymentTermCreator({ repository, audit });
const updater = buildPaymentTermUpdater({ repository, audit });
const deleter = buildPaymentTermDeleter({ repository, usageChecker, audit });
const statusChanger = buildPaymentTermStatusChanger({
repository,
audit,
companyDefaultUsageChecker,
});