@autobusal/routes-order 1.34.0 → 1.34.2
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 +4 -0
- package/Checkout/Checkout.tsx +34 -2
- package/Checkout/Sidebar/Sidebar.tsx +19 -1
- package/Step5/Payments/Payments.tsx +43 -7
- package/Step5/utilities.ts +39 -0
- package/package.json +1 -1
- package/types.ts +9 -0
package/CHANGELOG.md
CHANGED
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,21 +7,57 @@ 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
|
|
|
45
|
+
/*
|
|
46
|
+
* Claude - 2026-08-29 (audit A0-06, medium): a brand with several card
|
|
47
|
+
* gateways rendered N byte-identical "Credit Card" buttons - same icon,
|
|
48
|
+
* same words - so the buyer could not tell the options apart (and could
|
|
49
|
+
* not know which processor's surcharge they were accepting). With more
|
|
50
|
+
* than one card option each button now carries its processor's name;
|
|
51
|
+
* a single card gateway keeps the plain label it always had.
|
|
52
|
+
*/
|
|
53
|
+
const CARD_GATEWAYS = ['bkt', 'stripe', 'mollie', 'raiffeisen', 'nexi'] as const;
|
|
54
|
+
const cardCount = isPartner ? CARD_GATEWAYS.length : CARD_GATEWAYS.filter(gateway => data?.[gateway]).length;
|
|
55
|
+
const CARD_NAMES: Record<string, string> = { bkt: 'BKT', stripe: 'Stripe', mollie: 'Mollie', raiffeisen: 'Raiffeisen', nexi: 'Nexi' };
|
|
56
|
+
const cardLabel = (gateway: string): string => (
|
|
57
|
+
cardCount > 1 ? `${ t('routes_order.step5.actions.card') } (${ CARD_NAMES[gateway] })` : t('routes_order.step5.actions.card')
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
|
|
25
61
|
useEffect(() => {
|
|
26
62
|
if (isSuccess) {
|
|
27
63
|
const found = Object.keys(data).find(item => {
|
|
@@ -101,35 +137,35 @@ const Payments = ({ selected, isPartner, routeId, t, onChange }: Props): JSX.Ele
|
|
|
101
137
|
{ (data?.bkt || isPartner) && (
|
|
102
138
|
<ButtonPayment type="button" $selected={ selected === 'bkt' } onClick={ () => onChange('bkt') }>
|
|
103
139
|
<AiOutlineCreditCard />
|
|
104
|
-
{
|
|
140
|
+
{ cardLabel('bkt') }{ fee('bkt') }
|
|
105
141
|
</ButtonPayment>
|
|
106
142
|
) }
|
|
107
143
|
|
|
108
144
|
{ (data?.stripe || isPartner) && (
|
|
109
145
|
<ButtonPayment type="button" $selected={ selected === 'stripe' } onClick={ () => onChange('stripe') }>
|
|
110
146
|
<AiOutlineCreditCard />
|
|
111
|
-
{
|
|
147
|
+
{ cardLabel('stripe') }{ fee('stripe') }
|
|
112
148
|
</ButtonPayment>
|
|
113
149
|
) }
|
|
114
150
|
|
|
115
151
|
{ (data?.mollie || isPartner) && (
|
|
116
152
|
<ButtonPayment type="button" $selected={ selected === 'mollie' } onClick={ () => onChange('mollie') }>
|
|
117
153
|
<AiOutlineCreditCard />
|
|
118
|
-
{
|
|
154
|
+
{ cardLabel('mollie') }{ fee('mollie') }
|
|
119
155
|
</ButtonPayment>
|
|
120
156
|
) }
|
|
121
157
|
|
|
122
158
|
{ (data?.raiffeisen || isPartner) && (
|
|
123
159
|
<ButtonPayment type="button" $selected={ selected === 'raiffeisen' } onClick={ () => onChange('raiffeisen') }>
|
|
124
160
|
<AiOutlineCreditCard />
|
|
125
|
-
{
|
|
161
|
+
{ cardLabel('raiffeisen') }{ fee('raiffeisen') }
|
|
126
162
|
</ButtonPayment>
|
|
127
163
|
) }
|
|
128
164
|
|
|
129
165
|
{ (data?.nexi || isPartner) && (
|
|
130
166
|
<ButtonPayment type="button" $selected={ selected === 'nexi' } onClick={ () => onChange('nexi') }>
|
|
131
167
|
<AiOutlineCreditCard />
|
|
132
|
-
{
|
|
168
|
+
{ cardLabel('nexi') }{ fee('nexi') }
|
|
133
169
|
</ButtonPayment>
|
|
134
170
|
) }
|
|
135
171
|
</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 {
|