@v-office/website-headless 1.4.1 → 1.5.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.
package/README.md CHANGED
@@ -1457,8 +1457,8 @@ serializable, so it stays in this store until then.
1457
1457
  | `caps` | none | `{ maxGuests?, petsAllowed? }` from the catalogue, until `loadGuestCaps()` adds availability's |
1458
1458
  | `onEvent` | none | receives every `CheckoutEventDetail`; without it a rate fault is logged with `console.warn` |
1459
1459
 
1460
- `start()` parks the context and prices the handed-over stay (idempotent); `dispose()` drops every
1461
- in-flight result.
1460
+ `start()` parks the context, recognizes a stay this tab already booked (`earlierBooking`) and prices
1461
+ the handed-over stay (idempotent); `dispose()` drops every in-flight result.
1462
1462
 
1463
1463
  `CheckoutPhase` (`state.phase.kind`): `loading`, `missing-stay` (no complete stay arrived), `ready`,
1464
1464
  `unavailable` (`reason`), `error` (`retry()`), `booked` (`confirmation` is set).
@@ -1468,6 +1468,7 @@ refused while `blockers` is not empty.
1468
1468
 
1469
1469
  | blocker | meaning |
1470
1470
  | --------------------- | ---------------------------------------------------------- |
1471
+ | `earlier-booking` | this tab already booked the rental for these dates |
1471
1472
  | `no-quote` | no priced quote, or the phase is not `ready` |
1472
1473
  | `price-pending` | the shown price does not belong to the shown stay yet |
1473
1474
  | `busy` | a selection or a re-quote is running |
@@ -1482,7 +1483,13 @@ extra plus `count`, `canAdd`, `canRemove`), `policy` (`CheckoutPolicyState`: `ap
1482
1483
  `noneLabel`, `chosen`, `documents`, `requiredDocuments`, `documentsPending`), `voucher`
1483
1484
  (`CheckoutVoucherState`: `applied`, `error`, `checking`), `consents`, `pricePending`, `busy`,
1484
1485
  `issue` (`CheckoutIssue`: `reason`, `retryable`), `selectionError`, `submit`
1485
- (`"idle" | "submitting" | "failed"`), `confirmation`, `payment` (`busyOption`, `failed`).
1486
+ (`"idle" | "submitting" | "failed"`), `confirmation`, `earlierBooking`, `payment` (`busyOption`,
1487
+ `failed`).
1488
+
1489
+ `earlierBooking` is the `BookingSnapshot` of the booking this tab already made for this rental and
1490
+ these dates (a reload of the booked checkout, the browser's back button), read on `start()` from the
1491
+ payment-return snapshot; null otherwise. A UI renders it in place of the order (its `bookingNumber`
1492
+ and `kind`; no payment entries, the confirmation e-mail has them). Other dates are not that booking.
1486
1493
 
1487
1494
  | action | what it does |
1488
1495
  | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
@@ -1573,8 +1580,9 @@ The checkout flow calls these; a custom checkout built without the flow calls th
1573
1580
  - `submitBooking(sdk, quote, contact, options)` → `BookingConfirmation` (`kind`, `bookingNumber`,
1574
1581
  `schedules`, `alternatives` (settle the whole balance at once: an alternative, never added),
1575
1582
  `insurancePayments`, `payable`). ⚠ Reserves for real. `SubmitBookingOptions`: `returnPath`,
1576
- `kind`, `bankHolderFallback`, `stay`. Writes the payment-return snapshot the moment the booking
1577
- number exists. Logs the SDK's cause and rethrows.
1583
+ `kind`, `bankHolderFallback`, `stay`, `rentalId` (a reloaded checkout recognizes the booking by
1584
+ it). Writes the payment-return snapshot the moment the booking number exists. Logs the SDK's cause
1585
+ and rethrows.
1578
1586
  - `PaymentScheduleView`: `kind`, `label`, `dueOn` (a complete sentence: render, never parse),
1579
1587
  `paymentAt`, `lines` (`PaymentLine`), `amount`, `options` (empty = informational).
1580
1588
  - `isSubmittableOption(option)`: every kind but `bank_transfer`.
@@ -1601,14 +1609,16 @@ the guest's declaration).
1601
1609
  ### Payment return
1602
1610
 
1603
1611
  The return URL cannot carry the booking number, so a snapshot this tab wrote (sessionStorage, only
1604
- what the page renders, no personal data) is what the return page shows.
1612
+ what the page renders and the rental id, no personal data) is what the return page shows, and what
1613
+ the checkout recognizes a booked stay by (`earlierBooking`).
1605
1614
 
1606
1615
  - `readPaymentReturn(search, storage?)` → `PaymentReturn`, by `state`: `"booked"` (this tab booked:
1607
1616
  `snapshot` and `paymentCancelled`), `"returned"` (a provider return without a snapshot:
1608
1617
  `paymentCancelled`), or `"none"` (a bare or tracking-only URL). `paymentCancelled` is the
1609
1618
  provider's cancel marker (`payment=stripe&cancel=true`).
1610
- - `BookingSnapshot`: `bookingNumber`, `kind`, `rentalName?`, `location?`, `href?`, `images?`
1611
- (`SnapshotImage`, at most five), `start?`, `end?`, `guests?`, `total?`.
1619
+ - `BookingSnapshot`: `bookingNumber`, `kind`, `rentalId?` (since 1.5.0), `rentalName?`,
1620
+ `location?`, `href?`, `images?` (`SnapshotImage`, at most five), `start?`, `end?`, `guests?`,
1621
+ `total?`.
1612
1622
  - `saveCheckoutContext(context, storage?)`: the flow calls it on `start()`. Never throws.
1613
1623
  - `CHECKOUT_CONTEXT_KEY`, `BOOKING_SNAPSHOT_KEY`: the sessionStorage keys.
1614
1624
 
package/dist/index.d.mts CHANGED
@@ -1831,6 +1831,9 @@ interface SubmitBookingOptions {
1831
1831
  bankHolderFallback?: string | undefined;
1832
1832
  /** The stay being booked, for the payment-return snapshot. */
1833
1833
  stay: Stay;
1834
+ /** The rental being booked, for the payment-return snapshot: a reload of the checkout recognizes
1835
+ * the booking by it (`CheckoutState.earlierBooking`). */
1836
+ rentalId?: string | undefined;
1834
1837
  }
1835
1838
  /** ⚠ USER-initiated only: reserves for real. `quote` must be the live instance with every selection
1836
1839
  * applied. Logs the SDK's cause and rethrows (nothing is necessarily booked when it throws). Writes
@@ -1964,6 +1967,9 @@ interface BookingSnapshot {
1964
1967
  bookingNumber: string;
1965
1968
  /** A booking or an option. */
1966
1969
  kind: BookingKind;
1970
+ /** The booked rental's PMS id: a reload of the checkout recognizes its booking by it. Absent in a
1971
+ * snapshot written before 1.5.0. */
1972
+ rentalId?: string;
1967
1973
  /** The booked rental's name. */
1968
1974
  rentalName?: string;
1969
1975
  /** The rental's location line. */
@@ -2093,7 +2099,7 @@ type CheckoutPhase = {
2093
2099
  kind: "booked";
2094
2100
  };
2095
2101
  /** One reason the order button is off, in the order the UI should mention them. */
2096
- type CheckoutBlocker = "no-quote" | "price-pending" | "busy" | "issue" | "child-ages" | "insurance-documents" | "consents";
2102
+ type CheckoutBlocker = "earlier-booking" | "no-quote" | "price-pending" | "busy" | "issue" | "child-ages" | "insurance-documents" | "consents";
2097
2103
  /** One extra as the UI renders it. */
2098
2104
  interface CheckoutService extends QuoteAddonService {
2099
2105
  /** Units chosen (0 = not booked). */
@@ -2187,6 +2193,11 @@ interface CheckoutState {
2187
2193
  submit: "idle" | "submitting" | "failed";
2188
2194
  /** The confirmation after `book()`, or null. */
2189
2195
  confirmation: BookingConfirmation | null;
2196
+ /** The booking this tab already made for this rental and these dates, after a reload of the booked
2197
+ * checkout or a way back to it (the return page's snapshot, read on `start()`; only the tab's last
2198
+ * booking is kept). While it is set the order is refused (`earlier-booking`); other dates are not
2199
+ * it. The snapshot keeps no payment entries: the confirmation e-mail has them. Null otherwise. */
2200
+ earlierBooking: BookingSnapshot | null;
2190
2201
  /** The payment buttons' state: the option redirecting now, and a failed start. */
2191
2202
  payment: {
2192
2203
  busyOption: SubmittableBookingPaymentOption | null;
@@ -2231,7 +2242,8 @@ interface CheckoutActions {
2231
2242
  }
2232
2243
  /** The checkout store: its state, its actions and its lifecycle. */
2233
2244
  interface CheckoutFlow extends Store<CheckoutState>, CheckoutActions {
2234
- /** Park the context and price the handed-over stay. Idempotent. */
2245
+ /** Park the context, recognize a stay this tab already booked (`earlierBooking`) and price the
2246
+ * handed-over stay. Idempotent. */
2235
2247
  start(): void;
2236
2248
  /** Cancel pending work and drop every in-flight result. */
2237
2249
  dispose(): void;
package/dist/index.mjs CHANGED
@@ -7043,9 +7043,11 @@ function snapshotOf(record) {
7043
7043
  const end = record["end"];
7044
7044
  const guests = guestsOf(record["guests"]);
7045
7045
  const total = text(record["total"]);
7046
+ const rentalId = text(record["rentalId"]);
7046
7047
  return {
7047
7048
  bookingNumber,
7048
7049
  kind: record["kind"] === "option" ? "option" : "booking",
7050
+ ...rentalId ? { rentalId } : {},
7049
7051
  ...rentalFields(record),
7050
7052
  ...isIsoDay(start) ? { start } : {},
7051
7053
  ...isIsoDay(end) ? { end } : {},
@@ -7066,12 +7068,13 @@ function classifyReturn(params, storage) {
7066
7068
  paymentCancelled
7067
7069
  } : { state: "none" };
7068
7070
  }
7069
- function writeSnapshot({ bookingNumber, kind, stay, total }, storage) {
7071
+ function writeSnapshot({ bookingNumber, kind, rentalId, stay, total }, storage) {
7070
7072
  const context = readJson(CHECKOUT_CONTEXT_KEY, storage);
7071
7073
  const { adults, children, babies, pets } = stay.party;
7072
7074
  const snapshot = {
7073
7075
  bookingNumber,
7074
7076
  kind,
7077
+ ...rentalId ? { rentalId } : {},
7075
7078
  ...context ? rentalFields(context) : {},
7076
7079
  start: stay.start,
7077
7080
  end: stay.end,
@@ -7095,6 +7098,8 @@ const readPaymentReturn = (search, storage) => classifyReturn(new URLSearchParam
7095
7098
  const writeBookingSnapshot = (booking, storage) => {
7096
7099
  writeSnapshot(booking, resolveStorage(storage));
7097
7100
  };
7101
+ /** See {@link ReadBookingSnapshot}. */
7102
+ const readBookingSnapshot = (storage) => snapshotOf(readJson(BOOKING_SNAPSHOT_KEY, resolveStorage(storage)));
7098
7103
  //#endregion
7099
7104
  //#region src/checkout/booking.ts
7100
7105
  async function book(sdk, quote, contact, options) {
@@ -7111,6 +7116,7 @@ async function book(sdk, quote, contact, options) {
7111
7116
  writeBookingSnapshot({
7112
7117
  bookingNumber: out.bookingNumber,
7113
7118
  kind,
7119
+ rentalId: options.rentalId,
7114
7120
  stay: options.stay,
7115
7121
  total: quote.bookingCardInformation.total
7116
7122
  });
@@ -7521,13 +7527,20 @@ function insuranceOf(raw) {
7521
7527
  documentsPending: insuranceDocsPending(chosen ?? void 0, raw.insuranceDocs)
7522
7528
  };
7523
7529
  }
7524
- function derive(raw, holds, kind, how, availability) {
7530
+ /** The rental's last booking in this tab is this stay's when its dates are; the party does not count
7531
+ * (the rental is taken for those nights whoever comes). */
7532
+ function earlierBookingOf(earlier, stay) {
7533
+ return earlier && stay && earlier.start === stay.start && earlier.end === stay.end ? earlier : null;
7534
+ }
7535
+ function derive(raw, holds, kind, how, availability, earlier) {
7525
7536
  const busy = holds > 0;
7526
7537
  const stayKey = raw.stay ? stayKeyOf(raw.stay) : null;
7527
7538
  const quotedKey = raw.quoted ? stayKeyOf(raw.quoted) : null;
7528
7539
  const pricePending = stayKey !== null && stayKey !== quotedKey && raw.issue?.key !== stayKey;
7529
7540
  const insurance = insuranceOf(raw);
7541
+ const earlierBooking = earlierBookingOf(earlier, raw.stay);
7530
7542
  const blockers = [];
7543
+ if (earlierBooking) blockers.push("earlier-booking");
7531
7544
  if (!raw.view || raw.phase.kind !== "ready") blockers.push("no-quote");
7532
7545
  if (pricePending) blockers.push("price-pending");
7533
7546
  if (busy) blockers.push("busy");
@@ -7556,6 +7569,7 @@ function derive(raw, holds, kind, how, availability) {
7556
7569
  blockers,
7557
7570
  submit: raw.submit,
7558
7571
  confirmation: raw.confirmation,
7572
+ earlierBooking,
7559
7573
  payment: raw.payment
7560
7574
  };
7561
7575
  }
@@ -7581,11 +7595,14 @@ function checkoutFlow(source, options) {
7581
7595
  let sdk = null;
7582
7596
  let availability = null;
7583
7597
  let capsRequested = false;
7598
+ /** This rental's booking from the tab's return snapshot, read on `start()`. */
7599
+ let earlier = null;
7584
7600
  const faultsReported = /* @__PURE__ */ new Set();
7585
7601
  const lineOrder = /* @__PURE__ */ new Map();
7586
- const store = createStore(derive(raw, holds, handover.kind, how, availability));
7602
+ const derived = () => derive(raw, holds, handover.kind, how, availability, earlier);
7603
+ const store = createStore(derived());
7587
7604
  const publish = () => {
7588
- if (!disposed) store.setState(derive(raw, holds, handover.kind, how, availability));
7605
+ if (!disposed) store.setState(derived());
7589
7606
  };
7590
7607
  const patch = (next) => {
7591
7608
  raw = {
@@ -7748,7 +7765,8 @@ function checkoutFlow(source, options) {
7748
7765
  returnPath: options.returnPath,
7749
7766
  kind: handover.kind,
7750
7767
  bankHolderFallback: options.bankHolderFallback,
7751
- stay
7768
+ stay,
7769
+ rentalId
7752
7770
  });
7753
7771
  patch({
7754
7772
  confirmation,
@@ -7972,6 +7990,9 @@ function checkoutFlow(source, options) {
7972
7990
  if (started || disposed) return;
7973
7991
  started = true;
7974
7992
  if (options.context) saveCheckoutContext(options.context);
7993
+ const snapshot = readBookingSnapshot();
7994
+ earlier = snapshot?.rentalId === rentalId ? snapshot : null;
7995
+ if (earlier) publish();
7975
7996
  schedule(0);
7976
7997
  },
7977
7998
  dispose() {
@@ -4,6 +4,7 @@
4
4
 
5
5
  Release notes of `@v-office/website-headless`, newest first. Each version has its own directory.
6
6
 
7
+ - `versions/1.5.0/CHANGELOG.md`: release notes for 1.5.0
7
8
  - `versions/1.4.1/CHANGELOG.md`: release notes for 1.4.1
8
9
  - `versions/1.4.0/CHANGELOG.md`: release notes for 1.4.0
9
10
  - `versions/1.3.0/CHANGELOG.md`: release notes for 1.3.0
@@ -4,6 +4,7 @@
4
4
 
5
5
  Migration guides of `@v-office/website-headless`, newest first. Apply them in order when skipping versions.
6
6
 
7
+ - `versions/1.5.0/MIGRATION.md`: Migration: 1.4.1 to 1.5.0
7
8
  - `versions/1.4.1/MIGRATION.md`: Migration: 1.4.0 to 1.4.1
8
9
  - `versions/1.4.0/MIGRATION.md`: Migration: 1.3.0 to 1.4.0
9
10
  - `versions/1.3.0/MIGRATION.md`: Migration: 1.2.0 to 1.3.0
@@ -0,0 +1,32 @@
1
+ # Changelog: 1.5.0
2
+
3
+ The checkout recognizes a stay the guest already booked in this browser tab. After `book()` the
4
+ checkout page's URL still describes the stay, so a reload, or the browser's back button (from the
5
+ payment provider's page, say), started a new checkout for it: the guest saw the PMS refuse the
6
+ dates they had just booked, or, while the PMS's availability had not caught up, was offered the
7
+ order a second time. The checkout now names that booking and does not order the stay again.
8
+
9
+ ## Added
10
+
11
+ - `CheckoutState.earlierBooking`: the booking this tab already made for this rental and these
12
+ dates, as a `BookingSnapshot` (`bookingNumber`, `kind`, `start`, `end`, `guests`, `total`), or
13
+ null. The flow reads it on `start()` from the payment-return snapshot `book()` writes to session
14
+ storage, so only the tab's last booking counts, and only while the tab lives.
15
+ - `BookingSnapshot.rentalId` and `SubmitBookingOptions.rentalId`: the rental a snapshot belongs to.
16
+ The checkout flow passes it. A site that calls `submitBooking` itself passes its rental id to get
17
+ the same recognition.
18
+
19
+ ## Fixed
20
+
21
+ - A checkout started for a stay this tab already booked refuses the order: `blockers` starts with
22
+ `"earlier-booking"` and `book()` does nothing, whatever the quote says.
23
+
24
+ ## Unchanged
25
+
26
+ - The stay is still priced. A checkout UI that does not render `earlierBooking` shows what it
27
+ showed before (usually the PMS refusing the dates), and the blocker keeps its order button off.
28
+ - Other dates are not that booking: after `setDates` to another range `earlierBooking` is null and
29
+ the checkout orders as usual; back on the booked dates it is set again. The party does not count,
30
+ because the rental is taken for those nights.
31
+ - A snapshot written before 1.5.0 has no rental id and is no earlier booking.
32
+ - `readPaymentReturn` and the return page read the snapshot as before.
@@ -0,0 +1,37 @@
1
+ # Migration: 1.4.1 to 1.5.0
2
+
3
+ No changes required. A site with the platform's `Booking` shows the earlier booking through
4
+ `@v-office/website-components` 1.2.0. A site with its own checkout keeps working: for a stay the
5
+ tab already booked its order button stays off (`blockers` holds `"earlier-booking"`), and it shows
6
+ what it showed before.
7
+
8
+ ## Showing the earlier booking in a site's own checkout
9
+
10
+ A site that renders its own checkout (over `useCheckoutFlow` or `createCheckoutFlow`) can show the
11
+ booking instead. Render `state.earlierBooking` in place of the order: the guest booked this stay and
12
+ needs its booking number, not a refusal of the dates or a second order form.
13
+
14
+ ```tsx
15
+ const { state } = useCheckoutFlow({ … });
16
+ if (state.earlierBooking) {
17
+ const { bookingNumber, kind } = state.earlierBooking;
18
+ return (
19
+ <section role="status">
20
+ <h2>{kind === "option" ? "Die Reservierung ist eingegangen." : "Die Buchung ist eingegangen."}</h2>
21
+ <p>Buchungsnummer: {bookingNumber}. Die Bestätigung mit allen Details folgt per E-Mail.</p>
22
+ </section>
23
+ );
24
+ }
25
+ ```
26
+
27
+ - Use the site's own confirmation wording; `kind` tells a booking from an option.
28
+ - The snapshot keeps no payment entries (no bank details, no payment buttons): point the guest to
29
+ the confirmation e-mail.
30
+ - A checkout with a date editor may keep it beside the notice: other dates are not that booking.
31
+ - If a site's checkout states why its order button is off, `"earlier-booking"` is the first reason.
32
+
33
+ ## 1. Upgrade
34
+
35
+ ```sh
36
+ pnpm add --save-exact @v-office/website-headless@1.5.0
37
+ ```
@@ -0,0 +1,3 @@
1
+ {
2
+ "componentsAffectedByHeadlessChange": ["booking"]
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v-office/website-headless",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "Framework-free behaviour layer for v-office websites, never UI: stores, pure rules and SDK runners for the search bar, results, map, showcase, quote, checkout and contact form, the rental and card models, and progressive-enhancement engines for server-rendered markup (./enhance)",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "26.5.1",
29
- "@v-office/website-sdk": "3.0.0",
29
+ "@v-office/website-sdk": "3.2.0",
30
30
  "effect": "4.0.0-rc.115",
31
31
  "jsdom": "^30.0.1",
32
32
  "oxfmt": "0.67.0",