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