ระบบพร้อมเพย์ (PromptPay) ของประเทศไทยพัฒนาขึ้นบนมาตรฐานสากล EMVCo QR Code Specification for Payment Systems (Merchant-Presented Mode: MPM) ซึ่งเป็นมาตรฐานเดียวกับที่ใช้ในสิงคโปร์ (PayNow) และมาเลเซีย (DuitNow)

ในบทความนี้ เราจะมาแกะเบื้องหลัง Payload สตริงของ QR Code ว่าแต่ละตัวอักษรมีความหมายอย่างไร และวิธีเขียนฟังก์ชันสร้าง QR Code ด้วยตัวคุณเองโดยไม่ต้องพึ่งพาไลบรารีภายนอก


1. เข้าใจโครงสร้างข้อมูลแบบ TLV (Tag-Length-Value)

มาตรฐาน EMVCo บังคับเก็บข้อมูลในรูปแบบเรียงต่อกันเป็นคู่ข้อมูล Tag, Length, และ Value:

  • Tag (2 ตัวอักษร): หมายเลขหมวดหมู่ เช่น 00 คือเวอร์ชัน, 54 คือจำนวนเงิน
  • Length (2 ตัวอักษร): ความยาวของข้อมูล Value ที่ตามมา (นับเป็นจำนวนตัวอักษร)
  • Value (ตาม Length): ข้อมูลจริง

ตัวอย่าง:

Tag: "00", Length: "02", Value: "01"  => "000201"
Tag: "58", Length: "02", Value: "TH"  => "5802TH"
Tag: "54", Length: "06", Value: "150.00" => "5406150.00"

2. โครงสร้าง Tag สำคัญของ Thai PromptPay

Tag ID ชื่อ Field รายละเอียด ค่ามาตรฐานในไทย
00 Payload Format Indicator เวอร์ชันของมาตรฐาน EMVCo 01
01 Point of Initiation Method 11 = Static (กรอกเงินเอง), 12 = Dynamic (ระบุยอดเงิน) 11 หรือ 12
29 Merchant Account Info ข้อมูลบัญชีผู้รับเงิน PromptPay ประกอบด้วย AID + ข้อมูลปลายทาง
53 Transaction Currency รหัสสกุลเงิน ISO 4217 764 (Thai Baht THB)
54 Transaction Amount ยอดเงินชำระ (ทศนิยม 2 ตำแหน่ง) เช่น 250.00
58 Country Code รหัสประเทศ 2 หลัก ISO 3166-1 TH
63 Checksum (CRC16) Checksum ตรวจสอบความถูกต้องของ QR 4 หลัก Hexadecimal

3. AID และประเภทรหัสผู้รับเงิน (Tag 29)

สำหรับประเทศไทย Application Identifier (AID) คือ:

$$\text{AID} = \text{A000000677010111}$$

ประเภทรหัสผู้รับเงินใน Sub-tag:

  • 01 เบอร์โทรศัพท์มือถือ: แปลงเป็นรหัสสากล 13 หลัก เช่น 0812345678 $\rightarrow$ 0066812345678
  • 02 เลขบัตรประชาชน / เลขประจำตัวผู้เสียภาษี: 13 หลักตรงๆ
  • 03 e-Wallet ID: 15 หลัก

4. โค้ดคำนวณ CRC16-CCITT และสร้าง Payload

function crc16(data: string): string {
  let crc = 0xffff;
  for (let i = 0; i < data.length; i++) {
    let c = data.charCodeAt(i);
    crc ^= c << 8;
    for (let j = 0; j < 8; j++) {
      if ((crc & 0x8000) !== 0) {
        crc = ((crc << 1) ^ 0x1021) & 0xffff;
      } else {
        crc = (crc << 1) & 0xffff;
      }
    }
  }
  return (crc & 0xffff).toString(16).toUpperCase().padStart(4, "0");
}

function tlv(tag: string, value: string): string {
  const len = value.length.toString().padStart(2, "0");
  return `${tag}${len}${value}`;
}

export function generatePromptPayString(
  targetPhoneOrId: string,
  amount?: number,
): string {
  const isMobile = targetPhoneOrId.length === 10;
  const formatted = isMobile
    ? `0066${targetPhoneOrId.slice(1)}`
    : targetPhoneOrId;
  const subTag = isMobile ? "01" : "02";

  const aid = tlv("00", "A000000677010111");
  const target = tlv(subTag, formatted);
  const tag29 = tlv("29", aid + target);

  let raw =
    tlv("00", "01") +
    tlv("01", amount ? "12" : "11") +
    tag29 +
    tlv("53", "764") +
    (amount ? tlv("54", amount.toFixed(2)) : "") +
    tlv("58", "TH") +
    "6304";

  return raw + crc16(raw);
}

นำสตริงผลลัพธ์ที่ได้ไปเรนเดอร์ลงใน Canvas QR Code เพื่อให้ลูกค้าสแกนจ่ายเงินผ่านแอพพลิเคชันธนาคารไทยทุกแห่งได้ทันที!