Skip to content

Commit 66f1eda

Browse files
author
Arnel Jan Sarmiento
committed
feat: Enhance payment transaction handling by adding registration data support and updating dependencies
1 parent 83e9d5f commit 66f1eda

File tree

8 files changed

+185
-42
lines changed

8 files changed

+185
-42
lines changed

backend/controller/payment_controller.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastapi import APIRouter, Query
1+
from fastapi import APIRouter, Body, Path, Query
22
from fastapi.responses import JSONResponse
33
from model.common import Message
44
from model.payments.payments import PaymentTransactionIn, PaymentTransactionOut
@@ -66,8 +66,10 @@ def get_pending_payment_transactions():
6666
summary='Update payment transaction',
6767
)
6868
def update_payment_transaction(
69-
payment_transaction_id: str,
70-
payment_transaction: PaymentTransactionIn,
69+
payment_transaction_id: str = Path(
70+
..., description='The ID of the payment transaction', alias='paymentTransactionId'
71+
),
72+
payment_transaction: PaymentTransactionIn = Body(..., description='The payment transaction data'),
7173
):
7274
"""
7375
Update payment transaction

backend/model/payments/payments.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
from typing import Optional
33

44
from model.entities import Entities
5-
from model.pycon_registrations.pycon_registration import PyconRegistration
5+
from model.pycon_registrations.pycon_registration import (
6+
PyconRegistration,
7+
PyconRegistrationOut,
8+
)
69
from pydantic import BaseModel, Field
7-
from pynamodb.attributes import NumberAttribute, UnicodeAttribute
10+
from pynamodb.attributes import BooleanAttribute, NumberAttribute, UnicodeAttribute
811

912

1013
class TransactionStatus(str, Enum):
@@ -21,15 +24,40 @@ class PaymentTransaction(Entities, discriminator='PaymentTransaction'):
2124
eventId = UnicodeAttribute(null=False)
2225
transactionStatus = UnicodeAttribute(null=False)
2326

24-
# registration data
25-
firstName: str = Field(None, title='First Name')
26-
lastName: str = Field(None, title='Last Name')
27-
contactNumber: str = Field(None, title='Contact Number')
28-
careerStatus: str = Field(None, title='Career Status')
29-
yearsOfExperience: str = Field(None, title='Years of Experience')
30-
organization: str = Field(None, title='Organization')
31-
title: str = Field(None, title='Title')
32-
paymentRequestId: str = Field(None, title='Payment Request ID')
27+
# registration data - core info
28+
firstName = UnicodeAttribute(null=True)
29+
lastName = UnicodeAttribute(null=True)
30+
nickname = UnicodeAttribute(null=True)
31+
pronouns = UnicodeAttribute(null=True)
32+
email = UnicodeAttribute(null=True)
33+
contactNumber = UnicodeAttribute(null=True)
34+
organization = UnicodeAttribute(null=True)
35+
jobTitle = UnicodeAttribute(null=True)
36+
37+
# social media (will be stored as JSON string)
38+
socials = UnicodeAttribute(null=True)
39+
40+
# ticket and event preferences
41+
ticketType = UnicodeAttribute(null=True)
42+
sprintDay = BooleanAttribute(null=True)
43+
44+
# t-shirt preferences
45+
availTShirt = BooleanAttribute(null=True)
46+
shirtType = UnicodeAttribute(null=True)
47+
shirtSize = UnicodeAttribute(null=True)
48+
49+
# community and preferences
50+
communityInvolvement = BooleanAttribute(null=True)
51+
futureVolunteer = BooleanAttribute(null=True)
52+
dietaryRestrictions = UnicodeAttribute(null=True)
53+
accessibilityNeeds = UnicodeAttribute(null=True)
54+
55+
# discount and files
56+
discountCode = UnicodeAttribute(null=True)
57+
imageId = UnicodeAttribute(null=True)
58+
59+
# payment specific
60+
paymentRequestId = UnicodeAttribute(null=True)
3361

3462

3563
class PaymentTransactionIn(BaseModel):
@@ -48,3 +76,4 @@ class Config:
4876
extra = 'ignore'
4977

5078
entryId: str = Field(..., title='Entry ID')
79+
registrationData: Optional[PyconRegistrationOut] = Field(None, title='Registration Data')

backend/model/pycon_registrations/pycon_registration.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,8 @@ def check_shirt_availability(cls, values):
8888
if values.get('availTShirt') and (values.get('shirtType') is None or values.get('shirtSize') is None):
8989
raise ValueError('If availTShirt is True, then shirtType and shirtSize must be provided.')
9090
return values
91+
92+
93+
class PyconRegistrationOut(PyconRegistration):
94+
class Config:
95+
extra = 'ignore'

backend/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ dependencies = [
1717
"python-dateutil==2.9.0.post0",
1818
"requests==2.32.3",
1919
"pytz==2024.2",
20-
"commitizen>=4.8.3",
2120
]
2221

2322
[dependency-groups]
2423
dev = [
24+
"commitizen>=4.8.3",
2525
"boto3==1.40.11",
2626
"python-dotenv==1.0.0",
2727
"uvicorn==0.22.0",

backend/repository/payment_transaction_repository.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ def store_payment_transaction(
4646
4747
"""
4848
data = RepositoryUtils.load_data(pydantic_schema_in=payment_transaction_in)
49+
registration_data = data.pop('registrationData', {}) or {}
50+
4951
event_id = payment_transaction_in.eventId
5052
entry_id = ulid()
5153
hash_key = f'{self.core_obj}#{event_id}'
@@ -62,6 +64,7 @@ def store_payment_transaction(
6264
entryStatus=EntryStatus.ACTIVE.value,
6365
entryId=entry_id,
6466
**data,
67+
**registration_data,
6568
)
6669
payment_transaction_entry.save()
6770

@@ -180,6 +183,54 @@ def query_payment_transaction_with_payment_transaction_id(
180183
logger.info(f'[{self.core_obj}={payment_transaction_id}] Fetch PaymentTransaction data successful')
181184
return HTTPStatus.OK, payment_transaction_entries[0], None
182185

186+
def query_payment_transaction_by_id_only(
187+
self, payment_transaction_id: str
188+
) -> Tuple[HTTPStatus, PaymentTransaction, str]:
189+
"""Query payment_transaction by payment_transaction ID only (using scan).
190+
191+
:param payment_transaction_id: The ID of the payment_transaction to query.
192+
:type payment_transaction_id: str
193+
194+
:return: The HTTP status, the queried payment_transaction or None, and a message.
195+
:rtype: Tuple[HTTPStatus, PaymentTransaction, str]
196+
197+
"""
198+
try:
199+
# Use scan to find payment transaction by ID across all events
200+
range_key_suffix = f'#{payment_transaction_id}'
201+
202+
filter_condition = PaymentTransaction.rangeKey.contains(range_key_suffix)
203+
filter_condition &= PaymentTransaction.entryStatus == EntryStatus.ACTIVE.value
204+
filter_condition &= PaymentTransaction.rangeKey.startswith(f'v{self.latest_version}#')
205+
206+
payment_transaction_entries = list(
207+
PaymentTransaction.scan(
208+
filter_condition=filter_condition,
209+
)
210+
)
211+
if not payment_transaction_entries:
212+
message = f'PaymentTransaction with ID = {payment_transaction_id} not found'
213+
logger.error(f'[{self.core_obj} = {payment_transaction_id}] {message}')
214+
return HTTPStatus.NOT_FOUND, None, message
215+
216+
except ScanError as e:
217+
message = f'Failed to scan payment_transaction: {str(e)}'
218+
logger.error(f'[{self.core_obj}={payment_transaction_id}] {message}')
219+
return HTTPStatus.INTERNAL_SERVER_ERROR, None, message
220+
221+
except TableDoesNotExist as db_error:
222+
message = f'Error on Table, Please check config to make sure table is created: {str(db_error)}'
223+
logger.error(f'[{self.core_obj}={payment_transaction_id}] {message}')
224+
return HTTPStatus.INTERNAL_SERVER_ERROR, None, message
225+
226+
except PynamoDBConnectionError as db_error:
227+
message = f'Connection error occurred, Please check config(region, table name, etc): {str(db_error)}'
228+
logger.error(f'[{self.core_obj}={payment_transaction_id}] {message}')
229+
return HTTPStatus.INTERNAL_SERVER_ERROR, None, message
230+
else:
231+
logger.info(f'[{self.core_obj}={payment_transaction_id}] Fetch PaymentTransaction by ID successful')
232+
return HTTPStatus.OK, payment_transaction_entries[0], None
233+
183234
def update_payment_transaction(
184235
self, payment_transaction: PaymentTransaction, payment_transaction_in: PaymentTransactionIn
185236
) -> Tuple[HTTPStatus, PaymentTransaction, str]:
@@ -199,11 +250,15 @@ def update_payment_transaction(
199250
new_version = current_version + 1
200251

201252
data = RepositoryUtils.load_data(pydantic_schema_in=payment_transaction_in, exclude_unset=True)
253+
registration_data = data.pop('registrationData', {}) or {}
254+
merged_data = {**data, **registration_data}
255+
202256
has_update, updated_data = RepositoryUtils.get_update(
203-
old_data=RepositoryUtils.db_model_to_dict(payment_transaction), new_data=data
257+
old_data=RepositoryUtils.db_model_to_dict(payment_transaction), new_data=merged_data
204258
)
205259
if not has_update:
206260
return HTTPStatus.OK, payment_transaction, 'no update'
261+
207262
try:
208263
with TransactWrite(connection=self.conn) as transaction:
209264
# Update Entry -----------------------------------------------------------------------------

backend/requirements.txt

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,35 @@
11
# This file was autogenerated by uv via the following command:
22
# uv export --format requirements-txt --no-hashes --output-file requirements.txt --no-dev
33
anyio==4.9.0
4-
argcomplete==3.6.2
54
boto3==1.40.11
65
botocore==1.40.11
76
certifi==2025.7.14
87
cffi==1.17.1 ; platform_python_implementation != 'PyPy'
98
charset-normalizer==3.4.2
10-
colorama==0.4.6
11-
commitizen==4.8.3
129
cryptography==45.0.5
13-
decli==0.6.3
1410
dnspython==2.7.0
1511
ecdsa==0.19.1
1612
email-validator==2.2.0
1713
fastapi==0.96.0
1814
fastapi-cloudauth==0.4.3
1915
idna==3.10
20-
importlib-metadata==8.7.0
21-
jinja2==3.1.6
2216
jmespath==1.0.1
2317
lambda-decorators==0.6.0
2418
lambda-warmer-py==0.6.0
2519
mangum==0.15.0
26-
markupsafe==3.0.2
27-
packaging==25.0
28-
prompt-toolkit==3.0.51
2920
pyasn1==0.6.1
3021
pycparser==2.22 ; platform_python_implementation != 'PyPy'
3122
pydantic==1.10.22
3223
pynamodb==6.1.0
3324
python-dateutil==2.9.0.post0
3425
python-jose==3.5.0
3526
pytz==2024.2
36-
pyyaml==6.0.2
37-
questionary==2.1.0
3827
requests==2.32.3
3928
rsa==4.9.1
4029
s3transfer==0.13.1
4130
six==1.17.0
4231
sniffio==1.3.1
4332
starlette==0.27.0
44-
termcolor==3.1.0
45-
tomlkit==0.13.3
4633
typing-extensions==4.8.0
4734
ulid==1.1
4835
urllib3==1.26.20
49-
wcwidth==0.2.13
50-
zipp==3.23.0

backend/usecase/payment_usecase.py

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
PaymentTransactionOut,
77
TransactionStatus,
88
)
9+
from model.pycon_registrations.pycon_registration import PyconRegistrationOut
10+
from pydantic import ValidationError
911
from repository.events_repository import EventsRepository
1012
from repository.payment_transaction_repository import PaymentTransactionRepository
1113
from starlette.responses import JSONResponse
14+
from utils.logger import logger
1215

1316

1417
class PaymentUsecase:
@@ -17,41 +20,103 @@ def __init__(self):
1720
self.events_repo = EventsRepository()
1821

1922
def create_payment_transaction(self, payment_transaction: PaymentTransactionIn) -> PaymentTransactionOut:
23+
"""
24+
Create a new payment transaction
25+
26+
Arguments:
27+
payment_transaction -- The payment transaction data
28+
29+
Returns:
30+
PaymentTransactionOut -- The created payment transaction
31+
"""
32+
logger.info(f'Creating payment transaction for {payment_transaction.eventId}')
2033
status, payment_transaction, message = self.payment_repo.store_payment_transaction(payment_transaction)
2134
if status != HTTPStatus.OK:
35+
logger.error(f'[{payment_transaction.eventId}] {message}')
2236
return JSONResponse(status_code=status, content={'message': message})
2337

38+
logger.info(f'Payment transaction created for {payment_transaction.eventId}')
2439
payment_transaction_dict = self.__convert_data_entry_to_dict(payment_transaction)
2540
return PaymentTransactionOut(**payment_transaction_dict)
2641

2742
def update_payment_transaction(
28-
self, payment_transaction_id: str, payment_transaction: PaymentTransactionIn
43+
self, payment_transaction_id: str, payment_transaction_in: PaymentTransactionIn
2944
) -> PaymentTransactionOut:
30-
status, payment_transaction, message = self.payment_repo.update_payment_transaction(
31-
payment_transaction_id, payment_transaction
45+
"""
46+
Update the payment transaction with the existing object
47+
48+
Arguments:
49+
payment_transaction_id -- The ID of the payment transaction
50+
payment_transaction_in -- The payment transaction data
51+
52+
Returns:
53+
PaymentTransactionOut -- The updated payment transaction
54+
"""
55+
logger.info(f'Updating payment transaction for {payment_transaction_id}')
56+
status, existing_payment_transaction, message = self.payment_repo.query_payment_transaction_by_id_only(
57+
payment_transaction_id=payment_transaction_id
3258
)
3359
if status != HTTPStatus.OK:
3460
return JSONResponse(status_code=status, content={'message': message})
3561

36-
payment_transaction_dict = self.__convert_data_entry_to_dict(payment_transaction)
62+
# Now update the payment transaction with the existing object
63+
status, updated_payment_transaction, message = self.payment_repo.update_payment_transaction(
64+
existing_payment_transaction, payment_transaction_in
65+
)
66+
if status != HTTPStatus.OK:
67+
logger.error(f'[{payment_transaction_id}] {message}')
68+
return JSONResponse(status_code=status, content={'message': message})
69+
70+
logger.info(f'Payment transaction updated for {payment_transaction_id}')
71+
payment_transaction_dict = self.__convert_data_entry_to_dict(updated_payment_transaction)
3772
return PaymentTransactionOut(**payment_transaction_dict)
3873

3974
def query_pending_payment_transactions(self) -> list[PaymentTransactionOut]:
75+
"""
76+
Query all pending payment transactions
77+
78+
Returns:
79+
list[PaymentTransactionOut] -- The list of payment transactions
80+
"""
4081
status, payment_transactions, message = self.payment_repo.query_pending_payment_transactions()
4182
if status != HTTPStatus.OK:
83+
logger.error(f'[{message}]')
4284
return JSONResponse(status_code=status, content={'message': message})
4385

44-
payment_transactions_out = [
45-
PaymentTransactionOut(**self.__convert_data_entry_to_dict(payment_transaction))
46-
for payment_transaction in payment_transactions
47-
]
48-
return payment_transactions_out
86+
payment_transaction_list = []
87+
for payment_transaction in payment_transactions:
88+
payment_transaction_data = self.__convert_data_entry_to_dict(payment_transaction)
89+
90+
try:
91+
pycon_registration_out = PyconRegistrationOut(**payment_transaction_data)
92+
payment_transaction_out = PaymentTransactionOut(
93+
**payment_transaction_data, registrationData=pycon_registration_out
94+
)
95+
96+
except ValidationError as e:
97+
logger.error(f'[{payment_transaction.rangeKey}] {e}, skipping this payment transaction')
98+
continue
99+
100+
payment_transaction_list.append(payment_transaction_out)
101+
102+
return payment_transaction_list
49103

50104
def payment_callback(self, payment_transaction_id: str, event_id: str):
105+
"""
106+
Update the payment transaction status to SUCCESS and redirect to the success page
107+
108+
Arguments:
109+
payment_transaction_id -- The ID of the payment transaction
110+
event_id -- The ID of the event
111+
112+
Returns:
113+
JSONResponse -- The response to the client
114+
"""
51115
status, payment_transaction, message = self.payment_repo.query_payment_transaction_with_payment_transaction_id(
52116
payment_transaction_id=payment_transaction_id, event_id=event_id
53117
)
54118
if status != HTTPStatus.OK:
119+
logger.error(f'[{payment_transaction_id}] {message}')
55120
return JSONResponse(status_code=status, content={'message': message})
56121

57122
success_payment_transaction_in = PaymentTransactionIn(
@@ -61,8 +126,10 @@ def payment_callback(self, payment_transaction_id: str, event_id: str):
61126
payment_transaction=payment_transaction, payment_transaction_in=success_payment_transaction_in
62127
)
63128
if status != HTTPStatus.OK:
129+
logger.error(f'[{payment_transaction_id}] {message}')
64130
return JSONResponse(status_code=status, content={'message': message})
65131

132+
logger.info(f'Payment transaction updated for {payment_transaction_id}')
66133
frontend_base_url = os.getenv('FRONTEND_URL')
67134
redirect_url = (
68135
f'{frontend_base_url}/{event_id}/register?step=Success&paymentTransactionId={payment_transaction_id}'

0 commit comments

Comments
 (0)