@voyant-travel/distribution 0.109.8
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/LICENSE +201 -0
- package/README.md +42 -0
- package/dist/booking-extension.d.ts +168 -0
- package/dist/booking-extension.d.ts.map +1 -0
- package/dist/booking-extension.js +102 -0
- package/dist/channel-push/admin-routes.d.ts +31 -0
- package/dist/channel-push/admin-routes.d.ts.map +1 -0
- package/dist/channel-push/admin-routes.js +165 -0
- package/dist/channel-push/availability-push.d.ts +76 -0
- package/dist/channel-push/availability-push.d.ts.map +1 -0
- package/dist/channel-push/availability-push.js +236 -0
- package/dist/channel-push/booking-push-helpers.d.ts +36 -0
- package/dist/channel-push/booking-push-helpers.d.ts.map +1 -0
- package/dist/channel-push/booking-push-helpers.js +169 -0
- package/dist/channel-push/booking-push.d.ts +108 -0
- package/dist/channel-push/booking-push.d.ts.map +1 -0
- package/dist/channel-push/booking-push.js +335 -0
- package/dist/channel-push/boundary-sql.d.ts +23 -0
- package/dist/channel-push/boundary-sql.d.ts.map +1 -0
- package/dist/channel-push/boundary-sql.js +75 -0
- package/dist/channel-push/content-push.d.ts +60 -0
- package/dist/channel-push/content-push.d.ts.map +1 -0
- package/dist/channel-push/content-push.js +252 -0
- package/dist/channel-push/index.d.ts +15 -0
- package/dist/channel-push/index.d.ts.map +1 -0
- package/dist/channel-push/index.js +18 -0
- package/dist/channel-push/plugin.d.ts +18 -0
- package/dist/channel-push/plugin.d.ts.map +1 -0
- package/dist/channel-push/plugin.js +21 -0
- package/dist/channel-push/reconciler.d.ts +85 -0
- package/dist/channel-push/reconciler.d.ts.map +1 -0
- package/dist/channel-push/reconciler.js +179 -0
- package/dist/channel-push/subscriber.d.ts +40 -0
- package/dist/channel-push/subscriber.d.ts.map +1 -0
- package/dist/channel-push/subscriber.js +199 -0
- package/dist/channel-push/types.d.ts +43 -0
- package/dist/channel-push/types.d.ts.map +1 -0
- package/dist/channel-push/types.js +32 -0
- package/dist/channel-push/workflows.d.ts +56 -0
- package/dist/channel-push/workflows.d.ts.map +1 -0
- package/dist/channel-push/workflows.js +100 -0
- package/dist/external-refs/index.d.ts +11 -0
- package/dist/external-refs/index.d.ts.map +1 -0
- package/dist/external-refs/index.js +12 -0
- package/dist/external-refs/routes.d.ts +253 -0
- package/dist/external-refs/routes.d.ts.map +1 -0
- package/dist/external-refs/routes.js +52 -0
- package/dist/external-refs/schema.d.ts +251 -0
- package/dist/external-refs/schema.d.ts.map +1 -0
- package/dist/external-refs/schema.js +32 -0
- package/dist/external-refs/service.d.ts +82 -0
- package/dist/external-refs/service.d.ts.map +1 -0
- package/dist/external-refs/service.js +112 -0
- package/dist/external-refs/validation.d.ts +91 -0
- package/dist/external-refs/validation.d.ts.map +1 -0
- package/dist/external-refs/validation.js +40 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/interface-types.d.ts +128 -0
- package/dist/interface-types.d.ts.map +1 -0
- package/dist/interface-types.js +1 -0
- package/dist/interface.d.ts +10 -0
- package/dist/interface.d.ts.map +1 -0
- package/dist/interface.js +286 -0
- package/dist/rate-limit.d.ts +69 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +135 -0
- package/dist/routes/batch.d.ts +200 -0
- package/dist/routes/batch.d.ts.map +1 -0
- package/dist/routes/batch.js +52 -0
- package/dist/routes/env.d.ts +8 -0
- package/dist/routes/env.d.ts.map +1 -0
- package/dist/routes/env.js +1 -0
- package/dist/routes/inventory.d.ts +604 -0
- package/dist/routes/inventory.d.ts.map +1 -0
- package/dist/routes/inventory.js +138 -0
- package/dist/routes/settlements.d.ts +1649 -0
- package/dist/routes/settlements.d.ts.map +1 -0
- package/dist/routes/settlements.js +265 -0
- package/dist/routes.d.ts +3909 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +323 -0
- package/dist/schema-automation.d.ts +680 -0
- package/dist/schema-automation.d.ts.map +1 -0
- package/dist/schema-automation.js +76 -0
- package/dist/schema-core.d.ts +1674 -0
- package/dist/schema-core.d.ts.map +1 -0
- package/dist/schema-core.js +227 -0
- package/dist/schema-finance.d.ts +1372 -0
- package/dist/schema-finance.d.ts.map +1 -0
- package/dist/schema-finance.js +153 -0
- package/dist/schema-inventory.d.ts +855 -0
- package/dist/schema-inventory.d.ts.map +1 -0
- package/dist/schema-inventory.js +102 -0
- package/dist/schema-push-intents.d.ts +387 -0
- package/dist/schema-push-intents.d.ts.map +1 -0
- package/dist/schema-push-intents.js +77 -0
- package/dist/schema-relations.d.ts +95 -0
- package/dist/schema-relations.d.ts.map +1 -0
- package/dist/schema-relations.js +196 -0
- package/dist/schema-shared.d.ts +24 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +123 -0
- package/dist/schema.d.ts +9 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +8 -0
- package/dist/service/channels.d.ts +167 -0
- package/dist/service/channels.d.ts.map +1 -0
- package/dist/service/channels.js +305 -0
- package/dist/service/commercial.d.ts +385 -0
- package/dist/service/commercial.d.ts.map +1 -0
- package/dist/service/commercial.js +248 -0
- package/dist/service/helpers.d.ts +10 -0
- package/dist/service/helpers.d.ts.map +1 -0
- package/dist/service/helpers.js +7 -0
- package/dist/service/inventory.d.ts +193 -0
- package/dist/service/inventory.d.ts.map +1 -0
- package/dist/service/inventory.js +154 -0
- package/dist/service/settlement-policies.d.ts +325 -0
- package/dist/service/settlement-policies.d.ts.map +1 -0
- package/dist/service/settlement-policies.js +272 -0
- package/dist/service/settlements.d.ts +357 -0
- package/dist/service/settlements.d.ts.map +1 -0
- package/dist/service/settlements.js +319 -0
- package/dist/service/types.d.ts +60 -0
- package/dist/service/types.d.ts.map +1 -0
- package/dist/service/types.js +1 -0
- package/dist/service.d.ts +1418 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +17 -0
- package/dist/suppliers/index.d.ts +15 -0
- package/dist/suppliers/index.d.ts.map +1 -0
- package/dist/suppliers/index.js +23 -0
- package/dist/suppliers/routes.d.ts +1202 -0
- package/dist/suppliers/routes.d.ts.map +1 -0
- package/dist/suppliers/routes.js +290 -0
- package/dist/suppliers/schema.d.ts +1272 -0
- package/dist/suppliers/schema.d.ts.map +1 -0
- package/dist/suppliers/schema.js +219 -0
- package/dist/suppliers/service-aggregates.d.ts +23 -0
- package/dist/suppliers/service-aggregates.d.ts.map +1 -0
- package/dist/suppliers/service-aggregates.js +51 -0
- package/dist/suppliers/service-core.d.ts +89 -0
- package/dist/suppliers/service-core.d.ts.map +1 -0
- package/dist/suppliers/service-core.js +164 -0
- package/dist/suppliers/service-identity.d.ts +162 -0
- package/dist/suppliers/service-identity.d.ts.map +1 -0
- package/dist/suppliers/service-identity.js +101 -0
- package/dist/suppliers/service-operations.d.ts +1500 -0
- package/dist/suppliers/service-operations.d.ts.map +1 -0
- package/dist/suppliers/service-operations.js +157 -0
- package/dist/suppliers/service-shared.d.ts +45 -0
- package/dist/suppliers/service-shared.d.ts.map +1 -0
- package/dist/suppliers/service-shared.js +294 -0
- package/dist/suppliers/service.d.ts +41 -0
- package/dist/suppliers/service.d.ts.map +1 -0
- package/dist/suppliers/service.js +40 -0
- package/dist/suppliers/validation.d.ts +2 -0
- package/dist/suppliers/validation.d.ts.map +1 -0
- package/dist/suppliers/validation.js +1 -0
- package/dist/validation.d.ts +1371 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +445 -0
- package/dist/webhook-deliveries.d.ts +86 -0
- package/dist/webhook-deliveries.d.ts.map +1 -0
- package/dist/webhook-deliveries.js +296 -0
- package/package.json +71 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { bookingItems } from "@voyant-travel/bookings/schema";
|
|
2
|
+
import { channelBookingLinks, channelContracts, channelProductMappings, } from "@voyant-travel/distribution/schema";
|
|
3
|
+
import { and, asc, eq, lte, or, sql } from "drizzle-orm";
|
|
4
|
+
import { prepareOutboundEnvelope } from "../webhook-deliveries.js";
|
|
5
|
+
/**
|
|
6
|
+
* Read the compensation policy for a channel by walking
|
|
7
|
+
* `channel_contracts` (most-recent active contract wins). Returns
|
|
8
|
+
* `eventually-consistent` when no contract exists or no compensation
|
|
9
|
+
* key is set — that's the doc-default safe behavior for travel
|
|
10
|
+
* inventory.
|
|
11
|
+
*/
|
|
12
|
+
export async function resolveCompensationPolicy(db, channelId) {
|
|
13
|
+
if (!channelId)
|
|
14
|
+
return "eventually-consistent";
|
|
15
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
16
|
+
const [contract] = (await db
|
|
17
|
+
.select({ policy: channelContracts.policy })
|
|
18
|
+
.from(channelContracts)
|
|
19
|
+
.where(and(eq(channelContracts.channelId, channelId), eq(channelContracts.status, "active"),
|
|
20
|
+
// agent-quality: raw-sql reviewed -- owner: distribution; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
21
|
+
or(sql `${channelContracts.endsAt} IS NULL`, lte(channelContracts.startsAt, today))))
|
|
22
|
+
.orderBy(asc(channelContracts.startsAt))
|
|
23
|
+
.limit(1));
|
|
24
|
+
const raw = contract?.policy?.compensation;
|
|
25
|
+
return raw === "strict-atomic" ? "strict-atomic" : "eventually-consistent";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Roll back a succeeded link by calling `adapter.cancel` for the
|
|
29
|
+
* upstream reference. Marks the link `compensated` regardless of the
|
|
30
|
+
* cancel call's outcome — leaving it `ok` would lie to the operator
|
|
31
|
+
* dashboard. Per §4.2.
|
|
32
|
+
*/
|
|
33
|
+
export async function compensateSucceededLink(db, entry, bookingId, logger) {
|
|
34
|
+
let cancelError = null;
|
|
35
|
+
if (entry.adapter.cancel) {
|
|
36
|
+
const envelope = await prepareOutboundEnvelope(db, {
|
|
37
|
+
sourceModule: "distribution",
|
|
38
|
+
sourceEvent: "channel.booking.compensate",
|
|
39
|
+
sourceEntityModule: "bookings",
|
|
40
|
+
sourceEntityId: bookingId,
|
|
41
|
+
targetUrl: `adapter:${entry.adapter.kind}`,
|
|
42
|
+
targetKind: `channel:${entry.adapter.kind}`,
|
|
43
|
+
targetRef: entry.channel.id,
|
|
44
|
+
requestMethod: "POST",
|
|
45
|
+
requestBody: { upstream_ref: entry.upstreamRef, reason: "channel-push-compensation" },
|
|
46
|
+
attemptNumber: 1,
|
|
47
|
+
idempotencyKey: `compensate:${entry.link.id}`,
|
|
48
|
+
});
|
|
49
|
+
try {
|
|
50
|
+
const result = await entry.adapter.cancel(entry.adapterCtx, {
|
|
51
|
+
upstream_ref: entry.upstreamRef,
|
|
52
|
+
reason: "channel-push-compensation",
|
|
53
|
+
});
|
|
54
|
+
await envelope.complete({ responseStatus: 200, responseBody: result });
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
cancelError = err instanceof Error ? err.message : String(err);
|
|
58
|
+
await envelope.complete({ errorClass: "adapter_error", errorMessage: cancelError });
|
|
59
|
+
logger.warn?.(`compensateSucceededLink: cancel failed for ${entry.link.id}`, {
|
|
60
|
+
error: cancelError,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
cancelError = "adapter does not implement cancel";
|
|
66
|
+
logger.warn?.(`compensateSucceededLink: ${entry.adapter.kind} has no cancel method`, {
|
|
67
|
+
linkId: entry.link.id,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const now = new Date();
|
|
71
|
+
await db
|
|
72
|
+
.update(channelBookingLinks)
|
|
73
|
+
.set({
|
|
74
|
+
pushStatus: "compensated",
|
|
75
|
+
lastPushAt: now,
|
|
76
|
+
lastError: cancelError,
|
|
77
|
+
updatedAt: now,
|
|
78
|
+
})
|
|
79
|
+
.where(eq(channelBookingLinks.id, entry.link.id));
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
// Helpers
|
|
84
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
export async function markLinkOk(db, linkId, attempts, upstreamRef, externalReference, externalStatus) {
|
|
86
|
+
const now = new Date();
|
|
87
|
+
await db
|
|
88
|
+
.update(channelBookingLinks)
|
|
89
|
+
.set({
|
|
90
|
+
pushStatus: "ok",
|
|
91
|
+
pushAttempts: attempts,
|
|
92
|
+
lastPushAt: now,
|
|
93
|
+
lastError: null,
|
|
94
|
+
externalBookingId: upstreamRef,
|
|
95
|
+
externalReference,
|
|
96
|
+
externalStatus,
|
|
97
|
+
lastSyncedAt: now,
|
|
98
|
+
updatedAt: now,
|
|
99
|
+
})
|
|
100
|
+
.where(eq(channelBookingLinks.id, linkId));
|
|
101
|
+
}
|
|
102
|
+
export async function markLinkFailed(db, linkId, attempts, message) {
|
|
103
|
+
const now = new Date();
|
|
104
|
+
await db
|
|
105
|
+
.update(channelBookingLinks)
|
|
106
|
+
.set({
|
|
107
|
+
pushStatus: "failed",
|
|
108
|
+
pushAttempts: attempts,
|
|
109
|
+
lastPushAt: now,
|
|
110
|
+
lastError: message,
|
|
111
|
+
updatedAt: now,
|
|
112
|
+
})
|
|
113
|
+
.where(eq(channelBookingLinks.id, linkId));
|
|
114
|
+
}
|
|
115
|
+
export async function readMappingForLink(db, link, booking) {
|
|
116
|
+
// Walk via booking_items.product_id when the link is item-scoped;
|
|
117
|
+
// otherwise pick the first mapping for any of the booking's items
|
|
118
|
+
// (booking-level fallback used by bookings that fully syndicate).
|
|
119
|
+
let productId = null;
|
|
120
|
+
if (link.bookingItemId) {
|
|
121
|
+
const [row] = (await db
|
|
122
|
+
.select({ productId: bookingItems.productId })
|
|
123
|
+
.from(bookingItems)
|
|
124
|
+
.where(eq(bookingItems.id, link.bookingItemId))
|
|
125
|
+
.limit(1));
|
|
126
|
+
productId = row?.productId ?? null;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const [row] = (await db
|
|
130
|
+
.select({ productId: bookingItems.productId })
|
|
131
|
+
.from(bookingItems)
|
|
132
|
+
.where(eq(bookingItems.bookingId, booking.id))
|
|
133
|
+
.limit(1));
|
|
134
|
+
productId = row?.productId ?? null;
|
|
135
|
+
}
|
|
136
|
+
if (!productId)
|
|
137
|
+
return null;
|
|
138
|
+
const [mapping] = (await db
|
|
139
|
+
.select()
|
|
140
|
+
.from(channelProductMappings)
|
|
141
|
+
.where(and(eq(channelProductMappings.channelId, link.channelId), eq(channelProductMappings.productId, productId)))
|
|
142
|
+
.limit(1));
|
|
143
|
+
return mapping ?? null;
|
|
144
|
+
}
|
|
145
|
+
export function rateLimitConfigForChannel(channel) {
|
|
146
|
+
if (!channel.rateLimitRps || !channel.rateLimitBurst)
|
|
147
|
+
return null;
|
|
148
|
+
return {
|
|
149
|
+
rps: channel.rateLimitRps,
|
|
150
|
+
burst: channel.rateLimitBurst,
|
|
151
|
+
priorityGates: channel.rateLimitPriorityGates ?? undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export function serializeBookingForPush(booking, bookingItemId) {
|
|
155
|
+
// v1: ship a thin shape — booking number, dates, pax, optionally
|
|
156
|
+
// narrowed to the targeted item. PII redaction (per §15) is left to
|
|
157
|
+
// the adapter; future iterations push redaction up here.
|
|
158
|
+
return {
|
|
159
|
+
bookingId: booking.id,
|
|
160
|
+
bookingNumber: booking.bookingNumber,
|
|
161
|
+
bookingItemId,
|
|
162
|
+
status: booking.status,
|
|
163
|
+
startDate: booking.startDate,
|
|
164
|
+
endDate: booking.endDate,
|
|
165
|
+
pax: booking.pax,
|
|
166
|
+
sellCurrency: booking.sellCurrency,
|
|
167
|
+
sellAmountCents: booking.sellAmountCents,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Booking-push pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Triggered by `booking.confirmed`. The subscriber writes pending
|
|
5
|
+
* `channel_booking_links` rows and returns immediately (per the EventBus
|
|
6
|
+
* fire-and-forget contract). The durable processor (`processBookingPush`)
|
|
7
|
+
* drains those rows, calls `adapter.pushBooking()` per link, and marks
|
|
8
|
+
* each row `ok` or `failed`.
|
|
9
|
+
*
|
|
10
|
+
* Per docs/architecture/channel-push-architecture.md §4 + §12.1.
|
|
11
|
+
*/
|
|
12
|
+
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
13
|
+
import { channelProductMappings, channels } from "@voyant-travel/distribution/schema";
|
|
14
|
+
import { type ChannelPushDeps } from "./types.js";
|
|
15
|
+
/** Stable string identifier for the booking-push workflow. */
|
|
16
|
+
export declare const CHANNEL_BOOKING_PUSH_WORKFLOW_ID: "channel.booking.push";
|
|
17
|
+
export interface ProcessBookingPushInput {
|
|
18
|
+
bookingId: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ProcessBookingPushResult {
|
|
21
|
+
bookingId: string;
|
|
22
|
+
attempted: number;
|
|
23
|
+
succeeded: number;
|
|
24
|
+
failed: number;
|
|
25
|
+
/**
|
|
26
|
+
* Number of succeeded links that were compensated (rolled back via
|
|
27
|
+
* `adapter.cancel`) because the contract's `compensation` policy is
|
|
28
|
+
* `"strict-atomic"` and at least one sibling failed. Always 0 under
|
|
29
|
+
* the default `"eventually-consistent"` policy.
|
|
30
|
+
*/
|
|
31
|
+
compensated: number;
|
|
32
|
+
/** Per-link outcomes for diagnostics. */
|
|
33
|
+
outcomes: Array<{
|
|
34
|
+
channelId: string;
|
|
35
|
+
bookingItemId: string | null;
|
|
36
|
+
status: "ok" | "failed" | "skipped" | "compensated";
|
|
37
|
+
upstreamRef?: string;
|
|
38
|
+
error?: string;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Compensation modes per `channel_contracts.policy.compensation`.
|
|
43
|
+
*
|
|
44
|
+
* - `eventually-consistent` (default): partial successes stay; ops gets
|
|
45
|
+
* alerted via `webhook_deliveries` and retries via the reconciler.
|
|
46
|
+
* Usually correct for travel inventory — succeeded channels know
|
|
47
|
+
* about the booking and will honor it; the failed ones converge.
|
|
48
|
+
* - `strict-atomic`: on any per-link failure, the engine calls
|
|
49
|
+
* `adapter.cancel` for succeeded siblings and marks them
|
|
50
|
+
* `push_status = 'compensated'`. Use only when ALL channels MUST
|
|
51
|
+
* agree on the booking's existence (rare).
|
|
52
|
+
*
|
|
53
|
+
* Per docs/architecture/channel-push-architecture.md §4.2 + §9.
|
|
54
|
+
*/
|
|
55
|
+
export type CompensationPolicy = "strict-atomic" | "eventually-consistent";
|
|
56
|
+
/**
|
|
57
|
+
* Build the stable idempotency key the upstream uses to dedupe pushes
|
|
58
|
+
* across retries. Per §3.
|
|
59
|
+
*/
|
|
60
|
+
export declare function bookingPushIdempotencyKey(bookingId: string, bookingItemId: string | null, channelId: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the channels that want a push for this booking. One row per
|
|
63
|
+
* (booking_item, channel) pair where the mapping has push_bookings =
|
|
64
|
+
* true and the channel is active. Booking-level pushes (no item id) are
|
|
65
|
+
* supported via a synthetic item id of null.
|
|
66
|
+
*
|
|
67
|
+
* Per §7.4 — booking push uses `channel_product_mappings` (not
|
|
68
|
+
* `channel_inventory_allotments`) so channels mapped to a product
|
|
69
|
+
* without a slot allotment still receive the push.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveBookingPushTargets(db: AnyDrizzleDb, bookingId: string): Promise<Array<{
|
|
72
|
+
bookingItemId: string | null;
|
|
73
|
+
productId: string;
|
|
74
|
+
mapping: typeof channelProductMappings.$inferSelect;
|
|
75
|
+
channel: typeof channels.$inferSelect;
|
|
76
|
+
}>>;
|
|
77
|
+
/**
|
|
78
|
+
* Insert pending `channel_booking_links` rows for each push target.
|
|
79
|
+
* `INSERT ... ON CONFLICT DO NOTHING` against the
|
|
80
|
+
* `(channel_id, booking_id, COALESCE(booking_item_id, ''))` unique
|
|
81
|
+
* index — durable handoff with no doubled-push risk per §7.1.
|
|
82
|
+
*
|
|
83
|
+
* Returns the count of newly-inserted rows. Subscribers don't strictly
|
|
84
|
+
* need this — the processor reads pending rows by query — but tests
|
|
85
|
+
* find it useful.
|
|
86
|
+
*/
|
|
87
|
+
export declare function upsertPendingBookingLinks(db: AnyDrizzleDb, bookingId: string, targets: Array<{
|
|
88
|
+
bookingItemId: string | null;
|
|
89
|
+
mapping: typeof channelProductMappings.$inferSelect;
|
|
90
|
+
channel: typeof channels.$inferSelect;
|
|
91
|
+
}>): Promise<number>;
|
|
92
|
+
/**
|
|
93
|
+
* Drain pending `channel_booking_links` rows for one booking and call
|
|
94
|
+
* `adapter.pushBooking()` per link. Idempotent: re-running the
|
|
95
|
+
* processor against the same booking is safe — the `idempotency_key`
|
|
96
|
+
* column ensures retries don't double-push upstream.
|
|
97
|
+
*
|
|
98
|
+
* Each adapter call:
|
|
99
|
+
* 1. Acquires a token from the per-channel/connection bucket.
|
|
100
|
+
* 2. Calls the adapter through `prepareOutboundEnvelope` so every
|
|
101
|
+
* attempt lands in `webhook_deliveries` with redacted headers.
|
|
102
|
+
* 3. Updates the link to `ok` (with upstream_ref, hash) or `failed`
|
|
103
|
+
* (with last_error, attempts++).
|
|
104
|
+
*
|
|
105
|
+
* Per §4.2 + §12.1.
|
|
106
|
+
*/
|
|
107
|
+
export declare function processBookingPush(input: ProcessBookingPushInput, deps?: ChannelPushDeps): Promise<ProcessBookingPushResult>;
|
|
108
|
+
//# sourceMappingURL=booking-push.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-push.d.ts","sourceRoot":"","sources":["../../src/channel-push/booking-push.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AASH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,OAAO,EAEL,sBAAsB,EACtB,QAAQ,EACT,MAAM,oCAAoC,CAAA;AAgB3C,OAAO,EAAE,KAAK,eAAe,EAA4C,MAAM,YAAY,CAAA;AAE3F,8DAA8D;AAC9D,eAAO,MAAM,gCAAgC,EAAG,sBAA+B,CAAA;AAE/E,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB,yCAAyC;IACzC,QAAQ,EAAE,KAAK,CAAC;QACd,SAAS,EAAE,MAAM,CAAA;QACjB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;QAC5B,MAAM,EAAE,IAAI,GAAG,QAAQ,GAAG,SAAS,GAAG,aAAa,CAAA;QACnD,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAC,CAAA;CACH;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,kBAAkB,GAAG,eAAe,GAAG,uBAAuB,CAAA;AAE1E;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,SAAS,EAAE,MAAM,GAChB,MAAM,CAER;AAED;;;;;;;;;GASG;AACH,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CACR,KAAK,CAAC;IACJ,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,sBAAsB,CAAC,YAAY,CAAA;IACnD,OAAO,EAAE,OAAO,QAAQ,CAAC,YAAY,CAAA;CACtC,CAAC,CACH,CAyDA;AAED;;;;;;;;;GASG;AACH,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,KAAK,CAAC;IACb,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,OAAO,EAAE,OAAO,sBAAsB,CAAC,YAAY,CAAA;IACnD,OAAO,EAAE,OAAO,QAAQ,CAAC,YAAY,CAAA;CACtC,CAAC,GACD,OAAO,CAAC,MAAM,CAAC,CAuBjB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,uBAAuB,EAC9B,IAAI,CAAC,EAAE,eAAe,GACrB,OAAO,CAAC,wBAAwB,CAAC,CA8PnC"}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Booking-push pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Triggered by `booking.confirmed`. The subscriber writes pending
|
|
5
|
+
* `channel_booking_links` rows and returns immediately (per the EventBus
|
|
6
|
+
* fire-and-forget contract). The durable processor (`processBookingPush`)
|
|
7
|
+
* drains those rows, calls `adapter.pushBooking()` per link, and marks
|
|
8
|
+
* each row `ok` or `failed`.
|
|
9
|
+
*
|
|
10
|
+
* Per docs/architecture/channel-push-architecture.md §4 + §12.1.
|
|
11
|
+
*/
|
|
12
|
+
import { bookingItems, bookings } from "@voyant-travel/bookings/schema";
|
|
13
|
+
import { AdapterRateLimitedError, } from "@voyant-travel/catalog";
|
|
14
|
+
import { newId } from "@voyant-travel/db/lib/typeid";
|
|
15
|
+
import { channelBookingLinks, channelProductMappings, channels, } from "@voyant-travel/distribution/schema";
|
|
16
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
17
|
+
import { acquireToken, channelScopeKey, drainBucket } from "../rate-limit.js";
|
|
18
|
+
import { prepareOutboundEnvelope } from "../webhook-deliveries.js";
|
|
19
|
+
import { compensateSucceededLink, markLinkFailed, markLinkOk, rateLimitConfigForChannel, readMappingForLink, resolveCompensationPolicy, serializeBookingForPush, } from "./booking-push-helpers.js";
|
|
20
|
+
import { defaultLogger, getChannelPushDepsOrThrow } from "./types.js";
|
|
21
|
+
/** Stable string identifier for the booking-push workflow. */
|
|
22
|
+
export const CHANNEL_BOOKING_PUSH_WORKFLOW_ID = "channel.booking.push";
|
|
23
|
+
/**
|
|
24
|
+
* Build the stable idempotency key the upstream uses to dedupe pushes
|
|
25
|
+
* across retries. Per §3.
|
|
26
|
+
*/
|
|
27
|
+
export function bookingPushIdempotencyKey(bookingId, bookingItemId, channelId) {
|
|
28
|
+
return `book:${bookingId}:${bookingItemId ?? "*"}:${channelId}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the channels that want a push for this booking. One row per
|
|
32
|
+
* (booking_item, channel) pair where the mapping has push_bookings =
|
|
33
|
+
* true and the channel is active. Booking-level pushes (no item id) are
|
|
34
|
+
* supported via a synthetic item id of null.
|
|
35
|
+
*
|
|
36
|
+
* Per §7.4 — booking push uses `channel_product_mappings` (not
|
|
37
|
+
* `channel_inventory_allotments`) so channels mapped to a product
|
|
38
|
+
* without a slot allotment still receive the push.
|
|
39
|
+
*/
|
|
40
|
+
export async function resolveBookingPushTargets(db, bookingId) {
|
|
41
|
+
const items = (await db
|
|
42
|
+
.select({
|
|
43
|
+
id: bookingItems.id,
|
|
44
|
+
productId: bookingItems.productId,
|
|
45
|
+
})
|
|
46
|
+
.from(bookingItems)
|
|
47
|
+
.where(eq(bookingItems.bookingId, bookingId)));
|
|
48
|
+
if (items.length === 0)
|
|
49
|
+
return [];
|
|
50
|
+
const productIds = Array.from(new Set(items.filter((i) => i.productId).map((i) => i.productId)));
|
|
51
|
+
if (productIds.length === 0)
|
|
52
|
+
return [];
|
|
53
|
+
const rows = (await db
|
|
54
|
+
.select({
|
|
55
|
+
mapping: channelProductMappings,
|
|
56
|
+
channel: channels,
|
|
57
|
+
})
|
|
58
|
+
.from(channelProductMappings)
|
|
59
|
+
.innerJoin(channels, eq(channelProductMappings.channelId, channels.id))
|
|
60
|
+
.where(and(eq(channelProductMappings.active, true), eq(channelProductMappings.pushBookings, true), inArray(channelProductMappings.productId, productIds), eq(channels.status, "active"))));
|
|
61
|
+
if (rows.length === 0)
|
|
62
|
+
return [];
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const item of items) {
|
|
65
|
+
if (!item.productId)
|
|
66
|
+
continue;
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
if (row.mapping.productId !== item.productId)
|
|
69
|
+
continue;
|
|
70
|
+
out.push({
|
|
71
|
+
bookingItemId: item.id,
|
|
72
|
+
productId: item.productId,
|
|
73
|
+
mapping: row.mapping,
|
|
74
|
+
channel: row.channel,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Insert pending `channel_booking_links` rows for each push target.
|
|
82
|
+
* `INSERT ... ON CONFLICT DO NOTHING` against the
|
|
83
|
+
* `(channel_id, booking_id, COALESCE(booking_item_id, ''))` unique
|
|
84
|
+
* index — durable handoff with no doubled-push risk per §7.1.
|
|
85
|
+
*
|
|
86
|
+
* Returns the count of newly-inserted rows. Subscribers don't strictly
|
|
87
|
+
* need this — the processor reads pending rows by query — but tests
|
|
88
|
+
* find it useful.
|
|
89
|
+
*/
|
|
90
|
+
export async function upsertPendingBookingLinks(db, bookingId, targets) {
|
|
91
|
+
if (targets.length === 0)
|
|
92
|
+
return 0;
|
|
93
|
+
const rows = targets.map((target) => ({
|
|
94
|
+
id: newId("channel_booking_links"),
|
|
95
|
+
channelId: target.channel.id,
|
|
96
|
+
bookingId,
|
|
97
|
+
bookingItemId: target.bookingItemId,
|
|
98
|
+
sourceKind: target.mapping.sourceKind ?? null,
|
|
99
|
+
sourceConnectionId: target.mapping.sourceConnectionId ?? null,
|
|
100
|
+
pushStatus: "pending",
|
|
101
|
+
idempotencyKey: bookingPushIdempotencyKey(bookingId, target.bookingItemId, target.channel.id),
|
|
102
|
+
}));
|
|
103
|
+
// Drizzle's onConflictDoNothing without an explicit target falls back
|
|
104
|
+
// to the (channel, booking, item) unique index we created in §7.1.
|
|
105
|
+
const inserted = (await db
|
|
106
|
+
.insert(channelBookingLinks)
|
|
107
|
+
.values(rows)
|
|
108
|
+
.onConflictDoNothing()
|
|
109
|
+
.returning());
|
|
110
|
+
return inserted.length;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Drain pending `channel_booking_links` rows for one booking and call
|
|
114
|
+
* `adapter.pushBooking()` per link. Idempotent: re-running the
|
|
115
|
+
* processor against the same booking is safe — the `idempotency_key`
|
|
116
|
+
* column ensures retries don't double-push upstream.
|
|
117
|
+
*
|
|
118
|
+
* Each adapter call:
|
|
119
|
+
* 1. Acquires a token from the per-channel/connection bucket.
|
|
120
|
+
* 2. Calls the adapter through `prepareOutboundEnvelope` so every
|
|
121
|
+
* attempt lands in `webhook_deliveries` with redacted headers.
|
|
122
|
+
* 3. Updates the link to `ok` (with upstream_ref, hash) or `failed`
|
|
123
|
+
* (with last_error, attempts++).
|
|
124
|
+
*
|
|
125
|
+
* Per §4.2 + §12.1.
|
|
126
|
+
*/
|
|
127
|
+
export async function processBookingPush(input, deps) {
|
|
128
|
+
const { db, registry, logger = defaultLogger } = deps ?? getChannelPushDepsOrThrow();
|
|
129
|
+
const outcomes = [];
|
|
130
|
+
const links = (await db
|
|
131
|
+
.select({
|
|
132
|
+
link: channelBookingLinks,
|
|
133
|
+
channel: channels,
|
|
134
|
+
})
|
|
135
|
+
.from(channelBookingLinks)
|
|
136
|
+
.innerJoin(channels, eq(channelBookingLinks.channelId, channels.id))
|
|
137
|
+
.where(and(eq(channelBookingLinks.bookingId, input.bookingId), eq(channelBookingLinks.pushStatus, "pending"))));
|
|
138
|
+
if (links.length === 0) {
|
|
139
|
+
return {
|
|
140
|
+
bookingId: input.bookingId,
|
|
141
|
+
attempted: 0,
|
|
142
|
+
succeeded: 0,
|
|
143
|
+
failed: 0,
|
|
144
|
+
compensated: 0,
|
|
145
|
+
outcomes,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const [booking] = (await db
|
|
149
|
+
.select()
|
|
150
|
+
.from(bookings)
|
|
151
|
+
.where(eq(bookings.id, input.bookingId))
|
|
152
|
+
.limit(1));
|
|
153
|
+
if (!booking) {
|
|
154
|
+
logger.error?.(`processBookingPush: booking ${input.bookingId} not found`, {});
|
|
155
|
+
return {
|
|
156
|
+
bookingId: input.bookingId,
|
|
157
|
+
attempted: 0,
|
|
158
|
+
succeeded: 0,
|
|
159
|
+
failed: 0,
|
|
160
|
+
compensated: 0,
|
|
161
|
+
outcomes,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
let succeeded = 0;
|
|
165
|
+
let failed = 0;
|
|
166
|
+
// Track succeeded links so we can compensate them if a sibling fails
|
|
167
|
+
// and the contract policy demands strict-atomicity. Per §4.2.
|
|
168
|
+
const successList = [];
|
|
169
|
+
for (const { link, channel } of links) {
|
|
170
|
+
const connectionId = link.sourceConnectionId ?? channel.id;
|
|
171
|
+
const adapter = registry.resolveByConnection(connectionId);
|
|
172
|
+
if (!adapter) {
|
|
173
|
+
// Skip — no adapter wired for this connection. Mark the row
|
|
174
|
+
// failed so ops sees it; the reconciler retries when the adapter
|
|
175
|
+
// shows up.
|
|
176
|
+
await markLinkFailed(db, link.id, link.pushAttempts + 1, "no_adapter_registered");
|
|
177
|
+
outcomes.push({
|
|
178
|
+
channelId: channel.id,
|
|
179
|
+
bookingItemId: link.bookingItemId ?? null,
|
|
180
|
+
status: "failed",
|
|
181
|
+
error: "no_adapter_registered",
|
|
182
|
+
});
|
|
183
|
+
failed += 1;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (!adapter.capabilities.supportsBookingPush || !adapter.pushBooking) {
|
|
187
|
+
await markLinkFailed(db, link.id, link.pushAttempts + 1, "adapter_unsupported");
|
|
188
|
+
outcomes.push({
|
|
189
|
+
channelId: channel.id,
|
|
190
|
+
bookingItemId: link.bookingItemId ?? null,
|
|
191
|
+
status: "failed",
|
|
192
|
+
error: "adapter_unsupported",
|
|
193
|
+
});
|
|
194
|
+
failed += 1;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// Resolve the per-(channel, item) mapping for the external ids.
|
|
198
|
+
const mapping = await readMappingForLink(db, link, booking);
|
|
199
|
+
if (!mapping) {
|
|
200
|
+
await markLinkFailed(db, link.id, link.pushAttempts + 1, "no_mapping");
|
|
201
|
+
outcomes.push({
|
|
202
|
+
channelId: channel.id,
|
|
203
|
+
bookingItemId: link.bookingItemId ?? null,
|
|
204
|
+
status: "failed",
|
|
205
|
+
error: "no_mapping",
|
|
206
|
+
});
|
|
207
|
+
failed += 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
// Rate limit before dispatching.
|
|
211
|
+
const rlConfig = rateLimitConfigForChannel(channel);
|
|
212
|
+
if (rlConfig) {
|
|
213
|
+
const acq = await acquireToken(db, channelScopeKey(channel.id, connectionId), rlConfig, "booking");
|
|
214
|
+
if (!acq.acquired) {
|
|
215
|
+
// Bookings are supposed to pre-empt other flows; if we can't
|
|
216
|
+
// acquire, the bucket is over-tight or the channel just got
|
|
217
|
+
// 429'd. Mark the link failed and move on — reconciler retries.
|
|
218
|
+
await markLinkFailed(db, link.id, link.pushAttempts + 1, "rate_limited");
|
|
219
|
+
outcomes.push({
|
|
220
|
+
channelId: channel.id,
|
|
221
|
+
bookingItemId: link.bookingItemId ?? null,
|
|
222
|
+
status: "failed",
|
|
223
|
+
error: "rate_limited",
|
|
224
|
+
});
|
|
225
|
+
failed += 1;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const request = {
|
|
230
|
+
idempotencyKey: link.idempotencyKey ??
|
|
231
|
+
bookingPushIdempotencyKey(input.bookingId, link.bookingItemId ?? null, channel.id),
|
|
232
|
+
bookingId: input.bookingId,
|
|
233
|
+
bookingItemId: link.bookingItemId ?? undefined,
|
|
234
|
+
externalProductId: mapping.externalProductId ?? "",
|
|
235
|
+
externalRateId: mapping.externalRateId ?? undefined,
|
|
236
|
+
externalCategoryId: mapping.externalCategoryId ?? undefined,
|
|
237
|
+
channelId: channel.id,
|
|
238
|
+
contractPolicy: undefined,
|
|
239
|
+
payload: serializeBookingForPush(booking, link.bookingItemId ?? null),
|
|
240
|
+
};
|
|
241
|
+
const adapterCtx = {
|
|
242
|
+
connection_id: connectionId,
|
|
243
|
+
};
|
|
244
|
+
// Every attempt writes a webhook_deliveries row through the
|
|
245
|
+
// redactor — direct INSERTs are forbidden per §11.3.
|
|
246
|
+
const envelope = await prepareOutboundEnvelope(db, {
|
|
247
|
+
sourceModule: "distribution",
|
|
248
|
+
sourceEvent: "channel.booking.push",
|
|
249
|
+
sourceEntityModule: "bookings",
|
|
250
|
+
sourceEntityId: input.bookingId,
|
|
251
|
+
targetUrl: `adapter:${adapter.kind}`,
|
|
252
|
+
targetKind: `channel:${adapter.kind}`,
|
|
253
|
+
targetRef: channel.id,
|
|
254
|
+
requestMethod: "POST",
|
|
255
|
+
requestBody: request,
|
|
256
|
+
attemptNumber: link.pushAttempts + 1,
|
|
257
|
+
idempotencyKey: request.idempotencyKey,
|
|
258
|
+
});
|
|
259
|
+
try {
|
|
260
|
+
const result = await adapter.pushBooking(adapterCtx, request);
|
|
261
|
+
await envelope.complete({
|
|
262
|
+
responseStatus: 200,
|
|
263
|
+
responseBody: result,
|
|
264
|
+
});
|
|
265
|
+
await markLinkOk(db, link.id, link.pushAttempts + 1, result.upstreamRef, result.externalReference ?? null, result.externalStatus ?? null);
|
|
266
|
+
outcomes.push({
|
|
267
|
+
channelId: channel.id,
|
|
268
|
+
bookingItemId: link.bookingItemId ?? null,
|
|
269
|
+
status: "ok",
|
|
270
|
+
upstreamRef: result.upstreamRef,
|
|
271
|
+
});
|
|
272
|
+
succeeded += 1;
|
|
273
|
+
successList.push({ link, channel, adapter, adapterCtx, upstreamRef: result.upstreamRef });
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
277
|
+
// 429 from upstream — drain the bucket for the cooldown so
|
|
278
|
+
// concurrent dispatchers also see "no tokens" until the channel
|
|
279
|
+
// is ready, and stamp the delivery with the rate-limited class
|
|
280
|
+
// (per §14.4).
|
|
281
|
+
const isRateLimited = err instanceof AdapterRateLimitedError;
|
|
282
|
+
if (isRateLimited) {
|
|
283
|
+
await drainBucket(db, channelScopeKey(channel.id, connectionId), err.retryAfterMs);
|
|
284
|
+
}
|
|
285
|
+
await envelope.complete({
|
|
286
|
+
errorClass: isRateLimited ? "rate_limited" : "adapter_error",
|
|
287
|
+
errorMessage: message,
|
|
288
|
+
});
|
|
289
|
+
await markLinkFailed(db, link.id, link.pushAttempts + 1, message);
|
|
290
|
+
outcomes.push({
|
|
291
|
+
channelId: channel.id,
|
|
292
|
+
bookingItemId: link.bookingItemId ?? null,
|
|
293
|
+
status: "failed",
|
|
294
|
+
error: message,
|
|
295
|
+
});
|
|
296
|
+
failed += 1;
|
|
297
|
+
logger.error?.(`pushBooking failed for ${link.id}`, { error: message });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Compensation pass: if any link failed and the channel-contract
|
|
301
|
+
// policy is strict-atomic, roll back succeeded siblings so all
|
|
302
|
+
// channels see a consistent "no booking" state. Per §4.2.
|
|
303
|
+
let compensated = 0;
|
|
304
|
+
if (failed > 0 && successList.length > 0) {
|
|
305
|
+
const policy = await resolveCompensationPolicy(db, links[0]?.channel.id ?? null);
|
|
306
|
+
if (policy === "strict-atomic") {
|
|
307
|
+
for (const entry of successList) {
|
|
308
|
+
const success = await compensateSucceededLink(db, entry, input.bookingId, logger);
|
|
309
|
+
if (success) {
|
|
310
|
+
compensated += 1;
|
|
311
|
+
// Update the existing outcome row to compensated.
|
|
312
|
+
for (const outcome of outcomes) {
|
|
313
|
+
if (outcome.channelId === entry.channel.id &&
|
|
314
|
+
outcome.bookingItemId === (entry.link.bookingItemId ?? null) &&
|
|
315
|
+
outcome.status === "ok") {
|
|
316
|
+
outcome.status = "compensated";
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (compensated > 0) {
|
|
323
|
+
logger.warn?.(`processBookingPush: compensated ${compensated} succeeded link(s) under strict-atomic policy`, { bookingId: input.bookingId, compensated, failed });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
bookingId: input.bookingId,
|
|
329
|
+
attempted: links.length,
|
|
330
|
+
succeeded,
|
|
331
|
+
failed,
|
|
332
|
+
compensated,
|
|
333
|
+
outcomes,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
2
|
+
export interface AvailabilityPushSlot {
|
|
3
|
+
id: string;
|
|
4
|
+
productId: string;
|
|
5
|
+
optionId: string | null;
|
|
6
|
+
startsAt: Date;
|
|
7
|
+
unlimited: boolean;
|
|
8
|
+
remainingPax: number | null;
|
|
9
|
+
updatedAt: Date;
|
|
10
|
+
}
|
|
11
|
+
export interface ContentPushProduct {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string | null;
|
|
15
|
+
}
|
|
16
|
+
export declare function loadAvailabilityPushSlot(db: AnyDrizzleDb, slotId: string): Promise<AvailabilityPushSlot | null>;
|
|
17
|
+
export declare function loadRecentlyUpdatedAvailabilityPushSlots(db: AnyDrizzleDb, input: {
|
|
18
|
+
updatedAfter: Date;
|
|
19
|
+
limit: number;
|
|
20
|
+
}): Promise<AvailabilityPushSlot[]>;
|
|
21
|
+
export declare function loadContentPushProduct(db: AnyDrizzleDb, productId: string): Promise<ContentPushProduct | null>;
|
|
22
|
+
export declare function loadContentPushProducts(db: AnyDrizzleDb, productIds: readonly string[]): Promise<Map<string, ContentPushProduct>>;
|
|
23
|
+
//# sourceMappingURL=boundary-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundary-sql.d.ts","sourceRoot":"","sources":["../../src/channel-push/boundary-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGrD,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,IAAI,CAAA;IACd,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,IAAI,CAAA;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B;AAqDD,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAYtC;AAED,wBAAsB,wCAAwC,CAC5D,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE;IAAE,YAAY,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC3C,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAajC;AAED,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAYpC;AAED,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,YAAY,EAChB,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAY1C"}
|