Skip to main content

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:

FunctionPurpose
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)

FieldRequiredUsed by BackendPurpose
objectId or idYesYesCharge ID; lookup/update CardPaymentRecordTable
tokenYesYesToken ID; identifies card payment record (hash key)
statusYesYes"true"/"success" or "false"/"failure"
messageNoNoNot used
saveCardNoNoNot 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, CardPaymentRecord
  • functions/callback/callback.py — updates status, invokes ProcessPaidOrder
  • functions/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 cards array

Flow 3: Payment Failed (Declined / 3DS Failed)

Scenario: Card declined or 3DS authentication failed.

  1. Callback receives status=false
  2. Updates CardPaymentRecordTable with status=False
  3. Still invokes ProcessPaidOrder (same as success)
  4. 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_order from 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):

  1. Query CardPaymentRecordTable.chargeId_index (branch table)
  2. If not found: query CardPaymentRecordTableMaster.chargeId_index
  3. If AttributeDeserializationError: fallback to boto3 direct query

orderId comes from the card payment record, not from the callback body.


Summary Table

FlowCallback statusProcessPaidOrderKBankCreateUserAddcard
New user, successstatus=trueInvokedCreates new user
Existing user, successstatus=trueInvokedInvokes addcardAdds card
Payment failedstatus=falseInvokedInvoked
ADDCARDWould 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