3
3
* Handles all guest/ticket actions
4
4
* @type {Object }
5
5
*/
6
- import { toDataURL as generateQRDataURI } from 'qrcode' ;
7
- import jwt from 'jsonwebtoken' ;
8
6
import { v4 as uuidV4 } from 'uuid' ;
9
7
import { createGuest } from '../services/guests.js' ;
10
8
import log from '../utils/log.js' ;
11
9
import { sql } from '../utils/db.js' ;
12
10
13
11
class TicketsServiceError extends Error {
14
- constructor ( message = 'An unknown error occured' , code = 'UNKNOWN' , context ) {
12
+ code : string ;
13
+ context ?: unknown ;
14
+
15
+ constructor ( message = 'An unknown error occured' , code = 'UNKNOWN' , context ?: unknown ) {
15
16
super ( message ) ;
16
17
17
18
this . name = this . constructor . name ;
@@ -22,15 +23,15 @@ class TicketsServiceError extends Error {
22
23
}
23
24
}
24
25
25
- // eslint-disable-next-line no-unused-vars
26
- const generateTicketToken = ( { id , created , ticketSeed } ) => jwt . sign ( {
27
- aud : ticketSeed ,
28
- iat : Math . round ( created / 1000 ) ,
29
- sub : id
30
- } ,
31
- ticketSeed ) ;
26
+ // For now, ticket "seed" is fine to be used as plaintext since we can use it as
27
+ // a revokable identifier, but are not rolling "live" tickets this year
28
+ // ie, we aren't seeding a TOTP with it, and therefore it does not need to be a secret value.
29
+ // This keeps the QR payload very short, and much quicker for scanning (both ease of reading and input time)
30
+ function generateQRPayload ( ticketSeed : string ) {
31
+ return ticketSeed ;
32
+ }
32
33
33
- export async function getOrderTickets ( orderId ) {
34
+ export async function getOrderTickets ( orderId : string ) {
34
35
let guests ;
35
36
try {
36
37
guests = await sql `
@@ -54,32 +55,32 @@ export async function getOrderTickets(orderId) {
54
55
// Inject the QR Codes
55
56
const tickets = [ ] ;
56
57
for ( const guest of guests ) {
57
- // const qrCode = await generateQRDataURI(generateTicketToken(guest).split('.').pop());
58
- // eslint-disable-next-line no-unused-vars
59
- const qrCode = await generateQRDataURI ( `${ guest . id } :${ Date . now ( ) } ` ) ;
58
+ const qrPayload = generateQRPayload ( guest . ticketSeed ) ;
60
59
61
60
tickets . push ( {
62
61
id : guest . id ,
63
62
admissionTier : guest . admissionTier ,
64
63
eventId : guest . eventId ,
65
64
eventName : guest . eventName ,
66
65
eventDate : guest . eventDate ,
67
- status : guest . status
68
- // qrCode
66
+ status : guest . status ,
67
+ qrPayload
69
68
} ) ;
70
69
}
71
70
72
71
return tickets ;
73
72
}
74
73
75
- export async function getCustomerActiveTicketsByOrderId ( orderId ) {
74
+ export async function getCustomerActiveTicketsByOrderId ( orderId : string ) {
76
75
let rows ;
77
76
try {
78
77
rows = await sql `
79
78
SELECT
80
79
o.created AT TIME ZONE 'UTC' AT TIME ZONE 'America/Los_Angeles' as order_created,
81
80
o.customer_id,
82
81
g.id as guest_id,
82
+ g.status as guest_status,
83
+ g.check_in_time as guest_check_in_time,
83
84
g.admission_tier as guest_admission_tier,
84
85
g.ticket_seed as guest_ticket_seed,
85
86
g.order_id as guest_order_id,
@@ -103,20 +104,20 @@ export async function getCustomerActiveTicketsByOrderId(orderId) {
103
104
// Inject the QR Codes
104
105
const tickets = [ ] ;
105
106
for ( const row of rows ) {
106
- // const qrCode = await generateQRDataURI(generateTicketToken(guest).split('.').pop());
107
- // eslint-disable-next-line no-unused-vars
108
- const qrCode = await generateQRDataURI ( `${ row . guestId } :${ Date . now ( ) } ` ) ;
107
+ const qrPayload = generateQRPayload ( row . guestTicketSeed ) ;
109
108
110
109
tickets . push ( {
111
110
id : row . guestId ,
112
111
customerId : row . customerId ,
113
112
orderId : row . guestOrderId ,
114
113
orderCreated : row . orderCreated ,
115
114
admissionTier : row . guestAdmissionTier ,
115
+ status : row . guestStatus ,
116
+ checkInTime : row . guestCheckInTime ,
116
117
eventId : row . eventId ,
117
118
eventName : row . eventName ,
118
- eventDate : row . eventDate
119
- // qrCode
119
+ eventDate : row . eventDate ,
120
+ // qrPayload
120
121
} ) ;
121
122
}
122
123
@@ -144,7 +145,20 @@ export async function getCustomerActiveTicketsByOrderId(orderId) {
144
145
* are merely records of "guest" transfers
145
146
* - The transferee will be upserted into customers, which means the original customer will need to input first/last/email
146
147
*/
147
- export async function transferTickets ( orderId , { transferee, guestIds } ) {
148
+ export async function transferTickets (
149
+ orderId : string ,
150
+ {
151
+ transferee,
152
+ guestIds
153
+ } : {
154
+ transferee : {
155
+ email : string ;
156
+ firstName : string ;
157
+ lastName : string ;
158
+ } ;
159
+ guestIds : string [ ] ;
160
+ }
161
+ ) {
148
162
if ( ! transferee ) throw new TicketsServiceError ( 'No transferee specified' , 'INVALID' ) ;
149
163
if ( ! guestIds ?. length ) throw new TicketsServiceError ( 'No tickets specified' , 'INVALID' ) ;
150
164
@@ -270,41 +284,58 @@ export async function transferTickets(orderId, { transferee, guestIds }) {
270
284
} ;
271
285
}
272
286
273
- export function checkInWithTicket ( ) {
274
- throw new TicketsServiceError ( 'Not yet implemented' , 'NOT_IMPLEMENTED' ) ;
275
- // let ticketId, ticketGuestId;
276
- // try {
277
- // ({ sub: ticketId, aud: ticketGuestId } = jwt.verify(ticketToken, config.jwt.ticketSecret, {issuer: 'mustachebash'}));
278
- // } catch(e) {
279
- // throw new TicketsServiceError('Invalid ticket token', 'INVALID_TICKET_TOKEN');
280
- // }
281
-
282
- // const [ { ticket, guest, event } = {} ] = await run(r.table('tickets')
283
- // .getAll([ticketGuestId, ticketId ], {index: 'guestAndTicketId'})
284
- // .eqJoin('eventId', r.table('events'))
285
- // .map({
286
- // ticket: r.row('left'),
287
- // event: r.row('right'),
288
- // guest: r.table('guests').get(r.row('left')('guestId'))
289
- // }))
290
- // .then(cursor => cursor.toArray());
291
-
292
- // if(!ticket) throw new TicketsServiceError('Ticket not found for guest', 'TICKET_NOT_FOUND');
293
-
294
- // // All three entities must be active to check in
295
- // if(ticket.status !== 'active' && ticket.status !== 'consumed') throw new TicketsServiceError('Ticket no longer active', 'TICKET_NOT_ACTIVE', {ticket, guest, event});
296
- // if(guest.status !== 'active') throw new TicketsServiceError('Guest no longer active', 'GUEST_NOT_ACTIVE', {ticket, guest, event});
297
- // if(event.status !== 'active') throw new TicketsServiceError('Event no longer active', 'EVENT_NOT_ACTIVE', {ticket, guest, event});
298
-
299
- // // Guests can't check in more than once
300
- // if(guest.checkedIn) throw new TicketsServiceError('Guest already checked in', 'GUEST_ALREADY_CHECKED_IN', {ticket, guest, event});
301
-
302
- // // Guests can't check in before the event starts
303
- // if(event.enforceCheckInTime && new Date() < new Date(event.date)) throw new TicketsServiceError('Event has not started yet', 'EVENT_NOT_STARTED', {ticket, guest, event});
304
-
305
- // // Ticket and check in is valid - mark guest as checked in and ticket as used (sequentially)
306
- // await run(r.table('guests').get(guest.id).update({checkedIn: r.now(), updated: r.now(), updatedBy: username}));
307
- // await run(r.table('tickets').get(ticket.id).update({status: 'consumed'}));
308
-
309
- // return {event, guest, ticket};
287
+ export async function checkInWithTicket ( ticketToken : string , scannedBy : string ) {
288
+ let guest ;
289
+ // For now, this is happening directly with ticket seeds
290
+ try {
291
+ [ guest ] = await sql `
292
+ SELECT
293
+ g.id,
294
+ g.first_name,
295
+ g.last_name,
296
+ g.status,
297
+ g.order_id,
298
+ g.admission_tier,
299
+ g.check_in_time,
300
+ e.id AS event_id,
301
+ e.name AS event_name,
302
+ e.date AS event_date,
303
+ e.status AS event_status
304
+ FROM guests AS g
305
+ LEFT JOIN events AS e
306
+ ON g.event_id = e.id
307
+ WHERE g.ticket_seed = ${ ticketToken }
308
+ ` ;
309
+ } catch ( e ) {
310
+ throw new TicketsServiceError ( 'Could not query guests for order' , 'UNKNOWN' , e ) ;
311
+ }
312
+
313
+ if ( ! guest ) throw new TicketsServiceError ( 'Ticket not found for guest' , 'TICKET_NOT_FOUND' ) ;
314
+
315
+ // Guests can't check in more than once
316
+ if ( guest . status === 'checked_in' ) throw new TicketsServiceError ( 'Guest already checked in' , 'GUEST_ALREADY_CHECKED_IN' , guest ) ;
317
+ // Both entities must be active to check in
318
+ if ( guest . status !== 'active' ) throw new TicketsServiceError ( 'Guest no longer active' , 'GUEST_NOT_ACTIVE' , guest ) ;
319
+ if ( guest . eventStatus !== 'active' ) throw new TicketsServiceError ( 'Event no longer active' , 'EVENT_NOT_ACTIVE' , guest ) ;
320
+
321
+
322
+ // Guests can't check in earlier than 1 hour before the event starts
323
+ if ( ( new Date ( ) ) . getTime ( ) < ( new Date ( guest . eventDate ) ) . getTime ( ) - ( 1000 * 60 * 60 * 24 * 10 ) ) throw new TicketsServiceError ( 'Event has not started yet' , 'EVENT_NOT_STARTED' , guest ) ;
324
+
325
+ // Ticket and check in is valid - mark guest as checked in
326
+ try {
327
+ await sql `
328
+ UPDATE guests
329
+ SET
330
+ status = 'checked_in',
331
+ check_in_time = now(),
332
+ updated_by = ${ scannedBy } ,
333
+ updated = now()
334
+ WHERE id = ${ guest . id }
335
+ ` ;
336
+ } catch ( e ) {
337
+ throw new TicketsServiceError ( 'Could not update guest' , 'UNKNOWN' , e ) ;
338
+ }
339
+
340
+ return guest ;
310
341
}
0 commit comments