@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.
- package/README.md +23 -0
- 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/refund-authorization.d.ts +68 -0
- package/dist/refund-authorization.js +184 -0
- 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/service-action-ledger-accounting.d.ts +22 -0
- package/dist/service-action-ledger-accounting.js +59 -45
- package/dist/service-booking-create.d.ts +2 -0
- package/dist/service-booking-create.js +3 -1
- package/dist/service-bookings-dual-create.d.ts +2 -0
- 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-admin.d.ts +4 -0
- package/dist/voyant-admin.js +1 -0
- package/dist/voyant.js +84 -12
- package/package.json +10 -10
|
@@ -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
|
|
189
|
-
let
|
|
159
|
+
const { issueInvoiceFromBookingCommand } = await import("./service-issue.js");
|
|
160
|
+
let outcome;
|
|
190
161
|
try {
|
|
191
|
-
|
|
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
|
-
|
|
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");
|
package/dist/routes-runtime.d.ts
CHANGED
|
@@ -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;
|
package/dist/routes-shared.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
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
|
|
304
|
-
actionVersion: "v1",
|
|
312
|
+
actionName,
|
|
313
|
+
actionVersion: options.capabilityVersion ?? "v1",
|
|
305
314
|
actionKind: "create",
|
|
306
315
|
status: "succeeded",
|
|
307
|
-
evaluatedRisk: "high",
|
|
308
|
-
targetType
|
|
309
|
-
targetId
|
|
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
|
-
|
|
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:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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";
|
package/dist/service-issue.d.ts
CHANGED
|
@@ -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
|