@voyant-travel/finance 0.158.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.
@@ -0,0 +1,184 @@
1
+ /** Approval orchestration for issuing an invoice credit note as a refund. */
2
+ import { ActionLedgerIdempotencyConflictError, actionLedgerService, appendActionLedgerMutation, buildActionApprovalCommandFingerprint, evaluateActionLedgerApprovalRequirement, evaluateActionLedgerCapabilityAccess, mapActionLedgerRequestContext, requestActionLedgerApproval, } from "@voyant-travel/action-ledger";
3
+ import { financeService } from "./service.js";
4
+ export const FINANCE_REFUND_CAPABILITY = {
5
+ id: "finance:refund",
6
+ version: "v1",
7
+ resource: "invoice",
8
+ action: "refund",
9
+ risk: "critical",
10
+ ledgerPolicy: "required",
11
+ approvalPolicy: "required",
12
+ reversible: false,
13
+ allowedActorTypes: ["staff", "system"],
14
+ requiredGrants: [{ resource: "finance", action: "refund" }],
15
+ };
16
+ export const FINANCE_REFUND_APPROVAL_POLICY = "finance-credit-note-refund-approval-v1";
17
+ export const FINANCE_REFUND_ACTION_NAME = "finance.credit_note.issue_refund";
18
+ export const FINANCE_REFUND_ROUTE_OR_TOOL_NAME = "finance.issue_invoice_refund";
19
+ export async function authorizeFinanceRefund(input) {
20
+ const access = evaluateActionLedgerCapabilityAccess({
21
+ definition: FINANCE_REFUND_CAPABILITY,
22
+ actor: input.actor,
23
+ callerType: input.callerType,
24
+ scopes: input.scopes,
25
+ isInternalRequest: input.isInternalRequest,
26
+ });
27
+ if (!access.allowed) {
28
+ await appendActionLedgerMutation(input.db, {
29
+ context: input.requestContext,
30
+ actionName: FINANCE_REFUND_ACTION_NAME,
31
+ actionVersion: FINANCE_REFUND_CAPABILITY.version,
32
+ actionKind: "create",
33
+ status: "denied",
34
+ evaluatedRisk: access.evaluatedRisk,
35
+ targetType: "invoice",
36
+ targetId: input.invoiceId,
37
+ routeOrToolName: FINANCE_REFUND_ROUTE_OR_TOOL_NAME,
38
+ capabilityId: access.capabilityId,
39
+ capabilityVersion: access.capabilityVersion,
40
+ authorizationSource: access.authorizationSource,
41
+ mutationDetail: {
42
+ summary: `Invoice refund denied: ${access.reason}`,
43
+ reversalKind: "none",
44
+ },
45
+ });
46
+ return { status: "denied", access };
47
+ }
48
+ const approvalRequirement = evaluateActionLedgerApprovalRequirement({
49
+ access,
50
+ conditionalApprovalRequired: true,
51
+ reasonCode: "invoice_credit_note_refund_requested_by_agent",
52
+ });
53
+ const targetState = await loadInvoiceRefundTargetState(input.db, input.invoiceId);
54
+ const fingerprint = await buildActionApprovalCommandFingerprint({
55
+ actionName: FINANCE_REFUND_ACTION_NAME,
56
+ actionVersion: FINANCE_REFUND_CAPABILITY.version,
57
+ targetType: "invoice",
58
+ targetId: input.invoiceId,
59
+ commandInput: { command: input.commandInput, targetState },
60
+ approvalPolicy: approvalRequirement.approvalPolicy,
61
+ capabilityId: access.capabilityId,
62
+ capabilityVersion: access.capabilityVersion,
63
+ evaluatedRisk: approvalRequirement.evaluatedRisk,
64
+ reasonCode: approvalRequirement.reasonCode,
65
+ });
66
+ if (input.approvalId) {
67
+ const principal = mapActionLedgerRequestContext(input.requestContext);
68
+ const validation = await actionLedgerService.validateApprovedAction(input.db, {
69
+ approvalId: input.approvalId,
70
+ actionName: FINANCE_REFUND_ACTION_NAME,
71
+ actionVersion: FINANCE_REFUND_CAPABILITY.version,
72
+ requestedActionKind: "create",
73
+ requestedActionStatus: "awaiting_approval",
74
+ targetType: "invoice",
75
+ targetId: input.invoiceId,
76
+ routeOrToolName: FINANCE_REFUND_ROUTE_OR_TOOL_NAME,
77
+ principalType: principal.principalType,
78
+ principalId: principal.principalId,
79
+ idempotencyFingerprint: fingerprint,
80
+ executionActionKind: "create",
81
+ executionStatus: "succeeded",
82
+ });
83
+ if (!validation.ok) {
84
+ const requestedAction = validation.requestedAction;
85
+ const isExactReplay = validation.reason === "already_executed" &&
86
+ requestedAction !== undefined &&
87
+ requestedAction.idempotencyFingerprint === fingerprint &&
88
+ (!principal.principalType ||
89
+ !principal.principalId ||
90
+ (requestedAction.principalType === principal.principalType &&
91
+ requestedAction.principalId === principal.principalId));
92
+ if (isExactReplay && validation.existingActionId) {
93
+ const creditNoteId = await resolveExecutedRefundCreditNoteId(input.db, validation.existingActionId);
94
+ if (creditNoteId)
95
+ return { status: "already_executed", access, creditNoteId };
96
+ }
97
+ return { status: "invalid_approval", access, validation };
98
+ }
99
+ return {
100
+ status: "authorized",
101
+ access,
102
+ approvedAction: {
103
+ requestedActionId: validation.requestedAction.id,
104
+ approvalId: validation.approval.id,
105
+ idempotencyFingerprint: validation.idempotencyFingerprint,
106
+ },
107
+ };
108
+ }
109
+ if (!input.idempotencyKey)
110
+ return { status: "missing_idempotency_key", access };
111
+ try {
112
+ const result = await requestActionLedgerApproval(input.db, {
113
+ context: input.requestContext,
114
+ actionName: FINANCE_REFUND_ACTION_NAME,
115
+ actionVersion: FINANCE_REFUND_CAPABILITY.version,
116
+ actionKind: "create",
117
+ evaluatedRisk: approvalRequirement.evaluatedRisk,
118
+ targetType: "invoice",
119
+ targetId: input.invoiceId,
120
+ routeOrToolName: FINANCE_REFUND_ROUTE_OR_TOOL_NAME,
121
+ capabilityId: access.capabilityId,
122
+ capabilityVersion: access.capabilityVersion,
123
+ authorizationSource: access.authorizationSource,
124
+ idempotencyScope: `${FINANCE_REFUND_ROUTE_OR_TOOL_NAME}:${input.invoiceId}`,
125
+ idempotencyKey: input.idempotencyKey,
126
+ idempotencyFingerprint: fingerprint,
127
+ mutationDetail: {
128
+ summary: "Invoice credit-note refund awaiting approval",
129
+ reversalKind: "none",
130
+ },
131
+ approval: {
132
+ policyName: FINANCE_REFUND_APPROVAL_POLICY,
133
+ policyVersion: FINANCE_REFUND_CAPABILITY.version,
134
+ riskSnapshot: approvalRequirement.evaluatedRisk,
135
+ reasonCode: approvalRequirement.reasonCode,
136
+ },
137
+ });
138
+ return {
139
+ status: "approval_required",
140
+ access,
141
+ requestedAction: result.requestedAction,
142
+ approval: result.approval,
143
+ replayed: result.replayed,
144
+ };
145
+ }
146
+ catch (error) {
147
+ if (error instanceof ActionLedgerIdempotencyConflictError) {
148
+ return {
149
+ status: "idempotency_conflict",
150
+ access,
151
+ message: error.message,
152
+ existingActionId: error.existingActionId,
153
+ };
154
+ }
155
+ throw error;
156
+ }
157
+ }
158
+ export function parseCreditNoteCommandResultRef(resultRef) {
159
+ const prefix = "credit_note:";
160
+ if (!resultRef?.startsWith(prefix))
161
+ return null;
162
+ const creditNoteId = resultRef.slice(prefix.length).trim();
163
+ return creditNoteId || null;
164
+ }
165
+ export async function resolveExecutedRefundCreditNoteId(db, existingActionId) {
166
+ const existing = await actionLedgerService.getEntry(db, existingActionId);
167
+ return parseCreditNoteCommandResultRef(existing?.mutationDetail?.commandResultRef ?? null);
168
+ }
169
+ async function loadInvoiceRefundTargetState(db, invoiceId) {
170
+ const invoice = await financeService.getInvoiceById(db, invoiceId);
171
+ if (!invoice)
172
+ return { exists: false };
173
+ return {
174
+ exists: true,
175
+ status: invoice.status,
176
+ invoiceType: invoice.invoiceType,
177
+ invoiceNumber: invoice.invoiceNumber,
178
+ currency: invoice.currency,
179
+ totalCents: invoice.totalCents,
180
+ paidCents: invoice.paidCents,
181
+ balanceDueCents: invoice.balanceDueCents,
182
+ updatedAt: invoice.updatedAt.toISOString(),
183
+ };
184
+ }
@@ -155,83 +155,11 @@ financeInvoiceIssueRoutes
155
155
  .openapi(issueInvoiceFromBookingRoute, async (c) => {
156
156
  const input = c.req.valid("json");
157
157
  const db = c.get("db");
158
- const [{ bookingItems, bookings }, { bookingPaymentSchedules }, { and, asc, eq }, { issueInvoiceFromBooking, issueProformaFromBooking },] = await Promise.all([
159
- import("@voyant-travel/bookings/schema"),
160
- import("./schema.js"),
161
- import("drizzle-orm"),
162
- import("./service-issue.js"),
163
- ]);
164
- const [booking] = await db
165
- .select()
166
- .from(bookings)
167
- .where(eq(bookings.id, input.bookingId))
168
- .limit(1);
169
- if (!booking) {
170
- return c.json({ error: "Booking not found" }, 404);
171
- }
172
- const items = await db
173
- .select()
174
- .from(bookingItems)
175
- .where(eq(bookingItems.bookingId, booking.id))
176
- .orderBy(asc(bookingItems.createdAt), asc(bookingItems.id));
177
- const [paymentSchedule] = input.bookingPaymentScheduleId
178
- ? await db
179
- .select()
180
- .from(bookingPaymentSchedules)
181
- .where(and(eq(bookingPaymentSchedules.id, input.bookingPaymentScheduleId), eq(bookingPaymentSchedules.bookingId, booking.id)))
182
- .limit(1)
183
- : [];
184
- if (input.bookingPaymentScheduleId && !paymentSchedule) {
185
- return c.json({ error: "Booking payment schedule not found" }, 404);
186
- }
187
158
  const runtime = getFinanceRouteRuntime(c);
188
- const issuer = input.invoiceType === "proforma" ? issueProformaFromBooking : issueInvoiceFromBooking;
189
- let row;
159
+ const { issueInvoiceFromBookingCommand } = await import("./service-issue.js");
160
+ let outcome;
190
161
  try {
191
- row = await issuer(db, input, {
192
- booking: {
193
- id: booking.id,
194
- bookingNumber: booking.bookingNumber,
195
- personId: booking.personId,
196
- organizationId: booking.organizationId,
197
- startDate: booking.startDate,
198
- endDate: booking.endDate,
199
- sellCurrency: booking.sellCurrency,
200
- baseCurrency: booking.baseCurrency,
201
- fxRateSetId: booking.fxRateSetId,
202
- sellAmountCents: booking.sellAmountCents,
203
- baseSellAmountCents: booking.baseSellAmountCents,
204
- },
205
- paymentSchedule: paymentSchedule
206
- ? {
207
- id: paymentSchedule.id,
208
- bookingId: paymentSchedule.bookingId,
209
- bookingItemId: paymentSchedule.bookingItemId,
210
- scheduleType: paymentSchedule.scheduleType,
211
- dueDate: paymentSchedule.dueDate,
212
- currency: paymentSchedule.currency,
213
- amountCents: paymentSchedule.amountCents,
214
- }
215
- : null,
216
- items: items.map((item) => ({
217
- id: item.id,
218
- title: item.title,
219
- productId: item.productId,
220
- productName: item.productNameSnapshot,
221
- productNameSnapshot: item.productNameSnapshot,
222
- optionNameSnapshot: item.optionNameSnapshot,
223
- unitNameSnapshot: item.unitNameSnapshot,
224
- departureLabelSnapshot: item.departureLabelSnapshot,
225
- startDate: item.serviceDate ?? item.startsAt,
226
- serviceDate: item.serviceDate,
227
- startsAt: item.startsAt,
228
- endDate: item.endsAt ?? item.serviceDate,
229
- endsAt: item.endsAt,
230
- quantity: item.quantity,
231
- unitSellAmountCents: item.unitSellAmountCents,
232
- totalSellAmountCents: item.totalSellAmountCents,
233
- })),
234
- }, {
162
+ outcome = await issueInvoiceFromBookingCommand(db, input, {
235
163
  ...(runtime ?? {}),
236
164
  actionLedgerContext: getActionLedgerRequestContext(c),
237
165
  actionLedgerAuthorizationSource: "finance.invoice.from_booking.route",
@@ -253,7 +181,13 @@ financeInvoiceIssueRoutes
253
181
  }
254
182
  throw error;
255
183
  }
256
- return c.json({ data: row }, 201);
184
+ if (outcome.status === "booking_not_found") {
185
+ return c.json({ error: "Booking not found" }, 404);
186
+ }
187
+ if (outcome.status === "payment_schedule_not_found") {
188
+ return c.json({ error: "Booking payment schedule not found" }, 404);
189
+ }
190
+ return c.json({ data: outcome.invoice }, 201);
257
191
  })
258
192
  .openapi(convertProformaToInvoiceRoute, async (c) => {
259
193
  const { convertProformaToInvoice } = await import("./service-issue.js");
@@ -24,6 +24,7 @@ export declare const routeIdempotencyKey: (scope: string, options?: {
24
24
  organizationId?: string | null;
25
25
  callerType?: "session" | "api_key" | "internal" | "agent" | "workflow";
26
26
  actor?: string;
27
+ scopes?: string[] | null;
27
28
  apiTokenId?: string;
28
29
  apiKeyId?: string;
29
30
  workflowRunId?: string;
@@ -20,6 +20,7 @@ export type Env = {
20
20
  organizationId?: string | null;
21
21
  callerType?: "session" | "api_key" | "internal" | "agent" | "workflow";
22
22
  actor?: string;
23
+ scopes?: string[] | null;
23
24
  apiTokenId?: string;
24
25
  apiKeyId?: string;
25
26
  workflowRunId?: string;
@@ -75,6 +75,16 @@ export declare function buildPaymentDeleteActionLedgerInput(context: ActionLedge
75
75
  }): BuildActionLedgerMutationInput;
76
76
  export declare function buildInvoiceIssuedActionLedgerInput(context: ActionLedgerRequestContextValues, input: InvoiceIssuedLedgerInput, options?: {
77
77
  authorizationSource?: string | null;
78
+ actionName?: string | null;
79
+ routeOrToolName?: string | null;
80
+ capabilityId?: string | null;
81
+ capabilityVersion?: string | null;
82
+ evaluatedRisk?: "low" | "medium" | "high" | "critical" | null;
83
+ causationActionId?: string | null;
84
+ approvalId?: string | null;
85
+ idempotencyScope?: string | null;
86
+ idempotencyKey?: string | null;
87
+ idempotencyFingerprint?: string | null;
78
88
  }): Promise<BuildActionLedgerMutationInput>;
79
89
  export declare function buildInvoiceUpdateActionLedgerInput(context: ActionLedgerRequestContextValues, input: InvoiceUpdateLedgerInput, options?: {
80
90
  authorizationSource?: string | null;
@@ -93,6 +103,18 @@ export declare function buildInvoiceLineItemDeleteActionLedgerInput(context: Act
93
103
  }): BuildActionLedgerMutationInput;
94
104
  export declare function buildCreditNoteCreationActionLedgerInput(context: ActionLedgerRequestContextValues, input: CreateCreditNoteLedgerInput, options?: {
95
105
  authorizationSource?: string | null;
106
+ actionName?: string | null;
107
+ routeOrToolName?: string | null;
108
+ targetType?: string | null;
109
+ targetId?: string | null;
110
+ capabilityId?: string | null;
111
+ capabilityVersion?: string | null;
112
+ evaluatedRisk?: BuildActionLedgerMutationInput["evaluatedRisk"] | null;
113
+ causationActionId?: string | null;
114
+ approvalId?: string | null;
115
+ idempotencyScope?: string | null;
116
+ idempotencyKey?: string | null;
117
+ idempotencyFingerprint?: string | null;
96
118
  }): Promise<BuildActionLedgerMutationInput>;
97
119
  export declare function buildCreditNoteUpdateActionLedgerInput(context: ActionLedgerRequestContextValues, input: CreditNoteUpdateLedgerInput, options?: {
98
120
  authorizationSource?: string | null;
@@ -133,36 +133,42 @@ function getInvoiceLedgerTarget(invoice) {
133
133
  export async function buildInvoiceIssuedActionLedgerInput(context, input, options = {}) {
134
134
  const target = getInvoiceLedgerTarget(input.invoice);
135
135
  const invoiceTypeLabel = input.invoice.invoiceType === "proforma" ? "Proforma" : "Invoice";
136
+ const actionName = options.actionName ?? "finance.invoice.issue_from_booking";
136
137
  return {
137
138
  context,
138
- actionName: "finance.invoice.issue_from_booking",
139
+ actionName,
139
140
  actionVersion: "v1",
140
141
  actionKind: "create",
141
142
  status: "succeeded",
142
- evaluatedRisk: "high",
143
+ evaluatedRisk: options.evaluatedRisk ?? "high",
143
144
  targetType: target.type,
144
145
  targetId: target.id,
145
- routeOrToolName: "finance.invoice.issue_from_booking",
146
+ routeOrToolName: options.routeOrToolName ?? "finance.invoice.issue_from_booking",
147
+ capabilityId: options.capabilityId,
148
+ capabilityVersion: options.capabilityVersion,
146
149
  authorizationSource: options.authorizationSource ?? "finance.invoice.from_booking.route",
147
- idempotencyScope: `finance.booking:${input.invoice.bookingId}:invoice_issue`,
148
- idempotencyKey: input.invoice.invoiceNumber,
149
- idempotencyFingerprint: await buildIdempotencyFingerprint({
150
- actionName: "finance.invoice.issue_from_booking",
151
- actionVersion: "v1",
152
- targetType: target.type,
153
- targetId: target.id,
154
- commandInput: {
155
- invoiceId: input.invoice.id,
156
- invoiceNumber: input.invoice.invoiceNumber,
157
- invoiceType: input.invoice.invoiceType,
158
- bookingId: input.invoice.bookingId,
159
- totalCents: input.invoice.totalCents,
160
- currency: input.invoice.currency,
161
- status: input.invoice.status,
162
- issueDate: input.invoice.issueDate,
163
- dueDate: input.invoice.dueDate,
164
- },
165
- }),
150
+ causationActionId: options.causationActionId,
151
+ approvalId: options.approvalId,
152
+ idempotencyScope: options.idempotencyScope ?? `finance.booking:${input.invoice.bookingId}:invoice_issue`,
153
+ idempotencyKey: options.idempotencyKey ?? input.invoice.invoiceNumber,
154
+ idempotencyFingerprint: options.idempotencyFingerprint ??
155
+ (await buildIdempotencyFingerprint({
156
+ actionName,
157
+ actionVersion: "v1",
158
+ targetType: target.type,
159
+ targetId: target.id,
160
+ commandInput: {
161
+ invoiceId: input.invoice.id,
162
+ invoiceNumber: input.invoice.invoiceNumber,
163
+ invoiceType: input.invoice.invoiceType,
164
+ bookingId: input.invoice.bookingId,
165
+ totalCents: input.invoice.totalCents,
166
+ currency: input.invoice.currency,
167
+ status: input.invoice.status,
168
+ issueDate: input.invoice.issueDate,
169
+ dueDate: input.invoice.dueDate,
170
+ },
171
+ })),
166
172
  mutationDetail: {
167
173
  commandInputRef: `booking:${input.invoice.bookingId}:invoice_issue`,
168
174
  commandResultRef: `invoice:${input.invoice.id}`,
@@ -297,35 +303,43 @@ export function buildInvoiceLineItemDeleteActionLedgerInput(context, input, opti
297
303
  }
298
304
  export async function buildCreditNoteCreationActionLedgerInput(context, input, options = {}) {
299
305
  const target = getInvoiceLedgerTarget(input.invoice);
300
- const idempotencyKey = input.creditNote.creditNoteNumber;
306
+ const targetType = options.targetType ?? target.type;
307
+ const targetId = options.targetId ?? target.id;
308
+ const actionName = options.actionName ?? "finance.credit_note.create";
309
+ const idempotencyKey = options.idempotencyKey ?? input.creditNote.creditNoteNumber;
301
310
  return {
302
311
  context,
303
- actionName: "finance.credit_note.create",
304
- actionVersion: "v1",
312
+ actionName,
313
+ actionVersion: options.capabilityVersion ?? "v1",
305
314
  actionKind: "create",
306
315
  status: "succeeded",
307
- evaluatedRisk: "high",
308
- targetType: target.type,
309
- targetId: target.id,
310
- routeOrToolName: "finance.credit_note.create",
316
+ evaluatedRisk: options.evaluatedRisk ?? "high",
317
+ targetType,
318
+ targetId,
319
+ routeOrToolName: options.routeOrToolName ?? "finance.credit_note.create",
320
+ capabilityId: options.capabilityId ?? null,
321
+ capabilityVersion: options.capabilityVersion ?? null,
311
322
  authorizationSource: options.authorizationSource ?? "finance.credit_note.route",
312
- idempotencyScope: `finance.invoice:${input.invoice.id}:credit_note`,
323
+ causationActionId: options.causationActionId ?? null,
324
+ approvalId: options.approvalId ?? null,
325
+ idempotencyScope: options.idempotencyScope ?? `finance.invoice:${input.invoice.id}:credit_note`,
313
326
  idempotencyKey,
314
- idempotencyFingerprint: await buildIdempotencyFingerprint({
315
- actionName: "finance.credit_note.create",
316
- actionVersion: "v1",
317
- targetType: target.type,
318
- targetId: target.id,
319
- commandInput: {
320
- invoiceId: input.invoice.id,
321
- creditNoteId: input.creditNote.id,
322
- creditNoteNumber: input.creditNote.creditNoteNumber,
323
- amountCents: input.creditNote.amountCents,
324
- currency: input.creditNote.currency,
325
- status: input.creditNote.status,
326
- reason: input.creditNote.reason,
327
- },
328
- }),
327
+ idempotencyFingerprint: options.idempotencyFingerprint ??
328
+ (await buildIdempotencyFingerprint({
329
+ actionName,
330
+ actionVersion: options.capabilityVersion ?? "v1",
331
+ targetType,
332
+ targetId,
333
+ commandInput: {
334
+ invoiceId: input.invoice.id,
335
+ creditNoteId: input.creditNote.id,
336
+ creditNoteNumber: input.creditNote.creditNoteNumber,
337
+ amountCents: input.creditNote.amountCents,
338
+ currency: input.creditNote.currency,
339
+ status: input.creditNote.status,
340
+ reason: input.creditNote.reason,
341
+ },
342
+ })),
329
343
  mutationDetail: {
330
344
  commandInputRef: `invoice:${input.invoice.id}:credit_note`,
331
345
  commandResultRef: `credit_note:${input.creditNote.id}`,
@@ -34,6 +34,7 @@ export declare const bookingCreateSchema: z.ZodObject<{
34
34
  productId: z.ZodString;
35
35
  optionId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
36
36
  slotId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
37
+ availabilityHoldToken: z.ZodOptional<z.ZodString>;
37
38
  bookingNumber: z.ZodString;
38
39
  personId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
39
40
  organizationId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
@@ -240,6 +241,7 @@ export declare const bookingCreateSubSchema: z.ZodObject<{
240
241
  travelerIndexes: z.ZodNullable<z.ZodOptional<z.ZodArray<z.ZodNumber>>>;
241
242
  }, z.core.$strip>>>;
242
243
  slotId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
244
+ availabilityHoldToken: z.ZodOptional<z.ZodString>;
243
245
  sellAmountCentsOverride: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
244
246
  catalogSellAmountCents: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
245
247
  confirmedSellAmountCents: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
@@ -256,6 +256,8 @@ const bookingCreateBaseSchema = z.object({
256
256
  productId: z.string().min(1),
257
257
  optionId: z.string().optional().nullable(),
258
258
  slotId: z.string().optional().nullable(),
259
+ /** Pre-booking availability hold converted inside the create transaction. */
260
+ availabilityHoldToken: z.string().min(1).optional(),
259
261
  bookingNumber: z.string().min(1),
260
262
  personId: z.string().optional().nullable(),
261
263
  organizationId: z.string().optional().nullable(),
@@ -924,7 +926,7 @@ export async function createBooking(db, rawInput, options = {}) {
924
926
  contactAddressLine2: input.contactAddressLine2 ?? null,
925
927
  contactPostalCode: input.contactPostalCode ?? null,
926
928
  itemLines: normalizedItemLines,
927
- });
929
+ }, userId, { availabilityHoldToken: input.availabilityHoldToken });
928
930
  if (!booking) {
929
931
  // Caller gave us a product that doesn't resolve. Throw so drizzle
930
932
  // rolls back any writes the convert helper may have made.
@@ -76,6 +76,7 @@ export declare const dualCreateBookingSchema: z.ZodObject<{
76
76
  travelerIndexes: z.ZodNullable<z.ZodOptional<z.ZodArray<z.ZodNumber>>>;
77
77
  }, z.core.$strip>>>;
78
78
  slotId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
79
+ availabilityHoldToken: z.ZodOptional<z.ZodString>;
79
80
  sellAmountCentsOverride: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
80
81
  catalogSellAmountCents: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
81
82
  confirmedSellAmountCents: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
@@ -197,6 +198,7 @@ export declare const dualCreateBookingSchema: z.ZodObject<{
197
198
  travelerIndexes: z.ZodNullable<z.ZodOptional<z.ZodArray<z.ZodNumber>>>;
198
199
  }, z.core.$strip>>>;
199
200
  slotId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
201
+ availabilityHoldToken: z.ZodOptional<z.ZodString>;
200
202
  sellAmountCentsOverride: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
201
203
  catalogSellAmountCents: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
202
204
  confirmedSellAmountCents: z.ZodNullable<z.ZodOptional<z.ZodNumber>>;
@@ -459,6 +459,21 @@ export declare const financeInvoiceCreditNoteService: {
459
459
  generated: undefined;
460
460
  }, {}, {}>;
461
461
  }>, "where" | "orderBy">;
462
+ getCreditNoteById(db: PostgresJsDatabase, creditNoteId: string): Promise<{
463
+ id: string;
464
+ creditNoteNumber: string;
465
+ invoiceId: string;
466
+ status: "draft" | "issued" | "applied";
467
+ amountCents: number;
468
+ currency: string;
469
+ baseCurrency: string | null;
470
+ baseAmountCents: number | null;
471
+ fxRateSetId: string | null;
472
+ reason: string;
473
+ notes: string | null;
474
+ createdAt: Date;
475
+ updatedAt: Date;
476
+ } | null>;
462
477
  createCreditNote(db: PostgresJsDatabase, invoiceId: string, data: CreateCreditNoteInput, runtime?: FinanceServiceRuntime): Promise<{
463
478
  id: string;
464
479
  status: "draft" | "issued" | "applied";
@@ -48,6 +48,14 @@ export const financeInvoiceCreditNoteService = {
48
48
  .where(eq(creditNotes.invoiceId, invoiceId))
49
49
  .orderBy(desc(creditNotes.createdAt));
50
50
  },
51
+ async getCreditNoteById(db, creditNoteId) {
52
+ const [row] = await db
53
+ .select()
54
+ .from(creditNotes)
55
+ .where(eq(creditNotes.id, creditNoteId))
56
+ .limit(1);
57
+ return row ?? null;
58
+ },
51
59
  async createCreditNote(db, invoiceId, data, runtime = {}) {
52
60
  const [invoice] = await db.select().from(invoices).where(eq(invoices.id, invoiceId)).limit(1);
53
61
  if (!invoice) {
@@ -73,6 +81,18 @@ export const financeInvoiceCreditNoteService = {
73
81
  creditNote: row,
74
82
  }, {
75
83
  authorizationSource: runtime.actionLedgerAuthorizationSource,
84
+ actionName: runtime.actionLedgerActionName,
85
+ routeOrToolName: runtime.actionLedgerRouteOrToolName,
86
+ targetType: runtime.actionLedgerTargetType,
87
+ targetId: runtime.actionLedgerTargetId,
88
+ capabilityId: runtime.actionLedgerCapabilityId,
89
+ capabilityVersion: runtime.actionLedgerCapabilityVersion,
90
+ evaluatedRisk: runtime.actionLedgerEvaluatedRisk,
91
+ causationActionId: runtime.actionLedgerCausationActionId,
92
+ approvalId: runtime.actionLedgerApprovalId,
93
+ idempotencyScope: runtime.actionLedgerIdempotencyScope,
94
+ idempotencyKey: runtime.actionLedgerIdempotencyKey,
95
+ idempotencyFingerprint: runtime.actionLedgerIdempotencyFingerprint,
76
96
  }));
77
97
  }
78
98
  return row;
@@ -458,6 +458,21 @@ export declare const financeInvoiceService: {
458
458
  generated: undefined;
459
459
  }, {}, {}>;
460
460
  }>, "where" | "orderBy">;
461
+ getCreditNoteById(db: import("drizzle-orm/postgres-js").PostgresJsDatabase, creditNoteId: string): Promise<{
462
+ id: string;
463
+ creditNoteNumber: string;
464
+ invoiceId: string;
465
+ status: "draft" | "issued" | "applied";
466
+ amountCents: number;
467
+ currency: string;
468
+ baseCurrency: string | null;
469
+ baseAmountCents: number | null;
470
+ fxRateSetId: string | null;
471
+ reason: string;
472
+ notes: string | null;
473
+ createdAt: Date;
474
+ updatedAt: Date;
475
+ } | null>;
461
476
  createCreditNote(db: import("drizzle-orm/postgres-js").PostgresJsDatabase, invoiceId: string, data: import("./service-shared.js").CreateCreditNoteInput, runtime?: import("./service-shared.js").FinanceServiceRuntime): Promise<{
462
477
  id: string;
463
478
  status: "draft" | "issued" | "applied";
@@ -12,6 +12,14 @@ import { type CreateInvoiceFromBookingInput, type FinanceServiceRuntime, type In
12
12
  */
13
13
  export interface InvoiceIssueRuntime extends FinanceServiceRuntime {
14
14
  }
15
+ export type InvoiceFromBookingCommandOutcome = {
16
+ status: "issued";
17
+ invoice: typeof invoices.$inferSelect;
18
+ } | {
19
+ status: "booking_not_found";
20
+ } | {
21
+ status: "payment_schedule_not_found";
22
+ };
15
23
  interface ExistingConvertedInvoicePointer {
16
24
  id: string;
17
25
  invoiceNumber: string;
@@ -87,6 +95,12 @@ export interface InvoiceProformaConvertedEvent extends InvoiceIssuedEvent {
87
95
  proformaId: string;
88
96
  proformaInvoiceNumber: string;
89
97
  }
98
+ /**
99
+ * Package-owned invoice-from-booking composer shared by HTTP routes and Tools.
100
+ * It owns the cross-module projection read so transport adapters never reach
101
+ * into booking or finance tables themselves.
102
+ */
103
+ export declare function issueInvoiceFromBookingCommand(db: PostgresJsDatabase, input: CreateInvoiceFromBookingInput, runtime?: InvoiceIssueRuntime): Promise<InvoiceFromBookingCommandOutcome>;
90
104
  /**
91
105
  * Create + emit an invoice from a booking. Returns the persisted row
92
106
  * after flipping the status from `draft` to `issued`. Drafts shouldn't