WalletCallback Lambda Function
Quick Reference
- Function Name:
payment3-wallet-callback-${BRANCH} - Handler:
walletcallback.lambda_handler - Runtime: Python 3.12 (Docker Image)
- Trigger: API Gateway (POST /wallet/callback)
- Template.yaml: Lines 201-243
Function Overview
The WalletCallback function processes wallet payment callbacks (e.g., TrueMoney Wallet, Rabbit LINE Pay). It saves callback records to WalletCallbackRecordTable and invokes ProcessPaidWalletOrderInternal for successful payments. This function uses the BaseCallbackProcessor pattern shared with Amex and BankApp callbacks.
Entry Point
File: functions/callback/walletcallback.py
Handler: lambda_handler(event, _)
def lambda_handler(event, _):
"""
Record wallet callback.
"""
processor = None
try:
print(f"Processing wallet callback event: {json.dumps(event)}")
processor = BaseCallbackProcessor(
event=event,
record_table_class=WalletCallbackRecordTable,
function_name="Wallet"
)
processor.save()
response = processor.response
print(f"Wallet callback processed successfully: {json.dumps(response)}")
if processor.success:
processor.call_processpaid("wallet-order-internal")
return response
except Exception as e:
error_msg = f"Error processing wallet callback: {e} {errorString()}"
print(error_msg)
# Extract orderId and chargeId for error logging
orderId = None
chargeId = None
if processor:
try:
orderId = processor.order_id
chargeId = processor.payment_id
except (AttributeError, KeyError):
# Processor may not have these attributes if initialization failed
pass
# Add context to Sentry
with sentry_sdk.push_scope() as scope:
if orderId:
scope.set_tag("orderId", orderId)
if chargeId:
scope.set_tag("chargeId", chargeId)
scope.set_tag("function", "walletcallback")
scope.set_context("callback_processor", {
"has_processor": processor is not None,
"chargeId": chargeId,
"orderId": orderId,
})
sentry_sdk.capture_exception(e)
# Log to callback error log table
log_callback_error(
error=e,
function_name="walletcallback",
event=event,
orderId=orderId,
chargeId=chargeId,
response_data=None,
metadata={"errorString": errorString()}
)
# Send email notification to admin
try:
send_callback_error_email(
error=e,
function_name="walletcallback",
orderId=orderId,
chargeId=chargeId,
event=event
)
except Exception as email_error:
print(f"Failed to send error email: {email_error}")
# Don't fail the whole function if email fails
return {
"statusCode": 500,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(
{
"success": False,
"error": str(e),
"message": "Failed to process wallet callback",
}
),
}
Event Structure
API Gateway Event
Path: POST /wallet/callback
Auth: NONE (public endpoint - called by payment gateway)
Request Body (JSON):
{
"orderId": "482500007436",
"paymentId": "pay_wallet_1234567890",
"success": true,
"amount": 2339.0,
"userId": "b5f392f7-6d89-433c-b24f-bf6a772cab1a",
"message": "Payment successful"
}
Required Fields:
orderId- Villa order ID (required)paymentId- Payment gateway payment ID (required)success- Payment success status: true or false (required)userId- Villa user ID (required)amount- Paid amount (optional)message- Optional message
Response (Success):
{
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": "{\"success\": true, \"message\": \"Wallet callback recorded successfully\", \"orderId\": \"...\", \"paymentId\": \"...\", \"amount\": ...}"
}
Response (Error):
{
"statusCode": 500,
"headers": {"Content-Type": "application/json"},
"body": "{\"success\": false, \"error\": \"error message\", \"message\": \"Failed to process wallet callback\"}"
}
DynamoDB Tables
WalletCallbackRecordTable
Table Name: payment3-wallet-callback-record-{BRANCH}
Region: ap-southeast-1
PynamoDB Model:
class WalletCallbackRecordTable(Model):
class Meta:
table_name = f"payment3-wallet-callback-record-{BRANCH}"
region = "ap-southeast-1"
orderId = UnicodeAttribute(hash_key=True)
paymentId = UnicodeAttribute()
success = BooleanAttribute()
userId = UnicodeAttribute()
message = UnicodeAttribute(null=True)
created_at = UnicodeAttribute()
updated_at = UnicodeAttribute()
event = JSONAttribute(null=True)
body = JSONAttribute(null=True)
amount = NumberAttribute(null=True)
def __post_init__(self):
if not self.created_at:
self.created_at = datetime.now().isoformat()
if not self.updated_at:
self.updated_at = datetime.now().isoformat()
Key Fields:
- Hash Key:
orderId(UnicodeAttribute) paymentId- Payment gateway payment IDsuccess- Payment success status (BooleanAttribute)userId- Villa user IDmessage- Optional messageamount- Paid amount (NumberAttribute, optional)event- Full event data (JSONAttribute, optional)body- Request body (JSONAttribute, optional)created_at- Creation timestamp (ISO format)updated_at- Update timestamp (ISO format)
Save Pattern:
from src.walletCallbackRecordTable import WalletCallbackRecordTable
record = WalletCallbackRecordTable(
orderId=order_id,
paymentId=payment_id,
success=success,
userId=user_id,
message=message,
created_at=datetime.now().isoformat(),
updated_at=datetime.now().isoformat(),
event=event,
body=body,
amount=amount
)
record.save()
Lambda Invocations
ProcessPaidWalletOrderInternal
Function: payment3-process-paid-wallet-order-internal-${BRANCH}
Invoked When: processor.success is True
Payload: Full request body (JSON string)
Invocation Type: Event (asynchronous)
Code (from BaseCallbackProcessor):
def call_processpaid(self, internal_function_name: str):
"""Call the process-paid-order Lambda function."""
lambda_client = Lambda()
lambda_client.invoke(
functionName=f"payment3-process-paid-{internal_function_name}-{BRANCH}",
input=json.dumps(self.body),
invocationType="Event",
)
Processing Flow
Main Flow
- Create Processor: Initialize BaseCallbackProcessor with WalletCallbackRecordTable
- Validate Input: Check required fields (orderId, paymentId, success, userId)
- Save Record: Save callback to WalletCallbackRecordTable
- Invoke Process Function: Call ProcessPaidWalletOrderInternal if success is True
- Return Response: Return JSON response
Detailed Algorithm
1. Parse event body (JSON or dict)
2. Create BaseCallbackProcessor:
- record_table_class = WalletCallbackRecordTable
- function_name = "Wallet"
3. Validate required fields:
- orderId (required)
- paymentId (required)
- success (required)
- userId (required)
4. Create WalletCallbackRecordTable record:
- orderId = order_id
- paymentId = payment_id
- success = success
- userId = userId
- message = message (optional)
- amount = amount (optional)
- event = event
- body = body
- created_at = datetime.now().isoformat()
- updated_at = datetime.now().isoformat()
5. Save record to DynamoDB
6. If success is True:
- Invoke ProcessPaidWalletOrderInternal with body as input
7. Return JSON response
Error Handling
Validation Errors
Missing Required Fields: Raised by BaseCallbackProcessor
# Validate required fields
if not self.order_id:
raise ValueError("orderId is required in body")
if self.payment_id is None:
raise ValueError("paymentId is required in body")
if self.success is None:
raise ValueError("success is required in body")
if not self.userId:
raise ValueError("userId is required in body")
Handling:
- Exception caught in lambda_handler
- Logged to CallbackErrorLogTable
- Sent to Sentry with context tags
- Error email sent via SES
- Returns 500 error response
Save Errors
Exception: Caught in BaseCallbackProcessor.save()
def save(self):
try:
record = self.record_table_class(
orderId=self.order_id,
paymentId=self.payment_id,
success=self.success,
userId=self.userId,
message=self.message,
created_at=self.created_at,
updated_at=self.updated_at,
event=self.event,
body=self.body,
amount=self.amount,
)
record.save()
print(
f"{self.function_name} callback record saved successfully for orderId: {self.order_id}"
)
except Exception as e:
print(f"Error saving {self.function_name} callback record: {e}")
sentry_sdk.capture_exception(e)
raise
IAM Policies
From template.yaml (lines 210-230):
DynamoDBWritePolicyforWalletCallbackRecordTable(line 211-212)DynamoDBReadPolicyfororder-table-dev(line 213-214)DynamoDBCrudPolicyforpayment3-card-payment-record-master(line 215-216)LambdaInvokePolicyforProcessPaidWalletOrderInternal(line 217-218)AWSSecretsManagerGetSecretValuePolicyfor kbank-dev (line 219-220)AWSSecretsManagerGetSecretValuePolicyfor kbank-prod (line 221-222)DynamoDBWritePolicyforCallbackErrorLogTable(line 223-225)SESCrudPolicyfor sending error emails (line 226-228)
Dependencies
External Lambda Functions
payment3-process-paid-wallet-order-internal-${BRANCH}- Process paid wallet orders (invoked)
DynamoDB Tables
payment3-wallet-callback-record-${BRANCH}- Callback records (write)order-table-dev- Order data (read)payment3-card-payment-record-master- Master payment records (read)payment3-callback-error-log-${BRANCH}- Error logs (write)
AWS Services
- SES: Send error notification emails
Python Dependencies
lambdasdk- Lambda invocation utilitiespynamodb- DynamoDB ORMsentry-sdk- Error tracking
Testing
Local Testing
File: functions/callback/walletcallback.py (lines 98-124)
if __name__ == "__main__":
import os
os.environ["BRANCH"] = "dev"
# Test event
test_event = {
"body": json.dumps(
{
"orderId": "test_order_123",
"paymentId": "test_payment_456",
"success": True,
"amount": 100,
"userId": "test_user_789",
"message": "Test payment successful",
}
)
}
print("Testing wallet callback handler...")
try:
r = lambda_handler(test_event, None)
print("✅ Test successful!")
print(f"Response: {json.dumps(r, indent=2)}")
except Exception as e:
print(f"❌ Test failed: {e}")
Test Event Example
{
"body": "{\"orderId\": \"482500007436\", \"paymentId\": \"pay_wallet_123\", \"success\": true, \"amount\": 2339.0, \"userId\": \"b5f392f7-6d89-433c-b24f-bf6a772cab1a\", \"message\": \"Payment successful\"}"
}
SAM Testing
sam local invoke WalletCallback -e test_event.json
Code Structure
File Organization:
functions/callback/
├── walletcallback.py # Main handler
└── src/
├── base_callback_processor.py # Base processor (shared)
└── walletCallbackRecordTable.py # Wallet callback table
Key Classes:
BaseCallbackProcessor- Shared processor for wallet/amex/bankappWalletCallbackRecordTable- Wallet callback record table model
Related Functions
- ProcessPaidWalletOrderInternal - Processes paid wallet orders
- BankAppCallback - Similar callback handler (uses same table)
- AmexCallback - Similar callback handler (different table)
Common Patterns
BaseCallbackProcessor Pattern
This function uses the BaseCallbackProcessor pattern shared with Amex and BankApp callbacks:
def __init__(self, event, record_table_class: Type[Model], function_name: str):
self.event = event
# Handle both string and dict body
if isinstance(event, dict):
body = event.get("body", event)
else:
body = event
if isinstance(body, str):
self.body = json.loads(body)
elif isinstance(body, dict):
self.body = body
else:
raise ValueError(f"Unexpected body type: {type(body)}")
self.order_id = self.body.get("orderId") or self.body.get("order_id")
self.payment_id = self.body.get("paymentId") or self.body.get("payment_id")
self.success = self.body.get("success")
self.userId = self.body.get("userId") or self.body.get("user_id")
self.message = self.body.get("message", "")
self.amount = self.body.get("amount", 0)
# Validate required fields
if not self.order_id:
raise ValueError("orderId is required in body")
if self.payment_id is None:
raise ValueError("paymentId is required in body")
if self.success is None:
raise ValueError("success is required in body")
if not self.userId:
raise ValueError("userId is required in body")
self.created_at = datetime.now().isoformat()
self.updated_at = datetime.now().isoformat()
self.record_table_class = record_table_class
self.function_name = function_name
Troubleshooting
Common Issues
-
Missing Required Fields
- Verify request body contains orderId, paymentId, success, userId
- Check JSON parsing is correct
-
Save Failures
- Check DynamoDB permissions
- Verify table exists
- Check CloudWatch logs for detailed errors
-
ProcessPaidWalletOrderInternal Not Invoked
- Verify success field is True (boolean, not string)
- Check Lambda invocation permissions
Debugging
- Check CloudWatch logs for execution flow
- Review CallbackErrorLogTable for error history
- Check Sentry for exception tracking
- Verify WalletCallbackRecordTable records are being saved
- Test with known good order IDs
References
- Template.yaml: Lines 201-243
- Related Functions: ProcessPaidWalletOrderInternal, BankAppCallback, AmexCallback
- Base Processor:
src/base_callback_processor.py