@v-office/website-sdk 2.5.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +28 -1
  2. package/dist/cli.mjs +25 -4
  3. package/dist/{client-C5ojrNQA.mjs → client-BNbBfJtO.mjs} +468 -189
  4. package/dist/index.d.mts +5 -4
  5. package/dist/index.mjs +3 -3
  6. package/dist/instructions/CHANGELOG.md +3 -0
  7. package/dist/instructions/MIGRATION.md +3 -0
  8. package/dist/instructions/README.md +8 -2
  9. package/dist/instructions/booking.md +39 -3
  10. package/dist/instructions/creation.md +65 -64
  11. package/dist/instructions/custom-attributes.md +556 -0
  12. package/dist/instructions/filter.md +51 -23
  13. package/dist/instructions/rentals.md +26 -2
  14. package/dist/instructions/search.md +29 -3
  15. package/dist/instructions/versions/2.5.0/MIGRATION.md +5 -0
  16. package/dist/instructions/versions/2.6.0/CHANGELOG.md +6 -0
  17. package/dist/instructions/versions/2.6.0/MIGRATION.md +6 -0
  18. package/dist/instructions/versions/2.7.0/CHANGELOG.md +44 -0
  19. package/dist/instructions/versions/2.7.0/MIGRATION.md +79 -0
  20. package/dist/instructions/versions/2.8.0/CHANGELOG.md +63 -0
  21. package/dist/instructions/versions/2.8.0/MIGRATION.md +252 -0
  22. package/dist/{rentals-Lw1qUKIj.mjs → rentals-cCejb_2d.mjs} +21 -21
  23. package/dist/{search-9yzAqHCg.mjs → search-Dzzra6IW.mjs} +16 -133
  24. package/dist/{to-rental-highlights-CxWlq76f.mjs → to-rental-highlights-OvNe9ZQS.mjs} +8 -6
  25. package/dist/translations/v10/de-DE/core.json +2 -0
  26. package/dist/translations/v10/en-US/core.json +2 -0
  27. package/instructions/CHANGELOG.md +3 -0
  28. package/instructions/MIGRATION.md +3 -0
  29. package/instructions/README.md +8 -2
  30. package/instructions/booking.md +39 -3
  31. package/instructions/creation.md +65 -64
  32. package/instructions/custom-attributes.md +556 -0
  33. package/instructions/filter.md +51 -23
  34. package/instructions/rentals.md +26 -2
  35. package/instructions/search.md +29 -3
  36. package/instructions/versions/2.5.0/MIGRATION.md +5 -0
  37. package/instructions/versions/2.6.0/CHANGELOG.md +6 -0
  38. package/instructions/versions/2.6.0/MIGRATION.md +6 -0
  39. package/instructions/versions/2.7.0/CHANGELOG.md +44 -0
  40. package/instructions/versions/2.7.0/MIGRATION.md +79 -0
  41. package/instructions/versions/2.8.0/CHANGELOG.md +63 -0
  42. package/instructions/versions/2.8.0/MIGRATION.md +252 -0
  43. package/package.json +5 -3
@@ -1,133 +1,7 @@
1
- import { n as stable_filters_default } from "./client-C5ojrNQA.mjs";
2
- import { a as searchQuery, c as VofficeUnitDataFieldSchemas, m as parseVofficeUnitData, n as toImages, r as toAddress, t as toRentalHighlights } from "./to-rental-highlights-CxWlq76f.mjs";
3
- import { CoreSDKError, STABLE_SEARCH_INPUTS, collectQueryParameters, daysBetweenLocalDates, expandCustomAttributeFilterCompositions, parseChildrenAges, parseOccupancyCount, parseQueryParameters, toCustomAttributeFilterLabel, toFormattedSearchPrice, toIsoDateFromPeriodQueryDate, toQueryString, toStableSearchInputBackendQueryKeys, toStableSearchInputParameterValues, toStableSearchInputQueryKeys, toUnusedFilterKeys } from "@v-office/sdk-core";
1
+ import { a as parseQueryParameters, i as collectQueryParameters, n as makeV9CustomAttributeFilterCapabilities, o as toQueryString, r as toBasicQueryInputs, s as applyVofficeFilterDerivedBasicQueryInputs } from "./client-BNbBfJtO.mjs";
2
+ import { a as searchQuery, c as VofficeUnitDataFieldSchemas, m as parseVofficeUnitData, n as toImages, r as toAddress, t as toRentalHighlights } from "./to-rental-highlights-OvNe9ZQS.mjs";
3
+ import { CoreSDKError, STABLE_SEARCH_INPUTS, daysBetweenLocalDates, expandCustomAttributeFilterCompositions, parseChildrenAges, parseOccupancyCount, resolveExpandedCompositions, toFormattedSearchPrice, toIsoDateFromPeriodQueryDate, toStableSearchInputBackendQueryKeys, toStableSearchInputParameterValues, toStableSearchInputQueryKeys, toUnusedFilterKeys } from "@v-office/sdk-core";
4
4
  import { Effect } from "effect";
5
- //#region src/legacy-v9/parser/search/input/filter/apply-voffice-filter-derived-basic-query-inputs.ts
6
- const hasBasicQueryInput = ({ basicQueryInputs, key }) => basicQueryInputs.some((input) => input.key === key);
7
- const derivedBooleanFilters = [{
8
- key: "pets",
9
- isSelected: (filter) => (filter.petsCount ?? 0) > 0
10
- }];
11
- const applyVofficeFilterDerivedBasicQueryInputs = ({ appliedFilters, basicQueryInputs, filter, locale, translations }) => Effect.gen(function* () {
12
- const nextAppliedFilters = [...appliedFilters];
13
- const nextBasicQueryInputs = [...basicQueryInputs];
14
- for (const derivedFilter of derivedBooleanFilters) {
15
- if (!derivedFilter.isSelected(filter) || hasBasicQueryInput({
16
- basicQueryInputs: nextBasicQueryInputs,
17
- key: derivedFilter.key
18
- })) continue;
19
- nextBasicQueryInputs.push({
20
- key: derivedFilter.key,
21
- value: true
22
- });
23
- nextAppliedFilters.push({
24
- key: derivedFilter.key,
25
- label: yield* translations.translate(locale, `filter.${derivedFilter.key}.label`, derivedFilter.key)
26
- });
27
- }
28
- return {
29
- appliedFilters: nextAppliedFilters,
30
- basicQueryInputs: nextBasicQueryInputs
31
- };
32
- });
33
- //#endregion
34
- //#region src/legacy-v9/parser/search/input/filter/filter-query-keys.ts
35
- const FILTER_QUERY_KEYS = new Set(Object.keys(stable_filters_default));
36
- //#endregion
37
- //#region src/legacy-v9/parser/search/input/filter/to-basic-query-inputs.ts
38
- const isEnabledBooleanFilter = (value) => {
39
- const normalizedValue = value.toLowerCase();
40
- return normalizedValue === "" || normalizedValue === "true" || normalizedValue === "1";
41
- };
42
- const toBasicQueryInput = ({ queryKey, value, filter }) => {
43
- if (filter.type === "BooleanFilter") return isEnabledBooleanFilter(value) ? {
44
- key: queryKey,
45
- value: true
46
- } : void 0;
47
- if (filter.type === "InputFilter") {
48
- const numberValue = Number(value);
49
- if (!Number.isInteger(numberValue) || numberValue < 0) return;
50
- return {
51
- key: queryKey,
52
- value: numberValue
53
- };
54
- }
55
- };
56
- const toCustomBackendFilterInput = ({ queryKey, value, filter }) => {
57
- switch (filter.type) {
58
- case "boolean": return isEnabledBooleanFilter(value) ? {
59
- key: queryKey,
60
- value: true
61
- } : void 0;
62
- case "int": {
63
- const numberValue = Number(value);
64
- return Number.isInteger(numberValue) && numberValue >= 0 ? {
65
- key: queryKey,
66
- value: numberValue
67
- } : void 0;
68
- }
69
- case "string": return value.length > 0 ? {
70
- key: queryKey,
71
- value
72
- } : void 0;
73
- case "option": return filter.options.some((option) => option.value === value) ? {
74
- key: queryKey,
75
- value
76
- } : void 0;
77
- }
78
- };
79
- const toBasicQueryInputs = ({ query, customAttributeFilterDefinitions, locale, translations }) => {
80
- const appliedFilterEffects = [];
81
- const basicQueryInputs = [];
82
- const remainingParameters = [];
83
- const seenScalarFilters = /* @__PURE__ */ new Set();
84
- const customBackendFiltersByKey = new Map(customAttributeFilterDefinitions.filter((definition) => definition.source === "backendFilter" && definition.searchable).map((definition) => [definition.key, definition]));
85
- for (const [queryKey, value] of parseQueryParameters(query)) {
86
- const filter = stable_filters_default[queryKey];
87
- const customBackendFilter = customBackendFiltersByKey.get(queryKey);
88
- if ((!FILTER_QUERY_KEYS.has(queryKey) || filter == null) && customBackendFilter == null) {
89
- remainingParameters.push([queryKey, value]);
90
- continue;
91
- }
92
- if (seenScalarFilters.has(queryKey)) {
93
- remainingParameters.push([queryKey, value]);
94
- continue;
95
- }
96
- const basicQueryInput = filter == null ? customBackendFilter != null ? toCustomBackendFilterInput({
97
- queryKey,
98
- value,
99
- filter: customBackendFilter
100
- }) : void 0 : toBasicQueryInput({
101
- queryKey,
102
- value,
103
- filter
104
- });
105
- if (basicQueryInput == null) {
106
- remainingParameters.push([queryKey, value]);
107
- continue;
108
- }
109
- seenScalarFilters.add(queryKey);
110
- basicQueryInputs.push(basicQueryInput);
111
- if (customBackendFilter != null) appliedFilterEffects.push(Effect.succeed({
112
- key: queryKey,
113
- label: toCustomAttributeFilterLabel({
114
- definition: customBackendFilter,
115
- locale,
116
- value
117
- })
118
- }));
119
- else appliedFilterEffects.push(translations.translate(locale, `filter.${queryKey}.label`, queryKey).pipe(Effect.map((label) => ({
120
- key: queryKey,
121
- label
122
- }))));
123
- }
124
- return {
125
- appliedFilters: Effect.all(appliedFilterEffects),
126
- basicQueryInputs,
127
- remainingQuery: toQueryString(remainingParameters)
128
- };
129
- };
130
- //#endregion
131
5
  //#region src/legacy-v9/parser/search/input/search-input-error.ts
132
6
  const failInvalidV9SearchInput = (message) => Effect.fail(new CoreSDKError({
133
7
  source: "v10",
@@ -305,6 +179,7 @@ const toVofficeSorting = (sort) => {
305
179
  //#region src/legacy-v9/parser/search/input/to-query-input.ts
306
180
  const toQueryInput = ({ query, sort, customAttributeFilterDefinitions, locale, translations }) => Effect.gen(function* () {
307
181
  const compositionResult = expandCustomAttributeFilterCompositions({
182
+ capabilities: makeV9CustomAttributeFilterCapabilities(customAttributeFilterDefinitions),
308
183
  customAttributeFilterDefinitions,
309
184
  locale,
310
185
  query
@@ -330,8 +205,16 @@ const toQueryInput = ({ query, sort, customAttributeFilterDefinitions, locale, t
330
205
  locale,
331
206
  translations
332
207
  });
208
+ const compositionFilters = resolveExpandedCompositions({
209
+ compositions: compositionResult.compositions,
210
+ unusedFilterKeys: [
211
+ ...compositionResult.unusedFilterKeys,
212
+ ...occupancyResult?.unusedFilterKeys ?? [],
213
+ ...toUnusedFilterKeys(basicQueryResult.remainingQuery)
214
+ ]
215
+ });
333
216
  return {
334
- appliedFilters: [...compositionResult.appliedFilters, ...appliedFilters],
217
+ appliedFilters: [...compositionFilters.appliedFilters, ...appliedFilters],
335
218
  queryInput: {
336
219
  vofficeData: {
337
220
  filter: Object.keys(filter).length > 0 ? filter : void 0,
@@ -340,7 +223,7 @@ const toQueryInput = ({ query, sort, customAttributeFilterDefinitions, locale, t
340
223
  },
341
224
  basicQueryInputs
342
225
  },
343
- unusedFilterKeys: [...occupancyResult?.unusedFilterKeys ?? [], ...toUnusedFilterKeys(basicQueryResult.remainingQuery)]
226
+ unusedFilterKeys: compositionFilters.unusedFilterKeys
344
227
  };
345
228
  });
346
229
  //#endregion
@@ -356,7 +239,7 @@ const toFormattedPrice = ({ locale, calc }) => {
356
239
  };
357
240
  //#endregion
358
241
  //#region src/legacy-v9/parser/search/to-search-output-item.ts
359
- const toSearchOutputItem = ({ locale, imageProxyBaseUrl, rentalHighlightPrioritization, customAttributeFilterDefinitions, rental, translations, includeFormattedTotal }) => Effect.gen(function* () {
242
+ const toSearchOutputItem = ({ locale, imageProxyBaseUrl, rentalHighlightPrioritization, customAttributes, rental, translations, includeFormattedTotal }) => Effect.gen(function* () {
360
243
  const data = parseVofficeUnitData(rental.data);
361
244
  const item = {
362
245
  id: rental.voffice_id.toString(),
@@ -371,7 +254,7 @@ const toSearchOutputItem = ({ locale, imageProxyBaseUrl, rentalHighlightPrioriti
371
254
  locale,
372
255
  translations,
373
256
  highlightPrioritization: rentalHighlightPrioritization,
374
- customAttributeFilterDefinitions,
257
+ customAttributes,
375
258
  attributes: data
376
259
  });
377
260
  if (highlights != null) item.highlights = highlights;
@@ -1,4 +1,4 @@
1
- import { localeToLanguage, makeTranslate, renderCustomAttributeHighlight, renderLabeledAttributeValue, toCountry } from "@v-office/sdk-core";
1
+ import { localeToLanguage, makeTranslate, renderLabeledAttributeValue, renderResolvedCustomAttribute, toCountry } from "@v-office/sdk-core";
2
2
  import { Effect, Schema } from "effect";
3
3
  //#region codegen/v9/heuristic-generation/v9-data.ts
4
4
  /**
@@ -5870,7 +5870,7 @@ const toV9RentalHighlightKey = (key) => {
5870
5870
  };
5871
5871
  //#endregion
5872
5872
  //#region src/legacy-v9/parser/rentals/to-rental-highlights.ts
5873
- const toRentalHighlights = ({ locale, translations, highlightPrioritization, customAttributeFilterDefinitions, attributes }) => Effect.gen(function* () {
5873
+ const toRentalHighlights = ({ locale, translations, highlightPrioritization, customAttributes, attributes }) => Effect.gen(function* () {
5874
5874
  const highlights = [];
5875
5875
  const seenKeys = /* @__PURE__ */ new Set();
5876
5876
  const knownAttributes = attributes.known;
@@ -5880,11 +5880,13 @@ const toRentalHighlights = ({ locale, translations, highlightPrioritization, cus
5880
5880
  if (v9Key == null) {
5881
5881
  if (seenKeys.has(highlightKey)) continue;
5882
5882
  seenKeys.add(highlightKey);
5883
- const highlight = renderCustomAttributeHighlight({
5884
- definitions: customAttributeFilterDefinitions,
5885
- key: highlightKey,
5883
+ const attribute = customAttributes.byKey[highlightKey];
5884
+ if (attribute === void 0) continue;
5885
+ const binding = attribute.v9;
5886
+ const highlight = renderResolvedCustomAttribute({
5887
+ attribute,
5886
5888
  locale,
5887
- value: knownAttributes[highlightKey] ?? attributes.unknown[highlightKey]
5889
+ value: binding === void 0 ? void 0 : knownAttributes[binding] ?? attributes.unknown[binding]
5888
5890
  });
5889
5891
  if (highlight != null) highlights.push(highlight);
5890
5892
  continue;
@@ -5077,6 +5077,8 @@
5077
5077
  "RentalAttributesCategory.MEDIA.label": "Medien",
5078
5078
  "RentalAttributesCategory.OTHER.label": "Andere",
5079
5079
  "RentalAttributesCategory.OUTDOORS.label": "Außenbereich",
5080
+ "RentalAttributesCategory.roomSummary.label": "Zimmer",
5081
+ "RentalAttributesCategory.vicinity.label": "Umgebung",
5080
5082
  "RentalAttributesCategory.PRACTICAL_NECESSITIES.label": "Praktische Notwendigkeiten",
5081
5083
  "RentalAttributesCategory.PROPERTY_DETAILS_RULES.label": "Objektdetails & Regeln",
5082
5084
  "RentalAttributesCategory.PROPERTY_TYPE_LOCATION.label": "Unterkunftstyp & Standort",
@@ -5077,6 +5077,8 @@
5077
5077
  "RentalAttributesCategory.MEDIA.label": "Media",
5078
5078
  "RentalAttributesCategory.OTHER.label": "Other",
5079
5079
  "RentalAttributesCategory.OUTDOORS.label": "Outdoor Area",
5080
+ "RentalAttributesCategory.roomSummary.label": "Rooms",
5081
+ "RentalAttributesCategory.vicinity.label": "Vicinity",
5080
5082
  "RentalAttributesCategory.PRACTICAL_NECESSITIES.label": "Practical Necessities",
5081
5083
  "RentalAttributesCategory.PROPERTY_DETAILS_RULES.label": "Property Details & Rules",
5082
5084
  "RentalAttributesCategory.PROPERTY_TYPE_LOCATION.label": "Property Type & Location",
@@ -4,6 +4,9 @@ This file is the versioned changelog index for the website SDK instructions.
4
4
 
5
5
  ## Versions
6
6
 
7
+ - `versions/2.8.0/CHANGELOG.md`: `@v-office/website-sdk` 2.8.0 release notes.
8
+ - `versions/2.7.0/CHANGELOG.md`: `@v-office/website-sdk` 2.7.0 release notes.
9
+ - `versions/2.6.0/CHANGELOG.md`: compatibility note for an unreleased version label.
7
10
  - `versions/2.5.0/CHANGELOG.md`: `@v-office/website-sdk` 2.5.0 release notes.
8
11
  - `versions/2.4.3/CHANGELOG.md`: `@v-office/website-sdk` 2.4.3 documentation release notes.
9
12
  - `versions/2.4.2/CHANGELOG.md`: `@v-office/website-sdk` 2.4.2 release notes.
@@ -4,6 +4,9 @@ This file is the versioned migration index for the website SDK instructions.
4
4
 
5
5
  ## Available Guides
6
6
 
7
+ - `versions/2.8.0/MIGRATION.md`: migrate from `@v-office/website-sdk` 2.7.0 to 2.8.0.
8
+ - `versions/2.7.0/MIGRATION.md`: migrate from `@v-office/website-sdk` 2.5.x to 2.7.0.
9
+ - `versions/2.6.0/MIGRATION.md`: compatibility note for an unreleased version label.
7
10
  - `versions/2.5.0/MIGRATION.md`: migrate from `@v-office/website-sdk` 2.4.3 to 2.5.0.
8
11
  - `versions/2.4.3/MIGRATION.md`: migrate from `@v-office/website-sdk` 2.4.2 to 2.4.3.
9
12
  - `versions/2.4.2/MIGRATION.md`: migrate from `@v-office/website-sdk` 2.4.1 to 2.4.2.
@@ -1,12 +1,15 @@
1
1
  # Website SDK Instructions
2
2
 
3
- These instructions describe `@v-office/website-sdk` 2.5.0.
3
+ These instructions describe the current source targeted for
4
+ `@v-office/website-sdk` 2.8.0. Package versions remain unchanged until the release
5
+ workflow performs the coordinated core and website bumps.
4
6
 
5
7
  Use this directory as the consumer-facing reference for the package:
6
8
 
7
9
  - `creation.md`: SDK construction, config, options, and CLI config files.
8
10
  - `rentals.md`: `sdk.static.rentals.getRentals`.
9
11
  - `filter.md`: `sdk.static.filter.getFilters`.
12
+ - `custom-attributes.md`: custom attribute catalog, configuration, per-backend behaviour, and the v9-to-v10 move.
10
13
  - `search.md`: `sdk.live.search.search`.
11
14
  - `availability.md`: date-picker availability flows.
12
15
  - `quote.md`: quote, additional services, cancellation policy, and insurance flows.
@@ -15,6 +18,9 @@ Use this directory as the consumer-facing reference for the package:
15
18
  - `document-structured-json.md`: `sdk.static.documents.getTermsAndPrivacyPolicy` and structured document JSON rendering rules.
16
19
  - `CHANGELOG.md`: versioned changelog index.
17
20
  - `MIGRATION.md`: versioned migration index.
21
+ - `versions/2.8.0/`: 2.8.0 custom-attribute release notes and 2.7.0-to-2.8.0 migration guide.
22
+ - `versions/2.7.0/`: 2.7.0 release notes and 2.5.x-to-2.7.0 migration guide.
23
+ - `versions/2.6.0/`: compatibility pointers for the unreleased 2.6.0 version label.
18
24
  - `versions/2.5.0/`: 2.5.0 release notes and 2.4.3-to-2.5.0 migration guide.
19
25
  - `versions/2.4.3/`: 2.4.3 documentation release notes and migration guide.
20
26
  - `versions/2.4.2/`: 2.4.2 release notes and 2.4.1-to-2.4.2 migration guide.
@@ -68,4 +74,4 @@ website-sdk --backend v10 search --locale de-DE --query "adults=2"
68
74
  website-sdk --backend v10 search --locale de-DE --query "adults=2" --sort '{"by":"field","orderBy":{"label":"ASC"}}'
69
75
  ```
70
76
 
71
- Config file examples in these docs use the flat `WebsiteSDKConfig` shape introduced in 2.0.0 and kept in 2.5.0. v10 config additionally requires `searchEndpoint` as of 2.5.0.
77
+ Config file examples in these docs use the flat `WebsiteSDKConfig` shape introduced in 2.0.0 and kept in 2.8.0. v10 config additionally requires `searchEndpoint` as of 2.5.0.
@@ -100,6 +100,13 @@ Sample:
100
100
  "provider": "adyen",
101
101
  "label": "Credit card",
102
102
  "url": "https://checkout.example.com"
103
+ },
104
+ {
105
+ "kind": "stripe_checkout",
106
+ "provider": "stripe",
107
+ "label": "Credit card",
108
+ "sessionId": "cs_live_a1Upmvi1v62AdILeDZl4l",
109
+ "accountId": "acct_1MpAB7AFrTg9gw7b"
103
110
  }
104
111
  ]
105
112
  }
@@ -113,13 +120,42 @@ Payment option variants:
113
120
  - `bank_transfer`: display-only payment details.
114
121
  - `redirect`: redirect the browser to the payment URL.
115
122
  - `form_post`: submit a generated HTML form, currently used for PayPal-style flows.
116
- - `stripe_checkout`: redirect to Stripe Checkout when a `url` is present.
123
+ - `stripe_checkout`: open Stripe Checkout. Emitted on `v9` only.
124
+
125
+ ## Payment Submission
126
+
127
+ Use `sdk.live.booking.submitPaymentOption(option)` or the top-level `submitPaymentOption(option)` export for every option except `bank_transfer`, which is display-only. Submission requires a browser environment and fails outside one.
128
+
129
+ Render a button for every submittable option. Do not gate rendering on `url`: a `stripe_checkout` option is submittable as soon as it carries a `sessionId`.
130
+
131
+ Stripe Checkout is opened one of two ways:
132
+
133
+ - When the option has a `url`, the browser is redirected to it.
134
+ - Otherwise the SDK loads Stripe.js from `https://js.stripe.com/v3/` and opens the session with `sessionId`, passing `accountId` as the connected account. The vOffice platform publishable keys are built in, and test (`cs_test_`) and live (`cs_live_`) sessions are handled automatically, so Stripe needs no configuration.
135
+
136
+ Stripe.js is fetched on first use and reused afterwards, and an existing `window.Stripe` instance is reused when the page already provides one. A failed load is retried by the next submission instead of being remembered, so a guest can retry a Stripe payment without reloading the page. Sites with a Content-Security-Policy must allow `script-src https://js.stripe.com` and `frame-src https://js.stripe.com https://hooks.stripe.com`.
137
+
138
+ Booking creation and payment are separate steps. When a payment option is on screen the reservation already exists, so present a submission failure as an unpaid booking rather than a failed booking:
139
+
140
+ ```ts
141
+ try {
142
+ await sdk.live.booking.submitPaymentOption(option);
143
+ } catch (error) {
144
+ showPaymentFailure({ bookingNumber: booking.bookingNumber, error });
145
+ }
146
+ ```
147
+
148
+ ## Payment Return
149
+
150
+ Online payment providers return the guest to a URL derived from `relativeRedirectUrl`. Stripe returns to that path with `payment=stripe&success=true` after payment and `payment=stripe&cancel=true` after cancellation, so the path must exist in your site and should read those query parameters.
151
+
152
+ `relativeRedirectUrl` is supplied before the reservation exists, so the return URL cannot carry the booking number or guest token. Persist whatever the return page needs, for example in `sessionStorage`, before submitting a payment option.
117
153
 
118
- Use `sdk.live.booking.submitPaymentOption(option)` or the top-level `submitPaymentOption(option)` export for non-bank-transfer payment options in a browser environment.
154
+ A successful return means the provider sent the browser back, not that the payment has settled. Confirmation reaches vOffice through the provider's webhook.
119
155
 
120
156
  ## Configuration
121
157
 
122
- Booking requests are rate-limited to one backend request per second with a queue size of five. `v9` books through the v0 `book` action and can initialize Stripe payment data. `v10` books through GraphQL and initializes Adyen redirect payment options.
158
+ Booking requests are rate-limited to one backend request per second with a queue size of five. `v9` books through the v0 `book` action and initializes one Stripe Checkout Session per payment schedule option when the property has Stripe enabled. `v10` books through GraphQL and initializes Adyen redirect payment options.
123
159
 
124
160
  ## CLI Usage
125
161
 
@@ -81,6 +81,7 @@ const options = defineWebsiteSDKOptions({
81
81
  },
82
82
  rentalHighlightPrioritization: ["bedrooms", "bathrooms", "maxPersons", "wifi"],
83
83
  rentalPropertyHighlightPrioritization: ["wifi", "parking"],
84
+ customAttributes,
84
85
  customAttributeFilterDefinitions: [],
85
86
  translationOverrides: {},
86
87
  });
@@ -91,8 +92,8 @@ const options = defineWebsiteSDKOptions({
91
92
  ```ts
92
93
  type WebsiteSDKOptions = {
93
94
  translationOverrides?: TranslationOverrides;
95
+ customAttributes?: CustomAttributeRegistrySnapshot;
94
96
  customAttributeFilterDefinitions?: readonly CustomAttributeFilterDefinition[];
95
- customAttributeDefinitionManifest?: CustomAttributeDefinitionManifest;
96
97
  rentalHighlightPrioritization?: readonly RentalHighlightPrioritizationKey[];
97
98
  rentalPropertyHighlightPrioritization?: readonly RentalPropertyHighlightPrioritizationKey[];
98
99
  rentalScope?: RentalScope;
@@ -102,8 +103,8 @@ type WebsiteSDKOptions = {
102
103
 
103
104
  Unset options are normalized by the SDK:
104
105
 
106
+ - `customAttributes` defaults to an empty registry.
105
107
  - `customAttributeFilterDefinitions` defaults to `[]`.
106
- - `customAttributeDefinitionManifest` is omitted unless provided; an absent manifest is treated as empty.
107
108
  - `rentalHighlightPrioritization` defaults to the SDK default rental highlight order.
108
109
  - `rentalPropertyHighlightPrioritization` defaults to the SDK default property highlight order.
109
110
  - `rentalScope` defaults to `{}`.
@@ -130,31 +131,45 @@ const options = defineWebsiteSDKOptions({
130
131
 
131
132
  The scope is always applied internally and is not returned by `static.filter.getFilters(...)`. Use it for website-level or tenant-level constraints.
132
133
 
133
- ## Custom Attribute Filter Definitions
134
+ ## Custom Attributes
134
135
 
135
- Use `customAttributeFilterDefinitions` to add SDK-known filter keys for custom attributes, v9 backend filters, or composed filters:
136
+ Custom attributes are configured through `customAttributes`, built by
137
+ `defineCustomAttributes` from the workspace catalog plus your product policy:
138
+
139
+ ```ts
140
+ const customAttributes = defineCustomAttributes({
141
+ catalog,
142
+ select: {
143
+ region: {
144
+ label: { "de-DE": "Region", "en-US": "Region" },
145
+ filter: true,
146
+ optionLabels: {
147
+ north: { "de-DE": "Nord", "en-US": "North" },
148
+ },
149
+ },
150
+ },
151
+ });
152
+
153
+ const options = defineWebsiteSDKOptions({ customAttributes });
154
+ ```
155
+
156
+ See `custom-attributes.md` for the catalog, the selection shape, per-backend
157
+ behaviour, and the v9-to-v10 move.
158
+
159
+ ## Backend Filters and Composed Filters
160
+
161
+ `customAttributeFilterDefinitions` holds the two filter concepts that are _not_
162
+ custom attributes:
136
163
 
137
164
  ```ts
138
165
  const options = defineWebsiteSDKOptions({
139
166
  customAttributeFilterDefinitions: [
140
167
  {
141
- key: "region",
142
- source: "customAttribute",
143
- category: "ESSENTIALS",
144
- label: {
145
- "de-DE": "Region",
146
- "en-US": "Region",
147
- },
148
- type: "option",
149
- options: [
150
- {
151
- value: "north",
152
- label: {
153
- "de-DE": "Nord",
154
- "en-US": "North",
155
- },
156
- },
157
- ],
168
+ key: "atLeastTwoBedrooms",
169
+ source: "backendFilter",
170
+ category: "LIVING_SLEEPING",
171
+ label: { "de-DE": "Mind. zwei Schlafzimmer", "en-US": "At least two bedrooms" },
172
+ type: "boolean",
158
173
  searchable: true,
159
174
  internal: false,
160
175
  },
@@ -165,59 +180,27 @@ const options = defineWebsiteSDKOptions({
165
180
  Supported filter types:
166
181
 
167
182
  - `boolean`: accepts enabled query values such as `?key`, `?key=true`, or `?key=1`.
168
- - `int`: accepts integer query values and can expose optional `numericBounds` for UI controls.
183
+ - `int`: accepts integer query values. The definition includes a `numericBounds`
184
+ object; omit its `min` and `max` fields when the UI needs no bounds.
169
185
  - `option`: accepts one of the configured `options` values and exposes those options through `getFilters`.
170
186
  - `string`: accepts any non-empty query value as an exact raw value.
171
187
  - `complex`: composition-only type that expands one boolean query-string filter into other configured filters.
172
188
 
173
189
  Supported sources:
174
190
 
175
- - `customAttribute`: backed by custom attributes.
176
- - `backendFilter`: backed by backend-defined search filter keys, mainly for v9 hub filters.
177
- - `composition`: expands one boolean query-string filter into other configured filters.
191
+ - `backendFilter`: backed by backend-defined search filter keys, mainly for v9 hub filters. Advertised and executed by v9 only.
192
+ - `composition`: expands one boolean query-string filter into other configured
193
+ filters. It is advertised only when every configured leaf can execute on the
194
+ active backend, and no generated leaf is committed unless all leaves are valid.
195
+ Nested compositions are rejected while recursive expansion is unsupported.
178
196
 
179
- Visibility:
197
+ Exposure:
180
198
 
181
199
  - `internal: false` exposes searchable filters through `static.filter.getFilters(...)`.
182
200
  - `internal: true` keeps filters usable by search and compositions but hides them from `getFilters`.
183
- - `searchable: false` keeps custom attributes as display metadata only.
184
201
 
185
- ## Custom Attribute Definition Manifest
186
-
187
- The v10 REST search backend filters and returns custom attributes by definition ID, while SDK configuration and query strings use keys. Because definition IDs are workspace/environment specific, use `customAttributeDefinitionManifest` to give the SDK a key-to-ID and ID-to-key mapping:
188
-
189
- ```ts
190
- const options = defineWebsiteSDKOptions({
191
- customAttributeDefinitionManifest: {
192
- byKey: {
193
- region: { id: "def-1", key: "region", type: "OPTION", visibility: "PUBLIC" },
194
- },
195
- byId: {
196
- "def-1": { id: "def-1", key: "region", type: "OPTION", visibility: "PUBLIC" },
197
- },
198
- },
199
- });
200
- ```
201
-
202
- ```ts
203
- type CustomAttributeDefinitionManifestEntry = {
204
- id: string;
205
- key: string;
206
- type: "BOOLEAN" | "INT" | "STRING" | "OPTION";
207
- visibility: "PUBLIC" | string | null;
208
- options?: readonly { value: string; label?: Record<string, string> }[];
209
- };
210
-
211
- type CustomAttributeDefinitionManifest = {
212
- byKey: Record<string, CustomAttributeDefinitionManifestEntry>;
213
- byId: Record<string, CustomAttributeDefinitionManifestEntry>;
214
- };
215
- ```
216
-
217
- - Generate the manifest at build time from your definitions source, and pass a separate manifest per workspace/environment because IDs are not portable.
218
- - Custom-attribute keys that have no manifest entry are left in `unusedFilterKeys` instead of being applied as filters.
219
- - Only definitions with `visibility` of `PUBLIC` (or `null`) are surfaced as v10 search highlights; others are ignored.
220
- - The manifest is only used by v10 search. v9 and the non-search v10 endpoints ignore it.
202
+ `internal` is a UI-visibility choice, not a security boundary — an internal filter is
203
+ still parseable from a query string. See `custom-attributes.md`.
221
204
 
222
205
  ## Translation Overrides
223
206
 
@@ -262,8 +245,26 @@ Example `website-sdk-options.json`:
262
245
  "rentalScope": {
263
246
  "propertyId": "property-1"
264
247
  },
265
- "rentalHighlightPrioritization": ["bedrooms", "bathrooms", "maxPersons", "wifi"],
248
+ "customAttributes": {
249
+ "catalog": {
250
+ "privateSauna": {
251
+ "id": "GQYKYgMVh28X7VLGursbkt",
252
+ "type": "BOOLEAN"
253
+ }
254
+ },
255
+ "select": {
256
+ "privateSauna": {
257
+ "label": { "de-DE": "Sauna", "en-US": "Sauna" },
258
+ "filter": true
259
+ }
260
+ }
261
+ },
262
+ "rentalHighlightPrioritization": ["bedrooms", "privateSauna", "wifi"],
266
263
  "customAttributeFilterDefinitions": [],
267
264
  "translationOverrides": {}
268
265
  }
269
266
  ```
267
+
268
+ CLI options carry the custom-attribute **catalog and selection inputs**, not a
269
+ pre-built registry. The CLI validates and joins them before creating the SDK. See
270
+ `custom-attributes.md` for the complete shape and key-collision rules.