403 lines
12 KiB
TypeScript
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);
|
|
});
|