301 lines
11 KiB
TypeScript
301 lines
11 KiB
TypeScript
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);
|
|
});
|