From 663e1e15d3abf94c58ce973d3718c9cf5bff83dd Mon Sep 17 00:00:00 2001 From: Arnel Jan Sarmiento Date: Sun, 7 Sep 2025 01:59:30 +0800 Subject: [PATCH 01/11] feat(payment): implement registration data extraction and email notification for payment failures, enhancing error handling in payment processing --- backend/usecase/payment_usecase.py | 184 +++++++++++++++++++++++- backend/usecase/registration_usecase.py | 9 +- 2 files changed, 186 insertions(+), 7 deletions(-) diff --git a/backend/usecase/payment_usecase.py b/backend/usecase/payment_usecase.py index 707253f6..96a1c217 100644 --- a/backend/usecase/payment_usecase.py +++ b/backend/usecase/payment_usecase.py @@ -1,16 +1,25 @@ import os from http import HTTPStatus +from model.email.email import EmailIn, EmailType from model.payments.payments import ( PaymentTransactionIn, PaymentTransactionOut, TransactionStatus, ) -from model.pycon_registrations.pycon_registration import PaymentRegistrationDetailsOut +from model.pycon_registrations.pycon_registration import ( + PaymentRegistrationDetailsOut, + PyconRegistrationIn, + TicketTypes, + TShirtSize, + TShirtType, +) from pydantic import ValidationError from repository.events_repository import EventsRepository from repository.payment_transaction_repository import PaymentTransactionRepository from starlette.responses import JSONResponse +from usecase.email_usecase import EmailUsecase +from usecase.pycon_registration_usecase import PyconRegistrationUsecase from utils.logger import logger @@ -18,6 +27,8 @@ class PaymentUsecase: def __init__(self): self.payment_repo = PaymentTransactionRepository() self.events_repo = EventsRepository() + self.pycon_registration_usecase = PyconRegistrationUsecase() + self.email_usecase = EmailUsecase() def create_payment_transaction(self, payment_transaction: PaymentTransactionIn) -> PaymentTransactionOut: """ @@ -114,6 +125,12 @@ def payment_callback(self, payment_transaction_id: str, event_id: str): Returns: JSONResponse -- The response to the client """ + logger.info( + f'Processing payment callback for payment transaction id: {payment_transaction_id} and event id: {event_id}' + ) + + frontend_base_url = os.getenv('FRONTEND_URL') + status, payment_transaction, message = self.payment_repo.query_payment_transaction_with_payment_transaction_id( payment_transaction_id=payment_transaction_id, event_id=event_id ) @@ -121,6 +138,34 @@ def payment_callback(self, payment_transaction_id: str, event_id: str): logger.error(f'[{payment_transaction_id}] {message}') return JSONResponse(status_code=status, content={'message': message}) + registration_data = self._extract_registration_data_from_payment_transaction( + payment_transaction, payment_transaction_id + ) + if not registration_data: + logger.error(f'[{payment_transaction_id}] Failed to extract registration data from payment transaction') + self._send_payment_failed_email(payment_transaction, payment_transaction_id, event_id) + error_redirect_url = ( + f'{frontend_base_url}/{event_id}/register?step=Error&paymentTransactionId={payment_transaction_id}' + ) + return JSONResponse( + status_code=302, + headers={'Location': error_redirect_url}, + content={'message': 'Redirecting to error page'}, + ) + + registration_result = self.pycon_registration_usecase.create_pycon_registration(registration_data) + if isinstance(registration_result, JSONResponse): + logger.error(f'[{payment_transaction_id}] Failed to create registration: {registration_result}') + self._send_payment_failed_email(payment_transaction, payment_transaction_id, event_id) + error_redirect_url = ( + f'{frontend_base_url}/{event_id}/register?step=Error&paymentTransactionId={payment_transaction_id}' + ) + return JSONResponse( + status_code=302, + headers={'Location': error_redirect_url}, + content={'message': 'Redirecting to error page'}, + ) + success_payment_transaction_in = PaymentTransactionIn( transactionStatus=TransactionStatus.SUCCESS, eventId=event_id ) @@ -129,10 +174,17 @@ def payment_callback(self, payment_transaction_id: str, event_id: str): ) if status != HTTPStatus.OK: logger.error(f'[{payment_transaction_id}] {message}') - return JSONResponse(status_code=status, content={'message': message}) + error_redirect_url = ( + f'{frontend_base_url}/{event_id}/register?step=Error&paymentTransactionId={payment_transaction_id}' + ) + return JSONResponse( + status_code=302, + headers={'Location': error_redirect_url}, + content={'message': 'Redirecting to error page'}, + ) logger.info(f'Payment transaction updated for {payment_transaction_id}') - frontend_base_url = os.getenv('FRONTEND_URL') + redirect_url = ( f'{frontend_base_url}/{event_id}/register?step=Success&paymentTransactionId={payment_transaction_id}' ) @@ -140,6 +192,132 @@ def payment_callback(self, payment_transaction_id: str, event_id: str): status_code=302, headers={'Location': redirect_url}, content={'message': 'Redirecting to success page'} ) + def _extract_registration_data_from_payment_transaction( + self, payment_transaction, payment_transaction_id: str + ) -> PyconRegistrationIn: + """ + Extract registration data from payment transaction and create PyconRegistrationIn object + + Arguments: + payment_transaction -- The payment transaction containing registration data + payment_transaction_id -- The ID of the payment transaction for logging + + Returns: + PyconRegistrationIn -- The registration data object or None if data is incomplete + """ + try: + # Convert enum string values back to enum types + ticket_type = ( + TicketTypes(payment_transaction.ticketType) if payment_transaction.ticketType else TicketTypes.CODER + ) + shirt_type = TShirtType(payment_transaction.shirtType) if payment_transaction.shirtType else None + shirt_size = TShirtSize(payment_transaction.shirtSize) if payment_transaction.shirtSize else None + + except ValueError as e: + logger.error(f'[{payment_transaction_id}] Invalid enum value in payment transaction: {e}') + return None + + try: + registration_data = PyconRegistrationIn( + firstName=payment_transaction.firstName, + lastName=payment_transaction.lastName, + nickname=payment_transaction.nickname, + pronouns=payment_transaction.pronouns, + email=payment_transaction.email, + eventId=payment_transaction.eventId, + contactNumber=payment_transaction.contactNumber, + organization=payment_transaction.organization, + jobTitle=payment_transaction.jobTitle, + facebookLink=payment_transaction.facebookLink, + linkedInLink=payment_transaction.linkedInLink, + ticketType=ticket_type, + sprintDay=payment_transaction.sprintDay or False, + availTShirt=payment_transaction.availTShirt or False, + shirtType=shirt_type, + shirtSize=shirt_size, + communityInvolvement=payment_transaction.communityInvolvement or False, + futureVolunteer=payment_transaction.futureVolunteer or False, + dietaryRestrictions=payment_transaction.dietaryRestrictions, + accessibilityNeeds=payment_transaction.accessibilityNeeds, + discountCode=payment_transaction.discountCode, + validIdObjectKey=payment_transaction.validIdObjectKey, + amountPaid=payment_transaction.price, + transactionId=payment_transaction_id, + ) + + logger.info(f'[{payment_transaction_id}] Successfully extracted registration data') + return registration_data + + except ValidationError as e: + logger.error(f'[{payment_transaction_id}] Validation error creating PyconRegistrationIn: {e}') + return None + + except AttributeError as e: + logger.error(f'[{payment_transaction_id}] Missing required attribute in payment transaction: {e}') + return None + + except TypeError as e: + logger.error(f'[{payment_transaction_id}] Type error in payment transaction data: {e}') + return None + + def _send_payment_failed_email(self, payment_transaction, payment_transaction_id: str, event_id: str): + """ + Send a payment failed email notification to the user + + Arguments: + payment_transaction -- The payment transaction object + payment_transaction_id -- The ID of the payment transaction + event_id -- The ID of the event + """ + try: + # Get event details + _, event_detail, _ = self.events_repo.query_events(event_id) + if not event_detail: + logger.error(f'[{payment_transaction_id}] Event details not found for eventId: {event_id}') + return + + # Extract email details from payment transaction + first_name = getattr(payment_transaction, 'firstName', 'User') + email = getattr(payment_transaction, 'email', None) + + if not email: + logger.error(f'[{payment_transaction_id}] No email found in payment transaction') + return + + # Check if this is a PyCon event + is_pycon_event = 'pycon' in event_detail.name.lower() if event_detail.name else False + + # Create email body similar to payment_tracking_usecase + def _create_failed_body(event_name: str, transaction_id: str) -> list[str]: + return [ + f'There was an issue processing your registration for {event_name}. Your payment may have been successful, but we encountered a problem creating your registration.', + f'Please contact our support team at durianpy.davao@gmail.com and present your transaction ID: {transaction_id}', + 'We will resolve this issue and ensure your registration is completed.', + ] + + # Determine email subject based on event type + if is_pycon_event: + subject = 'Issue with your PyCon Davao 2025 Registration' + else: + subject = f'Issue with your {event_detail.name} Registration' + + email_in = EmailIn( + to=[email], + subject=subject, + salutation=f'Hi {first_name},', + body=_create_failed_body(event_detail.name, payment_transaction_id), + regards=['Sincerely,'], + emailType=EmailType.REGISTRATION_EMAIL, + eventId=event_id, + isDurianPy=is_pycon_event, + ) + + self.email_usecase.send_email(email_in=email_in, event=event_detail) + logger.info(f'[{payment_transaction_id}] Payment failed email sent to {email}') + + except Exception as e: + logger.error(f'[{payment_transaction_id}] Failed to send payment failed email: {e}') + @staticmethod def __convert_data_entry_to_dict(data_entry): """Convert a data entry to a dictionary diff --git a/backend/usecase/registration_usecase.py b/backend/usecase/registration_usecase.py index 3df7053e..d9691c84 100644 --- a/backend/usecase/registration_usecase.py +++ b/backend/usecase/registration_usecase.py @@ -101,10 +101,11 @@ def create_registration(self, registration_in: RegistrationIn) -> Union[JSONResp message, ) = self.__registrations_repository.query_registrations_with_email(event_id=event_id, email=email) if status == HTTPStatus.OK and registrations: - return JSONResponse( - status_code=HTTPStatus.CONFLICT, - content={'message': f'Registration with email {email} already exists'}, - ) + logger.info(f'Registration with email {email} already exists, returning existing registration') + registration = registrations[0] + registration_data = self.__convert_data_entry_to_dict(registration) + registration_out = RegistrationOut(**registration_data) + return self.collect_pre_signed_url(registration_out) # check if ticket types in event exists future_registrations = event.registrationCount From 98db030ac767fe090626da5086fb7e1f0e32cf8e Mon Sep 17 00:00:00 2001 From: Arnel Jan Sarmiento Date: Mon, 8 Sep 2025 09:20:03 +0800 Subject: [PATCH 02/11] feat(registration): enhance registration handling by returning existing registration data instead of conflict response --- backend/usecase/pycon_registration_usecase.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/usecase/pycon_registration_usecase.py b/backend/usecase/pycon_registration_usecase.py index 89cb1208..9e14baf1 100644 --- a/backend/usecase/pycon_registration_usecase.py +++ b/backend/usecase/pycon_registration_usecase.py @@ -119,10 +119,11 @@ def create_pycon_registration( message, ) = self.__registrations_repository.query_registrations_with_email(event_id=event_id, email=email) if status == HTTPStatus.OK and registrations: - return JSONResponse( - status_code=HTTPStatus.CONFLICT, - content={'message': f'Registration with email {email} already exists'}, - ) + logger.info(f'Registration with email {email} already exists, returning existing registration') + registration = registrations[0] + registration_data = self.__convert_data_entry_to_dict(registration) + registration_out = PyconRegistrationOut(**registration_data) + return self.collect_pre_signed_url_pycon(registration_out) # check if ticket types in event exists future_registrations = event.registrationCount From a09591e4532545a5909cae9eeb9a7653aeeb7630 Mon Sep 17 00:00:00 2001 From: Arnel Jan Sarmiento Date: Mon, 8 Sep 2025 09:48:17 +0800 Subject: [PATCH 03/11] feat(payment): add handling for failed and cancelled payment notifications, including fetching existing registrations and enhancing email templates for user engagement --- backend/usecase/payment_tracking_usecase.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/usecase/payment_tracking_usecase.py b/backend/usecase/payment_tracking_usecase.py index 48835473..2313ffe0 100644 --- a/backend/usecase/payment_tracking_usecase.py +++ b/backend/usecase/payment_tracking_usecase.py @@ -66,6 +66,16 @@ def process_payment_event(self, message_body: dict) -> None: if not recorded_registration_data: logger.error(f'Failed to save registration for entryId {entry_id}') + elif transaction_status == TransactionStatus.FAILED: + status, registrations, msg = self.registration_repository.query_registrations_with_email( + event_id=event_id, email=registration_data.email + ) + if status == HTTPStatus.OK and registrations: + logger.info( + f'Skipping failed payment email for {registration_data.email} - user already has existing registration' + ) + return + self._send_email_notification( first_name=registration_data.firstName, email=registration_data.email, From 4fa2f380e975a09948171e1c1e615ac2898d8bce Mon Sep 17 00:00:00 2001 From: Arnel Jan Sarmiento Date: Mon, 8 Sep 2025 20:40:36 +0800 Subject: [PATCH 04/11] feat(permissions): add DynamoDB index permissions for paymentHandler to enhance data access --- backend/resources/functions.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/resources/functions.yml b/backend/resources/functions.yml index 6634dd41..826c4974 100644 --- a/backend/resources/functions.yml +++ b/backend/resources/functions.yml @@ -38,6 +38,7 @@ paymentHandler: - "dynamodb:UpdateItem" Resource: - "arn:aws:dynamodb:${self:provider.region}:${aws:accountId}:table/${self:custom.registrations}" + - "arn:aws:dynamodb:${self:provider.region}:${aws:accountId}:table/${self:custom.registrations}/index/*" - "arn:aws:dynamodb:${self:provider.region}:${aws:accountId}:table/${self:custom.entities}" - "arn:aws:dynamodb:${self:provider.region}:${aws:accountId}:table/${self:custom.events}" - "arn:aws:dynamodb:${self:provider.region}:${aws:accountId}:table/${self:custom.events}/index/*" From cc8ff8a03617b91cef0413fa98330d52654af7cb Mon Sep 17 00:00:00 2001 From: ASPactores Date: Thu, 11 Sep 2025 19:28:18 +0800 Subject: [PATCH 05/11] feat(payment): enhance payment event processing to skip duplicate registrations based on email --- backend/usecase/payment_tracking_usecase.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/usecase/payment_tracking_usecase.py b/backend/usecase/payment_tracking_usecase.py index 2313ffe0..9161b2c0 100644 --- a/backend/usecase/payment_tracking_usecase.py +++ b/backend/usecase/payment_tracking_usecase.py @@ -21,6 +21,7 @@ def __init__(self): self.email_usecase = EmailUsecase() self.event_repository = EventsRepository() self.payment_transaction_repository = PaymentTransactionRepository() + self.registration_repository = RegistrationsRepository() def process_payment_event(self, message_body: dict) -> None: """ @@ -59,6 +60,16 @@ def process_payment_event(self, message_body: dict) -> None: logger.info(f'Payment transaction status updated to {transaction_status} for entryId {entry_id}') + status, registration_details, _ = self.registration_repository.query_registrations_with_email( + event_id=event_id, email=registration_data.email + ) + + if status == HTTPStatus.OK and registration_details: + logger.info( + f'Skipping duplicate email for {registration_data.email} - user already has existing registration' + ) + return + if transaction_status == TransactionStatus.SUCCESS: recorded_registration_data = self._create_and_save_registration( payment_tracking_body=payment_tracking_body From e898f5c690d3ee0d5b3479fc729978e60b773942 Mon Sep 17 00:00:00 2001 From: ASPactores Date: Wed, 10 Sep 2025 19:41:39 +0800 Subject: [PATCH 06/11] feat(discount): implement function to adjust max usage of reusable discount codes --- .isort.cfg | 2 +- backend/scripts/change_discount_code_uses.py | 81 ++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 backend/scripts/change_discount_code_uses.py diff --git a/.isort.cfg b/.isort.cfg index 07456f53..47fb658a 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,2 +1,2 @@ [settings] -known_third_party = aws,boto3,botocore,constants,controller,external_gateway,fastapi,fastapi_cloudauth,lambda_decorators,lambdawarmer,mangum,model,pydantic,pynamodb,pytz,repository,requests,starlette,ulid,usecase,utils +known_third_party = aws,boto3,botocore,constants,controller,dotenv,external_gateway,fastapi,fastapi_cloudauth,lambda_decorators,lambdawarmer,mangum,model,pydantic,pynamodb,pytz,repository,requests,starlette,typing_extensions,ulid,usecase,utils diff --git a/backend/scripts/change_discount_code_uses.py b/backend/scripts/change_discount_code_uses.py new file mode 100644 index 00000000..14c51474 --- /dev/null +++ b/backend/scripts/change_discount_code_uses.py @@ -0,0 +1,81 @@ +import argparse +import os +from copy import deepcopy + +from dotenv import load_dotenv +from typing_extensions import Literal + +script_dir = os.path.dirname(os.path.abspath(__file__)) +args = argparse.ArgumentParser() +args.add_argument('--env-file', type=str, default=os.path.join(script_dir, '..', '.env'), help='Path to the .env file') +load_dotenv(dotenv_path=args.parse_args().env_file) + +from model.discount.discount import DiscountDBIn +from repository.discount_repository import DiscountsRepository +from utils.logger import logger + + +def change_discount_code_uses( + event_id: str, discount_id: str, uses_change: int, operation: Literal['add', 'deduct'] +) -> None: + """ + Adjusts the max usage of a reusable discount code. + + Args: + event_id (str): The ID of the event. + discount_id (str): The ID of the discount code. + uses_change (int): The number of uses to add or deduct. Must be a positive integer. + operation (Literal['add', 'deduct']): The type of operation to perform. + """ + discount_repository = DiscountsRepository() + status, discount_entry, message = discount_repository.query_discount_with_discount_id( + event_id=event_id, discount_id=discount_id + ) + + if not status: + logger.error(f"Error changing uses of discount code '{discount_id}' for event '{event_id}'. {message}") + return + + original_discount_entry = deepcopy(discount_entry) + + if not discount_entry.isReusable: + logger.warning(f"Discount code '{discount_id}' is not reusable. Cannot change max uses.") + return + + if uses_change < 0: + logger.error('`uses_change` must be a non-negative integer.') + return + + new_max_uses = 0 + if operation == 'add': + new_max_uses = discount_entry.maxDiscountUses + uses_change + elif operation == 'deduct': + new_max_uses = discount_entry.maxDiscountUses - uses_change + + if new_max_uses < discount_entry.currentDiscountUses: + logger.error( + f'Cannot deduct {uses_change} uses. The new max uses ({new_max_uses}) would be less than the current uses ({discount_entry.currentDiscountUses}).' + ) + return + else: + logger.error(f"Invalid operation '{operation}'. Must be 'add' or 'deduct'.") + return + + discount_entry.maxDiscountUses = new_max_uses + discount_entry.remainingUses = new_max_uses - discount_entry.currentDiscountUses + + logger.info( + f"Changed max uses for discount code '{discount_id}' for event '{event_id}' from {original_discount_entry.maxDiscountUses} to {discount_entry.maxDiscountUses}." + ) + + if discount_entry: + discount_repository.update_discount( + discount_entry=original_discount_entry, discount_in=DiscountDBIn(**discount_entry.attribute_values) + ) + + +if __name__ == '__main__': + event_id = 'test-event-id' + entry_id = 'test-entry-id' + uses = 50 + change_discount_code_uses(event_id=event_id, discount_id=entry_id, uses_change=uses, operation='deduct') From 86312d25c767645d6013ff13719f30ee4ed0cad4 Mon Sep 17 00:00:00 2001 From: ASPactores Date: Tue, 9 Sep 2025 03:22:36 +0800 Subject: [PATCH 07/11] feat(email): add endpoint to resend confirmation email for registrations --- .../pycon_registration_controller.py | 21 ++++++++++++++ backend/usecase/pycon_registration_usecase.py | 29 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/backend/controller/pycon_registration_controller.py b/backend/controller/pycon_registration_controller.py index 5d774a58..b7f439a3 100644 --- a/backend/controller/pycon_registration_controller.py +++ b/backend/controller/pycon_registration_controller.py @@ -278,3 +278,24 @@ def delete_registration( _ = current_user registrations_uc = PyconRegistrationUsecase() return registrations_uc.delete_pycon_registration(event_id=event_id, registration_id=entry_id) + + +@pycon_registration_router.post( + '/resend-confirmation', + status_code=HTTPStatus.OK, + responses={ + 200: {'model': Message, 'description': 'Confirmation email sent successfully'}, + 400: {'model': Message, 'description': 'Bad request'}, + 404: {'model': Message, 'description': 'Registration not found'}, + }, + summary='Resend confirmation email', +) +def resend_confirmation_email( + email: EmailStr = Body(..., embed=True, title='Email'), + event_id: str = Body(..., embed=True, title='Event Id'), +): + """ + Resend the confirmation email for a specific registration. + """ + registrations_uc = PyconRegistrationUsecase() + return registrations_uc.resend_confirmation_email(event_id=event_id, email=email) diff --git a/backend/usecase/pycon_registration_usecase.py b/backend/usecase/pycon_registration_usecase.py index 9e14baf1..8275cb50 100644 --- a/backend/usecase/pycon_registration_usecase.py +++ b/backend/usecase/pycon_registration_usecase.py @@ -419,6 +419,35 @@ def collect_pre_signed_url_pycon(self, registration: PyconRegistrationOut) -> Py return registration + def resend_confirmation_email(self, event_id: str, email: str): + """Resends the registration confirmation email for a specific PyCon registration entry. + + :param event_id: The ID of the event + :type event_id: str + + :param email: The email address of the registrant + :type email: str + + :return: If successful, returns a JSONResponse indicating the email was sent. If unsuccessful, returns a JSONResponse with an error message. + :rtype: JSONResponse + + """ + status, event, message = self.__events_repository.query_events(event_id=event_id) + if status != HTTPStatus.OK: + return JSONResponse(status_code=status, content={'message': message}) + + (status, registrations, message) = self.__registrations_repository.query_registrations_with_email( + event_id=event_id, email=email + ) + + if status == HTTPStatus.OK and registrations and registrations[0].transactionId: + registration = registrations[0] + logger.info(f'Resending confirmation email to {email} for event {event_id}') + self.__email_usecase.send_registration_creation_email(registration=registration, event=event) + return JSONResponse(status_code=HTTPStatus.OK, content={'message': f'Confirmation email sent to {email}'}) + + return JSONResponse(status_code=status, content={'message': message}) + @staticmethod def __convert_data_entry_to_dict(data_entry): """Converts a data entry to a dictionary. From 49441283e4af9ba910a58cac8fb3ddd7e3893f4c Mon Sep 17 00:00:00 2001 From: ASPactores Date: Fri, 12 Sep 2025 02:34:52 +0800 Subject: [PATCH 08/11] feat(email): implement resend confirmation email functionality for event registrations --- backend/scripts/resend_confirmation_email.py | 29 ++++ .../pycon_registration_email_notification.py | 128 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 backend/scripts/resend_confirmation_email.py create mode 100644 backend/usecase/pycon_registration_email_notification.py diff --git a/backend/scripts/resend_confirmation_email.py b/backend/scripts/resend_confirmation_email.py new file mode 100644 index 00000000..0a67b68c --- /dev/null +++ b/backend/scripts/resend_confirmation_email.py @@ -0,0 +1,29 @@ +import argparse +import os +from http import HTTPStatus + +from dotenv import load_dotenv + +script_dir = os.path.dirname(os.path.abspath(__file__)) +parser = argparse.ArgumentParser() +parser.add_argument( + '--env-file', type=str, default=os.path.join(script_dir, '..', '.env'), help='Path to the .env file' +) +parser.add_argument('--email', type=str, required=True, help='Registrant email address') +parser.add_argument('--event-id', type=str, required=True, help='Event ID') +args = parser.parse_args() +load_dotenv(dotenv_path=args.env_file) + + +from usecase.pycon_registration_email_notification import ( + PyConRegistrationEmailNotification, +) +from utils.logger import logger + +if __name__ == '__main__': + usecase = PyConRegistrationEmailNotification() + response = usecase.resend_confirmation_email(event_id=args.event_id, email=args.email) + if response.status_code == HTTPStatus.OK: + logger.info(f'Successfully resent confirmation email to {args.email} for event {args.event_id}') + else: + logger.error(f'Failed to resend confirmation email: {response.content}') diff --git a/backend/usecase/pycon_registration_email_notification.py b/backend/usecase/pycon_registration_email_notification.py new file mode 100644 index 00000000..2dcf52ea --- /dev/null +++ b/backend/usecase/pycon_registration_email_notification.py @@ -0,0 +1,128 @@ +from http import HTTPStatus + +from constants.common_constants import EmailType +from fastapi.responses import JSONResponse +from model.email.email import EmailIn +from model.events.event import Event +from model.payments.payments import PaymentTransactionOut +from repository.events_repository import EventsRepository +from repository.registrations_repository import RegistrationsRepository +from usecase.email_usecase import EmailUsecase +from utils.logger import logger + + +class PyConRegistrationEmailNotification: + def __init__(self): + self.__email_usecase = EmailUsecase() + self.__registrations_repository = RegistrationsRepository() + self.__events_repository = EventsRepository() + + def send_registration_success_email(self, email: str, event: Event, is_pycon_event: bool = True) -> None: + logger.info(f'Preparing to send registration success email to {email} for event {event.name}') + _, registration, _ = self.__registrations_repository.query_registrations_with_email( + email=email, event_id=event.eventId + ) + registration_data = registration[0] if registration else None + + if not registration: + logger.error(f'No registration found for email: {email} and event_id: {event.eventId}') + return + + body = [ + f"Thank you for registering for {event.name}! Your payment was successful, and we're excited to see you at the event." + if not is_pycon_event + else "Thank you for registering for PyCon Davao 2025 by DurianPy! Your payment was successful, and we're excited to see you at the event.", + self.__email_bold_element('Below is a summary of your registration details:'), + self.__email_list_elements( + [ + f'Registration ID: {registration_data.registrationId}', + f"Ticket Type: {str(registration_data.ticketType).capitalize() if registration_data.ticketType else 'N/A'}", + f"Sprint Day Participation: {'Yes' if registration_data.sprintDay else 'No'}", + f"Amount Paid: ₱{registration_data.amountPaid if registration_data.amountPaid else '0'}", + f"Transaction ID: {registration_data.transactionId if registration_data.transactionId else 'N/A'}", + ] + ), + self.__email_newline_element(), + 'See you at the event!', + ] + + email_in = EmailIn( + to=[email], + subject=f'Issue with your {event.name} Payment' + if not is_pycon_event + else 'Issue with your PyCon Davao 2025 Payment', + salutation=f'Dear {registration_data.firstName},' + if registration_data and registration_data.firstName + else 'Dear Attendee,', + body=body, + regards=['Best,'], + emailType=EmailType.REGISTRATION_EMAIL, + eventId=str(event.eventId), + isDurianPy=is_pycon_event, + ) + self.__email_usecase.send_email(email_in=email_in, event=event) + logger.info(f'Sent registration success email to {email} for event {event.name}') + + def send_registration_failure_email( + self, email: str, event: Event, payment_transaction: PaymentTransactionOut, is_pycon_event: bool = True + ) -> None: + body = [ + f'There was an issue processing your payment for {event.name}. Please check your payment details or try again.', + f'If the problem persists, please contact our support team at durianpy.davao@gmail.com and present your transaction ID: {payment_transaction.transactionId}.' + if is_pycon_event + else f'If the problem persists, please contact our support team at {event.email} and present your transaction ID: {payment_transaction.transactionId}.', + ] + + email_in = EmailIn( + to=[email], + subject=f'Issue with your {event.name} Payment' + if not is_pycon_event + else 'Issue with your PyCon Davao 2025 Payment', + salutation='Dear Attendee,' + if not payment_transaction.registrationData + else f'Dear {payment_transaction.registrationData.firstName},', + body=body, + regards=['Sincerely,'], + emailType=EmailType.REGISTRATION_EMAIL, + eventId=event.eventId, + isDurianPy=is_pycon_event, + ) + + self.__email_usecase.send_email(email_in=email_in, event=event) + logger.info(f'Sent registration failure email to {email} for event {event.name}') + + def resend_confirmation_email(self, event_id: str, email: str) -> JSONResponse: + event_status, event_detail, event_message = self.__events_repository.query_events(event_id=event_id) + if event_status != HTTPStatus.OK: + return JSONResponse(status_code=event_status, content={'message': event_message}) + + reg_status, registrations, reg_message = self.__registrations_repository.query_registrations_with_email( + event_id=event_id, email=email + ) + + if reg_status != HTTPStatus.OK or not registrations or not registrations[0].transactionId: + message = reg_message if reg_message else 'Registration not found or incomplete.' + return JSONResponse(status_code=HTTPStatus.NOT_FOUND, content={'message': message}) + + logger.info( + f'Found registration for email {email} and event {event_detail.name}, resending confirmation email.' + ) + + try: + self.send_registration_success_email(email=email, event=event_detail, is_pycon_event=True) + logger.info(f'Resent confirmation email to {email} for event {event_id}') + return JSONResponse(status_code=HTTPStatus.OK, content={'message': f'Confirmation email sent to {email}'}) + except Exception as e: + logger.error(f'Failed to resend confirmation email to {email}: {e}') + return JSONResponse( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, content={'message': 'Failed to send email.'} + ) + + def __email_list_elements(self, elements: list[str]) -> str: + return '\n'.join([f'
  • {element}
  • ' for element in elements]) + + def __email_bold_element(self, element: str) -> str: + return f'{element}' + + def __email_newline_element(self) -> str: + return '
    ' From b45d8d21093392e067428fce8dc8fae10255666b Mon Sep 17 00:00:00 2001 From: ASPactores Date: Fri, 12 Sep 2025 02:49:17 +0800 Subject: [PATCH 09/11] refactor(email): remove resend confirmation email endpoint and associated logic --- .../pycon_registration_controller.py | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/backend/controller/pycon_registration_controller.py b/backend/controller/pycon_registration_controller.py index b7f439a3..5d774a58 100644 --- a/backend/controller/pycon_registration_controller.py +++ b/backend/controller/pycon_registration_controller.py @@ -278,24 +278,3 @@ def delete_registration( _ = current_user registrations_uc = PyconRegistrationUsecase() return registrations_uc.delete_pycon_registration(event_id=event_id, registration_id=entry_id) - - -@pycon_registration_router.post( - '/resend-confirmation', - status_code=HTTPStatus.OK, - responses={ - 200: {'model': Message, 'description': 'Confirmation email sent successfully'}, - 400: {'model': Message, 'description': 'Bad request'}, - 404: {'model': Message, 'description': 'Registration not found'}, - }, - summary='Resend confirmation email', -) -def resend_confirmation_email( - email: EmailStr = Body(..., embed=True, title='Email'), - event_id: str = Body(..., embed=True, title='Event Id'), -): - """ - Resend the confirmation email for a specific registration. - """ - registrations_uc = PyconRegistrationUsecase() - return registrations_uc.resend_confirmation_email(event_id=event_id, email=email) From 82104abe69bbe1b2f8bd2419b78ec643ac37720d Mon Sep 17 00:00:00 2001 From: ASPactores Date: Mon, 22 Sep 2025 00:35:33 +0800 Subject: [PATCH 10/11] fix(event): fix ticket type dissapearing issue --- backend/usecase/event_usecase.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/usecase/event_usecase.py b/backend/usecase/event_usecase.py index f39f9ce2..b45a89ea 100644 --- a/backend/usecase/event_usecase.py +++ b/backend/usecase/event_usecase.py @@ -17,6 +17,7 @@ from starlette.responses import JSONResponse from usecase.email_usecase import EmailUsecase from usecase.file_s3_usecase import FileS3Usecase +from utils.logger import logger from utils.utils import Utils @@ -101,7 +102,9 @@ def update_event(self, event_id: str, event_in: EventIn) -> Union[JSONResponse, if event_in.ticketTypes: _, ticket_types_entries, _ = self.__ticket_type_repository.query_ticket_types(event_id=event_id) - existing_ticket_types_map = {ticket_type.name: ticket_type for ticket_type in ticket_types_entries or []} + existing_ticket_types_map = { + Utils.convert_to_slug(ticket_type.name): ticket_type for ticket_type in ticket_types_entries or [] + } for ticket_type in event_in.ticketTypes: ticket_type.eventId = event_id @@ -121,7 +124,7 @@ def update_event(self, event_id: str, event_in: EventIn) -> Union[JSONResponse, # Delete ticket types not present in the input ticket_types_in_input = {Utils.convert_to_slug(ticket_type.name) for ticket_type in event_in.ticketTypes} for existing_ticket_type in existing_ticket_types_map.values(): - if existing_ticket_type.name in ticket_types_in_input: + if Utils.convert_to_slug(existing_ticket_type.name) in ticket_types_in_input: continue status, message = self.__ticket_type_repository.delete_ticket_type(existing_ticket_type) From b2bdf9741bd93f56cea646c3facc28ee7990fe52 Mon Sep 17 00:00:00 2001 From: ASPactores Date: Mon, 22 Sep 2025 00:42:21 +0800 Subject: [PATCH 11/11] refactor(event): remove unused logger import from event_usecase.py --- backend/usecase/event_usecase.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/usecase/event_usecase.py b/backend/usecase/event_usecase.py index b45a89ea..88d70519 100644 --- a/backend/usecase/event_usecase.py +++ b/backend/usecase/event_usecase.py @@ -17,7 +17,6 @@ from starlette.responses import JSONResponse from usecase.email_usecase import EmailUsecase from usecase.file_s3_usecase import FileS3Usecase -from utils.logger import logger from utils.utils import Utils