feat(audit): implement customer audit functionality and integrate with services

This commit is contained in:
David Arranz 2026-08-25 09:42:16 +02:00
parent 71b79cc4b1
commit 0bcccd5e80
12 changed files with 598 additions and 24 deletions

View File

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

View File

@ -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"
}
}
}

View File

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

View File

@ -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 = <T extends { toPrimitive(): string }>(value: {
match: <R>(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(),
});

View File

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

View File

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

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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));

View File

@ -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