@ticketboothapp/booking 1.2.61 → 1.2.63
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/CHANGE_BOOKING_BE_HANDOFF.md +86 -0
- package/package.json +1 -1
- package/src/components/booking/AddOnsSection.tsx +6 -3
- package/src/components/booking/AdminChangeBookingFlow.tsx +4915 -0
- package/src/components/booking/BookingFlow.tsx +23 -5343
- package/src/components/booking/Calendar.tsx +79 -35
- package/src/components/booking/CancellationPolicySelector.tsx +9 -2
- package/src/components/booking/ChangeBookingDialog.tsx +20 -10
- package/src/components/booking/ChangeBookingFlow.tsx +4915 -0
- package/src/components/booking/ChangeBookingPricingDriftPanel.tsx +268 -0
- package/src/components/booking/CheckoutForm.tsx +29 -19
- package/src/components/booking/CurrencySwitcher.tsx +1 -1
- package/src/components/booking/MealDrinkAddOnSelector.tsx +4 -2
- package/src/components/booking/NewBookingFlow.tsx +3256 -0
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +8 -6
- package/src/components/booking/PromoCodeInput.tsx +4 -1
- package/src/components/booking/ReturnTimeSelector.tsx +12 -5
- package/src/components/booking/TicketSelector.tsx +6 -1
- package/src/components/booking/booking-flow-types.ts +141 -0
- package/src/index.ts +10 -1
- package/src/lib/booking/change-booking-pricing-drift.ts +331 -0
- package/src/lib/booking/change-booking-server-preview.ts +139 -0
- package/src/lib/booking/change-flow-pricing.ts +162 -27
- package/src/lib/booking-api.ts +72 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Backend handoff: customer change-booking quote (full UI preview)
|
|
2
|
+
|
|
3
|
+
This document describes what the **booking API** should return so the website’s customer **self-serve change flow** can show **server-authored** dollar amounts in checkout lines and optional picker overrides.
|
|
4
|
+
|
|
5
|
+
Existing endpoint (names may vary in your stack): **`POST …/change/quote`** — response type `ChangeBookingQuoteResponse` in `@ticketboothapp/booking` / shared client types.
|
|
6
|
+
|
|
7
|
+
## Already required for “totals confirmed” UI
|
|
8
|
+
|
|
9
|
+
The frontend treats pricing as confirmed when the quote includes receipt-style totals (via `newReceipt` / `proposed` / `newTotalCents`, etc.) and `amountDueCents` or `priceDiff` for the amount owed. That unlocks subtotal/tax/total rows and replaces the pre-quote placeholder block.
|
|
10
|
+
|
|
11
|
+
## Ticket pricing on change (authoritative product rules)
|
|
12
|
+
|
|
13
|
+
These rules apply when changing an existing booking for the **same parent catalog product** (self-serve). Use **booking snapshot + original receipt** to know what was sold; use **live catalog** for the **currently selected** departure date and product option.
|
|
14
|
+
|
|
15
|
+
### Inputs (per ticket category `y`)
|
|
16
|
+
|
|
17
|
+
| Symbol | Meaning |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `x_y` | Original booked count for category `y` on the **original** booking (protected headcount cap). |
|
|
20
|
+
| `lockedUnit_y` | Unit price those tickets were sold at (from receipt / booking line items), major currency units. |
|
|
21
|
+
| `liveUnit_y` | Live catalog unit for category `y` at the **currently selected** departure date + product option (+ availability slot as modeled). |
|
|
22
|
+
| `q_y` | Current quantity selected in the change UI for category `y`. |
|
|
23
|
+
|
|
24
|
+
Define:
|
|
25
|
+
|
|
26
|
+
- `protected_y = min(q_y, x_y)` — seats that correspond to **original** tickets still present.
|
|
27
|
+
- `incremental_y = max(0, q_y - x_y)` — **new** seats added in this change session.
|
|
28
|
+
|
|
29
|
+
Define **unchanged itinerary** as: selected departure **date** equals the original booking’s date **and** selected **product option** equals the original booking’s product option (same IDs / same comparison rules you use elsewhere for “same PO”).
|
|
30
|
+
|
|
31
|
+
### Rule A — unchanged itinerary (same date **and** same product option)
|
|
32
|
+
|
|
33
|
+
For each category `y`:
|
|
34
|
+
|
|
35
|
+
- **`protected_y` seats:** unit price is **`lockedUnit_y` exactly** — no adjustment up or down based on today’s catalog. Catalog price changes **do not** move those seats.
|
|
36
|
+
- **`incremental_y` seats:** unit price is **`liveUnit_y`** (live catalog for the current selection).
|
|
37
|
+
|
|
38
|
+
Adding passengers or changing **return** does **not** re-price those protected seats; only net-new seats follow live catalog. **Return add-ons** keep the separate return-floor behavior already agreed (per-person floor from what was paid; incremental passengers pay live return pricing where applicable).
|
|
39
|
+
|
|
40
|
+
### Rule B — itinerary changed (date changed **or** product option changed)
|
|
41
|
+
|
|
42
|
+
For each category `y`:
|
|
43
|
+
|
|
44
|
+
- **`protected_y` seats:** unit price is **`max(lockedUnit_y, liveUnit_y)`** — they never go **below** what they paid (**floor**), but they **may go up** if the new date or option prices higher.
|
|
45
|
+
- **`incremental_y` seats:** unit price is **`liveUnit_y`**.
|
|
46
|
+
|
|
47
|
+
### Fees
|
|
48
|
+
|
|
49
|
+
Fee lines should follow the **same structural split** as tickets (protected vs incremental headcount). Align fee formulas with whatever you implement alongside these ticket rules.
|
|
50
|
+
|
|
51
|
+
### Backend / reconciliation
|
|
52
|
+
|
|
53
|
+
`POST …/change/quote` should compute ticket line totals with **Rule A vs Rule B** from **server-held booking state + selected date/PO + live catalog**, not from trusting the browser alone. **`clientProposedTotal`** remains a cross-check when the client sends it.
|
|
54
|
+
|
|
55
|
+
### Current website package (FYI)
|
|
56
|
+
|
|
57
|
+
The `@ticketboothapp/booking` change flow implements Rule **A** vs **B** for ticket and config-fee lines when self‑serve receipt pricing applies (same parent product): **exact** receipt units on protected seats/fees when calendar date **and** product option match the booking; otherwise **`max(locked, live)`** for protected headcount.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Recommended fields for server-owned detail
|
|
62
|
+
|
|
63
|
+
Pickers show **catalog** prices by default (same booking **currency** as the original receipt — the change UI does not switch currency). These optional fields **override** display when present on the quote:
|
|
64
|
+
|
|
65
|
+
1. **`ticketUnitPriceByCategory`** — `Record<string, number>`
|
|
66
|
+
- Keys: ticket categories (`ADULT`, `CHILD`, …), uppercase preferred.
|
|
67
|
+
- Values: **unit price in major units** (e.g. dollars), display currency.
|
|
68
|
+
- When present, these update ticket **picker** display units on self‑serve change; totals must still follow **Rule A / Rule B** above unless the quote fully replaces breakdown.
|
|
69
|
+
|
|
70
|
+
2. **Line items** — `newReceipt.lineItems` or `proposed.lineItems` (fallback `original`)
|
|
71
|
+
- Shape: `{ label?, amount?, type?, quantity? }`.
|
|
72
|
+
- Mapped to checkout **`PriceSummary`** rows when present; otherwise checkout falls back to FE-built lines after totals confirm.
|
|
73
|
+
|
|
74
|
+
### Additional optional maps
|
|
75
|
+
|
|
76
|
+
| Field | Purpose |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| **`returnOptionPriceByReturnAvailabilityId`** | `Record<returnAvailabilityId, number>` — **per-person** return add-on in major units; overrides catalog return card amounts for matching ids. The FE still **floors** per-person return at what was paid on the original receipt when applicable — values below that floor are raised client-side. |
|
|
79
|
+
| **`cancellationPolicyFeeByPolicyId`** | Optional passthrough on the quote type / `serverPreview` for future use or tooling. **Not used** to drive cancellation UI in change flow today — cancellation policy is **fixed** to the existing booking; there is no policy picker. |
|
|
80
|
+
|
|
81
|
+
## Notes for implementers
|
|
82
|
+
|
|
83
|
+
- **`clientProposedTotal`** (existing): FE still sends this as a hint for reconciliation; display should follow **quote response**, not the client-only cart.
|
|
84
|
+
- **Currency**: align optional maps and line `amount` values with `currency` / receipt currency; change booking stays in the booking’s sold currency end-to-end on FE.
|
|
85
|
+
- **Cancellation policy on change**: FE sends the booked policy on reserve/confirm paths; it is **not** chosen from product config during change — preserve the booking’s policy server-side unless product rules say otherwise.
|
|
86
|
+
- **`serverPreview.completeness`**: the package sets this to **`full`** whenever a preview is built (informational); UI does not gate on `totals_only` vs `full`.
|
package/package.json
CHANGED
|
@@ -15,6 +15,7 @@ interface AddOnsSectionProps {
|
|
|
15
15
|
locale: string;
|
|
16
16
|
onSelectionsChange: (selections: AddOnSelection[] | ((prev: AddOnSelection[]) => AddOnSelection[])) => void;
|
|
17
17
|
minimumTotalByAddOnId?: Map<string, number>;
|
|
18
|
+
suppressPrices?: boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export function AddOnsSection({
|
|
@@ -24,6 +25,7 @@ export function AddOnsSection({
|
|
|
24
25
|
locale,
|
|
25
26
|
onSelectionsChange,
|
|
26
27
|
minimumTotalByAddOnId,
|
|
28
|
+
suppressPrices = false,
|
|
27
29
|
}: AddOnsSectionProps) {
|
|
28
30
|
return (
|
|
29
31
|
<div className="border-t border-stone-200 pt-6 space-y-4">
|
|
@@ -60,7 +62,7 @@ export function AddOnsSection({
|
|
|
60
62
|
{isSelected ? `Yes, add ${addOn.name}` : `No ${addOn.name}`}
|
|
61
63
|
</span>
|
|
62
64
|
<span className="text-sm font-semibold text-stone-700">
|
|
63
|
-
|
|
65
|
+
{suppressPrices ? '—' : `+${formatCurrencyAmount(addOn.price ?? 0, currency, locale as 'en' | 'fr')}`}
|
|
64
66
|
</span>
|
|
65
67
|
</button>
|
|
66
68
|
</div>
|
|
@@ -77,6 +79,7 @@ export function AddOnsSection({
|
|
|
77
79
|
currency={currency}
|
|
78
80
|
locale={locale as 'en' | 'fr'}
|
|
79
81
|
minimumTotal={minTotalForAddOn}
|
|
82
|
+
suppressPrices={suppressPrices}
|
|
80
83
|
/>
|
|
81
84
|
);
|
|
82
85
|
}
|
|
@@ -128,7 +131,7 @@ export function AddOnsSection({
|
|
|
128
131
|
+
|
|
129
132
|
</button>
|
|
130
133
|
<span className="text-sm font-medium text-stone-700 w-16 text-right">
|
|
131
|
-
{formatCurrencyAmount(price, currency, locale as 'en' | 'fr')} ea
|
|
134
|
+
{suppressPrices ? '—' : `${formatCurrencyAmount(price, currency, locale as 'en' | 'fr')} ea`}
|
|
132
135
|
</span>
|
|
133
136
|
</div>
|
|
134
137
|
</div>
|
|
@@ -168,7 +171,7 @@ export function AddOnsSection({
|
|
|
168
171
|
>
|
|
169
172
|
<span className="text-sm font-medium text-stone-800">{variant.label}</span>
|
|
170
173
|
<span className="text-sm font-semibold text-stone-700">
|
|
171
|
-
|
|
174
|
+
{suppressPrices ? '—' : `+${formatCurrencyAmount(price, currency, locale as 'en' | 'fr')}`}
|
|
172
175
|
</span>
|
|
173
176
|
</button>
|
|
174
177
|
);
|