@autobusal/routes-order 1.34.1 → 1.34.3
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,3 +1,11 @@
|
|
|
1
|
+
## 1.34.3 - 2026-08-29
|
|
2
|
+
|
|
3
|
+
- Staff sellers (agent/subagent/operator/employee/admin) get an OPTIONAL passenger email box at checkout, so a counter-sold ticket reaches the passenger instead of only the seller and operator (audit A4-05, owner decision). obtapi already read {type}{i}_email/_notify_email; it now validates them too.
|
|
4
|
+
|
|
5
|
+
## 1.34.2 - 2026-08-29
|
|
6
|
+
|
|
7
|
+
- With more than one card gateway available, each Credit Card button carries its processor's name - they used to render byte-identical (audit A0-06).
|
|
8
|
+
|
|
1
9
|
# Changelog
|
|
2
10
|
|
|
3
11
|
## 1.34.0 (2026-08-29)
|
package/Checkout/Checkout.tsx
CHANGED
|
@@ -442,6 +442,14 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
|
|
|
442
442
|
values={ values }
|
|
443
443
|
step1={ step1 }
|
|
444
444
|
fields={ fields }
|
|
445
|
+
/*
|
|
446
|
+
* Claude - 2026-08-29 (audit A4-05, owner decision): only staff
|
|
447
|
+
* are asked. They are the sellers with no billing block - see
|
|
448
|
+
* `gaps` above - so this optional box is the only way a
|
|
449
|
+
* counter-sold ticket reaches the passenger directly instead of
|
|
450
|
+
* landing solely with the agent and the operator.
|
|
451
|
+
*/
|
|
452
|
+
askEmail={ UserData !== undefined && ['agent', 'subagent', 'operator', 'employee', 'admin'].includes(UserData.type) }
|
|
445
453
|
busFrom={ busFrom }
|
|
446
454
|
busTo={ busTo }
|
|
447
455
|
saved={ saved }
|
|
@@ -15,6 +15,15 @@ interface Props {
|
|
|
15
15
|
number: number
|
|
16
16
|
values?: PersonData
|
|
17
17
|
fields: string[]
|
|
18
|
+
/**
|
|
19
|
+
* Claude - 2026-08-29 (audit A4-05, owner decision): staff selling at the
|
|
20
|
+
* counter get an OPTIONAL email box per passenger. They are the only
|
|
21
|
+
* sellers with no billing block (an agent or operator writes no
|
|
22
|
+
* orders_contact row), so without it a walk-in customer's ticket reached
|
|
23
|
+
* only the agent and the operator and had to be Re-Sent by hand. Optional
|
|
24
|
+
* on purpose: a counter sale often has no email to give.
|
|
25
|
+
*/
|
|
26
|
+
askEmail?: boolean
|
|
18
27
|
withReturn: boolean
|
|
19
28
|
busFrom: OccupiedData
|
|
20
29
|
busTo: OccupiedData
|
|
@@ -47,7 +56,7 @@ interface Props {
|
|
|
47
56
|
// telegram render ONLY when the route requires them - and are then
|
|
48
57
|
// required. Name and date of birth stay mandatory always. `fields` is the
|
|
49
58
|
// union of both legs on a return trip (see Step4).
|
|
50
|
-
const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, pickedFrom, pickedTo, saved, savedMax, seatsOptional, seatsFee, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element => {
|
|
59
|
+
const Passenger = ({ type, number, values, fields, askEmail, withReturn, busFrom, busTo, pickedFrom, pickedTo, saved, savedMax, seatsOptional, seatsFee, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element => {
|
|
51
60
|
const name = `${ type }${ number }`;
|
|
52
61
|
|
|
53
62
|
const names = {
|
|
@@ -65,7 +74,13 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
65
74
|
// Edited: Claude - Date: 2026-08-20
|
|
66
75
|
// "Remember this passenger" - obtapi reads `{type}{i}_save` alongside
|
|
67
76
|
// the `_fn` / `_pass` fields this form already posts.
|
|
68
|
-
save: `${ name }_save
|
|
77
|
+
save: `${ name }_save`,
|
|
78
|
+
|
|
79
|
+
// the counter-sale email (audit A4-05). obtapi already reads
|
|
80
|
+
// `{type}{i}_email` + `_notify_email` - see Orders\Make\Tickets - it
|
|
81
|
+
// was only the web form that never offered them.
|
|
82
|
+
email: `${ name }_email`,
|
|
83
|
+
notifyEmail: `${ name }_notify_email`
|
|
69
84
|
};
|
|
70
85
|
|
|
71
86
|
const has = (field: string): boolean => fields.includes(field);
|
|
@@ -340,6 +355,31 @@ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, p
|
|
|
340
355
|
</div>
|
|
341
356
|
) }
|
|
342
357
|
|
|
358
|
+
{ askEmail && (
|
|
359
|
+
<div className="column">
|
|
360
|
+
<div className="row">
|
|
361
|
+
<label htmlFor={ id(names.email) }>{ t('routes_order.step4.email') }</label>
|
|
362
|
+
|
|
363
|
+
<input
|
|
364
|
+
type="email"
|
|
365
|
+
key={ `${ names.email }-${ applied }` }
|
|
366
|
+
id={ id(names.email) }
|
|
367
|
+
defaultValue={ start(names.email, undefined) }
|
|
368
|
+
{ ...refs(names.email, Validate('email|max_length:255', t)) }
|
|
369
|
+
/>
|
|
370
|
+
|
|
371
|
+
{ Display(errors[names.email]) }
|
|
372
|
+
|
|
373
|
+
{ /* the ticket goes to this address as well as to the seller -
|
|
374
|
+
obtapi gates the per-passenger copy on `_notify_email`, so
|
|
375
|
+
an address typed here has to arrive with the flag set */ }
|
|
376
|
+
<input type="hidden" value="1" { ...refs(names.notifyEmail) } />
|
|
377
|
+
|
|
378
|
+
<Notice>{ t('routes_order.step4.email_notice') }</Notice>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
) }
|
|
382
|
+
|
|
343
383
|
{ (has('whatsapp') || has('telegram')) && (
|
|
344
384
|
<div className="column">
|
|
345
385
|
{ has('whatsapp') && (
|
|
@@ -8,6 +8,8 @@ interface Props {
|
|
|
8
8
|
type: 'adult' | 'child' | 'baby'
|
|
9
9
|
amount: number
|
|
10
10
|
fields: string[]
|
|
11
|
+
/** staff sellers are offered an optional passenger email (audit A4-05) */
|
|
12
|
+
askEmail?: boolean
|
|
11
13
|
withReturn: boolean
|
|
12
14
|
values?: PersonData
|
|
13
15
|
busFrom: OccupiedData
|
|
@@ -25,7 +27,7 @@ interface Props {
|
|
|
25
27
|
onUpdate: (name: string, value: (string | number)) => void
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
const Display = ({ type, amount, fields, withReturn, values, busFrom, busTo, pickedFrom, pickedTo, saved, savedMax, seatsOptional, seatsFee, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element[] => {
|
|
30
|
+
const Display = ({ type, amount, fields, askEmail, withReturn, values, busFrom, busTo, pickedFrom, pickedTo, saved, savedMax, seatsOptional, seatsFee, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element[] => {
|
|
29
31
|
const passengers: JSX.Element[] = [];
|
|
30
32
|
|
|
31
33
|
for (let i = 1; i <= amount; i++) {
|
|
@@ -35,6 +37,7 @@ const Display = ({ type, amount, fields, withReturn, values, busFrom, busTo, pic
|
|
|
35
37
|
type={ type }
|
|
36
38
|
number={ i }
|
|
37
39
|
fields={ fields }
|
|
40
|
+
askEmail={ askEmail }
|
|
38
41
|
withReturn={ withReturn }
|
|
39
42
|
values={ values }
|
|
40
43
|
busFrom={ busFrom }
|
|
@@ -11,6 +11,8 @@ interface Props {
|
|
|
11
11
|
values?: PersonData
|
|
12
12
|
step1: RoutesSearchForm
|
|
13
13
|
fields: string[]
|
|
14
|
+
/** staff sellers are offered an optional passenger email (audit A4-05) */
|
|
15
|
+
askEmail?: boolean
|
|
14
16
|
busFrom: OccupiedData
|
|
15
17
|
busTo: OccupiedData
|
|
16
18
|
/**
|
|
@@ -38,7 +40,7 @@ interface Props {
|
|
|
38
40
|
onUpdate: (name: string, value: (string | number)) => void
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
const Passengers = ({ values, step1, fields, busFrom, busTo, saved, savedMax, seatsOptional, seatsFee, onPickedCount, t, errors, refs, onUpdate }: Props): JSX.Element => {
|
|
43
|
+
const Passengers = ({ values, step1, fields, askEmail, busFrom, busTo, saved, savedMax, seatsOptional, seatsFee, onPickedCount, t, errors, refs, onUpdate }: Props): JSX.Element => {
|
|
42
44
|
const [ pickedFrom, setPickedFrom ] = useState<number[]>([]);
|
|
43
45
|
const [ pickedTo, setPickedTo ] = useState<number[]>([]);
|
|
44
46
|
|
|
@@ -84,6 +86,7 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, saved, savedMax, se
|
|
|
84
86
|
type="adult"
|
|
85
87
|
amount={ step1.adults }
|
|
86
88
|
fields={ fields }
|
|
89
|
+
askEmail={ askEmail }
|
|
87
90
|
withReturn={ step1.type === 'return' }
|
|
88
91
|
values={ values }
|
|
89
92
|
busFrom={ busFrom }
|
|
@@ -105,6 +108,7 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, saved, savedMax, se
|
|
|
105
108
|
type="child"
|
|
106
109
|
amount={ step1.children }
|
|
107
110
|
fields={ fields }
|
|
111
|
+
askEmail={ askEmail }
|
|
108
112
|
withReturn={ step1.type === 'return' }
|
|
109
113
|
values={ values }
|
|
110
114
|
busFrom={ busFrom }
|
|
@@ -126,6 +130,7 @@ const Passengers = ({ values, step1, fields, busFrom, busTo, saved, savedMax, se
|
|
|
126
130
|
type="baby"
|
|
127
131
|
amount={ step1.babies }
|
|
128
132
|
fields={ fields }
|
|
133
|
+
askEmail={ askEmail }
|
|
129
134
|
withReturn={ step1.type === 'return' }
|
|
130
135
|
values={ values }
|
|
131
136
|
busFrom={ busFrom }
|
|
@@ -42,6 +42,22 @@ const Payments = ({ selected, isPartner, routeId, total, currency, t, onChange }
|
|
|
42
42
|
|
|
43
43
|
const available = getAvailablePayments(data, isPartner);
|
|
44
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
|
+
|
|
45
61
|
useEffect(() => {
|
|
46
62
|
if (isSuccess) {
|
|
47
63
|
const found = Object.keys(data).find(item => {
|
|
@@ -121,35 +137,35 @@ const Payments = ({ selected, isPartner, routeId, total, currency, t, onChange }
|
|
|
121
137
|
{ (data?.bkt || isPartner) && (
|
|
122
138
|
<ButtonPayment type="button" $selected={ selected === 'bkt' } onClick={ () => onChange('bkt') }>
|
|
123
139
|
<AiOutlineCreditCard />
|
|
124
|
-
{
|
|
140
|
+
{ cardLabel('bkt') }{ fee('bkt') }
|
|
125
141
|
</ButtonPayment>
|
|
126
142
|
) }
|
|
127
143
|
|
|
128
144
|
{ (data?.stripe || isPartner) && (
|
|
129
145
|
<ButtonPayment type="button" $selected={ selected === 'stripe' } onClick={ () => onChange('stripe') }>
|
|
130
146
|
<AiOutlineCreditCard />
|
|
131
|
-
{
|
|
147
|
+
{ cardLabel('stripe') }{ fee('stripe') }
|
|
132
148
|
</ButtonPayment>
|
|
133
149
|
) }
|
|
134
150
|
|
|
135
151
|
{ (data?.mollie || isPartner) && (
|
|
136
152
|
<ButtonPayment type="button" $selected={ selected === 'mollie' } onClick={ () => onChange('mollie') }>
|
|
137
153
|
<AiOutlineCreditCard />
|
|
138
|
-
{
|
|
154
|
+
{ cardLabel('mollie') }{ fee('mollie') }
|
|
139
155
|
</ButtonPayment>
|
|
140
156
|
) }
|
|
141
157
|
|
|
142
158
|
{ (data?.raiffeisen || isPartner) && (
|
|
143
159
|
<ButtonPayment type="button" $selected={ selected === 'raiffeisen' } onClick={ () => onChange('raiffeisen') }>
|
|
144
160
|
<AiOutlineCreditCard />
|
|
145
|
-
{
|
|
161
|
+
{ cardLabel('raiffeisen') }{ fee('raiffeisen') }
|
|
146
162
|
</ButtonPayment>
|
|
147
163
|
) }
|
|
148
164
|
|
|
149
165
|
{ (data?.nexi || isPartner) && (
|
|
150
166
|
<ButtonPayment type="button" $selected={ selected === 'nexi' } onClick={ () => onChange('nexi') }>
|
|
151
167
|
<AiOutlineCreditCard />
|
|
152
|
-
{
|
|
168
|
+
{ cardLabel('nexi') }{ fee('nexi') }
|
|
153
169
|
</ButtonPayment>
|
|
154
170
|
) }
|
|
155
171
|
</ContainerPayments>
|