@v-office/website-headless 0.0.3 → 0.1.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 +3 -4
- package/dist/enhance/index.d.mts +4 -1
- package/dist/enhance/index.mjs +6 -2
- package/dist/index.d.mts +456 -11
- package/dist/index.mjs +687 -14
- package/package.json +3 -4
- package/dist/manifest/index.d.mts +0 -122
- package/dist/manifest/index.mjs +0 -256
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ HERE, so a site's own UI is held to the same contract.
|
|
|
10
10
|
@v-office/sdk-core domain contracts + PMS adapters
|
|
11
11
|
@v-office/website-sdk website facade (static + live namespaces)
|
|
12
12
|
@v-office/website-headless ← THIS: orchestration hooks, validation, legal
|
|
13
|
-
gates,
|
|
13
|
+
gates, enhancement engines
|
|
14
14
|
@v-office/website-components imported UI over these hooks (a site may copy and own it)
|
|
15
15
|
userland pages, brand components, tokens, copy
|
|
16
16
|
```
|
|
@@ -20,7 +20,6 @@ userland pages, brand components, tokens, copy
|
|
|
20
20
|
| Import | Contents |
|
|
21
21
|
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
22
22
|
| `@v-office/website-headless` | Framework-free runtime: `createCheckoutFlow` — the whole checkout machine (stay → quote → selections → § 7 gate → book → pay) as a store with `getState` / `subscribe` / actions / `update` / `start` / `dispose`; `createStore`, the primitive every flow here builds on; `fetchQuote` + the quote view model, `book()`/`pay()` runners, the payment-return snapshot, validation rules, `CHECKOUT_PARTS` + `assertCheckoutStructure`, `logSdkFailure` |
|
|
23
|
-
| `…/manifest` | `siteManifest()` Astro integration — writes `dist/site-manifest.json` with installed package versions and per-surface states derived from the site's imports (`imported` = imports the component from website-components, `custom` = imports the surface's hook without it; see `manifest/surfaces.ts`). Three signals cross-checked per build. Node-only; import it in `astro.config` and nowhere else |
|
|
24
23
|
| `…/enhance` | Progressive enhancement for server-rendered markup: card image strips, history-backed content overlays, the rental gallery's layered viewer. They bind site markup and own none of it |
|
|
25
24
|
|
|
26
25
|
The flows are stores: a UI calls `getState()`, `subscribe(listener)` and the
|
|
@@ -66,13 +65,13 @@ parameters — the rules live here, the words live in the site.
|
|
|
66
65
|
|
|
67
66
|
```bash
|
|
68
67
|
pnpm install
|
|
69
|
-
pnpm build # tsdown → dist/ (index,
|
|
68
|
+
pnpm build # tsdown → dist/ (index, enhance; .mjs + .d.mts)
|
|
70
69
|
pnpm check # tsc · oxlint · oxfmt --check
|
|
71
70
|
pnpm test # vitest, jsdom environment
|
|
72
71
|
```
|
|
73
72
|
|
|
74
73
|
The tests cover the pure rules (validation, insurance gate, stay helpers,
|
|
75
|
-
quote adapter
|
|
74
|
+
quote adapter), the checkout flow driven as a store
|
|
76
75
|
against a fake SDK, and the structural gate against rendered markup. Nothing
|
|
77
76
|
books or pays for real. vitest is used because the structural gate needs a
|
|
78
77
|
DOM (jsdom); the SDKs use `node --test` because they do not.
|
package/dist/enhance/index.d.mts
CHANGED
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
* what the iOS swipe momentum/snap relies on; transform-based
|
|
16
16
|
* shifting can't reproduce it.
|
|
17
17
|
*
|
|
18
|
-
* Dots are optional and only updated when present.
|
|
18
|
+
* Dots are optional and only updated when present. Their accessible label
|
|
19
|
+
* follows `data-label-template` on the dots list (`{count}` images,
|
|
20
|
+
* `{current}` shown) — the component renders the template from its copy, so
|
|
21
|
+
* the enhancer carries no language of its own.
|
|
19
22
|
*/
|
|
20
23
|
declare function initImageCarousels(selector?: string): void;
|
|
21
24
|
declare function initRentalCarousel(viewport: HTMLElement): void;
|
package/dist/enhance/index.mjs
CHANGED
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
* what the iOS swipe momentum/snap relies on; transform-based
|
|
16
16
|
* shifting can't reproduce it.
|
|
17
17
|
*
|
|
18
|
-
* Dots are optional and only updated when present.
|
|
18
|
+
* Dots are optional and only updated when present. Their accessible label
|
|
19
|
+
* follows `data-label-template` on the dots list (`{count}` images,
|
|
20
|
+
* `{current}` shown) — the component renders the template from its copy, so
|
|
21
|
+
* the enhancer carries no language of its own.
|
|
19
22
|
*/
|
|
20
23
|
function initImageCarousels(selector = ".card-images") {
|
|
21
24
|
document.querySelectorAll(selector).forEach((el) => {
|
|
@@ -49,7 +52,8 @@ function initImageCarousels(selector = ".card-images") {
|
|
|
49
52
|
d.removeAttribute("data-active");
|
|
50
53
|
}
|
|
51
54
|
});
|
|
52
|
-
|
|
55
|
+
const template = dotsList.dataset.labelTemplate;
|
|
56
|
+
if (template) dotsList.setAttribute("aria-label", template.replace(/\{current\}/g, String(idx + 1)).replace(/\{count\}/g, String(total)));
|
|
53
57
|
};
|
|
54
58
|
const goTo = (idx) => {
|
|
55
59
|
const clamped = Math.max(0, Math.min(total - 1, idx));
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BookingPaymentOption, BookingPaymentScheduleKind, GuestQuote, GuestQuoteResult, SubmittableBookingPaymentOption, createWebsiteSDK } from "@v-office/website-sdk";
|
|
1
|
+
import { BookingPaymentOption, BookingPaymentScheduleKind, GuestQuote, GuestQuoteResult, Locale, SearchSearchSort, SubmittableBookingPaymentOption, WebsiteSDKOptions, createWebsiteSDK } from "@v-office/website-sdk";
|
|
2
2
|
//#region src/store/index.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* The smallest external store that every UI framework can subscribe to:
|
|
@@ -22,6 +22,50 @@ declare function createStore<S>(initial: S): WritableStore<S>;
|
|
|
22
22
|
//#endregion
|
|
23
23
|
//#region src/sdk.d.ts
|
|
24
24
|
type WebsiteSdk = ReturnType<typeof createWebsiteSDK>;
|
|
25
|
+
/** Deployment stage: selects the public endpoint set below. */
|
|
26
|
+
declare const STAGES: readonly ["development", "production"];
|
|
27
|
+
type Stage = (typeof STAGES)[number];
|
|
28
|
+
/** Public endpoints of the v10 backend per stage. */
|
|
29
|
+
declare const VOFFICE_ENDPOINTS: Record<Stage, {
|
|
30
|
+
apiEndpoint: string;
|
|
31
|
+
imageBaseUrl: string;
|
|
32
|
+
searchEndpoint: string;
|
|
33
|
+
}>;
|
|
34
|
+
/** Public endpoints of the v9 backend per stage; API URLs derive from `apiBaseUrl`. */
|
|
35
|
+
declare const HUB_ENDPOINTS: Record<Stage, {
|
|
36
|
+
apiBaseUrl: string;
|
|
37
|
+
imageProxyBaseUrl: string;
|
|
38
|
+
}>;
|
|
39
|
+
/** Everything the SDK needs, as plain public values — a static site has no
|
|
40
|
+
* secret to protect. Serializable on purpose: an Astro island receives it as a
|
|
41
|
+
* prop, so the site reads its env once (in .astro / a loader) and never inside
|
|
42
|
+
* a component. */
|
|
43
|
+
type SdkConfig = {
|
|
44
|
+
backend: "v9";
|
|
45
|
+
imageProxyBaseUrl: string;
|
|
46
|
+
v0ApiBaseUrl: string;
|
|
47
|
+
v1ApiBaseUrl: string;
|
|
48
|
+
graphqlUrl: string;
|
|
49
|
+
apiKey: string;
|
|
50
|
+
} | {
|
|
51
|
+
backend: "v10";
|
|
52
|
+
imageBaseUrl: string;
|
|
53
|
+
apiEndpoint: string;
|
|
54
|
+
searchEndpoint: string;
|
|
55
|
+
accessToken: string;
|
|
56
|
+
};
|
|
57
|
+
/** The three values a site actually decides — backend, stage, its token — as
|
|
58
|
+
* a full `SdkConfig`, with the endpoints the platform owns. */
|
|
59
|
+
declare function sdkConfigFor({ backend, stage, token }: {
|
|
60
|
+
backend: SdkConfig["backend"];
|
|
61
|
+
stage: Stage;
|
|
62
|
+
token: string;
|
|
63
|
+
}): SdkConfig;
|
|
64
|
+
/** Build an SDK instance from a config (+ the SDK's options: translation
|
|
65
|
+
* overrides, custom attributes, highlight prioritization). The caller owns the
|
|
66
|
+
* lifecycle and calls `await sdk.dispose()` when done. The API is rate-limited
|
|
67
|
+
* (1/s), so a browser surface keeps ONE instance for its lifetime. */
|
|
68
|
+
declare function createSdk(config: SdkConfig, options?: WebsiteSDKOptions): WebsiteSdk;
|
|
25
69
|
/**
|
|
26
70
|
* Render `error` and its whole cause chain as indented JSON.
|
|
27
71
|
*
|
|
@@ -288,7 +332,7 @@ declare function adaptGuestQuote(quote: GuestQuote): QuoteView;
|
|
|
288
332
|
declare function fetchQuote(rentalId: string, period: {
|
|
289
333
|
start: string;
|
|
290
334
|
end: string;
|
|
291
|
-
}, occupancy: QuoteOccupancy, sdk: WebsiteSdk, voucher?: string): Promise<QuoteResult>;
|
|
335
|
+
}, occupancy: QuoteOccupancy, sdk: WebsiteSdk, locale: Locale, voucher?: string): Promise<QuoteResult>;
|
|
292
336
|
type QuoteSelectionResult = {
|
|
293
337
|
ok: true;
|
|
294
338
|
quote: GuestQuote;
|
|
@@ -459,7 +503,7 @@ interface BookingConfirmation {
|
|
|
459
503
|
* real. Throws on transport/backend errors (the caller renders an honest
|
|
460
504
|
* retry message; nothing was necessarily booked when it throws).
|
|
461
505
|
*/
|
|
462
|
-
declare function submitBooking(quote: GuestQuote, contact: BookingContactForm, sdk: WebsiteSdk): Promise<BookingConfirmation>;
|
|
506
|
+
declare function submitBooking(quote: GuestQuote, contact: BookingContactForm, sdk: WebsiteSdk, returnPath?: string): Promise<BookingConfirmation>;
|
|
463
507
|
/** True for the option kinds `submitPaymentOption` can perform in-browser
|
|
464
508
|
* (redirect / form_post / stripe_checkout). Bank transfer is display-only. */
|
|
465
509
|
declare function isSubmittableOption(option: BookingPaymentOption): option is SubmittableBookingPaymentOption;
|
|
@@ -728,6 +772,13 @@ interface CheckoutFlowOptions {
|
|
|
728
772
|
code: QuoteUnavailableCode;
|
|
729
773
|
rentalId: string;
|
|
730
774
|
}) => void) | undefined;
|
|
775
|
+
/** The SDK locale: the PMS answers in that language (quote lines, reasons,
|
|
776
|
+
* policy names). */
|
|
777
|
+
locale: Locale;
|
|
778
|
+
/** Where the payment provider sends the guest back to (relative to the
|
|
779
|
+
* site's origin). Default: the platform's `/buchen/bestaetigung`; a site
|
|
780
|
+
* with its own return route passes it here. */
|
|
781
|
+
returnPath?: string | undefined;
|
|
731
782
|
}
|
|
732
783
|
interface CheckoutIssue {
|
|
733
784
|
/** The stayKey the issue belongs to — a different stay re-arms the loop. */
|
|
@@ -818,18 +869,19 @@ interface ValidationMessages {
|
|
|
818
869
|
postalcodeInvalidCH: string;
|
|
819
870
|
streetHouseNumberWarning: string;
|
|
820
871
|
}
|
|
821
|
-
declare function normalizePhone(raw: string, dial
|
|
822
|
-
declare function phoneError(raw: string, dial: string, c: ValidationMessages): string | undefined;
|
|
872
|
+
declare function normalizePhone(raw: string, dial: string | undefined): string;
|
|
873
|
+
declare function phoneError(raw: string, dial: string | undefined, c: ValidationMessages): string | undefined;
|
|
823
874
|
declare function emailError(raw: string, c: ValidationMessages): string | undefined;
|
|
824
875
|
declare function postalcodeError(raw: string, country: string, c: ValidationMessages): string | undefined;
|
|
825
876
|
declare function streetWarning(raw: string, c: ValidationMessages): string | undefined;
|
|
826
877
|
declare const VALIDATED_FIELDS: readonly ["forename", "surname", "email", "phone", "street", "postalcode", "city"];
|
|
827
878
|
/** Validate one named field. Takes the selected COUNTRY plus its calling code
|
|
828
|
-
* (`dial
|
|
829
|
-
*
|
|
830
|
-
* looked up because the ISO→dial
|
|
831
|
-
* register (its localized "Land"
|
|
832
|
-
|
|
879
|
+
* (`dial`, undefined when the register has none for it): two validators depend
|
|
880
|
+
* on the country — the phone via the dial code, the PLZ via its format rule.
|
|
881
|
+
* The dial code is passed in rather than looked up because the ISO→dial
|
|
882
|
+
* mapping lives with the site's country register (its localized "Land"
|
|
883
|
+
* options), not here. */
|
|
884
|
+
declare function fieldError(name: string, value: string, country: string, dial: string | undefined, c: ValidationMessages): string | undefined;
|
|
833
885
|
/** Country options in the shape the site's country register provides —
|
|
834
886
|
* `[code, label]` pairs, a featured group plus the full ISO register. Kept
|
|
835
887
|
* structural so the site's own `CountryGroups` type satisfies it. */
|
|
@@ -909,4 +961,397 @@ interface StructureViolation {
|
|
|
909
961
|
}
|
|
910
962
|
declare function assertCheckoutStructure(root: ParentNode): StructureViolation[];
|
|
911
963
|
//#endregion
|
|
912
|
-
|
|
964
|
+
//#region src/dates.d.ts
|
|
965
|
+
declare function parseParam(value: string | null | undefined): Date | undefined;
|
|
966
|
+
declare function parseGermanParam(value: string | null | undefined): Date | undefined;
|
|
967
|
+
/** A flexible-length stay ("3–5 Nächte") as the URL carries it. Off until the
|
|
968
|
+
* search UI offers it; the keys are reserved so a site can enable it. */
|
|
969
|
+
declare const NIGHT_RANGE_ENABLED = false;
|
|
970
|
+
interface NightRange {
|
|
971
|
+
min: number;
|
|
972
|
+
max: number;
|
|
973
|
+
}
|
|
974
|
+
declare const NIGHT_RANGE_KEYS: readonly ["minNights", "maxNights", "nights_min", "nights_max"];
|
|
975
|
+
declare function parseNightRange(params: URLSearchParams): NightRange | null;
|
|
976
|
+
//#endregion
|
|
977
|
+
//#region src/format.d.ts
|
|
978
|
+
/**
|
|
979
|
+
* Display tidy for an SDK pre-formatted German-locale price ("1.234,00 €").
|
|
980
|
+
* Nearly every stay is whole euros, so the trailing ",00" is noise — dropped only
|
|
981
|
+
* when it is the zero-cents group at the end, so real cents ("1.234,50 €") stay.
|
|
982
|
+
* A real value formatted for display, nothing fabricated; other formats pass.
|
|
983
|
+
*/
|
|
984
|
+
declare function tidyPrice(formatted: string): string;
|
|
985
|
+
//#endregion
|
|
986
|
+
//#region src/availability.d.ts
|
|
987
|
+
/**
|
|
988
|
+
* The per-night price a calendar CELL shows, rounded to whole euros:
|
|
989
|
+
* "116,43 €" -> "116 €", "116,50 €" -> "117 €".
|
|
990
|
+
*
|
|
991
|
+
* A month grid is thirty-odd numbers seen at a glance and acted on by NONE of
|
|
992
|
+
* them — the guest picks a range, and the quote under the calendar then states
|
|
993
|
+
* the stay to the last cent. Two decimals in every cell buy precision nobody
|
|
994
|
+
* reads, at the one size where the digits are already tight.
|
|
995
|
+
*
|
|
996
|
+
* ROUNDS TO NEAREST, AND THAT IS THE POINT OF THE FUNCTION. Stripping the cents
|
|
997
|
+
* with a regex is one line and understates every priced night by up to 99
|
|
998
|
+
* cents, always in the direction that flatters the price. That is the shape of
|
|
999
|
+
* claim a host has to stand behind, so the error goes both ways here and is
|
|
1000
|
+
* capped at 50 cents on a figure that is explicitly indicative.
|
|
1001
|
+
*
|
|
1002
|
+
* Parses the German format rather than doing string surgery on it: "." groups
|
|
1003
|
+
* thousands, "," is the decimal. Anything that does not match that shape —
|
|
1004
|
+
* another locale, no decimal group, a price the SDK formatted differently — is
|
|
1005
|
+
* returned UNCHANGED. A display tidy must never turn a price it did not
|
|
1006
|
+
* understand into a wrong one.
|
|
1007
|
+
*
|
|
1008
|
+
* This runs where `priceByDay` is BUILT rather than where a cell renders it, so
|
|
1009
|
+
* every calendar fed from this map agrees: the widget's picker and the
|
|
1010
|
+
* checkout's copy, which reads the same shape.
|
|
1011
|
+
*/
|
|
1012
|
+
declare function nightlyPrice(formatted: string, locale?: Locale): string;
|
|
1013
|
+
type VofficeSdk$1 = WebsiteSdk;
|
|
1014
|
+
interface InitialAvailability {
|
|
1015
|
+
/** ISO dates (`YYYY-MM-DD`) the SDK flags as a valid check-in/arrival day. */
|
|
1016
|
+
canStart: Set<string>;
|
|
1017
|
+
/** ISO dates the rental is genuinely unbookable on (booked / closed / past the
|
|
1018
|
+
* bookable horizon — the SDK's "hard" statuses). The picker STRIKES these
|
|
1019
|
+
* through. A disabled day NOT in this set is only blocked by a rule for the
|
|
1020
|
+
* current phase (e.g. check-in-not-allowed) and is merely dimmed — it could
|
|
1021
|
+
* work in another context. */
|
|
1022
|
+
hardBlocked: Set<string>;
|
|
1023
|
+
/** Per-night price by ISO date — the SDK's localized string, display-tidied
|
|
1024
|
+
* to drop the noise ",00" (e.g. "120 €"). Only days the SDK prices. */
|
|
1025
|
+
priceByDay: Map<string, string>;
|
|
1026
|
+
/** Localized reason a day is blocked, by ISO date (the SDK's own `tooltip`,
|
|
1027
|
+
* e.g. "An diesem Tag ist keine Anreise möglich."). */
|
|
1028
|
+
tooltipByDay: Map<string, string>;
|
|
1029
|
+
/** Occupancy caps the SDK states for this rental (null when omitted). The
|
|
1030
|
+
* picker caps the guest stepper at these so a quote can't fail on too many
|
|
1031
|
+
* guests; the below-minimum case is left to the quote (the SDK is the
|
|
1032
|
+
* arbiter of a bookable party), so `minPersons` isn't surfaced here. */
|
|
1033
|
+
maxPersons: number | null;
|
|
1034
|
+
maxAdults: number | null;
|
|
1035
|
+
}
|
|
1036
|
+
interface EndAvailability {
|
|
1037
|
+
/** ISO dates the SDK flags as a valid check-out/departure for the chosen
|
|
1038
|
+
* start (min-stay + checkout rules already applied by the SDK). */
|
|
1039
|
+
canEnd: Set<string>;
|
|
1040
|
+
/** ISO dates genuinely unbookable (booked / the range would cross an
|
|
1041
|
+
* unavailable night that is itself unavailable). STRUCK through. A disabled
|
|
1042
|
+
* day not here is only rule-blocked for this start (min-stay, check-out-not-
|
|
1043
|
+
* allowed, same-day, would-be a new start) and is merely dimmed. */
|
|
1044
|
+
hardBlocked: Set<string>;
|
|
1045
|
+
/** Localized reason a departure is blocked, by ISO date (min-stay-not-met,
|
|
1046
|
+
* check-out-not-allowed, range-crosses-booked, …) — the SDK's own `tooltip`. */
|
|
1047
|
+
tooltipByDay: Map<string, string>;
|
|
1048
|
+
}
|
|
1049
|
+
/** Possible check-in days + occupancy caps for `rentalId` across the window.
|
|
1050
|
+
* The caller's SDK instance is reused so its 30s per-rental cache spans the
|
|
1051
|
+
* picker's lifetime. */
|
|
1052
|
+
declare function fetchInitialAvailability(sdk: VofficeSdk$1, rentalId: string, calculateFromDate: string, calculateThroughDate: string, locale: Locale): Promise<InitialAvailability>;
|
|
1053
|
+
/** Possible check-out days for a chosen `selectedStartDate`. */
|
|
1054
|
+
declare function fetchEndAvailability(sdk: VofficeSdk$1, rentalId: string, calculateFromDate: string, calculateThroughDate: string, selectedStartDate: string, locale: Locale): Promise<EndAvailability>;
|
|
1055
|
+
//#endregion
|
|
1056
|
+
//#region src/search.d.ts
|
|
1057
|
+
type VofficeSdk = WebsiteSdk;
|
|
1058
|
+
interface SearchImage {
|
|
1059
|
+
src: string;
|
|
1060
|
+
srcset?: string | undefined;
|
|
1061
|
+
alt: string;
|
|
1062
|
+
}
|
|
1063
|
+
/** One window the SDK suggests for a rental it could not match exactly — a
|
|
1064
|
+
* concrete, priced, bookable stay.
|
|
1065
|
+
*
|
|
1066
|
+
* EVERY FIELD IS READ OFF THE PERIOD THE SDK SENDS, never off the rental. A
|
|
1067
|
+
* promotion belongs to a (rental x window) pair — the same rental prices
|
|
1068
|
+
* undiscounted in another window — so the price and the discount are read from
|
|
1069
|
+
* the same period together, the way `mapAlternativeEntry` already resolved them
|
|
1070
|
+
* for the single window it used to render. A window whose dates the feed omits
|
|
1071
|
+
* has no label and never reaches this list; one the feed does not price shows
|
|
1072
|
+
* no price rather than a placeholder. */
|
|
1073
|
+
interface AlternativePeriod {
|
|
1074
|
+
/** „31. Aug - 4. Sep", composed from this window's own start and end. */
|
|
1075
|
+
label: string;
|
|
1076
|
+
/** The detail-page link carrying THIS window's dates, so the rental opens on
|
|
1077
|
+
* the stay the row names rather than on the user's (unmatched) query. */
|
|
1078
|
+
href: string;
|
|
1079
|
+
/** This window's `formattedTotal`, verbatim. null when the feed omits it. */
|
|
1080
|
+
priceLabel: string | null;
|
|
1081
|
+
/** This window's length in nights. null when the feed omits a date. */
|
|
1082
|
+
nights: number | null;
|
|
1083
|
+
/** True when THIS window is discounted — the presence of the SDK's `discount`
|
|
1084
|
+
* block on this period, nothing inferred. Carried so the three discount facts
|
|
1085
|
+
* arrive together from `mapDiscount`, exactly as they do on the card. */
|
|
1086
|
+
isOffer: boolean;
|
|
1087
|
+
/** This window's pre-discount total, for the struck-through "was" price. */
|
|
1088
|
+
offerOriginalPriceLabel: string | null;
|
|
1089
|
+
/** This window's promotions, the PMS's own labels, verbatim. `amount` is
|
|
1090
|
+
* LINE-level and must not be printed beside a total — see the note on
|
|
1091
|
+
* `SearchResultItem.offers`. */
|
|
1092
|
+
offers: {
|
|
1093
|
+
label: string;
|
|
1094
|
+
amount: string | null;
|
|
1095
|
+
}[];
|
|
1096
|
+
}
|
|
1097
|
+
interface SearchResultItem {
|
|
1098
|
+
/** PMS id — shared by the result card and its map marker. */
|
|
1099
|
+
id: string;
|
|
1100
|
+
/** `nameOrLabel`, verbatim. */
|
|
1101
|
+
name: string;
|
|
1102
|
+
/** `address.city`, verbatim (empty when the feed omits it). */
|
|
1103
|
+
location: string;
|
|
1104
|
+
/** Internal detail link — `rentalsPath` (see `runLiveSearch`) + the slug
|
|
1105
|
+
* derived from the name (URL generation), with the user's booking context
|
|
1106
|
+
* (dates + party size) appended as a query so the search selection carries
|
|
1107
|
+
* into the rental page. */
|
|
1108
|
+
href: string;
|
|
1109
|
+
images: SearchImage[];
|
|
1110
|
+
/** Parsed out of the SDK highlights; 0 when the highlight is absent. */
|
|
1111
|
+
guests: number;
|
|
1112
|
+
bedrooms: number;
|
|
1113
|
+
area: number;
|
|
1114
|
+
/** Plain-text amenity highlights (the SDK has no icon concept), CARRIED WHOLE.
|
|
1115
|
+
*
|
|
1116
|
+
* This used to be `highlights.slice(3, 6)` — the SDK opens `highlights` with
|
|
1117
|
+
* the spec stats the line above already prints, and three was a guess at how
|
|
1118
|
+
* many. It is not a constant: how many spec lines a feed states depends on
|
|
1119
|
+
* which of them the PMS has filled and on `rentalHighlightPrioritization`, so
|
|
1120
|
+
* a feed stating four leaked „55 m²" into the pill row, while the same slice
|
|
1121
|
+
* ALSO discarded good candidates below position 6.
|
|
1122
|
+
*
|
|
1123
|
+
* The CARD decides now: `rental-detail-kit`'s `cardHighlights` drops the spec
|
|
1124
|
+
* block by SHAPE, then `pillRow` merges and caps. A consumer that renders this
|
|
1125
|
+
* array raw must cap it itself — it is no longer bounded here. */
|
|
1126
|
+
pills: string[];
|
|
1127
|
+
/** `formattedTotal`, verbatim (null when the feed omits a price). */
|
|
1128
|
+
priceLabel: string | null;
|
|
1129
|
+
/** Label for an SDK-provided alternative period, null for exact matches.
|
|
1130
|
+
* The FIRST of `alternativePeriods` — the SDK returns them best-first — kept
|
|
1131
|
+
* as its own field because the card's date pill names one window even where
|
|
1132
|
+
* the chooser lists several. */
|
|
1133
|
+
alternativePeriodLabel: string | null;
|
|
1134
|
+
/** EVERY window the SDK suggests for this card, best-first — not just the one
|
|
1135
|
+
* `alternativePeriodLabel` names. Empty on exact-match cards.
|
|
1136
|
+
*
|
|
1137
|
+
* THE FEED STATES MORE THAN ONE AND THIS PROJECT RENDERED ONE. Measured
|
|
1138
|
+
* against v10 production on 2026-08-27, query
|
|
1139
|
+
* `start=2026-08-31&end=2026-09-02&adults=2`: 19 alternative entries, and 14
|
|
1140
|
+
* of them carried TWO priced windows. `mapAlternativeEntry` read
|
|
1141
|
+
* `alternativePeriods[0]` and dropped the rest, so on three cards in four a
|
|
1142
|
+
* real, priced, bookable window existed that the guest was never shown. The
|
|
1143
|
+
* same query's flexible sibling (`month=10-2026&nights=7`) returned exactly
|
|
1144
|
+
* one window on all 20 entries — so the count is a property of the QUERY, not
|
|
1145
|
+
* a constant, and every surface reading this must handle both. */
|
|
1146
|
+
alternativePeriods: AlternativePeriod[];
|
|
1147
|
+
/** True only when the SDK flags this item as a non-exact date match. */
|
|
1148
|
+
isAlternativePeriod: boolean;
|
|
1149
|
+
/** Stay length for THIS card's own window, in nights. Set only on
|
|
1150
|
+
* alternative-period cards (whose date pill describes the SDK's suggested
|
|
1151
|
+
* window, not the user's search) so the footer's "N Nächte" matches the dates
|
|
1152
|
+
* shown. null on exact-match cards — they fall back to the page-level
|
|
1153
|
+
* `nights` (their window IS the user's search). */
|
|
1154
|
+
nights: number | null;
|
|
1155
|
+
/** True when THIS card's window is discounted ("Angebot" badge) — the mere
|
|
1156
|
+
* PRESENCE of the SDK's `discount` block on the priced result, nothing
|
|
1157
|
+
* inferred. A promotion belongs to a (rental × window) PAIR, not to a rental:
|
|
1158
|
+
* the same rental prices undiscounted in another window, so this travels with
|
|
1159
|
+
* the price, not with the identity. Never derived from a lead-time threshold —
|
|
1160
|
+
* that number is the customer's PMS setting and moves. */
|
|
1161
|
+
isOffer: boolean;
|
|
1162
|
+
/** `discount.formattedOriginalTotal`, verbatim — the pre-discount total, for
|
|
1163
|
+
* the struck-through "was" price. null when the feed states a discount but no
|
|
1164
|
+
* original total: presence of a discount and the ability to price it are
|
|
1165
|
+
* different facts, so the "was" price gates on its own field. */
|
|
1166
|
+
offerOriginalPriceLabel: string | null;
|
|
1167
|
+
/** `discount.applied`, verbatim — the PMS's own name for each promotion and
|
|
1168
|
+
* its amount. CARRIED, NEVER AUTHORED: the label names the offer ("Last-Minute
|
|
1169
|
+
* Angebot" is what this feed states today); writing our own over it would be
|
|
1170
|
+
* fabricated data. Empty when the feed states a discount it doesn't itemise —
|
|
1171
|
+
* `isOffer` can be true with nothing here, so the label row gates on length.
|
|
1172
|
+
*
|
|
1173
|
+
* ⚠ `amount` IS LINE-LEVEL, AND MUST NOT BE SHOWN BESIDE A TOTAL. It is the
|
|
1174
|
+
* modifier on the rent line, while `offerOriginalPriceLabel` and `priceLabel`
|
|
1175
|
+
* are totals — so wherever the PMS puts taxes or fees outside the discounted
|
|
1176
|
+
* line, the three do not reconcile. Measured on a live v10 quote of the same
|
|
1177
|
+
* stay the search priced: Inklusivpreis 484,35 € − 72,87 € = 411,48 €, plus
|
|
1178
|
+
* Steuern & Abgaben 10,77 €, total 422,25 €, against a search
|
|
1179
|
+
* `formattedOriginalTotal` of 497,12 €. Both endpoints agree on every figure;
|
|
1180
|
+
* the real total saving is 74,87 € because the tax shrank too, while `amount`
|
|
1181
|
+
* states the rent's 72,87 €. `search-results-kit` renders the LABEL alone for
|
|
1182
|
+
* this reason — it did render the amount, in 1.10.0, and that was the bug.
|
|
1183
|
+
* The field STAYS because it is correct DATA: a surface that shows the rent
|
|
1184
|
+
* line itself may pair it with that line. Do not "fix" it by computing
|
|
1185
|
+
* `original − total` — that number is nowhere in the feed. */
|
|
1186
|
+
offers: {
|
|
1187
|
+
label: string;
|
|
1188
|
+
amount: string | null;
|
|
1189
|
+
}[];
|
|
1190
|
+
/** True when the rental is newly listed ("Neu" badge). The live search summary
|
|
1191
|
+
* feed carries no listing-age signal — the SDK's search result
|
|
1192
|
+
* (`toSearchOutputItem`) exposes only id/name/timeZone/address/images/
|
|
1193
|
+
* highlights/formattedTotal, with no created/published/firstListed field — so
|
|
1194
|
+
* this is always false today and the badge self-hides; wire it to a real SDK
|
|
1195
|
+
* source (e.g. a per-rental created/published date) when search exposes one.
|
|
1196
|
+
* Never fabricate it — straight-through rule. */
|
|
1197
|
+
isNew: boolean;
|
|
1198
|
+
/** `property.id`, verbatim (null when the rental belongs to no property). The
|
|
1199
|
+
* v-office "Property" is the Anlage/complex a rental sits in; the home page's
|
|
1200
|
+
* Anlage cards link to `/suchen?property=<id>`, and the shared search store
|
|
1201
|
+
* scopes its snapshot to this id client-side (the live search has no property
|
|
1202
|
+
* filter key — verified: `property=` lands in `unusedFilterKeys`). Same
|
|
1203
|
+
* scoping model as favorites mode. */
|
|
1204
|
+
propertyId: string | null;
|
|
1205
|
+
/** `location`, verbatim (null when the feed omits coordinates). */
|
|
1206
|
+
coords: {
|
|
1207
|
+
lat: number;
|
|
1208
|
+
lng: number;
|
|
1209
|
+
} | null;
|
|
1210
|
+
}
|
|
1211
|
+
interface SearchPayload {
|
|
1212
|
+
items: SearchResultItem[];
|
|
1213
|
+
/** Stay length from the user's start/end query, or null when undated. */
|
|
1214
|
+
nights: number | null;
|
|
1215
|
+
/** Count of SDK-provided alternative-period results in this page. */
|
|
1216
|
+
alternativePeriodCount: number;
|
|
1217
|
+
/** `pageInfo.nextCursor`, verbatim — opaque, passed straight back to fetch
|
|
1218
|
+
* the next page. null when the feed omits it (last page / no more). */
|
|
1219
|
+
cursor: string | null;
|
|
1220
|
+
/** `pageInfo.hasNextPage`, verbatim. */
|
|
1221
|
+
hasNextPage: boolean;
|
|
1222
|
+
}
|
|
1223
|
+
interface LiveSearchOptions {
|
|
1224
|
+
/** Path of the site's rental detail pages, prefixed to every result `href`
|
|
1225
|
+
* before the slug (e.g. `/de/unterkuenfte`). A trailing slash is tolerated.
|
|
1226
|
+
* Default: `DEFAULT_RENTALS_PATH`. */
|
|
1227
|
+
rentalsPath?: string | undefined;
|
|
1228
|
+
}
|
|
1229
|
+
/** The rental detail pages' path when the caller names none — the site
|
|
1230
|
+
* starter's German route. A site whose pages live elsewhere (a locale prefix,
|
|
1231
|
+
* another segment) passes its own `rentalsPath` to `runLiveSearch`. */
|
|
1232
|
+
declare const DEFAULT_RENTALS_PATH = "/unterkuenfte";
|
|
1233
|
+
/**
|
|
1234
|
+
* Run a live search for the given URL query string (e.g.
|
|
1235
|
+
* `window.location.search`). Builds the SDK query from the picker params,
|
|
1236
|
+
* calls the rate-limited live API once, and maps the items. Pass an SDK
|
|
1237
|
+
* instance to reuse its internal cache across an eager pagination walk;
|
|
1238
|
+
* otherwise this function owns a short-lived instance for a single call.
|
|
1239
|
+
*
|
|
1240
|
+
* Pass `cursor` (the previous page's `pageInfo.nextCursor`, treated as
|
|
1241
|
+
* opaque) to fetch the next page. The returned `cursor`/`hasNextPage` are
|
|
1242
|
+
* the SDK's `pageInfo` straight through, so the caller can keep paging.
|
|
1243
|
+
*
|
|
1244
|
+
* `options.rentalsPath` is the path the site serves its rental detail pages
|
|
1245
|
+
* under — the part of every result `href` before the slug. Routing is the
|
|
1246
|
+
* site's decision (a locale prefix like `/de/unterkuenfte`, another segment),
|
|
1247
|
+
* so it arrives as an option the way `SearchBar.action` and `Quote.checkoutPath`
|
|
1248
|
+
* do; `locale` never implies it. Defaults to `DEFAULT_RENTALS_PATH`.
|
|
1249
|
+
*/
|
|
1250
|
+
declare function runLiveSearch(sdk: VofficeSdk, searchString: string, locale: Locale, cursor?: string | null, options?: LiveSearchOptions): Promise<SearchPayload>;
|
|
1251
|
+
//#endregion
|
|
1252
|
+
//#region src/sort.d.ts
|
|
1253
|
+
/** URL param. A client-side scope like `property`/`region`, NOT an SDK query
|
|
1254
|
+
* key — it is dropped from the SDK query string in search.ts's DROP_KEYS and
|
|
1255
|
+
* travels as the separate `sort` input instead. */
|
|
1256
|
+
declare const SORT_PARAM = "sort";
|
|
1257
|
+
type SortValue = "empfohlen" | "preis-auf" | "preis-ab";
|
|
1258
|
+
/** The backend's own order. Emits no `sort` at all, so it is exactly the
|
|
1259
|
+
* ordering the page has always shown. Typed as the literal (not `SortValue`)
|
|
1260
|
+
* so `value === DEFAULT_SORT` narrows the rest away for `SDK_SORT`. */
|
|
1261
|
+
declare const DEFAULT_SORT: "empfohlen";
|
|
1262
|
+
interface SortOption {
|
|
1263
|
+
value: SortValue;
|
|
1264
|
+
label: string;
|
|
1265
|
+
/** Needs a period + occupancy, or the SDK rejects it before transport. */
|
|
1266
|
+
pricedOnly?: boolean;
|
|
1267
|
+
}
|
|
1268
|
+
declare const SORT_OPTIONS: readonly SortOption[];
|
|
1269
|
+
/** The sort in the URL, or the default when absent/unknown. */
|
|
1270
|
+
declare function activeSort(search: string): SortValue;
|
|
1271
|
+
/**
|
|
1272
|
+
* Can this query be priced? Mirrors the SDK's own precondition: a fixed period
|
|
1273
|
+
* (`start`+`end`) or a flexible window (`dates`/`month`/`weekend`), AND
|
|
1274
|
+
* occupancy. Without both, the backend returns no totals and the SDK rejects
|
|
1275
|
+
* price ordering before it reaches the wire.
|
|
1276
|
+
*
|
|
1277
|
+
* Period keys count only with a value: the search's `buildQuery` drops empty
|
|
1278
|
+
* params (`?start=&end=`), so a key that is merely present would pass this
|
|
1279
|
+
* gate and still reach the SDK undated. `weekend` is the exception — it is a
|
|
1280
|
+
* presence-only flag on both sides.
|
|
1281
|
+
*/
|
|
1282
|
+
declare function isPricedQuery(search: string): boolean;
|
|
1283
|
+
/** Is this option selectable for the current query? */
|
|
1284
|
+
declare function isSortAvailable(option: SortOption, search: string): boolean;
|
|
1285
|
+
/**
|
|
1286
|
+
* The `sort` to hand the SDK for this URL, or `undefined` for the backend's
|
|
1287
|
+
* own order. Falls back to `undefined` (rather than throwing) when a price
|
|
1288
|
+
* sort survives in the URL after the dates were cleared.
|
|
1289
|
+
*/
|
|
1290
|
+
declare function resolveSdkSort(search: string): SearchSearchSort | undefined;
|
|
1291
|
+
//#endregion
|
|
1292
|
+
//#region src/filters.d.ts
|
|
1293
|
+
/**
|
|
1294
|
+
* The search filter model the UI consumes — client-safe, no SDK import. The SDK
|
|
1295
|
+
* exposes filter definitions via `getFilters`; a site curates them at build time
|
|
1296
|
+
* into `FilterDef[]` (which keys, in which order) and hands the list to the
|
|
1297
|
+
* results surface. Boolean filters toggle, integer filters are a lower bound.
|
|
1298
|
+
*/
|
|
1299
|
+
type FilterType = "boolean" | "int";
|
|
1300
|
+
interface FilterDef {
|
|
1301
|
+
/** The URL query key, which is also the SDK's search parameter key. */
|
|
1302
|
+
key: string;
|
|
1303
|
+
label: string;
|
|
1304
|
+
type: FilterType;
|
|
1305
|
+
/** Only meaningful for `int` filters. */
|
|
1306
|
+
min: number;
|
|
1307
|
+
max: number;
|
|
1308
|
+
}
|
|
1309
|
+
interface ActiveFilter {
|
|
1310
|
+
key: string;
|
|
1311
|
+
label: string;
|
|
1312
|
+
}
|
|
1313
|
+
/** A boolean filter is active when its param is present and truthy. */
|
|
1314
|
+
declare const isBooleanFilterActive: (value: string) => boolean;
|
|
1315
|
+
/**
|
|
1316
|
+
* The filters currently applied in the query, in the order the defs are
|
|
1317
|
+
* declared — the removable chips. An `int` label is prefixed with the localized
|
|
1318
|
+
* "minimum" wording, because its value is a lower bound.
|
|
1319
|
+
*/
|
|
1320
|
+
declare function activeFiltersFromQuery(searchParams: URLSearchParams, defs: readonly FilterDef[], minimumPrefix: string): ActiveFilter[];
|
|
1321
|
+
/** The dog-friendly search as the URL carries it: `pets=true` (or a count) and,
|
|
1322
|
+
* from an older vocabulary, `petsCount=N`. Read by the live search. */
|
|
1323
|
+
declare const PETS_PARAM = "pets";
|
|
1324
|
+
declare const PETS_COUNT_PARAM = "petsCount";
|
|
1325
|
+
declare function petsCountInQuery(search: string): number;
|
|
1326
|
+
//#endregion
|
|
1327
|
+
//#region src/regions.d.ts
|
|
1328
|
+
/**
|
|
1329
|
+
* The "Region" scope of the live search.
|
|
1330
|
+
*
|
|
1331
|
+
* "Region" is a v10 custom attribute (MULTI_SELECT, PUBLIC) set per rental in
|
|
1332
|
+
* v-office — e.g. Duhnen, Döse. The live search has no usable server-side key
|
|
1333
|
+
* for it: the SDK filters a custom attribute only on an exact raw match of its
|
|
1334
|
+
* stored JSON-array value, and registering the definition would fold a
|
|
1335
|
+
* `Region: ["…"]` string into every card and detail highlight (verified). So
|
|
1336
|
+
* `region` is dropped from the SDK query (search.ts, DROP_KEYS) and applied
|
|
1337
|
+
* here, client-side, against the per-rental regions the site hydrates from its
|
|
1338
|
+
* static catalogue at build time — the same channel as the other facts the
|
|
1339
|
+
* search projection does not carry.
|
|
1340
|
+
*/
|
|
1341
|
+
/** URL query key carrying the active region scope. Repeatable: several
|
|
1342
|
+
* `region=<name>` values OR together (show rentals in ANY selected region). */
|
|
1343
|
+
declare const REGION_PARAM = "region";
|
|
1344
|
+
/** The regions active in a URL query string: the `region` values, trimmed,
|
|
1345
|
+
* de-duplicated, order preserved. Absent or empty → `[]`. */
|
|
1346
|
+
declare function activeRegions(search: string): string[];
|
|
1347
|
+
/**
|
|
1348
|
+
* The results inside the region scope. A rental may carry several regions
|
|
1349
|
+
* (MULTI_SELECT) and matches when any of them is selected; with no active
|
|
1350
|
+
* region every result stays. A rental the map does not know is outside every
|
|
1351
|
+
* region, so it drops out of a scoped search rather than passing unseen.
|
|
1352
|
+
*/
|
|
1353
|
+
declare function scopeToRegions<T extends {
|
|
1354
|
+
id: string;
|
|
1355
|
+
}>(items: readonly T[], active: readonly string[], regionsByRentalId: (id: string) => readonly string[] | undefined): T[];
|
|
1356
|
+
//#endregion
|
|
1357
|
+
export { ActiveFilter, AlternativePeriod, 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, DEFAULT_RENTALS_PATH, DEFAULT_SORT, EndAvailability, FilterDef, FilterType, HUB_ENDPOINTS, InitialAvailability, type InsuranceDocsState, InsurancePaymentLink, Listener, LiveSearchOptions, type Locale, MULTI_RATE_CODES, NIGHT_RANGE_ENABLED, NIGHT_RANGE_KEYS, NO_INSURANCE_DOCS, NightRange, PETS_COUNT_PARAM, PETS_PARAM, PaymentLine, PaymentScheduleView, PolicyBranch, PriceQuote, QuoteAddonService, QuoteCancellationPolicy, QuoteInsurance, QuoteInsuranceOption, QuoteLine, QuoteOccupancy, QuotePolicyRule, QuoteResult, QuoteSelectionResult, QuoteUnavailableCode, QuoteView, QuoteVoucher, REGION_PARAM, SORT_OPTIONS, SORT_PARAM, STAGES, SdkConfig, SearchImage, SearchPayload, SearchResultItem, SnapshotImage, SortOption, SortValue, Stage, type Stay, Store, type StructureViolation, Unsubscribe, VALIDATED_FIELDS, VOFFICE_ENDPOINTS, type ValidationMessages, WebsiteSdk, WritableStore, activeFiltersFromQuery, activeRegions, activeSort, adaptGuestQuote, addQuoteService, assertCheckoutStructure, clearQuoteServices, createCheckoutFlow, createSdk, createStore, describeSdkError, detectCountry, emailError, fetchEndAvailability, fetchInitialAvailability, fetchQuote, fieldError, freeCancellationUntil, fullRefundRule, insuranceDocsPending, insuranceIdToReapply, insuranceProductKey, isBooleanFilterActive, isPricedQuery, isRateConfigurationFault, isSortAvailable, isSubmittableOption, isoDate, logSdkFailure, nightlyPrice, normalizePhone, occupancyOf, parseGermanParam, parseNightRange, parseParam, payWithOption, petsCountInQuery, phoneError, policyFreeCancellationUntil, postalcodeError, pricePolicyBranches, removeQuoteService, resolveSdkSort, runLiveSearch, saveBookingSnapshot, saveCheckoutContext, scopeToRegions, sdkConfigFor, selectQuoteInsurance, selectQuotePolicy, selectableInsuranceOptions, selectedInsuranceOption, shownInsuranceResources, stabilizeCardOrder, stayKeyOf, stayParams, streetWarning, submitBooking, tidyPrice };
|