fix(housekeeping): preserve audit reasons and completed effects
This commit is contained in:
1 parent
866f38818b
commit
555bc75f9e
10 files changed
+307
-13
No files matched your search
@@ -0,0 +1,45 @@
|
|||||||
|
# Housekeeping Backend Integrity Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** Use superpowers:executing-plans task-by-task. Keep checkpoints independently verifiable.
|
||||||
|
|
||||||
|
**Goal:** Complete the approved backend review, starting with reproducible audit and external-completion defects.
|
||||||
|
|
||||||
|
**Architecture:** Retain the six domain services and production adapters. Follow every operation from its public entrypoint through authorization, validation, storage, external effects and audit. Route coverage is not functional completion.
|
||||||
|
|
||||||
|
**Tech Stack:** TypeScript, Drizzle/MySQL, Vitest, Next.js, pnpm.
|
||||||
|
|
||||||
|
**Spec:** Backend scope approved in conversation on 2026-09-05: complete operations, permissions, validation, transactions, concurrency, audit and partial outcomes; preserve UI and production routing.
|
||||||
|
|
||||||
|
## Global constraints
|
||||||
|
|
||||||
|
- Work in the canonical checkout on `codex/housekeeping-rebuild-stepwise`; no worktrees.
|
||||||
|
- Preserve untracked local files and existing administration routes.
|
||||||
|
- Keep PR 53 draft and update English and Dutch evidence after each verified push.
|
||||||
|
- No production writes or deployment. Missing local services block live acceptance, not implementation.
|
||||||
|
|
||||||
|
## Task 1: Preserve reasons through service and production audit
|
||||||
|
|
||||||
|
Files: Content and Economy `services/mutations.ts`, `services/mutations-production.ts`; new `src/features/housekeeping/backend-audit-integrity.test.ts`.
|
||||||
|
|
||||||
|
Interfaces: optional normalized `reason` in mutation context; existing `AuditEntry.reason` at persistence.
|
||||||
|
|
||||||
|
- [ ] Exercise real service plus production adapter with captured audit writes. For database and external operations assert `entry.reason === "Remove obsolete resource"` from a whitespace-padded invocation.
|
||||||
|
- [ ] Run `pnpm exec vitest run --coverage.enabled=false src/features/housekeeping/backend-audit-integrity.test.ts`; verify missing reason fails.
|
||||||
|
- [ ] Normalize once at the service boundary, conditionally include the reason in the adapter context, persist in every adapter audit outcome.
|
||||||
|
- [ ] Repeat focused tests, including absent reasons and audit-failure outcomes.
|
||||||
|
|
||||||
|
## Task 2: Preserve completed Hotel external effects when audit fails
|
||||||
|
|
||||||
|
Files: Hotel `services/mutations-production.ts` and its existing test.
|
||||||
|
|
||||||
|
Interfaces: existing `HotelMutationSnapshot.completion`, with `status: "partial", external: "completed", audit: "unavailable"`.
|
||||||
|
|
||||||
|
- [ ] Execute `room.runtime` with successful effect but failing success-audit writer; assert resolved snapshot with partial completion and no failure audit.
|
||||||
|
- [ ] Verify failure against current production adapter.
|
||||||
|
- [ ] Separate external-execution errors from completion-audit errors. Preserve intent-before-effect and original external failure semantics.
|
||||||
|
- [ ] Test failed intent prevents execution and failed failure-audit does not replace the original exception.
|
||||||
|
- [ ] Run focused suites, typecheck, scoped Biome, full suite and review the diff; commit exact paths and push with normal hooks.
|
||||||
|
|
||||||
|
## Subsequent independently verified blocks
|
||||||
|
|
||||||
|
These are open audit scope, not completed tasks: Economy marketplace/voucher concurrency; operation-level parity and validation across Content, Economy, Hotel, People, System and Operations; all corresponding API and legacy entrypoints. Each block requires concrete findings, regression tests and its own implementation steps before changes. Do not mark the whole backend complete from Tasks 1-2.
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { AuditEntry } from "@/lib/services/audit";
|
||||||
|
import { createContentMutationService } from "./domains/content/services/mutations";
|
||||||
|
import {
|
||||||
|
ContentCommittedExternalFailure,
|
||||||
|
createContentProductionMutationAdapter,
|
||||||
|
} from "./domains/content/services/mutations-production";
|
||||||
|
import { createEconomyMutationService } from "./domains/economy/services/mutations";
|
||||||
|
import {
|
||||||
|
createEconomyProductionMutationAdapter,
|
||||||
|
EconomyCommittedExternalFailure,
|
||||||
|
} from "./domains/economy/services/mutations-production";
|
||||||
|
import { createHotelCommands } from "./domains/hotel/commands/hotel-commands";
|
||||||
|
import { createHotelMutationService } from "./domains/hotel/services/mutations";
|
||||||
|
import { createHotelProductionMutationAdapter } from "./domains/hotel/services/mutations-production";
|
||||||
|
import type { HousekeepingCapabilityContext } from "./foundation/contracts";
|
||||||
|
|
||||||
|
const capability: HousekeepingCapabilityContext = {
|
||||||
|
actor: { id: 42, username: "operator", rank: 7 },
|
||||||
|
isSuperAdmin: false,
|
||||||
|
has: () => true,
|
||||||
|
hasAny: () => true,
|
||||||
|
hasAll: () => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Backend audit reason integrity", () => {
|
||||||
|
it("retains the Hotel command reason through execution and partial audit completion", async () => {
|
||||||
|
const entries: AuditEntry[] = [];
|
||||||
|
const service = createHotelMutationService(
|
||||||
|
createHotelProductionMutationAdapter({
|
||||||
|
transaction: async (run) => run({}),
|
||||||
|
writeAudit: async (entry) => {
|
||||||
|
entries.push(entry);
|
||||||
|
if (entry.outcome === "success") throw new Error("audit unavailable");
|
||||||
|
},
|
||||||
|
executeOperation: async () => ({
|
||||||
|
before: null,
|
||||||
|
after: { dispatched: true },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async () => capability,
|
||||||
|
);
|
||||||
|
const command = createHotelCommands(service).find(
|
||||||
|
(item) => item.operation === "room.runtime",
|
||||||
|
);
|
||||||
|
if (!command) throw new Error("missing runtime command");
|
||||||
|
const result = await command.execute(
|
||||||
|
{
|
||||||
|
capability,
|
||||||
|
correlationId: "hotel-reason",
|
||||||
|
ipAddress: "127.0.0.1",
|
||||||
|
reason: " Reload after maintenance ",
|
||||||
|
},
|
||||||
|
{ roomId: 7, action: "reload" },
|
||||||
|
);
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
correlationId: "hotel-reason",
|
||||||
|
completion: {
|
||||||
|
status: "partial",
|
||||||
|
external: "completed",
|
||||||
|
audit: "unavailable",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(entries.map((entry) => entry.reason)).toEqual([
|
||||||
|
"Reload after maintenance",
|
||||||
|
"Reload after maintenance",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
for (const domain of ["content", "economy"] as const) {
|
||||||
|
it.each(["failure", "partial", "absent", "blank"] as const)(
|
||||||
|
`${domain} preserves audit reason semantics for %s`,
|
||||||
|
async (scenario) => {
|
||||||
|
const entries: AuditEntry[] = [];
|
||||||
|
const dependencies = {
|
||||||
|
transaction: async <T>(run: (tx: unknown) => Promise<T>) => run({}),
|
||||||
|
writeAudit: async (entry: AuditEntry) => {
|
||||||
|
entries.push(entry);
|
||||||
|
},
|
||||||
|
executeOperation: async () => {
|
||||||
|
if (scenario === "failure")
|
||||||
|
throw new Error("transport unavailable");
|
||||||
|
if (scenario === "partial") {
|
||||||
|
const snapshot = {
|
||||||
|
before: { id: "7" },
|
||||||
|
after: { id: "7", saved: true },
|
||||||
|
};
|
||||||
|
throw domain === "content"
|
||||||
|
? new ContentCommittedExternalFailure(snapshot)
|
||||||
|
: new EconomyCommittedExternalFailure(snapshot);
|
||||||
|
}
|
||||||
|
return { before: null, after: { saved: true } };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const invocation = {
|
||||||
|
expectedActorId: 42,
|
||||||
|
correlationId: "outcome-check",
|
||||||
|
...(scenario === "absent"
|
||||||
|
? {}
|
||||||
|
: { reason: scenario === "blank" ? " " : " Maintenance " }),
|
||||||
|
};
|
||||||
|
const result =
|
||||||
|
domain === "content"
|
||||||
|
? await createContentMutationService(
|
||||||
|
createContentProductionMutationAdapter(dependencies),
|
||||||
|
async () => capability,
|
||||||
|
).execute(invocation, "theme.update", {})
|
||||||
|
: await createEconomyMutationService(
|
||||||
|
createEconomyProductionMutationAdapter(dependencies),
|
||||||
|
async () => capability,
|
||||||
|
).execute(invocation, "catalog-page.change", {});
|
||||||
|
expect(result.ok).toBe(scenario !== "failure");
|
||||||
|
if (scenario === "partial")
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
completion: {
|
||||||
|
status: "partial",
|
||||||
|
external: "failed",
|
||||||
|
audit: "persisted",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(entries.map((entry) => entry.outcome)).toEqual([
|
||||||
|
"intent",
|
||||||
|
scenario === "failure" || scenario === "partial"
|
||||||
|
? scenario
|
||||||
|
: "success",
|
||||||
|
]);
|
||||||
|
expect(entries.map((entry) => entry.reason)).toEqual(
|
||||||
|
scenario === "absent" || scenario === "blank"
|
||||||
|
? [undefined, undefined]
|
||||||
|
: ["Maintenance", "Maintenance"],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
for (const external of [false, true]) {
|
||||||
|
it(`${domain} retains the normalized reason in ${external ? "external" : "database"} audit records`, async () => {
|
||||||
|
const entries: AuditEntry[] = [];
|
||||||
|
const dependencies = {
|
||||||
|
transaction: async <T>(run: (tx: unknown) => Promise<T>) => run({}),
|
||||||
|
writeAudit: async (entry: AuditEntry) => {
|
||||||
|
entries.push(entry);
|
||||||
|
},
|
||||||
|
executeOperation: async () => ({ before: { id: "7" }, after: null }),
|
||||||
|
};
|
||||||
|
const invocation = {
|
||||||
|
expectedActorId: 42,
|
||||||
|
correlationId: "reason-check",
|
||||||
|
reason: " Remove obsolete resource ",
|
||||||
|
};
|
||||||
|
const result =
|
||||||
|
domain === "content"
|
||||||
|
? await createContentMutationService(
|
||||||
|
createContentProductionMutationAdapter(dependencies),
|
||||||
|
async () => capability,
|
||||||
|
).execute(invocation, external ? "media.delete" : "tag.change", {
|
||||||
|
action: "delete",
|
||||||
|
id: "7",
|
||||||
|
})
|
||||||
|
: await createEconomyMutationService(
|
||||||
|
createEconomyProductionMutationAdapter(dependencies),
|
||||||
|
async () => capability,
|
||||||
|
).execute(
|
||||||
|
invocation,
|
||||||
|
external ? "badge.upload" : "voucher.change",
|
||||||
|
{ action: "delete", id: "7" },
|
||||||
|
);
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
expect(
|
||||||
|
entries.map((entry) => ({
|
||||||
|
outcome: entry.outcome,
|
||||||
|
reason: entry.reason,
|
||||||
|
})),
|
||||||
|
).toEqual(
|
||||||
|
external
|
||||||
|
? [
|
||||||
|
{ outcome: "intent", reason: "Remove obsolete resource" },
|
||||||
|
{ outcome: "success", reason: "Remove obsolete resource" },
|
||||||
|
]
|
||||||
|
: [{ outcome: "success", reason: "Remove obsolete resource" }],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -82,6 +82,7 @@ function auditEntry(
|
|||||||
action: `content.${operation}`,
|
action: `content.${operation}`,
|
||||||
target: "Content",
|
target: "Content",
|
||||||
correlationId: context.correlationId,
|
correlationId: context.correlationId,
|
||||||
|
...(context.reason === undefined ? {} : { reason: context.reason }),
|
||||||
domain: "content",
|
domain: "content",
|
||||||
outcome,
|
outcome,
|
||||||
before:
|
before:
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export interface ContentMutationContext {
|
|||||||
readonly capability: HousekeepingCapabilityContext;
|
readonly capability: HousekeepingCapabilityContext;
|
||||||
readonly correlationId: string;
|
readonly correlationId: string;
|
||||||
readonly legacy: boolean;
|
readonly legacy: boolean;
|
||||||
|
readonly reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContentMutationAdapter {
|
export interface ContentMutationAdapter {
|
||||||
@@ -158,10 +159,8 @@ export function createContentMutationService(
|
|||||||
invocation.correlationId,
|
invocation.correlationId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
const reason = normalizeHousekeepingReason(invocation.reason);
|
||||||
requiresHousekeepingReason(false, input) &&
|
if (requiresHousekeepingReason(false, input) && !reason) {
|
||||||
!normalizeHousekeepingReason(invocation.reason)
|
|
||||||
) {
|
|
||||||
return fail(
|
return fail(
|
||||||
"VALIDATION",
|
"VALIDATION",
|
||||||
"errors.housekeeping.validation",
|
"errors.housekeeping.validation",
|
||||||
@@ -174,6 +173,7 @@ export function createContentMutationService(
|
|||||||
capability,
|
capability,
|
||||||
correlationId: invocation.correlationId,
|
correlationId: invocation.correlationId,
|
||||||
legacy: invocation.legacy === true,
|
legacy: invocation.legacy === true,
|
||||||
|
...(reason === undefined ? {} : { reason }),
|
||||||
});
|
});
|
||||||
return ok(snapshot, invocation.correlationId, snapshot.completion);
|
return ok(snapshot, invocation.correlationId, snapshot.completion);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ function auditEntry(
|
|||||||
action: `economy.${operation}`,
|
action: `economy.${operation}`,
|
||||||
target: "Economy",
|
target: "Economy",
|
||||||
correlationId: context.correlationId,
|
correlationId: context.correlationId,
|
||||||
|
...(context.reason === undefined ? {} : { reason: context.reason }),
|
||||||
domain: "economy",
|
domain: "economy",
|
||||||
outcome,
|
outcome,
|
||||||
before:
|
before:
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export interface EconomyMutationContext {
|
|||||||
readonly capability: HousekeepingCapabilityContext;
|
readonly capability: HousekeepingCapabilityContext;
|
||||||
readonly correlationId: string;
|
readonly correlationId: string;
|
||||||
readonly legacy: boolean;
|
readonly legacy: boolean;
|
||||||
|
readonly reason?: string;
|
||||||
}
|
}
|
||||||
export interface EconomyMutationAdapter {
|
export interface EconomyMutationAdapter {
|
||||||
execute(
|
execute(
|
||||||
@@ -134,10 +135,8 @@ export function createEconomyMutationService(
|
|||||||
invocation.correlationId,
|
invocation.correlationId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
const reason = normalizeHousekeepingReason(invocation.reason);
|
||||||
requiresHousekeepingReason(false, input) &&
|
if (requiresHousekeepingReason(false, input) && !reason) {
|
||||||
!normalizeHousekeepingReason(invocation.reason)
|
|
||||||
) {
|
|
||||||
return fail(
|
return fail(
|
||||||
"VALIDATION",
|
"VALIDATION",
|
||||||
"errors.housekeeping.validation",
|
"errors.housekeeping.validation",
|
||||||
@@ -150,6 +149,7 @@ export function createEconomyMutationService(
|
|||||||
capability,
|
capability,
|
||||||
correlationId: invocation.correlationId,
|
correlationId: invocation.correlationId,
|
||||||
legacy: invocation.legacy === true,
|
legacy: invocation.legacy === true,
|
||||||
|
...(reason === undefined ? {} : { reason }),
|
||||||
});
|
});
|
||||||
return ok(snapshot, invocation.correlationId, snapshot.completion);
|
return ok(snapshot, invocation.correlationId, snapshot.completion);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export function createHotelCommands(
|
|||||||
{
|
{
|
||||||
correlationId: context.correlationId,
|
correlationId: context.correlationId,
|
||||||
expectedActorId: context.capability.actor.id,
|
expectedActorId: context.capability.actor.id,
|
||||||
|
...(context.reason === undefined ? {} : { reason: context.reason }),
|
||||||
},
|
},
|
||||||
operation,
|
operation,
|
||||||
input,
|
input,
|
||||||
|
|||||||
@@ -45,6 +45,51 @@ function dependencies() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("Hotel production mutation adapter", () => {
|
describe("Hotel production mutation adapter", () => {
|
||||||
|
it("retains completed RCON delivery when the completion audit fails", async () => {
|
||||||
|
const deps = dependencies();
|
||||||
|
deps.writeAudit.mockImplementation(async (entry) => {
|
||||||
|
if (entry.outcome === "success") throw new Error("audit unavailable");
|
||||||
|
});
|
||||||
|
const result = await deps.adapter.execute(
|
||||||
|
"room.runtime",
|
||||||
|
{ roomId: 7, action: "reload" },
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
after: { operation: "room.runtime", state: "after" },
|
||||||
|
completion: {
|
||||||
|
status: "partial",
|
||||||
|
external: "completed",
|
||||||
|
audit: "unavailable",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(deps.writeAudit.mock.calls.map(([entry]) => entry.outcome)).toEqual([
|
||||||
|
"intent",
|
||||||
|
"success",
|
||||||
|
]);
|
||||||
|
expect(deps.executeOperation).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not execute RCON when intent persistence fails", async () => {
|
||||||
|
const deps = dependencies();
|
||||||
|
deps.writeAudit.mockRejectedValueOnce(new Error("intent unavailable"));
|
||||||
|
await expect(
|
||||||
|
deps.adapter.execute("room.runtime", {}, context),
|
||||||
|
).rejects.toThrow("intent unavailable");
|
||||||
|
expect(deps.executeOperation).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves the original RCON failure if failure auditing is unavailable", async () => {
|
||||||
|
const deps = dependencies();
|
||||||
|
const failure = new Error("RCON unavailable");
|
||||||
|
deps.executeOperation.mockRejectedValueOnce(failure);
|
||||||
|
deps.writeAudit.mockImplementation(async (entry) => {
|
||||||
|
if (entry.outcome === "failure") throw new Error("audit unavailable");
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
deps.adapter.execute("room.runtime", {}, context),
|
||||||
|
).rejects.toBe(failure);
|
||||||
|
});
|
||||||
it("classifies every operation exactly once", () => {
|
it("classifies every operation exactly once", () => {
|
||||||
const classified = [
|
const classified = [
|
||||||
...HOTEL_DATABASE_OPERATIONS,
|
...HOTEL_DATABASE_OPERATIONS,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ function auditEntry(
|
|||||||
action: `hotel.${operation}`,
|
action: `hotel.${operation}`,
|
||||||
target: "Hotel",
|
target: "Hotel",
|
||||||
correlationId: context.correlationId,
|
correlationId: context.correlationId,
|
||||||
|
...(context.reason === undefined ? {} : { reason: context.reason }),
|
||||||
domain: "hotel",
|
domain: "hotel",
|
||||||
outcome,
|
outcome,
|
||||||
before: snapshot?.before ? { ...snapshot.before } : undefined,
|
before: snapshot?.before ? { ...snapshot.before } : undefined,
|
||||||
@@ -70,16 +71,13 @@ export function createHotelProductionMutationAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await dependencies.writeAudit(auditEntry(operation, context, "intent"));
|
await dependencies.writeAudit(auditEntry(operation, context, "intent"));
|
||||||
|
let snapshot: HotelMutationSnapshot;
|
||||||
try {
|
try {
|
||||||
const snapshot = await dependencies.executeOperation(
|
snapshot = await dependencies.executeOperation(
|
||||||
operation,
|
operation,
|
||||||
input,
|
input,
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
await dependencies.writeAudit(
|
|
||||||
auditEntry(operation, context, "success", snapshot),
|
|
||||||
);
|
|
||||||
return snapshot;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
try {
|
try {
|
||||||
await dependencies.writeAudit(
|
await dependencies.writeAudit(
|
||||||
@@ -90,6 +88,21 @@ export function createHotelProductionMutationAdapter(
|
|||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
await dependencies.writeAudit(
|
||||||
|
auditEntry(operation, context, "success", snapshot),
|
||||||
|
);
|
||||||
|
return snapshot;
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
...snapshot,
|
||||||
|
completion: {
|
||||||
|
status: "partial",
|
||||||
|
external: "completed",
|
||||||
|
audit: "unavailable",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PERMS } from "@/lib/permission-slugs";
|
import { PERMS } from "@/lib/permission-slugs";
|
||||||
import { satisfiesCapability } from "../../../foundation/capability-context";
|
import { satisfiesCapability } from "../../../foundation/capability-context";
|
||||||
|
import { normalizeHousekeepingReason } from "../../../foundation/commands/reason-policy";
|
||||||
import {
|
import {
|
||||||
anyCapability,
|
anyCapability,
|
||||||
fail,
|
fail,
|
||||||
@@ -48,12 +49,14 @@ export interface HotelMutationInvocation {
|
|||||||
readonly correlationId: string;
|
readonly correlationId: string;
|
||||||
readonly expectedActorId: number;
|
readonly expectedActorId: number;
|
||||||
readonly legacy?: boolean;
|
readonly legacy?: boolean;
|
||||||
|
readonly reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HotelMutationContext {
|
export interface HotelMutationContext {
|
||||||
readonly capability: HousekeepingCapabilityContext;
|
readonly capability: HousekeepingCapabilityContext;
|
||||||
readonly correlationId: string;
|
readonly correlationId: string;
|
||||||
readonly legacy: boolean;
|
readonly legacy: boolean;
|
||||||
|
readonly reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HotelMutationAdapter {
|
export interface HotelMutationAdapter {
|
||||||
@@ -116,10 +119,12 @@ export function createHotelMutationService(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const reason = normalizeHousekeepingReason(invocation.reason);
|
||||||
const snapshot = await adapter.execute(operation, input, {
|
const snapshot = await adapter.execute(operation, input, {
|
||||||
capability,
|
capability,
|
||||||
correlationId: invocation.correlationId,
|
correlationId: invocation.correlationId,
|
||||||
legacy: invocation.legacy === true,
|
legacy: invocation.legacy === true,
|
||||||
|
...(reason === undefined ? {} : { reason }),
|
||||||
});
|
});
|
||||||
return ok(snapshot, invocation.correlationId, snapshot.completion);
|
return ok(snapshot, invocation.correlationId, snapshot.completion);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in new issue
Block a user