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 = (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); });