2026-08-25 07:42:16 +00:00
|
|
|
import assert from "node:assert/strict";
|
|
|
|
|
import { randomUUID } from "node:crypto";
|
|
|
|
|
import test from "node:test";
|
|
|
|
|
|
|
|
|
|
import { type IModuleServer, SequelizeTransactionManager } from "@erp/core/api";
|
|
|
|
|
import type { ICustomerPublicServices } from "@erp/customers/api";
|
|
|
|
|
import { Country, CurrencyCode, LanguageCode, Name, UniqueID } from "@repo/rdx-ddd";
|
|
|
|
|
import { Maybe, Result } from "@repo/rdx-utils";
|
|
|
|
|
import type { Request, Response } from "express";
|
|
|
|
|
|
|
|
|
|
import { AuditChangeModel, AuditEventModel } from "@/infrastructure/audit/sequelize";
|
|
|
|
|
|
|
|
|
|
import { CustomerStatus } from "../../../../../modules/customers/src/api/domain";
|
|
|
|
|
import { CustomerModel } from "../../../../../modules/customers/src/api/infrastructure/persistence/sequelize/models/sequelize-customer.model";
|
|
|
|
|
import { requireIdentityTenantMembership } from "../../../../../modules/identity/src/api/infrastructure/express/middlewares/require-identity-middlewares";
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
buildCompanyHeaders,
|
|
|
|
|
createTestAccount,
|
|
|
|
|
createTestCompany,
|
|
|
|
|
createTestMembership,
|
|
|
|
|
loginByApi,
|
|
|
|
|
} from "./http-test-fixtures";
|
|
|
|
|
import { createHttpTestRuntime } from "./http-test-runtime";
|
|
|
|
|
|
|
|
|
|
const publicCustomerCreateProbe: IModuleServer = {
|
|
|
|
|
name: "customer-public-create-probe",
|
|
|
|
|
version: "1.0.0",
|
|
|
|
|
dependencies: ["customers", "identity"],
|
|
|
|
|
start: (params) => {
|
|
|
|
|
const customerServices = params.getService<ICustomerPublicServices>("customers:general");
|
|
|
|
|
|
|
|
|
|
params.app.post(
|
|
|
|
|
"/api/v1/__test/customers/public-create",
|
|
|
|
|
...requireIdentityTenantMembership(params),
|
|
|
|
|
async (request: Request, response: Response) => {
|
|
|
|
|
const companyIdResult = UniqueID.create(request.header("x-company-id") ?? "");
|
|
|
|
|
if (companyIdResult.isFailure) {
|
|
|
|
|
response.status(400).json({ success: false });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const transactionManager = new SequelizeTransactionManager(
|
|
|
|
|
params.database,
|
|
|
|
|
params.transactionContext
|
|
|
|
|
);
|
|
|
|
|
const customerId = UniqueID.generateNewID();
|
|
|
|
|
try {
|
|
|
|
|
const result = await transactionManager.complete(async (transaction) => {
|
|
|
|
|
const customerResult = await customerServices.createCustomer(
|
|
|
|
|
customerId,
|
|
|
|
|
{
|
|
|
|
|
companyId: companyIdResult.data,
|
|
|
|
|
status: CustomerStatus.createActive(),
|
|
|
|
|
reference: Maybe.none(),
|
|
|
|
|
isCompany: true,
|
|
|
|
|
name: Name.create("FactuGES customer").data,
|
|
|
|
|
tradeName: Maybe.none(),
|
|
|
|
|
tin: Maybe.none(),
|
|
|
|
|
address: {
|
|
|
|
|
street: Maybe.none(),
|
|
|
|
|
street2: Maybe.none(),
|
|
|
|
|
city: Maybe.none(),
|
|
|
|
|
province: Maybe.none(),
|
|
|
|
|
postalCode: Maybe.none(),
|
|
|
|
|
country: Maybe.some(Country.create("ES").data),
|
|
|
|
|
},
|
|
|
|
|
emailPrimary: Maybe.none(),
|
|
|
|
|
emailSecondary: Maybe.none(),
|
|
|
|
|
phonePrimary: Maybe.none(),
|
|
|
|
|
phoneSecondary: Maybe.none(),
|
|
|
|
|
mobilePrimary: Maybe.none(),
|
|
|
|
|
mobileSecondary: Maybe.none(),
|
|
|
|
|
fax: Maybe.none(),
|
|
|
|
|
website: Maybe.none(),
|
|
|
|
|
legalRecord: Maybe.none(),
|
|
|
|
|
paymentMethodId: Maybe.none(),
|
|
|
|
|
paymentTermId: Maybe.none(),
|
|
|
|
|
taxRegimeCode: Maybe.none(),
|
|
|
|
|
usesEquivalenceSurcharge: false,
|
|
|
|
|
usesRetention: false,
|
|
|
|
|
languageCode: LanguageCode.create("es").data,
|
|
|
|
|
currencyCode: CurrencyCode.create("EUR").data,
|
|
|
|
|
},
|
|
|
|
|
{ companyId: companyIdResult.data, transaction }
|
|
|
|
|
);
|
|
|
|
|
if (customerResult.isFailure) {
|
|
|
|
|
return customerResult;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (request.query.rollback === "true") {
|
|
|
|
|
return Result.fail(new Error("forced public customer rollback"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Result.ok({ customerId: customerId.toPrimitive() });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
response
|
|
|
|
|
.status(result.isSuccess ? 201 : 409)
|
|
|
|
|
.json(result.isSuccess ? result.data : { success: false });
|
|
|
|
|
} catch {
|
|
|
|
|
response.status(409).json({ success: false });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
test("customers registra create, update y el create público en MariaDB", async (t) => {
|
|
|
|
|
const runtime = await createHttpTestRuntime({ testModules: [publicCustomerCreateProbe] });
|
|
|
|
|
t.after(async () => runtime.stop());
|
|
|
|
|
|
|
|
|
|
const account = await createTestAccount(runtime.database, {
|
|
|
|
|
email: `customer-audit-${randomUUID()}@example.test`,
|
|
|
|
|
password: "Password123!",
|
|
|
|
|
});
|
|
|
|
|
const company = await createTestCompany(runtime.database, {
|
|
|
|
|
legalName: "Customer audit company",
|
|
|
|
|
});
|
|
|
|
|
await createTestMembership(runtime.database, {
|
|
|
|
|
accountId: account.id,
|
|
|
|
|
companyId: company.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 customerId = randomUUID();
|
|
|
|
|
|
|
|
|
|
const createResponse = await runtime.request("/api/v1/customers", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
...buildCompanyHeaders({ accessToken, companyId: company.id }),
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
"x-correlation-id": "customer-create-correlation",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
id: customerId,
|
|
|
|
|
is_company: true,
|
|
|
|
|
name: "Acme",
|
|
|
|
|
tin: "B12345678",
|
|
|
|
|
address: { country: "ES", postal_code: "28001" },
|
|
|
|
|
contact: { email_primary: "contact@acme.test" },
|
|
|
|
|
uses_retention: false,
|
|
|
|
|
language_code: "es",
|
|
|
|
|
currency_code: "EUR",
|
|
|
|
|
payment_term_id: null,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
assert.equal(createResponse.status, 201);
|
|
|
|
|
|
|
|
|
|
const createdEvent = await AuditEventModel.findOne({
|
|
|
|
|
where: { action: "customer.created", target_id: customerId },
|
|
|
|
|
});
|
|
|
|
|
assert.ok(createdEvent);
|
|
|
|
|
assert.equal(createdEvent.source_type, "application");
|
2026-08-26 12:38:44 +00:00
|
|
|
assert.equal(createdEvent.source_id, "factuges-erp");
|
2026-08-25 07:42:16 +00:00
|
|
|
assert.equal(createdEvent.origin, "http");
|
|
|
|
|
assert.equal(createdEvent.operation_id, createResponse.headers.get("x-request-id"));
|
|
|
|
|
assert.equal(createdEvent.correlation_id, "customer-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, "customer");
|
|
|
|
|
assert.equal(createdEvent.target_label, "Acme");
|
|
|
|
|
|
|
|
|
|
const updateResponse = await runtime.request(`/api/v1/customers/${customerId}`, {
|
|
|
|
|
method: "PUT",
|
|
|
|
|
headers: {
|
|
|
|
|
...buildCompanyHeaders({ accessToken, companyId: company.id }),
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
"x-correlation-id": "customer-update-correlation",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
name: "Acme updated",
|
|
|
|
|
uses_retention: true,
|
|
|
|
|
address: { postal_code: "08001" },
|
|
|
|
|
contact: { email_primary: "billing@acme.test" },
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
assert.equal(updateResponse.status, 200);
|
|
|
|
|
|
|
|
|
|
const updatedEvent = await AuditEventModel.findOne({
|
|
|
|
|
where: { action: "customer.updated", target_id: customerId },
|
|
|
|
|
});
|
|
|
|
|
assert.ok(updatedEvent);
|
|
|
|
|
assert.equal(updatedEvent.target_label, "Acme updated");
|
|
|
|
|
assert.equal(updatedEvent.operation_id, updateResponse.headers.get("x-request-id"));
|
|
|
|
|
assert.equal(updatedEvent.correlation_id, "customer-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: "contact@acme.test",
|
|
|
|
|
after: "billing@acme.test",
|
|
|
|
|
},
|
|
|
|
|
{ path: "name", kind: "change", before: "Acme", after: "Acme updated" },
|
|
|
|
|
{ path: "usesRetention", kind: "change", before: false, after: true },
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const auditEventsBeforeNoop = await AuditEventModel.count({
|
|
|
|
|
where: { action: "customer.updated", target_id: customerId },
|
|
|
|
|
});
|
|
|
|
|
const noopResponse = await runtime.request(`/api/v1/customers/${customerId}`, {
|
|
|
|
|
method: "PUT",
|
|
|
|
|
headers: {
|
|
|
|
|
...buildCompanyHeaders({ accessToken, companyId: company.id }),
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({}),
|
|
|
|
|
});
|
|
|
|
|
assert.equal(noopResponse.status, 200);
|
|
|
|
|
assert.equal(
|
|
|
|
|
await AuditEventModel.count({ where: { action: "customer.updated", target_id: customerId } }),
|
|
|
|
|
auditEventsBeforeNoop
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const auditEventsBeforeIdenticalUpdate = await AuditEventModel.count({
|
|
|
|
|
where: { action: "customer.updated", target_id: customerId },
|
|
|
|
|
});
|
|
|
|
|
const identicalUpdateResponse = await runtime.request(`/api/v1/customers/${customerId}`, {
|
|
|
|
|
method: "PUT",
|
|
|
|
|
headers: {
|
|
|
|
|
...buildCompanyHeaders({ accessToken, companyId: company.id }),
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
name: "Acme updated",
|
|
|
|
|
uses_retention: true,
|
|
|
|
|
language_code: "es",
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
assert.equal(identicalUpdateResponse.status, 200);
|
|
|
|
|
const unchangedCustomer = await CustomerModel.findByPk(customerId);
|
|
|
|
|
assert.ok(unchangedCustomer);
|
|
|
|
|
assert.equal(unchangedCustomer.name, "Acme updated");
|
|
|
|
|
assert.equal(unchangedCustomer.uses_retention, true);
|
|
|
|
|
assert.equal(unchangedCustomer.language_code, "es");
|
|
|
|
|
assert.equal(
|
|
|
|
|
await AuditEventModel.count({ where: { action: "customer.updated", target_id: customerId } }),
|
|
|
|
|
auditEventsBeforeIdenticalUpdate
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const auditEventsBeforeNotFound = await AuditEventModel.count();
|
|
|
|
|
const notFoundResponse = await runtime.request(`/api/v1/customers/${randomUUID()}`, {
|
|
|
|
|
method: "PUT",
|
|
|
|
|
headers: {
|
|
|
|
|
...buildCompanyHeaders({ accessToken, companyId: company.id }),
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ name: "Missing" }),
|
|
|
|
|
});
|
|
|
|
|
assert.equal(notFoundResponse.status, 404);
|
|
|
|
|
assert.equal(await AuditEventModel.count(), auditEventsBeforeNotFound);
|
|
|
|
|
|
|
|
|
|
const publicCreateResponse = await runtime.request("/api/v1/__test/customers/public-create", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
...buildCompanyHeaders({ accessToken, companyId: company.id }),
|
|
|
|
|
"x-correlation-id": "customer-public-create-correlation",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
assert.equal(publicCreateResponse.status, 201);
|
|
|
|
|
const publicCustomerId = (publicCreateResponse.body as { customerId: string }).customerId;
|
|
|
|
|
assert.ok(await CustomerModel.findByPk(publicCustomerId));
|
|
|
|
|
const publicEvent = await AuditEventModel.findOne({
|
|
|
|
|
where: { action: "customer.created", target_id: publicCustomerId },
|
|
|
|
|
});
|
|
|
|
|
assert.ok(publicEvent);
|
|
|
|
|
assert.equal(publicEvent.correlation_id, "customer-public-create-correlation");
|
|
|
|
|
assert.equal(publicEvent.scope_id, company.id);
|
|
|
|
|
assert.equal(publicEvent.actor_id, account.id);
|
|
|
|
|
|
|
|
|
|
const auditChangesBeforeRollback = await AuditChangeModel.count();
|
|
|
|
|
const rollbackResponse = await runtime.request(
|
|
|
|
|
"/api/v1/__test/customers/public-create?rollback=true",
|
|
|
|
|
{
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
...buildCompanyHeaders({ accessToken, companyId: company.id }),
|
|
|
|
|
"x-correlation-id": "customer-public-rollback-correlation",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
assert.equal(rollbackResponse.status, 409);
|
|
|
|
|
assert.equal(
|
|
|
|
|
await CustomerModel.count({ where: { company_id: company.id, name: "FactuGES customer" } }),
|
|
|
|
|
1
|
|
|
|
|
);
|
|
|
|
|
assert.equal(
|
|
|
|
|
await AuditEventModel.count({
|
|
|
|
|
where: { correlation_id: "customer-public-rollback-correlation" },
|
|
|
|
|
}),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
assert.equal(await AuditChangeModel.count(), auditChangesBeforeRollback);
|
|
|
|
|
});
|