From 7b8a6656b4a16bed4134fbeec649cbff56c896a5 Mon Sep 17 00:00:00 2001 From: Anakin Skywalker Pactores Date: Mon, 7 Sep 2026 18:01:15 +0800 Subject: [PATCH] chore: use receipt emaiil for pycon registration flow --- backend/usecase/payment_tracking_usecase.py | 218 +++++++----------- .../pycon_registration_email_notification.py | 130 ++++++++--- backend/usecase/pycon_registration_usecase.py | 18 +- 3 files changed, 201 insertions(+), 165 deletions(-) diff --git a/backend/usecase/payment_tracking_usecase.py b/backend/usecase/payment_tracking_usecase.py index 1803f4d2..004dc50e 100644 --- a/backend/usecase/payment_tracking_usecase.py +++ b/backend/usecase/payment_tracking_usecase.py @@ -3,7 +3,6 @@ from typing import Optional import ulid -from model.email.email import EmailIn, EmailType from model.events.event import Event from model.payments.payments import PaymentTrackingBody, TransactionStatus from model.pycon_registrations.pycon_registration import PyconRegistrationIn @@ -11,26 +10,26 @@ from repository.events_repository import EventsRepository from repository.payment_transaction_repository import PaymentTransactionRepository from repository.registrations_repository import RegistrationsRepository -from usecase.email_usecase import EmailUsecase +from usecase.pycon_registration_email_notification import ( + PyConRegistrationEmailNotification, +) from utils.logger import log_execution, logger, mask_email class PaymentTrackingUsecase: def __init__(self): self.registration_repository = RegistrationsRepository() - self.email_usecase = EmailUsecase() self.event_repository = EventsRepository() self.payment_transaction_repository = PaymentTransactionRepository() - self.registration_repository = RegistrationsRepository() + self.pycon_email_notification = PyConRegistrationEmailNotification() @log_execution - def process_payment_event(self, message_body: dict) -> None: + def process_payment_event(self, message_body: dict, is_pycon_event: bool = True) -> None: """ Processes a payment event message, updates the payment transaction status, and stores the registration details. """ try: - logger.info('Processing payment event message') self._update_timestamps(message_body) payment_tracking_body = PaymentTrackingBody(**message_body) @@ -39,15 +38,27 @@ def process_payment_event(self, message_body: dict) -> None: registration_data = registration_details.registrationData event_id = registration_details.eventId entry_id = registration_details.entryId + masked_email = ( + mask_email(registration_data.email) + if registration_data and registration_data.email + else 'unknown' + ) recorded_registration_data = None + logger.info( + f'Processing payment event message for entry_id={entry_id}, event_id={event_id}, ' + f'status={transaction_status}, email={masked_email}' + ) + if transaction_status == TransactionStatus.PENDING: - logger.info(f'Skipping PENDING message for entryId: {entry_id}') + logger.info( + f'Skipping PENDING payment event for entry_id={entry_id}, event_id={event_id}, email={masked_email}' + ) return _, event_detail, _ = self.event_repository.query_events(event_id) if not event_detail: - logger.error(f'Event details not found for eventId: {event_id}') + logger.error(f'Event details not found for event_id={event_id}, entry_id={entry_id}') raise ValueError(f'Event details not found for eventId: {event_id}') # Update Payment Transaction Status @@ -56,18 +67,24 @@ def process_payment_event(self, message_body: dict) -> None: ) if status != HTTPStatus.OK: - logger.error(f'Failed to update payment transaction status for entryId {entry_id}: {msg}') + logger.error( + f'Failed to update payment transaction status to {transaction_status} for entry_id={entry_id}, ' + f'event_id={event_id}: {msg}' + ) return - logger.info(f'Payment transaction status updated to {transaction_status} for entryId {entry_id}') + logger.info( + f'Payment transaction status updated to {transaction_status} for entry_id={entry_id}, event_id={event_id}' + ) - status, registration_details, _ = self.registration_repository.query_registrations_with_email( + status, existing_registrations, _ = self.registration_repository.query_registrations_with_email( event_id=event_id, email=registration_data.email ) - if status == HTTPStatus.OK and registration_details: + if status == HTTPStatus.OK and existing_registrations: logger.info( - f'Skipping duplicate email for {mask_email(registration_data.email)} - user already has existing registration' + f'Skipping duplicate registration for entry_id={entry_id}, event_id={event_id}, ' + f'email={masked_email} - user already has existing registration' ) return @@ -76,10 +93,13 @@ def process_payment_event(self, message_body: dict) -> None: payment_tracking_body=payment_tracking_body ) if not recorded_registration_data: - logger.error(f'Failed to save registration for entryId {entry_id}') + logger.error( + f'Failed to save registration for entry_id={entry_id}, event_id={event_id}, email={masked_email}' + ) else: logger.info( - f'Registration created via payment tracking for entry_id={entry_id}, event_id={event_id}, email={mask_email(registration_data.email)}' + f'Registration created via payment tracking for entry_id={entry_id}, ' + f'event_id={event_id}, email={masked_email}' ) elif transaction_status == TransactionStatus.FAILED: @@ -88,23 +108,48 @@ def process_payment_event(self, message_body: dict) -> None: ) if status == HTTPStatus.OK and registrations: logger.info( - f'Skipping failed payment email for {mask_email(registration_data.email)} - user already has existing registration' + f'Skipping failed payment email for entry_id={entry_id}, event_id={event_id}, ' + f'email={masked_email} - user already has existing registration' ) return - self._send_email_notification( - first_name=registration_data.firstName, - email=registration_data.email, - transaction_id=entry_id, - recorded_registration=recorded_registration_data, - ticket_type=registration_data.ticketType.value, - status=transaction_status, - event_detail=event_detail, + if transaction_status == TransactionStatus.SUCCESS: + logger.info( + f'Triggering registration success email for entry_id={entry_id}, event_id={event_id}, email={masked_email}' + ) + self.pycon_email_notification.send_registration_success_email( + email=registration_data.email, + event=event_detail, + is_pycon_event=is_pycon_event, + registration_data=recorded_registration_data, + ) + elif transaction_status == TransactionStatus.FAILED: + logger.info( + f'Triggering registration failure email for entry_id={entry_id}, event_id={event_id}, email={masked_email}' + ) + self.pycon_email_notification.send_registration_failure_email( + email=registration_data.email, + event=event_detail, + payment_transaction=registration_details, + is_pycon_event=is_pycon_event, + ) + logger.info( + f'Successfully processed payment event for entry_id={entry_id}, event_id={event_id}, ' + f'status={transaction_status}, email={masked_email}' ) - logger.info(f'Successfully processed registration for {mask_email(registration_data.email)}') except Exception as e: - logger.error(f'Failed to process successful payment for entryId {registration_details.entryId}: {e}') + entry_id = ( + message_body.get('registration_details', {}).get('entryId', 'unknown') + if isinstance(message_body, dict) + else 'unknown' + ) + event_id = ( + message_body.get('registration_details', {}).get('eventId', 'unknown') + if isinstance(message_body, dict) + else 'unknown' + ) + logger.error(f'Failed to process payment event for entry_id={entry_id}, event_id={event_id}: {e}') raise def _update_timestamps(self, message_body: dict): @@ -162,117 +207,22 @@ def _create_and_save_registration(self, payment_tracking_body: PaymentTrackingBo entryStatus=payment_tracking_body.status.value, ) - status, registration_data, _ = self.registration_repository.store_registration( + status, stored_registration, message = self.registration_repository.store_registration( registration_in=registration_in, registration_id=registration_id ) - logger.info( - f'Registration stored with status: {status}, registrationId: {registration_id}, data: {registration_data}' - ) - - return registration_data - - def _send_email_notification( - self, - email: str, - first_name: str, - transaction_id: str, - ticket_type: str, - status: TransactionStatus, - event_detail: Event, - is_pycon_event: bool = True, - recorded_registration: Optional[Registration] = None, - ): - """ - Sends an email notification based on the transaction status and event type. - """ - if not event_detail: - logger.error('Event details are missing. Cannot send email.') - return - - def _email_list_elements(elements: list[str]) -> str: - return '\n'.join([f'
  • {element}
  • ' for element in elements]) - - def _email_bold_element(element: str) -> str: - return f'{element}' - - def _email_newline_element() -> str: - return '
    ' - - def _create_success_body(reg_data: Registration, ticket: str) -> list[str]: - logger.info(f'Creating success email body for registration: {reg_data}') - base_body = [ - f"Thank you for registering for {event_detail.name}! Your payment was successful, and we're excited to see you at the event.", - _email_bold_element('Below is a summary of your registration details:'), - ] - - list_items = [ - f'Registration ID: {reg_data.registrationId if reg_data.registrationId else "N/A"}', - f'Ticket Type: {ticket.capitalize() if ticket else "N/A"}', - f'Sprint Day Participation: {"Yes" if reg_data.sprintDay else "No"}', - f'Amount Paid: ₱{reg_data.amountPaid if reg_data.amountPaid else "0"}', - f'Transaction ID: {reg_data.transactionId if reg_data.transactionId else "N/A"}', - f'Payment Method: {reg_data.paymentMethod.capitalize() if reg_data.paymentMethod else "N/A"}', - ] - - if is_pycon_event: - base_body[0] = ( - "Thank you for registering for PyCon Davao 2026 by DurianPy! Your payment was successful, and we're excited to see you at the event." - ) - list_items.pop() - - base_body.append(_email_list_elements(list_items)) - base_body.append(_email_newline_element()) - base_body.append('See you there!') - - return base_body - - def _create_failed_body(name: str, transaction_id: str) -> list[str]: - return [ - f'There was an issue processing your payment for {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: {transaction_id}', - ] - - templates = { - TransactionStatus.SUCCESS: { - 'subject': f"You're all set for {event_detail.name}!", - 'salutation': f'Hi {first_name},', - 'body': lambda: _create_success_body(recorded_registration, ticket_type), - 'regards': ['Best,'], - }, - TransactionStatus.FAILED: { - 'subject': f'Issue with your {event_detail.name} Payment', - 'salutation': f'Hi {first_name},', - 'body': lambda: _create_failed_body(event_detail.name, transaction_id), - 'regards': ['Sincerely,'], - }, - } - - if is_pycon_event: - templates[TransactionStatus.SUCCESS]['subject'] = "You're all set for PyCon Davao 2026!" - templates[TransactionStatus.FAILED]['subject'] = 'Issue with your PyCon Davao 2026 Payment' - - template = templates.get(status) + if status == HTTPStatus.OK: + logger.info( + f'Registration stored successfully with status={status}, registration_id={registration_id}, ' + f'event_id={registration_in.eventId}, email={mask_email(registration_in.email)}, ' + f'ticket_type={registration_in.ticketType}' + ) + else: + logger.error( + f'Failed to store registration with status={status}, registration_id={registration_id}, ' + f'event_id={registration_in.eventId}, email={mask_email(registration_in.email)}: {message}' + ) - if not template: - logger.error(f'No email template found for status: {status}') - return + return stored_registration - logger.info( - f'Preparing to send email for event {event_detail.eventId} with status {status} to {mask_email(email)}.' - ) - email_in = EmailIn( - to=[email], - subject=template['subject'], - salutation=template['salutation'], - body=template['body'](), - regards=template['regards'], - emailType=EmailType.REGISTRATION_EMAIL, - eventId=event_detail.eventId, - isDurianPy=is_pycon_event, - ) - self.email_usecase.send_email(email_in=email_in, event=event_detail) - logger.info( - f'Email notification sent for event {event_detail.eventId} with status {status} to {mask_email(email)}.' - ) diff --git a/backend/usecase/pycon_registration_email_notification.py b/backend/usecase/pycon_registration_email_notification.py index c5246502..0c26574f 100644 --- a/backend/usecase/pycon_registration_email_notification.py +++ b/backend/usecase/pycon_registration_email_notification.py @@ -1,55 +1,83 @@ from http import HTTPStatus +from typing import Optional -from constants.common_constants import EmailType from fastapi.responses import JSONResponse -from model.email.email import EmailIn +from model.email.email import EmailIn, EmailType from model.events.event import Event from model.payments.payments import PaymentTransactionOut +from model.registrations.registration import Registration from repository.events_repository import EventsRepository from repository.registrations_repository import RegistrationsRepository from usecase.email_usecase import EmailUsecase from utils.logger import log_execution, logger, mask_email +DURIANPY_BCC_EMAIL = 'durianpy.davao+email_service@gmail.com' + + class PyConRegistrationEmailNotification: - def __init__(self): + def __init__(self) -> None: self.__email_usecase = EmailUsecase() self.__registrations_repository = RegistrationsRepository() self.__events_repository = EventsRepository() @log_execution - def send_registration_success_email(self, email: str, event: Event, is_pycon_event: bool = True) -> None: + def send_registration_success_email( + self, + email: str, + event: Event, + is_pycon_event: bool = True, + registration_data: Optional[Registration] = None, + bcc: Optional[list[str]] = None, + ) -> None: masked_email = mask_email(email) - logger.info(f'Preparing to send registration success email to {masked_email} for event {event.name}') - _, registration, _ = self.__registrations_repository.query_registrations_with_email( - email=email, event_id=event.eventId + logger.info( + f'Preparing to send registration success email to {masked_email} for event {event.name} (event_id={event.eventId})' ) - registration_data = registration[0] if registration else None + if not registration_data: + _, 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: + if not registration_data: logger.error(f'No registration found for email: {masked_email} and event_id: {event.eventId}') return + ticket_type_val = ( + registration_data.ticketType.value + if hasattr(registration_data.ticketType, 'value') + else registration_data.ticketType + ) + + details = [ + f'Email: {email}', + f'Registration ID: {registration_data.registrationId}', + f'Ticket Type: {str(ticket_type_val).capitalize() if ticket_type_val 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"}', + ] + + contact_email = 'durianpy.davao@gmail.com' if is_pycon_event else (event.email or 'durianpy.davao@gmail.com') + bcc_list = list({DURIANPY_BCC_EMAIL, *(b for b in (bcc or []) if b)}) + 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 2026 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_list_elements(details), + self.__email_newline_element(), + f"If you have any questions or need assistance, please don't hesitate to reach out to us at {contact_email}. We're here to help!", self.__email_newline_element(), 'See you at the event!', ] email_in = EmailIn( to=[email], + cc=None, + bcc=bcc_list, subject=f"You're all set for {event.name}!" if not is_pycon_event else "You're all set for PyCon Davao 2026!", @@ -64,23 +92,51 @@ def send_registration_success_email(self, email: str, event: Event, is_pycon_eve ) self.__email_usecase.send_email(email_in=email_in, event=event) logger.info( - f'Registration success email sent to {masked_email} for event {event.name} (event_id={event.eventId})' + f'Registration success email sent to {masked_email} for event {event.name} ' + f'(event_id={event.eventId}, registration_id={registration_data.registrationId})' ) @log_execution def send_registration_failure_email( - self, email: str, event: Event, payment_transaction: PaymentTransactionOut, is_pycon_event: bool = True + self, + email: str, + event: Event, + payment_transaction: PaymentTransactionOut, + is_pycon_event: bool = True, + bcc: Optional[list[str]] = None, ) -> None: masked_email = mask_email(email) + tx_id = ( + payment_transaction.transactionId + or getattr(payment_transaction, 'entryId', None) + or 'N/A' + ) + logger.info( + f'Preparing to send registration failure email to {masked_email} for event {event.name} ' + f'(event_id={event.eventId}, transaction_id={tx_id})' + ) + + details = [ + f'Email: {email}', + f'Transaction ID: {tx_id}', + ] + + bcc_list = list({DURIANPY_BCC_EMAIL, *(b for b in (bcc or []) if b)}) + 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}.' + self.__email_bold_element('Payment details:'), + self.__email_list_elements(details), + self.__email_newline_element(), + f'If the problem persists, please contact our support team at durianpy.davao@gmail.com and present your transaction ID: {tx_id}.' 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}.', + else f'If the problem persists, please contact our support team at {event.email} and present your transaction ID: {tx_id}.', ] email_in = EmailIn( to=[email], + cc=None, + bcc=bcc_list, subject=f'Issue with your {event.name} Payment' if not is_pycon_event else 'Issue with your PyCon Davao 2026 Payment', @@ -97,15 +153,22 @@ def send_registration_failure_email( self.__email_usecase.send_email(email_in=email_in, event=event) logger.info( f'Registration failure email sent to {masked_email} for event {event.name} ' - f'(transaction_id={payment_transaction.transactionId})' + f'(event_id={event.eventId}, transaction_id={tx_id})' ) @log_execution def resend_confirmation_email(self, event_id: str, email: str) -> JSONResponse: masked_email = mask_email(email) + logger.info(f'Processing resend confirmation email request for email={masked_email}, event_id={event_id}') 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}) + if isinstance(event_detail, list): + event_detail = event_detail[0] if event_detail else None + + if event_status != HTTPStatus.OK or not isinstance(event_detail, Event): + logger.error( + f'Event not found for event_id={event_id} while resending confirmation email to {masked_email}: {event_message}' + ) + return JSONResponse(status_code=event_status or HTTPStatus.NOT_FOUND, content={'message': event_message}) reg_status, registrations, reg_message = self.__registrations_repository.query_registrations_with_email( event_id=event_id, email=email @@ -113,18 +176,27 @@ def resend_confirmation_email(self, event_id: str, email: str) -> JSONResponse: 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.' + logger.warning( + f'Registration not found or incomplete for email={masked_email}, event_id={event_id}: {message}' + ) return JSONResponse(status_code=HTTPStatus.NOT_FOUND, content={'message': message}) logger.info( - f'Found registration for email {masked_email} and event {event_detail.name}, resending confirmation email.' + f'Found registration for email={masked_email} and event={event_detail.name} (event_id={event_id}), ' + f'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 {masked_email} for event {event_id}') + self.send_registration_success_email( + email=email, + event=event_detail, + is_pycon_event=True, + registration_data=registrations[0], + ) + logger.info(f'Resent confirmation email to {masked_email} for event_id={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 {masked_email}: {e}') + logger.error(f'Failed to resend confirmation email to {masked_email} for event_id={event_id}: {e}') return JSONResponse( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, content={'message': 'Failed to send email.'} ) diff --git a/backend/usecase/pycon_registration_usecase.py b/backend/usecase/pycon_registration_usecase.py index b30d1e83..63d7759d 100644 --- a/backend/usecase/pycon_registration_usecase.py +++ b/backend/usecase/pycon_registration_usecase.py @@ -20,6 +20,9 @@ from usecase.discount_usecase import DiscountUsecase from usecase.email_usecase import EmailUsecase from usecase.file_s3_usecase import FileS3Usecase +from usecase.pycon_registration_email_notification import ( + PyConRegistrationEmailNotification, +) from utils.logger import log_execution, logger, mask_email @@ -38,6 +41,7 @@ def __init__(self): self.__registrations_repository = RegistrationsRepository() self.__events_repository = EventsRepository() self.__email_usecase = EmailUsecase() + self.__pycon_email_notification = PyConRegistrationEmailNotification() self.__discount_usecase = DiscountUsecase() self.__file_s3_usecase = FileS3Usecase() self.__ticket_type_repository = TicketTypeRepository() @@ -212,7 +216,12 @@ def create_pycon_registration( registration_data = self.__convert_data_entry_to_dict(registration) if not registration.registrationEmailSent: - self.__email_usecase.send_registration_creation_email(registration=registration, event=event) + self.__pycon_email_notification.send_registration_success_email( + email=registration.email, + event=event, + is_pycon_event=True, + registration_data=registration, + ) registration_out = PyconRegistrationOut(**registration_data) return self.collect_pre_signed_url_pycon(registration_out) @@ -475,7 +484,12 @@ def resend_confirmation_email(self, event_id: str, email: str): if status == HTTPStatus.OK and registrations and registrations[0].transactionId: registration = registrations[0] logger.info(f'Resending confirmation email for event {event_id} to {mask_email(email)}') - self.__email_usecase.send_registration_creation_email(registration=registration, event=event) + self.__pycon_email_notification.send_registration_success_email( + email=email, + event=event, + is_pycon_event=True, + registration_data=registration, + ) return JSONResponse(status_code=HTTPStatus.OK, content={'message': f'Confirmation email sent to {email}'}) return JSONResponse(status_code=status, content={'message': message})