@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)
@@ -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
- { t('routes_order.step5.actions.card') }{ fee('bkt') }
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
- { t('routes_order.step5.actions.card') }{ fee('stripe') }
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
- { t('routes_order.step5.actions.card') }{ fee('mollie') }
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
- { t('routes_order.step5.actions.card') }{ fee('raiffeisen') }
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
- { t('routes_order.step5.actions.card') }{ fee('nexi') }
168
+ { cardLabel('nexi') }{ fee('nexi') }
153
169
  </ButtonPayment>
154
170
  ) }
155
171
  </ContainerPayments>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.34.1",
3
+ "version": "1.34.3",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"