Skip to content

Commit 6a52da9

Browse files
authored
Enhance Ticketing Check-In (#80)
* use atomic update for ticket checkIn, fix audit log to include module * update unit tests * fix unit tests
1 parent b8e19a8 commit 6a52da9

File tree

7 files changed

+236
-77
lines changed

7 files changed

+236
-77
lines changed

src/api/functions/mobileWallet.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export async function issueAppleWalletMembershipCard(
118118
pkpass.backFields.push({ label: "Membership ID", key: "id", value: email });
119119
const buffer = pkpass.getAsBuffer();
120120
logger.info(
121-
{ type: "audit", actor: initiator, target: email },
121+
{ type: "audit", module: "mobileWallet", actor: initiator, target: email },
122122
"Created membership verification pass",
123123
);
124124
return buffer;

src/api/routes/events.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,12 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
327327
resource: `/api/v1/events/${entryUUID}`,
328328
});
329329
request.log.info(
330-
{ type: "audit", actor: request.username, target: entryUUID },
330+
{
331+
type: "audit",
332+
module: "events",
333+
actor: request.username,
334+
target: entryUUID,
335+
},
331336
`${verb} event "${entryUUID}"`,
332337
);
333338
} catch (e: unknown) {
@@ -388,7 +393,12 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
388393
false,
389394
);
390395
request.log.info(
391-
{ type: "audit", actor: request.username, target: id },
396+
{
397+
type: "audit",
398+
module: "events",
399+
actor: request.username,
400+
target: id,
401+
},
392402
`deleted event "${id}"`,
393403
);
394404
},

src/api/routes/iam.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,12 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
201201
}
202202
reply.send({ message: "OK" });
203203
request.log.info(
204-
{ type: "audit", actor: request.username, target: groupId },
204+
{
205+
type: "audit",
206+
module: "iam",
207+
actor: request.username,
208+
target: groupId,
209+
},
205210
`set target roles to ${request.body.roles.toString()}`,
206211
);
207212
},
@@ -241,13 +246,23 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
241246
const result = results[i];
242247
if (result.status === "fulfilled") {
243248
request.log.info(
244-
{ type: "audit", actor: request.username, target: emails[i] },
249+
{
250+
type: "audit",
251+
module: "iam",
252+
actor: request.username,
253+
target: emails[i],
254+
},
245255
"invited user to Entra ID tenant.",
246256
);
247257
response.success.push({ email: emails[i] });
248258
} else {
249259
request.log.info(
250-
{ type: "audit", actor: request.username, target: emails[i] },
260+
{
261+
type: "audit",
262+
module: "iam",
263+
actor: request.username,
264+
target: emails[i],
265+
},
251266
"failed to invite user to Entra ID tenant.",
252267
);
253268
if (result.reason instanceof EntraInvitationError) {
@@ -345,6 +360,7 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
345360
request.log.info(
346361
{
347362
type: "audit",
363+
module: "iam",
348364
actor: request.username,
349365
target: request.body.add[i],
350366
},
@@ -354,6 +370,7 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
354370
request.log.info(
355371
{
356372
type: "audit",
373+
module: "iam",
357374
actor: request.username,
358375
target: request.body.add[i],
359376
},
@@ -379,6 +396,7 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
379396
request.log.info(
380397
{
381398
type: "audit",
399+
module: "iam",
382400
actor: request.username,
383401
target: request.body.remove[i],
384402
},
@@ -388,6 +406,7 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
388406
request.log.info(
389407
{
390408
type: "audit",
409+
module: "iam",
391410
actor: request.username,
392411
target: request.body.add[i],
393412
},

src/api/routes/stripe.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
139139
request.log.info(
140140
{
141141
type: "audit",
142+
module: "stripe",
142143
actor: request.username,
143144
target: `Link ${linkId} | Invoice ${invoiceId}`,
144145
},

src/api/routes/tickets.ts

Lines changed: 32 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { FastifyPluginAsync } from "fastify";
22
import { z } from "zod";
33
import {
4+
ConditionalCheckFailedException,
45
QueryCommand,
56
ScanCommand,
67
UpdateItemCommand,
@@ -300,14 +301,17 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
300301
stripe_pi: { S: ticketId },
301302
},
302303
UpdateExpression: "SET fulfilled = :true_val",
303-
ConditionExpression: "#email = :email_val",
304+
ConditionExpression:
305+
"#email = :email_val AND (attribute_not_exists(fulfilled) OR fulfilled = :false_val) AND (attribute_not_exists(refunded) OR refunded = :false_val)",
304306
ExpressionAttributeNames: {
305307
"#email": "email",
306308
},
307309
ExpressionAttributeValues: {
308310
":true_val": { BOOL: true },
311+
":false_val": { BOOL: false },
309312
":email_val": { S: request.body.email },
310313
},
314+
ReturnValuesOnConditionCheckFailure: "ALL_OLD",
311315
ReturnValues: "ALL_OLD",
312316
});
313317
break;
@@ -319,12 +323,16 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
319323
ticket_id: { S: ticketId },
320324
},
321325
UpdateExpression: "SET #used = :trueValue",
326+
ConditionExpression:
327+
"(attribute_not_exists(#used) OR #used = :falseValue) AND (attribute_not_exists(refunded) OR refunded = :falseValue)",
322328
ExpressionAttributeNames: {
323329
"#used": "used",
324330
},
325331
ExpressionAttributeValues: {
326332
":trueValue": { BOOL: true },
333+
":falseValue": { BOOL: false },
327334
},
335+
ReturnValuesOnConditionCheckFailure: "ALL_OLD",
328336
ReturnValues: "ALL_OLD",
329337
});
330338
break;
@@ -342,16 +350,6 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
342350
});
343351
}
344352
const attributes = unmarshall(ticketEntry.Attributes);
345-
if (attributes["refunded"]) {
346-
throw new TicketNotValidError({
347-
message: "Ticket was already refunded.",
348-
});
349-
}
350-
if (attributes["used"] || attributes["fulfilled"]) {
351-
throw new TicketNotValidError({
352-
message: "Ticket has already been used.",
353-
});
354-
}
355353
if (request.body.type === "ticket") {
356354
const rawData = attributes["ticketholder_netid"];
357355
const isEmail = validateEmail(attributes["ticketholder_netid"]);
@@ -376,65 +374,41 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
376374
if (e instanceof BaseError) {
377375
throw e;
378376
}
379-
if (e.name === "ConditionalCheckFailedException") {
377+
if (e instanceof ConditionalCheckFailedException) {
378+
if (e.Item) {
379+
const unmarshalled = unmarshall(e.Item);
380+
if (unmarshalled["fulfilled"] || unmarshalled["used"]) {
381+
throw new TicketNotValidError({
382+
message: "Ticket has already been used.",
383+
});
384+
}
385+
if (unmarshalled["refunded"]) {
386+
throw new TicketNotValidError({
387+
message: "Ticket was already refunded.",
388+
});
389+
}
390+
}
380391
throw new TicketNotFoundError({
381-
message: "Ticket does not exist",
392+
message: "Ticket does not exist.",
382393
});
383394
}
384395
throw new DatabaseFetchError({
385396
message: "Could not set ticket to used - database operation failed",
386397
});
387398
}
388-
const response = {
399+
reply.send({
389400
valid: true,
390401
type: request.body.type,
391402
ticketId,
392403
purchaserData,
393-
};
394-
switch (request.body.type) {
395-
case "merch":
396-
ticketId = request.body.stripePi;
397-
command = new UpdateItemCommand({
398-
TableName: genericConfig.MerchStorePurchasesTableName,
399-
Key: {
400-
stripe_pi: { S: ticketId },
401-
},
402-
UpdateExpression:
403-
"SET scannerEmail = :scanner_email, scanISOTimestamp = :scan_time",
404-
ConditionExpression: "email = :email_val",
405-
ExpressionAttributeValues: {
406-
":scanner_email": { S: request.username },
407-
":scan_time": { S: new Date().toISOString() },
408-
":email_val": { S: request.body.email },
409-
},
410-
});
411-
break;
412-
413-
case "ticket":
414-
ticketId = request.body.ticketId;
415-
command = new UpdateItemCommand({
416-
TableName: genericConfig.TicketPurchasesTableName,
417-
Key: {
418-
ticket_id: { S: ticketId },
419-
},
420-
UpdateExpression:
421-
"SET scannerEmail = :scanner_email, scanISOTimestamp = :scan_time",
422-
ExpressionAttributeValues: {
423-
":scanner_email": { S: request.username },
424-
":scan_time": { S: new Date().toISOString() },
425-
},
426-
});
427-
break;
428-
429-
default:
430-
throw new ValidationError({
431-
message: `Unknown verification type!`,
432-
});
433-
}
434-
await fastify.dynamoClient.send(command);
435-
reply.send(response);
404+
});
436405
request.log.info(
437-
{ type: "audit", actor: request.username, target: ticketId },
406+
{
407+
type: "audit",
408+
module: "tickets",
409+
actor: request.username,
410+
target: ticketId,
411+
},
438412
`checked in ticket of type "${request.body.type}" ${request.body.type === "merch" ? `purchased by email ${request.body.email}.` : "."}`,
439413
);
440414
},

src/api/sqs/handlers.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,12 @@ export const provisionNewMemberHandler: SQSHandlerFunction<
109109
});
110110
if (updated) {
111111
logger.info(
112-
{ type: "audit", actor: metadata.initiator, target: email },
112+
{
113+
type: "audit",
114+
module: "provisionNewMember",
115+
actor: metadata.initiator,
116+
target: email,
117+
},
113118
"marked user as a paid member.",
114119
);
115120
logger.info(

0 commit comments

Comments
 (0)