@autobusal/providers 1.22.0 → 1.24.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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.24.0
4
+
5
+ **New `Setup/commerce.ts` — the booking funnel, pushed to the dataLayer.**
6
+
7
+ `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.
8
+
9
+ 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.
10
+
11
+ `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.
12
+
13
+ Each push clears `ecommerce` first; GTM's dataLayer is cumulative, and without that a later event inherits the previous one's items.
14
+
15
+ ## 1.23.0
16
+
17
+ - `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.
18
+
3
19
  ## 1.22.0
4
20
 
5
21
  **Flexible Ticket, and the `policies` rename.**
@@ -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.22.0",
3
+ "version": "1.24.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
@@ -26,6 +26,11 @@ export interface CountryData {
26
26
  available?: number
27
27
  stats?: CountryStats
28
28
 
29
+ // Edited: Ferjolt Ozuni - Date: 2026-08-03
30
+ // IANA zone, so the date picker can floor itself on today WHERE THE COACH
31
+ // LEAVES rather than on our clock or the buyer's.
32
+ timezone?: string | null
33
+
29
34
  // Edited: Ferjolt Ozuni - Date: 2026-07-31
30
35
  // Optional hand-written override for the public country page's title/
31
36
  // description/keywords - see LocationCountry.tsx for the fallback-to-