@autobusal/providers 1.23.0 → 1.26.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
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.26.0
4
+
5
+ ### Added
6
+
7
+ - `FoundData.arrival_offset` — whole days between boarding and arriving, for
8
+ the overnight marker on result cards. Optional: external-provider offers do
9
+ not report one, and a missing value must read as same-day rather than
10
+ invent a day.
11
+
12
+ ## 1.25.0
13
+
14
+ ### Added
15
+
16
+ - `LabelSettings.whatsapp.alert_template_name` / `alert_template_language` —
17
+ the separate approved template the route+date broadcast composer sends
18
+ through. Optional: a brand can confirm purchases over WhatsApp without ever
19
+ broadcasting, and Meta approves a template for a stated purpose, so service
20
+ alerts cannot reuse the confirmation one.
21
+
22
+ ## 1.24.0
23
+
24
+ **New `Setup/commerce.ts` — the booking funnel, pushed to the dataLayer.**
25
+
26
+ `analytics.ts` already loaded the container and announced page views; this is what makes those page views mean something. Until now the funnel was unmeasurable for a more basic reason than missing events — every step of the wizard shared one URL, so no tool could tell a search from a checkout.
27
+
28
+ Uses **GA4's own event names** (`view_item_list`, `select_item`, `begin_checkout`, `add_to_cart`, `remove_from_cart`, `purchase`) so a container wires them by picking from a list rather than needing a custom mapping per brand.
29
+
30
+ `purchase` is the hard one and the reason this is a module rather than a few inline pushes: a card payment leaves the site entirely and returns on a URL carrying nothing but an order hash, and most buyers are guests with no account to read the order back from. So `stash()` remembers the value at order time and `flush()` announces it on return — clearing as it fires, and checking the hash, so neither a refresh nor a stale entry from an abandoned attempt can report a sale.
31
+
32
+ Each push clears `ecommerce` first; GTM's dataLayer is cumulative, and without that a later event inherits the previous one's items.
33
+
3
34
  ## 1.23.0
4
35
 
5
36
  - `CountryData.timezone` — the IANA zone, so the date picker can floor itself on the departure city's date rather than ours or the buyer's. Carried on `/api/cities/browse`, which the search form already loads.
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The booking funnel, pushed to the dataLayer.
3
+ *
4
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
5
+ *
6
+ * `analytics.ts` loads the container and announces page views; this is what
7
+ * makes those page views mean something. Until now the funnel was
8
+ * unmeasurable for a more basic reason than missing events - every step of
9
+ * the wizard shared one URL, so no tool could tell a search from a checkout.
10
+ * That is fixed; this is the other half.
11
+ *
12
+ * GA4'S OWN EVENT NAMES, deliberately. `view_item_list`, `select_item`,
13
+ * `begin_checkout`, `add_to_cart` and `purchase` are wired in GTM by
14
+ * choosing them from a list; anything invented here would need a custom
15
+ * mapping in every brand's container, and nothing in this codebase should
16
+ * know which tags a brand runs.
17
+ *
18
+ * PURCHASE IS THE HARD ONE, and the reason this file exists rather than a
19
+ * few inline pushes. A card payment leaves the site entirely and comes back
20
+ * on a URL carrying nothing but an order hash, and most buyers are guests
21
+ * with no account to look the order up with. So the value is stashed when
22
+ * the order is placed and flushed when the buyer returns - see stash() and
23
+ * flush() below.
24
+ */
25
+
26
+ const push = (payload: Record<string, unknown>): void => {
27
+ window.dataLayer = window.dataLayer || [];
28
+
29
+ // Clear the previous event's ecommerce object first. GTM's dataLayer is
30
+ // cumulative, so without this a later event inherits whatever items the
31
+ // last one left behind - the classic double-counted purchase.
32
+ window.dataLayer.push({ ecommerce: null });
33
+ window.dataLayer.push(payload);
34
+ };
35
+
36
+ export interface CommerceItem {
37
+ item_id: string
38
+ item_name: string
39
+ item_brand?: string
40
+ price?: number
41
+ quantity?: number
42
+ }
43
+
44
+ /** Search results were shown. */
45
+ export const viewItemList = (items: CommerceItem[], currency?: string): void => {
46
+ if (items.length === 0) {
47
+ return;
48
+ }
49
+
50
+ push({ event: 'view_item_list', ecommerce: { currency, items } });
51
+ };
52
+
53
+ /** One coach was chosen from the results. */
54
+ export const selectItem = (item: CommerceItem, currency?: string): void => {
55
+ push({ event: 'select_item', ecommerce: { currency, items: [item] } });
56
+ };
57
+
58
+ /** The buyer reached the passenger-details and payment screen. */
59
+ export const beginCheckout = (value: number, items: CommerceItem[], currency?: string): void => {
60
+ push({ event: 'begin_checkout', ecommerce: { currency, value, items } });
61
+ };
62
+
63
+ /**
64
+ * A paid add-on was ticked or unticked.
65
+ *
66
+ * The attach rate this measures is the single input the Flexible Ticket
67
+ * pricing decision is waiting on - it cannot be banded by window without
68
+ * knowing how often each window is actually bought.
69
+ */
70
+ export const addon = (added: boolean, name: string, price: number, currency?: string): void => {
71
+ push({
72
+ event: added ? 'add_to_cart' : 'remove_from_cart',
73
+ ecommerce: {
74
+ currency,
75
+ value: price,
76
+ items: [{ item_id: name, item_name: name, price, quantity: 1 }]
77
+ }
78
+ });
79
+ };
80
+
81
+ const KEY = 'commerce.purchase';
82
+
83
+ /**
84
+ * Remember what an order was worth, against its hash.
85
+ *
86
+ * sessionStorage rather than a variable: a card payment navigates away to
87
+ * the bank and back, so anything held in memory is gone by the time the
88
+ * buyer returns. Same origin either side, so the value survives.
89
+ */
90
+ export const stash = (hash: string, value: number, items: CommerceItem[], currency?: string): void => {
91
+ try {
92
+ sessionStorage.setItem(KEY, JSON.stringify({ hash, value, items, currency }));
93
+ } catch {
94
+ // storage disabled or full - measurement is never worth breaking a sale
95
+ }
96
+ };
97
+
98
+ /**
99
+ * Announce a completed purchase, once.
100
+ *
101
+ * CLEARED AS IT FIRES. The confirmation page is a plain URL a buyer can
102
+ * refresh, bookmark or reach twice from their history, and each of those
103
+ * would otherwise report another sale. The hash is checked too, so a stale
104
+ * entry from an abandoned attempt cannot be attributed to a later order.
105
+ */
106
+ export const flush = (hash: string): void => {
107
+ let stored: { hash?: string, value?: number, items?: CommerceItem[], currency?: string } | null = null;
108
+
109
+ try {
110
+ const raw = sessionStorage.getItem(KEY);
111
+
112
+ stored = raw ? JSON.parse(raw) : null;
113
+ } catch {
114
+ stored = null;
115
+ }
116
+
117
+ if (!stored || stored.hash !== hash) {
118
+ return;
119
+ }
120
+
121
+ try {
122
+ sessionStorage.removeItem(KEY);
123
+ } catch {
124
+ // if it cannot be cleared, do not fire - a repeatable purchase event is
125
+ // worse than a missing one
126
+ return;
127
+ }
128
+
129
+ push({
130
+ event: 'purchase',
131
+ ecommerce: {
132
+ transaction_id: hash,
133
+ currency: stored.currency,
134
+ value: stored.value,
135
+ items: stored.items ?? []
136
+ }
137
+ });
138
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/providers",
3
- "version": "1.23.0",
3
+ "version": "1.26.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
package/types/routes.ts CHANGED
@@ -146,6 +146,19 @@ export interface FoundData extends Omit<RouteData, 'locations' | 'id'> {
146
146
  // another timezone, and "leaves in 20 minutes" must not be wrong.
147
147
  departs_in?: number | null
148
148
 
149
+ /**
150
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
151
+ * Whole days between boarding and arriving - 0 for a trip that arrives the
152
+ * same day, 1 for one arriving the next morning. Counted server-side from
153
+ * the timetable's own midnight markers, never guessed from the two clock
154
+ * times: a coach leaving 23:00 and arriving 07:00 is indistinguishable
155
+ * from one running backwards unless the timetable says which.
156
+ *
157
+ * Optional because external-provider offers do not report one; the card
158
+ * must read a missing value as same-day rather than invent a day.
159
+ */
160
+ arrival_offset?: number
161
+
149
162
  // The operator's subscription priority, as obtapi already sends it.
150
163
  // Used only as a tiebreak in the "recommended" ordering - see
151
164
  // routes-order's Found/refine.ts. Absent on external-provider offers.
package/types/settings.ts CHANGED
@@ -217,6 +217,16 @@ export interface LabelSettings {
217
217
  access_token?: string
218
218
  template_name?: string
219
219
  template_language?: string
220
+
221
+ /**
222
+ * Edited: Ferjolt Ozuni - Date: 2026-08-05
223
+ * The separate approved template used by the route+date broadcast
224
+ * composer. Optional: a brand can confirm purchases without ever
225
+ * broadcasting, and Meta approves a template for a stated purpose, so
226
+ * service alerts cannot reuse the confirmation one.
227
+ */
228
+ alert_template_name?: string
229
+ alert_template_language?: string
220
230
  display_phone?: string
221
231
  }
222
232