Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions backend/usecase/payment_tracking_usecase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
184 changes: 181 additions & 3 deletions backend/usecase/payment_usecase.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
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


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:
"""
Expand Down Expand Up @@ -114,13 +125,47 @@ 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
)
if status != HTTPStatus.OK:
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
)
Expand All @@ -129,17 +174,150 @@ 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}'
)
return JSONResponse(
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
Expand Down
9 changes: 5 additions & 4 deletions backend/usecase/pycon_registration_usecase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions backend/usecase/registration_usecase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down