apps/web/src/lib/ticketAnswer.test.ts

1import { expect, test } from 'bun:test'
2import {
3	ticketFactsReply as renderTicketFacts,
4	ticketAnswerKind,
5} from './ticketAnswer'
6
7import { verifiedBadgePricing } from './ticketPriceFixtures.test-helper'
8
9const ticketFactsReply = (
10	event: 'paris' | 'miami',
11	kind: Parameters<typeof renderTicketFacts>[1],
12	message: string,
13	history: Parameters<typeof renderTicketFacts>[3],
14	date: Date,
15) =>
16	renderTicketFacts(
17		event,
18		kind,
19		message,
20		history,
21		verifiedBadgePricing(`/events/unleash-${event}`, date).map((p) => ({
22			...p,
23			eventId: 'test',
24			taxLabel: p.taxTreatment,
25			status:
26				p.amount === null ? ('application-only' as const) : ('available' as const),
27		})),
28	)
29const now = new Date('2026-09-17T12:00:00Z')
30test('a second request for help advances to a concrete sourced recommendation', () => {
31	const history = [
32		{ role: 'user' as const, text: 'help' },
33		{ role: 'assistant' as const, text: 'What matters most to you?' },
34		{ role: 'user' as const, text: 'help please' },
35	]
36	const text =
37		ticketFactsReply('paris', 'comparison', 'help please', history, now) || ''
38	expect(text).toContain('I’d start with')
39	expect(text).toContain('General Attendee')
40	expect(text).not.toContain('What matters most')
41	expect(text).toContain('not a selected or reserved pass')
42	expect(ticketAnswerKind('I am torn between Explorer and Diamond')).toBe(
43		'comparison',
44	)
45})
46
47test('repeating quantity after the pass question moves to choosing help instead of the same prompt', () => {
48	expect(
49		ticketAnswerKind('One ticket please.', [
50			{
51				role: 'assistant',
52				text:
53					'1 ticket noted. Which pass would you like, or would you like help choosing one?',
54			},
55		]),
56	).toBe('comparison')
57	expect(
58		ticketAnswerKind('One ticket please.', [
59			{ role: 'assistant', text: 'How many Diamond badges?' },
60		]),
61	).toBe('other')
62})
63test('prices stay with their badge and include tax; Explorer never receives a number', () => {
64	const text =
65		ticketFactsReply('paris', 'comparison', 'Compare all badges', [], now) || ''
66	expect(text).toContain('EUR 2,995 + VAT')
67	expect(text).toContain('EUR 4,995 + VAT')
68	const explorer =
69		text.split('\n').find((line) => line.includes('[Explorer]')) || ''
70	expect(explorer).toContain('Price on application')
71	expect(explorer).not.toMatch(/2,995|3,485|4,995|5,995/)
72	expect(explorer).toContain('vendor registrations are not accepted')
73})
74test('expired and different-event prices fail closed', () => {
75	expect(
76		ticketFactsReply(
77			'paris',
78			'price',
79			'Diamond price',
80			[],
81			new Date('2026-09-18T22:00:00Z'),
82		),
83	).not.toContain('4,995')
84	expect(
85		ticketFactsReply('miami', 'price', 'Diamond price', [], now),
86	).not.toContain('4,995')
87})
88test('short price follow-up resolves a single badge without borrowing another price', () => {
89	const history = [
90		{ role: 'assistant' as const, text: 'How many Diamond badges?' },
91	]
92	expect(ticketAnswerKind('How much?', history)).toBe('price')
93	const text = ticketFactsReply('paris', 'price', 'How much?', history, now)
94	expect(text).toContain('4,995')
95	expect(text).not.toContain('2,995')
96})
97test('purchases, support and unrelated detours do not become price answers', () => {
98	const history = [
99		{ role: 'assistant' as const, text: 'How many Diamond badges?' },
100	]
101	for (const message of [
102		'Buy two Diamond badges',
103		'Cancel that',
104		'I need my ticket invoice',
105		'How much is the hotel?',
106		'Compare the hotels',
107	])
108		expect(ticketAnswerKind(message, history)).toBe('other')
109})
110
111test('a named comparison does not add an unrequested badge', () => {
112	const text = ticketFactsReply(
113		'paris',
114		'comparison',
115		'Compare Explorer and Diamond',
116		[],
117		now,
118	)
119	expect(text).toContain('[Explorer]')
120	expect(text).toContain('[Diamond]')
121	expect(text).not.toContain('[General Attendee]')
122})
123
124test('help after an existing comparison advances, and another help narrows to one decision', () => {
125	const comparison =
126		ticketFactsReply('paris', 'comparison', 'Compare badges', [], now) || ''
127	const recommendation =
128		ticketFactsReply(
129			'paris',
130			'comparison',
131			'help',
132			[{ role: 'assistant', text: comparison }],
133			now,
134		) || ''
135	expect(recommendation).toContain('I’d start with')
136	expect(recommendation).not.toContain('What matters most')
137	const narrowed =
138		ticketFactsReply(
139			'paris',
140			'comparison',
141			'help please',
142			[
143				{ role: 'user', text: 'help' },
144				{ role: 'assistant', text: recommendation },
145			],
146			now,
147		) || ''
148	expect(narrowed).toContain('Do you want Day 1 leadership sessions included?')
149	expect(narrowed).not.toBe(recommendation)
150})
151