@voyant-travel/finance 0.169.2 → 0.171.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,2 @@
1
+ import type { FinanceAppApiRuntime } from "@voyant-travel/finance-contracts/app-api";
2
+ export declare function createFinanceAppApiRuntime(): FinanceAppApiRuntime;
@@ -0,0 +1,242 @@
1
+ import { bookings } from "@voyant-travel/bookings/schema";
2
+ import { FinanceAppApiNumberConflictError } from "@voyant-travel/finance-contracts/app-api";
3
+ import { and, asc, eq, sql } from "drizzle-orm";
4
+ import { invoiceExternalRefs, invoiceLineItems, invoiceNumberSeries, invoices, taxRegimes, } from "./schema.js";
5
+ import { financeService } from "./service.js";
6
+ import { buildInvoiceIssuedEvent } from "./service-issue.js";
7
+ import { InvoiceNumberConflictError, toRows } from "./service-shared.js";
8
+ export function createFinanceAppApiRuntime() {
9
+ return {
10
+ async getIssuanceDocument(db, documentId) {
11
+ const [invoice] = await db.select().from(invoices).where(eq(invoices.id, documentId)).limit(1);
12
+ if (!invoice || invoice.invoiceType === "credit_note")
13
+ return null;
14
+ const [[booking], [series], [taxRegime], lines, issuedEvent] = await Promise.all([
15
+ db.select().from(bookings).where(eq(bookings.id, invoice.bookingId)).limit(1),
16
+ invoice.seriesId
17
+ ? db
18
+ .select()
19
+ .from(invoiceNumberSeries)
20
+ .where(eq(invoiceNumberSeries.id, invoice.seriesId))
21
+ .limit(1)
22
+ : Promise.resolve([]),
23
+ invoice.taxRegimeId
24
+ ? db.select().from(taxRegimes).where(eq(taxRegimes.id, invoice.taxRegimeId)).limit(1)
25
+ : Promise.resolve([]),
26
+ db
27
+ .select()
28
+ .from(invoiceLineItems)
29
+ .where(eq(invoiceLineItems.invoiceId, documentId))
30
+ .orderBy(asc(invoiceLineItems.sortOrder)),
31
+ buildInvoiceIssuedEvent(db, invoice),
32
+ ]);
33
+ return {
34
+ id: invoice.id,
35
+ documentType: invoice.invoiceType,
36
+ number: invoice.invoiceNumber,
37
+ status: invoice.status,
38
+ booking: { id: invoice.bookingId, number: booking?.bookingNumber ?? null },
39
+ billing: {
40
+ name: [booking?.contactFirstName, booking?.contactLastName].filter(Boolean).join(" ") ||
41
+ "Customer",
42
+ email: booking?.contactEmail ?? null,
43
+ phone: booking?.contactPhone ?? null,
44
+ address: [booking?.contactAddressLine1, booking?.contactAddressLine2]
45
+ .filter(Boolean)
46
+ .join("\n") || null,
47
+ city: booking?.contactCity ?? null,
48
+ region: booking?.contactRegion ?? null,
49
+ country: booking?.contactCountry ?? null,
50
+ vatCode: booking?.contactTaxId ?? null,
51
+ registrationNumber: null,
52
+ },
53
+ currency: { document: invoice.currency, base: invoice.baseCurrency },
54
+ fx: buildHydratedFx(invoice, issuedEvent),
55
+ totals: {
56
+ subtotalCents: invoice.subtotalCents,
57
+ taxCents: invoice.taxCents,
58
+ totalCents: invoice.totalCents,
59
+ },
60
+ dates: { issuedOn: invoice.issueDate, dueOn: invoice.dueDate },
61
+ language: invoice.language,
62
+ taxRegime: taxRegime
63
+ ? {
64
+ id: taxRegime.id,
65
+ code: taxRegime.code,
66
+ name: taxRegime.name,
67
+ legalReference: taxRegime.legalReference,
68
+ specialRegime: !["standard", "reduced"].includes(taxRegime.code),
69
+ marginSchemeArticle311: taxRegime.code === "margin_scheme_art311",
70
+ }
71
+ : null,
72
+ series: series
73
+ ? {
74
+ id: series.id,
75
+ code: series.code,
76
+ name: series.name,
77
+ scope: series.scope,
78
+ }
79
+ : null,
80
+ allocation: {
81
+ required: Boolean(series?.externalProvider),
82
+ pending: invoice.status === "pending_external_allocation",
83
+ placeholderNumber: invoice.status === "pending_external_allocation" ? invoice.invoiceNumber : null,
84
+ },
85
+ lines: lines.map((line, index) => {
86
+ const projectedLine = issuedEvent.lineItems?.[index];
87
+ return {
88
+ id: line.id,
89
+ description: line.description,
90
+ quantity: line.quantity,
91
+ unitPriceCents: line.unitPriceCents,
92
+ totalCents: line.totalCents,
93
+ tax: {
94
+ ratePercent: projectedLine?.taxPercentage ?? line.taxRate,
95
+ name: projectedLine?.taxName ?? null,
96
+ regimeCode: projectedLine?.taxRegimeCode ?? null,
97
+ },
98
+ };
99
+ }),
100
+ };
101
+ },
102
+ async getExternalReference(db, documentId, provider) {
103
+ const [row] = await db
104
+ .select()
105
+ .from(invoiceExternalRefs)
106
+ .where(and(eq(invoiceExternalRefs.invoiceId, documentId), eq(invoiceExternalRefs.provider, provider)))
107
+ .limit(1);
108
+ return row ? mapExternalReference(row) : null;
109
+ },
110
+ async upsertExternalReference(db, documentId, provider, input) {
111
+ const locked = toRows(await db.execute(
112
+ // agent-quality: raw-sql reviewed -- identifiers are static and values are bound.
113
+ sql `SELECT invoice_number, series_id, status FROM invoices WHERE id = ${documentId} FOR UPDATE`))[0];
114
+ if (!locked)
115
+ return { status: "not_found" };
116
+ let allocationOutcome = "not_requested";
117
+ if (input.allocation) {
118
+ if (locked.status !== "pending_external_allocation") {
119
+ if (locked.invoice_number !== input.allocation.invoiceNumber) {
120
+ return {
121
+ status: "allocation_conflict",
122
+ currentNumber: locked.invoice_number,
123
+ currentStatus: locked.status,
124
+ };
125
+ }
126
+ allocationOutcome = "already_applied";
127
+ }
128
+ else {
129
+ const series = toRows(await db.execute(
130
+ // agent-quality: raw-sql reviewed -- identifiers are static and values are bound.
131
+ sql `SELECT external_provider FROM invoice_number_series WHERE id = ${locked.series_id} FOR UPDATE`))[0];
132
+ if (!series || series.external_provider !== provider) {
133
+ return {
134
+ status: "allocation_conflict",
135
+ currentNumber: locked.invoice_number,
136
+ currentStatus: locked.status,
137
+ };
138
+ }
139
+ try {
140
+ const allocation = await financeService.applyExternalInvoiceAllocation(db, documentId, {
141
+ invoiceNumber: input.allocation.invoiceNumber,
142
+ });
143
+ if (allocation.status === "not_found")
144
+ return { status: "not_found" };
145
+ if (allocation.status === "not_pending_external_allocation") {
146
+ return {
147
+ status: "allocation_conflict",
148
+ currentNumber: allocation.invoice.invoiceNumber,
149
+ currentStatus: allocation.invoice.status,
150
+ };
151
+ }
152
+ allocationOutcome = "applied";
153
+ }
154
+ catch (error) {
155
+ if (error instanceof InvoiceNumberConflictError) {
156
+ throw new FinanceAppApiNumberConflictError(input.allocation.invoiceNumber);
157
+ }
158
+ throw error;
159
+ }
160
+ }
161
+ }
162
+ const [existing] = await db
163
+ .select()
164
+ .from(invoiceExternalRefs)
165
+ .where(and(eq(invoiceExternalRefs.invoiceId, documentId), eq(invoiceExternalRefs.provider, provider)))
166
+ .limit(1);
167
+ const referenceOutcome = existing
168
+ ? referenceMatches(existing, input.reference)
169
+ ? "unchanged"
170
+ : "updated"
171
+ : "created";
172
+ const row = referenceOutcome === "unchanged"
173
+ ? existing
174
+ : await financeService.registerInvoiceExternalRef(db, documentId, {
175
+ provider,
176
+ ...input.reference,
177
+ });
178
+ if (!row)
179
+ return { status: "not_found" };
180
+ return {
181
+ status: "ok",
182
+ reference: mapExternalReference(row),
183
+ referenceOutcome,
184
+ allocationOutcome,
185
+ };
186
+ },
187
+ };
188
+ }
189
+ function buildHydratedFx(invoice, issuedEvent) {
190
+ if (!invoice.baseCurrency || invoice.baseCurrency === invoice.currency)
191
+ return null;
192
+ const persistedRate = derivePersistedFxRate(invoice);
193
+ return {
194
+ rateSetId: issuedEvent.fxRateSetId ?? invoice.fxRateSetId,
195
+ rate: issuedEvent.fxRate ?? persistedRate,
196
+ effectiveRate: issuedEvent.effectiveRate ?? persistedRate,
197
+ source: issuedEvent.fxRateSource ?? null,
198
+ quotedAt: issuedEvent.fxRateQuotedAt ?? null,
199
+ validUntil: issuedEvent.fxRateValidUntil ?? null,
200
+ commissionBasisPoints: issuedEvent.fxCommissionBps ?? null,
201
+ invoiceMention: issuedEvent.fxCommissionInvoiceMention ?? null,
202
+ };
203
+ }
204
+ function derivePersistedFxRate(invoice) {
205
+ const pairs = [
206
+ [invoice.baseTotalCents, invoice.totalCents],
207
+ [invoice.baseSubtotalCents, invoice.subtotalCents],
208
+ ];
209
+ for (const [baseAmount, documentAmount] of pairs) {
210
+ if (baseAmount != null && documentAmount > 0)
211
+ return baseAmount / documentAmount;
212
+ }
213
+ return null;
214
+ }
215
+ function mapExternalReference(row) {
216
+ return {
217
+ id: row.id,
218
+ documentId: row.invoiceId,
219
+ provider: row.provider,
220
+ externalId: row.externalId,
221
+ externalNumber: row.externalNumber,
222
+ externalUrl: row.externalUrl,
223
+ status: row.status,
224
+ metadata: isRecord(row.metadata) ? row.metadata : null,
225
+ syncedAt: row.syncedAt?.toISOString() ?? null,
226
+ syncError: row.syncError,
227
+ createdAt: row.createdAt.toISOString(),
228
+ updatedAt: row.updatedAt.toISOString(),
229
+ };
230
+ }
231
+ function referenceMatches(existing, input) {
232
+ return (existing.externalId === (input.externalId ?? null) &&
233
+ existing.externalNumber === (input.externalNumber ?? null) &&
234
+ existing.externalUrl === (input.externalUrl ?? null) &&
235
+ existing.status === (input.status ?? null) &&
236
+ JSON.stringify(existing.metadata) === JSON.stringify(input.metadata ?? null) &&
237
+ (existing.syncedAt?.toISOString() ?? null) === (input.syncedAt ?? null) &&
238
+ existing.syncError === (input.syncError ?? null));
239
+ }
240
+ function isRecord(value) {
241
+ return value !== null && typeof value === "object" && !Array.isArray(value);
242
+ }
@@ -94,6 +94,17 @@ export declare function createBookingTaxSettingsRoutes(options?: BookingTaxRoute
94
94
  } & {
95
95
  "/tax-settings": {
96
96
  $patch: {
97
+ input: {
98
+ json: {
99
+ taxPriceMode?: "inclusive" | "exclusive" | undefined;
100
+ taxPolicyProfileId?: string | null | undefined;
101
+ invoicingMode?: "direct" | "proforma-first" | undefined;
102
+ };
103
+ };
104
+ output: {};
105
+ outputFormat: string;
106
+ status: 409;
107
+ } | {
97
108
  input: {
98
109
  json: {
99
110
  taxPriceMode?: "inclusive" | "exclusive" | undefined;
@@ -110,17 +121,6 @@ export declare function createBookingTaxSettingsRoutes(options?: BookingTaxRoute
110
121
  };
111
122
  outputFormat: "json";
112
123
  status: 200;
113
- } | {
114
- input: {
115
- json: {
116
- taxPriceMode?: "inclusive" | "exclusive" | undefined;
117
- taxPolicyProfileId?: string | null | undefined;
118
- invoicingMode?: "direct" | "proforma-first" | undefined;
119
- };
120
- };
121
- output: {};
122
- outputFormat: string;
123
- status: 409;
124
124
  };
125
125
  };
126
126
  }, "/">;
@@ -929,10 +929,10 @@ export declare const initiatedCheckoutCollectionSchema: z.ZodObject<{
929
929
  idempotencyKey: z.ZodNullable<z.ZodOptional<z.ZodString>>;
930
930
  }, z.core.$strip>>>;
931
931
  targetType: z.ZodEnum<{
932
+ invoice: "invoice";
932
933
  other: "other";
933
934
  booking: "booking";
934
935
  order: "order";
935
- invoice: "invoice";
936
936
  booking_payment_schedule: "booking_payment_schedule";
937
937
  booking_guarantee: "booking_guarantee";
938
938
  flight_order: "flight_order";
@@ -1022,7 +1022,7 @@ export declare const initiatedCheckoutCollectionSchema: z.ZodObject<{
1022
1022
  idempotencyKey?: string | null | undefined;
1023
1023
  } | null;
1024
1024
  id: string;
1025
- targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
1025
+ targetType: "invoice" | "other" | "booking" | "order" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
1026
1026
  targetId: string | null;
1027
1027
  bookingId: string | null;
1028
1028
  invoiceId: string | null;
@@ -1049,7 +1049,7 @@ export declare const initiatedCheckoutCollectionSchema: z.ZodObject<{
1049
1049
  notes: string | null;
1050
1050
  }, {
1051
1051
  id: string;
1052
- targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
1052
+ targetType: "invoice" | "other" | "booking" | "order" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
1053
1053
  targetId: string | null;
1054
1054
  bookingId: string | null;
1055
1055
  invoiceId: string | null;
@@ -1306,10 +1306,10 @@ export declare const bootstrappedCheckoutCollectionSchema: z.ZodObject<{
1306
1306
  idempotencyKey: z.ZodNullable<z.ZodOptional<z.ZodString>>;
1307
1307
  }, z.core.$strip>>>;
1308
1308
  targetType: z.ZodEnum<{
1309
+ invoice: "invoice";
1309
1310
  other: "other";
1310
1311
  booking: "booking";
1311
1312
  order: "order";
1312
- invoice: "invoice";
1313
1313
  booking_payment_schedule: "booking_payment_schedule";
1314
1314
  booking_guarantee: "booking_guarantee";
1315
1315
  flight_order: "flight_order";
@@ -1399,7 +1399,7 @@ export declare const bootstrappedCheckoutCollectionSchema: z.ZodObject<{
1399
1399
  idempotencyKey?: string | null | undefined;
1400
1400
  } | null;
1401
1401
  id: string;
1402
- targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
1402
+ targetType: "invoice" | "other" | "booking" | "order" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
1403
1403
  targetId: string | null;
1404
1404
  bookingId: string | null;
1405
1405
  invoiceId: string | null;
@@ -1426,7 +1426,7 @@ export declare const bootstrappedCheckoutCollectionSchema: z.ZodObject<{
1426
1426
  notes: string | null;
1427
1427
  }, {
1428
1428
  id: string;
1429
- targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
1429
+ targetType: "invoice" | "other" | "booking" | "order" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
1430
1430
  targetId: string | null;
1431
1431
  bookingId: string | null;
1432
1432
  invoiceId: string | null;
@@ -36,6 +36,17 @@ declare function getPaymentSessionLedgerTarget(session: {
36
36
  export declare const financeActionLedgerRoutes: OpenAPIHono<Env, {
37
37
  [x: string]: {
38
38
  $get: {
39
+ input: {
40
+ param: {
41
+ [x: string]: string;
42
+ };
43
+ };
44
+ output: {
45
+ error: string;
46
+ };
47
+ outputFormat: "json";
48
+ status: 404;
49
+ } | {
39
50
  input: {
40
51
  param: {
41
52
  [x: string]: string;
@@ -47,7 +58,7 @@ export declare const financeActionLedgerRoutes: OpenAPIHono<Env, {
47
58
  occurredAt: string;
48
59
  actionName: string;
49
60
  actionVersion: string;
50
- actionKind: "reverse" | "execute" | "delete" | "update" | "read" | "create" | "approve" | "reject" | "compensate" | "duplicate";
61
+ actionKind: "reverse" | "execute" | "update" | "delete" | "read" | "create" | "approve" | "reject" | "compensate" | "duplicate";
51
62
  status: "failed" | "cancelled" | "expired" | "approved" | "requested" | "awaiting_approval" | "denied" | "succeeded" | "reversed" | "compensated" | "superseded";
52
63
  evaluatedRisk: "low" | "medium" | "high" | "critical";
53
64
  actorType: string | null;
@@ -89,7 +100,11 @@ export declare const financeActionLedgerRoutes: OpenAPIHono<Env, {
89
100
  };
90
101
  outputFormat: "json";
91
102
  status: 200;
92
- } | {
103
+ };
104
+ };
105
+ } & {
106
+ [x: string]: {
107
+ $get: {
93
108
  input: {
94
109
  param: {
95
110
  [x: string]: string;
@@ -100,11 +115,7 @@ export declare const financeActionLedgerRoutes: OpenAPIHono<Env, {
100
115
  };
101
116
  outputFormat: "json";
102
117
  status: 404;
103
- };
104
- };
105
- } & {
106
- [x: string]: {
107
- $get: {
118
+ } | {
108
119
  input: {
109
120
  param: {
110
121
  [x: string]: string;
@@ -116,7 +127,7 @@ export declare const financeActionLedgerRoutes: OpenAPIHono<Env, {
116
127
  occurredAt: string;
117
128
  actionName: string;
118
129
  actionVersion: string;
119
- actionKind: "reverse" | "execute" | "delete" | "update" | "read" | "create" | "approve" | "reject" | "compensate" | "duplicate";
130
+ actionKind: "reverse" | "execute" | "update" | "delete" | "read" | "create" | "approve" | "reject" | "compensate" | "duplicate";
120
131
  status: "failed" | "cancelled" | "expired" | "approved" | "requested" | "awaiting_approval" | "denied" | "succeeded" | "reversed" | "compensated" | "superseded";
121
132
  evaluatedRisk: "low" | "medium" | "high" | "critical";
122
133
  actorType: string | null;
@@ -158,17 +169,6 @@ export declare const financeActionLedgerRoutes: OpenAPIHono<Env, {
158
169
  };
159
170
  outputFormat: "json";
160
171
  status: 200;
161
- } | {
162
- input: {
163
- param: {
164
- [x: string]: string;
165
- };
166
- };
167
- output: {
168
- error: string;
169
- };
170
- outputFormat: "json";
171
- status: 404;
172
172
  };
173
173
  };
174
174
  }, "/">;