diff --git a/apps/server/src/testing/http/customer-audit.integration.test.ts b/apps/server/src/testing/http/customer-audit.integration.test.ts new file mode 100644 index 00000000..d6521151 --- /dev/null +++ b/apps/server/src/testing/http/customer-audit.integration.test.ts @@ -0,0 +1,314 @@ +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, "uecko-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); +}); diff --git a/modules/customers/package.json b/modules/customers/package.json index a1940677..e0e395ae 100644 --- a/modules/customers/package.json +++ b/modules/customers/package.json @@ -41,6 +41,7 @@ "@hookform/resolvers": "^5.2.2", "@repo/i18next": "workspace:*", "@repo/rdx-criteria": "workspace:*", + "@repo/rdx-audit": "workspace:*", "@repo/rdx-ddd": "workspace:*", "@repo/rdx-logger": "workspace:*", "@repo/rdx-ui": "workspace:*", @@ -57,4 +58,4 @@ "use-debounce": "^10.1.1", "zod": "^4.3.6" } -} \ No newline at end of file +} diff --git a/modules/customers/src/api/application/audit/customer-audit.test.ts b/modules/customers/src/api/application/audit/customer-audit.test.ts new file mode 100644 index 00000000..1cc69d31 --- /dev/null +++ b/modules/customers/src/api/application/audit/customer-audit.test.ts @@ -0,0 +1,110 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + City, + Country, + CurrencyCode, + EmailAddress, + LanguageCode, + Name, + PhoneNumber, + PostalCode, + Province, + Street, + TINNumber, + URLAddress, + UniqueID, +} from "@repo/rdx-ddd"; +import { Maybe } from "@repo/rdx-utils"; + +import { Customer, CustomerStatus } from "../../domain"; + +import { toCustomerAuditState, toCustomerAuditTarget } from "./customer-audit"; + +const unwrap = (result: { data: T; isSuccess: boolean }): T => { + assert.equal(result.isSuccess, true); + return result.data; +}; + +const buildCustomer = () => + unwrap( + Customer.create( + { + companyId: UniqueID.generateNewID(), + status: CustomerStatus.createActive(), + reference: Maybe.some(unwrap(Name.create("C-001"))), + isCompany: true, + name: unwrap(Name.create("Acme")), + tradeName: Maybe.some(unwrap(Name.create("Acme 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("primary@example.test"))), + emailSecondary: Maybe.none(), + phonePrimary: Maybe.some(unwrap(PhoneNumber.create("+34910000000"))), + phoneSecondary: Maybe.none(), + mobilePrimary: Maybe.some(unwrap(PhoneNumber.create("+34600000000"))), + mobileSecondary: Maybe.none(), + fax: Maybe.some(unwrap(PhoneNumber.create("+34910000001"))), + website: Maybe.some(unwrap(URLAddress.create("https://example.test"))), + legalRecord: Maybe.none(), + paymentMethodId: Maybe.some(UniqueID.generateNewID()), + paymentTermId: Maybe.some(UniqueID.generateNewID()), + taxRegimeCode: Maybe.some("RE"), + usesEquivalenceSurcharge: true, + usesRetention: true, + languageCode: unwrap(LanguageCode.create("es")), + currencyCode: unwrap(CurrencyCode.create("EUR")), + }, + UniqueID.generateNewID() + ) + ); + +test("proyecta Customer a un estado histórico portable completo sin legalRecord", () => { + const customer = buildCustomer(); + + assert.deepEqual(toCustomerAuditState(customer), { + status: "active", + reference: "C-001", + isCompany: true, + name: "Acme", + tradeName: "Acme Trading", + tin: "B12345678", + address: { + street: "Main street 1", + street2: null, + city: "Madrid", + province: "Madrid", + postalCode: "28001", + country: "ES", + }, + emailPrimary: "primary@example.test", + emailSecondary: null, + phonePrimary: "+34910000000", + phoneSecondary: null, + mobilePrimary: "+34600000000", + mobileSecondary: null, + fax: "+34910000001", + website: "https://example.test", + paymentMethodId: customer.paymentMethodId.unwrap().toPrimitive(), + paymentTermId: customer.paymentTermId.unwrap().toPrimitive(), + taxRegimeCode: "RE", + usesEquivalenceSurcharge: true, + usesRetention: true, + languageCode: "es", + currencyCode: "EUR", + }); + assert.equal("legalRecord" in toCustomerAuditState(customer), false); + assert.deepEqual(toCustomerAuditTarget(customer), { + type: "customer", + id: customer.id.toPrimitive(), + label: "Acme", + }); +}); diff --git a/modules/customers/src/api/application/audit/customer-audit.ts b/modules/customers/src/api/application/audit/customer-audit.ts new file mode 100644 index 00000000..1e81a134 --- /dev/null +++ b/modules/customers/src/api/application/audit/customer-audit.ts @@ -0,0 +1,94 @@ +import type { AuditMutationTarget, AuditState } from "@repo/rdx-audit"; + +import type { Customer } from "../../domain"; + +export const CUSTOMER_AUDIT_ACTIONS = { + CREATED: "customer.created", + UPDATED: "customer.updated", +} as const; + +type CustomerAuditState = Readonly<{ + status: "active" | "inactive"; + reference: string | null; + isCompany: boolean; + name: string; + tradeName: string | null; + tin: string | null; + address: Readonly<{ + street: string | null; + street2: string | null; + city: string | null; + province: string | null; + postalCode: string | null; + country: string | null; + }>; + emailPrimary: string | null; + emailSecondary: string | null; + phonePrimary: string | null; + phoneSecondary: string | null; + mobilePrimary: string | null; + mobileSecondary: string | null; + fax: string | null; + website: string | null; + paymentMethodId: string | null; + paymentTermId: string | null; + taxRegimeCode: string | null; + usesEquivalenceSurcharge: boolean; + usesRetention: boolean; + languageCode: string; + currencyCode: string; +}>; + +const toNullablePrimitive = (value: { + match: (some: (item: T) => R, none: () => R) => R; +}): string | null => + value.match( + (item) => item.toPrimitive(), + () => null + ); + +/** Proyección histórica explícita; `legalRecord` se excluye deliberadamente en v1. */ +export const toCustomerAuditState = (customer: Customer): CustomerAuditState => { + const status: CustomerAuditState["status"] = customer.isActive ? "active" : "inactive"; + + return { + status, + reference: toNullablePrimitive(customer.reference), + isCompany: customer.isCompany, + name: customer.name.toPrimitive(), + tradeName: toNullablePrimitive(customer.tradeName), + tin: toNullablePrimitive(customer.tin), + address: { + street: toNullablePrimitive(customer.address.getProps().street), + street2: toNullablePrimitive(customer.address.getProps().street2), + city: toNullablePrimitive(customer.address.getProps().city), + province: toNullablePrimitive(customer.address.getProps().province), + postalCode: toNullablePrimitive(customer.address.getProps().postalCode), + country: toNullablePrimitive(customer.address.getProps().country), + }, + emailPrimary: toNullablePrimitive(customer.emailPrimary), + emailSecondary: toNullablePrimitive(customer.emailSecondary), + phonePrimary: toNullablePrimitive(customer.phonePrimary), + phoneSecondary: toNullablePrimitive(customer.phoneSecondary), + mobilePrimary: toNullablePrimitive(customer.mobilePrimary), + mobileSecondary: toNullablePrimitive(customer.mobileSecondary), + fax: toNullablePrimitive(customer.fax), + website: toNullablePrimitive(customer.website), + paymentMethodId: toNullablePrimitive(customer.paymentMethodId), + paymentTermId: toNullablePrimitive(customer.paymentTermId), + taxRegimeCode: customer.taxRegimeCode.match( + (code) => code, + () => null + ), + usesEquivalenceSurcharge: customer.usesEquivalenceSurcharge, + usesRetention: customer.usesRetention, + languageCode: customer.languageCode.toPrimitive(), + currencyCode: customer.currencyCode.toPrimitive(), + } satisfies AuditState; +}; + +export const toCustomerAuditTarget = (customer: Customer): AuditMutationTarget => ({ + type: "customer", + id: customer.id.toPrimitive(), + label: customer.name.toPrimitive(), +}); diff --git a/modules/customers/src/api/application/di/customer-creator.di.ts b/modules/customers/src/api/application/di/customer-creator.di.ts index 0fca6ae1..580ed5c5 100644 --- a/modules/customers/src/api/application/di/customer-creator.di.ts +++ b/modules/customers/src/api/application/di/customer-creator.di.ts @@ -1,12 +1,16 @@ +import type { IAudit } from "@repo/rdx-audit"; + import type { ICustomerRepository } from "../repositories"; import { CustomerCreator, type ICustomerCreator } from "../services"; export const buildCustomerCreator = (params: { repository: ICustomerRepository; + audit: IAudit; }): ICustomerCreator => { - const { repository } = params; + const { repository, audit } = params; return new CustomerCreator({ repository, + audit, }); }; diff --git a/modules/customers/src/api/application/di/customer-updater.di.ts b/modules/customers/src/api/application/di/customer-updater.di.ts index d40ff454..b45f7264 100644 --- a/modules/customers/src/api/application/di/customer-updater.di.ts +++ b/modules/customers/src/api/application/di/customer-updater.di.ts @@ -1,12 +1,16 @@ +import type { IAudit } from "@repo/rdx-audit"; + import type { ICustomerRepository } from "../repositories"; import { CustomerUpdater, type ICustomerUpdater } from "../services"; export const buildCustomerUpdater = (params: { repository: ICustomerRepository; + audit: IAudit; }): ICustomerUpdater => { - const { repository } = params; + const { repository, audit } = params; return new CustomerUpdater({ repository, + audit, }); }; diff --git a/modules/customers/src/api/application/services/customer-creator.ts b/modules/customers/src/api/application/services/customer-creator.ts index f1ced91b..cf932e24 100644 --- a/modules/customers/src/api/application/services/customer-creator.ts +++ b/modules/customers/src/api/application/services/customer-creator.ts @@ -1,8 +1,14 @@ import { DuplicateEntityError } from "@erp/core/api"; +import type { IAudit } from "@repo/rdx-audit"; import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { Customer, type ICustomerCreateProps } from "../../domain"; +import { + CUSTOMER_AUDIT_ACTIONS, + toCustomerAuditState, + toCustomerAuditTarget, +} from "../audit/customer-audit"; import type { ICustomerRepository } from "../repositories"; import { CustomerNotExistsInCompanySpecification } from "../specs"; @@ -17,13 +23,16 @@ export interface ICustomerCreator { type CustomerCreatorDeps = { repository: ICustomerRepository; + audit: IAudit; }; export class CustomerCreator implements ICustomerCreator { private readonly repository: ICustomerRepository; + private readonly audit: IAudit; constructor(deps: CustomerCreatorDeps) { this.repository = deps.repository; + this.audit = deps.audit; } async create(params: { @@ -69,6 +78,16 @@ export class CustomerCreator implements ICustomerCreator { return Result.fail(saveResult.error); } + const auditResult = await this.audit.recordMutation({ + action: CUSTOMER_AUDIT_ACTIONS.CREATED, + target: toCustomerAuditTarget(newCustomer), + before: {}, + after: toCustomerAuditState(newCustomer), + }); + if (auditResult.isFailure) { + return Result.fail(auditResult.error); + } + return Result.ok(newCustomer); } } diff --git a/modules/customers/src/api/application/services/customer-updater.ts b/modules/customers/src/api/application/services/customer-updater.ts index 42f34a31..bef75aa5 100644 --- a/modules/customers/src/api/application/services/customer-updater.ts +++ b/modules/customers/src/api/application/services/customer-updater.ts @@ -1,7 +1,13 @@ +import type { IAudit } from "@repo/rdx-audit"; import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import type { Customer, CustomerPatchProps } from "../../domain"; +import { + CUSTOMER_AUDIT_ACTIONS, + toCustomerAuditState, + toCustomerAuditTarget, +} from "../audit/customer-audit"; import type { ICustomerRepository } from "../repositories"; export interface ICustomerUpdater { @@ -15,13 +21,16 @@ export interface ICustomerUpdater { type CustomerUpdaterDeps = { repository: ICustomerRepository; + audit: IAudit; }; export class CustomerUpdater implements ICustomerUpdater { private readonly repository: ICustomerRepository; + private readonly audit: IAudit; constructor(deps: CustomerUpdaterDeps) { this.repository = deps.repository; + this.audit = deps.audit; } async update(params: { @@ -40,6 +49,7 @@ export class CustomerUpdater implements ICustomerUpdater { } const customer = existingResult.data; + const before = toCustomerAuditState(customer); // Aplicar cambios en el agregado const updateResult = customer.update(props); @@ -48,6 +58,20 @@ export class CustomerUpdater implements ICustomerUpdater { return Result.fail(updateResult.error); } + if (Object.keys(props).length === 0) { + const auditResult = await this.audit.recordMutation({ + action: CUSTOMER_AUDIT_ACTIONS.UPDATED, + target: toCustomerAuditTarget(customer), + before, + after: toCustomerAuditState(customer), + }); + if (auditResult.isFailure) { + return Result.fail(auditResult.error); + } + + return Result.ok(customer); + } + // Persistir cambios const saveResult = await this.repository.update(customer, transaction); @@ -55,6 +79,16 @@ export class CustomerUpdater implements ICustomerUpdater { return Result.fail(saveResult.error); } + const auditResult = await this.audit.recordMutation({ + action: CUSTOMER_AUDIT_ACTIONS.UPDATED, + target: toCustomerAuditTarget(customer), + before, + after: toCustomerAuditState(customer), + }); + if (auditResult.isFailure) { + return Result.fail(auditResult.error); + } + return Result.ok(customer); } } diff --git a/modules/customers/src/api/infrastructure/di/customer-public-services.ts b/modules/customers/src/api/infrastructure/di/customer-public-services.ts index 9e3e10aa..26966f74 100644 --- a/modules/customers/src/api/infrastructure/di/customer-public-services.ts +++ b/modules/customers/src/api/infrastructure/di/customer-public-services.ts @@ -1,4 +1,4 @@ -import type { SetupParams } from "@erp/core/api"; +import type { ModuleParams } from "@erp/core/api"; import type { TINNumber, UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; @@ -16,18 +16,18 @@ import { buildCustomerRepository } from "./customer-repositories.di"; import type { CustomersInternalDeps } from "./customers.di"; export function buildCustomerPublicServices( - params: SetupParams, - deps: CustomersInternalDeps + params: ModuleParams, + _deps: CustomersInternalDeps ): ICustomerPublicServices { const { database } = params; - const catalogs = resolveCustomerCatalogsDeps(params); + const _catalogs = resolveCustomerCatalogsDeps(params); // Infrastructure const persistenceMappers = buildCustomerPersistenceMappers(); const repository = buildCustomerRepository({ database, mappers: persistenceMappers }); const finder = buildCustomerFinder({ repository }); - const creator = buildCustomerCreator({ repository }); + const creator = buildCustomerCreator({ repository, audit: params.audit }); return { findCustomerByTIN: async (tin: TINNumber, context: ICustomerServicesContext) => { @@ -53,7 +53,7 @@ export function buildCustomerPublicServices( return Result.ok(customerResult.data); }, - + createCustomer: async ( id: UniqueID, props: ICustomerCreateProps, diff --git a/modules/customers/src/api/infrastructure/di/customers.di.ts b/modules/customers/src/api/infrastructure/di/customers.di.ts index a70c2ef7..37506222 100644 --- a/modules/customers/src/api/infrastructure/di/customers.di.ts +++ b/modules/customers/src/api/infrastructure/di/customers.di.ts @@ -69,8 +69,8 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter }); const finder = buildCustomerFinder({ repository }); - const creator = buildCustomerCreator({ repository }); - const updater = buildCustomerUpdater({ repository }); + const creator = buildCustomerCreator({ repository, audit: params.audit }); + const updater = buildCustomerUpdater({ repository, audit: params.audit }); const defaultPaymentTermResolver = new CustomerDefaultPaymentTermResolver({ companyFinder: companies.company.finder, paymentResolver: catalogResolvers.paymentResolver, diff --git a/modules/customers/src/api/infrastructure/persistence/sequelize/repositories/customer.repository.ts b/modules/customers/src/api/infrastructure/persistence/sequelize/repositories/customer.repository.ts index 7f346b5a..d8461e6a 100644 --- a/modules/customers/src/api/infrastructure/persistence/sequelize/repositories/customer.repository.ts +++ b/modules/customers/src/api/infrastructure/persistence/sequelize/repositories/customer.repository.ts @@ -1,9 +1,4 @@ -import { - EntityNotFoundError, - InfrastructureRepositoryError, - SequelizeRepository, - translateSequelizeError, -} from "@erp/core/api"; +import { EntityNotFoundError, SequelizeRepository, translateSequelizeError } from "@erp/core/api"; import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; import type { TINNumber, UniqueID } from "@repo/rdx-ddd"; import { type Collection, Result } from "@repo/rdx-utils"; @@ -68,18 +63,14 @@ export class CustomerRepository const { id, ...updatePayload } = dtoResult.data; - const [affected] = await CustomerModel.update(updatePayload, { + await CustomerModel.update(updatePayload, { where: { id /*, version */ }, transaction, //individualHooks: true, }); - if (affected === 0) { - return Result.fail( - new InfrastructureRepositoryError("Concurrency conflict or not found update customer") - ); - } - + // CustomerUpdater ya comprobó existencia y scope antes de persistir. + // MariaDB devuelve 0 cuando el estado recibido es idéntico al almacenado. return Result.ok(); } catch (err: unknown) { return Result.fail(translateSequelizeError(err)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 028105ff..401ca420 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -831,6 +831,9 @@ importers: '@repo/i18next': specifier: workspace:* version: link:../../packages/i18n + '@repo/rdx-audit': + specifier: workspace:* + version: link:../../packages/rdx-audit '@repo/rdx-criteria': specifier: workspace:* version: link:../../packages/rdx-criteria