@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 +28 -5
- package/dist/booking-tax.d.ts +3 -3
- package/dist/booking-tax.js +2 -2
- package/dist/index.d.ts +8 -8
- package/dist/index.js +6 -6
- package/dist/invoice-fx.d.ts +2 -2
- package/dist/invoice-fx.js +1 -1
- package/dist/invoice-issue-authorization.d.ts +69 -0
- package/dist/invoice-issue-authorization.js +156 -0
- package/dist/mcp-runtime.js +262 -2
- package/dist/payment-schedule/routes.d.ts +3 -3
- package/dist/payment-schedule/routes.js +2 -2
- package/dist/refund-authorization.d.ts +68 -0
- package/dist/refund-authorization.js +184 -0
- package/dist/routes-booking-create.d.ts +2 -2
- package/dist/routes-invoice-issue.js +10 -76
- package/dist/routes-runtime.d.ts +1 -0
- package/dist/routes-shared.d.ts +1 -0
- package/dist/runtime.d.ts +3 -3
- package/dist/service-action-ledger-accounting.d.ts +22 -0
- package/dist/service-action-ledger-accounting.js +59 -45
- package/dist/service-booking-create.js +1 -1
- package/dist/service-invoice-credit-notes.d.ts +15 -0
- package/dist/service-invoice-credit-notes.js +20 -0
- package/dist/service-invoices.d.ts +15 -0
- package/dist/service-issue.d.ts +14 -0
- package/dist/service-issue.js +95 -2
- package/dist/service-shared.d.ts +12 -0
- package/dist/service.d.ts +15 -0
- package/dist/tools.d.ts +933 -3
- package/dist/tools.js +262 -11
- package/dist/voyant.js +88 -8
- package/package.json +9 -9
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 {
|
|
120
|
+
import { createBookingTaxApiExtension } from "@voyant-travel/finance/booking-tax"
|
|
98
121
|
|
|
99
122
|
createApp({
|
|
100
123
|
extensions: [
|
|
101
|
-
|
|
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
|
|
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
|
-
|
|
175
|
+
createFinanceApiModule,
|
|
153
176
|
createVoyantDataFxExchangeRateResolver,
|
|
154
177
|
} from "@voyant-travel/finance"
|
|
155
178
|
|
|
156
|
-
|
|
179
|
+
createFinanceApiModule({
|
|
157
180
|
invoiceFxSettings: {
|
|
158
181
|
baseCurrency: "RON",
|
|
159
182
|
fxCommissionBps: 200,
|
package/dist/booking-tax.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
-
import type {
|
|
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
|
|
133
|
-
export declare const createBookingTaxVoyantRuntime: import("@voyant-travel/core/project").VoyantGraphRuntimeFactory<
|
|
132
|
+
export declare function createBookingTaxApiExtension(options?: BookingTaxRouteOptions): ApiExtension;
|
|
133
|
+
export declare const createBookingTaxVoyantRuntime: import("@voyant-travel/core/project").VoyantGraphRuntimeFactory<ApiExtension>;
|
package/dist/booking-tax.js
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
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
|
|
21
|
+
export interface FinanceApiModuleOptions extends FinanceRuntimeOptions, PublicFinanceRouteOptions, CheckoutRoutesOptions, BookingTaxRouteOptions {
|
|
22
22
|
}
|
|
23
|
-
export declare function
|
|
24
|
-
export declare const
|
|
25
|
-
export declare const createFinanceVoyantRuntime: import("@voyant-travel/core/project").VoyantGraphRuntimeFactory<
|
|
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,
|
|
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 {
|
|
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,
|
|
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
|
|
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
|
|
66
|
+
export const financeApiModule = createFinanceApiModule();
|
|
67
67
|
export const createFinanceVoyantRuntime = defineGraphRuntimeFactory(async ({ api, getPort, getPorts, hasPort }) => {
|
|
68
|
-
const configured =
|
|
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,
|
|
90
|
+
export { computeBookingItemTaxLine, createBookingTaxApiExtension, createBookingTaxRoutes, createBookingTaxVoyantRuntime, loadProductTaxFacts, matchesTaxPolicyCondition, mountBookingTaxRoutes, resolveBookingSellTaxRate, } from "./booking-tax.js";
|
|
91
91
|
export { resolveStoredDocumentDownload, } from "./document-download.js";
|
|
92
|
-
export {
|
|
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,
|
|
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";
|
package/dist/invoice-fx.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ModuleContainer } from "@voyant-travel/core";
|
|
2
|
-
import type {
|
|
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
|
|
132
|
+
export declare function createInvoiceFxApiExtension(options?: InvoiceFxRouteOptions): ApiExtension;
|
|
133
133
|
export {};
|
package/dist/invoice-fx.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|