@v-office/website-headless 0.0.1
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 +86 -0
- package/dist/enhance/index.d.mts +181 -0
- package/dist/enhance/index.mjs +533 -0
- package/dist/index.d.mts +912 -0
- package/dist/index.mjs +1423 -0
- package/dist/manifest/index.d.mts +118 -0
- package/dist/manifest/index.mjs +264 -0
- package/package.json +48 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
import { BookingPaymentOption, BookingPaymentScheduleKind, GuestQuote, GuestQuoteResult, SubmittableBookingPaymentOption, createWebsiteSDK } from "@v-office/website-sdk";
|
|
2
|
+
//#region src/store/index.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The smallest external store that every UI framework can subscribe to:
|
|
5
|
+
* `getState` + `subscribe` is what React's useSyncExternalStore, Vue's
|
|
6
|
+
* watchers, Svelte's store contract and a plain `<script>` all understand.
|
|
7
|
+
* Flows in this package (checkout today, more later) build on it so the
|
|
8
|
+
* package never depends on a framework; the adapters live in ./react and
|
|
9
|
+
* friends.
|
|
10
|
+
*/
|
|
11
|
+
type Listener<S> = (state: S) => void;
|
|
12
|
+
type Unsubscribe = () => void;
|
|
13
|
+
interface Store<S> {
|
|
14
|
+
getState(): S;
|
|
15
|
+
subscribe(listener: Listener<S>): Unsubscribe;
|
|
16
|
+
}
|
|
17
|
+
interface WritableStore<S> extends Store<S> {
|
|
18
|
+
/** Replaces the state and notifies every listener. Same reference = no-op. */
|
|
19
|
+
setState(next: S): void;
|
|
20
|
+
}
|
|
21
|
+
declare function createStore<S>(initial: S): WritableStore<S>;
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/sdk.d.ts
|
|
24
|
+
type WebsiteSdk = ReturnType<typeof createWebsiteSDK>;
|
|
25
|
+
/**
|
|
26
|
+
* Render `error` and its whole cause chain as indented JSON.
|
|
27
|
+
*
|
|
28
|
+
* Walks the chain because a cause nests (a `CoreSDKError` wrapping a thrown
|
|
29
|
+
* transport object wrapping a response body). Skips `stack` — a backend
|
|
30
|
+
* refusal is not diagnosed from the frames that asked, and the raw error is
|
|
31
|
+
* one `console.error` away for the cases that are. Reads `name`/`message` by
|
|
32
|
+
* hand because `Error` declares both non-enumerable, so `Object.entries`
|
|
33
|
+
* alone would return an Error as `{}`.
|
|
34
|
+
*
|
|
35
|
+
* **`cause` is read by hand for exactly the same reason**, and it was missed
|
|
36
|
+
* the first time — so this file shipped without the one property it exists to
|
|
37
|
+
* print. Verified against the real class: `Object.keys(new CoreSDKError({…}))`
|
|
38
|
+
* is `["source", "operation", "_tag"]`, and `cause`'s own descriptor carries
|
|
39
|
+
* `enumerable: false`. `Object.entries` therefore never saw it, and every
|
|
40
|
+
* `CoreSDKError` logged as the five bland fields above with the sentence that
|
|
41
|
+
* names the fault dropped.
|
|
42
|
+
*/
|
|
43
|
+
declare function describeSdkError(error: unknown): string;
|
|
44
|
+
/**
|
|
45
|
+
* Log a failed SDK call with the backend's reason, tagged by `scope`.
|
|
46
|
+
*
|
|
47
|
+
* `scope` names the SDK operation, not the component — `"quote.selectInsurance"`
|
|
48
|
+
* rather than `"the extras panel"`. What the console needs to answer is which
|
|
49
|
+
* call the backend refused; which button was on screen at the time is the part
|
|
50
|
+
* the reporter already knows.
|
|
51
|
+
*
|
|
52
|
+
* Logs the description ALONE. Callers that also have the raw error in hand may
|
|
53
|
+
* log it separately for a clickable stack; doubling it here would print every
|
|
54
|
+
* failure twice on the surfaces that already do.
|
|
55
|
+
*/
|
|
56
|
+
declare function logSdkFailure(scope: string, error: unknown): void;
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/quote.d.ts
|
|
59
|
+
/** One row of the price breakdown. `label`/`amount` are the SDK's own
|
|
60
|
+
* `position`/`appliedCharge`, verbatim; `nested` marks a sub-line so the UI
|
|
61
|
+
* can indent it under the line above.
|
|
62
|
+
*
|
|
63
|
+
* `discount` marks a row synthesised from an SDK price MODIFIER rather than
|
|
64
|
+
* from a charge line — see `adaptGuestQuote` for why those rows exist. Its
|
|
65
|
+
* `amount` is the modifier's own signed string ("-140,00 €"), so the row is a
|
|
66
|
+
* deduction from the running total, not another charge. */
|
|
67
|
+
interface QuoteLine {
|
|
68
|
+
label: string;
|
|
69
|
+
amount: string;
|
|
70
|
+
nested: boolean;
|
|
71
|
+
discount: boolean;
|
|
72
|
+
}
|
|
73
|
+
interface PriceQuote {
|
|
74
|
+
/** Section/line breakdown flattened in SDK order (sub-items follow their
|
|
75
|
+
* parent line, flagged `nested`). */
|
|
76
|
+
lines: QuoteLine[];
|
|
77
|
+
/** SDK `bookingCardInformation.total`, verbatim (already locale-formatted). */
|
|
78
|
+
total: string;
|
|
79
|
+
}
|
|
80
|
+
/** Bookable add-on from `quote.additionalServices`, verbatim fields. */
|
|
81
|
+
interface QuoteAddonService {
|
|
82
|
+
id: string;
|
|
83
|
+
label: string;
|
|
84
|
+
/** SDK charge string as-is (e.g. "25,00 €" — the PMS's own formatting). */
|
|
85
|
+
charge: string;
|
|
86
|
+
description?: string | undefined;
|
|
87
|
+
/** Max selectable per booking; absent = the SDK states no cap. */
|
|
88
|
+
maxPerBooking?: number | undefined;
|
|
89
|
+
/** Quantity the PMS PROPOSES for this service (`0` included). A proposal,
|
|
90
|
+
* NOT a selection — the distinction is the whole reason this field is
|
|
91
|
+
* named the way it is.
|
|
92
|
+
*
|
|
93
|
+
* THE SDK IS EXPLICIT (`instructions/quote.md`): "When the backend provides
|
|
94
|
+
* a line amount for an optional service, it is exposed as `proposedQuantity`
|
|
95
|
+
* (including `0`). By default this is informational only and is not added to
|
|
96
|
+
* the quote." A fresh quote carries these as selections ONLY on a project
|
|
97
|
+
* that sets `shouldAddProposedAdditionalServiceAmount: true`, which the SDK
|
|
98
|
+
* defaults to `false`.
|
|
99
|
+
*
|
|
100
|
+
* SO THIS IS NEVER A BASELINE. What a quote actually has selected is not
|
|
101
|
+
* readable from the public SDK surface at all — `GuestQuote` keeps its
|
|
102
|
+
* `selections` behind an unexported symbol, and there is no quote-side
|
|
103
|
+
* counterpart to `getBookingModificationSelectedQuantity`. Code that needs a
|
|
104
|
+
* known starting point therefore MAKES one with `clearQuoteServices` and
|
|
105
|
+
* steps up from zero. It does not read this number and hope.
|
|
106
|
+
*
|
|
107
|
+
* RENAMED FROM `preSelectedQuantity` IN booking-kit 1.21.0, because the old
|
|
108
|
+
* name stated the one thing the value does not mean, and `checkout-kit`
|
|
109
|
+
* believed it — see that kit's 1.21.0 entry. */
|
|
110
|
+
proposedQuantity: number;
|
|
111
|
+
/** False only when the PMS explicitly marks this service unbookable for the
|
|
112
|
+
* current multi-rate combination (SDK `available: false`). Absent → true.
|
|
113
|
+
* The UI disables adding it rather than letting the click silently no-op. */
|
|
114
|
+
available: boolean;
|
|
115
|
+
/** PMS's own reason the service is unavailable, verbatim; self-hides when
|
|
116
|
+
* the feed omits it. */
|
|
117
|
+
unavailableReason?: string | undefined;
|
|
118
|
+
}
|
|
119
|
+
interface QuotePolicyRule {
|
|
120
|
+
daysBeforeArrival: number;
|
|
121
|
+
/** SDK's formatted percentage string, verbatim. */
|
|
122
|
+
refundPercentage: string;
|
|
123
|
+
}
|
|
124
|
+
/** Selectable cancellation policy from `quote.cancellationPolicies`. */
|
|
125
|
+
interface QuoteCancellationPolicy {
|
|
126
|
+
id: string;
|
|
127
|
+
label: string;
|
|
128
|
+
selected: boolean;
|
|
129
|
+
description?: string | undefined;
|
|
130
|
+
markUpPercentage?: string | undefined;
|
|
131
|
+
processingFee?: string | undefined;
|
|
132
|
+
rules: QuotePolicyRule[];
|
|
133
|
+
/** False only when the PMS explicitly marks this policy unselectable for the
|
|
134
|
+
* current multi-rate combination (SDK `available: false`). Absent → true. */
|
|
135
|
+
available: boolean;
|
|
136
|
+
/** PMS's own reason the policy is unavailable, verbatim; self-hides when
|
|
137
|
+
* the feed omits it. */
|
|
138
|
+
unavailableReason?: string | undefined;
|
|
139
|
+
}
|
|
140
|
+
/** Voucher status echoed back on a re-priced quote (`quote.voucher`). On v10
|
|
141
|
+
* the SDK authoritatively reports whether the code applied a modifier; v9
|
|
142
|
+
* reports `"unknown"`. `message` is the PMS's own note, verbatim. */
|
|
143
|
+
interface QuoteVoucher {
|
|
144
|
+
code: string;
|
|
145
|
+
status: "applied" | "not_applied" | "unknown";
|
|
146
|
+
message?: string | undefined;
|
|
147
|
+
}
|
|
148
|
+
/** One selectable travel-insurance product from `quote.insurance.options`
|
|
149
|
+
* (kind "insurance"), verbatim fields. The backend loads these live from
|
|
150
|
+
* whichever insurer the hub setup configures, and only when that setup enables
|
|
151
|
+
* insurance at all — otherwise the SDK returns the "none" option alone and the
|
|
152
|
+
* adapter yields no `QuoteInsurance`. */
|
|
153
|
+
interface QuoteInsuranceOption {
|
|
154
|
+
id: string;
|
|
155
|
+
label: string;
|
|
156
|
+
/** SDK's own locale-formatted premium (e.g. "14,00 €"). There is no numeric
|
|
157
|
+
* amount on the public shape — the SDK keeps that internally — so a premium
|
|
158
|
+
* can be DISPLAYED but never added to another price. */
|
|
159
|
+
formattedPrice: string;
|
|
160
|
+
description?: string | undefined;
|
|
161
|
+
/** Insurer logo, when the feed provides one. The kit's insurance card does
|
|
162
|
+
* not render it; a site that wants the insurer's mark has it here. */
|
|
163
|
+
logoUrl?: string | undefined;
|
|
164
|
+
/** Plan feature names, verbatim (e.g. "Reiserücktritt"). */
|
|
165
|
+
features?: string[] | undefined;
|
|
166
|
+
/** Product documents (IPID, Bedingungen, …) — label + URL, verbatim. */
|
|
167
|
+
resources?: {
|
|
168
|
+
label: string;
|
|
169
|
+
url: string;
|
|
170
|
+
}[] | undefined;
|
|
171
|
+
/** True when the product needs per-traveler data before selection (the ERGO
|
|
172
|
+
* path: traveler forms, pre-contract, SEPA mandate, a post-booking
|
|
173
|
+
* `bookInsurance` call). False = the id alone completes the selection and
|
|
174
|
+
* the premium books WITH the rental, because `booking.book` injects
|
|
175
|
+
* `travelInsurancePlan`/`travelInsuranceAmount` from the quote for exactly
|
|
176
|
+
* these products. The checkout only offers the ones it can complete. */
|
|
177
|
+
requiresTravelers: boolean;
|
|
178
|
+
}
|
|
179
|
+
/** Travel-insurance choice on a live quote (`quote.insurance`), flattened for
|
|
180
|
+
* the checkout's radio section. Absent from the view when the feed offers no
|
|
181
|
+
* real product (options empty or "none"-only) — the section self-hides.
|
|
182
|
+
*
|
|
183
|
+
* The premium is NOT part of `card.total`: the SDK prices the stay and the
|
|
184
|
+
* policy separately, so a UI that shows both must show them as two figures.
|
|
185
|
+
* Adding them would mean parsing the PMS's formatted strings, which this file
|
|
186
|
+
* does not do for any other charge either. */
|
|
187
|
+
interface QuoteInsurance {
|
|
188
|
+
/** The SDK's own "no insurance" label, verbatim. */
|
|
189
|
+
noneLabel: string;
|
|
190
|
+
options: QuoteInsuranceOption[];
|
|
191
|
+
/** Selected product id, or null = no insurance (the default). */
|
|
192
|
+
selectedId: string | null;
|
|
193
|
+
}
|
|
194
|
+
/** The renderable view of a live GuestQuote — card + selectable options. */
|
|
195
|
+
interface QuoteView {
|
|
196
|
+
card: PriceQuote;
|
|
197
|
+
services: QuoteAddonService[];
|
|
198
|
+
policies: QuoteCancellationPolicy[];
|
|
199
|
+
/** Present once a code has been submitted and the quote re-priced; absent on
|
|
200
|
+
* a fresh quote. */
|
|
201
|
+
voucher?: QuoteVoucher | undefined;
|
|
202
|
+
/** Selectable travel insurance — absent when the feed offers no product. */
|
|
203
|
+
insurance?: QuoteInsurance | undefined;
|
|
204
|
+
}
|
|
205
|
+
type QuoteResult = {
|
|
206
|
+
status: "available";
|
|
207
|
+
/** Flattened price card (back-compat: the widget renders this). */
|
|
208
|
+
quote: PriceQuote;
|
|
209
|
+
/** The live SDK quote instance — required by booking.book. In-memory
|
|
210
|
+
* only; not serializable across navigations. */
|
|
211
|
+
live: GuestQuote;
|
|
212
|
+
/** Add-ons + policies for the checkout's optional sections. */
|
|
213
|
+
view: QuoteView;
|
|
214
|
+
} | {
|
|
215
|
+
status: "unavailable";
|
|
216
|
+
reason: string;
|
|
217
|
+
code: QuoteUnavailableCode;
|
|
218
|
+
};
|
|
219
|
+
/** Why the SDK refused to price the stay — machine-readable since website-sdk
|
|
220
|
+
* 2.16.0, and the ONLY thing callers may branch on. `reason` is localized and
|
|
221
|
+
* carries the SAME text for different codes, so comparing it is how a UI ends
|
|
222
|
+
* up telling a guest a free week is booked out.
|
|
223
|
+
*
|
|
224
|
+
* DERIVED from `GuestQuoteResult`, not re-listed: the SDK owns this set and a
|
|
225
|
+
* copy here would rot silently the day it grows a fourth member.
|
|
226
|
+
* `@v-office/sdk-core` exports the union as `GuestQuoteUnavailableCode`, but
|
|
227
|
+
* `@v-office/website-sdk` does not re-export it and sdk-core is a transitive
|
|
228
|
+
* dependency an adopting project does not declare — so `Extract` off the
|
|
229
|
+
* result type is the reachable spelling. */
|
|
230
|
+
type QuoteUnavailableCode = Extract<GuestQuoteResult, {
|
|
231
|
+
status: "unavailable";
|
|
232
|
+
}>["code"];
|
|
233
|
+
/** The two codes that mean "the stay WAS priced, but the rental's rate setup
|
|
234
|
+
* cannot be resolved to the single multi-rate a booking must be written
|
|
235
|
+
* against" — a v10 backend-configuration fault the guest has no part in.
|
|
236
|
+
*
|
|
237
|
+
* `resolveV10MultiRate` matches the priced quote's service lines against the
|
|
238
|
+
* rental's multi-rate rows for the policy, then tie-breaks on `baseRentRate`,
|
|
239
|
+
* which only fires when EXACTLY ONE tied row carries it. A rental whose PMS
|
|
240
|
+
* holds several equivalent base-rent rows per cancellation policy leaves the
|
|
241
|
+
* tie unbroken, and the SDK reports the quote unavailable rather than guess a
|
|
242
|
+
* rate — the price alone does not identify which row the booking belongs to.
|
|
243
|
+
*
|
|
244
|
+
* There is nothing an adopting site can do about the stay itself: it stays
|
|
245
|
+
* unbookable until the surplus rows are removed in the PMS. What the site owes
|
|
246
|
+
* the guest is honest copy — never the generic "not available", which asserts
|
|
247
|
+
* a booking conflict that does not exist. */
|
|
248
|
+
declare const MULTI_RATE_CODES: readonly ["AmbiguousMultiRate", "UnresolvableMultiRate"];
|
|
249
|
+
/** True when an unavailable quote is a rate-configuration fault rather than a
|
|
250
|
+
* genuinely unbookable stay. Callers branch the MESSAGE on this and must keep
|
|
251
|
+
* booking disabled either way (SDK 2.16.0 MIGRATION, "Required Consumer
|
|
252
|
+
* Work"). */
|
|
253
|
+
declare function isRateConfigurationFault(code: QuoteUnavailableCode | undefined): boolean;
|
|
254
|
+
interface QuoteOccupancy {
|
|
255
|
+
adults: number;
|
|
256
|
+
children: number;
|
|
257
|
+
childrenAges?: number[] | undefined;
|
|
258
|
+
babies: number;
|
|
259
|
+
pets: number;
|
|
260
|
+
}
|
|
261
|
+
/** Flatten a GuestQuote into the renderable view. Values verbatim.
|
|
262
|
+
*
|
|
263
|
+
* DISCOUNTS: the SDK does not hand us a discount LINE. When the PMS applies a
|
|
264
|
+
* price modifier (Frühbucher/Last-Minute, a voucher, a policy markup) it hangs
|
|
265
|
+
* it off the charge line as `modifiers[]` — label plus signed amount — and puts
|
|
266
|
+
* the pre-modifier figure in `totalBeforeModifiers`, leaving `appliedCharge`
|
|
267
|
+
* already net. Render `appliedCharge` alone (what every adoption did until now)
|
|
268
|
+
* and the saving is invisible: the guest sees only the reduced number and is
|
|
269
|
+
* never told a discount applied.
|
|
270
|
+
*
|
|
271
|
+
* So each modifier becomes its own row, and the line above it switches to
|
|
272
|
+
* `totalBeforeModifiers`. That pairing is not cosmetic — it is what makes the
|
|
273
|
+
* card add up. A deduction row under an `appliedCharge` that has ALREADY been
|
|
274
|
+
* reduced double-counts on sight: the column no longer sums to the total, and a
|
|
275
|
+
* guest checking the arithmetic finds it wrong. Gross line, then the deduction,
|
|
276
|
+
* then the remaining charges: 1.694,00 − 140,00 + 42,70 = 1.596,70.
|
|
277
|
+
*
|
|
278
|
+
* `bookingCardInformation.totalBeforeModifiers` is deliberately NOT rendered.
|
|
279
|
+
* The deduction rows already carry the saving, and a struck-through "was" price
|
|
280
|
+
* would state it a second time — as the one figure on the card that cannot be
|
|
281
|
+
* checked by adding, and as a price claim the customer would have to stand
|
|
282
|
+
* behind. The total stays the single net number.
|
|
283
|
+
*/
|
|
284
|
+
declare function adaptGuestQuote(quote: GuestQuote): QuoteView;
|
|
285
|
+
/** Price a stay for `rentalId`. The caller owns the SDK instance and its
|
|
286
|
+
* lifecycle — pass a shared one to reuse its queue + cache across the caller's
|
|
287
|
+
* lifetime. `voucher` re-prices with a code (the PMS decides what it's worth). */
|
|
288
|
+
declare function fetchQuote(rentalId: string, period: {
|
|
289
|
+
start: string;
|
|
290
|
+
end: string;
|
|
291
|
+
}, occupancy: QuoteOccupancy, sdk: WebsiteSdk, voucher?: string): Promise<QuoteResult>;
|
|
292
|
+
type QuoteSelectionResult = {
|
|
293
|
+
ok: true;
|
|
294
|
+
quote: GuestQuote;
|
|
295
|
+
} | {
|
|
296
|
+
ok: false;
|
|
297
|
+
reason?: string | undefined;
|
|
298
|
+
};
|
|
299
|
+
declare function addQuoteService(quote: GuestQuote, id: string, sdk: WebsiteSdk): Promise<QuoteSelectionResult>;
|
|
300
|
+
declare function removeQuoteService(quote: GuestQuote, id: string, sdk: WebsiteSdk): Promise<QuoteSelectionResult>;
|
|
301
|
+
/** Drop every additional-service selection from `quote`.
|
|
302
|
+
*
|
|
303
|
+
* A fresh quote's selection state is not knowable from the service list. It is
|
|
304
|
+
* empty by default and carries the PMS's proposed quantities only when the
|
|
305
|
+
* project sets `shouldAddProposedAdditionalServiceAmount: true` — an SDK
|
|
306
|
+
* option a component cannot see — and the quote will not say which happened
|
|
307
|
+
* (`GuestQuote.selections` is behind an unexported symbol). Rather than branch
|
|
308
|
+
* on something invisible, a caller that needs a known starting point clears
|
|
309
|
+
* first and steps up from zero with `addQuoteService`. */
|
|
310
|
+
declare function clearQuoteServices(quote: GuestQuote, sdk: WebsiteSdk): Promise<QuoteSelectionResult>;
|
|
311
|
+
declare function selectQuotePolicy(quote: GuestQuote, id: string, sdk: WebsiteSdk): Promise<QuoteSelectionResult>;
|
|
312
|
+
/** Select a travel-insurance product (`id`) or none (`null`). Unlike the
|
|
313
|
+
* service/policy calls the SDK returns the new GuestQuote directly and throws
|
|
314
|
+
* on failure (unknown id) — normalized here to the same QuoteSelectionResult
|
|
315
|
+
* the other selection helpers use, so a stale id is a silent no-op the caller
|
|
316
|
+
* handles by keeping the previous quote.
|
|
317
|
+
*
|
|
318
|
+
* For a product that needs no traveler data this is the COMPLETE selection:
|
|
319
|
+
* the premium books with the rental via `booking.book`, which reads the plan
|
|
320
|
+
* off the quote. A product with `requiresTravelers` needs the pre-contract /
|
|
321
|
+
* payment / `bookInsurance` steps on top, which the checkout does not
|
|
322
|
+
* implement — it offers only the products it can complete. */
|
|
323
|
+
declare function selectQuoteInsurance(quote: GuestQuote, id: string | null, sdk: WebsiteSdk): Promise<QuoteSelectionResult>;
|
|
324
|
+
/**
|
|
325
|
+
* The concrete "free cancellation until" date for ONE policy — DERIVED, not
|
|
326
|
+
* fabricated: the PMS states its rules relatively (`daysBeforeArrival` +
|
|
327
|
+
* `refundPercentage`), so with the chosen arrival in hand the deadline is
|
|
328
|
+
* plain arithmetic (arrival − daysBeforeArrival), the same category of
|
|
329
|
+
* derivation as computing nights from a range.
|
|
330
|
+
*
|
|
331
|
+
* Uses the policy's full-refund (100%) rule. Returns null when the policy has
|
|
332
|
+
* no full-refund rule OR the derived date is already in the past (booking
|
|
333
|
+
* inside the window — advertising a bygone deadline would be wrong).
|
|
334
|
+
*/
|
|
335
|
+
/** The policy's full-refund rule, if it has one. The refund figure is the SDK's
|
|
336
|
+
* own formatted string ("100%"), so this reads it rather than comparing a
|
|
337
|
+
* number — the one place that pattern is written, because both the derived
|
|
338
|
+
* deadline and the "is this a refundable rate at all" question depend on it. */
|
|
339
|
+
declare function fullRefundRule(policy: QuoteCancellationPolicy | undefined): QuotePolicyRule | undefined;
|
|
340
|
+
declare function policyFreeCancellationUntil(policy: QuoteCancellationPolicy | undefined, arrival: Date): Date | null;
|
|
341
|
+
/** The deadline for the stay as it stands — i.e. of the SELECTED policy.
|
|
342
|
+
* Kept as its own name because that is what a summary line means: not "some
|
|
343
|
+
* policy allows this" but "what you have chosen allows this". */
|
|
344
|
+
declare function freeCancellationUntil(policies: QuoteCancellationPolicy[], arrival: Date): Date | null;
|
|
345
|
+
/** The same stay priced under ONE cancellation policy. `quote` is a real
|
|
346
|
+
* GuestQuote branched off the base, so the UI can show its card, and hand the
|
|
347
|
+
* branch on to a later step, without re-quoting the stay. */
|
|
348
|
+
interface PolicyBranch {
|
|
349
|
+
id: string;
|
|
350
|
+
/** The branch quote — the base itself for the already-selected policy. */
|
|
351
|
+
quote: GuestQuote;
|
|
352
|
+
/** That quote flattened, so `view.card.total` is THIS policy's real price. */
|
|
353
|
+
view: QuoteView;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Price every selectable cancellation policy off ONE base quote, so a guest can
|
|
357
|
+
* compare the policies by what each actually costs rather than by a percentage.
|
|
358
|
+
*
|
|
359
|
+
* WHY REAL BRANCHES AND NOT ARITHMETIC. `markUpPercentage` looks like enough to
|
|
360
|
+
* compute the alternative price, and it is not: the modifier applies to the
|
|
361
|
+
* RENT, while taxes and fees ride along untouched, so the change it makes to
|
|
362
|
+
* the total is always smaller than the percentage says — and a guest who checks
|
|
363
|
+
* the number against the total would find the site's own figure wrong. The
|
|
364
|
+
* arithmetic is unavailable in any case: every amount the SDK returns is a
|
|
365
|
+
* locale-formatted string, so computing on one means parsing it back, which
|
|
366
|
+
* this file does for no other charge either. What the PMS prices is what the UI
|
|
367
|
+
* shows.
|
|
368
|
+
*
|
|
369
|
+
* `selectCancellationPolicy` does NOT mutate its input — it returns a new quote
|
|
370
|
+
* — so each policy can be branched off the same base and the branches held side
|
|
371
|
+
* by side. Costs one SDK call per policy MINUS one: the already-selected policy
|
|
372
|
+
* is the base quote itself. Sequential, because the API allows 1 req/s.
|
|
373
|
+
*
|
|
374
|
+
* A policy the PMS marks `available: false` is skipped rather than attempted:
|
|
375
|
+
* selecting it would fail by definition, and a burnt request inside a 1 req/s
|
|
376
|
+
* budget delays the ones that can succeed. It simply gets no branch, and the UI
|
|
377
|
+
* renders it as the unselectable row it already is.
|
|
378
|
+
*/
|
|
379
|
+
declare function pricePolicyBranches(base: GuestQuote, sdk: WebsiteSdk): Promise<PolicyBranch[]>;
|
|
380
|
+
//#endregion
|
|
381
|
+
//#region src/booking.d.ts
|
|
382
|
+
/** Where the payment provider sends the guest back after an online payment
|
|
383
|
+
* (relative — the SDK/PSP resolves it against the site origin). */
|
|
384
|
+
declare const CHECKOUT_RETURN_PATH = "/buchen/bestaetigung";
|
|
385
|
+
/** The checkout form's flat contact fields (the UI shape). `toBookingContact`
|
|
386
|
+
* folds them into the SDK's nested BookingContact. */
|
|
387
|
+
interface BookingContactForm {
|
|
388
|
+
forename: string;
|
|
389
|
+
surname: string;
|
|
390
|
+
/** Anrede ("Frau" / "Herr"); omitted when the guest picks none. */
|
|
391
|
+
title?: string | undefined;
|
|
392
|
+
street: string;
|
|
393
|
+
housenumber?: string | undefined;
|
|
394
|
+
postalcode: string;
|
|
395
|
+
city: string;
|
|
396
|
+
/** ISO 3166-1 alpha-2, e.g. "DE". */
|
|
397
|
+
countryCode: string;
|
|
398
|
+
email: string;
|
|
399
|
+
phone?: string | undefined;
|
|
400
|
+
note?: string | undefined;
|
|
401
|
+
}
|
|
402
|
+
interface PaymentLine {
|
|
403
|
+
label: string;
|
|
404
|
+
amount: string;
|
|
405
|
+
}
|
|
406
|
+
/** One payment entry, verbatim — a contractual step (Anzahlung / Restzahlung /
|
|
407
|
+
* Rate / Kaution) or a way to settle the whole outstanding balance. */
|
|
408
|
+
interface PaymentScheduleView {
|
|
409
|
+
/** SDK's stable kind, for programmatic behaviour ONLY. Everything the guest
|
|
410
|
+
* reads comes from `label`: the SDK translates it, and a v10 installment is
|
|
411
|
+
* not automatically a v9 prepayment. */
|
|
412
|
+
kind: BookingPaymentScheduleKind | "full_amount";
|
|
413
|
+
label: string;
|
|
414
|
+
/** Complete translated phrase, e.g. "Zahlbar bis zum 6. Februar 2027".
|
|
415
|
+
* Render as-is — never parse it, never prefix another due-date label. It
|
|
416
|
+
* stopped being a bare date in website-sdk 2.13.0, and it was never reliably
|
|
417
|
+
* ISO before that either. */
|
|
418
|
+
dueOn?: string | undefined;
|
|
419
|
+
/** Where the payment happens, translated (v10 supplies it when the backend
|
|
420
|
+
* states a payment location). Same rule: render, don't reformat. */
|
|
421
|
+
paymentAt?: string | undefined;
|
|
422
|
+
lines: PaymentLine[];
|
|
423
|
+
amount: string;
|
|
424
|
+
/** The PMS's own payment options — the SDK union (bank_transfer / redirect /
|
|
425
|
+
* form_post / stripe_checkout), untouched. **EMPTY means informational:**
|
|
426
|
+
* the SDK cannot perform this payment right now (a future step, on-site, or
|
|
427
|
+
* external), so the card must not offer a button for it. */
|
|
428
|
+
options: BookingPaymentOption[];
|
|
429
|
+
}
|
|
430
|
+
/** A travel-insurance premium still to be settled WITH THE INSURER, from
|
|
431
|
+
* `book().insurancePaymentOptions` — label and URL verbatim. Deliberately not
|
|
432
|
+
* a `PaymentScheduleView`: those are the PMS's own steps, owed to the site's
|
|
433
|
+
* operator and payable through the SDK. This one is a different contract with
|
|
434
|
+
* a different recipient, and it leaves the site entirely. */
|
|
435
|
+
interface InsurancePaymentLink {
|
|
436
|
+
label: string;
|
|
437
|
+
url: string;
|
|
438
|
+
description?: string | undefined;
|
|
439
|
+
}
|
|
440
|
+
interface BookingConfirmation {
|
|
441
|
+
bookingNumber: string;
|
|
442
|
+
/** The contractual steps, in backend order — what the guest owes and when. */
|
|
443
|
+
schedules: PaymentScheduleView[];
|
|
444
|
+
/** Ways to settle the COMPLETE outstanding balance at once (kind
|
|
445
|
+
* `full_amount`), an ALTERNATIVE to the schedule above rather than an
|
|
446
|
+
* addition to it. Kept apart because summing the two would double-count, and
|
|
447
|
+
* because a synthesized alternative carries no deadline of its own — the
|
|
448
|
+
* schedule's steps are where the dates live. */
|
|
449
|
+
alternatives: PaymentScheduleView[];
|
|
450
|
+
/** Empty unless insurance was actually booked AND the insurer returned a
|
|
451
|
+
* payment page for it. Kept out of `schedules` and `alternatives` on
|
|
452
|
+
* purpose: the premium is in neither, so a card that listed it among them
|
|
453
|
+
* would misstate what is owed to whom (CONTRACT.md, "Travel insurance"). */
|
|
454
|
+
insurancePayments: InsurancePaymentLink[];
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Create the booking. `quote` MUST be the live GuestQuote instance (with any
|
|
458
|
+
* service/policy selections applied). USER-initiated only — this reserves for
|
|
459
|
+
* real. Throws on transport/backend errors (the caller renders an honest
|
|
460
|
+
* retry message; nothing was necessarily booked when it throws).
|
|
461
|
+
*/
|
|
462
|
+
declare function submitBooking(quote: GuestQuote, contact: BookingContactForm, sdk: WebsiteSdk): Promise<BookingConfirmation>;
|
|
463
|
+
/** True for the option kinds `submitPaymentOption` can perform in-browser
|
|
464
|
+
* (redirect / form_post / stripe_checkout). Bank transfer is display-only. */
|
|
465
|
+
declare function isSubmittableOption(option: BookingPaymentOption): option is SubmittableBookingPaymentOption;
|
|
466
|
+
/** Perform an online payment option (redirects the browser to the PSP).
|
|
467
|
+
* USER-initiated only.
|
|
468
|
+
*
|
|
469
|
+
* Logged on failure, and this is the worst place in the flow to lose a reason:
|
|
470
|
+
* by the time a payment option is on screen THE RESERVATION ALREADY EXISTS
|
|
471
|
+
* (the SDK's `booking.md` says as much), so a failure here is an unpaid
|
|
472
|
+
* booking, not a failed one. The checkout's catch discards the error entirely
|
|
473
|
+
* — `.catch(() => setPayError(true))` — so without this a guest who cannot
|
|
474
|
+
* pay leaves no trace at all, on a booking someone now has to chase. */
|
|
475
|
+
declare function payWithOption(option: SubmittableBookingPaymentOption, sdk: WebsiteSdk): Promise<void>;
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/booking-snapshot.d.ts
|
|
478
|
+
/** Rental identity, parked by the checkout page (which has it from SSR) and
|
|
479
|
+
* folded into the snapshot at book time — the SDK seam that writes the
|
|
480
|
+
* snapshot never sees a rental name or a link. */
|
|
481
|
+
declare const CHECKOUT_CONTEXT_KEY = "voffice:checkout-context";
|
|
482
|
+
/** The finished record `/buchen/bestaetigung` reads. */
|
|
483
|
+
declare const BOOKING_SNAPSHOT_KEY = "voffice:booking-return";
|
|
484
|
+
/** One gallery image, in the SDK's own shape (`RentalGalleryImage` minus the
|
|
485
|
+
* fields this page does not render). Stored so the confirmation can show the
|
|
486
|
+
* unit rather than describe it — the guest booked a place, not a table. */
|
|
487
|
+
interface SnapshotImage {
|
|
488
|
+
src: string;
|
|
489
|
+
srcset?: string | undefined;
|
|
490
|
+
alt: string;
|
|
491
|
+
}
|
|
492
|
+
interface CheckoutContext {
|
|
493
|
+
rentalName: string;
|
|
494
|
+
location?: string | undefined;
|
|
495
|
+
href?: string | undefined;
|
|
496
|
+
/** Capped at the same five the checkout's own summary strip shows. */
|
|
497
|
+
images?: SnapshotImage[] | undefined;
|
|
498
|
+
}
|
|
499
|
+
interface BookingSnapshotGuests {
|
|
500
|
+
adults: number;
|
|
501
|
+
children: number;
|
|
502
|
+
babies: number;
|
|
503
|
+
pets: number;
|
|
504
|
+
}
|
|
505
|
+
/** Every field but `bookingNumber` self-hides when absent — the return page
|
|
506
|
+
* renders a row only for what it actually has, never an empty shell
|
|
507
|
+
* (`DATA_SCHEMA.md`: "Optional field gates optional UI"). */
|
|
508
|
+
interface BookingSnapshot {
|
|
509
|
+
bookingNumber: string;
|
|
510
|
+
rentalName?: string | undefined;
|
|
511
|
+
location?: string | undefined;
|
|
512
|
+
href?: string | undefined;
|
|
513
|
+
images?: SnapshotImage[] | undefined;
|
|
514
|
+
/** `YYYY-MM-DD` — the vocabulary the whole funnel already carries. */
|
|
515
|
+
start?: string | undefined;
|
|
516
|
+
end?: string | undefined;
|
|
517
|
+
guests?: BookingSnapshotGuests | undefined;
|
|
518
|
+
/** The SDK's own formatted total, verbatim. Never re-derived, never summed. */
|
|
519
|
+
total?: string | undefined;
|
|
520
|
+
}
|
|
521
|
+
/** Park the rental's identity for the return page. Called by the checkout page
|
|
522
|
+
* on load — it is the last place in the flow that knows the rental by name. */
|
|
523
|
+
declare function saveCheckoutContext(context: CheckoutContext): void;
|
|
524
|
+
/**
|
|
525
|
+
* Write the return page's record. Called by `submitBooking` the moment the
|
|
526
|
+
* reservation exists — EARLIER than the SDK's "before submitting a payment
|
|
527
|
+
* option", because that is when the booking number is born, and a guest who
|
|
528
|
+
* settles by bank transfer never leaves the checkout at all.
|
|
529
|
+
*
|
|
530
|
+
* Never throws. The snapshot is a nicety; the booking is not.
|
|
531
|
+
*/
|
|
532
|
+
declare function saveBookingSnapshot(bookingNumber: string, quote: GuestQuote): void;
|
|
533
|
+
//#endregion
|
|
534
|
+
//#region src/checkout/insurance.d.ts
|
|
535
|
+
/** The plan's documents as the checkout treats them: what it lists, what the
|
|
536
|
+
* guest must open, and what makes a plan sellable at all read the SAME list,
|
|
537
|
+
* so a hidden document can never end up required-but-unlisted (which would be
|
|
538
|
+
* a gate the guest cannot close). */
|
|
539
|
+
declare function shownInsuranceResources(option: QuoteInsuranceOption): {
|
|
540
|
+
label: string;
|
|
541
|
+
url: string;
|
|
542
|
+
}[];
|
|
543
|
+
/** The insurance products a checkout can actually complete. Two gates, and
|
|
544
|
+
* this filter is where the layer's capability is declared — a site that adds a
|
|
545
|
+
* missing flow adds it HERE, upstream, never in its own copy.
|
|
546
|
+
*
|
|
547
|
+
* 1. **Technical.** Products that select with the id alone and book WITH the
|
|
548
|
+
* rental booking. One that demands per-traveler data first (traveler forms,
|
|
549
|
+
* pre-contract, SEPA mandate, a post-booking insurance call) needs a flow
|
|
550
|
+
* this layer does not implement — offering it would dead-end the guest.
|
|
551
|
+
*
|
|
552
|
+
* 2. **Legal.** The product must carry its `resources`, the insurer's
|
|
553
|
+
* pre-contractual documents (see "Mandatory pre-contractual documents"
|
|
554
|
+
* below for which duties they discharge). A product the feed sends WITHOUT
|
|
555
|
+
* them cannot be concluded lawfully here, so it self-hides rather than
|
|
556
|
+
* being sold undocumented — the same rule as (1), applied to a legal
|
|
557
|
+
* precondition instead of a technical one. */
|
|
558
|
+
declare function selectableInsuranceOptions(insurance: QuoteInsurance): QuoteInsuranceOption[];
|
|
559
|
+
/** The selected plan, but only when it is one the guest could actually pick
|
|
560
|
+
* here — so no price row can name a product the section above never showed. */
|
|
561
|
+
declare function selectedInsuranceOption(insurance: QuoteInsurance | undefined): QuoteInsuranceOption | undefined;
|
|
562
|
+
/** The identity of an insurance PRODUCT, as opposed to the id of one priced
|
|
563
|
+
* variant of it.
|
|
564
|
+
*
|
|
565
|
+
* An insurer may band its tariffs by trip value, and then a plan id is not the
|
|
566
|
+
* product — it is the product AT one price. The same tariff comes back under a
|
|
567
|
+
* new id and a new premium the moment the total crosses a band. URV is the
|
|
568
|
+
* observed case: "RRV ohne SB Deutschland" is `RROR0625` at 56,00 € up to
|
|
569
|
+
* roughly 1.250 €, `RROR0725` at 65,00 € above that, `RROR0825` at 74,00 €
|
|
570
|
+
* above that again. Every extra, every policy markup and every stay change can
|
|
571
|
+
* cross a band, and each of them re-prices the quote — which is what makes the
|
|
572
|
+
* SDK re-fetch the insurer's plan list against the new total.
|
|
573
|
+
*
|
|
574
|
+
* The label is what survives that: the same string in every band, and the name
|
|
575
|
+
* the guest actually chose, since it is what the radio shows. */
|
|
576
|
+
declare function insuranceProductKey(option: QuoteInsuranceOption): string;
|
|
577
|
+
/** The plan id to re-apply onto a re-priced quote, or null when there is
|
|
578
|
+
* nothing to do — no choice, already applied, or a product this feed no longer
|
|
579
|
+
* offers as a selectable one. The last case is why the feed is checked rather
|
|
580
|
+
* than the call simply attempted: a doomed selection would spend one of the
|
|
581
|
+
* rate-limited requests to be refused, and the guest ends up on "keine
|
|
582
|
+
* Versicherung" either way.
|
|
583
|
+
*
|
|
584
|
+
* Matched by id first, then by product (see insuranceProductKey). That
|
|
585
|
+
* fallback is what keeps a banded re-price from silently un-selecting
|
|
586
|
+
* insurance the feed still sells: the SDK carries a selection across its
|
|
587
|
+
* refresh only on an exact id match, so a renamed-but-identical tariff reads
|
|
588
|
+
* to it as a plan that vanished, and a guest who only added a towel package
|
|
589
|
+
* lands back on "keine Versicherung". Re-applying picks the SAME product at
|
|
590
|
+
* its new premium — the one they would in fact be buying — and that premium is
|
|
591
|
+
* on screen in the radio, in the summary rail and above the submit button, so
|
|
592
|
+
* nothing about the change is hidden.
|
|
593
|
+
*
|
|
594
|
+
* Checked against the SELECTABLE options, not the raw list: a plan the fresh
|
|
595
|
+
* feed carries without its documents (or newly demanding traveler data) is one
|
|
596
|
+
* the section no longer shows, so re-applying it would leave the quote
|
|
597
|
+
* carrying an insurance the page neither displays nor can take a document
|
|
598
|
+
* confirmation for. */
|
|
599
|
+
declare function insuranceIdToReapply(fresh: QuoteView, wanted: QuoteInsuranceOption | undefined): string | null;
|
|
600
|
+
/** Progress through the document step. Held by the orchestration hook because
|
|
601
|
+
* the submit button, a card away, is gated on it. */
|
|
602
|
+
interface InsuranceDocsState {
|
|
603
|
+
/** URLs opened so far — keyed by URL, not by plan. Plans routinely share
|
|
604
|
+
* every file but the IPID, and having received an identical document does
|
|
605
|
+
* not un-happen when the guest switches tariff. Keying by URL also survives
|
|
606
|
+
* a re-quote, which returns fresh option objects for the same product. */
|
|
607
|
+
opened: ReadonlySet<string>;
|
|
608
|
+
/** Product whose documents the guest confirmed receiving (keyed by
|
|
609
|
+
* insuranceProductKey, not by plan id); null = none yet. Product-bound on
|
|
610
|
+
* purpose, so a tariff switch demands a fresh acknowledgement while the
|
|
611
|
+
* opened files above carry over. By PRODUCT rather than by plan for the same
|
|
612
|
+
* reason the selection is carried by product: a banded re-price hands back
|
|
613
|
+
* the identical documents under a new plan id, and making the guest re-tick
|
|
614
|
+
* a box for files they already have — because a towel package moved the
|
|
615
|
+
* total — would be friction with nothing behind it. */
|
|
616
|
+
confirmedProduct: string | null;
|
|
617
|
+
}
|
|
618
|
+
declare const NO_INSURANCE_DOCS: InsuranceDocsState;
|
|
619
|
+
/** True while a chosen plan's document step is unfinished. No plan chosen (or
|
|
620
|
+
* "keine Versicherung") is never blocking — the duty attaches to the insurance
|
|
621
|
+
* contract, not to the stay. */
|
|
622
|
+
declare function insuranceDocsPending(option: QuoteInsuranceOption | undefined, docs: InsuranceDocsState): boolean;
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region src/checkout/stay.d.ts
|
|
625
|
+
interface Stay {
|
|
626
|
+
start: Date;
|
|
627
|
+
end: Date;
|
|
628
|
+
adults: number;
|
|
629
|
+
children: number;
|
|
630
|
+
childrenAges: number[];
|
|
631
|
+
babies: number;
|
|
632
|
+
pets: number;
|
|
633
|
+
}
|
|
634
|
+
/** Date → `YYYY-MM-DD` in LOCAL time — the vocabulary the URL handover and the
|
|
635
|
+
* SDK both use. Local on purpose (matches date-fns `format(date,
|
|
636
|
+
* "yyyy-MM-dd")`, which the sites' own lib/dates.ts uses): a calendar day
|
|
637
|
+
* picked in the browser must not shift across midnight UTC. */
|
|
638
|
+
declare function isoDate(date: Date): string;
|
|
639
|
+
declare function occupancyOf(stay: Stay): QuoteOccupancy;
|
|
640
|
+
/** The identity of a stay for "is the shown price still the shown stay"
|
|
641
|
+
* comparisons — every field that changes the quote, in one string. */
|
|
642
|
+
declare function stayKeyOf(stay: Stay): string;
|
|
643
|
+
/** The stay as URL search params (`start`/`end`/`adults`/… — the same
|
|
644
|
+
* vocabulary the rental page reads + writes). */
|
|
645
|
+
declare function stayParams(stay: Stay): string;
|
|
646
|
+
//#endregion
|
|
647
|
+
//#region src/checkout/createCheckoutFlow.d.ts
|
|
648
|
+
/**
|
|
649
|
+
* The checkout's orchestration — everything between "a complete stay is in
|
|
650
|
+
* hand" and "the guest is booked and paying", with no markup and no framework.
|
|
651
|
+
*
|
|
652
|
+
* The flow owns the quote lifecycle (initial load, re-quote on stay change,
|
|
653
|
+
* supersession), the selection mutations (extras / insurance / voucher /
|
|
654
|
+
* policy re-apply), the § 7 VVG document gate, booking submit and payment —
|
|
655
|
+
* the parts that must be correct on every site and update mechanically. The
|
|
656
|
+
* UI owns stay EDITING (calendar, guest panel, URL read/write), copy, and
|
|
657
|
+
* every pixel. A UI subscribes to `subscribe`, reads `getState`, calls the
|
|
658
|
+
* actions, and feeds stay/policy changes through `update` (React: one
|
|
659
|
+
* useSyncExternalStore — see the README).
|
|
660
|
+
*
|
|
661
|
+
* ONE flow instance owns the whole thing because the live `GuestQuote` that
|
|
662
|
+
* `booking.book` requires is not serializable across a navigation: the
|
|
663
|
+
* consumer re-quotes from the URL handover on mount and the flow keeps the
|
|
664
|
+
* instance in memory through extras / policy / voucher selections until
|
|
665
|
+
* book().
|
|
666
|
+
*
|
|
667
|
+
* ⚠ `book()` creates a REAL booking; `pay()` starts a REAL payment. Both are
|
|
668
|
+
* strictly user-initiated — never automate them.
|
|
669
|
+
*/
|
|
670
|
+
type CheckoutPhase = {
|
|
671
|
+
kind: "loading";
|
|
672
|
+
} | {
|
|
673
|
+
kind: "ready";
|
|
674
|
+
} | {
|
|
675
|
+
kind: "unavailable";
|
|
676
|
+
reason: string;
|
|
677
|
+
} | {
|
|
678
|
+
kind: "error";
|
|
679
|
+
};
|
|
680
|
+
/** The user-facing strings the flow itself needs — the reasons it reports when
|
|
681
|
+
* the backend gives none, or gives one that would be a false statement. Copy,
|
|
682
|
+
* so it stays per-site. */
|
|
683
|
+
interface CheckoutFlowMessages {
|
|
684
|
+
/** Shown INSTEAD of the SDK's localized unavailability reason when the quote
|
|
685
|
+
* failed on a rate-configuration fault (AmbiguousMultiRate /
|
|
686
|
+
* UnresolvableMultiRate) — the stay is free; only the price cannot be
|
|
687
|
+
* resolved, so the availability wording would be a false statement. */
|
|
688
|
+
rateFault: string;
|
|
689
|
+
/** Fallback when an unavailable result carries no reason of its own. */
|
|
690
|
+
unavailable: string;
|
|
691
|
+
/** Inline issue line when a RE-quote (not the initial load) failed on transport. */
|
|
692
|
+
quoteRefreshFailed: string;
|
|
693
|
+
/** Voucher could not be applied at all (transport / stay unavailable). */
|
|
694
|
+
voucherFailed: string;
|
|
695
|
+
/** Voucher was processed and rejected / not recognized. */
|
|
696
|
+
voucherRejected: string;
|
|
697
|
+
}
|
|
698
|
+
interface CheckoutFlowOptions {
|
|
699
|
+
rentalId: string;
|
|
700
|
+
/** The site's SDK accessor (one instance for the flow's lifetime — quote +
|
|
701
|
+
* selections + book share its queue and 1 req/s rate limit). The flow never
|
|
702
|
+
* disposes it; the owner does. */
|
|
703
|
+
getSdk: () => WebsiteSdk;
|
|
704
|
+
/** The current COMPLETE stay, or null while none is in hand (mid-edit, URL
|
|
705
|
+
* handover missing). The flow re-quotes whenever it changes. */
|
|
706
|
+
stay: Stay | null;
|
|
707
|
+
/** The cancellation rate chosen a step earlier (`?policy=` handover). A SEED
|
|
708
|
+
* for the first quote, not the source of truth: once a quote exists, the
|
|
709
|
+
* live quote's own `selected` flag is. Null means "whatever the PMS
|
|
710
|
+
* pre-selects", which is also what an unknown id degrades to. */
|
|
711
|
+
chosenPolicy: string | null;
|
|
712
|
+
/**
|
|
713
|
+
* Which of a fresh quote's policies may be treated as OFFERED for the
|
|
714
|
+
* `chosenPolicy` re-apply — REQUIRED, because this is the enforcement point
|
|
715
|
+
* for the whole offer-withdrawal rule: a rate stops being an offer when the
|
|
716
|
+
* stay moves inside the standard rate's cancellation window, at which point
|
|
717
|
+
* the cheaper non-refundable one is the identical booking for less. Every
|
|
718
|
+
* route onto the checkout passes through this check (a stale link, a
|
|
719
|
+
* hand-built `?policy=`, an in-page date edit), so all of them lapse to the
|
|
720
|
+
* PMS pre-selection together.
|
|
721
|
+
*/
|
|
722
|
+
filterOfferedPolicies: (policies: QuoteCancellationPolicy[], arrival: Date) => QuoteCancellationPolicy[];
|
|
723
|
+
messages: CheckoutFlowMessages;
|
|
724
|
+
/** Called when a quote returns `AmbiguousMultiRate` / `UnresolvableMultiRate`
|
|
725
|
+
* — a PMS rate-configuration fault, not a booked-out stay. Route these to
|
|
726
|
+
* monitoring or site operations. */
|
|
727
|
+
onRateConfigurationFault?: ((info: {
|
|
728
|
+
code: QuoteUnavailableCode;
|
|
729
|
+
rentalId: string;
|
|
730
|
+
}) => void) | undefined;
|
|
731
|
+
}
|
|
732
|
+
interface CheckoutIssue {
|
|
733
|
+
/** The stayKey the issue belongs to — a different stay re-arms the loop. */
|
|
734
|
+
key: string;
|
|
735
|
+
reason: string;
|
|
736
|
+
/** Transport errors offer a manual retry; unavailability does not. */
|
|
737
|
+
retryable: boolean;
|
|
738
|
+
}
|
|
739
|
+
/** Everything a UI renders. A new object per change; unchanged otherwise, so
|
|
740
|
+
* `useSyncExternalStore` and friends can compare by reference. */
|
|
741
|
+
interface CheckoutFlowState {
|
|
742
|
+
phase: CheckoutPhase;
|
|
743
|
+
view: QuoteView | null;
|
|
744
|
+
counts: Record<string, number>;
|
|
745
|
+
busy: boolean;
|
|
746
|
+
issue: CheckoutIssue | null;
|
|
747
|
+
selectionError: string | null;
|
|
748
|
+
quotedKey: string | null;
|
|
749
|
+
stayKey: string | null;
|
|
750
|
+
/** A re-quote is pending: the shown price does not match the shown stay. */
|
|
751
|
+
pricePending: boolean;
|
|
752
|
+
/** Booking is blocked (stale price, in-flight mutation, issue, or documents pending). */
|
|
753
|
+
blocked: boolean;
|
|
754
|
+
chosenInsurance: QuoteInsuranceOption | undefined;
|
|
755
|
+
insuranceDocs: InsuranceDocsState;
|
|
756
|
+
documentsPending: boolean;
|
|
757
|
+
voucherApplied: string | null;
|
|
758
|
+
voucherError: string | null;
|
|
759
|
+
bookState: "idle" | "submitting" | "error";
|
|
760
|
+
confirmation: BookingConfirmation | null;
|
|
761
|
+
payBusyKey: string | null;
|
|
762
|
+
payError: boolean;
|
|
763
|
+
}
|
|
764
|
+
/** The actions a UI calls. Stable functions for the flow's lifetime. */
|
|
765
|
+
interface CheckoutFlowActions {
|
|
766
|
+
changeService(id: string, delta: 1 | -1): void;
|
|
767
|
+
selectInsurance(id: string | null): void;
|
|
768
|
+
openInsuranceDoc(url: string): void;
|
|
769
|
+
confirmInsuranceDocs(productKey: string, confirmed: boolean): void;
|
|
770
|
+
applyVoucher(code: string | null): void;
|
|
771
|
+
/** ⚠ USER-initiated only: creates a real booking. */
|
|
772
|
+
book(contact: BookingContactForm): void;
|
|
773
|
+
/** ⚠ USER-initiated only: redirects to the real payment provider. */
|
|
774
|
+
pay(key: string, option: PaymentScheduleView["options"][number]): void;
|
|
775
|
+
clearIssue(): void;
|
|
776
|
+
}
|
|
777
|
+
interface CheckoutFlow extends Store<CheckoutFlowState>, CheckoutFlowActions {
|
|
778
|
+
/** Feed the UI's current inputs. `stay` and `chosenPolicy` drive the
|
|
779
|
+
* re-quote; messages and callbacks are read at call time. */
|
|
780
|
+
update(options: CheckoutFlowOptions): void;
|
|
781
|
+
/** Arm the auto-quote. Idempotent; call once the UI is mounted. */
|
|
782
|
+
start(): void;
|
|
783
|
+
/** Cancel pending work and drop every in-flight result. */
|
|
784
|
+
dispose(): void;
|
|
785
|
+
}
|
|
786
|
+
/** What a UI binding hands to the rendering code: the state plus the actions. */
|
|
787
|
+
type CheckoutFlowApi = CheckoutFlowState & CheckoutFlowActions;
|
|
788
|
+
declare function createCheckoutFlow(initialOptions: CheckoutFlowOptions): CheckoutFlow;
|
|
789
|
+
//#endregion
|
|
790
|
+
//#region src/checkout/card-order.d.ts
|
|
791
|
+
declare function stabilizeCardOrder(view: QuoteView, order: Map<string, number>): QuoteView;
|
|
792
|
+
//#endregion
|
|
793
|
+
//#region src/checkout/validation.d.ts
|
|
794
|
+
/**
|
|
795
|
+
* Contact-form field validation — the checkout's data-quality behavior,
|
|
796
|
+
* framework-free.
|
|
797
|
+
*
|
|
798
|
+
* Extracted from the sites' checkout island (`CheckoutFlow.tsx`). The RULES live here so every site validates a booking
|
|
799
|
+
* contact the same way; the MESSAGES are copy and stay per-site — every
|
|
800
|
+
* function takes them as a parameter (the site's `CheckoutFlowCopy` object
|
|
801
|
+
* satisfies `ValidationMessages` structurally, so skins pass their merged copy
|
|
802
|
+
* straight through).
|
|
803
|
+
*/
|
|
804
|
+
/** The subset of the checkout copy the validators read. A site's full copy
|
|
805
|
+
* object satisfies this structurally. */
|
|
806
|
+
interface ValidationMessages {
|
|
807
|
+
forenameRequired: string;
|
|
808
|
+
surnameRequired: string;
|
|
809
|
+
streetRequired: string;
|
|
810
|
+
cityRequired: string;
|
|
811
|
+
postalcodeRequired: string;
|
|
812
|
+
emailRequired: string;
|
|
813
|
+
emailInvalid: string;
|
|
814
|
+
phoneInvalid: string;
|
|
815
|
+
phoneNeedsDialCode: string;
|
|
816
|
+
postalcodeInvalidDE: string;
|
|
817
|
+
postalcodeInvalidAT: string;
|
|
818
|
+
postalcodeInvalidCH: string;
|
|
819
|
+
streetHouseNumberWarning: string;
|
|
820
|
+
}
|
|
821
|
+
declare function normalizePhone(raw: string, dial?: string): string;
|
|
822
|
+
declare function phoneError(raw: string, dial: string, c: ValidationMessages): string | undefined;
|
|
823
|
+
declare function emailError(raw: string, c: ValidationMessages): string | undefined;
|
|
824
|
+
declare function postalcodeError(raw: string, country: string, c: ValidationMessages): string | undefined;
|
|
825
|
+
declare function streetWarning(raw: string, c: ValidationMessages): string | undefined;
|
|
826
|
+
declare const VALIDATED_FIELDS: readonly ["forename", "surname", "email", "phone", "street", "postalcode", "city"];
|
|
827
|
+
/** Validate one named field. Takes the selected COUNTRY plus its calling code
|
|
828
|
+
* (`dial`): two validators depend on the country — the phone via the dial
|
|
829
|
+
* code, the PLZ via its format rule. The dial code is passed in rather than
|
|
830
|
+
* looked up because the ISO→dial mapping lives with the site's country
|
|
831
|
+
* register (its localized "Land" options), not here. */
|
|
832
|
+
declare function fieldError(name: string, value: string, country: string, dial: string, c: ValidationMessages): string | undefined;
|
|
833
|
+
/** Country options in the shape the site's country register provides —
|
|
834
|
+
* `[code, label]` pairs, a featured group plus the full ISO register. Kept
|
|
835
|
+
* structural so the site's own `CountryGroups` type satisfies it. */
|
|
836
|
+
interface CountryGroupsLike {
|
|
837
|
+
featured: ReadonlyArray<readonly [string, string]>;
|
|
838
|
+
all: ReadonlyArray<readonly [string, string]>;
|
|
839
|
+
}
|
|
840
|
+
declare function detectCountry(countries: CountryGroupsLike): string;
|
|
841
|
+
//#endregion
|
|
842
|
+
//#region src/checkout/structure.d.ts
|
|
843
|
+
/**
|
|
844
|
+
* The checkout's STRUCTURAL requirements — what must be on screen, in what
|
|
845
|
+
* order, regardless of who produced the markup.
|
|
846
|
+
*
|
|
847
|
+
* WHY THIS EXISTS. Headless holds the RULES (the § 7 VVG document gate, the
|
|
848
|
+
* stale-price booking guard) but no markup, so a site that renders its own
|
|
849
|
+
* checkout UI can silently drop a legal obligation — the § 312j BGB
|
|
850
|
+
* Button-Lösung wording, the insurance documents listed before the guest's
|
|
851
|
+
* confirmation, the price stated directly above the order button — and every
|
|
852
|
+
* unit test still passes. This module is a contract over the RENDERED result,
|
|
853
|
+
* checked by the platform's publish gate for imported and custom checkouts
|
|
854
|
+
* alike.
|
|
855
|
+
*
|
|
856
|
+
* Two halves:
|
|
857
|
+
* - `CHECKOUT_PARTS`: the `data-part` vocabulary. Imported components carry
|
|
858
|
+
* these attributes; a site's own markup must keep them so the gate (and
|
|
859
|
+
* a site's own CSS) can find the elements.
|
|
860
|
+
* - `CHECKOUT_STRUCTURE` + `assertCheckoutStructure(root)`: the requirements
|
|
861
|
+
* and a pure DOM check the smoke suite runs against the rendered checkout
|
|
862
|
+
* (a browser page or a jsdom document). Every requirement names the legal
|
|
863
|
+
* basis it exists for, so a failure message tells the reader WHY.
|
|
864
|
+
*/
|
|
865
|
+
declare const CHECKOUT_PARTS: {
|
|
866
|
+
/** The one place the guest is told the price they are about to owe. */
|
|
867
|
+
readonly priceTotal: "checkout-price-total";
|
|
868
|
+
/** The § 312j order button. */
|
|
869
|
+
readonly submit: "checkout-submit";
|
|
870
|
+
/** The terms line (AGB + privacy triggers). */
|
|
871
|
+
readonly terms: "checkout-terms";
|
|
872
|
+
/** One link per mandatory insurance document (§ 7 VVG). */
|
|
873
|
+
readonly insuranceDocument: "checkout-insurance-document";
|
|
874
|
+
/** The guest's receipt confirmation for those documents. */
|
|
875
|
+
readonly insuranceConfirm: "checkout-insurance-confirm";
|
|
876
|
+
};
|
|
877
|
+
type CheckoutPart = (typeof CHECKOUT_PARTS)[keyof typeof CHECKOUT_PARTS];
|
|
878
|
+
/** German "kostenpflichtig bestellen"-class labels the courts have accepted
|
|
879
|
+
* for § 312j Abs. 3 BGB. The check is on the RENDERED button text, so a site
|
|
880
|
+
* may re-voice the label — but only within this class. */
|
|
881
|
+
declare const BUTTON_SOLUTION_PATTERN: RegExp;
|
|
882
|
+
declare const CHECKOUT_STRUCTURE: {
|
|
883
|
+
readonly "submit-present": {
|
|
884
|
+
readonly basis: "§ 312j Abs. 3 BGB (Button-Lösung)";
|
|
885
|
+
readonly description: "exactly one order button carries data-part=checkout-submit";
|
|
886
|
+
};
|
|
887
|
+
readonly "submit-wording": {
|
|
888
|
+
readonly basis: "§ 312j Abs. 3 BGB (Button-Lösung)";
|
|
889
|
+
readonly description: "the order button's visible text is a 'zahlungspflichtig buchen'-class label";
|
|
890
|
+
};
|
|
891
|
+
readonly "total-before-submit": {
|
|
892
|
+
readonly basis: "§ 312j Abs. 2 BGB (price stated immediately before the order)";
|
|
893
|
+
readonly description: "a data-part=checkout-price-total precedes the order button in DOM order";
|
|
894
|
+
};
|
|
895
|
+
readonly "terms-before-submit": {
|
|
896
|
+
readonly basis: "§ 312d BGB / Art. 13 DSGVO (terms + privacy notice before contracting)";
|
|
897
|
+
readonly description: "a data-part=checkout-terms precedes the order button";
|
|
898
|
+
};
|
|
899
|
+
readonly "insurance-docs-before-confirm": {
|
|
900
|
+
readonly basis: "§ 7 Abs. 1 VVG, § 4 VVG-InfoV (documents before the declaration)";
|
|
901
|
+
readonly description: "when an insurance confirmation is rendered, at least one insurance document precedes it in DOM order";
|
|
902
|
+
};
|
|
903
|
+
};
|
|
904
|
+
type CheckoutRequirement = keyof typeof CHECKOUT_STRUCTURE;
|
|
905
|
+
interface StructureViolation {
|
|
906
|
+
requirement: CheckoutRequirement;
|
|
907
|
+
basis: string;
|
|
908
|
+
detail: string;
|
|
909
|
+
}
|
|
910
|
+
declare function assertCheckoutStructure(root: ParentNode): StructureViolation[];
|
|
911
|
+
//#endregion
|
|
912
|
+
export { BOOKING_SNAPSHOT_KEY, BUTTON_SOLUTION_PATTERN, BookingConfirmation, BookingContactForm, BookingSnapshot, BookingSnapshotGuests, CHECKOUT_CONTEXT_KEY, CHECKOUT_PARTS, CHECKOUT_RETURN_PATH, CHECKOUT_STRUCTURE, CheckoutContext, type CheckoutFlow, type CheckoutFlowActions, type CheckoutFlowApi, type CheckoutFlowMessages, type CheckoutFlowOptions, type CheckoutFlowState, type CheckoutIssue, type CheckoutPart, type CheckoutPhase, type CountryGroupsLike, type InsuranceDocsState, InsurancePaymentLink, Listener, MULTI_RATE_CODES, NO_INSURANCE_DOCS, PaymentLine, PaymentScheduleView, PolicyBranch, PriceQuote, QuoteAddonService, QuoteCancellationPolicy, QuoteInsurance, QuoteInsuranceOption, QuoteLine, QuoteOccupancy, QuotePolicyRule, QuoteResult, QuoteSelectionResult, QuoteUnavailableCode, QuoteView, QuoteVoucher, SnapshotImage, type Stay, Store, type StructureViolation, Unsubscribe, VALIDATED_FIELDS, type ValidationMessages, WebsiteSdk, WritableStore, adaptGuestQuote, addQuoteService, assertCheckoutStructure, clearQuoteServices, createCheckoutFlow, createStore, describeSdkError, detectCountry, emailError, fetchQuote, fieldError, freeCancellationUntil, fullRefundRule, insuranceDocsPending, insuranceIdToReapply, insuranceProductKey, isRateConfigurationFault, isSubmittableOption, isoDate, logSdkFailure, normalizePhone, occupancyOf, payWithOption, phoneError, policyFreeCancellationUntil, postalcodeError, pricePolicyBranches, removeQuoteService, saveBookingSnapshot, saveCheckoutContext, selectQuoteInsurance, selectQuotePolicy, selectableInsuranceOptions, selectedInsuranceOption, shownInsuranceResources, stabilizeCardOrder, stayKeyOf, stayParams, streetWarning, submitBooking };
|