Skip to main content

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 ID
  • success - Payment success status (BooleanAttribute)
  • userId - Villa user ID
  • message - Optional message
  • amount - 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

  1. Create Processor: Initialize BaseCallbackProcessor with WalletCallbackRecordTable
  2. Validate Input: Check required fields (orderId, paymentId, success, userId)
  3. Save Record: Save callback to WalletCallbackRecordTable
  4. Invoke Process Function: Call ProcessPaidWalletOrderInternal if success is True
  5. 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):

  • DynamoDBWritePolicy for WalletCallbackRecordTable (line 211-212)
  • DynamoDBReadPolicy for order-table-dev (line 213-214)
  • DynamoDBCrudPolicy for payment3-card-payment-record-master (line 215-216)
  • LambdaInvokePolicy for ProcessPaidWalletOrderInternal (line 217-218)
  • AWSSecretsManagerGetSecretValuePolicy for kbank-dev (line 219-220)
  • AWSSecretsManagerGetSecretValuePolicy for kbank-prod (line 221-222)
  • DynamoDBWritePolicy for CallbackErrorLogTable (line 223-225)
  • SESCrudPolicy for 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 utilities
  • pynamodb - DynamoDB ORM
  • sentry-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/bankapp
  • WalletCallbackRecordTable - Wallet callback record table model
  • 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

  1. Missing Required Fields

    • Verify request body contains orderId, paymentId, success, userId
    • Check JSON parsing is correct
  2. Save Failures

    • Check DynamoDB permissions
    • Verify table exists
    • Check CloudWatch logs for detailed errors
  3. 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