@autobusal/routes-order 1.33.7 → 1.33.9
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 +11 -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/Step4/checkAge.ts +15 -4
- package/Step5/utilities.ts +24 -11
- package/package.json +1 -1
- package/services.ts +24 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.33.9 (2026-08-28)
|
|
4
|
+
|
|
5
|
+
- 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).
|
|
6
|
+
- 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).
|
|
7
|
+
- useGetFlex takes the coupon-adjusted total so the quoted flex price matches what the server bills on a couponed booking (sweep #53).
|
|
8
|
+
- Passenger age bands match the server rules: adult 12+, child 2-12, baby under 2 (sweep #48).
|
|
9
|
+
|
|
10
|
+
## 1.33.8 (2026-08-28)
|
|
11
|
+
|
|
12
|
+
- usePostOrder replays the stored referral token as `referral`, so widget/link arrivals attribute their affiliate at checkout.
|
|
13
|
+
|
|
3
14
|
## 1.33.7 (2026-08-26)
|
|
4
15
|
|
|
5
16
|
- Seat-selection add-on: checkout reads settings addons.seats - picking turns optional with the fee on the CTA and a live summary line; submission strips unpicked (zero) seat fields and sends addon_seats only when a hand-picked seat remains. Fixes the return-leg picked-seat list rebuilding from the departure's on re-pick.
|
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/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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMutation, UseMutationResult, useQuery, UseQueryResult } from '@tanstack/react-query';
|
|
2
2
|
import { AxiosError } from 'axios';
|
|
3
|
-
import { apiClient } from '@autobusal/providers';
|
|
3
|
+
import { apiClient, storedReferral } from '@autobusal/providers';
|
|
4
4
|
import { RoutesSearchForm } from '@autobusal/routes-search/types';
|
|
5
5
|
import { FoundData, FlexOffer } from '@autobusal/providers/types/routes';
|
|
6
6
|
import { CouponData } from '@autobusal/providers/types/orders';
|
|
@@ -224,6 +224,21 @@ export const usePostOrder = (
|
|
|
224
224
|
order.action = action;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/*
|
|
228
|
+
* Edited: Claude - Date: 2026-08-28
|
|
229
|
+
*
|
|
230
|
+
* The referral token minted when this visitor arrived through ?ref=
|
|
231
|
+
* (providers useReferralTouch) - replayed under the exact field name
|
|
232
|
+
* obtapi's Attribution::FIELD declares. Expiry is checked client-side
|
|
233
|
+
* only to avoid sending a corpse; the server re-validates everything
|
|
234
|
+
* and a stale or self-referred token simply attributes nothing.
|
|
235
|
+
*/
|
|
236
|
+
const referral = storedReferral();
|
|
237
|
+
|
|
238
|
+
if (referral !== null) {
|
|
239
|
+
order.referral = referral.token;
|
|
240
|
+
}
|
|
241
|
+
|
|
227
242
|
if (data !== undefined) {
|
|
228
243
|
Object.keys(data).forEach(index => {
|
|
229
244
|
order[index] = data[index as keyof typeof data];
|
|
@@ -296,10 +311,14 @@ export const useGetFlex = (
|
|
|
296
311
|
returnPriceId: (number | undefined),
|
|
297
312
|
adults: number,
|
|
298
313
|
children: number,
|
|
299
|
-
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
|
|
300
319
|
): UseQueryResult<FlexOffer> => (
|
|
301
320
|
useQuery({
|
|
302
|
-
queryKey: ['flex-offer', { departurePriceId, returnPriceId, adults, children, babies }],
|
|
321
|
+
queryKey: ['flex-offer', { departurePriceId, returnPriceId, adults, children, babies, total }],
|
|
303
322
|
enabled: departurePriceId !== undefined,
|
|
304
323
|
queryFn: async () => (
|
|
305
324
|
await apiClient
|
|
@@ -309,7 +328,8 @@ export const useGetFlex = (
|
|
|
309
328
|
return_price_id: returnPriceId,
|
|
310
329
|
adults,
|
|
311
330
|
children,
|
|
312
|
-
babies
|
|
331
|
+
babies,
|
|
332
|
+
...(total !== undefined ? { total } : {})
|
|
313
333
|
}
|
|
314
334
|
})
|
|
315
335
|
.then(response => response.data)
|