@autobusal/routes-order 1.25.0 → 1.26.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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.26.0
4
+
5
+ **Funnel events on every step**: the result list, the coach chosen, checkout reached, the Flexible Ticket ticked or unticked, and the value stashed for the purchase event.
6
+
7
+ - `view_item_list` is keyed on the **result set**, not on render — this component re-renders on every filter tick and sort change, and announcing again each time would report a dozen searches for one.
8
+ - `begin_checkout` lives in `Checkout` rather than `RoutesOrder`, because that screen already derives the currency from its own formatted total (the public settings payload deliberately carries none) and it mounts exactly once per arrival.
9
+ - The **add-on attach rate** is the one input the Flexible Ticket pricing decision has been waiting on — it cannot be banded by window without knowing how often each window is actually bought.
10
+ - One `item()` mapping shared by every event, keyed on the **route code** rather than the numeric id, so a report can be reconciled against something a human recognises.
11
+
3
12
  ## 1.25.0
4
13
 
5
14
  **Reviews on the route-pair page.** A new section between the timetable and the internal links, with its own nav anchor.
@@ -1,4 +1,4 @@
1
- import { useCallback, useState } from 'react';
1
+ import { useCallback, useState, useEffect } from 'react';
2
2
  import { TFunction } from 'i18next';
3
3
  import { useForm } from 'react-hook-form';
4
4
  import { useNavigate } from 'react-router-dom';
@@ -25,6 +25,8 @@ import { TotalData } from '../types';
25
25
  import { useUserStore } from '@autobusal/providers/stores/user';
26
26
  import { useGetSettings } from '@autobusal/providers/services';
27
27
  import { usePostOrder, useGetFlex } from '../services';
28
+ import { addon as commerceAddon, beginCheckout, stash } from '@autobusal/providers/Setup/commerce';
29
+ import { item as commerceItem } from '../commerce';
28
30
 
29
31
  interface Props {
30
32
  values?: PersonData
@@ -129,6 +131,21 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
129
131
  value: total.value + addonsValue
130
132
  };
131
133
 
134
+ /**
135
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
136
+ *
137
+ * Here rather than in RoutesOrder because this screen already derives the
138
+ * currency from its own formatted total - the public settings payload
139
+ * carries none - and because it mounts exactly once per arrival at
140
+ * checkout, which is the cadence this event wants.
141
+ */
142
+ useEffect(() => {
143
+ const items = [commerceItem(step2), ...(step3 ? [commerceItem(step3)] : [])];
144
+
145
+ beginCheckout(total.value, items, currency);
146
+ // eslint-disable-next-line react-hooks/exhaustive-deps
147
+ }, []);
148
+
132
149
  const { mutate: OrderSend, isPending } = usePostOrder(step1, step2, step3, coupon, action, {
133
150
  whatsapp: addonWhatsapp,
134
151
  whatsappNumber: addonWhatsappNumber,
@@ -200,6 +217,21 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
200
217
 
201
218
  const hash = step1.type === 'return' ? data.return : data.departure;
202
219
 
220
+ // Edited: Ferjolt Ozuni - Date: 2026-08-05
221
+ //
222
+ // Remembered here because the confirmation page cannot work it out.
223
+ // A card payment leaves the site for the bank and returns on a URL
224
+ // carrying nothing but this hash, and most buyers are guests with no
225
+ // account the order could be read back from. Keyed on the SAME hash
226
+ // the confirmation is reached with, so it cannot be attributed to a
227
+ // different order.
228
+ stash(
229
+ String(hash),
230
+ grandTotal.value,
231
+ [commerceItem(step2), ...(step3 ? [commerceItem(step3)] : [])],
232
+ currency
233
+ );
234
+
203
235
  navigate(`/orders/process/${ action }/${ hash }`);
204
236
  }
205
237
  });
@@ -244,7 +276,14 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
244
276
  onChangeWhatsapp={ setAddonWhatsapp }
245
277
  onChangeWhatsappNumber={ setAddonWhatsappNumber }
246
278
  onChangeTelegram={ setAddonTelegram }
247
- onChangeFlex={ setAddonFlex }
279
+ onChangeFlex={ (value) => {
280
+ setAddonFlex(value);
281
+
282
+ // The attach rate this records is the one input the Flexible
283
+ // Ticket pricing decision is waiting on - it cannot be banded
284
+ // by window without knowing how often each is actually bought.
285
+ commerceAddon(value, 'Flexible Ticket', FlexData?.price ?? 0, currency);
286
+ } }
248
287
  />
249
288
 
250
289
  { UserData === undefined && (
package/Found/Found.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useMemo, useState } from 'react';
1
+ import { useMemo, useState, useEffect } from 'react';
2
2
  import { useSearchParams } from 'react-router-dom';
3
3
  import { TFunction } from 'i18next';
4
4
  import Dates from './Dates/Dates';
@@ -13,6 +13,8 @@ import { Results, Sidebar, Listing } from '../styles';
13
13
  import { useGetAlternatives } from '../services';
14
14
  import { RoutesSearchForm } from '@autobusal/routes-search/types';
15
15
  import { FoundData } from '@autobusal/providers/types/routes';
16
+ import { viewItemList } from '@autobusal/providers/Setup/commerce';
17
+ import { item as commerceItem } from '../commerce';
16
18
  import { Refinement, BucketKey, fromParams, toParams, filterItems, buildFacets, priceRange, currencyOf, isRefined } from './refine';
17
19
 
18
20
  interface Props {
@@ -31,6 +33,17 @@ interface Props {
31
33
  const OWNED = [ 'sort', 'operators', 'times', 'direct', 'from_stops', 'to_stops', 'features', 'max_price' ];
32
34
 
33
35
  const Found = ({ type, loading, data, step1, preferredStop, t, onSearch, onSave }: Props): JSX.Element => {
36
+ /**
37
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
38
+ *
39
+ * Keyed on the RESULT SET, not on every render. This component re-renders
40
+ * on each filter tick and sort change, and announcing the list again every
41
+ * time would report a dozen searches for one.
42
+ */
43
+ useEffect(() => {
44
+ viewItemList((data ?? []).map(commerceItem), currencyOf(data ?? []));
45
+ }, [data]);
46
+
34
47
  const [ searchParams, setSearchParams ] = useSearchParams();
35
48
  const [ open, setOpen ] = useState<boolean>(false);
36
49
 
package/RoutesOrder.tsx CHANGED
@@ -5,6 +5,9 @@ import { AiOutlineSearch, AiOutlineCaretDown, AiOutlineCaretUp } from 'react-ico
5
5
  import { Meta } from '@autobusal/common';
6
6
  import { useSystemTheme } from '@autobusal/hooks';
7
7
  import { useGetSettings } from '@autobusal/providers/services';
8
+ import { selectItem } from '@autobusal/providers/Setup/commerce';
9
+ import { item as commerceItem } from './commerce';
10
+ import { currencyOf } from './Found/refine';
8
11
  import RoutesSearch from '@autobusal/routes-search';
9
12
  import { useGetSuggestions } from '@autobusal/routes-search/services';
10
13
  import { SearchAgain, SearchInner, SearchToggle, SearchSummary, SearchPanel } from './styles';
@@ -142,6 +145,8 @@ const RoutesOrder = ({ t }: Props): JSX.Element => {
142
145
  const onStep2 = (data: FoundData): void => {
143
146
  setStep2(data);
144
147
 
148
+ selectItem(commerceItem(data), currencyOf([data]));
149
+
145
150
  goto(step1?.type === 'return' ? 'return' : 'checkout');
146
151
  };
147
152
 
package/commerce.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { CommerceItem } from '@autobusal/providers/Setup/commerce';
2
+ import { FoundData } from '@autobusal/providers/types/routes';
3
+
4
+ /**
5
+ * A coach, as the analytics layer sees it.
6
+ *
7
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
8
+ *
9
+ * One mapping used by every event, so a route cannot be identified one way
10
+ * in `select_item` and another in `purchase` - which is exactly how a funnel
11
+ * ends up unable to join its own steps together.
12
+ *
13
+ * The ROUTE CODE is the id rather than the numeric key: it is what appears
14
+ * on the ticket, in the timetable and on the operator's own paperwork, so a
15
+ * report built on it can be reconciled against something a human recognises.
16
+ */
17
+ export const item = (route: FoundData): CommerceItem => ({
18
+ item_id: String(route.code ?? route.id),
19
+ item_name: `${ route.locations?.from?.city?.name ?? '' } - ${ route.locations?.to?.city?.name ?? '' }`.trim(),
20
+ item_brand: route.operator?.company,
21
+ price: Number(route.price?.value ?? 0),
22
+ quantity: 1
23
+ });
24
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.25.0",
3
+ "version": "1.26.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"