@autobusal/routes-order 1.34.0 → 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/Checkout/Checkout.tsx +34 -2
- package/Checkout/Sidebar/Sidebar.tsx +19 -1
- package/Step5/Payments/Payments.tsx +27 -7
- package/Step5/utilities.ts +39 -0
- package/package.json +1 -1
- package/types.ts +9 -0
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, 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={
|
|
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
|
|
|
@@ -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>
|
package/Step5/utilities.ts
CHANGED
|
@@ -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
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 {
|