@session-services/web-elements 0.1.20 → 0.1.22
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/dist/cjs/{index-C6e98Atu.js → index-BYBgrATL.js} +1 -0
- package/dist/cjs/loader.cjs.js +2 -2
- package/dist/cjs/session-services-elements.cjs.js +2 -2
- package/dist/cjs/session-services-formatted-text.cjs.entry.js +181 -165
- package/dist/cjs/session-services-ticketing.cjs.entry.js +24370 -19296
- package/dist/collection/components/session-services-formatted-text/session-services-formatted-text.js +33 -34
- package/dist/collection/components/session-services-ticketing/session-services-ticketing.css +429 -340
- package/dist/collection/components/session-services-ticketing/session-services-ticketing.js +1090 -269
- package/dist/collection/components/session-services-ticketing/session-services-ticketing.logic.js +270 -0
- package/dist/collection/components/session-services-ticketing/session-services-ticketing.logic.test.js +368 -0
- package/dist/collection/components/session-services-ticketing/session-services-ticketing.test.js +264 -0
- package/dist/components/session-services-formatted-text.js +180 -164
- package/dist/components/session-services-ticketing.js +24458 -19366
- package/dist/esm/{index-CeXOR4aK.js → index-CNOpF6tk.js} +1 -1
- package/dist/esm/loader.js +3 -3
- package/dist/esm/session-services-elements.js +3 -3
- package/dist/esm/session-services-formatted-text.entry.js +181 -165
- package/dist/esm/session-services-ticketing.entry.js +24378 -19304
- package/dist/session-services-elements/{p-a2404c46.entry.js → p-703444ed.entry.js} +1 -1
- package/dist/session-services-elements/p-94173d1b.entry.js +1 -0
- package/dist/session-services-elements/p-CNOpF6tk.js +2 -0
- package/dist/session-services-elements/session-services-elements.esm.js +1 -1
- package/dist/sst-env.d.ts +1 -0
- package/dist/types/components/session-services-formatted-text/session-services-formatted-text.d.ts +3 -0
- package/dist/types/components/session-services-ticketing/session-services-ticketing.d.ts +117 -3
- package/dist/types/components/session-services-ticketing/session-services-ticketing.logic.d.ts +196 -0
- package/dist/types/components/session-services-ticketing/session-services-ticketing.logic.test.d.ts +1 -0
- package/dist/types/components/session-services-ticketing/session-services-ticketing.test.d.ts +1 -0
- package/dist/types/components.d.ts +18 -0
- package/package.json +8 -7
- package/dist/session-services-elements/p-3bb6ceef.entry.js +0 -1
- package/dist/session-services-elements/p-CeXOR4aK.js +0 -2
package/dist/collection/components/session-services-ticketing/session-services-ticketing.logic.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { formatNaiveDateTime, isNaiveDateTimeFuture, isNaiveDateTimePast, } from "@session-services/utils/naive-datetime";
|
|
2
|
+
import { createQuoteItemMap } from "@session-services/utils/ticket-quote";
|
|
3
|
+
import { getAdmissionSelectionCapacity, isAdmissionSoldOut, } from "@session-services/utils/ticket-availability";
|
|
4
|
+
const UNLIMITED_CAP = 10;
|
|
5
|
+
const SCARCITY_THRESHOLD = 20;
|
|
6
|
+
export function selectionKey(admissionId, sectionId) {
|
|
7
|
+
return `${admissionId}::${sectionId || "none"}`;
|
|
8
|
+
}
|
|
9
|
+
export function parseSelectionKey(key) {
|
|
10
|
+
const [admissionId, sectionId] = key.split("::");
|
|
11
|
+
return {
|
|
12
|
+
admissionId,
|
|
13
|
+
sectionId: sectionId === "none" ? undefined : sectionId,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function priceTotal(price) {
|
|
17
|
+
if (!price)
|
|
18
|
+
return 0;
|
|
19
|
+
if ("total" in price && typeof price.total === "number")
|
|
20
|
+
return price.total;
|
|
21
|
+
return (price.base ?? 0) + (price.booking ?? 0);
|
|
22
|
+
}
|
|
23
|
+
export function discountCouponFromSearch(search) {
|
|
24
|
+
return new URLSearchParams(search).get("discount") ?? "";
|
|
25
|
+
}
|
|
26
|
+
export function eventTimeZone(event) {
|
|
27
|
+
return event.location?.timeZone ?? "UTC";
|
|
28
|
+
}
|
|
29
|
+
function eventEnded(event) {
|
|
30
|
+
return isNaiveDateTimePast(event.entryEndsAt, eventTimeZone(event));
|
|
31
|
+
}
|
|
32
|
+
function sectionsById(event) {
|
|
33
|
+
return new Map(event.sections.map((section) => [section.id, section]));
|
|
34
|
+
}
|
|
35
|
+
function hasAdmissionSection(admission, sections) {
|
|
36
|
+
if (!admission.sectionId)
|
|
37
|
+
return true;
|
|
38
|
+
return sections.has(admission.sectionId);
|
|
39
|
+
}
|
|
40
|
+
function admissionSection(admission, sections) {
|
|
41
|
+
if (!admission.sectionId)
|
|
42
|
+
return undefined;
|
|
43
|
+
return sections.get(admission.sectionId);
|
|
44
|
+
}
|
|
45
|
+
function isAdmissionOnSale(admission, zone) {
|
|
46
|
+
if (isNaiveDateTimeFuture(admission.saleStartsAt, zone))
|
|
47
|
+
return false;
|
|
48
|
+
return !isNaiveDateTimePast(admission.saleEndsAt, zone);
|
|
49
|
+
}
|
|
50
|
+
function hasAdmissionCapacity(admission, section) {
|
|
51
|
+
return getAdmissionSelectionCapacity({ admission, section }) > 0;
|
|
52
|
+
}
|
|
53
|
+
function isAdmissionPurchasable(admission, zone, sections) {
|
|
54
|
+
if (!hasAdmissionSection(admission, sections))
|
|
55
|
+
return false;
|
|
56
|
+
if (!isAdmissionOnSale(admission, zone))
|
|
57
|
+
return false;
|
|
58
|
+
return hasAdmissionCapacity(admission, admissionSection(admission, sections));
|
|
59
|
+
}
|
|
60
|
+
function hasPurchasableAdmissions(event) {
|
|
61
|
+
const sections = sectionsById(event);
|
|
62
|
+
const zone = eventTimeZone(event);
|
|
63
|
+
return event.admissions.some((admission) => isAdmissionPurchasable(admission, zone, sections));
|
|
64
|
+
}
|
|
65
|
+
function admissionSaleLabel(admission, zone) {
|
|
66
|
+
if (!isNaiveDateTimeFuture(admission.saleStartsAt, zone)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
text: `On sale ${formatNaiveDateTime(admission.saleStartsAt, "EEE, MMM d · h:mm a")}`,
|
|
71
|
+
tone: "muted",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function admissionStatus(input) {
|
|
75
|
+
if (input.value === "DOOR" && (input.soldOut || input.saleEnded)) {
|
|
76
|
+
return { text: "Available At Door", tone: "success" };
|
|
77
|
+
}
|
|
78
|
+
if (input.soldOut)
|
|
79
|
+
return { text: "Sold Out", tone: "destructive" };
|
|
80
|
+
if (!input.saleEnded)
|
|
81
|
+
return null;
|
|
82
|
+
return { text: "Sale Ended", tone: "destructive" };
|
|
83
|
+
}
|
|
84
|
+
function endedStatus(admissions, zone) {
|
|
85
|
+
const ended = admissions.filter((admission) => isNaiveDateTimePast(admission.saleEndsAt, zone));
|
|
86
|
+
if (ended.length === 0)
|
|
87
|
+
return undefined;
|
|
88
|
+
if (ended.some((admission) => admission.saleEndsBehavior === "DOOR"))
|
|
89
|
+
return "DOOR";
|
|
90
|
+
if (ended.every((admission) => admission.saleEndsBehavior === "SOLD_OUT"))
|
|
91
|
+
return "SOLD_OUT";
|
|
92
|
+
return "SALE_ENDED";
|
|
93
|
+
}
|
|
94
|
+
function salesStatus(event) {
|
|
95
|
+
const sections = sectionsById(event);
|
|
96
|
+
const admissions = event.admissions.filter((admission) => admission.visibility !== "HIDDEN" &&
|
|
97
|
+
hasAdmissionSection(admission, sections));
|
|
98
|
+
if (admissions.length === 0)
|
|
99
|
+
return undefined;
|
|
100
|
+
if (hasPurchasableAdmissions(event))
|
|
101
|
+
return "ONLINE";
|
|
102
|
+
return endedStatus(admissions, eventTimeZone(event));
|
|
103
|
+
}
|
|
104
|
+
function hasDisplayableAdmissions(event) {
|
|
105
|
+
const sections = sectionsById(event);
|
|
106
|
+
return event.admissions.some((admission) => hasAdmissionSection(admission, sections));
|
|
107
|
+
}
|
|
108
|
+
function isDoorOnly(event) {
|
|
109
|
+
if (hasPurchasableAdmissions(event))
|
|
110
|
+
return false;
|
|
111
|
+
return salesStatus(event) === "DOOR";
|
|
112
|
+
}
|
|
113
|
+
export function getTicketingState(event) {
|
|
114
|
+
const ended = eventEnded(event);
|
|
115
|
+
const externalTicketUrl = event.url || undefined;
|
|
116
|
+
const doorOnly = isDoorOnly(event);
|
|
117
|
+
const showExternalTicketing = !ended && Boolean(externalTicketUrl);
|
|
118
|
+
const showTicketing = !ended && !showExternalTicketing && hasDisplayableAdmissions(event);
|
|
119
|
+
return {
|
|
120
|
+
externalTicketUrl,
|
|
121
|
+
isDoorOnly: doorOnly,
|
|
122
|
+
isEnded: ended,
|
|
123
|
+
isUnavailableForSale: !doorOnly && !hasPurchasableAdmissions(event) && showTicketing,
|
|
124
|
+
showExternalTicketing,
|
|
125
|
+
showTicketing,
|
|
126
|
+
ticketHeading: doorOnly ? "Tickets at the Door" : "Get Tickets",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export function visibleAdmissions(event, mode) {
|
|
130
|
+
if (mode !== "door")
|
|
131
|
+
return event.admissions;
|
|
132
|
+
const zone = eventTimeZone(event);
|
|
133
|
+
return event.admissions.filter((admission) => admission.saleEndsBehavior === "DOOR" &&
|
|
134
|
+
isNaiveDateTimePast(admission.saleEndsAt, zone));
|
|
135
|
+
}
|
|
136
|
+
function sectionItems(sections, admissions) {
|
|
137
|
+
return sections
|
|
138
|
+
.map((section) => ({
|
|
139
|
+
type: "section",
|
|
140
|
+
section,
|
|
141
|
+
admissions: admissions
|
|
142
|
+
.filter((admission) => admission.sectionId === section.id)
|
|
143
|
+
.sort((a, b) => a.order - b.order),
|
|
144
|
+
}))
|
|
145
|
+
.filter((item) => item.admissions.length > 0);
|
|
146
|
+
}
|
|
147
|
+
function topLevelOrder(item) {
|
|
148
|
+
if (item.type === "section")
|
|
149
|
+
return item.section.order;
|
|
150
|
+
return item.admission.order;
|
|
151
|
+
}
|
|
152
|
+
export function topLevelTicketItems(sections, admissions) {
|
|
153
|
+
return [
|
|
154
|
+
...sectionItems(sections, admissions),
|
|
155
|
+
...admissions
|
|
156
|
+
.filter((admission) => !admission.sectionId)
|
|
157
|
+
.map((admission) => ({ type: "admission", admission })),
|
|
158
|
+
].sort((a, b) => topLevelOrder(a) - topLevelOrder(b));
|
|
159
|
+
}
|
|
160
|
+
function isUnlimitedAdmission(admission, section) {
|
|
161
|
+
if ((admission.available ?? 0) !== -1)
|
|
162
|
+
return false;
|
|
163
|
+
if (admission.capacity !== -1)
|
|
164
|
+
return false;
|
|
165
|
+
return !section || (section.available ?? 0) === -1;
|
|
166
|
+
}
|
|
167
|
+
function maxQuantity(admission, section) {
|
|
168
|
+
if (isUnlimitedAdmission(admission, section))
|
|
169
|
+
return UNLIMITED_CAP;
|
|
170
|
+
return Math.min(getAdmissionSelectionCapacity({ admission, section }), UNLIMITED_CAP);
|
|
171
|
+
}
|
|
172
|
+
function showScarcity(admission, soldOut, disabled) {
|
|
173
|
+
return [
|
|
174
|
+
!soldOut,
|
|
175
|
+
!disabled,
|
|
176
|
+
(admission.available ?? 0) > 0,
|
|
177
|
+
(admission.available ?? 0) < SCARCITY_THRESHOLD,
|
|
178
|
+
].every(Boolean);
|
|
179
|
+
}
|
|
180
|
+
export function getAdmissionDisplayState(input) {
|
|
181
|
+
const zone = eventTimeZone(input.event);
|
|
182
|
+
const saleNotStarted = isNaiveDateTimeFuture(input.admission.saleStartsAt, zone);
|
|
183
|
+
const saleEnded = isNaiveDateTimePast(input.admission.saleEndsAt, zone);
|
|
184
|
+
const saleEndedSoldOut = saleEnded && input.admission.saleEndsBehavior === "SOLD_OUT";
|
|
185
|
+
const soldOut = Boolean(input.sectionSoldOut) ||
|
|
186
|
+
saleEndedSoldOut ||
|
|
187
|
+
isAdmissionSoldOut({
|
|
188
|
+
admission: input.admission,
|
|
189
|
+
section: input.section,
|
|
190
|
+
});
|
|
191
|
+
const disabled = soldOut || saleNotStarted || saleEnded;
|
|
192
|
+
const status = admissionStatus({
|
|
193
|
+
saleEnded,
|
|
194
|
+
soldOut,
|
|
195
|
+
value: input.admission.saleEndsBehavior,
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
disabled,
|
|
199
|
+
label: admissionSaleLabel(input.admission, zone),
|
|
200
|
+
maxQuantity: maxQuantity(input.admission, input.section),
|
|
201
|
+
postSale: saleEnded,
|
|
202
|
+
showScarcity: showScarcity(input.admission, soldOut, disabled),
|
|
203
|
+
soldOut,
|
|
204
|
+
statusLabel: status?.text ?? null,
|
|
205
|
+
statusTone: status?.tone ?? null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function quoteItemUnits(item) {
|
|
209
|
+
return Math.max(item?.quantity ?? 1, 1);
|
|
210
|
+
}
|
|
211
|
+
export function getAdmissionPriceDisplay(input) {
|
|
212
|
+
const item = input.quoteItem ?? input.previewItem;
|
|
213
|
+
if (item) {
|
|
214
|
+
const units = quoteItemUnits(item);
|
|
215
|
+
const base = Math.round(Math.max((item.price.base + item.price.discount) / units, 0));
|
|
216
|
+
const fees = Math.round(item.price.booking / units);
|
|
217
|
+
if (!item.price.discount || item.price.discount >= 0) {
|
|
218
|
+
return {
|
|
219
|
+
base,
|
|
220
|
+
discounted: false,
|
|
221
|
+
fees,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
base,
|
|
226
|
+
discounted: true,
|
|
227
|
+
fees,
|
|
228
|
+
gross: Math.round(item.price.base / units),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (input.initialPrice && input.initialPrice.discount < 0) {
|
|
232
|
+
const fees = input.admission.price.booking ?? 0;
|
|
233
|
+
return {
|
|
234
|
+
base: Math.max(input.initialPrice.total - fees, 0),
|
|
235
|
+
discounted: true,
|
|
236
|
+
fees,
|
|
237
|
+
gross: Math.max(input.initialPrice.gross - fees, 0),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
base: input.admission.price.base ?? 0,
|
|
242
|
+
discounted: false,
|
|
243
|
+
fees: input.admission.price.booking ?? 0,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
export function quoteItemsBySelection(quote) {
|
|
247
|
+
return createQuoteItemMap(quote);
|
|
248
|
+
}
|
|
249
|
+
export function admissionPriceSavings(price) {
|
|
250
|
+
if (!price.discounted || price.gross === undefined)
|
|
251
|
+
return 0;
|
|
252
|
+
return Math.max(price.gross - price.base, 0);
|
|
253
|
+
}
|
|
254
|
+
export function previewQuoteItems(event) {
|
|
255
|
+
const ids = new Set(event.sections.map((section) => section.id));
|
|
256
|
+
const zone = eventTimeZone(event);
|
|
257
|
+
return event.admissions
|
|
258
|
+
.filter((admission) => {
|
|
259
|
+
if (admission.sectionId && !ids.has(admission.sectionId))
|
|
260
|
+
return false;
|
|
261
|
+
if (isNaiveDateTimeFuture(admission.saleStartsAt, zone))
|
|
262
|
+
return false;
|
|
263
|
+
return !isNaiveDateTimePast(admission.saleEndsAt, zone);
|
|
264
|
+
})
|
|
265
|
+
.map((admission) => ({
|
|
266
|
+
admissionId: admission.id ?? "",
|
|
267
|
+
quantity: 1,
|
|
268
|
+
sectionId: admission.sectionId ?? undefined,
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { admissionPriceSavings, discountCouponFromSearch, getAdmissionDisplayState, getAdmissionPriceDisplay, getTicketingState, topLevelTicketItems, visibleAdmissions, } from "./session-services-ticketing.logic";
|
|
3
|
+
const futureStart = "2030-01-01T20:00:00";
|
|
4
|
+
const futureEnd = "2030-01-01T23:00:00";
|
|
5
|
+
const pastStart = "2020-01-01T20:00:00";
|
|
6
|
+
const pastEnd = "2020-01-01T23:00:00";
|
|
7
|
+
function admission(data = {}) {
|
|
8
|
+
return {
|
|
9
|
+
id: "adm_general",
|
|
10
|
+
name: "General Admission",
|
|
11
|
+
capacity: 100,
|
|
12
|
+
available: 100,
|
|
13
|
+
sold: 0,
|
|
14
|
+
reserved: 0,
|
|
15
|
+
limit: 10,
|
|
16
|
+
order: 1,
|
|
17
|
+
entryStartsAt: futureStart,
|
|
18
|
+
entryEndsAt: futureEnd,
|
|
19
|
+
saleStartsAt: pastStart,
|
|
20
|
+
saleEndsAt: futureEnd,
|
|
21
|
+
saleEndsBehavior: "SALE_ENDED",
|
|
22
|
+
price: {
|
|
23
|
+
base: 2000,
|
|
24
|
+
booking: 200,
|
|
25
|
+
currency: "AUD",
|
|
26
|
+
},
|
|
27
|
+
visibility: "ALWAYS",
|
|
28
|
+
teamId: "team_1",
|
|
29
|
+
promoterId: "pro_1",
|
|
30
|
+
eventId: "evt_1",
|
|
31
|
+
eventSlug: "event",
|
|
32
|
+
...data,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function section(data = {}) {
|
|
36
|
+
return {
|
|
37
|
+
id: "sec_1",
|
|
38
|
+
name: "Main Room",
|
|
39
|
+
capacity: 100,
|
|
40
|
+
available: 100,
|
|
41
|
+
sold: 0,
|
|
42
|
+
reserved: 0,
|
|
43
|
+
limit: 10,
|
|
44
|
+
order: 2,
|
|
45
|
+
teamId: "team_1",
|
|
46
|
+
promoterId: "pro_1",
|
|
47
|
+
eventId: "evt_1",
|
|
48
|
+
eventSlug: "event",
|
|
49
|
+
location: {
|
|
50
|
+
id: "loc_1",
|
|
51
|
+
name: "Club Room",
|
|
52
|
+
address: "1 Test St",
|
|
53
|
+
city: "Sydney",
|
|
54
|
+
country: "AU",
|
|
55
|
+
latitude: -33.86,
|
|
56
|
+
longitude: 151.2,
|
|
57
|
+
timeZone: "Australia/Sydney",
|
|
58
|
+
createdAt: pastStart,
|
|
59
|
+
updatedAt: pastStart,
|
|
60
|
+
},
|
|
61
|
+
...data,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function event(data = {}) {
|
|
65
|
+
return {
|
|
66
|
+
id: "evt_1",
|
|
67
|
+
name: "Test Event",
|
|
68
|
+
slug: "event",
|
|
69
|
+
currency: "AUD",
|
|
70
|
+
entryStartsAt: futureStart,
|
|
71
|
+
entryEndsAt: futureEnd,
|
|
72
|
+
publishAt: pastStart,
|
|
73
|
+
updatedAt: pastStart,
|
|
74
|
+
status: "UPCOMING",
|
|
75
|
+
visibility: "PUBLIC",
|
|
76
|
+
approval: "APPROVED",
|
|
77
|
+
teamId: "team_1",
|
|
78
|
+
promoterId: "pro_1",
|
|
79
|
+
admissions: [admission()],
|
|
80
|
+
sections: [],
|
|
81
|
+
images: {},
|
|
82
|
+
location: {
|
|
83
|
+
id: "loc_1",
|
|
84
|
+
name: "Club",
|
|
85
|
+
address: "1 Test St",
|
|
86
|
+
city: "Sydney",
|
|
87
|
+
country: "AU",
|
|
88
|
+
latitude: -33.86,
|
|
89
|
+
longitude: 151.2,
|
|
90
|
+
timeZone: "Australia/Sydney",
|
|
91
|
+
createdAt: pastStart,
|
|
92
|
+
updatedAt: pastStart,
|
|
93
|
+
},
|
|
94
|
+
...data,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function quoteItem(data = {}) {
|
|
98
|
+
return {
|
|
99
|
+
id: "item_1",
|
|
100
|
+
kind: "ADMISSION",
|
|
101
|
+
admissionId: "adm_general",
|
|
102
|
+
quantity: 1,
|
|
103
|
+
price: {
|
|
104
|
+
base: 2000,
|
|
105
|
+
booking: 200,
|
|
106
|
+
currency: "AUD",
|
|
107
|
+
discount: -1000,
|
|
108
|
+
gross: 2200,
|
|
109
|
+
net: 1200,
|
|
110
|
+
refunded: 0,
|
|
111
|
+
total: 1200,
|
|
112
|
+
},
|
|
113
|
+
...data,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
describe("session-services-ticketing logic", () => {
|
|
117
|
+
it("reads discount coupon codes from query strings", () => {
|
|
118
|
+
expect(discountCouponFromSearch("?discount=save20")).toBe("save20");
|
|
119
|
+
expect(discountCouponFromSearch("?utm_source=test&discount=SAVE20")).toBe("SAVE20");
|
|
120
|
+
expect(discountCouponFromSearch("?coupon=SAVE20")).toBe("");
|
|
121
|
+
});
|
|
122
|
+
it("shows external ticketing instead of internal selection when an external URL exists", () => {
|
|
123
|
+
const state = getTicketingState(event({ url: "https://tickets.example" }));
|
|
124
|
+
expect(state.showExternalTicketing).toBe(true);
|
|
125
|
+
expect(state.showTicketing).toBe(false);
|
|
126
|
+
expect(state.externalTicketUrl).toBe("https://tickets.example");
|
|
127
|
+
});
|
|
128
|
+
it("detects door-only events and filters to door admissions", () => {
|
|
129
|
+
const door = admission({
|
|
130
|
+
id: "adm_door",
|
|
131
|
+
saleEndsAt: pastEnd,
|
|
132
|
+
saleEndsBehavior: "DOOR",
|
|
133
|
+
});
|
|
134
|
+
const ended = admission({
|
|
135
|
+
id: "adm_ended",
|
|
136
|
+
saleEndsAt: pastEnd,
|
|
137
|
+
saleEndsBehavior: "SALE_ENDED",
|
|
138
|
+
});
|
|
139
|
+
const data = event({ admissions: [door, ended] });
|
|
140
|
+
expect(getTicketingState(data).isDoorOnly).toBe(true);
|
|
141
|
+
expect(visibleAdmissions(data, "door").map((item) => item.id)).toEqual([
|
|
142
|
+
"adm_door",
|
|
143
|
+
]);
|
|
144
|
+
});
|
|
145
|
+
it("disables admissions outside their online sale window", () => {
|
|
146
|
+
const data = event({
|
|
147
|
+
admissions: [
|
|
148
|
+
admission({
|
|
149
|
+
saleStartsAt: futureStart,
|
|
150
|
+
saleEndsAt: "2030-01-02T20:00:00",
|
|
151
|
+
}),
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
const state = getAdmissionDisplayState({
|
|
155
|
+
admission: data.admissions[0],
|
|
156
|
+
event: data,
|
|
157
|
+
});
|
|
158
|
+
expect(state.disabled).toBe(true);
|
|
159
|
+
expect(state.label?.text).toContain("On sale");
|
|
160
|
+
});
|
|
161
|
+
it("treats sold-out sale endings as sold out instead of a price label", () => {
|
|
162
|
+
const data = event({
|
|
163
|
+
admissions: [
|
|
164
|
+
admission({
|
|
165
|
+
saleEndsAt: pastEnd,
|
|
166
|
+
saleEndsBehavior: "SOLD_OUT",
|
|
167
|
+
}),
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
const state = getAdmissionDisplayState({
|
|
171
|
+
admission: data.admissions[0],
|
|
172
|
+
event: data,
|
|
173
|
+
});
|
|
174
|
+
expect(state.disabled).toBe(true);
|
|
175
|
+
expect(state.label).toBeNull();
|
|
176
|
+
expect(state.soldOut).toBe(true);
|
|
177
|
+
expect(state.statusLabel).toBe("Sold Out");
|
|
178
|
+
expect(state.statusTone).toBe("destructive");
|
|
179
|
+
});
|
|
180
|
+
it("moves post-sale labels into admission status", () => {
|
|
181
|
+
const data = event({
|
|
182
|
+
admissions: [
|
|
183
|
+
admission({
|
|
184
|
+
saleEndsAt: pastEnd,
|
|
185
|
+
saleEndsBehavior: "SALE_ENDED",
|
|
186
|
+
}),
|
|
187
|
+
admission({
|
|
188
|
+
id: "adm_door",
|
|
189
|
+
saleEndsAt: pastEnd,
|
|
190
|
+
saleEndsBehavior: "DOOR",
|
|
191
|
+
}),
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
const ended = getAdmissionDisplayState({
|
|
195
|
+
admission: data.admissions[0],
|
|
196
|
+
event: data,
|
|
197
|
+
});
|
|
198
|
+
const door = getAdmissionDisplayState({
|
|
199
|
+
admission: data.admissions[1],
|
|
200
|
+
event: data,
|
|
201
|
+
});
|
|
202
|
+
expect(ended.label).toBeNull();
|
|
203
|
+
expect(ended.statusLabel).toBe("Sale Ended");
|
|
204
|
+
expect(ended.statusTone).toBe("destructive");
|
|
205
|
+
expect(door.label).toBeNull();
|
|
206
|
+
expect(door.statusLabel).toBe("Available At Door");
|
|
207
|
+
expect(door.statusTone).toBe("success");
|
|
208
|
+
});
|
|
209
|
+
it("shows sold-out door admissions as available at door while online sales continue", () => {
|
|
210
|
+
const data = event({
|
|
211
|
+
admissions: [
|
|
212
|
+
admission({
|
|
213
|
+
id: "adm_first_release",
|
|
214
|
+
available: 0,
|
|
215
|
+
sold: 54,
|
|
216
|
+
capacity: 54,
|
|
217
|
+
order: 20,
|
|
218
|
+
saleEndsAt: futureEnd,
|
|
219
|
+
saleEndsBehavior: "DOOR",
|
|
220
|
+
}),
|
|
221
|
+
admission({
|
|
222
|
+
id: "adm_final_release",
|
|
223
|
+
available: 997,
|
|
224
|
+
sold: 3,
|
|
225
|
+
capacity: 1000,
|
|
226
|
+
order: 40,
|
|
227
|
+
saleEndsAt: futureEnd,
|
|
228
|
+
saleEndsBehavior: "DOOR",
|
|
229
|
+
}),
|
|
230
|
+
],
|
|
231
|
+
});
|
|
232
|
+
const first = getAdmissionDisplayState({
|
|
233
|
+
admission: data.admissions[0],
|
|
234
|
+
event: data,
|
|
235
|
+
});
|
|
236
|
+
const final = getAdmissionDisplayState({
|
|
237
|
+
admission: data.admissions[1],
|
|
238
|
+
event: data,
|
|
239
|
+
});
|
|
240
|
+
expect(first.disabled).toBe(true);
|
|
241
|
+
expect(first.soldOut).toBe(true);
|
|
242
|
+
expect(first.statusLabel).toBe("Available At Door");
|
|
243
|
+
expect(first.statusTone).toBe("success");
|
|
244
|
+
expect(final.disabled).toBe(false);
|
|
245
|
+
expect(final.statusLabel).toBeNull();
|
|
246
|
+
});
|
|
247
|
+
it("orders top-level sections and unsectioned admissions together", () => {
|
|
248
|
+
const main = section({ id: "sec_main", order: 2 });
|
|
249
|
+
const early = admission({ id: "adm_early", order: 1 });
|
|
250
|
+
const sectioned = admission({
|
|
251
|
+
id: "adm_sectioned",
|
|
252
|
+
order: 1,
|
|
253
|
+
sectionId: main.id,
|
|
254
|
+
});
|
|
255
|
+
const items = topLevelTicketItems([main], [sectioned, early]);
|
|
256
|
+
expect(items.map((item) => item.type)).toEqual(["admission", "section"]);
|
|
257
|
+
});
|
|
258
|
+
it("caps unlimited admission quantity at the selector limit", () => {
|
|
259
|
+
const data = event({
|
|
260
|
+
admissions: [admission({ available: -1, capacity: -1 })],
|
|
261
|
+
});
|
|
262
|
+
expect(getAdmissionDisplayState({
|
|
263
|
+
admission: data.admissions[0],
|
|
264
|
+
event: data,
|
|
265
|
+
}).maxQuantity).toBe(10);
|
|
266
|
+
});
|
|
267
|
+
it("uses discounted quote pricing when available", () => {
|
|
268
|
+
const price = getAdmissionPriceDisplay({
|
|
269
|
+
admission: admission(),
|
|
270
|
+
mode: "default",
|
|
271
|
+
quoteItem: quoteItem(),
|
|
272
|
+
});
|
|
273
|
+
expect(price).toEqual({
|
|
274
|
+
base: 1000,
|
|
275
|
+
discounted: true,
|
|
276
|
+
fees: 200,
|
|
277
|
+
gross: 2000,
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
it("uses quote item base pricing without discount styling", () => {
|
|
281
|
+
const price = getAdmissionPriceDisplay({
|
|
282
|
+
admission: admission(),
|
|
283
|
+
mode: "default",
|
|
284
|
+
quoteItem: quoteItem({
|
|
285
|
+
price: {
|
|
286
|
+
base: 2000,
|
|
287
|
+
booking: 200,
|
|
288
|
+
currency: "AUD",
|
|
289
|
+
discount: 0,
|
|
290
|
+
gross: 2200,
|
|
291
|
+
net: 2250,
|
|
292
|
+
refunded: 0,
|
|
293
|
+
total: 2250,
|
|
294
|
+
},
|
|
295
|
+
}),
|
|
296
|
+
});
|
|
297
|
+
expect(price).toEqual({
|
|
298
|
+
base: 2000,
|
|
299
|
+
discounted: false,
|
|
300
|
+
fees: 200,
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
it("separates admission base and booking fees for every mode", () => {
|
|
304
|
+
expect(getAdmissionPriceDisplay({
|
|
305
|
+
admission: admission(),
|
|
306
|
+
mode: "default",
|
|
307
|
+
})).toMatchObject({ base: 2000, fees: 200 });
|
|
308
|
+
expect(getAdmissionPriceDisplay({
|
|
309
|
+
admission: admission(),
|
|
310
|
+
mode: "door",
|
|
311
|
+
})).toMatchObject({ base: 2000, fees: 200 });
|
|
312
|
+
});
|
|
313
|
+
it("uses discounted initial preview pricing when available", () => {
|
|
314
|
+
const price = getAdmissionPriceDisplay({
|
|
315
|
+
admission: admission(),
|
|
316
|
+
initialPrice: {
|
|
317
|
+
discount: -1000,
|
|
318
|
+
gross: 2200,
|
|
319
|
+
total: 1200,
|
|
320
|
+
},
|
|
321
|
+
mode: "default",
|
|
322
|
+
});
|
|
323
|
+
expect(price).toEqual({
|
|
324
|
+
base: 1000,
|
|
325
|
+
discounted: true,
|
|
326
|
+
fees: 200,
|
|
327
|
+
gross: 2000,
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
it("calculates admission savings from discounted display prices", () => {
|
|
331
|
+
expect(admissionPriceSavings({
|
|
332
|
+
base: 1000,
|
|
333
|
+
discounted: true,
|
|
334
|
+
fees: 200,
|
|
335
|
+
gross: 2000,
|
|
336
|
+
})).toBe(1000);
|
|
337
|
+
expect(admissionPriceSavings({
|
|
338
|
+
base: 2200,
|
|
339
|
+
discounted: false,
|
|
340
|
+
fees: 200,
|
|
341
|
+
})).toBe(0);
|
|
342
|
+
});
|
|
343
|
+
it("divides preview quote pricing by preview quantity", () => {
|
|
344
|
+
const price = getAdmissionPriceDisplay({
|
|
345
|
+
admission: admission(),
|
|
346
|
+
mode: "default",
|
|
347
|
+
previewItem: quoteItem({
|
|
348
|
+
quantity: 2,
|
|
349
|
+
price: {
|
|
350
|
+
base: 4000,
|
|
351
|
+
booking: 400,
|
|
352
|
+
currency: "AUD",
|
|
353
|
+
discount: -2000,
|
|
354
|
+
gross: 4400,
|
|
355
|
+
net: 2400,
|
|
356
|
+
refunded: 0,
|
|
357
|
+
total: 2400,
|
|
358
|
+
},
|
|
359
|
+
}),
|
|
360
|
+
});
|
|
361
|
+
expect(price).toEqual({
|
|
362
|
+
base: 1000,
|
|
363
|
+
discounted: true,
|
|
364
|
+
fees: 200,
|
|
365
|
+
gross: 2000,
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|