diff --git a/functions/checkout_complete/lambda_function.py b/functions/checkout_complete/lambda_function.py index dc485cd4..f4e2409e 100644 --- a/functions/checkout_complete/lambda_function.py +++ b/functions/checkout_complete/lambda_function.py @@ -84,6 +84,25 @@ def lambda_handler(event, context): 'body': json.dumps({'error': 'Checkout session does not exist'}) } + if stripe_response['metadata'].get('ticket_upgrade', False): + ticket_number = stripe_response.get('client_reference_id', None) + full_name = next((item for item in stripe_response['custom_fields'] if item["key"] == "fullname"), {})['text'].get('value', "unknown") + payload = { + 'ticket_number': ticket_number, + 'source': "stripe_checkout", + 'upgrade_type': stripe_response['metadata'].get('upgrade_type', 'unknown'), + 'full_name': full_name + } + + # upgrade the ticket + logger.info("Invoking ticket_upgrade lambda") + response = lambda_client.invoke( + FunctionName=os.environ.get("TICKET_UPGRADE_LAMBDA"), + InvocationType='Event', + Payload=json.dumps(payload, cls=shared.DecimalEncoder), + ) + logger.info(response) + access, line_items = process_line_items(stripe_response['line_items']) student_ticket = True if stripe_response['line_items']['data'][0]['price']['nickname' ] == "student_active" else False meal = json.loads(stripe_response['metadata']['preferences']) if 'preferences' in stripe_response['metadata'] else None diff --git a/functions/send_email/example.html b/functions/send_email/example.html index 2281a904..66e4f1da 100644 --- a/functions/send_email/example.html +++ b/functions/send_email/example.html @@ -1,538 +1,153 @@ - - - - - - - - - - - - -
-
- - -
- - - - -
- - - - -
- - - - - -
- - - - - - - - - -
- - - - - -
- + + + + + + + + + + + + + + + +
+
+
- - - -
- - - - - -
- -
-
-
- - - - - -
-

THANK YOU FOR YOUR PURCHASE!

-

-
Your Event Ticket
- - - - - -
-
Name: CK Monaghan
-
Email: c.monaghan@liverpool.ac.uk
-
Ticket Number: 5520910259
- - - - - -
- - - - - - -
-
- - - - - -
- - - - - -
- - - - - -
ITEM
- - - - - -
- - - - - -
QTY
- - - - - -
- - - - - -
PRICE
- - - - - -
- - - - - - -
-
- -
- - - - - - - - -
- - - - - -
- - - - - -
Party Pass
- - - - - -
- - - - - -
1
- - - - - -
- - - - - -
£35.00
- - - - - - - -
- - - - - - -
-
- - - - - - - -
- - - - - - -
-
- - - - - - - -
- - - - - -
- - - - - -
- - - - - -
- - - - - -
Total
- - - - - -
- - - - - -
£35.00
- - - - - - - -
- - - - - - -
-
- - - - - - - -
-
- - - - - - -
- - + - - -
+ - - - -
-
- - - - - -
-
- - - - - - - -
- - Find out more details and view your ticket details online.
- If your ticket includes the Saturday Dinner you can check and modify your meal preferences online by following this link. -
- - - - -
- - - - - - +
- View Online + + + + + +
+ + + + + +
+ + + + + + + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ + + + + + +
+ +
+ +
+
+
+ +
+ $body + + + + + + +
+
+ + + + + + +
+
+
Do not hesitate to get in touch with us if you have any questions regarding your ticket.
+

+
We hope you enjoy the festival!
+
+
+
+
+
+ + +
+ +
+
+ +
- - - - - -
-
- - - - - - -
-
-

-
- Transferred From -
-
-
-
- - - - - - -
-
-
- Name: - $oldname -
-
- Email: - $oldemail -
-
-
-
- - - - - - -
Do not hesitate to get in touch with us if you have any questions regarding your ticket.
-

-
We hope you enjoy the festival!
-
- - -
- - -
- -
-
-
- - - - \ No newline at end of file +
+
+
+ + \ No newline at end of file diff --git a/functions/send_email/lambda_function.py b/functions/send_email/lambda_function.py index 3d901c21..0d46734f 100644 --- a/functions/send_email/lambda_function.py +++ b/functions/send_email/lambda_function.py @@ -152,7 +152,23 @@ def lambda_handler(event, context): body = event['message_body'] subject = event['subject'] attachment = None + + elif event['email_type'] == "ticket_upgrade_notification": + + subject = "Merseyside Latin Festival - Ticket Upgrade Confirmation" + subdomain = "www" if os.environ.get("STAGE_NAME") == "prod" else os.environ.get("STAGE_NAME") + manage_ticket_link = "http://{}.merseysidelatinfestival.co.uk/preferences?email={}&ticket_number={}".format(subdomain, event['email'], event['ticket_number']) + upgrade_details = "Your ticket has been upgraded to include: {}".format(event['upgrade_details'].replace("_", " ")) + + with open("./send_email/upgrade_notification.html", "r") as body_file: + body_tmpl = Template(body_file.read()) + subdomain = "www" if os.environ.get("STAGE_NAME") == "prod" else os.environ.get("STAGE_NAME") + body = body_tmpl.substitute({ + 'upgrade_details': upgrade_details, + 'ticket_link':manage_ticket_link, + }) + attachment = None else: return False diff --git a/functions/send_email/upgrade_notification.html b/functions/send_email/upgrade_notification.html new file mode 100644 index 00000000..e84ecc4b --- /dev/null +++ b/functions/send_email/upgrade_notification.html @@ -0,0 +1,45 @@ + + + + + + +
+
+
+ Ticket Upgrade Notification +
+
+
+
+ + + + + + + + + + + +
+
+
+ + $upgrade_details +
+
+
+ + + + + + +
+ +

MANAGE MY TICKET

+
+
+
\ No newline at end of file diff --git a/functions/serverless.yml b/functions/serverless.yml index 4ffcba75..4dd49bd5 100644 --- a/functions/serverless.yml +++ b/functions/serverless.yml @@ -202,6 +202,7 @@ functions: STRIPE_SECRET_KEY: ${param:stripeSecretKey} CREATE_TICKET_LAMBDA: "${sls:stage}-create_ticket" #! Can't find a way to set this other than as a params that gets used byt function and the function itself ATTENDEE_GROUPS_LAMBDA: "${sls:stage}-attendee_groups" + TICKET_UPGRADE_LAMBDA: "${sls:stage}-ticket_upgrade" layers: - !Ref StripeLambdaLayer # - !Ref DynamodbLambdaLayer @@ -556,4 +557,23 @@ functions: path: /meal/summary method: get +#-------------------- + TicketUpgrade: + runtime: python3.11 + handler: ticket_upgrade/lambda_function.lambda_handler + name: "${sls:stage}-ticket_upgrade" + package: + patterns: + - '!**/**' + - "ticket_upgrade/**" + - "shared/**" + environment: + STAGE_NAME: ${sls:stage} + EVENT_TABLE_NAME: ${param:eventTableName} + ATTENDEES_TABLE_NAME: ${param:attendeesTableName} + SEND_EMAIL_LAMBDA: "${sls:stage}-send_email" + events: + - httpApi: + path: /ticket_upgrade + method: post diff --git a/functions/shared/parser.py b/functions/shared/parser.py new file mode 100644 index 00000000..77b63e0c --- /dev/null +++ b/functions/shared/parser.py @@ -0,0 +1,43 @@ +import json +import logging + +logger = logging.getLogger() +logger.setLevel("INFO") + +def parse_event(event): + ''' + Parses the input event and ensures it is returned as a dictionary. + ''' + try: + # Check if event is a dictionary with a 'body' (HTTP POST case) + if isinstance(event, dict) and 'body' in event: + logger.info("Parsing event from HTTP POST request body") + event = json.loads(event['body']) + + # Check if event is a JSON string + elif isinstance(event, str): + logger.info("Parsing event from JSON string") + event = json.loads(event) + + # Validate that the event is now a dictionary + if not isinstance(event, dict): + logger.error("Event is not a dictionary after parsing") + raise TypeError("Event must be a dictionary or a valid JSON string") + + except (json.JSONDecodeError, TypeError) as e: + logger.error("Event parsing error: %s", str(e)) + raise ValueError("Invalid event format or JSON structure") + + return event + +def validate_event(event, required_fields): + ''' + Validates that the necessary fields are present in the event. + ''' + missing_fields = [field for field in required_fields if field not in event] + + if missing_fields: + logger.error("Missing required fields: %s", missing_fields) + raise KeyError(f"Missing required fields: {missing_fields}") + + return event \ No newline at end of file diff --git a/functions/ticket_upgrade/lambda_function.py b/functions/ticket_upgrade/lambda_function.py new file mode 100644 index 00000000..abbae823 --- /dev/null +++ b/functions/ticket_upgrade/lambda_function.py @@ -0,0 +1,190 @@ +import json +import logging +import os +import time + +import boto3 +from boto3.dynamodb.conditions import Key +from shared.DecimalEncoder import DecimalEncoder +from shared.parser import parse_event, validate_event + +#ENV +attendees_table_name = os.environ.get("ATTENDEES_TABLE_NAME") +event_table_name = os.environ.get("EVENT_TABLE_NAME") +send_email_lambda = os.environ.get("SEND_EMAIL_LAMBDA") + +logger = logging.getLogger() +logger.setLevel("INFO") + +db = boto3.resource('dynamodb') +attendees_table = db.Table(attendees_table_name) +event_table = db.Table(event_table_name) + +lambda_client = boto3.client('lambda') + +UPGRADE_MAP = { + 'volunteer_meal': [2] +} + +def record_failed_upgrade(event, reason): + ''' + Records details of a failed upgrade attempt + ''' + logger.info("Recording failed upgrade attempt.") + current_time = int(time.time()) + pk = f"UPGRADEFAIL#{event.get('ticket_number', 'unknown')}" + sk = f"ERROR#{current_time}" + + backup_entry = { + 'PK': pk, + 'SK': sk, + 'timestamp': current_time, + 'email': event.get('email', 'unknown'), + 'full_name': event.get('full_name', 'unknown'), + 'reason': reason, + 'event_details': json.dumps(event, cls=DecimalEncoder) + } + + try: + event_table.put_item(Item=backup_entry) + logger.info("Failed upgrade recorded successfully.") + except boto3.exceptions.Boto3Error as e: + logger.error("Error recording failed attempt: %s", str(e)) + + +def is_upgrade_possible(current_access, upgrade_type): + ''' + Check if an upgrade is possible based on the current access array and upgrade type + ''' + upgrade_positions = UPGRADE_MAP.get(upgrade_type, []) + for pos in upgrade_positions: + if current_access[pos] == 0: + return True + return False + +def apply_upgrade(current_access, upgrade_type): + ''' + Apply the upgrade to the access array + ''' + upgrade_positions = UPGRADE_MAP.get(upgrade_type, []) + for pos in upgrade_positions: + current_access[pos] = 1 + return current_access + +def lambda_handler(event, context): + # get ticket being upgraded + # what did they purchase? + # check upgrade is possible + # upgrade the ticket + # send confirmation email + + # if can't find the ticket being upgraded: + # search again but using email + # using context see if it makes sense if any found are + + try: + event = parse_event(event) + event = validate_event(event, ['ticket_number', 'upgrade_type', 'source']) + except (ValueError, TypeError, KeyError) as e: + logger.error("Event validation failed: %s", str(e)) + logger.error(event) + return { + 'statusCode': 400, + 'body': f'Invalid input: {str(e)}' + } + + + logger.info("#### UPGRADING TICKET ####") + logger.info("Event received: %s", json.dumps(event, indent=2, cls=DecimalEncoder)) + + ticket_number = event['ticket_number'] + upgrade_type = event['upgrade_type'] + source = event.get('source', 'unknown') + full_name = event.get('full_name', 'unknown') + logger.info("Processing upgrade for ticket number: %s, upgrade type: %s, source: %s", ticket_number, upgrade_type, source) + + if upgrade_type not in UPGRADE_MAP: + logger.warning("Unsupported upgrade type: %s", upgrade_type) + record_failed_upgrade(event, f"Unsupported upgrade type: {upgrade_type}") + + return { + 'statusCode': 400, + 'body': f'Unsupported upgrade type: {upgrade_type}' + } + + + response = attendees_table.query( + IndexName="ticket_number-index", + KeyConditionExpression=Key('ticket_number').eq(ticket_number) + ) + + if 'Items' not in response or not response['Items']: + logger.warning("No ticket found with the provided ticket number") + record_failed_upgrade(event, "Ticket not found") + return { + 'statusCode': 404, + 'body': 'Ticket not found' + } + + + ticket = response['Items'][0] + logger.info("Ticket found: %s", json.dumps(ticket, indent=2, cls=DecimalEncoder)) + + current_access = ticket.get('access', []) + if not is_upgrade_possible(current_access, upgrade_type): + logger.warning("Upgrade not possible for this ticket") + record_failed_upgrade(event, "Upgrade not possible, access clash") + return { + 'statusCode': 400, + 'body': 'Upgrade not allowed' + } + + try: + ticket['access'] = apply_upgrade(current_access, upgrade_type) + + if ticket.get('history', []) == None: + ticket['history'] = [] + + ticket['history'] = ticket.get('history', []) + [{ + "timestamp": int(time.time()), + "action": "upgrade", + "type": upgrade_type, + "source": source # Include source of the upgrade in the history + }] + + logger.info("Updating ticket in DynamoDB...") + attendees_table.put_item(Item=ticket) + logger.info("Ticket upgrade successful") + + except boto3.exceptions.Boto3Error as e: + logger.error("Failed to update ticket in DynamoDB: %s", str(e)) + record_failed_upgrade(event, f"Failed to update ticket: {str(e)}") + return { + 'statusCode': 500, + 'body': 'Failed to update ticket in database' + } + + if event.get('send_confirmation', True): + try: + logger.info("Invoking send_email lambda function") + response = lambda_client.invoke( + FunctionName=send_email_lambda, + InvocationType='Event', + Payload=json.dumps({ + 'email_type': "ticket_upgrade_notification", + 'name': ticket['full_name'], + 'email': ticket['email'], + 'ticket_number': ticket_number, + 'upgrade_details': upgrade_type + }, cls=DecimalEncoder), + ) + logger.info("Email invocation response: %s", response) + + except boto3.exceptions.Boto3Error as e: + logger.error("Failed to invoke send_email lambda: %s", str(e)) + record_failed_upgrade(event, f"Failed to send confirmation email: {str(e)}") + + return { + 'statusCode': 200, + 'body': 'Ticket upgraded successfully' + } \ No newline at end of file