FactuGES_ERP/modules/suppliers/src/api/application/services/supplier-audit.services.test.ts

403 lines
12 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import type { IAudit, RecordAuditMutationShortcutInput } from "@repo/rdx-audit";
import {
City,
Country,
CurrencyCode,
EmailAddress,
LanguageCode,
Name,
PostalCode,
Province,
Street,
TINNumber,
URLAddress,
UniqueID,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import { type ISupplierCreateProps, Supplier, SupplierStatus } from "../../domain";
import type { ISupplierRepository } from "../repositories";
import { SupplierCreator } from "./supplier-creator.service";
import { SupplierStatusChanger } from "./supplier-status-changer.service";
import { SupplierUpdater } from "./supplier-updater.service";
const unwrap = <T>(result: { data: T; isSuccess: boolean }): T => {
assert.equal(result.isSuccess, true);
return result.data;
};
const buildSupplier = (status = SupplierStatus.active()) =>
unwrap(
Supplier.create(
{
companyId: UniqueID.generateNewID(),
status,
reference: Maybe.some(unwrap(Name.create("SUP-001"))),
isCompany: true,
name: unwrap(Name.create("Supplier A")),
tradeName: Maybe.some(unwrap(Name.create("Supplier Trading"))),
tin: Maybe.some(unwrap(TINNumber.create("B12345678"))),
address: {
street: Maybe.some(unwrap(Street.create("Main street 1"))),
street2: Maybe.none(),
city: Maybe.some(unwrap(City.create("Madrid"))),
province: Maybe.some(unwrap(Province.create("Madrid"))),
postalCode: Maybe.some(unwrap(PostalCode.create("28001"))),
country: Maybe.some(unwrap(Country.create("ES"))),
},
emailPrimary: Maybe.some(unwrap(EmailAddress.create("supplier@example.test"))),
emailSecondary: Maybe.none(),
phonePrimary: Maybe.none(),
phoneSecondary: Maybe.none(),
mobilePrimary: Maybe.none(),
mobileSecondary: Maybe.none(),
fax: Maybe.none(),
website: Maybe.some(unwrap(URLAddress.create("https://supplier.example.test"))),
languageCode: unwrap(LanguageCode.create("es")),
currencyCode: unwrap(CurrencyCode.create("EUR")),
},
UniqueID.generateNewID()
)
);
const toCreateProps = (supplier: Supplier): ISupplierCreateProps => ({
companyId: supplier.companyId,
status: SupplierStatus.active(),
reference: supplier.reference,
isCompany: supplier.isCompany,
name: supplier.name,
tradeName: supplier.tradeName,
tin: supplier.tin,
address: supplier.address.getProps(),
emailPrimary: supplier.emailPrimary,
emailSecondary: supplier.emailSecondary,
phonePrimary: supplier.phonePrimary,
phoneSecondary: supplier.phoneSecondary,
mobilePrimary: supplier.mobilePrimary,
mobileSecondary: supplier.mobileSecondary,
fax: supplier.fax,
website: supplier.website,
languageCode: supplier.languageCode,
currencyCode: supplier.currencyCode,
});
const buildRepository = (counters: { creates: number; updates: number }): ISupplierRepository => ({
create: async () => {
counters.creates += 1;
return Result.ok();
},
update: async () => {
counters.updates += 1;
return Result.ok();
},
existsByIdInCompany: async () => Result.ok(false),
getByIdInCompany: async () => Result.fail(new Error("unused")),
getByTINInCompany: async () => Result.fail(new Error("unused")),
findByCriteriaInCompany: async () => Result.fail(new Error("unused")),
});
const buildAudit = (failure?: Error) => {
const mutations: RecordAuditMutationShortcutInput[] = [];
const audit = {
record: async () => Result.ok(),
recordEvent: async () => Result.ok(),
recordMutation: async (input: RecordAuditMutationShortcutInput) => {
mutations.push(input);
return failure ? Result.fail(failure as never) : Result.ok();
},
} satisfies IAudit;
return { audit, mutations };
};
test("SupplierCreator registra supplier.created con el estado histórico completo", async () => {
const supplier = buildSupplier();
const counters = { creates: 0, updates: 0 };
const audit = buildAudit();
const creator = new SupplierCreator({
repository: buildRepository(counters),
audit: audit.audit,
});
const result = await creator.create({
companyId: supplier.companyId,
id: supplier.id,
props: toCreateProps(supplier),
});
assert.equal(result.isSuccess, true);
assert.equal(counters.creates, 1);
assert.deepEqual(audit.mutations[0], {
action: "supplier.created",
target: { type: "supplier", id: supplier.id.toPrimitive(), label: "Supplier A" },
before: {},
after: {
status: "active",
reference: "SUP-001",
isCompany: true,
name: "Supplier A",
tradeName: "Supplier Trading",
tin: "B12345678",
address: {
street: "Main street 1",
street2: null,
city: "Madrid",
province: "Madrid",
postalCode: "28001",
country: "ES",
},
emailPrimary: "supplier@example.test",
emailSecondary: null,
phonePrimary: null,
phoneSecondary: null,
mobilePrimary: null,
mobileSecondary: null,
fax: null,
website: "https://supplier.example.test",
languageCode: "es",
currencyCode: "EUR",
},
});
});
test("SupplierUpdater persiste y audita sólo cambios semánticos", async () => {
const supplier = buildSupplier();
const counters = { creates: 0, updates: 0 };
const audit = buildAudit();
const updater = new SupplierUpdater({
repository: buildRepository(counters),
audit: audit.audit,
});
const name = unwrap(Name.create("Supplier B"));
const email = unwrap(EmailAddress.create("billing@supplier.example.test"));
const postalCode = unwrap(PostalCode.create("08001"));
const languageCode = unwrap(LanguageCode.create("en"));
const currencyCode = unwrap(CurrencyCode.create("USD"));
const updateResult = await updater.update({
supplier,
props: {
name,
emailPrimary: Maybe.some(email),
address: { postalCode: Maybe.some(postalCode) },
languageCode,
currencyCode,
},
transaction: undefined,
});
assert.equal(updateResult.isSuccess, true);
assert.equal(counters.updates, 1);
assert.equal(audit.mutations.length, 1);
assert.equal(audit.mutations[0]?.action, "supplier.updated");
assert.equal(audit.mutations[0]?.target.label, "Supplier B");
assert.deepEqual(audit.mutations[0]?.before, {
status: "active",
reference: "SUP-001",
isCompany: true,
name: "Supplier A",
tradeName: "Supplier Trading",
tin: "B12345678",
address: {
street: "Main street 1",
street2: null,
city: "Madrid",
province: "Madrid",
postalCode: "28001",
country: "ES",
},
emailPrimary: "supplier@example.test",
emailSecondary: null,
phonePrimary: null,
phoneSecondary: null,
mobilePrimary: null,
mobileSecondary: null,
fax: null,
website: "https://supplier.example.test",
languageCode: "es",
currencyCode: "EUR",
});
const mutation = audit.mutations[0];
assert.ok(mutation);
assert.deepEqual(mutation.after, {
status: "active",
reference: "SUP-001",
isCompany: true,
name: "Supplier B",
tradeName: "Supplier Trading",
tin: "B12345678",
address: {
street: "Main street 1",
street2: null,
city: "Madrid",
province: "Madrid",
postalCode: "08001",
country: "ES",
},
emailPrimary: "billing@supplier.example.test",
emailSecondary: null,
phonePrimary: null,
phoneSecondary: null,
mobilePrimary: null,
mobileSecondary: null,
fax: null,
website: "https://supplier.example.test",
languageCode: "en",
currencyCode: "USD",
});
const emptyPatchResult = await updater.update({ supplier, props: {}, transaction: undefined });
assert.equal(emptyPatchResult.isSuccess, true);
assert.equal(counters.updates, 1);
assert.equal(audit.mutations.length, 1);
const identicalPatchResult = await updater.update({
supplier,
props: { name, languageCode, currencyCode },
transaction: undefined,
});
assert.equal(identicalPatchResult.isSuccess, true);
assert.equal(counters.updates, 1);
assert.equal(audit.mutations.length, 1);
});
test("SupplierUpdater propaga el fallo obligatorio de Audit después de persistir", async () => {
const supplier = buildSupplier();
const counters = { creates: 0, updates: 0 };
const audit = buildAudit(new Error("audit unavailable"));
const updater = new SupplierUpdater({
repository: buildRepository(counters),
audit: audit.audit,
});
const result = await updater.update({
supplier,
props: { name: unwrap(Name.create("Supplier unavailable")) },
transaction: undefined,
});
assert.equal(result.isFailure, true);
assert.equal(result.error.message, "audit unavailable");
assert.equal(counters.updates, 1);
assert.equal(audit.mutations.length, 1);
});
test("SupplierStatusChanger persiste y audita activate/deactivate, pero omite no-ops", async () => {
const counters = { creates: 0, updates: 0 };
const audit = buildAudit();
const changer = new SupplierStatusChanger(buildRepository(counters), audit.audit);
const inactiveSupplier = buildSupplier(SupplierStatus.inactive());
const activateResult = await changer.changeStatus({
supplier: inactiveSupplier,
action: "activate",
transaction: undefined,
});
assert.equal(activateResult.isSuccess, true);
assert.equal(inactiveSupplier.isActive, true);
assert.equal(counters.updates, 1);
assert.deepEqual(audit.mutations[0], {
action: "supplier.activated",
target: {
type: "supplier",
id: inactiveSupplier.id.toPrimitive(),
label: "Supplier A",
},
before: {
status: "inactive",
reference: "SUP-001",
isCompany: true,
name: "Supplier A",
tradeName: "Supplier Trading",
tin: "B12345678",
address: {
street: "Main street 1",
street2: null,
city: "Madrid",
province: "Madrid",
postalCode: "28001",
country: "ES",
},
emailPrimary: "supplier@example.test",
emailSecondary: null,
phonePrimary: null,
phoneSecondary: null,
mobilePrimary: null,
mobileSecondary: null,
fax: null,
website: "https://supplier.example.test",
languageCode: "es",
currencyCode: "EUR",
},
after: {
status: "active",
reference: "SUP-001",
isCompany: true,
name: "Supplier A",
tradeName: "Supplier Trading",
tin: "B12345678",
address: {
street: "Main street 1",
street2: null,
city: "Madrid",
province: "Madrid",
postalCode: "28001",
country: "ES",
},
emailPrimary: "supplier@example.test",
emailSecondary: null,
phonePrimary: null,
phoneSecondary: null,
mobilePrimary: null,
mobileSecondary: null,
fax: null,
website: "https://supplier.example.test",
languageCode: "es",
currencyCode: "EUR",
},
});
const repeatedActivateResult = await changer.changeStatus({
supplier: inactiveSupplier,
action: "activate",
transaction: undefined,
});
assert.equal(repeatedActivateResult.isSuccess, true);
assert.equal(counters.updates, 1);
assert.equal(audit.mutations.length, 1);
const deactivateResult = await changer.changeStatus({
supplier: inactiveSupplier,
action: "deactivate",
transaction: undefined,
});
assert.equal(deactivateResult.isSuccess, true);
assert.equal(inactiveSupplier.isActive, false);
assert.equal(counters.updates, 2);
assert.equal(audit.mutations[1]?.action, "supplier.deactivated");
assert.equal((audit.mutations[1]?.before as { status: string }).status, "active");
assert.equal((audit.mutations[1]?.after as { status: string }).status, "inactive");
});
test("SupplierStatusChanger propaga el fallo obligatorio de Audit", async () => {
const counters = { creates: 0, updates: 0 };
const changer = new SupplierStatusChanger(
buildRepository(counters),
buildAudit(new Error("audit unavailable")).audit
);
const result = await changer.changeStatus({
supplier: buildSupplier(),
action: "deactivate",
transaction: undefined,
});
assert.equal(result.isFailure, true);
assert.equal(result.error.message, "audit unavailable");
assert.equal(counters.updates, 1);
});