import assert from "node:assert/strict"; import { randomUUID } from "node:crypto"; import test from "node:test"; import { SequelizeTransactionManager } from "@erp/core/api"; import { Name, UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { AuditChangeModel, AuditEventModel } from "@/infrastructure/audit/sequelize"; import { SupplierUpdater } from "../../../../../modules/suppliers/src/api/application/services/supplier-updater.service"; import { SequelizeSupplierDomainMapper } from "../../../../../modules/suppliers/src/api/infrastructure/persistence/sequelize/mappers/domain/sequelize-supplier.mapper"; import { SequelizeSupplierSummaryMapper } from "../../../../../modules/suppliers/src/api/infrastructure/persistence/sequelize/mappers/summary/sequelize-supplier-summary.mapper"; import { SupplierModel } from "../../../../../modules/suppliers/src/api/infrastructure/persistence/sequelize/models/sequelize-supplier.model"; import { SupplierRepository } from "../../../../../modules/suppliers/src/api/infrastructure/persistence/sequelize/repositories/sequelize-supplier.repository"; import { buildCompanyHeaders, createTestAccount, createTestCompany, createTestMembership, loginByApi, } from "./http-test-fixtures"; import { createHttpTestRuntime } from "./http-test-runtime"; test("suppliers registra create/update, omite no-ops y revierte Supplier + Audit en MariaDB", async (t) => { const runtime = await createHttpTestRuntime(); t.after(async () => runtime.stop()); const account = await createTestAccount(runtime.database, { email: `supplier-audit-${randomUUID()}@example.test`, password: "Password123!", }); const company = await createTestCompany(runtime.database, { legalName: "Supplier audit company", }); const otherCompany = await createTestCompany(runtime.database, { legalName: "Other supplier audit company", }); await createTestMembership(runtime.database, { accountId: account.id, companyId: company.id, }); await createTestMembership(runtime.database, { accountId: account.id, companyId: otherCompany.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 supplierId = randomUUID(); const createResponse = await runtime.request("/api/v1/suppliers", { method: "POST", headers: { ...buildCompanyHeaders({ accessToken, companyId: company.id }), "content-type": "application/json", "x-correlation-id": "supplier-create-correlation", }, body: JSON.stringify({ id: supplierId, reference: "SUP-001", is_company: true, name: "Supplier A", trade_name: "Supplier Trading", tin: "B12345678", street: "Main street 1", city: "Madrid", province: "Madrid", postal_code: "28001", country: "ES", email_primary: "supplier@example.test", language_code: "es", currency_code: "EUR", }), }); assert.equal(createResponse.status, 201); const createdEvent = await AuditEventModel.findOne({ where: { action: "supplier.created", target_id: supplierId }, }); assert.ok(createdEvent); assert.equal(createdEvent.source_type, "application"); assert.equal(createdEvent.source_id, "factuges-erp"); assert.equal(createdEvent.origin, "http"); assert.equal(createdEvent.operation_id, createResponse.headers.get("x-request-id")); assert.equal(createdEvent.correlation_id, "supplier-create-correlation"); assert.equal(createdEvent.actor_type, "user"); assert.equal(createdEvent.actor_id, account.id); assert.equal(createdEvent.scope_type, "company"); assert.equal(createdEvent.scope_id, company.id); assert.equal(createdEvent.target_type, "supplier"); assert.equal(createdEvent.target_label, "Supplier A"); const updateResponse = await runtime.request(`/api/v1/suppliers/${supplierId}`, { method: "PATCH", headers: { ...buildCompanyHeaders({ accessToken, companyId: company.id }), "content-type": "application/json", "x-correlation-id": "supplier-update-correlation", }, body: JSON.stringify({ email_primary: "billing@supplier.example.test", postal_code: "08001", }), }); assert.equal(updateResponse.status, 200); const updatedEvent = await AuditEventModel.findOne({ where: { action: "supplier.updated", target_id: supplierId }, }); assert.ok(updatedEvent); assert.equal(updatedEvent.operation_id, updateResponse.headers.get("x-request-id")); assert.equal(updatedEvent.correlation_id, "supplier-update-correlation"); const updatedChanges = await AuditChangeModel.findAll({ where: { audit_event_id: updatedEvent.id }, order: [["position", "ASC"]], }); assert.deepEqual( updatedChanges.map((change) => ({ path: change.path, kind: change.kind, before: change.before_value, after: change.after_value, })), [ { path: "address.postalCode", kind: "change", before: "28001", after: "08001", }, { path: "emailPrimary", kind: "change", before: "supplier@example.test", after: "billing@supplier.example.test", }, ] ); const [identicalAffected] = await SupplierModel.update( { name: "Supplier A" }, { where: { id: supplierId, company_id: company.id }, silent: true, } ); assert.equal(identicalAffected, 0); const originalUpdate = SupplierModel.update; let noOpUpdateCalls = 0; (SupplierModel as unknown as { update: typeof SupplierModel.update }).update = async ( ...params ) => { noOpUpdateCalls += 1; return originalUpdate.call(SupplierModel, ...params); }; try { const eventsBeforeNoop = await AuditEventModel.count({ where: { action: "supplier.updated", target_id: supplierId }, }); const changesBeforeNoop = await AuditChangeModel.count(); const emptyPatchResponse = await runtime.request(`/api/v1/suppliers/${supplierId}`, { method: "PATCH", headers: { ...buildCompanyHeaders({ accessToken, companyId: company.id }), "content-type": "application/json", }, body: JSON.stringify({}), }); assert.equal(emptyPatchResponse.status, 200); const identicalPatchResponse = await runtime.request(`/api/v1/suppliers/${supplierId}`, { method: "PATCH", headers: { ...buildCompanyHeaders({ accessToken, companyId: company.id }), "content-type": "application/json", }, body: JSON.stringify({ name: "Supplier A", language_code: "es", currency_code: "EUR", }), }); assert.equal(identicalPatchResponse.status, 200); assert.equal(noOpUpdateCalls, 0); assert.equal( await AuditEventModel.count({ where: { action: "supplier.updated", target_id: supplierId } }), eventsBeforeNoop ); assert.equal(await AuditChangeModel.count(), changesBeforeNoop); } finally { (SupplierModel as unknown as { update: typeof SupplierModel.update }).update = originalUpdate; } const eventsBeforeRejectedRequests = await AuditEventModel.count(); const crossCompanyResponse = await runtime.request(`/api/v1/suppliers/${supplierId}`, { method: "PATCH", headers: { ...buildCompanyHeaders({ accessToken, companyId: otherCompany.id }), "content-type": "application/json", }, body: JSON.stringify({ name: "Forbidden supplier" }), }); assert.equal(crossCompanyResponse.status, 404); const notFoundResponse = await runtime.request(`/api/v1/suppliers/${randomUUID()}`, { method: "PATCH", headers: { ...buildCompanyHeaders({ accessToken, companyId: company.id }), "content-type": "application/json", }, body: JSON.stringify({ name: "Missing supplier" }), }); assert.equal(notFoundResponse.status, 404); const invalidPatchResponse = await runtime.request(`/api/v1/suppliers/${supplierId}`, { method: "PATCH", headers: { ...buildCompanyHeaders({ accessToken, companyId: company.id }), "content-type": "application/json", }, body: JSON.stringify({ name: "" }), }); assert.notEqual(invalidPatchResponse.status, 200); assert.equal(await AuditEventModel.count(), eventsBeforeRejectedRequests); const duplicateCreateResponse = await runtime.request("/api/v1/suppliers", { method: "POST", headers: { ...buildCompanyHeaders({ accessToken, companyId: company.id }), "content-type": "application/json", }, body: JSON.stringify({ id: supplierId, is_company: true, name: "Duplicate supplier" }), }); assert.notEqual(duplicateCreateResponse.status, 201); assert.equal(await AuditEventModel.count(), eventsBeforeRejectedRequests); const supplierIdResult = UniqueID.create(supplierId); const companyIdResult = UniqueID.create(company.id); const rollbackName = Name.create("Supplier rollback"); assert.equal(supplierIdResult.isSuccess, true); assert.equal(companyIdResult.isSuccess, true); assert.equal(rollbackName.isSuccess, true); const repository = new SupplierRepository( new SequelizeSupplierDomainMapper(), new SequelizeSupplierSummaryMapper(), runtime.database ); const updater = new SupplierUpdater({ repository, audit: runtime.audit }); const transactionManager = new SequelizeTransactionManager( runtime.database, runtime.transactionContext ); const changesBeforeRollback = await AuditChangeModel.count(); await assert.rejects( () => runtime.operationContext.run( { operationId: "supplier-rollback-operation", correlationId: "supplier-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 supplierResult = await repository.getByIdInCompany( companyIdResult.data, supplierIdResult.data, transaction ); assert.equal(supplierResult.isSuccess, true); const updateResult = await updater.update({ supplier: supplierResult.data, props: { name: rollbackName.data }, transaction, }); assert.equal(updateResult.isSuccess, true); return Result.fail(new Error("forced supplier rollback")); }) ), /forced supplier rollback/ ); const persistedSupplier = await SupplierModel.findByPk(supplierId); assert.ok(persistedSupplier); assert.equal(persistedSupplier.name, "Supplier A"); assert.equal( await AuditEventModel.count({ where: { operation_id: "supplier-rollback-operation" } }), 0 ); assert.equal(await AuditChangeModel.count(), changesBeforeRollback); });