@autobusal/routes-order 1.33.8 → 1.33.10
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/CHANGELOG.md +12 -0
- package/Checkout/Checkout.tsx +7 -3
- package/Found/Empty/Empty.tsx +38 -10
- package/Found/Found.tsx +14 -1
- package/Found/Orders/Orders.tsx +1 -1
- package/Step1/prepare.ts +21 -2
- package/Step4/checkAge.ts +15 -4
- package/Step5/utilities.ts +24 -11
- package/package.json +1 -1
- package/services.ts +8 -3
- package/styles.ts +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.33.10 (2026-08-29)
|
|
4
|
+
|
|
5
|
+
- The return date falls back to the outbound when the URL carries no real one ('none' on a one-way link), so switching a collapsed search to Round Trip opens the calendar on the month being travelled and cannot offer a day before the departure.
|
|
6
|
+
- The collapsed search panel sits above the step markers: its backdrop-filter creates a stacking context, so the date picker used to draw underneath them.
|
|
7
|
+
|
|
8
|
+
## 1.33.9 (2026-08-28)
|
|
9
|
+
|
|
10
|
+
- Return-leg empty state fetches RETURN alternatives (swapped pair + return date) and its suggested-day links fill the return slot instead of rewriting the departure; nearby-city swaps map back to the URL's outbound pair (sweep #50).
|
|
11
|
+
- Checkout money parsing survives the server's space-thousands format: currency is the last token, coupon amounts strip grouping spaces - totals over 1000 no longer corrupt the rendered total or the GA4 values (sweep #51).
|
|
12
|
+
- useGetFlex takes the coupon-adjusted total so the quoted flex price matches what the server bills on a couponed booking (sweep #53).
|
|
13
|
+
- Passenger age bands match the server rules: adult 12+, child 2-12, baby under 2 (sweep #48).
|
|
14
|
+
|
|
3
15
|
## 1.33.8 (2026-08-28)
|
|
4
16
|
|
|
5
17
|
- usePostOrder replays the stored referral token as `referral`, so widget/link arrivals attribute their affiliate at checkout.
|
package/Checkout/Checkout.tsx
CHANGED
|
@@ -15,7 +15,7 @@ import Addons from '../Step5/Addons/Addons';
|
|
|
15
15
|
import Billing from '../Step5/Billing/Billing';
|
|
16
16
|
import Payments from '../Step5/Payments/Payments';
|
|
17
17
|
import Sidebar from './Sidebar/Sidebar';
|
|
18
|
-
import { getTotal, convertCoupon, getAvailablePayments } from '../Step5/utilities';
|
|
18
|
+
import { getTotal, convertCoupon, getAvailablePayments, currencyOf } from '../Step5/utilities';
|
|
19
19
|
import { Title, Actions, Results, Listing } from '../styles';
|
|
20
20
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
21
21
|
import { PersonData, BillingData } from '@autobusal/providers/types/persons';
|
|
@@ -179,7 +179,9 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
179
179
|
const busFrom = useBusOccupied(step1.departure, step2.id as number);
|
|
180
180
|
const busTo = useBusOccupied(step1._return, step3?.id as (number | undefined));
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
// Edited: Claude - Date: 2026-08-28 (sweep, low): the currency is the
|
|
183
|
+
// LAST token - split(' ')[1] read '250.00' out of '1 250.00 EUR'
|
|
184
|
+
const currency = currencyOf(total.display);
|
|
183
185
|
|
|
184
186
|
// the brand sells seat choice: picking turns optional (auto-assign is the
|
|
185
187
|
// free default) and each hand-picked seat costs this
|
|
@@ -195,7 +197,9 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
195
197
|
step3?.price.id as (number | undefined),
|
|
196
198
|
step1.adults ?? 1,
|
|
197
199
|
step1.children ?? 0,
|
|
198
|
-
step1.babies ?? 0
|
|
200
|
+
step1.babies ?? 0,
|
|
201
|
+
// the coupon-adjusted fare, so the quote and the charge share a base
|
|
202
|
+
total.value
|
|
199
203
|
);
|
|
200
204
|
|
|
201
205
|
/**
|
package/Found/Empty/Empty.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
|
4
4
|
import { AlternativesData, NearbyCity, FoundDay } from '../../types';
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
7
|
+
type: 'departure' | '_return'
|
|
7
8
|
step1: RoutesSearchForm
|
|
8
9
|
data?: AlternativesData
|
|
9
10
|
loading: boolean
|
|
@@ -25,29 +26,51 @@ interface Props {
|
|
|
25
26
|
* even when the further city is cheaper: the passenger's problem here is
|
|
26
27
|
* getting to the bus, not saving two euros.
|
|
27
28
|
*/
|
|
28
|
-
const Empty = ({ step1, data, loading, t }: Props): JSX.Element => {
|
|
29
|
+
const Empty = ({ type, step1, data, loading, t }: Props): JSX.Element => {
|
|
29
30
|
// obtapi wants DD/MM/YYYY, the URL carries DD-MM-YYYY
|
|
30
31
|
const asPath = (date: string): string => date.replace(/\//g, '-');
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
/*
|
|
34
|
+
* Edited: Claude - Date: 2026-08-28 (sweep, low)
|
|
35
|
+
*
|
|
36
|
+
* The suggestions here are now leg-aware. The URL always carries the
|
|
37
|
+
* OUTBOUND pair and both dates, but on the return leg the search that
|
|
38
|
+
* came back empty is destination->origin on the return date - so a
|
|
39
|
+
* suggested day must land in the RETURN slot (the old link moved the
|
|
40
|
+
* departure the buyer never asked to change and led straight back into
|
|
41
|
+
* the same empty step), and a nearby city for the return's origin is a
|
|
42
|
+
* replacement for the URL's `to`, not its `from`.
|
|
43
|
+
*/
|
|
44
|
+
const isReturn = type === '_return';
|
|
45
|
+
|
|
46
|
+
const link = (from: string, to: string, departure: string, _return: string | undefined): string => ([
|
|
33
47
|
'/bus-lines',
|
|
34
48
|
from,
|
|
35
49
|
to,
|
|
36
50
|
asPath(departure),
|
|
37
|
-
|
|
51
|
+
_return ? asPath(_return) : 'none',
|
|
38
52
|
step1.type ?? 'departure',
|
|
39
53
|
String(step1.adults),
|
|
40
54
|
String(step1.children),
|
|
41
55
|
String(step1.babies)
|
|
42
56
|
].join('/'));
|
|
43
57
|
|
|
58
|
+
const cityLink = (item: NearbyCity, side: 'from' | 'to'): string => {
|
|
59
|
+
// `side` is relative to the leg that was searched; map it back to the
|
|
60
|
+
// URL's outbound pair
|
|
61
|
+
if (side === 'from') {
|
|
62
|
+
return isReturn
|
|
63
|
+
? link(step1.from, item.city.slug, step1.departure, step1._return)
|
|
64
|
+
: link(item.city.slug, step1.to, step1.departure, step1._return);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return isReturn
|
|
68
|
+
? link(item.city.slug, step1.to, step1.departure, step1._return)
|
|
69
|
+
: link(step1.from, item.city.slug, step1.departure, step1._return);
|
|
70
|
+
};
|
|
71
|
+
|
|
44
72
|
const city = (item: NearbyCity, side: 'from' | 'to'): JSX.Element => (
|
|
45
|
-
<Option
|
|
46
|
-
key={ item.city.id }
|
|
47
|
-
to={ side === 'from'
|
|
48
|
-
? link(item.city.slug, step1.to, step1.departure)
|
|
49
|
-
: link(step1.from, item.city.slug, step1.departure) }
|
|
50
|
-
>
|
|
73
|
+
<Option key={ item.city.id } to={ cityLink(item, side) }>
|
|
51
74
|
<OptionName title={ item.city.name }>{ item.city.name }</OptionName>
|
|
52
75
|
<OptionMeta>{ t('routes_order.step2.empty.distance', { km: item.distance }) }</OptionMeta>
|
|
53
76
|
<OptionPrice>{ item.from_price_display }</OptionPrice>
|
|
@@ -55,7 +78,12 @@ const Empty = ({ step1, data, loading, t }: Props): JSX.Element => {
|
|
|
55
78
|
);
|
|
56
79
|
|
|
57
80
|
const day = (item: FoundDay): JSX.Element => (
|
|
58
|
-
<Option
|
|
81
|
+
<Option
|
|
82
|
+
key={ item.id }
|
|
83
|
+
to={ isReturn
|
|
84
|
+
? link(step1.from, step1.to, step1.departure, item.day)
|
|
85
|
+
: link(step1.from, step1.to, item.day, step1._return) }
|
|
86
|
+
>
|
|
59
87
|
<OptionName>{ item.day }</OptionName>
|
|
60
88
|
<OptionMeta>{ t('routes_order.step2.empty.available') }</OptionMeta>
|
|
61
89
|
{ item.from_price_display && <OptionPrice>{ item.from_price_display }</OptionPrice> }
|
package/Found/Found.tsx
CHANGED
|
@@ -91,8 +91,21 @@ const Found = ({ type, loading, data, step1, preferredStop, t, onSearch, onSave
|
|
|
91
91
|
// Edited: Ferjolt Ozuni - Date: 2026-08-01
|
|
92
92
|
// Only fetched once the search itself has finished and come back empty,
|
|
93
93
|
// so a search that found something never pays for the alternatives.
|
|
94
|
+
/*
|
|
95
|
+
* Edited: Claude - Date: 2026-08-28 (sweep, low)
|
|
96
|
+
*
|
|
97
|
+
* On the return leg the journey with no results is destination->origin
|
|
98
|
+
* on the RETURN date - the same swap useGetDates and `watched` below
|
|
99
|
+
* have always made. Passing step1 unswapped asked the API for OUTBOUND
|
|
100
|
+
* alternatives, so the empty return step suggested days for a leg the
|
|
101
|
+
* buyer already has (including the very day they booked).
|
|
102
|
+
*/
|
|
103
|
+
const alternativesQuery = type === '_return'
|
|
104
|
+
? { ...step1, from: step1.to, to: step1.from, departure: step1._return ?? '' }
|
|
105
|
+
: step1;
|
|
106
|
+
|
|
94
107
|
const { data: alternatives, isFetching: alternativesLoading } = useGetAlternatives(
|
|
95
|
-
|
|
108
|
+
alternativesQuery,
|
|
96
109
|
!loading && items.length === 0
|
|
97
110
|
);
|
|
98
111
|
|
package/Found/Orders/Orders.tsx
CHANGED
|
@@ -43,7 +43,7 @@ const Orders = ({ loading, data, empty, refined, sort, type, passengers, preferr
|
|
|
43
43
|
// confirmed actually have seats.
|
|
44
44
|
if (empty) {
|
|
45
45
|
return (
|
|
46
|
-
<Empty step1={ step1 } data={ alternatives } loading={ alternativesLoading } t={ t } />
|
|
46
|
+
<Empty type={ type } step1={ step1 } data={ alternatives } loading={ alternativesLoading } t={ t } />
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
|
package/Step1/prepare.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Params } from 'react-router-dom';
|
|
2
|
-
import { futureDate } from '@autobusal/utilities';
|
|
2
|
+
import { futureDate, isPreparedDate } from '@autobusal/utilities';
|
|
3
3
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
4
4
|
|
|
5
5
|
const prepare = (
|
|
@@ -18,7 +18,26 @@ const prepare = (
|
|
|
18
18
|
const to = params.to ?? defaultLocations.to;
|
|
19
19
|
|
|
20
20
|
const departure = params.departure?.replace(/-/g, '/') ?? futureDate(1);
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* Edited: Claude - Date: 2026-08-29
|
|
24
|
+
*
|
|
25
|
+
* THE RETURN DATE IS TIED TO THE OUTBOUND. A one-way URL carries the
|
|
26
|
+
* literal "none" in the return slot, and `??` only catches null - so
|
|
27
|
+
* "none" travelled all the way into the picker, whose grid is built by
|
|
28
|
+
* splitting on "/" and feeding Date.UTC: an Invalid Date, rendered as
|
|
29
|
+
* January. Switching a collapsed search from One Way to Round Trip
|
|
30
|
+
* showed exactly that.
|
|
31
|
+
*
|
|
32
|
+
* Anything that is not a real date falls back to the departure, which is
|
|
33
|
+
* also the picker's floor - so the return calendar opens on the month
|
|
34
|
+
* being travelled and cannot offer a day before the outbound.
|
|
35
|
+
*/
|
|
36
|
+
const requested = params._return?.replace(/-/g, '/');
|
|
37
|
+
|
|
38
|
+
const _return = isPreparedDate(requested)
|
|
39
|
+
? String(requested)
|
|
40
|
+
: (isPreparedDate(departure) ? departure : futureDate(2));
|
|
22
41
|
|
|
23
42
|
const adults = params.adults ?? 1;
|
|
24
43
|
const children = params.children ?? 0;
|
package/Step4/checkAge.ts
CHANGED
|
@@ -3,12 +3,23 @@ import { error, differenceYears, createDate } from '@autobusal/utilities';
|
|
|
3
3
|
import { PersonData } from '@autobusal/providers/types/persons';
|
|
4
4
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Edited: Claude - Date: 2026-08-28 (sweep finding, medium)
|
|
8
|
+
*
|
|
9
|
+
* ONE SET OF BANDS, the ones the picker advertises in all 15 languages:
|
|
10
|
+
* adults 12+, children 2-12, babies under 2. This check used to say
|
|
11
|
+
* adult>=18 / child 3-17 / baby<=2 while the server said something
|
|
12
|
+
* different again - a 2-year-old could not be booked in any category and
|
|
13
|
+
* a teenager the copy calls an adult was refused at pay time. The
|
|
14
|
+
* boundaries mirror obtapi's RulesGeneral::getDob exactly (2nd birthday
|
|
15
|
+
* makes a child, 12th an adult).
|
|
16
|
+
*/
|
|
6
17
|
const checkAge = (step1: RoutesSearchForm, data: PersonData, t: TFunction<'general'>): boolean => {
|
|
7
18
|
// check adults ages
|
|
8
19
|
for (let i = 1; i <= step1.adults; i++) {
|
|
9
20
|
const age = getAge(data[`adult${ i }_dob`]);
|
|
10
21
|
|
|
11
|
-
if (age <
|
|
22
|
+
if (age < 12) {
|
|
12
23
|
error(t('routes_order.step4.errors.age'));
|
|
13
24
|
return false;
|
|
14
25
|
}
|
|
@@ -18,7 +29,7 @@ const checkAge = (step1: RoutesSearchForm, data: PersonData, t: TFunction<'gener
|
|
|
18
29
|
for (let i = 1; i <= step1.children; i++) {
|
|
19
30
|
const age = getAge(data[`child${ i }_dob`])
|
|
20
31
|
|
|
21
|
-
if (age <
|
|
32
|
+
if (age < 2 || age >= 12) {
|
|
22
33
|
error(t('routes_order.step4.errors.age'));
|
|
23
34
|
return false;
|
|
24
35
|
}
|
|
@@ -28,7 +39,7 @@ const checkAge = (step1: RoutesSearchForm, data: PersonData, t: TFunction<'gener
|
|
|
28
39
|
for (let i = 1; i <= step1.babies; i++) {
|
|
29
40
|
const age = getAge(data[`baby${ i }_dob`]);
|
|
30
41
|
|
|
31
|
-
if (age
|
|
42
|
+
if (age >= 2) {
|
|
32
43
|
error(t('routes_order.step4.errors.age'));
|
|
33
44
|
return false;
|
|
34
45
|
}
|
|
@@ -45,4 +56,4 @@ const getAge = (date: string | number): number => {
|
|
|
45
56
|
return age;
|
|
46
57
|
};
|
|
47
58
|
|
|
48
|
-
export default checkAge;
|
|
59
|
+
export default checkAge;
|
package/Step5/utilities.ts
CHANGED
|
@@ -2,26 +2,39 @@ import { FoundData } from '@autobusal/providers/types/routes';
|
|
|
2
2
|
import { CouponData } from '@autobusal/providers/types/orders';
|
|
3
3
|
import { ActionData, TotalData } from '../types';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/*
|
|
6
|
+
* Edited: Claude - Date: 2026-08-28 (sweep, low)
|
|
7
|
+
*
|
|
8
|
+
* The server formats thousands with a SPACE ('1 250.00 EUR', Fundable
|
|
9
|
+
* 2026-08-19), so split(' ')[0]/[1] read the wrong pieces the moment a
|
|
10
|
+
* basket crosses 1000 - the currency became '250.00' and a couponed total
|
|
11
|
+
* became the number 1, corrupting the rendered total and the GA4 values.
|
|
12
|
+
* The currency is the LAST token; the amount is everything else with the
|
|
13
|
+
* grouping spaces stripped.
|
|
14
|
+
*/
|
|
15
|
+
export const currencyOf = (display: string): string => (
|
|
16
|
+
display.trim().split(' ').pop() ?? ''
|
|
17
|
+
);
|
|
8
18
|
|
|
19
|
+
export const amountOf = (display: string): number => (
|
|
20
|
+
Number(display.trim().split(' ').slice(0, -1).join(''))
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export const getTotal = (step2: FoundData, step3?: FoundData): TotalData => {
|
|
9
24
|
const total = step2.price.value + (step3?.price.value ?? 0);
|
|
10
25
|
|
|
11
26
|
return {
|
|
12
|
-
display: `${ total } ${
|
|
27
|
+
display: `${ total } ${ currencyOf(step2.price.display) }`,
|
|
13
28
|
value: total
|
|
14
29
|
};
|
|
15
30
|
};
|
|
16
31
|
|
|
17
|
-
export const convertCoupon = (data: CouponData): TotalData =>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return {
|
|
32
|
+
export const convertCoupon = (data: CouponData): TotalData => (
|
|
33
|
+
{
|
|
21
34
|
display: data.total,
|
|
22
|
-
value:
|
|
23
|
-
}
|
|
24
|
-
|
|
35
|
+
value: amountOf(data.total)
|
|
36
|
+
}
|
|
37
|
+
);
|
|
25
38
|
|
|
26
39
|
/**
|
|
27
40
|
* The payment methods actually offered on this checkout, in the order the
|
package/package.json
CHANGED
package/services.ts
CHANGED
|
@@ -311,10 +311,14 @@ export const useGetFlex = (
|
|
|
311
311
|
returnPriceId: (number | undefined),
|
|
312
312
|
adults: number,
|
|
313
313
|
children: number,
|
|
314
|
-
babies: number
|
|
314
|
+
babies: number,
|
|
315
|
+
// Edited: Claude - Date: 2026-08-28 (sweep, low): the coupon-adjusted
|
|
316
|
+
// fare, so the quoted flex price matches what Make\Addons will charge -
|
|
317
|
+
// without it a couponed booking showed one figure and billed another
|
|
318
|
+
total?: number
|
|
315
319
|
): UseQueryResult<FlexOffer> => (
|
|
316
320
|
useQuery({
|
|
317
|
-
queryKey: ['flex-offer', { departurePriceId, returnPriceId, adults, children, babies }],
|
|
321
|
+
queryKey: ['flex-offer', { departurePriceId, returnPriceId, adults, children, babies, total }],
|
|
318
322
|
enabled: departurePriceId !== undefined,
|
|
319
323
|
queryFn: async () => (
|
|
320
324
|
await apiClient
|
|
@@ -324,7 +328,8 @@ export const useGetFlex = (
|
|
|
324
328
|
return_price_id: returnPriceId,
|
|
325
329
|
adults,
|
|
326
330
|
children,
|
|
327
|
-
babies
|
|
331
|
+
babies,
|
|
332
|
+
...(total !== undefined ? { total } : {})
|
|
328
333
|
}
|
|
329
334
|
})
|
|
330
335
|
.then(response => response.data)
|
package/styles.ts
CHANGED
|
@@ -94,6 +94,19 @@ export const SearchPanel = styled.div<{ $open: boolean }>`
|
|
|
94
94
|
display: ${ props => props.$open ? 'block' : 'none' };
|
|
95
95
|
margin-top: 14px;
|
|
96
96
|
padding: 20px;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Edited: Claude - Date: 2026-08-29
|
|
100
|
+
*
|
|
101
|
+
* ABOVE THE STEPS, because backdrop-filter below creates a stacking
|
|
102
|
+
* context: everything inside this panel is confined to it, so the date
|
|
103
|
+
* picker's own z-index (100) could never beat the step markers sitting
|
|
104
|
+
* at z-index 1 in the page's context. Opening the collapsed search and
|
|
105
|
+
* clicking a date drew the calendar UNDERNEATH the 1-2-3-4 circles.
|
|
106
|
+
* Raising the panel itself lifts its whole subtree with it.
|
|
107
|
+
*/
|
|
108
|
+
position: relative;
|
|
109
|
+
z-index: 20;
|
|
97
110
|
background: ${ props => props.theme.header.transparent };
|
|
98
111
|
backdrop-filter: blur(25px);
|
|
99
112
|
-webkit-backdrop-filter: blur(25px);
|