feat(audit): integrate audit functionality for payment terms management
This commit is contained in:
parent
f1c0df685b
commit
71b79cc4b1
@ -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);
|
||||
});
|
||||
@ -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(),
|
||||
});
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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 });
|
||||
};
|
||||
|
||||
@ -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 });
|
||||
};
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user