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})