diff --git a/backend/usecase/payment_tracking_usecase.py b/backend/usecase/payment_tracking_usecase.py index 4883547..2313ffe 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, diff --git a/backend/usecase/payment_usecase.py b/backend/usecase/payment_usecase.py index 707253f..96a1c21 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/pycon_registration_usecase.py b/backend/usecase/pycon_registration_usecase.py index 89cb120..9e14baf 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 diff --git a/backend/usecase/registration_usecase.py b/backend/usecase/registration_usecase.py index 3df7053..d9691c8 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