feat(audit): implement supplier status activation and deactivation functionality with audit logging
This commit is contained in:
parent
2fba3b1108
commit
6b9d131c14
@ -0,0 +1,249 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { SequelizeTransactionManager } from "@erp/core/api";
|
||||||
|
import type { IAudit } from "@repo/rdx-audit";
|
||||||
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import { AuditChangeModel, AuditEventModel } from "@/infrastructure/audit/sequelize";
|
||||||
|
|
||||||
|
import { SupplierStatusChanger } from "../../../../../modules/suppliers/src/api/application/services/supplier-status-changer.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 activa y desactiva con Audit transaccional en MariaDB", async (t) => {
|
||||||
|
const runtime = await createHttpTestRuntime();
|
||||||
|
t.after(async () => runtime.stop());
|
||||||
|
|
||||||
|
const account = await createTestAccount(runtime.database, {
|
||||||
|
email: `supplier-status-${randomUUID()}@example.test`,
|
||||||
|
password: "Password123!",
|
||||||
|
});
|
||||||
|
const company = await createTestCompany(runtime.database, {
|
||||||
|
legalName: "Supplier status company",
|
||||||
|
});
|
||||||
|
const otherCompany = await createTestCompany(runtime.database, {
|
||||||
|
legalName: "Other supplier status 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 headers = buildCompanyHeaders({ accessToken, companyId: company.id });
|
||||||
|
|
||||||
|
const createResponse = await runtime.request("/api/v1/suppliers", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...headers, "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ id: supplierId, is_company: true, name: "Supplier status" }),
|
||||||
|
});
|
||||||
|
assert.equal(createResponse.status, 201);
|
||||||
|
|
||||||
|
const deactivateResponse = await runtime.request(`/api/v1/suppliers/${supplierId}/deactivate`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...headers, "x-correlation-id": "supplier-deactivate-correlation" },
|
||||||
|
});
|
||||||
|
assert.equal(deactivateResponse.status, 200);
|
||||||
|
assert.equal((deactivateResponse.body as { status: string }).status, "inactive");
|
||||||
|
assert.equal((await SupplierModel.findByPk(supplierId))?.status, "inactive");
|
||||||
|
|
||||||
|
const deactivatedEvent = await AuditEventModel.findOne({
|
||||||
|
where: { action: "supplier.deactivated", target_id: supplierId },
|
||||||
|
});
|
||||||
|
assert.ok(deactivatedEvent);
|
||||||
|
assert.equal(deactivatedEvent.source_type, "application");
|
||||||
|
assert.equal(deactivatedEvent.source_id, "uecko-erp");
|
||||||
|
assert.equal(deactivatedEvent.origin, "http");
|
||||||
|
assert.equal(deactivatedEvent.operation_id, deactivateResponse.headers.get("x-request-id"));
|
||||||
|
assert.equal(deactivatedEvent.correlation_id, "supplier-deactivate-correlation");
|
||||||
|
assert.equal(deactivatedEvent.actor_type, "user");
|
||||||
|
assert.equal(deactivatedEvent.actor_id, account.id);
|
||||||
|
assert.equal(deactivatedEvent.scope_type, "company");
|
||||||
|
assert.equal(deactivatedEvent.scope_id, company.id);
|
||||||
|
assert.equal(deactivatedEvent.target_type, "supplier");
|
||||||
|
assert.equal(deactivatedEvent.target_id, supplierId);
|
||||||
|
assert.deepEqual(
|
||||||
|
(await AuditChangeModel.findAll({ where: { audit_event_id: deactivatedEvent.id } })).map(
|
||||||
|
(change) => ({
|
||||||
|
path: change.path,
|
||||||
|
kind: change.kind,
|
||||||
|
before: change.before_value,
|
||||||
|
after: change.after_value,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
[{ path: "status", kind: "change", before: "active", after: "inactive" }]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getInactiveResponse = await runtime.request(`/api/v1/suppliers/${supplierId}`, {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
assert.equal(getInactiveResponse.status, 200);
|
||||||
|
assert.equal((getInactiveResponse.body as { status: string }).status, "inactive");
|
||||||
|
|
||||||
|
const eventsBeforeRepeatedDeactivate = await AuditEventModel.count({
|
||||||
|
where: { target_id: supplierId },
|
||||||
|
});
|
||||||
|
const repeatedDeactivateResponse = await runtime.request(
|
||||||
|
`/api/v1/suppliers/${supplierId}/deactivate`,
|
||||||
|
{ method: "POST", headers }
|
||||||
|
);
|
||||||
|
assert.equal(repeatedDeactivateResponse.status, 200);
|
||||||
|
assert.equal(
|
||||||
|
await AuditEventModel.count({ where: { target_id: supplierId } }),
|
||||||
|
eventsBeforeRepeatedDeactivate
|
||||||
|
);
|
||||||
|
|
||||||
|
const activateResponse = await runtime.request(`/api/v1/suppliers/${supplierId}/activate`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...headers, "x-correlation-id": "supplier-activate-correlation" },
|
||||||
|
});
|
||||||
|
assert.equal(activateResponse.status, 200);
|
||||||
|
assert.equal((activateResponse.body as { status: string }).status, "active");
|
||||||
|
assert.equal((await SupplierModel.findByPk(supplierId))?.status, "active");
|
||||||
|
|
||||||
|
const activatedEvent = await AuditEventModel.findOne({
|
||||||
|
where: { action: "supplier.activated", target_id: supplierId },
|
||||||
|
});
|
||||||
|
assert.ok(activatedEvent);
|
||||||
|
assert.equal(activatedEvent.correlation_id, "supplier-activate-correlation");
|
||||||
|
assert.deepEqual(
|
||||||
|
(await AuditChangeModel.findAll({ where: { audit_event_id: activatedEvent.id } })).map(
|
||||||
|
(change) => ({
|
||||||
|
path: change.path,
|
||||||
|
kind: change.kind,
|
||||||
|
before: change.before_value,
|
||||||
|
after: change.after_value,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
[{ path: "status", kind: "change", before: "inactive", after: "active" }]
|
||||||
|
);
|
||||||
|
|
||||||
|
const eventsBeforeRejectedRequests = await AuditEventModel.count();
|
||||||
|
const crossCompanyResponse = await runtime.request(`/api/v1/suppliers/${supplierId}/deactivate`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: buildCompanyHeaders({ accessToken, companyId: otherCompany.id }),
|
||||||
|
});
|
||||||
|
assert.equal(crossCompanyResponse.status, 404);
|
||||||
|
const missingResponse = await runtime.request(`/api/v1/suppliers/${randomUUID()}/activate`, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
assert.equal(missingResponse.status, 404);
|
||||||
|
const patchStatusResponse = await runtime.request(`/api/v1/suppliers/${supplierId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { ...headers, "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ status: "inactive" }),
|
||||||
|
});
|
||||||
|
assert.notEqual(patchStatusResponse.status, 200);
|
||||||
|
assert.equal(await AuditEventModel.count(), eventsBeforeRejectedRequests);
|
||||||
|
|
||||||
|
const repository = new SupplierRepository(
|
||||||
|
new SequelizeSupplierDomainMapper(),
|
||||||
|
new SequelizeSupplierSummaryMapper(),
|
||||||
|
runtime.database
|
||||||
|
);
|
||||||
|
const statusChanger = new SupplierStatusChanger(repository, runtime.audit);
|
||||||
|
const transactionManager = new SequelizeTransactionManager(
|
||||||
|
runtime.database,
|
||||||
|
runtime.transactionContext
|
||||||
|
);
|
||||||
|
const supplierIdResult = UniqueID.create(supplierId);
|
||||||
|
const companyIdResult = UniqueID.create(company.id);
|
||||||
|
assert.equal(supplierIdResult.isSuccess, true);
|
||||||
|
assert.equal(companyIdResult.isSuccess, true);
|
||||||
|
const changesBeforeRollback = await AuditChangeModel.count();
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() =>
|
||||||
|
runtime.operationContext.run(
|
||||||
|
{
|
||||||
|
operationId: "supplier-status-rollback-operation",
|
||||||
|
correlationId: "supplier-status-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 statusResult = await statusChanger.changeStatus({
|
||||||
|
supplier: supplierResult.data,
|
||||||
|
action: "deactivate",
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
assert.equal(statusResult.isSuccess, true);
|
||||||
|
return Result.fail(new Error("forced supplier status rollback"));
|
||||||
|
})
|
||||||
|
),
|
||||||
|
/forced supplier status rollback/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal((await SupplierModel.findByPk(supplierId))?.status, "active");
|
||||||
|
assert.equal(
|
||||||
|
await AuditEventModel.count({ where: { operation_id: "supplier-status-rollback-operation" } }),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert.equal(await AuditChangeModel.count(), changesBeforeRollback);
|
||||||
|
|
||||||
|
const auditFailureChanger = new SupplierStatusChanger(repository, {
|
||||||
|
record: async () => Result.ok(),
|
||||||
|
recordEvent: async () => Result.ok(),
|
||||||
|
recordMutation: async () => Result.fail(new Error("audit unavailable")),
|
||||||
|
} satisfies IAudit);
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() =>
|
||||||
|
runtime.operationContext.run(
|
||||||
|
{
|
||||||
|
operationId: "supplier-status-audit-failure-operation",
|
||||||
|
correlationId: "supplier-status-audit-failure-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);
|
||||||
|
return auditFailureChanger.changeStatus({
|
||||||
|
supplier: supplierResult.data,
|
||||||
|
action: "deactivate",
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
),
|
||||||
|
/audit unavailable/
|
||||||
|
);
|
||||||
|
assert.equal((await SupplierModel.findByPk(supplierId))?.status, "active");
|
||||||
|
});
|
||||||
@ -5,6 +5,8 @@ import type { Supplier } from "../../domain";
|
|||||||
export const SUPPLIER_AUDIT_ACTIONS = {
|
export const SUPPLIER_AUDIT_ACTIONS = {
|
||||||
CREATED: "supplier.created",
|
CREATED: "supplier.created",
|
||||||
UPDATED: "supplier.updated",
|
UPDATED: "supplier.updated",
|
||||||
|
ACTIVATED: "supplier.activated",
|
||||||
|
DEACTIVATED: "supplier.deactivated",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type SupplierAuditState = Readonly<{
|
type SupplierAuditState = Readonly<{
|
||||||
|
|||||||
@ -2,5 +2,6 @@ export * from "./supplier-creator.di";
|
|||||||
export * from "./supplier-finder.di";
|
export * from "./supplier-finder.di";
|
||||||
export * from "./supplier-input-mappers.di";
|
export * from "./supplier-input-mappers.di";
|
||||||
export * from "./supplier-snapshot-builders.di";
|
export * from "./supplier-snapshot-builders.di";
|
||||||
|
export * from "./supplier-status-changer.di";
|
||||||
export * from "./supplier-updater.di";
|
export * from "./supplier-updater.di";
|
||||||
export * from "./supplier-use-cases.di";
|
export * from "./supplier-use-cases.di";
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import type { IAudit } from "@repo/rdx-audit";
|
||||||
|
|
||||||
|
import type { ISupplierRepository } from "../repositories";
|
||||||
|
import { SupplierStatusChanger } from "../services";
|
||||||
|
|
||||||
|
export function buildSupplierStatusChanger(params: {
|
||||||
|
repository: ISupplierRepository;
|
||||||
|
audit: IAudit;
|
||||||
|
}) {
|
||||||
|
return new SupplierStatusChanger(params.repository, params.audit);
|
||||||
|
}
|
||||||
@ -1,18 +1,35 @@
|
|||||||
import type { ITransactionManager } from "@erp/core/api";
|
import type { ITransactionManager } from "@erp/core/api";
|
||||||
|
|
||||||
import type { ICreateSupplierInputMapper, IUpdateSupplierInputMapper } from "../mappers";
|
import type { ICreateSupplierInputMapper, IUpdateSupplierInputMapper } from "../mappers";
|
||||||
import type { ISupplierCreator, ISupplierFinder, ISupplierUpdater } from "../services";
|
import type {
|
||||||
|
ISupplierCreator,
|
||||||
|
ISupplierFinder,
|
||||||
|
ISupplierStatusChanger,
|
||||||
|
ISupplierUpdater,
|
||||||
|
SupplierStatusAction,
|
||||||
|
} from "../services";
|
||||||
import type {
|
import type {
|
||||||
ISupplierFullSnapshotBuilder,
|
ISupplierFullSnapshotBuilder,
|
||||||
ISupplierSummarySnapshotBuilder,
|
ISupplierSummarySnapshotBuilder,
|
||||||
} from "../snapshot-builders";
|
} from "../snapshot-builders";
|
||||||
import {
|
import {
|
||||||
|
ChangeSupplierStatusUseCase,
|
||||||
CreateSupplierUseCase,
|
CreateSupplierUseCase,
|
||||||
GetSupplierByIdUseCase,
|
GetSupplierByIdUseCase,
|
||||||
ListSuppliersUseCase,
|
ListSuppliersUseCase,
|
||||||
UpdateSupplierUseCase,
|
UpdateSupplierUseCase,
|
||||||
} from "../use-cases";
|
} from "../use-cases";
|
||||||
|
|
||||||
|
export function buildChangeSupplierStatusUseCase(deps: {
|
||||||
|
finder: ISupplierFinder;
|
||||||
|
statusChanger: ISupplierStatusChanger;
|
||||||
|
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
action: SupplierStatusAction;
|
||||||
|
}) {
|
||||||
|
return new ChangeSupplierStatusUseCase(deps);
|
||||||
|
}
|
||||||
|
|
||||||
export function buildGetSupplierByIdUseCase(deps: {
|
export function buildGetSupplierByIdUseCase(deps: {
|
||||||
finder: ISupplierFinder;
|
finder: ISupplierFinder;
|
||||||
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from "./supplier-creator.service";
|
export * from "./supplier-creator.service";
|
||||||
export * from "./supplier-finder.service";
|
export * from "./supplier-finder.service";
|
||||||
|
export * from "./supplier-status-changer.service";
|
||||||
export * from "./supplier-updater.service";
|
export * from "./supplier-updater.service";
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import { type ISupplierCreateProps, Supplier, SupplierStatus } from "../../domai
|
|||||||
import type { ISupplierRepository } from "../repositories";
|
import type { ISupplierRepository } from "../repositories";
|
||||||
|
|
||||||
import { SupplierCreator } from "./supplier-creator.service";
|
import { SupplierCreator } from "./supplier-creator.service";
|
||||||
|
import { SupplierStatusChanger } from "./supplier-status-changer.service";
|
||||||
import { SupplierUpdater } from "./supplier-updater.service";
|
import { SupplierUpdater } from "./supplier-updater.service";
|
||||||
|
|
||||||
const unwrap = <T>(result: { data: T; isSuccess: boolean }): T => {
|
const unwrap = <T>(result: { data: T; isSuccess: boolean }): T => {
|
||||||
@ -29,12 +30,12 @@ const unwrap = <T>(result: { data: T; isSuccess: boolean }): T => {
|
|||||||
return result.data;
|
return result.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildSupplier = () =>
|
const buildSupplier = (status = SupplierStatus.active()) =>
|
||||||
unwrap(
|
unwrap(
|
||||||
Supplier.create(
|
Supplier.create(
|
||||||
{
|
{
|
||||||
companyId: UniqueID.generateNewID(),
|
companyId: UniqueID.generateNewID(),
|
||||||
status: SupplierStatus.active(),
|
status,
|
||||||
reference: Maybe.some(unwrap(Name.create("SUP-001"))),
|
reference: Maybe.some(unwrap(Name.create("SUP-001"))),
|
||||||
isCompany: true,
|
isCompany: true,
|
||||||
name: unwrap(Name.create("Supplier A")),
|
name: unwrap(Name.create("Supplier A")),
|
||||||
@ -284,3 +285,118 @@ test("SupplierUpdater propaga el fallo obligatorio de Audit después de persisti
|
|||||||
assert.equal(counters.updates, 1);
|
assert.equal(counters.updates, 1);
|
||||||
assert.equal(audit.mutations.length, 1);
|
assert.equal(audit.mutations.length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("SupplierStatusChanger persiste y audita activate/deactivate, pero omite no-ops", async () => {
|
||||||
|
const counters = { creates: 0, updates: 0 };
|
||||||
|
const audit = buildAudit();
|
||||||
|
const changer = new SupplierStatusChanger(buildRepository(counters), audit.audit);
|
||||||
|
const inactiveSupplier = buildSupplier(SupplierStatus.inactive());
|
||||||
|
|
||||||
|
const activateResult = await changer.changeStatus({
|
||||||
|
supplier: inactiveSupplier,
|
||||||
|
action: "activate",
|
||||||
|
transaction: undefined,
|
||||||
|
});
|
||||||
|
assert.equal(activateResult.isSuccess, true);
|
||||||
|
assert.equal(inactiveSupplier.isActive, true);
|
||||||
|
assert.equal(counters.updates, 1);
|
||||||
|
assert.deepEqual(audit.mutations[0], {
|
||||||
|
action: "supplier.activated",
|
||||||
|
target: {
|
||||||
|
type: "supplier",
|
||||||
|
id: inactiveSupplier.id.toPrimitive(),
|
||||||
|
label: "Supplier A",
|
||||||
|
},
|
||||||
|
before: {
|
||||||
|
status: "inactive",
|
||||||
|
reference: "SUP-001",
|
||||||
|
isCompany: true,
|
||||||
|
name: "Supplier A",
|
||||||
|
tradeName: "Supplier Trading",
|
||||||
|
tin: "B12345678",
|
||||||
|
address: {
|
||||||
|
street: "Main street 1",
|
||||||
|
street2: null,
|
||||||
|
city: "Madrid",
|
||||||
|
province: "Madrid",
|
||||||
|
postalCode: "28001",
|
||||||
|
country: "ES",
|
||||||
|
},
|
||||||
|
emailPrimary: "supplier@example.test",
|
||||||
|
emailSecondary: null,
|
||||||
|
phonePrimary: null,
|
||||||
|
phoneSecondary: null,
|
||||||
|
mobilePrimary: null,
|
||||||
|
mobileSecondary: null,
|
||||||
|
fax: null,
|
||||||
|
website: "https://supplier.example.test",
|
||||||
|
languageCode: "es",
|
||||||
|
currencyCode: "EUR",
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
status: "active",
|
||||||
|
reference: "SUP-001",
|
||||||
|
isCompany: true,
|
||||||
|
name: "Supplier A",
|
||||||
|
tradeName: "Supplier Trading",
|
||||||
|
tin: "B12345678",
|
||||||
|
address: {
|
||||||
|
street: "Main street 1",
|
||||||
|
street2: null,
|
||||||
|
city: "Madrid",
|
||||||
|
province: "Madrid",
|
||||||
|
postalCode: "28001",
|
||||||
|
country: "ES",
|
||||||
|
},
|
||||||
|
emailPrimary: "supplier@example.test",
|
||||||
|
emailSecondary: null,
|
||||||
|
phonePrimary: null,
|
||||||
|
phoneSecondary: null,
|
||||||
|
mobilePrimary: null,
|
||||||
|
mobileSecondary: null,
|
||||||
|
fax: null,
|
||||||
|
website: "https://supplier.example.test",
|
||||||
|
languageCode: "es",
|
||||||
|
currencyCode: "EUR",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const repeatedActivateResult = await changer.changeStatus({
|
||||||
|
supplier: inactiveSupplier,
|
||||||
|
action: "activate",
|
||||||
|
transaction: undefined,
|
||||||
|
});
|
||||||
|
assert.equal(repeatedActivateResult.isSuccess, true);
|
||||||
|
assert.equal(counters.updates, 1);
|
||||||
|
assert.equal(audit.mutations.length, 1);
|
||||||
|
|
||||||
|
const deactivateResult = await changer.changeStatus({
|
||||||
|
supplier: inactiveSupplier,
|
||||||
|
action: "deactivate",
|
||||||
|
transaction: undefined,
|
||||||
|
});
|
||||||
|
assert.equal(deactivateResult.isSuccess, true);
|
||||||
|
assert.equal(inactiveSupplier.isActive, false);
|
||||||
|
assert.equal(counters.updates, 2);
|
||||||
|
assert.equal(audit.mutations[1]?.action, "supplier.deactivated");
|
||||||
|
assert.equal((audit.mutations[1]?.before as { status: string }).status, "active");
|
||||||
|
assert.equal((audit.mutations[1]?.after as { status: string }).status, "inactive");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("SupplierStatusChanger propaga el fallo obligatorio de Audit", async () => {
|
||||||
|
const counters = { creates: 0, updates: 0 };
|
||||||
|
const changer = new SupplierStatusChanger(
|
||||||
|
buildRepository(counters),
|
||||||
|
buildAudit(new Error("audit unavailable")).audit
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await changer.changeStatus({
|
||||||
|
supplier: buildSupplier(),
|
||||||
|
action: "deactivate",
|
||||||
|
transaction: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.isFailure, true);
|
||||||
|
assert.equal(result.error.message, "audit unavailable");
|
||||||
|
assert.equal(counters.updates, 1);
|
||||||
|
});
|
||||||
|
|||||||
@ -0,0 +1,61 @@
|
|||||||
|
import type { IAudit } from "@repo/rdx-audit";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { Supplier } from "../../domain";
|
||||||
|
import { SUPPLIER_AUDIT_ACTIONS, toSupplierAuditState, toSupplierAuditTarget } from "../audit";
|
||||||
|
import type { ISupplierRepository } from "../repositories";
|
||||||
|
|
||||||
|
export type SupplierStatusAction = "activate" | "deactivate";
|
||||||
|
|
||||||
|
export interface ISupplierStatusChanger {
|
||||||
|
changeStatus(params: {
|
||||||
|
supplier: Supplier;
|
||||||
|
action: SupplierStatusAction;
|
||||||
|
transaction: unknown;
|
||||||
|
}): Promise<Result<Supplier, Error>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SupplierStatusChanger implements ISupplierStatusChanger {
|
||||||
|
public constructor(
|
||||||
|
private readonly repository: ISupplierRepository,
|
||||||
|
private readonly audit: IAudit
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async changeStatus(params: {
|
||||||
|
supplier: Supplier;
|
||||||
|
action: SupplierStatusAction;
|
||||||
|
transaction: unknown;
|
||||||
|
}): Promise<Result<Supplier, Error>> {
|
||||||
|
const before = toSupplierAuditState(params.supplier);
|
||||||
|
const statusResult =
|
||||||
|
params.action === "activate" ? params.supplier.activate() : params.supplier.deactivate();
|
||||||
|
|
||||||
|
if (statusResult.isFailure) {
|
||||||
|
return Result.fail(statusResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statusResult.data) {
|
||||||
|
return Result.ok(params.supplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
const persistResult = await this.repository.update(params.supplier, params.transaction);
|
||||||
|
if (persistResult.isFailure) {
|
||||||
|
return Result.fail(persistResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auditResult = await this.audit.recordMutation({
|
||||||
|
action:
|
||||||
|
params.action === "activate"
|
||||||
|
? SUPPLIER_AUDIT_ACTIONS.ACTIVATED
|
||||||
|
: SUPPLIER_AUDIT_ACTIONS.DEACTIVATED,
|
||||||
|
target: toSupplierAuditTarget(params.supplier),
|
||||||
|
before,
|
||||||
|
after: toSupplierAuditState(params.supplier),
|
||||||
|
});
|
||||||
|
if (auditResult.isFailure) {
|
||||||
|
return Result.fail(auditResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(params.supplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import type { ITransactionManager } from "@erp/core/api";
|
||||||
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { ISupplierFinder, ISupplierStatusChanger, SupplierStatusAction } from "../services";
|
||||||
|
import type { ISupplierFullSnapshotBuilder } from "../snapshot-builders";
|
||||||
|
|
||||||
|
type ChangeSupplierStatusUseCaseDeps = {
|
||||||
|
finder: ISupplierFinder;
|
||||||
|
statusChanger: ISupplierStatusChanger;
|
||||||
|
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
action: SupplierStatusAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ChangeSupplierStatusUseCase {
|
||||||
|
public constructor(private readonly deps: ChangeSupplierStatusUseCaseDeps) {}
|
||||||
|
|
||||||
|
public execute(params: { companyId: UniqueID; supplierId: string }) {
|
||||||
|
const supplierIdResult = UniqueID.create(params.supplierId);
|
||||||
|
if (supplierIdResult.isFailure) {
|
||||||
|
return Result.fail(supplierIdResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.deps.transactionManager.complete(async (transaction: unknown) => {
|
||||||
|
try {
|
||||||
|
const supplierResult = await this.deps.finder.findSupplierById(
|
||||||
|
params.companyId,
|
||||||
|
supplierIdResult.data,
|
||||||
|
transaction
|
||||||
|
);
|
||||||
|
if (supplierResult.isFailure) {
|
||||||
|
return Result.fail(supplierResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusResult = await this.deps.statusChanger.changeStatus({
|
||||||
|
supplier: supplierResult.data,
|
||||||
|
action: this.deps.action,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
if (statusResult.isFailure) {
|
||||||
|
return Result.fail(statusResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(this.deps.fullSnapshotBuilder.toOutput(statusResult.data));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return Result.fail(error as Error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./change-supplier-status.use-case";
|
||||||
export * from "./create-supplier.command";
|
export * from "./create-supplier.command";
|
||||||
export * from "./create-supplier.use-case";
|
export * from "./create-supplier.use-case";
|
||||||
export * from "./get-supplier.use-case";
|
export * from "./get-supplier.use-case";
|
||||||
|
|||||||
@ -1,13 +1,7 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
|
||||||
import {
|
import { CurrencyCode, LanguageCode, Name, Street, UniqueID } from "@repo/rdx-ddd";
|
||||||
CurrencyCode,
|
|
||||||
LanguageCode,
|
|
||||||
Name,
|
|
||||||
Street,
|
|
||||||
UniqueID,
|
|
||||||
} from "@repo/rdx-ddd";
|
|
||||||
import { Maybe } from "@repo/rdx-utils";
|
import { Maybe } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import { Supplier } from "./supplier.aggregate";
|
import { Supplier } from "./supplier.aggregate";
|
||||||
@ -61,3 +55,51 @@ test("crea Supplier activo por defecto y actualiza su dirección", () => {
|
|||||||
assert.equal(updateResult.isSuccess, true);
|
assert.equal(updateResult.isSuccess, true);
|
||||||
assert.equal(supplierResult.data.address.street.unwrap().toPrimitive(), "Calle de prueba");
|
assert.equal(supplierResult.data.address.street.unwrap().toPrimitive(), "Calle de prueba");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("activa y desactiva Supplier sin persistir transiciones repetidas", () => {
|
||||||
|
const supplierResult = Supplier.create({
|
||||||
|
companyId: UniqueID.generateNewID(),
|
||||||
|
reference: Maybe.none(),
|
||||||
|
isCompany: true,
|
||||||
|
name: Name.create("Proveedor de estado").data,
|
||||||
|
tradeName: Maybe.none(),
|
||||||
|
tin: Maybe.none(),
|
||||||
|
address: {
|
||||||
|
street: Maybe.none(),
|
||||||
|
street2: Maybe.none(),
|
||||||
|
city: Maybe.none(),
|
||||||
|
postalCode: Maybe.none(),
|
||||||
|
province: Maybe.none(),
|
||||||
|
country: Maybe.none(),
|
||||||
|
},
|
||||||
|
emailPrimary: Maybe.none(),
|
||||||
|
emailSecondary: Maybe.none(),
|
||||||
|
phonePrimary: Maybe.none(),
|
||||||
|
phoneSecondary: Maybe.none(),
|
||||||
|
mobilePrimary: Maybe.none(),
|
||||||
|
mobileSecondary: Maybe.none(),
|
||||||
|
fax: Maybe.none(),
|
||||||
|
website: Maybe.none(),
|
||||||
|
languageCode: LanguageCode.create("es").data,
|
||||||
|
currencyCode: CurrencyCode.create("EUR").data,
|
||||||
|
});
|
||||||
|
assert.equal(supplierResult.isSuccess, true);
|
||||||
|
|
||||||
|
const alreadyActive = supplierResult.data.activate();
|
||||||
|
assert.equal(alreadyActive.isSuccess, true);
|
||||||
|
assert.equal(alreadyActive.data, false);
|
||||||
|
|
||||||
|
const deactivateResult = supplierResult.data.deactivate();
|
||||||
|
assert.equal(deactivateResult.isSuccess, true);
|
||||||
|
assert.equal(deactivateResult.data, true);
|
||||||
|
assert.equal(supplierResult.data.isActive, false);
|
||||||
|
|
||||||
|
const alreadyInactive = supplierResult.data.deactivate();
|
||||||
|
assert.equal(alreadyInactive.isSuccess, true);
|
||||||
|
assert.equal(alreadyInactive.data, false);
|
||||||
|
|
||||||
|
const activateResult = supplierResult.data.activate();
|
||||||
|
assert.equal(activateResult.isSuccess, true);
|
||||||
|
assert.equal(activateResult.data, true);
|
||||||
|
assert.equal(supplierResult.data.isActive, true);
|
||||||
|
});
|
||||||
|
|||||||
@ -271,14 +271,22 @@ export class Supplier extends AggregateRoot<SupplierInternalProps> implements IS
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public activate(): Result<Supplier, Error> {
|
public activate(): Result<boolean, Error> {
|
||||||
|
if (this.props.status.isActive()) {
|
||||||
|
return Result.ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
this.props.status = SupplierStatus.active();
|
this.props.status = SupplierStatus.active();
|
||||||
return Result.ok(this);
|
return Result.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deactivate(): Result<Supplier, Error> {
|
public deactivate(): Result<boolean, Error> {
|
||||||
|
if (this.props.status.isInactive()) {
|
||||||
|
return Result.ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
this.props.status = SupplierStatus.inactive();
|
this.props.status = SupplierStatus.inactive();
|
||||||
return Result.ok(this);
|
return Result.ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type ChangeSupplierStatusUseCase,
|
||||||
type CreateSupplierUseCase,
|
type CreateSupplierUseCase,
|
||||||
type GetSupplierByIdUseCase,
|
type GetSupplierByIdUseCase,
|
||||||
type ListSuppliersUseCase,
|
type ListSuppliersUseCase,
|
||||||
type UpdateSupplierUseCase,
|
type UpdateSupplierUseCase,
|
||||||
|
buildChangeSupplierStatusUseCase,
|
||||||
buildCreateSupplierUseCase,
|
buildCreateSupplierUseCase,
|
||||||
buildGetSupplierByIdUseCase,
|
buildGetSupplierByIdUseCase,
|
||||||
buildListSuppliersUseCase,
|
buildListSuppliersUseCase,
|
||||||
@ -12,6 +14,7 @@ import {
|
|||||||
buildSupplierFinder,
|
buildSupplierFinder,
|
||||||
buildSupplierInputMappers,
|
buildSupplierInputMappers,
|
||||||
buildSupplierSnapshotBuilders,
|
buildSupplierSnapshotBuilders,
|
||||||
|
buildSupplierStatusChanger,
|
||||||
buildSupplierUpdater,
|
buildSupplierUpdater,
|
||||||
buildUpdateSupplierUseCase,
|
buildUpdateSupplierUseCase,
|
||||||
} from "../../application";
|
} from "../../application";
|
||||||
@ -25,6 +28,8 @@ export type SuppliersInternalDeps = {
|
|||||||
getSupplierById: () => GetSupplierByIdUseCase;
|
getSupplierById: () => GetSupplierByIdUseCase;
|
||||||
createSupplier: () => CreateSupplierUseCase;
|
createSupplier: () => CreateSupplierUseCase;
|
||||||
updateSupplier: () => UpdateSupplierUseCase;
|
updateSupplier: () => UpdateSupplierUseCase;
|
||||||
|
activateSupplier: () => ChangeSupplierStatusUseCase;
|
||||||
|
deactivateSupplier: () => ChangeSupplierStatusUseCase;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -42,6 +47,7 @@ export function buildSuppliersDependencies(params: ModuleParams): SuppliersInter
|
|||||||
const finder = buildSupplierFinder({ repository });
|
const finder = buildSupplierFinder({ repository });
|
||||||
const creator = buildSupplierCreator({ repository, audit });
|
const creator = buildSupplierCreator({ repository, audit });
|
||||||
const updater = buildSupplierUpdater({ repository, audit });
|
const updater = buildSupplierUpdater({ repository, audit });
|
||||||
|
const statusChanger = buildSupplierStatusChanger({ repository, audit });
|
||||||
|
|
||||||
const snapshotBuilders = buildSupplierSnapshotBuilders();
|
const snapshotBuilders = buildSupplierSnapshotBuilders();
|
||||||
|
|
||||||
@ -78,6 +84,24 @@ export function buildSuppliersDependencies(params: ModuleParams): SuppliersInter
|
|||||||
fullSnapshotBuilder: snapshotBuilders.full,
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
transactionManager,
|
transactionManager,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
activateSupplier: () =>
|
||||||
|
buildChangeSupplierStatusUseCase({
|
||||||
|
finder,
|
||||||
|
statusChanger,
|
||||||
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
|
transactionManager,
|
||||||
|
action: "activate",
|
||||||
|
}),
|
||||||
|
|
||||||
|
deactivateSupplier: () =>
|
||||||
|
buildChangeSupplierStatusUseCase({
|
||||||
|
finder,
|
||||||
|
statusChanger,
|
||||||
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
|
transactionManager,
|
||||||
|
action: "deactivate",
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
ExpressController,
|
||||||
|
requireAuthenticatedGuard,
|
||||||
|
requireCompanyContextGuard,
|
||||||
|
} from "@erp/core/api";
|
||||||
|
|
||||||
|
import type { ChangeSupplierStatusUseCase } from "../../../application";
|
||||||
|
import { suppliersApiErrorMapper } from "../supplier-api-error-mapper";
|
||||||
|
|
||||||
|
export class ChangeSupplierStatusController extends ExpressController {
|
||||||
|
public constructor(private readonly useCase: ChangeSupplierStatusUseCase) {
|
||||||
|
super();
|
||||||
|
this.errorMapper = suppliersApiErrorMapper;
|
||||||
|
this.registerGuards(requireAuthenticatedGuard(), requireCompanyContextGuard());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl() {
|
||||||
|
const companyId = this.getTenantId();
|
||||||
|
const { supplier_id: supplierId } = this.req.params;
|
||||||
|
if (!(companyId && supplierId)) {
|
||||||
|
return this.invalidInputError("Supplier ID missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.useCase.execute({ companyId, supplierId });
|
||||||
|
|
||||||
|
return result.match(
|
||||||
|
(data) => this.ok(data),
|
||||||
|
(error) => this.handleError(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./change-supplier-status.controller";
|
||||||
export * from "./create-supplier.controller";
|
export * from "./create-supplier.controller";
|
||||||
export * from "./get-supplier.controller";
|
export * from "./get-supplier.controller";
|
||||||
export * from "./list-suppliers.controller";
|
export * from "./list-suppliers.controller";
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import test from "node:test";
|
|||||||
|
|
||||||
import type { StartParams } from "@erp/core/api";
|
import type { StartParams } from "@erp/core/api";
|
||||||
|
|
||||||
|
import { UpdateSupplierByIdRequestSchema } from "../../../common";
|
||||||
|
|
||||||
import { suppliersRouter } from "./suppliers.routes";
|
import { suppliersRouter } from "./suppliers.routes";
|
||||||
|
|
||||||
type ExpressRouteLayer = {
|
type ExpressRouteLayer = {
|
||||||
@ -62,4 +64,17 @@ test("registra PATCH como única operación de actualización", () => {
|
|||||||
),
|
),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
registeredRouter.stack.some(
|
||||||
|
(layer) => layer.route?.path === "/:supplier_id/activate" && layer.route.methods.post
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
registeredRouter.stack.some(
|
||||||
|
(layer) => layer.route?.path === "/:supplier_id/deactivate" && layer.route.methods.post
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert.equal(UpdateSupplierByIdRequestSchema.safeParse({ status: "inactive" }).success, false);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { requireIdentityTenant } from "@erp/identity/api";
|
|||||||
import { type NextFunction, type Request, type Response, Router } from "express";
|
import { type NextFunction, type Request, type Response, Router } from "express";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ChangeSupplierStatusParamsRequestSchema,
|
||||||
CreateSupplierRequestSchema,
|
CreateSupplierRequestSchema,
|
||||||
GetSupplierByIdRequestSchema,
|
GetSupplierByIdRequestSchema,
|
||||||
SupplierListRequestSchema,
|
SupplierListRequestSchema,
|
||||||
@ -12,6 +13,7 @@ import {
|
|||||||
import type { SuppliersInternalDeps } from "../di";
|
import type { SuppliersInternalDeps } from "../di";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ChangeSupplierStatusController,
|
||||||
CreateSupplierController,
|
CreateSupplierController,
|
||||||
GetSupplierController,
|
GetSupplierController,
|
||||||
ListSuppliersController,
|
ListSuppliersController,
|
||||||
@ -83,5 +85,23 @@ export const suppliersRouter = (params: StartParams) => {
|
|||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/:supplier_id/activate",
|
||||||
|
validateRequest(ChangeSupplierStatusParamsRequestSchema, "params"),
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const controller = new ChangeSupplierStatusController(deps.useCases.activateSupplier());
|
||||||
|
return controller.execute(req, res, next);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/:supplier_id/deactivate",
|
||||||
|
validateRequest(ChangeSupplierStatusParamsRequestSchema, "params"),
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const controller = new ChangeSupplierStatusController(deps.useCases.deactivateSupplier());
|
||||||
|
return controller.execute(req, res, next);
|
||||||
|
}
|
||||||
|
);
|
||||||
app.use(`${config.server.apiBasePath}/suppliers`, router);
|
app.use(`${config.server.apiBasePath}/suppliers`, router);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const ChangeSupplierStatusParamsRequestSchema = z.object({
|
||||||
|
supplier_id: z.uuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ChangeSupplierStatusParamsRequestDTO = z.infer<
|
||||||
|
typeof ChangeSupplierStatusParamsRequestSchema
|
||||||
|
>;
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./change-supplier-status.request.dto";
|
||||||
export * from "./create-supplier.request.dto";
|
export * from "./create-supplier.request.dto";
|
||||||
export * from "./get-supplier-by-id.request.dto";
|
export * from "./get-supplier-by-id.request.dto";
|
||||||
export * from "./supplier-list.request.dto";
|
export * from "./supplier-list.request.dto";
|
||||||
|
|||||||
@ -4,33 +4,35 @@ export const UpdateSupplierByIdParamsRequestSchema = z.object({
|
|||||||
supplier_id: z.uuid(),
|
supplier_id: z.uuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateSupplierByIdRequestSchema = z.object({
|
export const UpdateSupplierByIdRequestSchema = z
|
||||||
reference: z.string().optional(),
|
.object({
|
||||||
|
reference: z.string().optional(),
|
||||||
|
|
||||||
is_company: z.boolean().optional(),
|
is_company: z.boolean().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
trade_name: z.string().optional(),
|
trade_name: z.string().optional(),
|
||||||
tin: z.string().optional(),
|
tin: z.string().optional(),
|
||||||
|
|
||||||
street: z.string().optional(),
|
street: z.string().optional(),
|
||||||
street2: z.string().optional(),
|
street2: z.string().optional(),
|
||||||
city: z.string().optional(),
|
city: z.string().optional(),
|
||||||
province: z.string().optional(),
|
province: z.string().optional(),
|
||||||
postal_code: z.string().optional(),
|
postal_code: z.string().optional(),
|
||||||
country: z.string().optional(),
|
country: z.string().optional(),
|
||||||
|
|
||||||
email_primary: z.string().optional(),
|
email_primary: z.string().optional(),
|
||||||
email_secondary: z.string().optional(),
|
email_secondary: z.string().optional(),
|
||||||
phone_primary: z.string().optional(),
|
phone_primary: z.string().optional(),
|
||||||
phone_secondary: z.string().optional(),
|
phone_secondary: z.string().optional(),
|
||||||
mobile_primary: z.string().optional(),
|
mobile_primary: z.string().optional(),
|
||||||
mobile_secondary: z.string().optional(),
|
mobile_secondary: z.string().optional(),
|
||||||
|
|
||||||
fax: z.string().optional(),
|
fax: z.string().optional(),
|
||||||
website: z.string().optional(),
|
website: z.string().optional(),
|
||||||
|
|
||||||
language_code: z.string().optional(),
|
language_code: z.string().optional(),
|
||||||
currency_code: z.string().optional(),
|
currency_code: z.string().optional(),
|
||||||
});
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export type UpdateSupplierByIdRequestDTO = Partial<z.infer<typeof UpdateSupplierByIdRequestSchema>>;
|
export type UpdateSupplierByIdRequestDTO = Partial<z.infer<typeof UpdateSupplierByIdRequestSchema>>;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user