@voyant-travel/finance 0.159.0 → 0.162.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  Finance module for Voyant. Invoices, payments, credit notes, supplier payments, and finance notes.
4
4
 
5
+ ## Composed agent commands
6
+
7
+ `create_booking` is owned by the Finance booking-create extension because that package
8
+ already owns the atomic composer spanning product conversion, travelers, room/item
9
+ lines, payment schedules, optional credits and group membership, invoices, ledger
10
+ records, and post-commit events. The Tool is a structural adapter over that service.
11
+
12
+ `issue_invoice_from_booking` creates and issues either an invoice or proforma through
13
+ the same package-owned composer used by the HTTP route. Agent execution requires an
14
+ idempotency key and exact approval. Approved execution records approval and causation
15
+ in the action ledger; replaying an already executed approval returns the original
16
+ invoice rather than issuing a duplicate.
17
+
18
+ ## Agent Tools
19
+
20
+ `issue_invoice_refund` defines a refund as issuing an `issued` credit note
21
+ against an invoice through the existing credit-note domain service. Agent
22
+ callers cannot issue it directly: the first call creates a pending action-ledger
23
+ approval, and execution requires the approved id plus an exact match on the
24
+ principal, command, current invoice snapshot, and fingerprint. Successful
25
+ execution is recorded as `finance.credit_note.issue_refund`, linked to the
26
+ requested action and approval, and requires `finance:refund`.
27
+
5
28
  ## Install
6
29
 
7
30
  ```bash
@@ -94,11 +117,11 @@ Booking creation UIs can show the same tax line that booking finalization will
94
117
  persist by mounting the booking-tax extension:
95
118
 
96
119
  ```typescript
97
- import { createBookingTaxHonoExtension } from "@voyant-travel/finance/booking-tax"
120
+ import { createBookingTaxApiExtension } from "@voyant-travel/finance/booking-tax"
98
121
 
99
122
  createApp({
100
123
  extensions: [
101
- createBookingTaxHonoExtension({
124
+ createBookingTaxApiExtension({
102
125
  resolveBookingTaxSettings: async (db) => {
103
126
  const settings = await getTaxSettings(db)
104
127
  return {
@@ -117,7 +140,7 @@ store, while `@voyant-travel/finance` owns the tax policy rule walker, tax-regim
117
140
  lookup, product tax-class fallback, and inclusive/exclusive math.
118
141
 
119
142
  Templates that already mount custom routes can call `mountBookingTaxRoutes(...)`
120
- from the same entrypoint instead of using the Hono extension.
143
+ from the same entrypoint instead of using the API extension.
121
144
 
122
145
  Mounting this route registers:
123
146
 
@@ -149,11 +172,11 @@ exchange-rate resolver:
149
172
 
150
173
  ```typescript
151
174
  import {
152
- createFinanceHonoModule,
175
+ createFinanceApiModule,
153
176
  createVoyantDataFxExchangeRateResolver,
154
177
  } from "@voyant-travel/finance"
155
178
 
156
- createFinanceHonoModule({
179
+ createFinanceApiModule({
157
180
  invoiceFxSettings: {
158
181
  baseCurrency: "RON",
159
182
  fxCommissionBps: 200,
@@ -1,5 +1,5 @@
1
1
  import { OpenAPIHono } from "@hono/zod-openapi";
2
- import type { HonoExtension } from "@voyant-travel/hono/module";
2
+ import type { ApiExtension } from "@voyant-travel/hono/module";
3
3
  import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
4
4
  import type { Hono } from "hono";
5
5
  export type ProductTaxFacts = {
@@ -129,5 +129,5 @@ export declare function createBookingTaxRoutes(options?: BookingTaxRouteOptions)
129
129
  };
130
130
  }, "/">;
131
131
  export declare function mountBookingTaxRoutes(hono: Hono, options?: BookingTaxRouteOptions): void;
132
- export declare function createBookingTaxHonoExtension(options?: BookingTaxRouteOptions): HonoExtension;
133
- export declare const createBookingTaxVoyantRuntime: import("@voyant-travel/core/project").VoyantGraphRuntimeFactory<HonoExtension>;
132
+ export declare function createBookingTaxApiExtension(options?: BookingTaxRouteOptions): ApiExtension;
133
+ export declare const createBookingTaxVoyantRuntime: import("@voyant-travel/core/project").VoyantGraphRuntimeFactory<ApiExtension>;
@@ -320,7 +320,7 @@ export function createBookingTaxRoutes(options = {}) {
320
320
  export function mountBookingTaxRoutes(hono, options = {}) {
321
321
  hono.route("/v1/admin/bookings", createBookingTaxRoutes(options));
322
322
  }
323
- export function createBookingTaxHonoExtension(options = {}) {
323
+ export function createBookingTaxApiExtension(options = {}) {
324
324
  const extension = {
325
325
  name: "booking-tax",
326
326
  module: "bookings",
@@ -331,5 +331,5 @@ export function createBookingTaxHonoExtension(options = {}) {
331
331
  };
332
332
  }
333
333
  export const createBookingTaxVoyantRuntime = defineGraphRuntimeFactory(async ({ getPort }) => {
334
- return createBookingTaxHonoExtension(createFinanceBookingTaxRuntime(await getPort(financeOperatorSettingsRuntimePort)));
334
+ return createBookingTaxApiExtension(createFinanceBookingTaxRuntime(await getPort(financeOperatorSettingsRuntimePort)));
335
335
  });
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Module } from "@voyant-travel/core";
2
- import type { HonoModule } from "@voyant-travel/hono/module";
2
+ import type { ApiModule } from "@voyant-travel/hono/module";
3
3
  import { type BookingTaxRouteOptions } from "./booking-tax.js";
4
4
  import { type CheckoutRoutesOptions } from "./checkout-routes.js";
5
5
  import { type FinanceRuntimeOptions } from "./route-runtime.js";
@@ -18,21 +18,21 @@ export { createPublicFinanceRoutes, type PublicFinanceRouteOptions, publicFinanc
18
18
  export { type SupplierInvoiceRoutes, supplierInvoiceRoutes, } from "./routes-supplier-invoices.js";
19
19
  export { type PublicFinanceRuntimeOptions, publicFinanceService } from "./service-public.js";
20
20
  export declare const financeModule: Module;
21
- export interface FinanceHonoModuleOptions extends FinanceRuntimeOptions, PublicFinanceRouteOptions, CheckoutRoutesOptions, BookingTaxRouteOptions {
21
+ export interface FinanceApiModuleOptions extends FinanceRuntimeOptions, PublicFinanceRouteOptions, CheckoutRoutesOptions, BookingTaxRouteOptions {
22
22
  }
23
- export declare function createFinanceHonoModule(options?: FinanceHonoModuleOptions): HonoModule;
24
- export declare const financeHonoModule: HonoModule;
25
- export declare const createFinanceVoyantRuntime: import("@voyant-travel/core/project").VoyantGraphRuntimeFactory<HonoModule>;
23
+ export declare function createFinanceApiModule(options?: FinanceApiModuleOptions): ApiModule;
24
+ export declare const financeApiModule: ApiModule;
25
+ export declare const createFinanceVoyantRuntime: import("@voyant-travel/core/project").VoyantGraphRuntimeFactory<ApiModule>;
26
26
  export { type BookingCancellationSettlementInput, buildPaidBookingCancellationSettlementNote, closeTerminalBookingPaymentSchedules, financeBookingLifecycle, recordPaidBookingCancellationSettlement, } from "./booking-lifecycle.js";
27
- export { type BookingTaxRouteOptions, type BookingTaxSettings, computeBookingItemTaxLine, createBookingTaxHonoExtension, createBookingTaxRoutes, createBookingTaxVoyantRuntime, loadProductTaxFacts, matchesTaxPolicyCondition, mountBookingTaxRoutes, type ProductTaxFacts, type ResolveBookingSellTaxRateOptions, type ResolveBookingTaxSettings, type ResolvedBookingSellTaxRate, resolveBookingSellTaxRate, type TaxPolicyCondition, type UpdateBookingTaxSettings, } from "./booking-tax.js";
27
+ export { type BookingTaxRouteOptions, type BookingTaxSettings, computeBookingItemTaxLine, createBookingTaxApiExtension, createBookingTaxRoutes, createBookingTaxVoyantRuntime, loadProductTaxFacts, matchesTaxPolicyCondition, mountBookingTaxRoutes, type ProductTaxFacts, type ResolveBookingSellTaxRateOptions, type ResolveBookingTaxSettings, type ResolvedBookingSellTaxRate, resolveBookingSellTaxRate, type TaxPolicyCondition, type UpdateBookingTaxSettings, } from "./booking-tax.js";
28
28
  export type { CardPaymentBilling, CardPaymentStartArgs, CardPaymentStarter, CardPaymentStartResult, } from "./card-payment.js";
29
29
  export { type DocumentDownloadEnvelope, type DocumentDownloadResolution, type DocumentDownloadResolver, resolveStoredDocumentDownload, type StoredDocumentReference, } from "./document-download.js";
30
- export { createInvoiceFxHonoExtension, createInvoiceFxRoutes, createVoyantDataFxExchangeRateResolver, type InvoiceExchangeRateResolution, type InvoiceFxContext, type InvoiceFxOptions, type InvoiceFxRouteOptions, type InvoiceFxSettings, mountInvoiceFxRoutes, type ResolvedInvoiceFxSettings, type ResolveInvoiceExchangeRate, type ResolveInvoiceExchangeRateInput, type ResolveInvoiceFxSettings, resolveInvoiceFxContext, resolveInvoiceFxSettingsOrDefault, type UpdateInvoiceFxSettings, type VoyantDataFxResolverOptions, } from "./invoice-fx.js";
30
+ export { createInvoiceFxApiExtension, createInvoiceFxRoutes, createVoyantDataFxExchangeRateResolver, type InvoiceExchangeRateResolution, type InvoiceFxContext, type InvoiceFxOptions, type InvoiceFxRouteOptions, type InvoiceFxSettings, mountInvoiceFxRoutes, type ResolvedInvoiceFxSettings, type ResolveInvoiceExchangeRate, type ResolveInvoiceExchangeRateInput, type ResolveInvoiceFxSettings, resolveInvoiceFxContext, resolveInvoiceFxSettingsOrDefault, type UpdateInvoiceFxSettings, type VoyantDataFxResolverOptions, } from "./invoice-fx.js";
31
31
  export { type CreateOrderPaymentSessionsOptions, createOrderPaymentSessions, type EnsureOrderSessionParams, type OrderPaymentSessionSummary, type OrderPaymentSessions, type OrderPaymentSessionTargetType, type StartOrderPaymentProvider, } from "./order-payment-sessions.js";
32
32
  export type { ComputedScheduleEntry, ComputeScheduleInput, DepositKind, DepositRule, PaymentPolicy, PaymentPolicyCascadeLayers, PaymentPolicySource, PaymentScheduleEntryType, ResolvedPaymentPolicy, } from "./payment-policy.js";
33
33
  export { computePaymentSchedule, isPaymentPolicyEmpty, noDepositPolicy, normalizePaymentPolicy, policyShouldRequireFullPayment, resolveEffectivePaymentPolicy, } from "./payment-policy.js";
34
34
  export { createPaymentPolicyCascade, type PaymentPolicyCascade, type PaymentPolicyCascadeOptions, type PaymentPolicyCascadeReaders, readPolicySourceFromInternalNotes, stampPolicySourceOnBooking, } from "./payment-policy-cascade.js";
35
- export { type BookingScheduleRoutesOptions, createBookingScheduleAdminRoutes, createBookingScheduleHonoExtension, createBookingScheduleVoyantRuntime, createPaymentPolicyPublicRoutes, generatePaymentScheduleForBooking, type PaymentPolicyEntityContext, } from "./payment-schedule/routes.js";
35
+ export { type BookingScheduleRoutesOptions, createBookingScheduleAdminRoutes, createBookingScheduleApiExtension, createBookingScheduleVoyantRuntime, createPaymentPolicyPublicRoutes, generatePaymentScheduleForBooking, type PaymentPolicyEntityContext, } from "./payment-schedule/routes.js";
36
36
  export { buildFinanceRouteRuntime, FINANCE_ROUTE_RUNTIME_CONTAINER_KEY, type FinanceRouteRuntime, type FinanceRuntimeOptions, } from "./route-runtime.js";
37
37
  export { bookingsCreateExtension } from "./routes-booking-create.js";
38
38
  export { createFinanceAdminDocumentRoutes, type FinanceDocumentRouteOptions, } from "./routes-documents.js";
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ export const financeModule = {
30
30
  linkable: financeLinkable,
31
31
  requiresTransactionalDb: true,
32
32
  };
33
- export function createFinanceHonoModule(options = {}) {
33
+ export function createFinanceApiModule(options = {}) {
34
34
  const adminRoutes = stampOpenApiRegistryApiId(new OpenAPIHono()
35
35
  .route("/", financeRoutes)
36
36
  .route("/", createFinanceCheckoutAdminRoutes(options))
@@ -63,9 +63,9 @@ export function createFinanceHonoModule(options = {}) {
63
63
  publicRoutes,
64
64
  };
65
65
  }
66
- export const financeHonoModule = createFinanceHonoModule();
66
+ export const financeApiModule = createFinanceApiModule();
67
67
  export const createFinanceVoyantRuntime = defineGraphRuntimeFactory(async ({ api, getPort, getPorts, hasPort }) => {
68
- const configured = createFinanceHonoModule(createFinanceRuntime(await getPort(financeHostRuntimePort), await getPort(financeNotificationsRuntimePort), hasPort(financeCheckoutPaymentStartersRuntimePort)
68
+ const configured = createFinanceApiModule(createFinanceRuntime(await getPort(financeHostRuntimePort), await getPort(financeNotificationsRuntimePort), hasPort(financeCheckoutPaymentStartersRuntimePort)
69
69
  ? await getPort(financeCheckoutPaymentStartersRuntimePort)
70
70
  : undefined, await getPorts(financeInvoiceSettlementPollerRuntimePort)));
71
71
  const bootstrap = configured.module.bootstrap;
@@ -87,13 +87,13 @@ export const createFinanceVoyantRuntime = defineGraphRuntimeFactory(async ({ api
87
87
  return selected;
88
88
  });
89
89
  export { buildPaidBookingCancellationSettlementNote, closeTerminalBookingPaymentSchedules, financeBookingLifecycle, recordPaidBookingCancellationSettlement, } from "./booking-lifecycle.js";
90
- export { computeBookingItemTaxLine, createBookingTaxHonoExtension, createBookingTaxRoutes, createBookingTaxVoyantRuntime, loadProductTaxFacts, matchesTaxPolicyCondition, mountBookingTaxRoutes, resolveBookingSellTaxRate, } from "./booking-tax.js";
90
+ export { computeBookingItemTaxLine, createBookingTaxApiExtension, createBookingTaxRoutes, createBookingTaxVoyantRuntime, loadProductTaxFacts, matchesTaxPolicyCondition, mountBookingTaxRoutes, resolveBookingSellTaxRate, } from "./booking-tax.js";
91
91
  export { resolveStoredDocumentDownload, } from "./document-download.js";
92
- export { createInvoiceFxHonoExtension, createInvoiceFxRoutes, createVoyantDataFxExchangeRateResolver, mountInvoiceFxRoutes, resolveInvoiceFxContext, resolveInvoiceFxSettingsOrDefault, } from "./invoice-fx.js";
92
+ export { createInvoiceFxApiExtension, createInvoiceFxRoutes, createVoyantDataFxExchangeRateResolver, mountInvoiceFxRoutes, resolveInvoiceFxContext, resolveInvoiceFxSettingsOrDefault, } from "./invoice-fx.js";
93
93
  export { createOrderPaymentSessions, } from "./order-payment-sessions.js";
94
94
  export { computePaymentSchedule, isPaymentPolicyEmpty, noDepositPolicy, normalizePaymentPolicy, policyShouldRequireFullPayment, resolveEffectivePaymentPolicy, } from "./payment-policy.js";
95
95
  export { createPaymentPolicyCascade, readPolicySourceFromInternalNotes, stampPolicySourceOnBooking, } from "./payment-policy-cascade.js";
96
- export { createBookingScheduleAdminRoutes, createBookingScheduleHonoExtension, createBookingScheduleVoyantRuntime, createPaymentPolicyPublicRoutes, generatePaymentScheduleForBooking, } from "./payment-schedule/routes.js";
96
+ export { createBookingScheduleAdminRoutes, createBookingScheduleApiExtension, createBookingScheduleVoyantRuntime, createPaymentPolicyPublicRoutes, generatePaymentScheduleForBooking, } from "./payment-schedule/routes.js";
97
97
  export { buildFinanceRouteRuntime, FINANCE_ROUTE_RUNTIME_CONTAINER_KEY, } from "./route-runtime.js";
98
98
  export { bookingsCreateExtension } from "./routes-booking-create.js";
99
99
  export { createFinanceAdminDocumentRoutes, } from "./routes-documents.js";
@@ -1,5 +1,5 @@
1
1
  import type { ModuleContainer } from "@voyant-travel/core";
2
- import type { HonoExtension } from "@voyant-travel/hono/module";
2
+ import type { ApiExtension } from "@voyant-travel/hono/module";
3
3
  import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
4
4
  import type { Hono } from "hono";
5
5
  import type { Env } from "./routes-shared.js";
@@ -129,5 +129,5 @@ export declare function createInvoiceFxRoutes(options?: InvoiceFxRouteOptions):
129
129
  };
130
130
  }, "/", "/invoice-fx-rate">;
131
131
  export declare function mountInvoiceFxRoutes(hono: Hono, options?: InvoiceFxRouteOptions): Hono;
132
- export declare function createInvoiceFxHonoExtension(options?: InvoiceFxRouteOptions): HonoExtension;
132
+ export declare function createInvoiceFxApiExtension(options?: InvoiceFxRouteOptions): ApiExtension;
133
133
  export {};
@@ -174,7 +174,7 @@ export function mountInvoiceFxRoutes(hono, options = {}) {
174
174
  hono.route("/v1/admin/finance", createInvoiceFxRoutes(options));
175
175
  return hono;
176
176
  }
177
- export function createInvoiceFxHonoExtension(options = {}) {
177
+ export function createInvoiceFxApiExtension(options = {}) {
178
178
  const extension = {
179
179
  name: "finance.invoice-fx",
180
180
  module: "finance",
@@ -0,0 +1,69 @@
1
+ /** Exact approval and idempotency orchestration for invoice issue Tools. */
2
+ import { type ActionLedgerCapabilityAccessResult, type ActionLedgerRequestContextValues, actionLedgerService, type BuildActionLedgerApprovedExecutionFieldsInput, requestActionLedgerApproval } from "@voyant-travel/action-ledger";
3
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
4
+ import type { CreateInvoiceFromBookingInput } from "./service.js";
5
+ export declare const FINANCE_INVOICE_ISSUE_CAPABILITY: {
6
+ readonly id: "finance:invoice-issue-from-booking";
7
+ readonly version: "v1";
8
+ readonly resource: "finance";
9
+ readonly action: "write";
10
+ readonly risk: "high";
11
+ readonly ledgerPolicy: "required";
12
+ readonly approvalPolicy: "required";
13
+ readonly reversible: false;
14
+ readonly allowedActorTypes: readonly ["staff", "system"];
15
+ readonly requiredGrants: readonly [{
16
+ readonly resource: "finance";
17
+ readonly action: "write";
18
+ }, {
19
+ readonly resource: "bookings";
20
+ readonly action: "read";
21
+ }];
22
+ };
23
+ export declare const FINANCE_INVOICE_ISSUE_APPROVAL_POLICY = "finance-invoice-issue-approval-v1";
24
+ export declare const FINANCE_INVOICE_ISSUE_ACTION_NAME = "finance.invoice.issue_from_booking";
25
+ export declare const FINANCE_INVOICE_ISSUE_TOOL_NAME = "finance.issue_invoice_from_booking";
26
+ export interface FinanceInvoiceIssueAuthorizationInput {
27
+ db: PostgresJsDatabase;
28
+ commandInput: CreateInvoiceFromBookingInput;
29
+ actor?: string | null;
30
+ callerType?: string | null;
31
+ scopes?: readonly string[] | null;
32
+ isInternalRequest?: boolean | null;
33
+ requestContext: ActionLedgerRequestContextValues;
34
+ approvalId?: string | null;
35
+ idempotencyKey?: string | null;
36
+ }
37
+ export type FinanceInvoiceIssueAuthorizationResult = {
38
+ status: "authorized";
39
+ access: ActionLedgerCapabilityAccessResult;
40
+ approvedAction: BuildActionLedgerApprovedExecutionFieldsInput;
41
+ } | {
42
+ status: "approval_required";
43
+ access: ActionLedgerCapabilityAccessResult;
44
+ requestedAction: Awaited<ReturnType<typeof requestActionLedgerApproval>>["requestedAction"];
45
+ approval: Awaited<ReturnType<typeof requestActionLedgerApproval>>["approval"];
46
+ replayed: boolean;
47
+ } | {
48
+ status: "already_executed";
49
+ access: ActionLedgerCapabilityAccessResult;
50
+ invoiceId: string;
51
+ } | {
52
+ status: "denied";
53
+ access: ActionLedgerCapabilityAccessResult;
54
+ } | {
55
+ status: "missing_idempotency_key";
56
+ access: ActionLedgerCapabilityAccessResult;
57
+ } | {
58
+ status: "idempotency_conflict";
59
+ access: ActionLedgerCapabilityAccessResult;
60
+ message: string;
61
+ existingActionId: string;
62
+ } | {
63
+ status: "invalid_approval";
64
+ access: ActionLedgerCapabilityAccessResult;
65
+ validation: Exclude<Awaited<ReturnType<typeof actionLedgerService.validateApprovedAction>>, {
66
+ ok: true;
67
+ }>;
68
+ };
69
+ export declare function authorizeFinanceInvoiceIssue(input: FinanceInvoiceIssueAuthorizationInput): Promise<FinanceInvoiceIssueAuthorizationResult>;
@@ -0,0 +1,156 @@
1
+ /** Exact approval and idempotency orchestration for invoice issue Tools. */
2
+ import { ActionLedgerIdempotencyConflictError, actionLedgerService, appendActionLedgerMutation, buildActionApprovalCommandFingerprint, evaluateActionLedgerApprovalRequirement, evaluateActionLedgerCapabilityAccess, mapActionLedgerRequestContext, requestActionLedgerApproval, } from "@voyant-travel/action-ledger";
3
+ export const FINANCE_INVOICE_ISSUE_CAPABILITY = {
4
+ id: "finance:invoice-issue-from-booking",
5
+ version: "v1",
6
+ resource: "finance",
7
+ action: "write",
8
+ risk: "high",
9
+ ledgerPolicy: "required",
10
+ approvalPolicy: "required",
11
+ reversible: false,
12
+ allowedActorTypes: ["staff", "system"],
13
+ requiredGrants: [
14
+ { resource: "finance", action: "write" },
15
+ { resource: "bookings", action: "read" },
16
+ ],
17
+ };
18
+ export const FINANCE_INVOICE_ISSUE_APPROVAL_POLICY = "finance-invoice-issue-approval-v1";
19
+ export const FINANCE_INVOICE_ISSUE_ACTION_NAME = "finance.invoice.issue_from_booking";
20
+ export const FINANCE_INVOICE_ISSUE_TOOL_NAME = "finance.issue_invoice_from_booking";
21
+ export async function authorizeFinanceInvoiceIssue(input) {
22
+ const access = evaluateActionLedgerCapabilityAccess({
23
+ definition: FINANCE_INVOICE_ISSUE_CAPABILITY,
24
+ actor: input.actor,
25
+ callerType: input.callerType,
26
+ scopes: input.scopes,
27
+ isInternalRequest: input.isInternalRequest,
28
+ });
29
+ if (!access.allowed) {
30
+ await appendActionLedgerMutation(input.db, {
31
+ context: input.requestContext,
32
+ actionName: FINANCE_INVOICE_ISSUE_ACTION_NAME,
33
+ actionVersion: FINANCE_INVOICE_ISSUE_CAPABILITY.version,
34
+ actionKind: "create",
35
+ status: "denied",
36
+ evaluatedRisk: access.evaluatedRisk,
37
+ targetType: "booking",
38
+ targetId: input.commandInput.bookingId,
39
+ routeOrToolName: FINANCE_INVOICE_ISSUE_TOOL_NAME,
40
+ capabilityId: access.capabilityId,
41
+ capabilityVersion: access.capabilityVersion,
42
+ authorizationSource: access.authorizationSource,
43
+ mutationDetail: {
44
+ summary: `Invoice issue denied: ${access.reason}`,
45
+ reversalKind: "none",
46
+ },
47
+ });
48
+ return { status: "denied", access };
49
+ }
50
+ const approvalRequirement = evaluateActionLedgerApprovalRequirement({
51
+ access,
52
+ conditionalApprovalRequired: true,
53
+ reasonCode: "invoice_issue_from_booking_requested_by_agent",
54
+ });
55
+ const fingerprint = await buildActionApprovalCommandFingerprint({
56
+ actionName: FINANCE_INVOICE_ISSUE_ACTION_NAME,
57
+ actionVersion: FINANCE_INVOICE_ISSUE_CAPABILITY.version,
58
+ targetType: "booking",
59
+ targetId: input.commandInput.bookingId,
60
+ commandInput: input.commandInput,
61
+ approvalPolicy: approvalRequirement.approvalPolicy,
62
+ capabilityId: access.capabilityId,
63
+ capabilityVersion: access.capabilityVersion,
64
+ evaluatedRisk: approvalRequirement.evaluatedRisk,
65
+ reasonCode: approvalRequirement.reasonCode,
66
+ });
67
+ if (input.approvalId) {
68
+ const principal = mapActionLedgerRequestContext(input.requestContext);
69
+ const validation = await actionLedgerService.validateApprovedAction(input.db, {
70
+ approvalId: input.approvalId,
71
+ actionName: FINANCE_INVOICE_ISSUE_ACTION_NAME,
72
+ actionVersion: FINANCE_INVOICE_ISSUE_CAPABILITY.version,
73
+ requestedActionKind: "create",
74
+ requestedActionStatus: "awaiting_approval",
75
+ targetType: "booking",
76
+ targetId: input.commandInput.bookingId,
77
+ routeOrToolName: FINANCE_INVOICE_ISSUE_TOOL_NAME,
78
+ principalType: principal.principalType,
79
+ principalId: principal.principalId,
80
+ idempotencyFingerprint: fingerprint,
81
+ executionActionKind: "create",
82
+ executionStatus: "succeeded",
83
+ });
84
+ if (!validation.ok) {
85
+ if (validation.reason === "already_executed" && validation.existingActionId) {
86
+ const existing = await actionLedgerService.getEntry(input.db, validation.existingActionId);
87
+ const resultRef = existing?.mutationDetail?.commandResultRef;
88
+ if (resultRef?.startsWith("invoice:")) {
89
+ return {
90
+ status: "already_executed",
91
+ access,
92
+ invoiceId: resultRef.slice("invoice:".length),
93
+ };
94
+ }
95
+ }
96
+ return { status: "invalid_approval", access, validation };
97
+ }
98
+ return {
99
+ status: "authorized",
100
+ access,
101
+ approvedAction: {
102
+ requestedActionId: validation.requestedAction.id,
103
+ approvalId: validation.approval.id,
104
+ idempotencyFingerprint: validation.idempotencyFingerprint,
105
+ },
106
+ };
107
+ }
108
+ if (!input.idempotencyKey)
109
+ return { status: "missing_idempotency_key", access };
110
+ try {
111
+ const result = await requestActionLedgerApproval(input.db, {
112
+ context: input.requestContext,
113
+ actionName: FINANCE_INVOICE_ISSUE_ACTION_NAME,
114
+ actionVersion: FINANCE_INVOICE_ISSUE_CAPABILITY.version,
115
+ actionKind: "create",
116
+ evaluatedRisk: approvalRequirement.evaluatedRisk,
117
+ targetType: "booking",
118
+ targetId: input.commandInput.bookingId,
119
+ routeOrToolName: FINANCE_INVOICE_ISSUE_TOOL_NAME,
120
+ capabilityId: access.capabilityId,
121
+ capabilityVersion: access.capabilityVersion,
122
+ authorizationSource: access.authorizationSource,
123
+ idempotencyScope: `${FINANCE_INVOICE_ISSUE_TOOL_NAME}:${input.commandInput.bookingId}`,
124
+ idempotencyKey: input.idempotencyKey,
125
+ idempotencyFingerprint: fingerprint,
126
+ mutationDetail: {
127
+ summary: "Invoice issue from booking awaiting approval",
128
+ reversalKind: "none",
129
+ },
130
+ approval: {
131
+ policyName: FINANCE_INVOICE_ISSUE_APPROVAL_POLICY,
132
+ policyVersion: FINANCE_INVOICE_ISSUE_CAPABILITY.version,
133
+ riskSnapshot: approvalRequirement.evaluatedRisk,
134
+ reasonCode: approvalRequirement.reasonCode,
135
+ },
136
+ });
137
+ return {
138
+ status: "approval_required",
139
+ access,
140
+ requestedAction: result.requestedAction,
141
+ approval: result.approval,
142
+ replayed: result.replayed,
143
+ };
144
+ }
145
+ catch (error) {
146
+ if (error instanceof ActionLedgerIdempotencyConflictError) {
147
+ return {
148
+ status: "idempotency_conflict",
149
+ access,
150
+ message: error.message,
151
+ existingActionId: error.existingActionId,
152
+ };
153
+ }
154
+ throw error;
155
+ }
156
+ }