@ticketboothapp/booking 1.2.56 → 1.2.59
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/package.json +1 -1
- package/src/components/booking/BookingFlow.tsx +1289 -195
- package/src/components/booking/Calendar.module.css +6 -1
- package/src/components/booking/ChangeBookingDialog.tsx +41 -178
- package/src/components/booking/CheckoutForm.tsx +29 -4
- package/src/components/booking/DefaultTermsContent.tsx +178 -0
- package/src/components/booking/PriceBreakdown.tsx +92 -27
- package/src/components/booking/PriceSummary.tsx +77 -4
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +83 -27
- package/src/components/booking/TermsAcceptance.tsx +2 -1
- package/src/components/booking/booking-flow-ui.ts +42 -0
- package/src/components/booking/booking-flow.css +32 -0
- package/src/components/booking/change-booking-compare.module.css +97 -0
- package/src/components/booking/change-booking-compare.tsx +228 -0
- package/src/components/booking/provider-dashboard-change-booking.ts +53 -0
- package/src/index.ts +19 -0
- package/src/lib/booking/pricing.ts +27 -19
- package/src/lib/booking-api.ts +7 -0
- package/src/runtime/types.ts +2 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact compare cards: Figtree only (no Poppins). Small type + tight spacing for dialog embeds.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
.column {
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
border-radius: 0.5rem;
|
|
8
|
+
padding: 0.625rem 0.75rem;
|
|
9
|
+
background: var(--light-orange-background);
|
|
10
|
+
/* Cascade to itinerary `<span>`s that only use Tailwind weight/color classes */
|
|
11
|
+
font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.kicker {
|
|
15
|
+
margin: 0 0 0.375rem 0;
|
|
16
|
+
font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
|
|
17
|
+
font-size: 0.625rem;
|
|
18
|
+
font-weight: 600;
|
|
19
|
+
letter-spacing: 0.07em;
|
|
20
|
+
text-transform: uppercase;
|
|
21
|
+
line-height: 1.2;
|
|
22
|
+
color: var(--grey-text, var(--booking-text-muted, #78716c));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.row {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-wrap: wrap;
|
|
28
|
+
align-items: baseline;
|
|
29
|
+
gap: 0;
|
|
30
|
+
margin: 0 0 0.2rem 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.label {
|
|
34
|
+
font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
|
|
35
|
+
font-size: 0.8125rem;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
line-height: 1.3;
|
|
38
|
+
color: var(--primary-text, var(--booking-text, #1c1917));
|
|
39
|
+
margin-right: 0.3em;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.value {
|
|
43
|
+
font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
|
|
44
|
+
font-size: 0.8125rem;
|
|
45
|
+
font-weight: 400;
|
|
46
|
+
line-height: 1.3;
|
|
47
|
+
color: var(--primary-text, var(--booking-text, #1c1917));
|
|
48
|
+
flex: 1;
|
|
49
|
+
min-width: 0;
|
|
50
|
+
overflow-wrap: break-word;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.itineraryBlock {
|
|
54
|
+
margin: 0.15rem 0 0.2rem 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.itineraryList {
|
|
58
|
+
margin: 0.2rem 0 0 0;
|
|
59
|
+
padding: 0;
|
|
60
|
+
list-style: none;
|
|
61
|
+
font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
|
|
62
|
+
font-size: 0.8125rem;
|
|
63
|
+
font-weight: 400;
|
|
64
|
+
line-height: 1.3;
|
|
65
|
+
color: var(--primary-text, var(--booking-text, #1c1917));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.itineraryList li + li {
|
|
69
|
+
margin-top: 0.125rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.totalRow {
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-wrap: wrap;
|
|
75
|
+
align-items: baseline;
|
|
76
|
+
justify-content: space-between;
|
|
77
|
+
gap: 0.5rem 0.75rem;
|
|
78
|
+
margin-top: 0.5rem;
|
|
79
|
+
padding-top: 0.5rem;
|
|
80
|
+
border-top: 1px solid rgba(231, 229, 228, 0.8);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.totalLabel {
|
|
84
|
+
font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
|
|
85
|
+
font-size: 0.8125rem;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
color: var(--primary-text, var(--booking-text, #1c1917));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.totalAmount {
|
|
91
|
+
font-family: 'Figtree', var(--booking-font-sans, ui-sans-serif), sans-serif;
|
|
92
|
+
font-size: 0.875rem;
|
|
93
|
+
font-weight: 600;
|
|
94
|
+
font-variant-numeric: tabular-nums;
|
|
95
|
+
line-height: 1.2;
|
|
96
|
+
color: var(--primary-text, var(--booking-text, #1c1917));
|
|
97
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import styles from './change-booking-compare.module.css';
|
|
4
|
+
|
|
5
|
+
export type ItineraryStepLine = { time: string | null; label: string };
|
|
6
|
+
|
|
7
|
+
export type ChangeHighlightVariant = 'current' | 'new';
|
|
8
|
+
|
|
9
|
+
/** Same date formatting as manage-booking / ChangeBookingDialog (Mountain Time label). */
|
|
10
|
+
export function formatBookingDateForChangeCompare(dateTime: string | null | undefined): string {
|
|
11
|
+
if (!dateTime) return '—';
|
|
12
|
+
try {
|
|
13
|
+
const date = new Date(dateTime);
|
|
14
|
+
return date.toLocaleDateString('en-US', {
|
|
15
|
+
weekday: 'short',
|
|
16
|
+
year: 'numeric',
|
|
17
|
+
month: 'short',
|
|
18
|
+
day: 'numeric',
|
|
19
|
+
timeZone: 'America/Denver',
|
|
20
|
+
});
|
|
21
|
+
} catch {
|
|
22
|
+
return dateTime;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatBookingItemsForCompare(
|
|
27
|
+
items: Array<{ category: string; count: number }> | null | undefined,
|
|
28
|
+
): string {
|
|
29
|
+
if (!items?.length) return '—';
|
|
30
|
+
const labels: Record<string, string> = {
|
|
31
|
+
ADULT: 'adult',
|
|
32
|
+
CHILD: 'child',
|
|
33
|
+
INFANT: 'infant',
|
|
34
|
+
SENIOR: 'senior',
|
|
35
|
+
STUDENT: 'student',
|
|
36
|
+
};
|
|
37
|
+
const parts = items
|
|
38
|
+
.filter((item) => item.count > 0)
|
|
39
|
+
.map((item) => {
|
|
40
|
+
const label = labels[item.category] || item.category.toLowerCase();
|
|
41
|
+
return `${item.count} ${label}${item.count !== 1 ? 's' : ''}`;
|
|
42
|
+
});
|
|
43
|
+
return parts.length > 0 ? parts.join(', ') : '—';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function computeItineraryStepChanged(
|
|
47
|
+
stepsA: ItineraryStepLine[] | null,
|
|
48
|
+
stepsB: ItineraryStepLine[] | null,
|
|
49
|
+
): boolean[] | undefined {
|
|
50
|
+
if (!stepsA) return undefined;
|
|
51
|
+
return stepsA.map((a, i) => {
|
|
52
|
+
const b = stepsB?.[i];
|
|
53
|
+
return !b || a.time !== b.time || a.label !== b.label;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** True when the two rendered itinerary lines are the same. */
|
|
58
|
+
export function itinerariesEqualForChangeCompare(
|
|
59
|
+
a: ItineraryStepLine[] | null,
|
|
60
|
+
b: ItineraryStepLine[] | null,
|
|
61
|
+
): boolean {
|
|
62
|
+
const lenA = a?.length ?? 0;
|
|
63
|
+
const lenB = b?.length ?? 0;
|
|
64
|
+
if (lenA === 0 && lenB === 0) return true;
|
|
65
|
+
if (lenA !== lenB || !a || !b) return false;
|
|
66
|
+
return a.every((step, i) => {
|
|
67
|
+
const o = b[i];
|
|
68
|
+
return step.time === o.time && step.label === o.label;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* When the flow reports “changes” but the compare cards show the same date, tickets line,
|
|
74
|
+
* itinerary, and total, hide the second column and skip strike-through/bold.
|
|
75
|
+
*/
|
|
76
|
+
export function changeBookingCompareColumnsVisuallyMatch(params: {
|
|
77
|
+
dateCurrent: string;
|
|
78
|
+
dateNew: string;
|
|
79
|
+
ticketsCurrent: string;
|
|
80
|
+
ticketsNew: string;
|
|
81
|
+
itineraryCurrent: ItineraryStepLine[] | null;
|
|
82
|
+
itineraryNew: ItineraryStepLine[] | null;
|
|
83
|
+
totalCurrent: string;
|
|
84
|
+
totalNew: string;
|
|
85
|
+
}): boolean {
|
|
86
|
+
return (
|
|
87
|
+
params.dateCurrent === params.dateNew &&
|
|
88
|
+
params.ticketsCurrent === params.ticketsNew &&
|
|
89
|
+
params.totalCurrent === params.totalNew &&
|
|
90
|
+
itinerariesEqualForChangeCompare(params.itineraryCurrent, params.itineraryNew)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function BookingChangeSummaryColumn({
|
|
95
|
+
kicker,
|
|
96
|
+
tourName,
|
|
97
|
+
dateStr,
|
|
98
|
+
ticketsStr,
|
|
99
|
+
itinerarySteps,
|
|
100
|
+
highlightVariant,
|
|
101
|
+
totalFormatted,
|
|
102
|
+
dateChanged = false,
|
|
103
|
+
ticketsChanged = false,
|
|
104
|
+
itineraryStepChanged,
|
|
105
|
+
}: {
|
|
106
|
+
kicker: string;
|
|
107
|
+
tourName: string;
|
|
108
|
+
dateStr: string;
|
|
109
|
+
ticketsStr: string;
|
|
110
|
+
itinerarySteps: ItineraryStepLine[] | null;
|
|
111
|
+
highlightVariant: ChangeHighlightVariant;
|
|
112
|
+
/** e.g. "C$123.45" */
|
|
113
|
+
totalFormatted: string;
|
|
114
|
+
dateChanged?: boolean;
|
|
115
|
+
ticketsChanged?: boolean;
|
|
116
|
+
itineraryStepChanged?: boolean[];
|
|
117
|
+
}) {
|
|
118
|
+
const dateClass =
|
|
119
|
+
highlightVariant === 'current' && dateChanged
|
|
120
|
+
? 'line-through text-stone-500'
|
|
121
|
+
: highlightVariant === 'new' && dateChanged
|
|
122
|
+
? 'font-semibold text-stone-900'
|
|
123
|
+
: '';
|
|
124
|
+
const ticketsClass =
|
|
125
|
+
highlightVariant === 'current' && ticketsChanged
|
|
126
|
+
? 'line-through text-stone-500'
|
|
127
|
+
: highlightVariant === 'new' && ticketsChanged
|
|
128
|
+
? 'font-semibold text-stone-900'
|
|
129
|
+
: '';
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className={`booking-change-summary-column ${styles.column}`}>
|
|
133
|
+
<p className={styles.kicker}>{kicker}</p>
|
|
134
|
+
|
|
135
|
+
{/* Same pattern as manage-booking `BookingDetails`: label + value on one baseline row */}
|
|
136
|
+
<div className={styles.row}>
|
|
137
|
+
<span className={styles.label}>Tour:</span>
|
|
138
|
+
<span className={styles.value}>{tourName.trim()}</span>
|
|
139
|
+
</div>
|
|
140
|
+
<div className={styles.row}>
|
|
141
|
+
<span className={styles.label}>Date:</span>
|
|
142
|
+
<span className={styles.value}>
|
|
143
|
+
{dateClass ? <span className={dateClass}>{dateStr}</span> : dateStr}
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
<div className={styles.row}>
|
|
147
|
+
<span className={styles.label}>Tickets:</span>
|
|
148
|
+
<span className={styles.value}>
|
|
149
|
+
{ticketsClass ? <span className={ticketsClass}>{ticketsStr}</span> : ticketsStr}
|
|
150
|
+
</span>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div className={styles.itineraryBlock}>
|
|
154
|
+
<div className={styles.row}>
|
|
155
|
+
<span className={styles.label}>Itinerary:</span>
|
|
156
|
+
</div>
|
|
157
|
+
{itinerarySteps && itinerarySteps.length > 0 ? (
|
|
158
|
+
<ul className={styles.itineraryList}>
|
|
159
|
+
{itinerarySteps.map((step, i) => (
|
|
160
|
+
<li
|
|
161
|
+
key={i}
|
|
162
|
+
className={
|
|
163
|
+
itineraryStepChanged?.[i]
|
|
164
|
+
? highlightVariant === 'current'
|
|
165
|
+
? 'line-through text-stone-500'
|
|
166
|
+
: 'font-semibold text-stone-900'
|
|
167
|
+
: undefined
|
|
168
|
+
}
|
|
169
|
+
>
|
|
170
|
+
{step.time ? (
|
|
171
|
+
<>
|
|
172
|
+
<span
|
|
173
|
+
className={
|
|
174
|
+
itineraryStepChanged?.[i]
|
|
175
|
+
? highlightVariant === 'current'
|
|
176
|
+
? 'font-medium text-stone-500'
|
|
177
|
+
: 'font-semibold text-stone-900'
|
|
178
|
+
: 'font-medium text-stone-800'
|
|
179
|
+
}
|
|
180
|
+
>
|
|
181
|
+
{step.time}
|
|
182
|
+
</span>
|
|
183
|
+
<span
|
|
184
|
+
className={
|
|
185
|
+
itineraryStepChanged?.[i] && highlightVariant === 'new'
|
|
186
|
+
? 'text-stone-400'
|
|
187
|
+
: 'text-stone-500'
|
|
188
|
+
}
|
|
189
|
+
>
|
|
190
|
+
{' '}
|
|
191
|
+
·{' '}
|
|
192
|
+
</span>
|
|
193
|
+
<span
|
|
194
|
+
className={
|
|
195
|
+
itineraryStepChanged?.[i] && highlightVariant === 'new'
|
|
196
|
+
? 'font-semibold text-stone-900'
|
|
197
|
+
: undefined
|
|
198
|
+
}
|
|
199
|
+
>
|
|
200
|
+
{step.label}
|
|
201
|
+
</span>
|
|
202
|
+
</>
|
|
203
|
+
) : (
|
|
204
|
+
<span
|
|
205
|
+
className={
|
|
206
|
+
itineraryStepChanged?.[i] && highlightVariant === 'new'
|
|
207
|
+
? 'font-semibold text-stone-900'
|
|
208
|
+
: undefined
|
|
209
|
+
}
|
|
210
|
+
>
|
|
211
|
+
{step.label}
|
|
212
|
+
</span>
|
|
213
|
+
)}
|
|
214
|
+
</li>
|
|
215
|
+
))}
|
|
216
|
+
</ul>
|
|
217
|
+
) : (
|
|
218
|
+
<p className={`${styles.value} mt-1 text-stone-500`}>—</p>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div className={styles.totalRow}>
|
|
223
|
+
<span className={styles.totalLabel}>Total:</span>
|
|
224
|
+
<span className={styles.totalAmount}>{totalFormatted}</span>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Currency } from './CurrencySwitcher';
|
|
2
|
+
|
|
3
|
+
/** Payload passed to `onChangeBooking` when applying a dashboard-managed booking change. */
|
|
4
|
+
export type ProviderDashboardChangeBookingPayload = {
|
|
5
|
+
productId: string;
|
|
6
|
+
dateTime: string;
|
|
7
|
+
bookingItems: Array<{ category: string; count: number }>;
|
|
8
|
+
returnAvailabilityId?: string | null;
|
|
9
|
+
pickupLocationId?: string | null;
|
|
10
|
+
travelerHotel?: string | null;
|
|
11
|
+
startTime?: string | null;
|
|
12
|
+
passengerCount?: number | null;
|
|
13
|
+
childSafetySeatsCount?: number | null;
|
|
14
|
+
foodRestrictions?: string | null;
|
|
15
|
+
addOnSelections?: Array<{ addOnId: string; variantId?: string; quantity?: number }> | null;
|
|
16
|
+
cancellationPolicyId?: string | null;
|
|
17
|
+
promoCode?: string | null;
|
|
18
|
+
newTotalAmount?: number;
|
|
19
|
+
additionalHoursCount?: number | null;
|
|
20
|
+
pricingAdjustment?: {
|
|
21
|
+
mode: 'AUTO' | 'MANUAL_LINES';
|
|
22
|
+
lineOverrides?: Array<{ lineKey: string; amount: number; reason?: string }>;
|
|
23
|
+
additionalAdjustments?: Array<{ label: string; amount: number }>;
|
|
24
|
+
} | null;
|
|
25
|
+
capacitySeatCredit?: {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
previousPassengerCount?: number | null;
|
|
28
|
+
previousAvailabilityId?: string | null;
|
|
29
|
+
previousReturnAvailabilityId?: string | null;
|
|
30
|
+
} | null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Seeds the flow when opening provider change-booking (matches TicketBooth `BookingWidget` `initialBooking`). */
|
|
34
|
+
export type ProviderDashboardInitialBooking = {
|
|
35
|
+
bookingReference: string;
|
|
36
|
+
productId: string;
|
|
37
|
+
availabilityId?: string;
|
|
38
|
+
dateTime: string;
|
|
39
|
+
originalTotalAmount?: number;
|
|
40
|
+
originalCurrency?: Currency | string;
|
|
41
|
+
originalPromoAmount?: number;
|
|
42
|
+
originalPromoLabel?: string | null;
|
|
43
|
+
bookingItems: Array<{ category: string; count: number }>;
|
|
44
|
+
returnAvailabilityId?: string | null;
|
|
45
|
+
pickupLocationId?: string | null;
|
|
46
|
+
travelerHotel?: string | null;
|
|
47
|
+
startTime?: string | null;
|
|
48
|
+
privateShuttleDetails?: { passengerCount?: number };
|
|
49
|
+
cancellationPolicyId?: string | null;
|
|
50
|
+
promoCode?: string | null;
|
|
51
|
+
additionalHoursCount?: number | null;
|
|
52
|
+
addOnSelections?: Array<{ addOnId: string; variantId?: string; quantity?: number }> | null;
|
|
53
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -13,11 +13,30 @@ export {
|
|
|
13
13
|
|
|
14
14
|
/** Canonical Via Via booking UI — same modules as `@/components/booking/*` on the site. */
|
|
15
15
|
export { BookingFlow } from './components/booking/BookingFlow';
|
|
16
|
+
export type { ChangeFlowSelectionPreview } from './components/booking/BookingFlow';
|
|
16
17
|
export { PrivateShuttleBookingFlow } from './components/booking/PrivateShuttleBookingFlow';
|
|
17
18
|
export type { BookingFlowUiOptions } from './components/booking/booking-flow-ui';
|
|
19
|
+
export type {
|
|
20
|
+
ProviderDashboardChangeBookingPayload,
|
|
21
|
+
ProviderDashboardInitialBooking,
|
|
22
|
+
} from './components/booking/provider-dashboard-change-booking';
|
|
23
|
+
export {
|
|
24
|
+
BookingChangeSummaryColumn,
|
|
25
|
+
changeBookingCompareColumnsVisuallyMatch,
|
|
26
|
+
computeItineraryStepChanged,
|
|
27
|
+
formatBookingDateForChangeCompare,
|
|
28
|
+
formatBookingItemsForCompare,
|
|
29
|
+
itinerariesEqualForChangeCompare,
|
|
30
|
+
} from './components/booking/change-booking-compare';
|
|
31
|
+
export type {
|
|
32
|
+
ItineraryStepLine,
|
|
33
|
+
ChangeHighlightVariant,
|
|
34
|
+
} from './components/booking/change-booking-compare';
|
|
35
|
+
export { getItineraryStepLabel } from './lib/booking/itinerary-display';
|
|
18
36
|
export { PARTNER_EMBEDDED_BOOKING_FLOW_UI_BASE } from './components/booking/booking-flow-ui';
|
|
19
37
|
export { default as BookingDialog } from './components/booking/BookingDialog';
|
|
20
38
|
export { default as BookingProductGrid } from './components/booking/BookingProductGrid';
|
|
39
|
+
export { DefaultTermsContent as TermsContent } from './components/booking/DefaultTermsContent';
|
|
21
40
|
|
|
22
41
|
export type {
|
|
23
42
|
BookingAppMode,
|
|
@@ -413,18 +413,35 @@ export function computeOrderSummary(
|
|
|
413
413
|
|
|
414
414
|
const isTaxIncludedInPrice = pricingConfig.currenciesWithTaxIncluded.includes(currency);
|
|
415
415
|
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
416
|
+
const round2 = (n: number) => Math.round(n * 100) / 100;
|
|
417
|
+
|
|
418
|
+
const ticketLineItems: OrderSummaryTicketLine[] = pricing
|
|
419
|
+
.map((rate) => {
|
|
420
|
+
const qty = quantities[rate.category] ?? 0;
|
|
421
|
+
if (qty === 0) return null;
|
|
422
|
+
const pricePerUnit = rate.price ?? 0;
|
|
423
|
+
return {
|
|
424
|
+
category: rate.category,
|
|
425
|
+
qty,
|
|
426
|
+
pricePerUnit,
|
|
427
|
+
itemTotal: round2(qty * pricePerUnit),
|
|
428
|
+
};
|
|
429
|
+
})
|
|
430
|
+
.filter((line): line is OrderSummaryTicketLine => line != null);
|
|
431
|
+
|
|
432
|
+
const basePrice = ticketLineItems.reduce((sum, line) => sum + line.itemTotal, 0);
|
|
420
433
|
|
|
421
434
|
const perPersonInDisplay = selectedReturnOption?.priceAdjustmentByCurrency?.[currency] ?? 0;
|
|
422
435
|
const returnPriceAdjustment =
|
|
423
|
-
totalQuantity > 0 && perPersonInDisplay !== 0
|
|
436
|
+
totalQuantity > 0 && perPersonInDisplay !== 0
|
|
437
|
+
? round2(totalQuantity * perPersonInDisplay)
|
|
438
|
+
: 0;
|
|
424
439
|
|
|
425
440
|
const cancellationPolicyFee =
|
|
426
441
|
cancellationPolicyId && pricingConfig?.cancellationPolicies?.length
|
|
427
|
-
? (
|
|
442
|
+
? round2(
|
|
443
|
+
pricingConfig.cancellationPolicies.find((p) => p.id === cancellationPolicyId)?.feeByCurrency?.[currency] ?? 0
|
|
444
|
+
)
|
|
428
445
|
: 0;
|
|
429
446
|
|
|
430
447
|
const fees = pricingConfig.fees ?? {};
|
|
@@ -434,23 +451,14 @@ export function computeOrderSummary(
|
|
|
434
451
|
? []
|
|
435
452
|
: Object.entries(byCurrency).map(([name, amountPerPerson]) => ({
|
|
436
453
|
name,
|
|
437
|
-
totalAmount: totalQuantity * amountPerPerson,
|
|
454
|
+
totalAmount: round2(totalQuantity * amountPerPerson),
|
|
438
455
|
description: fees[name]?.description,
|
|
439
456
|
}));
|
|
440
457
|
|
|
441
458
|
const feesTotal = feeLineItems.reduce((s, f) => s + f.totalAmount, 0);
|
|
442
|
-
const subtotal = basePrice + returnPriceAdjustment + cancellationPolicyFee + feesTotal;
|
|
443
|
-
const tax = isTaxIncludedInPrice ? 0 : subtotal * pricingConfig.taxRate;
|
|
444
|
-
const total = subtotal + tax;
|
|
445
|
-
|
|
446
|
-
const ticketLineItems: OrderSummaryTicketLine[] = pricing
|
|
447
|
-
.map((rate) => {
|
|
448
|
-
const qty = quantities[rate.category] ?? 0;
|
|
449
|
-
if (qty === 0) return null;
|
|
450
|
-
const pricePerUnit = rate.price ?? 0;
|
|
451
|
-
return { category: rate.category, qty, pricePerUnit, itemTotal: qty * pricePerUnit };
|
|
452
|
-
})
|
|
453
|
-
.filter((line): line is OrderSummaryTicketLine => line != null);
|
|
459
|
+
const subtotal = round2(basePrice + returnPriceAdjustment + cancellationPolicyFee + feesTotal);
|
|
460
|
+
const tax = isTaxIncludedInPrice ? 0 : round2(subtotal * pricingConfig.taxRate);
|
|
461
|
+
const total = round2(subtotal + tax);
|
|
454
462
|
|
|
455
463
|
return {
|
|
456
464
|
subtotal,
|
package/src/lib/booking-api.ts
CHANGED
|
@@ -838,6 +838,13 @@ export interface ChangeBookingQuoteRequest {
|
|
|
838
838
|
newAddOnSelections?: Array<{ addOnId: string; variantId?: string; quantity?: number }>;
|
|
839
839
|
/** Full new-booking total shown in the UI; server verifies within tolerance then uses this for the session so charge matches screen. */
|
|
840
840
|
clientProposedTotal?: number;
|
|
841
|
+
/** Optional change-flow capacity hint so API can exclude current booking seats from sold counts. */
|
|
842
|
+
capacitySeatCredit?: {
|
|
843
|
+
enabled: boolean;
|
|
844
|
+
previousPassengerCount?: number | null;
|
|
845
|
+
previousAvailabilityId?: string | null;
|
|
846
|
+
previousReturnAvailabilityId?: string | null;
|
|
847
|
+
} | null;
|
|
841
848
|
}
|
|
842
849
|
|
|
843
850
|
export interface ChangeBookingQuoteReceipt {
|
package/src/runtime/types.ts
CHANGED
|
@@ -51,7 +51,8 @@ export interface BookingRuntimeSlots {
|
|
|
51
51
|
ProductTag: BookingSlotComponent;
|
|
52
52
|
PillVariant: unknown;
|
|
53
53
|
ImageModal: BookingSlotComponent;
|
|
54
|
-
|
|
54
|
+
/** Optional override; package supplies a full default terms component when omitted. */
|
|
55
|
+
TermsContent?: BookingSlotComponent;
|
|
55
56
|
PlusIcon: ComponentType<SVGProps<SVGSVGElement>>;
|
|
56
57
|
MinusIcon: ComponentType<SVGProps<SVGSVGElement>>;
|
|
57
58
|
/** Via Via `ButtonHoverColor` enum from `@/components/button`. */
|