@autobusal/routes-order 1.34.5 → 1.35.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.
@@ -27,7 +27,7 @@ import { useUserStore } from '@autobusal/providers/stores/user';
27
27
  import { useGetSettings } from '@autobusal/providers/services';
28
28
  import { usePostOrder, useGetFlex, useGetPayments, useGetSavedPassengers, useGetComission } from '../services';
29
29
  import { addon as commerceAddon, beginCheckout, stash } from '@autobusal/providers/Setup/commerce';
30
- import { item as commerceItem } from '../commerce';
30
+ import { item as commerceItem, isCounterSale } from '../commerce';
31
31
  import { error as notifyError } from '@autobusal/utilities';
32
32
 
33
33
  interface Props {
@@ -572,7 +572,11 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
572
572
  * counter-sold ticket reaches the passenger directly instead of
573
573
  * landing solely with the agent and the operator.
574
574
  */
575
- askEmail={ UserData !== undefined && ['agent', 'subagent', 'operator', 'employee', 'admin'].includes(UserData.type) }
575
+ askEmail={ isCounterSale(UserData?.type) }
576
+ /* same population, second reason: a walk-in often has no number
577
+ to give either, and a clerk forced to fill the box types a
578
+ placeholder that looks real to everyone downstream */
579
+ phoneOptional={ isCounterSale(UserData?.type) }
576
580
  busFrom={ busFrom }
577
581
  busTo={ busTo }
578
582
  saved={ saved }
@@ -654,7 +658,18 @@ const Checkout = ({ values, step1, step2, step3, isPartner, t, onStore, onBack }
654
658
  />
655
659
  ) }
656
660
  >
657
- <Coupons current={ coupon } step2={ step2 } total={ total } t={ t } onApplied={ onApplyCoupon } />
661
+ {/* Claude - 2026-08-30 (raised by Ferjolt: "why should an operator
662
+ have a coupon code while they do not pay for their own
663
+ tickets?"). A complimentary ticket costs nothing, so there is no
664
+ total for a discount to come off - the box was asking for a code
665
+ that could not do anything.
666
+
667
+ Only the FREE case is hidden, not coupons for operators. On
668
+ another operator's route an operator is an ordinary paying buyer
669
+ and a coupon applies to them like anyone else. */}
670
+ { action !== 'complimentary' && (
671
+ <Coupons current={ coupon } step2={ step2 } total={ total } t={ t } onApplied={ onApplyCoupon } />
672
+ ) }
658
673
  </Sidebar>
659
674
  </Results>
660
675
 
package/Found/Found.tsx CHANGED
@@ -182,7 +182,9 @@ const Found = ({ type, loading, data, step1, preferredStop, t, onSearch, onSave
182
182
  ) }
183
183
 
184
184
  <Listing>
185
- <Header t={ t } />
185
+ {/* the same refinement the Sort dropdown drives, so the two stay
186
+ in step and the URL keeps carrying the choice (refine.ts) */}
187
+ <Header sort={ refinement.sort } t={ t } onSort={ sort => onRefine({ ...refinement, sort }) } />
186
188
 
187
189
  <Orders
188
190
  loading={ loading }
@@ -2,20 +2,81 @@ import { TFunction } from 'i18next';
2
2
  import { RiBus2Line } from 'react-icons/ri';
3
3
  import { BiArrowFromLeft, BiHourglass } from 'react-icons/bi';
4
4
  import { FaFlagCheckered, FaCoins } from 'react-icons/fa';
5
- import { Container, Company, Location, Time, Price } from './styles';
5
+ import { SortKey } from '../refine';
6
+ import { Container, Company, Location, Time, Price, Direction } from './styles';
6
7
 
7
8
  interface Props {
9
+ sort: SortKey
8
10
  t: TFunction<'public'>
11
+ onSort: (sort: SortKey) => void
9
12
  }
10
13
 
11
- const Header = ({ t }: Props): JSX.Element => (
12
- <Container className="box">
13
- <Company><RiBus2Line /> { t('routes_order.step2.header.company') }</Company>
14
- <Location><BiArrowFromLeft /> { t('routes_order.step2.header.departs') }</Location>
15
- <Time><BiHourglass /> { t('routes_order.step2.header.duration') }</Time>
16
- <Location><FaFlagCheckered /> { t('routes_order.step2.header.arrives') }</Location>
17
- <Price><FaCoins /> { t('routes_order.step2.header.price') }</Price>
18
- </Container>
19
- );
20
-
21
- export default Header;
14
+ /**
15
+ * Claude - 2026-08-30 (asked for by Ferjolt): "the results header should be
16
+ * sortable on click".
17
+ *
18
+ * Each column owns a PAIR of sorts and clicking it walks between them, which
19
+ * is what a header that can only sort one way gets wrong - the second click
20
+ * looks broken. Landing on a column always starts with the direction people
21
+ * actually want (cheapest, fastest, earliest, first to arrive, A-Z); the
22
+ * reverse is only ever reached deliberately.
23
+ *
24
+ * `recommended` is not in any pair on purpose. It is the merit ordering the
25
+ * page opens with and it belongs to no single column, so clicking a header
26
+ * always leaves it - which is correct, the user just asked for something
27
+ * more specific.
28
+ */
29
+ const COLUMNS: Record<string, [SortKey, SortKey]> = {
30
+ company: [ 'company', 'company_desc' ],
31
+ departs: [ 'earliest', 'latest' ],
32
+ duration: [ 'fastest', 'slowest' ],
33
+ arrives: [ 'arrives_first', 'arrives_last' ],
34
+ price: [ 'cheapest', 'dearest' ]
35
+ };
36
+
37
+ const Header = ({ sort, t, onSort }: Props): JSX.Element => {
38
+ const next = (column: keyof typeof COLUMNS): SortKey => {
39
+ const [ first, second ] = COLUMNS[column];
40
+
41
+ // already on this column - flip it; otherwise start at its natural end
42
+ return sort === first ? second : first;
43
+ };
44
+
45
+ // the arrow shows only on the column in effect, so it says WHICH one it is
46
+ const arrow = (column: keyof typeof COLUMNS): (JSX.Element | null) => {
47
+ const [ first, second ] = COLUMNS[column];
48
+
49
+ if (sort !== first && sort !== second) {
50
+ return null;
51
+ }
52
+
53
+ return <Direction>{ sort === first ? '▲' : '▼' }</Direction>;
54
+ };
55
+
56
+
57
+ return (
58
+ <Container className="box">
59
+ <Company type="button" onClick={ () => onSort(next('company')) }>
60
+ <RiBus2Line /> { t('routes_order.step2.header.company') } { arrow('company') }
61
+ </Company>
62
+
63
+ <Location type="button" onClick={ () => onSort(next('departs')) }>
64
+ <BiArrowFromLeft /> { t('routes_order.step2.header.departs') } { arrow('departs') }
65
+ </Location>
66
+
67
+ <Time type="button" onClick={ () => onSort(next('duration')) }>
68
+ <BiHourglass /> { t('routes_order.step2.header.duration') } { arrow('duration') }
69
+ </Time>
70
+
71
+ <Location type="button" onClick={ () => onSort(next('arrives')) }>
72
+ <FaFlagCheckered /> { t('routes_order.step2.header.arrives') } { arrow('arrives') }
73
+ </Location>
74
+
75
+ <Price type="button" onClick={ () => onSort(next('price')) }>
76
+ <FaCoins /> { t('routes_order.step2.header.price') } { arrow('price') }
77
+ </Price>
78
+ </Container>
79
+ );
80
+ };
81
+
82
+ export default Header;
@@ -23,7 +23,16 @@ export const Container = styled.div`
23
23
  }
24
24
  `;
25
25
 
26
- const Item = styled.div`
26
+ /**
27
+ * Claude - 2026-08-30 (asked for by Ferjolt): each column sorts the list on
28
+ * click, so these are real <button>s - keyboard reachable and announced as
29
+ * controls, which a styled <div> with an onClick is not.
30
+ *
31
+ * `background: none; border: 0` because a button carries a chrome of its own
32
+ * that would otherwise break a row whose whole job is to line up invisibly
33
+ * over the cards below it. The geometry is unchanged.
34
+ */
35
+ const Item = styled.button`
27
36
  display: flex;
28
37
  flex-direction: column;
29
38
  gap: 4px;
@@ -31,11 +40,32 @@ const Item = styled.div`
31
40
  align-items: center;
32
41
  font-size: ${ props => props.theme.size.s };
33
42
 
43
+ background: none;
44
+ border: 0;
45
+ padding: 0;
46
+ font-family: inherit;
47
+ color: inherit;
48
+ cursor: pointer;
49
+
50
+ &:hover, &:focus-visible {
51
+ color: ${ props => props.theme.font.info };
52
+ }
53
+
34
54
  & > svg {
35
55
  font-size: ${ props => props.theme.size.xxl };
36
56
  }
37
57
  `;
38
58
 
59
+ /**
60
+ * The arrow marking the column the list is currently sorted by, and which
61
+ * way. Only ever rendered on the active column - an arrow on every header
62
+ * says nothing about which one is in effect.
63
+ */
64
+ export const Direction = styled.span`
65
+ font-size: ${ props => props.theme.size.xs };
66
+ line-height: 1;
67
+ `;
68
+
39
69
  // these five must stay identical to their counterparts in
40
70
  // Found/Route/styles.ts, or the labels stop sitting over their columns
41
71
  export const Company = styled(Item)`
package/Found/refine.ts CHANGED
@@ -18,12 +18,37 @@ import { FoundData } from '@autobusal/providers/types/routes';
18
18
  * answer this question" as a pass, not a fail.
19
19
  */
20
20
 
21
- export type SortKey = 'recommended' | 'cheapest' | 'fastest' | 'earliest' | 'latest';
21
+ /**
22
+ * Claude - 2026-08-30 (asked for by Ferjolt): the results header is
23
+ * clickable, and a column header that sorts one way only is a header people
24
+ * click twice and think is broken. So every axis the header offers now has
25
+ * both directions.
26
+ *
27
+ * SORTS below is deliberately NOT extended - it drives the sort dropdown,
28
+ * and "Dearest first" is not something to offer as a considered choice. The
29
+ * reverse directions exist to serve a second click on a column, nothing else.
30
+ */
31
+ export type SortKey = 'recommended' | 'cheapest' | 'fastest' | 'earliest' | 'latest'
32
+ | 'dearest' | 'slowest' | 'arrives_first' | 'arrives_last' | 'company' | 'company_desc';
22
33
 
23
34
  export type BucketKey = '0' | '6' | '12' | '18';
24
35
 
25
36
  export const SORTS: SortKey[] = [ 'recommended', 'cheapest', 'fastest', 'earliest', 'latest' ];
26
37
 
38
+ /**
39
+ * Every sort the URL may legally carry - the dropdown's five plus the
40
+ * reverse directions the column headers reach.
41
+ *
42
+ * THIS IS A SEPARATE LIST ON PURPOSE, and getting it wrong is not visible
43
+ * from the code. Validation used to run against SORTS, so `?sort=dearest`
44
+ * parsed straight back to 'recommended': the header highlighted, the URL
45
+ * updated, and the list did not move. Found by clicking Price twice in a
46
+ * browser - a typecheck cannot see it, because both lists are SortKey[].
47
+ */
48
+ export const VALID_SORTS: SortKey[] = [
49
+ ...SORTS, 'dearest', 'slowest', 'arrives_first', 'arrives_last', 'company', 'company_desc'
50
+ ];
51
+
27
52
  export const BUCKETS: BucketKey[] = [ '0', '6', '12', '18' ];
28
53
 
29
54
  export interface Refinement {
@@ -82,6 +107,25 @@ export const departureOf = (item: FoundData): number => (
82
107
  minutesOf(item.locations.from.departure)
83
108
  );
84
109
 
110
+ /**
111
+ * Arrival as minutes from the DEPARTURE day's midnight, so an overnight trip
112
+ * landing at 06:00 tomorrow sorts after one landing at 23:00 tonight rather
113
+ * than before it. `arrival_offset` is the midnight count the timetable
114
+ * already carries - reading the clock face alone is exactly the mistake that
115
+ * made overnight routes look like the earliest arrivals on the page.
116
+ */
117
+ export const arrivalOf = (item: FoundData): number => (
118
+ minutesOf(item.locations.to.arrival) + (item.arrival_offset ?? 0) * 1440
119
+ );
120
+
121
+ /**
122
+ * Operator name, case-insensitive and locale-aware, so 'Ç' lands where an
123
+ * Albanian reader expects rather than after 'Z'.
124
+ */
125
+ const byCompany = (a: FoundData, b: FoundData): number => (
126
+ (a.operator?.name ?? '').localeCompare(b.operator?.name ?? '', undefined, { sensitivity: 'base' })
127
+ );
128
+
85
129
  /**
86
130
  * Trip length in minutes, or null when obtapi could not compute one.
87
131
  *
@@ -208,7 +252,7 @@ export const fromParams = (params: URLSearchParams): Refinement => {
208
252
  const maxPrice = Number(params.get('max_price'));
209
253
 
210
254
  return {
211
- sort: sort && SORTS.includes(sort) ? sort : 'recommended',
255
+ sort: sort && VALID_SORTS.includes(sort) ? sort : 'recommended',
212
256
  operators: numbers('operators'),
213
257
  buckets: (params.get('times') ?? '').split(',').filter(value => BUCKETS.includes(value as BucketKey)) as BucketKey[],
214
258
  direct: params.get('direct') === '1',
@@ -351,6 +395,31 @@ const byDuration = (a: FoundData, b: FoundData): number => {
351
395
  return first - second;
352
396
  };
353
397
 
398
+ /**
399
+ * Longest first - but a route with NO computable duration still sinks to the
400
+ * bottom. Negating byDuration would have floated those to the top instead,
401
+ * which is the same "-" sentinel bug byDuration exists to prevent, just at
402
+ * the other end of the list.
403
+ */
404
+ const byDurationDesc = (a: FoundData, b: FoundData): number => {
405
+ const first = durationOf(a);
406
+ const second = durationOf(b);
407
+
408
+ if (first === null && second === null) {
409
+ return 0;
410
+ }
411
+
412
+ if (first === null) {
413
+ return 1;
414
+ }
415
+
416
+ if (second === null) {
417
+ return -1;
418
+ }
419
+
420
+ return second - first;
421
+ };
422
+
354
423
  /**
355
424
  * Edited: Ferjolt Ozuni - Date: 2026-08-01
356
425
  *
@@ -373,6 +442,25 @@ const compare = (sort: SortKey) => (a: FoundData, b: FoundData): number => {
373
442
  case 'fastest':
374
443
  return byDuration(a, b) || (a.price.value - b.price.value);
375
444
 
445
+ // the reverse directions, reached by clicking a column header twice
446
+ case 'dearest':
447
+ return (b.price.value - a.price.value) || byDuration(a, b);
448
+
449
+ case 'slowest':
450
+ return byDurationDesc(a, b) || (a.price.value - b.price.value);
451
+
452
+ case 'arrives_first':
453
+ return (arrivalOf(a) - arrivalOf(b)) || (a.price.value - b.price.value);
454
+
455
+ case 'arrives_last':
456
+ return (arrivalOf(b) - arrivalOf(a)) || (a.price.value - b.price.value);
457
+
458
+ case 'company':
459
+ return byCompany(a, b) || (a.price.value - b.price.value);
460
+
461
+ case 'company_desc':
462
+ return -byCompany(a, b) || (a.price.value - b.price.value);
463
+
376
464
  default:
377
465
  return 0;
378
466
  }
@@ -24,6 +24,16 @@ interface Props {
24
24
  * on purpose: a counter sale often has no email to give.
25
25
  */
26
26
  askEmail?: boolean
27
+ /**
28
+ * Claude - 2026-08-30 (ruled by Ferjolt): the same counter sale may leave
29
+ * the passenger PHONE blank too. A walk-in often has no number to give,
30
+ * and a clerk forced to fill the box types a placeholder - which is worse
31
+ * than a blank, because it looks like a real number to everyone
32
+ * downstream. obtapi relaxes the matching rule for exactly this set of
33
+ * roles (RulesGeneral::COUNTER_SELLERS); if the two ever disagree the form
34
+ * stops asking for something the server still rejects.
35
+ */
36
+ phoneOptional?: boolean
27
37
  withReturn: boolean
28
38
  busFrom: OccupiedData
29
39
  busTo: OccupiedData
@@ -56,7 +66,7 @@ interface Props {
56
66
  // telegram render ONLY when the route requires them - and are then
57
67
  // required. Name and date of birth stay mandatory always. `fields` is the
58
68
  // union of both legs on a return trip (see Step4).
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 => {
69
+ const Passenger = ({ type, number, values, fields, askEmail, phoneOptional, withReturn, busFrom, busTo, pickedFrom, pickedTo, saved, savedMax, seatsOptional, seatsFee, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element => {
60
70
  const name = `${ type }${ number }`;
61
71
 
62
72
  const names = {
@@ -331,14 +341,16 @@ const Passenger = ({ type, number, values, fields, askEmail, withReturn, busFrom
331
341
 
332
342
  { has('phone') && (
333
343
  <div className="row">
334
- <Required htmlFor={ id(names.phone) }>{ t('routes_order.step4.phone') }</Required>
344
+ { phoneOptional
345
+ ? <label htmlFor={ id(names.phone) }>{ t('routes_order.step4.phone') }</label>
346
+ : <Required htmlFor={ id(names.phone) }>{ t('routes_order.step4.phone') }</Required> }
335
347
 
336
348
  <input
337
349
  type="text"
338
350
  key={ `${ names.phone }-${ applied }` }
339
351
  id={ id(names.phone) }
340
352
  defaultValue={ start(names.phone, record?.phone) }
341
- { ...refs(names.phone, Validate('required|min_length:5|max_length:50', t)) }
353
+ { ...refs(names.phone, Validate(phoneOptional ? 'min_length:5|max_length:50' : 'required|min_length:5|max_length:50', t)) }
342
354
  />
343
355
 
344
356
  { Display(errors[names.phone]) }
@@ -10,6 +10,7 @@ interface Props {
10
10
  fields: string[]
11
11
  /** staff sellers are offered an optional passenger email (audit A4-05) */
12
12
  askEmail?: boolean
13
+ phoneOptional?: boolean
13
14
  withReturn: boolean
14
15
  values?: PersonData
15
16
  busFrom: OccupiedData
@@ -27,7 +28,7 @@ interface Props {
27
28
  onUpdate: (name: string, value: (string | number)) => void
28
29
  }
29
30
 
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[] => {
31
+ const Display = ({ type, amount, fields, askEmail, phoneOptional, withReturn, values, busFrom, busTo, pickedFrom, pickedTo, saved, savedMax, seatsOptional, seatsFee, t, errors, refs, onSeatSelect, onUpdate }: Props): JSX.Element[] => {
31
32
  const passengers: JSX.Element[] = [];
32
33
 
33
34
  for (let i = 1; i <= amount; i++) {
@@ -38,6 +39,7 @@ const Display = ({ type, amount, fields, askEmail, withReturn, values, busFrom,
38
39
  number={ i }
39
40
  fields={ fields }
40
41
  askEmail={ askEmail }
42
+ phoneOptional={ phoneOptional }
41
43
  withReturn={ withReturn }
42
44
  values={ values }
43
45
  busFrom={ busFrom }
@@ -13,6 +13,7 @@ interface Props {
13
13
  fields: string[]
14
14
  /** staff sellers are offered an optional passenger email (audit A4-05) */
15
15
  askEmail?: boolean
16
+ phoneOptional?: boolean
16
17
  busFrom: OccupiedData
17
18
  busTo: OccupiedData
18
19
  /**
@@ -40,7 +41,7 @@ interface Props {
40
41
  onUpdate: (name: string, value: (string | number)) => void
41
42
  }
42
43
 
43
- const Passengers = ({ values, step1, fields, askEmail, busFrom, busTo, saved, savedMax, seatsOptional, seatsFee, onPickedCount, t, errors, refs, onUpdate }: Props): JSX.Element => {
44
+ const Passengers = ({ values, step1, fields, askEmail, phoneOptional, busFrom, busTo, saved, savedMax, seatsOptional, seatsFee, onPickedCount, t, errors, refs, onUpdate }: Props): JSX.Element => {
44
45
  const [ pickedFrom, setPickedFrom ] = useState<number[]>([]);
45
46
  const [ pickedTo, setPickedTo ] = useState<number[]>([]);
46
47
 
@@ -87,6 +88,7 @@ const Passengers = ({ values, step1, fields, askEmail, busFrom, busTo, saved, sa
87
88
  amount={ step1.adults }
88
89
  fields={ fields }
89
90
  askEmail={ askEmail }
91
+ phoneOptional={ phoneOptional }
90
92
  withReturn={ step1.type === 'return' }
91
93
  values={ values }
92
94
  busFrom={ busFrom }
@@ -109,6 +111,7 @@ const Passengers = ({ values, step1, fields, askEmail, busFrom, busTo, saved, sa
109
111
  amount={ step1.children }
110
112
  fields={ fields }
111
113
  askEmail={ askEmail }
114
+ phoneOptional={ phoneOptional }
112
115
  withReturn={ step1.type === 'return' }
113
116
  values={ values }
114
117
  busFrom={ busFrom }
@@ -131,6 +134,7 @@ const Passengers = ({ values, step1, fields, askEmail, busFrom, busTo, saved, sa
131
134
  amount={ step1.babies }
132
135
  fields={ fields }
133
136
  askEmail={ askEmail }
137
+ phoneOptional={ phoneOptional }
134
138
  withReturn={ step1.type === 'return' }
135
139
  values={ values }
136
140
  busFrom={ busFrom }
@@ -66,11 +66,31 @@ const Payments = ({ selected, isPartner, routeId, total, currency, t, onChange }
66
66
 
67
67
  useEffect(() => {
68
68
  if (isSuccess) {
69
- const found = Object.keys(data).find(item => {
70
- return data[item as keyof typeof data] === true;
71
- });
69
+ /*
70
+ * Claude - 2026-08-30 (raised by Ferjolt: "why should an operator have
71
+ * to pay for its own routes?").
72
+ *
73
+ * IT NEVER HAD TO - BUT THE CHECKOUT PRESELECTED A PAID METHOD ANYWAY.
74
+ * This used to take the first key in the server's object that was
75
+ * true, and `funds` is written before `complimentary`, so an operator
76
+ * opening a ticket on their OWN route arrived with Funds (Online)
77
+ * already highlighted. One click on Make Payment and they had bought a
78
+ * seat on their own coach - the free option was sitting right there,
79
+ * looking like just another of four equal tiles.
80
+ *
81
+ * `complimentary` is only ever offered when the route really does
82
+ * belong to this operator (Actions::get re-checks that against the
83
+ * real order row, never the request), so whenever it is on the list it
84
+ * is the right default. Ruling BOOK-07: "Operators do not pay anything
85
+ * for their own routes - we do not charge them."
86
+ *
87
+ * The paid methods stay on the list on purpose: an operator also SELLS
88
+ * seats on its own routes, and those are real sales that have to be
89
+ * settled and recorded.
90
+ */
91
+ const available = Object.keys(data).filter(item => data[item as keyof typeof data] === true);
92
+ const found = available.includes('complimentary') ? 'complimentary' : available[0];
72
93
 
73
- // we assign the first available action
74
94
  onChange(found ?? '');
75
95
  }
76
96
  }, [isSuccess, data, onChange]);
package/commerce.ts CHANGED
@@ -22,3 +22,23 @@ export const item = (route: FoundData): CommerceItem => ({
22
22
  quantity: 1
23
23
  });
24
24
 
25
+
26
+ /**
27
+ * Staff selling over a counter, as opposed to a customer buying for
28
+ * themselves.
29
+ *
30
+ * Claude - 2026-08-30. This list was written inline at the askEmail prop and
31
+ * is now needed twice - the optional passenger email and, from today, the
32
+ * optional passenger phone. Two copies of it would drift, and drifting here
33
+ * is not a cosmetic bug: the form would stop requiring a field the server
34
+ * still rejects, so the sale fails at submit with an error pointing at a box
35
+ * the seller was told they could leave blank.
36
+ *
37
+ * Must stay in step with RulesGeneral::COUNTER_SELLERS in obtapi, which is
38
+ * the half that actually decides.
39
+ */
40
+ export const COUNTER_SELLERS = [ 'agent', 'subagent', 'operator', 'employee', 'admin' ];
41
+
42
+ export const isCounterSale = (type?: string): boolean => (
43
+ type !== undefined && COUNTER_SELLERS.includes(type)
44
+ );
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@autobusal/routes-order",
3
- "version": "1.34.5",
3
+ "version": "1.35.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
- "main": "index.ts"
6
+ "main": "index.ts",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ }
7
10
  }