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