@autobusal/routes-order 1.33.10 → 1.34.1

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,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.34.0 (2026-08-29)
4
+
5
+ - The bare /bus-lines form suggests an origin near the visitor and no longer pre-fills a destination; a URL that names one still wins.
6
+
3
7
  ## 1.33.10 (2026-08-29)
4
8
 
5
9
  - 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.
@@ -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, currencyOf } from '../Step5/utilities';
18
+ import { getTotal, convertCoupon, getAvailablePayments, currencyOf, surchargeOf } 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';
@@ -264,6 +264,35 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
264
264
  value: total.value + addonsValue
265
265
  };
266
266
 
267
+ /*
268
+ * Claude - 2026-08-29 (ruling by Ferjolt)
269
+ *
270
+ * THE CARD SURCHARGE, shown the moment a card method is picked rather
271
+ * than sprung on the buyer at the bank's own page: "Once selected a
272
+ * payment method, will show at it +{real value}, and not showing it only
273
+ * on the card filling details."
274
+ *
275
+ * Zero until a method is chosen, and zero forever for funds, cash,
276
+ * reserve and complimentary - see surchargeOf, which only recognises the
277
+ * five gateways obtapi actually surcharges.
278
+ */
279
+ const surcharge = surchargeOf(grandTotal.value, action, paymentsData);
280
+
281
+ /*
282
+ * What the buyer is actually asked for. Rounded rather than summed
283
+ * straight into the template: 19.90 + 1.00 is not 20.90 in IEEE-754, and
284
+ * a total rendering as 20.900000000000002 is the sort of thing that
285
+ * reaches a screenshot before it reaches a test.
286
+ *
287
+ * The un-surcharged `grandTotal` is what still goes to GA4 below - the
288
+ * value of the SALE is the tickets and the add-ons, and folding a card
289
+ * fee into it would quietly inflate revenue for anyone paying by card.
290
+ */
291
+ const payable: TotalData = surcharge > 0 ? {
292
+ display: `${ Math.round((grandTotal.value + surcharge) * 100) / 100 } ${ currency }`,
293
+ value: Math.round((grandTotal.value + surcharge) * 100) / 100
294
+ } : grandTotal;
295
+
267
296
  /**
268
297
  * Edited: Ferjolt Ozuni - Date: 2026-08-05
269
298
  *
@@ -469,7 +498,8 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
469
498
  fare={ getTotal(step2, step3).display }
470
499
  addons={ addonItems.map(item => ({ label: item.label, value: `${ item.price } ${ currency }` })) }
471
500
  coupon={ coupon }
472
- total={ grandTotal }
501
+ total={ payable }
502
+ surcharge={ surcharge > 0 ? `${ surcharge } ${ currency }` : undefined }
473
503
  action={ action }
474
504
  single={ singleMethod }
475
505
  pending={ isPending }
@@ -480,6 +510,8 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
480
510
  selected={ action ?? '' }
481
511
  isPartner={ isPartner ?? false }
482
512
  routeId={ step2.id }
513
+ total={ grandTotal.value }
514
+ currency={ currency }
483
515
  t={ t }
484
516
  onChange={ onAction }
485
517
  />
@@ -25,6 +25,15 @@ interface Props {
25
25
  addons: AddonItem[]
26
26
  coupon?: CouponData
27
27
  total: TotalData
28
+ /**
29
+ * The card surcharge already included in `total`, formatted with its
30
+ * currency - undefined when the chosen method carries none.
31
+ *
32
+ * Claude - 2026-08-29 (ruling by Ferjolt): the fee is stated as its own
33
+ * line rather than folded silently into the total, so the buyer can see
34
+ * what changed when they picked a card.
35
+ */
36
+ surcharge?: string
28
37
  action?: string
29
38
  single?: boolean
30
39
  pending: boolean
@@ -46,7 +55,7 @@ interface Props {
46
55
  * page and scrolled away while add-ons were being ticked - so the number
47
56
  * changed out of sight of the person it was changing for.
48
57
  */
49
- const Sidebar = ({ step1, step2, step3, fare, addons, coupon, total, action, single, pending, t, onPay, children, payments }: Props): JSX.Element => (
58
+ const Sidebar = ({ step1, step2, step3, fare, addons, coupon, total, surcharge, action, single, pending, t, onPay, children, payments }: Props): JSX.Element => (
50
59
  <Container>
51
60
  <Panel className="box">
52
61
  <SubTitle>
@@ -79,6 +88,15 @@ const Sidebar = ({ step1, step2, step3, fare, addons, coupon, total, action, sin
79
88
 
80
89
  { coupon && <Amount type="discount" code={ coupon.code } value={ coupon.discount } t={ t } /> }
81
90
 
91
+ { /* Claude - 2026-08-29: below the discount and above the total,
92
+ because it is the last thing added to what is owed - and it is
93
+ the only line here that appears and disappears as the payment
94
+ method changes, which reads as cause and effect when it sits
95
+ directly under the picker's own effect on the total. */ }
96
+ { surcharge && (
97
+ <Amount type="addon" label={ t('routes_order.step5.summary.surcharge') } value={ surcharge } t={ t } />
98
+ ) }
99
+
82
100
  <Amount type="total" value={ total.display } t={ t } />
83
101
  </div>
84
102
 
package/Step1/Step1.tsx CHANGED
@@ -59,6 +59,7 @@ const Step1 = ({ value, redirected, t, onSave }: Props): JSX.Element => {
59
59
  to={ `${ prepared.to }${ prepared.stop_to ? `:${ prepared.stop_to }` : '' }` }
60
60
  departure={ prepared.departure }
61
61
  _return={ prepared._return ?? '' }
62
+ suggest
62
63
  adultsNo={ prepared.adults }
63
64
  childrenNo={ prepared.children }
64
65
  babiesNo={ prepared.babies }
package/Step1/prepare.ts CHANGED
@@ -15,7 +15,16 @@ const prepare = (
15
15
  const type = params.type === 'return' ? 'return' : 'departure';
16
16
 
17
17
  const from = params.from ?? defaultLocations.from;
18
- const to = params.to ?? defaultLocations.to;
18
+ /*
19
+ * Edited: Claude - Date: 2026-08-29
20
+ *
21
+ * No default DESTINATION. A URL that names one is honoured; a bare form
22
+ * is left empty, because the destination is the one thing only the
23
+ * visitor knows and a pre-filled wrong one has to be noticed before it
24
+ * can be corrected. The origin still defaults - see the search form's
25
+ * `suggest`, which opens it near the visitor.
26
+ */
27
+ const to = params.to ?? '';
19
28
 
20
29
  const departure = params.departure?.replace(/-/g, '/') ?? futureDate(1);
21
30
 
@@ -7,19 +7,39 @@ import { Inline } from '@autobusal/common';
7
7
  import { SubTitle } from '../../styles';
8
8
  import { Container, ContainerPayments, ButtonPayment } from './styles';
9
9
  import { useGetPayments } from '../../services';
10
- import { getAvailablePayments } from '../utilities';
10
+ import { getAvailablePayments, surchargeOf } from '../utilities';
11
11
 
12
12
  interface Props {
13
13
  selected: string
14
14
  isPartner: boolean
15
15
  routeId?: number | string
16
+ /**
17
+ * The checkout total the surcharge is drawn against, and its currency.
18
+ *
19
+ * Claude - 2026-08-29 (ruling by Ferjolt): each card button says what it
20
+ * adds, so the cost of choosing a card is visible BEFORE it is chosen -
21
+ * the summary line can only ever explain the method already picked.
22
+ */
23
+ total?: number
24
+ currency?: string
16
25
  t: TFunction<'common'>
17
26
  onChange: (action: string) => void
18
27
  }
19
28
 
20
- const Payments = ({ selected, isPartner, routeId, t, onChange }: Props): JSX.Element | null => {
29
+ const Payments = ({ selected, isPartner, routeId, total, currency, t, onChange }: Props): JSX.Element | null => {
21
30
  const { data, isLoading, isSuccess } = useGetPayments(routeId);
22
31
 
32
+ /*
33
+ * What a given card gateway would add to this basket, as a "+1.00 EUR"
34
+ * suffix - or nothing at all when the brand charges no surcharge on it,
35
+ * which is every gateway until somebody configures one.
36
+ */
37
+ const fee = (method: string): string => {
38
+ const value = surchargeOf(total ?? 0, method, data);
39
+
40
+ return value > 0 ? ` +${ value }${ currency ? ` ${ currency }` : '' }` : '';
41
+ };
42
+
23
43
  const available = getAvailablePayments(data, isPartner);
24
44
 
25
45
  useEffect(() => {
@@ -101,35 +121,35 @@ const Payments = ({ selected, isPartner, routeId, t, onChange }: Props): JSX.Ele
101
121
  { (data?.bkt || isPartner) && (
102
122
  <ButtonPayment type="button" $selected={ selected === 'bkt' } onClick={ () => onChange('bkt') }>
103
123
  <AiOutlineCreditCard />
104
- { t('routes_order.step5.actions.card') }
124
+ { t('routes_order.step5.actions.card') }{ fee('bkt') }
105
125
  </ButtonPayment>
106
126
  ) }
107
127
 
108
128
  { (data?.stripe || isPartner) && (
109
129
  <ButtonPayment type="button" $selected={ selected === 'stripe' } onClick={ () => onChange('stripe') }>
110
130
  <AiOutlineCreditCard />
111
- { t('routes_order.step5.actions.card') }
131
+ { t('routes_order.step5.actions.card') }{ fee('stripe') }
112
132
  </ButtonPayment>
113
133
  ) }
114
134
 
115
135
  { (data?.mollie || isPartner) && (
116
136
  <ButtonPayment type="button" $selected={ selected === 'mollie' } onClick={ () => onChange('mollie') }>
117
137
  <AiOutlineCreditCard />
118
- { t('routes_order.step5.actions.card') }
138
+ { t('routes_order.step5.actions.card') }{ fee('mollie') }
119
139
  </ButtonPayment>
120
140
  ) }
121
141
 
122
142
  { (data?.raiffeisen || isPartner) && (
123
143
  <ButtonPayment type="button" $selected={ selected === 'raiffeisen' } onClick={ () => onChange('raiffeisen') }>
124
144
  <AiOutlineCreditCard />
125
- { t('routes_order.step5.actions.card') }
145
+ { t('routes_order.step5.actions.card') }{ fee('raiffeisen') }
126
146
  </ButtonPayment>
127
147
  ) }
128
148
 
129
149
  { (data?.nexi || isPartner) && (
130
150
  <ButtonPayment type="button" $selected={ selected === 'nexi' } onClick={ () => onChange('nexi') }>
131
151
  <AiOutlineCreditCard />
132
- { t('routes_order.step5.actions.card') }
152
+ { t('routes_order.step5.actions.card') }{ fee('nexi') }
133
153
  </ButtonPayment>
134
154
  ) }
135
155
  </ContainerPayments>
@@ -77,3 +77,42 @@ export const getAvailablePayments = (data?: ActionData, isPartner = false): stri
77
77
 
78
78
  return available;
79
79
  };
80
+
81
+ /**
82
+ * The card surcharge on a checkout total, for the method the buyer picked.
83
+ *
84
+ * Claude - 2026-08-29 (ruling by Ferjolt)
85
+ *
86
+ * Settling by card costs us the gateway's cut, and that cut is the payer's
87
+ * rather than ours - "We do not have to support from our fund the payment
88
+ * gateway fees". So the moment a card method is selected, the checkout says
89
+ * what it adds, instead of the buyer meeting a bigger number on the bank's
90
+ * own page.
91
+ *
92
+ * ZERO FOR EVERYTHING THAT IS NOT A CARD. `funds`, `cash`, `reserve` and
93
+ * `complimentary` move no card money, and the map the server sends only ever
94
+ * contains the five gateways - the same allowlist obtapi surcharges on, so
95
+ * an unrecognised method here answers 0 rather than guessing.
96
+ *
97
+ * ROUNDED TO THE CENT, the same round-half-up on the same base the server
98
+ * charges, so the figure the buyer agreed to on this screen is the figure
99
+ * the gateway asks for. Money that disagrees between the two by a cent is
100
+ * a support ticket, and it is the kind nobody can ever reproduce.
101
+ */
102
+ export const surchargeOf = (total: number, method?: string, data?: ActionData): number => {
103
+ if (!method || !data?.surcharges) {
104
+ return 0;
105
+ }
106
+
107
+ const percent = data.surcharges[method as keyof typeof data.surcharges] ?? 0;
108
+
109
+ if (!(percent > 0) || !(total > 0)) {
110
+ return 0;
111
+ }
112
+
113
+ // total * percent / 100, to the cent. Written as ONE rounding of
114
+ // (total * percent) because dividing first introduces a binary
115
+ // fraction the server's own round() never sees - 19.90 at 5% has to
116
+ // land on 1.00 in both languages, not 0.99 in one of them.
117
+ return Math.round(total * percent) / 100;
118
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.33.10",
3
+ "version": "1.34.1",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
package/types.ts CHANGED
@@ -48,6 +48,15 @@ export interface ActionData {
48
48
  mollie: boolean
49
49
  raiffeisen: boolean
50
50
  nexi: boolean
51
+ /**
52
+ * The card surcharge each gateway carries on this brand, as a PERCENTAGE
53
+ * (5 means 5%) - see obtapi Payments\Surcharge.
54
+ *
55
+ * Claude - 2026-08-29 (ruling by Ferjolt). Optional because an older
56
+ * obtapi does not send it, and a brand on a mixed deploy must show no
57
+ * surcharge rather than NaN.
58
+ */
59
+ surcharges?: Partial<Record<'bkt' | 'stripe' | 'mollie' | 'raiffeisen' | 'nexi', number>>
51
60
  }
52
61
 
53
62
  export interface SaveData {