@autobusal/routes-order 1.4.2 → 1.6.0

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
@@ -3,6 +3,31 @@
3
3
  All notable changes to this package are documented here. This project follows
4
4
  [Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [1.6.0] - 2026-07-25
7
+
8
+ ### Added
9
+
10
+ - **Checkout addon selection (Step5).** Buyers can now add WhatsApp and/or
11
+ Telegram notification addons at checkout, if the label has them enabled
12
+ (`SettingsData.addons` from `@autobusal/providers`). WhatsApp collects a
13
+ phone number inline; Telegram is a "connect after purchase" flow (a bot
14
+ can't message a bare phone number cold). Selected addons render as line
15
+ items in the order summary and are folded into the grand total shown
16
+ before payment - pricing/availability is re-verified server-side, this is
17
+ only what the buyer requests. `usePostOrder` now takes an `AddonsSelection`
18
+ and posts `addon_whatsapp`/`addon_whatsapp_number`/`addon_telegram`.
19
+
20
+ ## [1.5.0] - 2026-07-25
21
+
22
+ ### Added
23
+
24
+ - **Per-route passenger details in Step4.** The passenger form now renders
25
+ sex, passport, phone, and the new WhatsApp/Telegram contact fields ONLY
26
+ when the route's `trip.passenger_fields` requires them (then required);
27
+ name and date of birth stay always-mandatory. A return trip uses the
28
+ union of both legs' configs. Routes without a config keep the pre-feature
29
+ form (sex + passport). Request field suffixes: `_wa`, `_tg`.
30
+
6
31
  ## [1.4.2] - 2026-07-19
7
32
 
8
33
  ### Added
@@ -10,6 +10,7 @@ interface Props {
10
10
  type: 'adult' | 'child' | 'baby'
11
11
  number: number
12
12
  values?: PersonData
13
+ fields: string[]
13
14
  withReturn: boolean
14
15
  busFrom: OccupiedData
15
16
  busTo: OccupiedData
@@ -22,7 +23,13 @@ interface Props {
22
23
  onUpdate: (name: string, value: (string | number)) => void
23
24
  }
24
25
 
25
- const Passenger = ({ type, number, values, withReturn, busFrom, busTo, pickedFrom, pickedTo, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element => {
26
+ // Edited: Ferjolt Ozuni - Date: 2026-07-25
27
+ // The route now decides which passenger details it collects
28
+ // (routes_trip.passenger_fields): sex, passport, phone, whatsapp and
29
+ // telegram render ONLY when the route requires them - and are then
30
+ // required. Name and date of birth stay mandatory always. `fields` is the
31
+ // union of both legs on a return trip (see Step4).
32
+ const Passenger = ({ type, number, values, fields, withReturn, busFrom, busTo, pickedFrom, pickedTo, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element => {
26
33
  const name = `${ type }${ number }`;
27
34
 
28
35
  const names = {
@@ -32,10 +39,14 @@ const Passenger = ({ type, number, values, withReturn, busFrom, busTo, pickedFro
32
39
  sex: `${ name }_sex`,
33
40
  passport: `${ name }_pass`,
34
41
  phone: `${ name }_phone`,
42
+ whatsapp: `${ name }_wa`,
43
+ telegram: `${ name }_tg`,
35
44
  seatDeparture: `${ name }_sd`,
36
45
  seatReturn: `${ name }_sr`
37
46
  };
38
47
 
48
+ const has = (field: string): boolean => fields.includes(field);
49
+
39
50
  return (
40
51
  <Container className="box">
41
52
  <Title>
@@ -85,45 +96,85 @@ const Passenger = ({ type, number, values, withReturn, busFrom, busTo, pickedFro
85
96
  { Display(errors[names.dob]) }
86
97
  </div>
87
98
 
88
- <div className="row">
89
- { t('routes_order.step4.gender') }
99
+ { has('sex') && (
100
+ <div className="row">
101
+ { t('routes_order.step4.gender') }
90
102
 
91
- <Gender
92
- name={ names.sex }
93
- defaultValue={ values !== undefined ? Number(values[names.sex]) : undefined }
94
- t={ t }
95
- refs={ refs }
96
- />
103
+ <Gender
104
+ name={ names.sex }
105
+ defaultValue={ values !== undefined ? Number(values[names.sex]) : undefined }
106
+ t={ t }
107
+ refs={ refs }
108
+ />
97
109
 
98
- { Display(errors[names.sex]) }
99
- </div>
110
+ { Display(errors[names.sex]) }
111
+ </div>
112
+ ) }
100
113
  </div>
101
-
102
- <div className="column">
103
- <div className="row">
104
- { t('routes_order.step4.passport') }
105
114
 
106
- <input
107
- type="text"
108
- defaultValue={ values !== undefined ? values[names.passport] : '' }
109
- { ...refs(names.passport, Validate('required|min_length:2|max_length:50', t)) }
110
- />
115
+ { (has('passport') || has('phone')) && (
116
+ <div className="column">
117
+ { has('passport') && (
118
+ <div className="row">
119
+ { t('routes_order.step4.passport') }
120
+
121
+ <input
122
+ type="text"
123
+ defaultValue={ values !== undefined ? values[names.passport] : '' }
124
+ { ...refs(names.passport, Validate('required|min_length:2|max_length:50', t)) }
125
+ />
126
+
127
+ { Display(errors[names.passport]) }
128
+ </div>
129
+ ) }
130
+
131
+ { has('phone') && (
132
+ <div className="row">
133
+ { t('routes_order.step4.phone') }
134
+
135
+ <input
136
+ type="text"
137
+ defaultValue={ values !== undefined ? values[names.phone] : '' }
138
+ { ...refs(names.phone, Validate('required|min_length:5|max_length:50', t)) }
139
+ />
111
140
 
112
- { Display(errors[names.passport]) }
141
+ { Display(errors[names.phone]) }
142
+ </div>
143
+ ) }
113
144
  </div>
145
+ ) }
114
146
 
115
- <div className="row">
116
- { t('routes_order.step4.phone') }
147
+ { (has('whatsapp') || has('telegram')) && (
148
+ <div className="column">
149
+ { has('whatsapp') && (
150
+ <div className="row">
151
+ { t('routes_order.step4.whatsapp') }
117
152
 
118
- <input
119
- type="text"
120
- defaultValue={ values !== undefined ? values[names.phone] : '' }
121
- { ...refs(names.phone) }
122
- />
153
+ <input
154
+ type="text"
155
+ defaultValue={ values !== undefined ? values[names.whatsapp] : '' }
156
+ { ...refs(names.whatsapp, Validate('required|min_length:5|max_length:50', t)) }
157
+ />
158
+
159
+ { Display(errors[names.whatsapp]) }
160
+ </div>
161
+ ) }
162
+
163
+ { has('telegram') && (
164
+ <div className="row">
165
+ { t('routes_order.step4.telegram') }
123
166
 
124
- { Display(errors[names.phone]) }
167
+ <input
168
+ type="text"
169
+ defaultValue={ values !== undefined ? values[names.telegram] : '' }
170
+ { ...refs(names.telegram, Validate('required|min_length:2|max_length:100', t)) }
171
+ />
172
+
173
+ { Display(errors[names.telegram]) }
174
+ </div>
175
+ ) }
125
176
  </div>
126
- </div>
177
+ ) }
127
178
 
128
179
  { type !== 'baby' && (
129
180
  <div className="column">
@@ -170,4 +221,4 @@ const Passenger = ({ type, number, values, withReturn, busFrom, busTo, pickedFro
170
221
  );
171
222
  };
172
223
 
173
- export default Passenger;
224
+ export default Passenger;
@@ -7,6 +7,7 @@ import { OccupiedData } from '@autobusal/providers/types/buses';
7
7
  interface Props {
8
8
  type: 'adult' | 'child' | 'baby'
9
9
  amount: number
10
+ fields: string[]
10
11
  withReturn: boolean
11
12
  values?: PersonData
12
13
  busFrom: OccupiedData
@@ -20,7 +21,7 @@ interface Props {
20
21
  onUpdate: (name: string, value: (string | number)) => void
21
22
  }
22
23
 
23
- const Display = ({ type, amount, withReturn, values, busFrom, busTo, pickedFrom, pickedTo, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element[] => {
24
+ const Display = ({ type, amount, fields, withReturn, values, busFrom, busTo, pickedFrom, pickedTo, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element[] => {
24
25
  const passengers: JSX.Element[] = [];
25
26
 
26
27
  for (let i = 1; i <= amount; i++) {
@@ -29,6 +30,7 @@ const Display = ({ type, amount, withReturn, values, busFrom, busTo, pickedFrom,
29
30
  key={ i }
30
31
  type={ type }
31
32
  number={ i }
33
+ fields={ fields }
32
34
  withReturn={ withReturn }
33
35
  values={ values }
34
36
  busFrom={ busFrom }
@@ -10,6 +10,7 @@ import { OccupiedData } from '@autobusal/providers/types/buses';
10
10
  interface Props {
11
11
  values?: PersonData
12
12
  step1: RoutesSearchForm
13
+ fields: string[]
13
14
  busFrom: OccupiedData
14
15
  busTo: OccupiedData
15
16
  t: TFunction<'public'>
@@ -18,7 +19,7 @@ interface Props {
18
19
  onUpdate: (name: string, value: (string | number)) => void
19
20
  }
20
21
 
21
- const Passengers = ({ values, step1, busFrom, busTo, t, errors, refs, onUpdate }: Props): JSX.Element => {
22
+ const Passengers = ({ values, step1, fields, busFrom, busTo, t, errors, refs, onUpdate }: Props): JSX.Element => {
22
23
  const [ pickedFrom, setPickedFrom ] = useState<number[]>([]);
23
24
  const [ pickedTo, setPickedTo ] = useState<number[]>([]);
24
25
 
@@ -45,6 +46,7 @@ const Passengers = ({ values, step1, busFrom, busTo, t, errors, refs, onUpdate }
45
46
  <Display
46
47
  type="adult"
47
48
  amount={ step1.adults }
49
+ fields={ fields }
48
50
  withReturn={ step1.type === 'return' }
49
51
  values={ values }
50
52
  busFrom={ busFrom }
@@ -61,6 +63,7 @@ const Passengers = ({ values, step1, busFrom, busTo, t, errors, refs, onUpdate }
61
63
  <Display
62
64
  type="child"
63
65
  amount={ step1.children }
66
+ fields={ fields }
64
67
  withReturn={ step1.type === 'return' }
65
68
  values={ values }
66
69
  busFrom={ busFrom }
@@ -77,6 +80,7 @@ const Passengers = ({ values, step1, busFrom, busTo, t, errors, refs, onUpdate }
77
80
  <Display
78
81
  type="baby"
79
82
  amount={ step1.babies }
83
+ fields={ fields }
80
84
  withReturn={ step1.type === 'return' }
81
85
  values={ values }
82
86
  busFrom={ busFrom }
package/Step4/Step4.tsx CHANGED
@@ -25,9 +25,21 @@ interface Props {
25
25
  onBack: (step: number) => void
26
26
  }
27
27
 
28
+ // Edited: Ferjolt Ozuni - Date: 2026-07-25
29
+ // Routes without a passenger_fields config keep the pre-feature form
30
+ const DEFAULT_FIELDS = ['sex', 'passport'];
31
+
28
32
  const Step4 = ({ values, step1, step2, step3, isPartner, t, onSave, onBack }: Props): JSX.Element => {
29
33
  const { register, handleSubmit, formState: { errors }, setValue, setError } = useForm<PersonData>();
30
34
 
35
+ // Edited: Ferjolt Ozuni - Date: 2026-07-25
36
+ // one form serves both legs of a return trip, so it collects the UNION of
37
+ // what either route requires (mirrored by the backend rules)
38
+ const fields = Array.from(new Set([
39
+ ...(step2.trip?.passenger_fields ?? DEFAULT_FIELDS),
40
+ ...(step3 !== undefined ? (step3.trip?.passenger_fields ?? DEFAULT_FIELDS) : [])
41
+ ]));
42
+
31
43
  const busFrom = useBusOccupied(step1.departure, step2.id);
32
44
  const busTo = useBusOccupied(step1._return, step3?.id);
33
45
 
@@ -63,6 +75,7 @@ const Step4 = ({ values, step1, step2, step3, isPartner, t, onSave, onBack }: Pr
63
75
  <Passengers
64
76
  values={ values }
65
77
  step1={ step1 }
78
+ fields={ fields }
66
79
  busFrom={ busFrom }
67
80
  busTo={ busTo }
68
81
  t={ t }
@@ -0,0 +1,87 @@
1
+ import { TFunction } from 'i18next';
2
+ import { RiWhatsappLine, RiTelegramLine } from 'react-icons/ri';
3
+ import { SubTitle } from '../../styles';
4
+ import { Container, Option, Notice } from './styles';
5
+
6
+ interface AddonsData {
7
+ whatsapp: { available: boolean, price: number }
8
+ telegram: { available: boolean, price: number }
9
+ }
10
+
11
+ interface Props {
12
+ data?: AddonsData
13
+ currency: string
14
+ whatsapp: boolean
15
+ whatsappNumber: string
16
+ telegram: boolean
17
+ t: TFunction<'common'>
18
+ onChangeWhatsapp: (value: boolean) => void
19
+ onChangeWhatsappNumber: (value: string) => void
20
+ onChangeTelegram: (value: boolean) => void
21
+ }
22
+
23
+ // Edited: Ferjolt Ozuni - Date: 2026-07-25
24
+ // Paid checkout addons - hidden entirely if the label has neither channel
25
+ // enabled (an empty box would be confusing). The WhatsApp addon collects
26
+ // the number here; Telegram is a "connect after purchase" flow (a bot
27
+ // can't message a bare phone number cold), so it's just a checkbox.
28
+ const Addons = ({ data, currency, whatsapp, whatsappNumber, telegram, t, onChangeWhatsapp, onChangeWhatsappNumber, onChangeTelegram }: Props): (JSX.Element | null) => {
29
+ if (!data || (!data.whatsapp.available && !data.telegram.available)) {
30
+ return null;
31
+ }
32
+
33
+ return (
34
+ <Container className="box">
35
+ <SubTitle>
36
+ { t('routes_order.step5.addons.title') }
37
+ </SubTitle>
38
+
39
+ { data.whatsapp.available && (
40
+ <Option>
41
+ <label>
42
+ <input
43
+ type="checkbox"
44
+ checked={ whatsapp }
45
+ onChange={ (event) => onChangeWhatsapp(event.target.checked) }
46
+ />
47
+
48
+ <RiWhatsappLine />
49
+
50
+ { t('routes_order.step5.addons.whatsapp.label') } (+{ data.whatsapp.price } { currency })
51
+ </label>
52
+
53
+ <Notice>{ t('routes_order.step5.addons.whatsapp.description') }</Notice>
54
+
55
+ { whatsapp && (
56
+ <input
57
+ type="text"
58
+ placeholder={ t('routes_order.step5.addons.whatsapp.phone_placeholder') }
59
+ value={ whatsappNumber }
60
+ onChange={ (event) => onChangeWhatsappNumber(event.target.value) }
61
+ />
62
+ ) }
63
+ </Option>
64
+ ) }
65
+
66
+ { data.telegram.available && (
67
+ <Option>
68
+ <label>
69
+ <input
70
+ type="checkbox"
71
+ checked={ telegram }
72
+ onChange={ (event) => onChangeTelegram(event.target.checked) }
73
+ />
74
+
75
+ <RiTelegramLine />
76
+
77
+ { t('routes_order.step5.addons.telegram.label') } (+{ data.telegram.price } { currency })
78
+ </label>
79
+
80
+ <Notice>{ t('routes_order.step5.addons.telegram.description') }</Notice>
81
+ </Option>
82
+ ) }
83
+ </Container>
84
+ );
85
+ };
86
+
87
+ export default Addons;
@@ -0,0 +1,32 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const Container = styled.div`
4
+ margin-top: 25px;
5
+ `;
6
+
7
+ export const Option = styled.div`
8
+ margin-top: 15px;
9
+
10
+ &:first-child {
11
+ margin-top: 0;
12
+ }
13
+
14
+ label {
15
+ display: flex;
16
+ align-items: center;
17
+ gap: 6px;
18
+ cursor: pointer;
19
+ font-weight: 700;
20
+ }
21
+
22
+ input[type="text"] {
23
+ margin-top: 10px;
24
+ }
25
+ `;
26
+
27
+ export const Notice = styled.div`
28
+ margin-top: 4px;
29
+ margin-left: 22px;
30
+ font-size: ${ props => props.theme.size.xs };
31
+ color: ${ props => props.theme.font.faded };
32
+ `;
package/Step5/Step5.tsx CHANGED
@@ -9,6 +9,7 @@ import { Button } from '@autobusal/common';
9
9
  import Steps from '../Steps/Steps';
10
10
  import Summary from './Summary/Summary';
11
11
  import Coupons from './Coupons/Coupons';
12
+ import Addons from './Addons/Addons';
12
13
  import Billing from './Billing/Billing';
13
14
  import Payments from './Payments/Payments';
14
15
  import { getTotal, convertCoupon } from './utilities';
@@ -19,6 +20,7 @@ import { CouponData } from '@autobusal/providers/types/orders';
19
20
  import { RoutesSearchForm } from '@autobusal/routes-search/types';
20
21
  import { TotalData } from '../types';
21
22
  import { useUserStore } from '@autobusal/providers/stores/user';
23
+ import { useGetSettings } from '@autobusal/providers/services';
22
24
  import { usePostOrder } from '../services';
23
25
 
24
26
  interface Props {
@@ -38,13 +40,46 @@ const Step5 = ({ step1, step2, step3, step4, isPartner, t, onBack }: Props): JSX
38
40
  );
39
41
  const [ action, setAction ] = useState<string | undefined>(undefined);
40
42
 
43
+ const [ addonWhatsapp, setAddonWhatsapp ] = useState<boolean>(false);
44
+ const [ addonWhatsappNumber, setAddonWhatsappNumber ] = useState<string>('');
45
+ const [ addonTelegram, setAddonTelegram ] = useState<boolean>(false);
46
+
41
47
  const { data: UserData } = useUserStore();
48
+ const { data: SettingsData } = useGetSettings();
42
49
 
43
50
  const navigate = useNavigate();
44
51
 
45
52
  const { register, handleSubmit, formState: { errors } } = useForm<BillingData>();
46
53
 
47
- const { mutate: OrderSend, isPending } = usePostOrder(step1, step2, step3, step4, coupon, action);
54
+ // Edited: Ferjolt Ozuni - Date: 2026-07-25
55
+ // Paid addons are priced/verified server-side (see obtapi's
56
+ // App\Libraries\Orders\Make\Addons) - this is only for the checkout
57
+ // summary display and the grand total shown before payment.
58
+ const currency = total.display.split(' ')[1] ?? '';
59
+
60
+ const addonItems = [
61
+ addonWhatsapp && SettingsData?.addons?.whatsapp.available ? {
62
+ label: t('routes_order.step5.addons.whatsapp.label'),
63
+ price: SettingsData.addons.whatsapp.price
64
+ } : null,
65
+ addonTelegram && SettingsData?.addons?.telegram.available ? {
66
+ label: t('routes_order.step5.addons.telegram.label'),
67
+ price: SettingsData.addons.telegram.price
68
+ } : null
69
+ ].filter((item): item is { label: string, price: number } => item !== null);
70
+
71
+ const addonsValue = addonItems.reduce((sum, item) => sum + item.price, 0);
72
+
73
+ const grandTotal: TotalData = {
74
+ display: `${ total.value + addonsValue } ${ currency }`,
75
+ value: total.value + addonsValue
76
+ };
77
+
78
+ const { mutate: OrderSend, isPending } = usePostOrder(step1, step2, step3, step4, coupon, action, {
79
+ whatsapp: addonWhatsapp,
80
+ whatsappNumber: addonWhatsappNumber,
81
+ telegram: addonTelegram
82
+ });
48
83
 
49
84
  const onApplyCoupon = (data: CouponData): void => {
50
85
  setTotal(
@@ -59,6 +94,12 @@ const Step5 = ({ step1, step2, step3, step4, isPartner, t, onBack }: Props): JSX
59
94
  }, []);
60
95
 
61
96
  const onSendForm = async (): Promise<void> => {
97
+ if (addonWhatsapp && addonWhatsappNumber.trim().length < 5) {
98
+ alert(t('routes_order.step5.addons.whatsapp.phone_error'));
99
+
100
+ return;
101
+ }
102
+
62
103
  await handleSubmit(onSubmit)();
63
104
  };
64
105
 
@@ -91,12 +132,25 @@ const Step5 = ({ step1, step2, step3, step4, isPartner, t, onBack }: Props): JSX
91
132
  step3={ step3 }
92
133
  step4={ step4 }
93
134
  coupon={ coupon }
94
- total={ total }
135
+ addons={ addonItems.map(item => ({ label: item.label, value: `${ item.price } ${ currency }` })) }
136
+ total={ grandTotal }
95
137
  t={ t }
96
138
  />
97
139
 
98
140
  <Coupons current={ coupon } step2={ step2 } total={ total } t={ t } onApplied={ onApplyCoupon } />
99
141
 
142
+ <Addons
143
+ data={ SettingsData?.addons }
144
+ currency={ currency }
145
+ whatsapp={ addonWhatsapp }
146
+ whatsappNumber={ addonWhatsappNumber }
147
+ telegram={ addonTelegram }
148
+ t={ t }
149
+ onChangeWhatsapp={ setAddonWhatsapp }
150
+ onChangeWhatsappNumber={ setAddonWhatsappNumber }
151
+ onChangeTelegram={ setAddonTelegram }
152
+ />
153
+
100
154
  { UserData === undefined && <Billing t={ t } errors={ errors } refs={ register } /> }
101
155
 
102
156
  <Payments
@@ -2,15 +2,16 @@ import { TFunction } from 'i18next';
2
2
  import { Container, Value, Code } from './styles';
3
3
 
4
4
  interface Props {
5
- type: 'total' | 'discount'
5
+ type: 'total' | 'discount' | 'addon'
6
6
  value: string
7
7
  code?: string
8
+ label?: string
8
9
  t: TFunction<'common'>
9
10
  }
10
11
 
11
- const Amount = ({ type, value, code, t }: Props): JSX.Element => (
12
+ const Amount = ({ type, value, code, label, t }: Props): JSX.Element => (
12
13
  <Container $type={ type }>
13
- { t(`routes_order.step5.summary.${ type }`) }
14
+ { label ?? t(`routes_order.step5.summary.${ type }`) }
14
15
 
15
16
  { code && <Code>{ code }</Code> }
16
17
 
@@ -1,6 +1,6 @@
1
1
  import styled, { css } from 'styled-components';
2
2
 
3
- export const Container = styled.div<{ $type: 'total' | 'discount' }>`
3
+ export const Container = styled.div<{ $type: 'total' | 'discount' | 'addon' }>`
4
4
  display: flex;
5
5
  align-items: center;
6
6
  padding: 10px 15px;
@@ -11,17 +11,23 @@ import { PersonData } from '@autobusal/providers/types/persons';
11
11
  import { CouponData } from '@autobusal/providers/types/orders';
12
12
  import { TotalData } from '../../types';
13
13
 
14
+ interface AddonItem {
15
+ label: string
16
+ value: string
17
+ }
18
+
14
19
  interface Props {
15
20
  step1: RoutesSearchForm
16
21
  step2: FoundData
17
22
  step3?: FoundData
18
23
  step4: PersonData
19
24
  coupon?: CouponData
25
+ addons?: AddonItem[]
20
26
  total: TotalData
21
27
  t: TFunction<'common'>
22
28
  }
23
29
 
24
- const Summary = ({ step1, step2, step3, step4, coupon, total, t }: Props): JSX.Element => (
30
+ const Summary = ({ step1, step2, step3, step4, coupon, addons, total, t }: Props): JSX.Element => (
25
31
  <div className="box">
26
32
  <SubTitle>
27
33
  <AiOutlineInfoCircle />
@@ -35,6 +41,10 @@ const Summary = ({ step1, step2, step3, step4, coupon, total, t }: Props): JSX.E
35
41
 
36
42
  <Tickets step1={ step1 } step4={ step4 } t={ t } />
37
43
 
44
+ { addons?.map(item => (
45
+ <Amount key={ item.label } type="addon" label={ item.label } value={ item.value } t={ t } />
46
+ )) }
47
+
38
48
  { coupon && <Amount type="discount" code={ coupon.code } value={ coupon.discount } t={ t } /> }
39
49
 
40
50
  <Amount type="total" value={ total.display } t={ t } />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.4.2",
3
+ "version": "1.6.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
package/services.ts CHANGED
@@ -4,7 +4,7 @@ import { RoutesSearchForm } from '@autobusal/routes-search/types';
4
4
  import { FoundData } from '@autobusal/providers/types/routes';
5
5
  import { CouponData } from '@autobusal/providers/types/orders';
6
6
  import { BillingData, PersonData } from '@autobusal/providers/types/persons';
7
- import { FoundDay, ActionData, SaveData, OrderedData, CouponForm } from './types';
7
+ import { FoundDay, ActionData, SaveData, OrderedData, CouponForm, AddonsSelection } from './types';
8
8
 
9
9
  export const useGetDepartures = (data: RoutesSearchForm): UseQueryResult<FoundData[]> => (
10
10
  useQuery({
@@ -74,7 +74,8 @@ export const usePostOrder = (
74
74
  step3: (FoundData | undefined),
75
75
  step4: PersonData,
76
76
  coupon: (CouponData | undefined),
77
- action: (string | undefined)
77
+ action: (string | undefined),
78
+ addons: AddonsSelection
78
79
  ): UseMutationResult<OrderedData, Error, BillingData, unknown> => (
79
80
  useMutation({
80
81
  mutationKey: ['save-order'],
@@ -95,6 +96,18 @@ export const usePostOrder = (
95
96
  order.coupon = coupon.code;
96
97
  }
97
98
 
99
+ // Edited: Ferjolt Ozuni - Date: 2026-07-25
100
+ // Paid addons - obtapi re-verifies availability/price server-side
101
+ // against the label's own settings, this is just the buyer's request
102
+ if (addons.whatsapp && addons.whatsappNumber) {
103
+ order.addon_whatsapp = '1';
104
+ order.addon_whatsapp_number = addons.whatsappNumber;
105
+ }
106
+
107
+ if (addons.telegram) {
108
+ order.addon_telegram = '1';
109
+ }
110
+
98
111
  Object.keys(step4).forEach(index => {
99
112
  order[index] = step4[index];
100
113
  });
package/types.ts CHANGED
@@ -33,4 +33,12 @@ export interface OrderedData {
33
33
  export interface TotalData {
34
34
  display: string
35
35
  value: number
36
+ }
37
+
38
+ // Edited: Ferjolt Ozuni - Date: 2026-07-25
39
+ // The buyer's paid-addon selection at checkout - see Step5/Addons/Addons.tsx
40
+ export interface AddonsSelection {
41
+ whatsapp: boolean
42
+ whatsappNumber: string
43
+ telegram: boolean
36
44
  }