Credit Card Payment Flows - Complete Documentation
This document describes all possible flows for credit card/KBank payments, from checkout through callback to order processing and card tokenization.
Overview
The credit card payment system involves these Lambda functions:
| Function | Purpose |
|---|---|
payment3-cardtoken-${BRANCH} | Creates charge, stores CardPaymentRecord, redirects to KBank 3DS |
payment3-callback-${BRANCH} | Receives KBank callback, updates status, invokes ProcessPaidOrder |
payment3-process-paid-order-${BRANCH} | Processes order (success/failure), saves card token via KBankCreateUser |
payment3-kbank-create-user-${BRANCH} | Creates KBank user OR invokes addcard for existing users |
payment3-kbank-add-card-${BRANCH} | Adds new card to existing KBank customer |
Callback Body Parameters
Source: CreditCardCallbackProcessor in functions/callback/src/credit_card_callback_processor.py
Body format: URL-encoded string or JSON (JSON tried first, then urllib.parse.parse_qsl)
| Field | Required | Used by Backend | Purpose |
|---|---|---|---|
objectId or id | Yes | Yes | Charge ID; lookup/update CardPaymentRecordTable |
token | Yes | Yes | Token ID; identifies card payment record (hash key) |
status | Yes | Yes | "true"/"success" or "false"/"failure" |
message | No | No | Not used |
saveCard | No | No | Not used — echoed from KBank, not acted upon |
Redirect URL: KBank controls the callback body. Pass callback when calling POST /cardpayment; we store it in the payment record and the callback reads it from there. See Callback URL App Integration.
Flow 1: New User Pays (First-Time Card)
Scenario: User has no KBank customer record. Pays for order with card.
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Frontend │────▶│ CardToken │────▶│ KBank │────▶│ 3DS Page │
│ Checkout │ │ Lambda │ │ Charge │ │ (if 3DS) │
└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘
│ │ │
│ │ create CardPaymentRecord │
│ │ (tokenId, orderId, status=false) │
│ │ │
│ │◀────────────────────────────────────────┘
│ │ User completes 3DS
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ KBank Callback (POST /callback) │
│ body: objectId=chrg_xxx&token=tokn_xxx&status=true&saveCard=false (KBank fixed) │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────┐
│ callback.py │
│ CreditCardCallback │
│ Processor │
└──────────────────────┘
│
│ 1. Parse body (objectId, token, status)
│ 2. Query CardPaymentRecordTable by chargeId (objectId) via GSI
│ 3. Update record: chargeId, status
│ 4. Save to CreditCardNotifyTable
│ 5. Invoke ProcessPaidOrder (orderId from card_record)
│
▼
┌──────────────────────┐
│ ProcessPaidOrder │
└──────────────────────┘
│
│ 1. Query OrderTable by orderId
│ 2. Query CardPaymentRecordTable by orderId
│ 3. save_paid_order() → Pay Lambda
│ 4. send_notification() → success
│ 5. save_card_token() → KBankCreateUser
│
▼
┌──────────────────────┐
│ KBankCreateUser │
│ (createuser.py) │
└──────────────────────┘
│
│ Query KBankUserMappingTable: user (ownerId) exists?
│ → NO
│
│ Call KBank API: POST /card/v2/customer
│ Save mapping: ownerId → kbank_id in KBankUserMappingTable
│
▼
Success: New KBank user created, card token saved
Key files:
functions/cardtoken/app.py— creates charge, CardPaymentRecordfunctions/callback/callback.py— updates status, invokes ProcessPaidOrderfunctions/processPaidOrder/process_paid.py— save_card_token()functions/kbankuser/createuser.py— creates new KBank user
Flow 2: Existing User Pays with New Card (Add Card)
Scenario: User already has KBank customer record. Pays with a different card.
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Frontend │────▶│ CardToken │────▶│ KBank │
└─────────────┘ └──────────────┘ └─────────────┘
│ │ │
│ │ │ 3DS callback
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ KBank Callback (POST /callback) │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────┐ ┌──────────────────────┐
│ callback.py │────▶│ ProcessPaidOrder │
└──────────────────────┘ └──────────────────────┘
│ │
│ │ save_card_token()
│ │ payload: { ownerId, token }
│ ▼
│ ┌──────────────────────┐
│ │ KBankCreateUser │
│ │ (createuser.py) │
│ └──────────────────────┘
│ │
│ │ Query KBankUserMappingTable
│ │ → User EXISTS (has kbank_id)
│ │
│ │ Invoke addcard Lambda
│ │ payload: { kbank_customer_id, token }
│ ▼
│ ┌──────────────────────┐
│ │ KBank Add Card │
│ │ (addcard.py) │
│ └──────────────────────┘
│ │
│ │ POST {kbank_url}/card/v2/customer/{id}/card
│ │ body: { mode: "token", token: token }
│ │
│ ▼
│ Success: Card added to existing KBank customer
Key difference from Flow 1: createuser.py checks kbank_user_exists. If true, it invokes payment3-kbank-add-card-${BRANCH} instead of calling the KBank create-user API.
Addcard Lambda (functions/kbankuser/addcard.py):
- Input:
{ "kbank_customer_id": "...", "token": "tokn_xxx" } - Calls:
POST {url}/card/v2/customer/{kbank_customer_id}/card - Returns: customer info including updated
cardsarray
Flow 3: Payment Failed (Declined / 3DS Failed)
Scenario: Card declined or 3DS authentication failed.
- Callback receives
status=false - Updates CardPaymentRecordTable with
status=False - Still invokes ProcessPaidOrder (same as success)
- ProcessPaidOrder determines failure from order_status, sends failure notification
Note: ProcessPaidOrder is invoked for both success and failure. It infers success from order_status (checksum or CheckStatusTable).
Flow 4: ADDCARD (Card Verification Only)
Scenario: User adds card without a real order (1 THB verification charge).
CardToken: When orderId=ADDCARD:
- Charges 1 THB
- Skips order validation
- Creates CardPaymentRecord with
orderId="ADDCARD" - Skips
invoke_process_paid_orderfrom cardtoken (does not invoke ProcessPaidOrder)
Callback: When KBank sends callback for ADDCARD charge:
- callback.py receives objectId, token, status
- Updates CardPaymentRecord
- Invokes ProcessPaidOrder with
orderId="ADDCARD" - ProcessPaidOrder:
OrderTable.query("ADDCARD")→ OrderNotFound (no such order) - Flow fails at ProcessPaidOrder
Current state: ADDCARD callback flow is not fully wired. ProcessPaidOrder expects a real orderId.
Flow 5: Table Lookup Order (CardPaymentRecordTable)
Callback finds the card record by chargeId (objectId):
- Query
CardPaymentRecordTable.chargeId_index(branch table) - If not found: query
CardPaymentRecordTableMaster.chargeId_index - If AttributeDeserializationError: fallback to boto3 direct query
orderId comes from the card payment record, not from the callback body.
Summary Table
| Flow | Callback status | ProcessPaidOrder | KBankCreateUser | Addcard |
|---|---|---|---|---|
| New user, success | status=true | Invoked | Creates new user | — |
| Existing user, success | status=true | Invoked | Invokes addcard | Adds card |
| Payment failed | status=false | Invoked | Invoked | — |
| ADDCARD | — | Would fail (no order) | — | — |
saveCard Field
Status: Not used by the backend.
- Present in KBank callback body
- Not read by CreditCardCallbackProcessor
- Not stored in CardPaymentRecordTable
- Not passed to ProcessPaidOrder
- save_card_token() is always called for every processed order