QRCallback Lambda Function
Quick Reference
- Function Name:
payment3-qrcallback-${BRANCH} - Handler:
qrcallback.lambda_handler - Runtime: Python 3.12 (Docker Image)
- Timeout: 120 seconds
- Memory: 1024 MB
- Trigger: API Gateway (POST /qrcallback)
- Template.yaml: Lines 156-200
Function Overview
The QRCallback function processes QR payment callbacks from KBank. It validates payment amounts against KBank records, saves payment records, and invokes ProcessPaidOrderInternal for successful payments. This function handles Thai QR code payment callbacks.
Entry Point
File: functions/callback/qrcallback.py
Handler: lambda_handler(event, _)
def lambda_handler(event, _):
"""
Record QR payment callback.
"""
processor = None
order_id = None
charge_id = None
try:
body = dict(urllib.parse.parse_qsl(event["body"]))
order_id = body["orderId"]
status_string = body["status"]
amount = float(body["amount"])
charge_id = body["chargeId"]
processor = Processor(order_id, status_string, amount, charge_id)
except Exception as e:
charge_id = errorString()
order_id = body.get("orderId") if isinstance(body, dict) else None
status_string = body.get("status") if isinstance(body, dict) else str(body)
amount = body.get("amount") if isinstance(body, dict) else 0
processor = Processor(order_id or "unknown", status_string, amount, charge_id)
log_callback_error(
error=e,
function_name="qrcallback",
event=event,
orderId=order_id,
chargeId=charge_id,
response_data=None
)
send_callback_error_email(
error=e,
function_name="qrcallback",
orderId=order_id,
chargeId=charge_id,
event=event
)
return processor.error_html_response(str(e) + errorString())
try:
qr_processor = processor.qr_processor
success, paid_amount = qr_processor.check_kbank_record
if not success:
error_msg = f"Kbank record not found for charge id {charge_id}"
error_exception = Exception(error_msg)
log_callback_error(
error=error_exception,
function_name="qrcallback",
event=event,
orderId=order_id,
chargeId=charge_id,
response_data=None
)
send_callback_error_email(
error=error_exception,
function_name="qrcallback",
orderId=order_id,
chargeId=charge_id,
event=event
)
return processor.error_html_response(error_msg)
if abs(float(paid_amount) - float(processor.amount)) > 2.0: # Allow differences up to 2 for floating point errors
error_msg = f"Kbank record amount {paid_amount} does not match order amount {processor.amount}"
error_exception = Exception(error_msg)
log_callback_error(
error=error_exception,
function_name="qrcallback",
event=event,
orderId=order_id,
chargeId=charge_id,
response_data=None
)
send_callback_error_email(
error=error_exception,
function_name="qrcallback",
orderId=order_id,
chargeId=charge_id,
event=event
)
return processor.error_html_response(error_msg)
qr_processor.save_record()
qr_processor.process_paid_order()
except Exception as e:
log_callback_error(
error=e,
function_name="qrcallback",
event=event,
orderId=order_id,
chargeId=charge_id,
response_data=None
)
send_callback_error_email(
error=e,
function_name="qrcallback",
orderId=order_id,
chargeId=charge_id,
event=event
)
return processor.error_html_response(str(e) + errorString())
return processor.response
Event Structure
API Gateway Event
Path: POST /qrcallback
Auth: NONE (public endpoint - called by KBank)
Request Body (URL-encoded form data):
orderId=002500048744&status=success&amount=6&chargeId=chrg_prod_11430784e550fb6ea4f69902e0deda2591aa6
Required Fields:
orderId- Villa order ID (required)status- Payment status: "success" or "failure" (required)amount- Paid amount (required)chargeId- KBank charge ID (required)
Response (Success - HTML redirect):
<html>
<head>
<meta http-equiv="Refresh" content="0; URL='https://shop.villamarket.com/thankyou?orderId=...&status=success&amount=...&chargeId=...'">
</head>
<body>
<h2>Payment Processing Complete</h2>
<p>Click below to return to the store:</p>
<a href='...'>Click here to return to Villa Market</a>
</body>
</html>
DynamoDB Tables
QRCallbackRecordTable
Table Name: payment3-qr-callback-record-{BRANCH}
Region: ap-southeast-1
Schema (from template.yaml lines 1162-1187):
- Hash Key:
orderId(String) - Range Key:
created_at(String) - GSI:
chargeId-index(chargeId hash, created_at range)
Note: This table is referenced in template.yaml but the function uses QRPaymentRecordTable for actual payment records.
QRPaymentRecordTable
Table Name: payment3-qr-payment-record-{BRANCH}
Region: ap-southeast-1
PynamoDB Model:
class QRPaymentRecordTable(Model):
class Meta:
table_name = f"payment3-qr-payment-record-{BRANCH}"
region = "ap-southeast-1"
tokenId = UnicodeAttribute(hash_key=True)
orderId = UnicodeAttribute()
createdAt = UTCDateTimeAttribute()
chargeId = UnicodeAttribute(null=True)
status = BooleanAttribute(null=True)
paidAmount = NumberAttribute(null=True)
def __post_init__(self):
self.createdAt = datetime.now()
Key Fields:
- Hash Key:
tokenId(UnicodeAttribute) - Uses chargeId as tokenId orderId- Villa order IDchargeId- KBank charge IDstatus- Payment status (True = success, False = failure)paidAmount- Amount paid (NumberAttribute)
Save Pattern:
from src.qrPaymentRecordTable import QRPaymentRecordTable
QRPaymentRecordTable(
tokenId=charge_id,
orderId=order_id,
chargeId=charge_id,
status=kbank_success,
createdAt=datetime.now(),
paidAmount=paid_amount
).save()
OrderTable
Table Name: order-table-dev
Region: ap-southeast-1
Query Pattern:
from src.order import OrderTable
order = OrderTable.get(order_id)
order_data = order.data
grand_total = order_data["grandTotal"]
External Services
KBank QR Payment API
Endpoint: {BASE_URL}/qr/v2/qr/{charge_id}
Method: GET
Authentication: x-api-key header with KBank secret key
Request:
headers = {
"content-type": "application/json",
"x-api-key": os.environ["KBANK_SECRET_KEY"]
}
response = requests.get(
f"{os.environ['BASE_URL']}/qr/v2/qr/{charge_id}",
headers=headers
)
Response:
{
"status": "success",
"amount": 6.0,
...
}
Validation:
- Check
response["status"] == "success" - Extract
response["amount"] - Compare with order grandTotal (tolerance: 2.0 THB)
AWS Secrets Manager
Secrets:
kbank-dev(for dev branch)kbank-prod(for master branch)
Secret Structure:
{
"b2c": "api_key:secret_key",
"url": "https://api-endpoint.kasikornbank.com",
"mid": "merchant_id"
}
Lambda Invocations
ProcessPaidOrderInternal
Function: payment3-process-paid-order-internal-${BRANCH}
Invoked When: Payment is successful (kbank_success is True)
Payload:
{
"orderId": order_id,
"paymentType": "qr",
"success": kbank_success,
"chargeId": charge_id
}
Invocation Type: Event (asynchronous)
Code:
def process_paid_order(self):
print("payment is successful, invoking ProcessPaidOrder Lambda")
lambda_client = Lambda()
response = lambda_client.invoke(
functionName=f"payment3-process-paid-order-internal-{BRANCH}",
input=json.dumps(
{
"orderId": self.order_id,
"paymentType": "qr",
"success": self.kbank_success,
"chargeId": self.charge_id,
}
),
invocationType="Event", # Asynchronous invocation
)
print("process paid order invoked")
print(response)
return response
Processing Flow
Main Flow
- Parse Event: Extract URL-encoded form data
- Create Processor: Initialize Processor with orderId, status, amount, chargeId
- Create QR Processor: Initialize QRCallbackProcessor
- Check KBank Record: Query KBank API to verify payment
- Validate Amount: Compare KBank paid amount with order grandTotal
- Save Record: Save to QRPaymentRecordTable
- Process Paid Order: Invoke ProcessPaidOrderInternal if successful
- Return Response: Return HTML redirect response
Detailed Algorithm
1. Parse event["body"] as URL-encoded form data
2. Extract: orderId, status, amount, chargeId
3. Create Processor(orderId, status_string, amount, chargeId)
4. Create QRCallbackProcessor(charge_id, order_id, success)
5. Call check_kbank_record:
- GET {BASE_URL}/qr/v2/qr/{charge_id}
- Extract status and amount from response
- Return (success: bool, paid_amount: float)
6. Validate KBank record exists:
- If not success: Log error, send email, return error HTML
7. Validate amount match:
- If abs(paid_amount - order_amount) > 2.0:
- Log error, send email, return error HTML
8. Save QRPaymentRecordTable:
- tokenId = charge_id
- orderId = order_id
- chargeId = charge_id
- status = kbank_success
- paidAmount = paid_amount
9. If kbank_success is True:
- Invoke ProcessPaidOrderInternal with paymentType="qr"
10. Return HTML redirect response
Error Handling
Parse Errors
Exception: Caught in outer try-except
Handling:
- Create Processor with fallback values
- Log error to CallbackErrorLogTable
- Send error email
- Return error HTML response
KBank Record Not Found
Error: check_kbank_record returns success=False
Handling:
- Log error with message "Kbank record not found for charge id {charge_id}"
- Send error email
- Return error HTML response
Amount Mismatch
Error: abs(paid_amount - order_amount) > 2.0
Handling:
- Log error with amount mismatch details
- Send error email
- Return error HTML response
- Tolerance: 2.0 THB (for floating-point precision)
Error HTML Response
Function: processor.error_html_response(error)
def error_html_response(self, error: str):
return f"""
<html>
<body>
<h1>Callback recorded in error {'successfully' if self.success else 'failed'}</h1>
<p>error is {error}</p>
<p>order id is {self.order_id}</p>
<p>status is {self.status_string}</p>
<p>amount is {self.amount}</p>
<p>charge id is {self.charge_id}</p>
</body>
</html>
"""
IAM Policies
From template.yaml (lines 167-187):
DynamoDBWritePolicyforQRCallbackRecordTable(line 168-169)DynamoDBCrudPolicyforQRPaymentRecordTable(line 170-171)DynamoDBReadPolicyfororder-table-dev(line 172-173)DynamoDBCrudPolicyforpayment3-card-payment-record-master(line 174-175)LambdaInvokePolicyforProcessPaidOrderInternal(line 176-177)AWSSecretsManagerGetSecretValuePolicyfor kbank-dev (line 178-179)AWSSecretsManagerGetSecretValuePolicyfor kbank-prod (line 180-181)DynamoDBWritePolicyforCallbackErrorLogTable(line 182-184)SESCrudPolicyfor sending error emails (line 185-187)
Dependencies
External Lambda Functions
payment3-process-paid-order-internal-${BRANCH}- Process paid QR orders (invoked)
DynamoDB Tables
payment3-qr-callback-record-${BRANCH}- Callback records (write)payment3-qr-payment-record-${BRANCH}- Payment 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)
External Services
- KBank QR API: Verify payment status and amount
- AWS Secrets Manager: Get KBank API credentials
- SES: Send error notification emails
Environment Variables
BRANCH- Git branch nameKBANK_API_KEY- Set from secretsKBANK_SECRET_KEY- Set from secrets (used as x-api-key)BASE_URL- KBank API endpoint (from secrets)MID- Merchant ID (from secrets)
Python Dependencies
requests- HTTP client for KBank APIlambdasdk- Lambda invocation utilitiesnicHelper- AWS utilitiessentry-sdk- Error trackingpynamodb- DynamoDB ORM
Testing
Local Testing
File: functions/callback/qrcallback.py (lines 188-205)
if __name__ == "__main__":
import os
os.environ["BRANCH"] = "dev"
# Test event
test_event = {
"body": "orderId=002500048744&status=success&amount=6&chargeId=chrg_prod_11430784e550fb6ea4f69902e0deda2591aa6"
}
print("Testing QR callback handler...")
try:
r = lambda_handler(test_event, None)
print("✅ Test successful!")
print(f"Response status: {r.get('statusCode')}")
except Exception as e:
print(f"❌ Test failed: {e}")
Test Event Example
{
"body": "orderId=002500048744&status=success&amount=6&chargeId=chrg_prod_11430784e550fb6ea4f69902e0deda2591aa6"
}
SAM Testing
sam local invoke QRCallback -e test_event.json
Code Structure
File Organization:
functions/callback/
├── qrcallback.py # Main handler
└── src/
├── qrprocessor.py # QR callback processor
├── qrPaymentRecordTable.py # QR payment record table
└── order.py # OrderTable model
Key Classes:
Processor- Main callback processorQRCallbackProcessor- QR-specific processing logic
Related Functions
- KBankCreateOrder - Creates QR payment orders
- QRNotify - Receives QR payment notifications
- ProcessPaidOrderInternal - Processes paid QR orders
Common Patterns
KBank Record Validation
@cached_property
def check_kbank_record(self) -> tuple[bool, float]:
params = {
"charge_id": self.charge_id,
}
headers = {
"content-type": "application/json",
"x-api-key": os.environ["KBANK_SECRET_KEY"],
}
r = requests.get(
f"{os.environ['BASE_URL']}/qr/v2/qr/{params['charge_id']}",
headers=headers,
)
response = r.json()
# Print the full API response for debugging
print(f"KBank API Response for charge_id {self.charge_id}:")
print(f"Status Code: {r.status_code}")
print(f"Response: {response}")
try:
success = response["status"] == "success"
paid_amount = response["amount"]
return success, paid_amount
except KeyError as e:
print(f"KeyError in KBank API response: {e}")
print(f"Available keys in response: {list(response.keys())}")
raise
Amount Validation
if abs(float(paid_amount) - float(processor.amount)) > 2.0: # Allow differences up to 2 for floating point errors
error_msg = f"Kbank record amount {paid_amount} does not match order amount {processor.amount}"
error_exception = Exception(error_msg)
log_callback_error(
error=error_exception,
function_name="qrcallback",
event=event,
orderId=order_id,
chargeId=charge_id,
response_data=None
)
send_callback_error_email(
error=error_exception,
function_name="qrcallback",
orderId=order_id,
chargeId=charge_id,
event=event
)
return processor.error_html_response(error_msg)
Troubleshooting
Common Issues
-
KBank Record Not Found
- Verify chargeId exists in KBank system
- Check KBank API endpoint is accessible
- Verify API credentials are correct
-
Amount Mismatch
- Check order grandTotal matches KBank paid amount
- Tolerance is 2.0 THB for floating-point errors
- Verify order wasn't modified after QR creation
-
API Errors
- Check KBank API status
- Verify network connectivity
- Check API credentials in Secrets Manager
Debugging
- Check CloudWatch logs for KBank API responses
- Review CallbackErrorLogTable for error history
- Check Sentry for exception tracking
- Verify QRPaymentRecordTable records are being saved
- Test with known good charge IDs
References
- Template.yaml: Lines 156-200
- Related Functions: KBankCreateOrder, QRNotify, ProcessPaidOrderInternal
- KBank QR API:
/qr/v2/qr/{charge_id}endpoint