@voyant-travel/finance 0.159.0 → 0.161.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
@@ -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
+ }
@@ -1,16 +1,276 @@
1
- import { defineToolContextContribution } from "@voyant-travel/tools";
1
+ import { buildActionLedgerApprovedExecutionFields, } from "@voyant-travel/action-ledger";
2
+ import { defineToolContextContribution, ToolError } from "@voyant-travel/tools";
3
+ import { authorizeFinanceInvoiceIssue, FINANCE_INVOICE_ISSUE_ACTION_NAME, FINANCE_INVOICE_ISSUE_CAPABILITY, FINANCE_INVOICE_ISSUE_TOOL_NAME, } from "./invoice-issue-authorization.js";
4
+ import { authorizeFinanceRefund, FINANCE_REFUND_ACTION_NAME, FINANCE_REFUND_CAPABILITY, FINANCE_REFUND_ROUTE_OR_TOOL_NAME, } from "./refund-authorization.js";
5
+ import { getActionLedgerRequestContext, getFinanceRouteRuntime } from "./routes-runtime.js";
2
6
  import { financeService } from "./service.js";
7
+ import { createBooking } from "./service-booking-create.js";
8
+ import { issueInvoiceFromBookingCommand } from "./service-issue.js";
3
9
  export * from "./tools.js";
4
10
  export const voyantToolContextContribution = defineToolContextContribution({
5
11
  context: ["finance"],
6
- contribute: ({ context }) => {
12
+ contribute: ({ context, request }) => {
13
+ const c = request;
7
14
  const db = context.db;
8
15
  return {
9
16
  finance: {
10
17
  listInvoices: (query) => financeService.listInvoices(db, query),
11
18
  getInvoiceById: (id) => financeService.getInvoiceById(db, id),
19
+ getFinanceAggregates: (query) => financeService.getFinanceAggregates(db, query),
12
20
  voidInvoice: (id, input) => financeService.voidInvoice(db, id, input),
21
+ createBooking: (input) => createBooking(db, input, {
22
+ userId: c.get("userId") ?? undefined,
23
+ runtime: {
24
+ ...getFinanceRouteRuntime(c),
25
+ actionLedgerContext: getActionLedgerRequestContext(c),
26
+ actionLedgerAuthorizationSource: "finance.booking_create.tool",
27
+ },
28
+ }),
29
+ async issueInvoiceFromBooking(input) {
30
+ const { command, idempotencyKey, approvalId } = input;
31
+ const requestContext = financeToolActionLedgerContext(c);
32
+ const authorization = await authorizeFinanceInvoiceIssue({
33
+ db,
34
+ commandInput: command,
35
+ actor: c.get("actor"),
36
+ callerType: c.get("callerType"),
37
+ scopes: c.get("scopes"),
38
+ isInternalRequest: c.get("isInternalRequest"),
39
+ requestContext,
40
+ approvalId: approvalId ?? null,
41
+ idempotencyKey,
42
+ });
43
+ if (authorization.status === "approval_required") {
44
+ return pendingApprovalResult(authorization);
45
+ }
46
+ if (authorization.status === "already_executed") {
47
+ const invoice = await financeService.getInvoiceById(db, authorization.invoiceId);
48
+ if (!invoice) {
49
+ throw new ToolError("The previously issued invoice was not found.", "NOT_FOUND", {
50
+ invoiceId: authorization.invoiceId,
51
+ });
52
+ }
53
+ return { status: "issued", invoice: toJsonValue(invoice), replayed: true };
54
+ }
55
+ if (authorization.status !== "authorized") {
56
+ throw financeInvoiceIssueAuthorizationError(authorization);
57
+ }
58
+ const approved = buildActionLedgerApprovedExecutionFields(authorization.approvedAction);
59
+ const outcome = await issueInvoiceFromBookingCommand(db, command, {
60
+ ...getFinanceRouteRuntime(c),
61
+ actionLedgerContext: requestContext,
62
+ actionLedgerAuthorizationSource: authorization.access.authorizationSource,
63
+ actionLedgerActionName: FINANCE_INVOICE_ISSUE_ACTION_NAME,
64
+ actionLedgerRouteOrToolName: FINANCE_INVOICE_ISSUE_TOOL_NAME,
65
+ actionLedgerCapabilityId: FINANCE_INVOICE_ISSUE_CAPABILITY.id,
66
+ actionLedgerCapabilityVersion: FINANCE_INVOICE_ISSUE_CAPABILITY.version,
67
+ actionLedgerEvaluatedRisk: FINANCE_INVOICE_ISSUE_CAPABILITY.risk,
68
+ actionLedgerCausationActionId: approved.causationActionId,
69
+ actionLedgerApprovalId: approved.approvalId,
70
+ actionLedgerIdempotencyScope: approved.idempotencyScope,
71
+ actionLedgerIdempotencyKey: approved.idempotencyKey,
72
+ actionLedgerIdempotencyFingerprint: approved.idempotencyFingerprint,
73
+ });
74
+ if (outcome.status !== "issued") {
75
+ const subject = outcome.status === "booking_not_found" ? "Booking" : "Booking payment schedule";
76
+ throw new ToolError(`${subject} was not found.`, "NOT_FOUND", { outcome });
77
+ }
78
+ return {
79
+ status: "issued",
80
+ invoice: toJsonValue(outcome.invoice),
81
+ replayed: false,
82
+ };
83
+ },
84
+ async issueInvoiceRefund(input) {
85
+ const command = {
86
+ creditNoteNumber: input.creditNoteNumber,
87
+ status: "issued",
88
+ amountCents: input.amountCents,
89
+ currency: input.currency,
90
+ baseCurrency: input.baseCurrency,
91
+ baseAmountCents: input.baseAmountCents,
92
+ fxRateSetId: input.fxRateSetId,
93
+ reason: input.reason,
94
+ notes: input.notes,
95
+ };
96
+ const requestContext = financeToolActionLedgerContext(c);
97
+ const authorization = await authorizeFinanceRefund({
98
+ db,
99
+ invoiceId: input.invoiceId,
100
+ commandInput: command,
101
+ actor: c.get("actor"),
102
+ callerType: c.get("callerType"),
103
+ scopes: c.get("scopes"),
104
+ isInternalRequest: c.get("isInternalRequest"),
105
+ requestContext,
106
+ approvalId: input.approvalId ?? null,
107
+ idempotencyKey: input.idempotencyKey,
108
+ });
109
+ if (authorization.status === "approval_required") {
110
+ return {
111
+ status: "approval_required",
112
+ requestedAction: {
113
+ id: authorization.requestedAction.id,
114
+ status: authorization.requestedAction.status,
115
+ actionName: authorization.requestedAction.actionName,
116
+ targetType: authorization.requestedAction.targetType,
117
+ targetId: authorization.requestedAction.targetId,
118
+ },
119
+ approval: {
120
+ id: authorization.approval.id,
121
+ status: authorization.approval.status,
122
+ requestedActionId: authorization.approval.requestedActionId,
123
+ policyName: authorization.approval.policyName,
124
+ policyVersion: authorization.approval.policyVersion,
125
+ riskSnapshot: authorization.approval.riskSnapshot,
126
+ reasonCode: authorization.approval.reasonCode,
127
+ expiresAt: toIsoString(authorization.approval.expiresAt),
128
+ createdAt: toIsoString(authorization.approval.createdAt),
129
+ },
130
+ replayed: authorization.replayed,
131
+ };
132
+ }
133
+ if (authorization.status === "already_executed") {
134
+ const creditNote = await financeService.getCreditNoteById(db, authorization.creditNoteId);
135
+ if (!creditNote) {
136
+ throw new ToolError("The previously issued credit note was not found.", "NOT_FOUND", {
137
+ creditNoteId: authorization.creditNoteId,
138
+ });
139
+ }
140
+ if (creditNote.invoiceId !== input.invoiceId || creditNote.status !== "issued") {
141
+ throw new ToolError("The previous refund result does not match this issued invoice credit note.", "INVALID_INPUT", { creditNoteId: authorization.creditNoteId, invoiceId: input.invoiceId });
142
+ }
143
+ return {
144
+ status: "issued",
145
+ creditNote: toJsonValue(creditNote),
146
+ replayed: true,
147
+ };
148
+ }
149
+ if (authorization.status !== "authorized") {
150
+ throw financeRefundAuthorizationError(authorization);
151
+ }
152
+ const approved = buildActionLedgerApprovedExecutionFields(authorization.approvedAction);
153
+ const creditNote = await financeService.createCreditNote(db, input.invoiceId, command, {
154
+ eventBus: c.get("eventBus"),
155
+ actionLedgerContext: requestContext,
156
+ actionLedgerAuthorizationSource: authorization.access.authorizationSource,
157
+ actionLedgerActionName: FINANCE_REFUND_ACTION_NAME,
158
+ actionLedgerRouteOrToolName: FINANCE_REFUND_ROUTE_OR_TOOL_NAME,
159
+ actionLedgerTargetType: "invoice",
160
+ actionLedgerTargetId: input.invoiceId,
161
+ actionLedgerCapabilityId: FINANCE_REFUND_CAPABILITY.id,
162
+ actionLedgerCapabilityVersion: FINANCE_REFUND_CAPABILITY.version,
163
+ actionLedgerEvaluatedRisk: FINANCE_REFUND_CAPABILITY.risk,
164
+ actionLedgerCausationActionId: approved.causationActionId,
165
+ actionLedgerApprovalId: approved.approvalId,
166
+ actionLedgerIdempotencyScope: approved.idempotencyScope,
167
+ actionLedgerIdempotencyKey: approved.idempotencyKey,
168
+ actionLedgerIdempotencyFingerprint: approved.idempotencyFingerprint,
169
+ });
170
+ if (!creditNote) {
171
+ throw new ToolError(`Invoice "${input.invoiceId}" was not found.`, "NOT_FOUND", {
172
+ invoiceId: input.invoiceId,
173
+ });
174
+ }
175
+ return {
176
+ status: "issued",
177
+ creditNote: toJsonValue(creditNote),
178
+ replayed: false,
179
+ };
180
+ },
13
181
  },
14
182
  };
15
183
  },
16
184
  });
185
+ function financeToolActionLedgerContext(c) {
186
+ return {
187
+ userId: c.get("userId") ?? null,
188
+ agentId: c.get("agentId") ?? null,
189
+ workflowPrincipalId: c.get("workflowPrincipalId") ?? null,
190
+ principalSubtype: c.get("principalSubtype") ?? null,
191
+ sessionId: c.get("sessionId") ?? null,
192
+ apiTokenId: c.get("apiTokenId") ?? c.get("apiKeyId") ?? null,
193
+ callerType: c.get("callerType") ?? null,
194
+ actor: c.get("actor") ?? null,
195
+ isInternalRequest: c.get("isInternalRequest") ?? false,
196
+ organizationId: c.get("organizationId") ?? null,
197
+ workflowRunId: c.get("workflowRunId") ?? null,
198
+ workflowStepId: c.get("workflowStepId") ?? null,
199
+ correlationId: c.req.header("x-correlation-id") ?? c.req.header("x-request-id") ?? null,
200
+ };
201
+ }
202
+ function financeRefundAuthorizationError(result) {
203
+ switch (result.status) {
204
+ case "denied":
205
+ return new ToolError("Invoice refund is not authorized.", "AUTHORIZATION_DENIED", {
206
+ reason: result.access.reason,
207
+ });
208
+ case "missing_idempotency_key":
209
+ return new ToolError("Invoice refund requires an idempotency key.", "INVALID_INPUT");
210
+ case "idempotency_conflict":
211
+ return new ToolError(result.message, "INVALID_INPUT", {
212
+ existingActionId: result.existingActionId,
213
+ });
214
+ case "invalid_approval":
215
+ return new ToolError("The approval does not authorize this exact invoice credit-note refund.", "INVALID_INPUT", {
216
+ reason: result.validation.reason,
217
+ approvalId: result.validation.approval?.id,
218
+ });
219
+ }
220
+ }
221
+ function pendingApprovalResult(input) {
222
+ return {
223
+ status: "approval_required",
224
+ requestedAction: {
225
+ id: input.requestedAction.id,
226
+ status: input.requestedAction.status,
227
+ actionName: input.requestedAction.actionName,
228
+ targetType: input.requestedAction.targetType,
229
+ targetId: input.requestedAction.targetId,
230
+ },
231
+ approval: {
232
+ id: input.approval.id,
233
+ status: input.approval.status,
234
+ requestedActionId: input.approval.requestedActionId,
235
+ policyName: input.approval.policyName,
236
+ policyVersion: input.approval.policyVersion,
237
+ riskSnapshot: input.approval.riskSnapshot,
238
+ reasonCode: input.approval.reasonCode ?? "approval_required",
239
+ expiresAt: toIsoString(input.approval.expiresAt),
240
+ createdAt: toIsoString(input.approval.createdAt),
241
+ },
242
+ replayed: input.replayed,
243
+ };
244
+ }
245
+ function financeInvoiceIssueAuthorizationError(result) {
246
+ switch (result.status) {
247
+ case "denied":
248
+ return new ToolError("Invoice issue is not authorized.", "AUTHORIZATION_DENIED", {
249
+ reason: result.access.reason,
250
+ });
251
+ case "missing_idempotency_key":
252
+ return new ToolError("Invoice issue requires an idempotency key.", "INVALID_INPUT");
253
+ case "idempotency_conflict":
254
+ return new ToolError(result.message, "INVALID_INPUT", {
255
+ existingActionId: result.existingActionId,
256
+ });
257
+ case "invalid_approval":
258
+ return new ToolError("The approval does not authorize this exact invoice issue command.", "INVALID_INPUT", { reason: result.validation.reason, approvalId: result.validation.approval?.id });
259
+ }
260
+ }
261
+ function toIsoString(value) {
262
+ if (!value)
263
+ return null;
264
+ return value instanceof Date ? value.toISOString() : value;
265
+ }
266
+ function toJsonValue(value) {
267
+ if (value instanceof Date)
268
+ return value.toISOString();
269
+ if (Array.isArray(value))
270
+ return value.map(toJsonValue);
271
+ if (typeof value !== "object" || value === null)
272
+ return value;
273
+ return Object.fromEntries(Object.entries(value)
274
+ .map(([key, nested]) => [key, toJsonValue(nested)])
275
+ .filter(([, nested]) => nested !== undefined));
276
+ }
@@ -0,0 +1,68 @@
1
+ /** Approval orchestration for issuing an invoice credit note as a refund. */
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
+ export declare const FINANCE_REFUND_CAPABILITY: {
5
+ readonly id: "finance:refund";
6
+ readonly version: "v1";
7
+ readonly resource: "invoice";
8
+ readonly action: "refund";
9
+ readonly risk: "critical";
10
+ readonly ledgerPolicy: "required";
11
+ readonly approvalPolicy: "required";
12
+ readonly reversible: false;
13
+ readonly allowedActorTypes: readonly ["staff", "system"];
14
+ readonly requiredGrants: readonly [{
15
+ readonly resource: "finance";
16
+ readonly action: "refund";
17
+ }];
18
+ };
19
+ export declare const FINANCE_REFUND_APPROVAL_POLICY = "finance-credit-note-refund-approval-v1";
20
+ export declare const FINANCE_REFUND_ACTION_NAME = "finance.credit_note.issue_refund";
21
+ export declare const FINANCE_REFUND_ROUTE_OR_TOOL_NAME = "finance.issue_invoice_refund";
22
+ export interface FinanceRefundAuthorizationInput {
23
+ db: PostgresJsDatabase;
24
+ invoiceId: string;
25
+ commandInput: unknown;
26
+ actor?: string | null;
27
+ callerType?: string | null;
28
+ scopes?: readonly string[] | null;
29
+ isInternalRequest?: boolean | null;
30
+ requestContext: ActionLedgerRequestContextValues;
31
+ approvalId?: string | null;
32
+ idempotencyKey?: string | null;
33
+ }
34
+ export type FinanceRefundAuthorizationResult = {
35
+ status: "authorized";
36
+ access: ActionLedgerCapabilityAccessResult;
37
+ approvedAction: BuildActionLedgerApprovedExecutionFieldsInput;
38
+ } | {
39
+ status: "approval_required";
40
+ access: ActionLedgerCapabilityAccessResult;
41
+ requestedAction: Awaited<ReturnType<typeof requestActionLedgerApproval>>["requestedAction"];
42
+ approval: Awaited<ReturnType<typeof requestActionLedgerApproval>>["approval"];
43
+ replayed: boolean;
44
+ } | {
45
+ status: "already_executed";
46
+ access: ActionLedgerCapabilityAccessResult;
47
+ creditNoteId: string;
48
+ } | {
49
+ status: "denied";
50
+ access: ActionLedgerCapabilityAccessResult;
51
+ } | {
52
+ status: "missing_idempotency_key";
53
+ access: ActionLedgerCapabilityAccessResult;
54
+ } | {
55
+ status: "idempotency_conflict";
56
+ access: ActionLedgerCapabilityAccessResult;
57
+ message: string;
58
+ existingActionId: string;
59
+ } | {
60
+ status: "invalid_approval";
61
+ access: ActionLedgerCapabilityAccessResult;
62
+ validation: Exclude<Awaited<ReturnType<typeof actionLedgerService.validateApprovedAction>>, {
63
+ ok: true;
64
+ }>;
65
+ };
66
+ export declare function authorizeFinanceRefund(input: FinanceRefundAuthorizationInput): Promise<FinanceRefundAuthorizationResult>;
67
+ export declare function parseCreditNoteCommandResultRef(resultRef: string | null): string | null;
68
+ export declare function resolveExecutedRefundCreditNoteId(db: PostgresJsDatabase, existingActionId: string): Promise<string | null>;