เชื่อม payment gateway กับ Laravel โดยใช้ 2C2P เป็นตัวอย่าง

payment gateway รับเงินแทนร้านทั้งบัตรและ QR พร้อมเพย์ แล้วแจ้งผลกลับมาที่ระบบ งานเชื่อมต่อมีสามส่วน คือเซ็นทุก request ด้วย secret key ขอ payment token ต่อคำสั่งซื้อ แล้วพาลูกค้าไปจ่าย โพสต์นี้ใช้ 2C2P เป็นตัวอย่าง พร้อมค่าที่ gateway ตรวจเข้มกว่าที่คิด

เผยแพร่
เวลาอ่าน
6 นาที
ในโพสต์นี้ 8 หัวข้อ
  1. payment gateway ทำอะไรให้ร้าน
  2. การจ่ายเงินหนึ่งครั้งมีห้าขั้น
  3. หน้าชำระเงินของ gateway กับ QR ที่ร้านแสดงเอง
  4. 2C2P เซ็นทุก request เป็น JWT ด้วย secret key
  5. ขอ payment token ต่อคำสั่งซื้อก่อนทุกครั้ง
  6. ค่าที่ gateway ตรวจเข้มกว่าที่คิด
  7. เก็บ key ใน env และให้ค่าตั้งต้นชี้ไป sandbox
  8. กรณีที่อาจเจอในงานจริง

ในโพสต์ สามทางรับเงินในเว็บขายของออนไลน์ gateway เป็นทางเดียวที่แจ้งผลกลับมาให้ระบบเอง โพสต์นี้ดูว่าเชื่อม gateway เข้ากับเว็บที่ทำด้วย Laravel ต้องทำอะไรบ้าง โดยใช้ 2C2P เป็นตัวอย่าง เพราะเป็น gateway ที่เราเชื่อมให้ร้านค้าออนไลน์ของลูกค้า gateway เจ้าอื่นในไทยใช้แนวคิดคล้ายกัน ชื่อ API กับรูปแบบการเซ็นอาจต่างไป

payment gateway ทำอะไรให้ร้าน

gateway ยืนอยู่ระหว่างร้านกับธนาคาร ลูกค้ากรอกบัตรหรือสแกน QR ที่ gateway สร้าง gateway คุยกับธนาคารให้ แล้วบอกผลกลับมาที่ระบบของร้าน เงินจะเข้าบัญชีร้านทีหลังตามรอบที่ตกลงกันไว้ หักค่าธรรมเนียมแล้ว

ก่อนเริ่มงานเชื่อมต่อ ร้านต้องสมัครเป็นร้านค้าของ gateway แล้วได้ค่าสามอย่างที่ใช้ตลอด

  • merchant ID รหัสร้านค้า ส่งไปกับทุก request
  • secret key ใช้เซ็น request และตรวจคำตอบ ห้ามหลุดออกไปนอกเซิร์ฟเวอร์
  • URL ของ sandbox กับ production แยกกัน ระหว่างพัฒนาใช้ sandbox กับ merchant ID ทดสอบ

ตอนคุยกับ gateway ควรถามให้ได้คำตอบตั้งแต่แรกด้วยว่า ค่าธรรมเนียมแต่ละช่องทางเท่าไหร่ เงินเข้าบัญชีร้านรอบไหน และก่อนได้ key จริงต้องผ่านการทดสอบอะไรบ้าง ข้อสุดท้ายกินเวลาโปรเจกต์มากกว่าที่คิด

การจ่ายเงินหนึ่งครั้งมีห้าขั้น

  1. ระบบของร้านขอ payment token บอก gateway ว่าคำสั่งซื้อเลขอะไร ยอดเท่าไหร่ เปิดช่องทางไหนบ้าง
  2. gateway ตอบลิงก์หน้าชำระเงิน หรือถ้าร้านเลือกแสดง QR เอง ก็ขอรูป QR ต่อด้วย token นั้น
  3. ลูกค้าจ่าย ด้วยบัตรในหน้าของ gateway หรือสแกน QR
  4. gateway แจ้งผลกลับมาสองทาง เรียก URL ของร้านตรงจากเซิร์ฟเวอร์ และพาเบราว์เซอร์ของลูกค้ากลับมาที่หน้าเว็บ
  5. ระบบอัปเดตคำสั่งซื้อ และถามผลซ้ำจาก gateway ได้เสมอด้วยเลขคำสั่งซื้อ

ขั้นที่ 4 กับ 5 เป็นส่วนที่พลาดแล้วเสียหายที่สุด จึงแยกไปอธิบายใน ตรวจผลชำระเงินจาก gateway ก่อนเชื่อ โพสต์นี้ดูขั้นที่ 1 ถึง 3

หน้าชำระเงินของ gateway กับ QR ที่ร้านแสดงเอง

2C2P ให้เลือกได้สองแบบ แบบแรกคือ Redirect API พาลูกค้าไปหน้าชำระเงินของ gateway ลูกค้าเลือกช่องทาง กรอกบัตรหรือสแกน QR ที่นั่น จ่ายเสร็จแล้ว gateway พากลับมาที่เว็บของร้าน ข้อดีคือข้อมูลบัตรไม่ผ่านเซิร์ฟเวอร์ของร้านเลย ภาระด้านความปลอดภัยของข้อมูลบัตรจึงอยู่กับ gateway

แบบที่สองคือ Direct API ร้านขอให้ gateway สร้าง QR พร้อมเพย์ แล้วเอารูป QR มาแสดงในหน้าของร้านเอง ลูกค้าไม่ต้องออกจากเว็บ เหมาะกับ QR ที่ไม่มีข้อมูลบัตรให้ห่วง ระบบที่เราทำใช้ทั้งสองแบบ บัตรไปหน้าของ gateway ส่วน QR แอดมินเลือกได้จากหน้าตั้งค่า

QR ที่ gateway สร้างต่างจาก QR ที่ร้านสร้างเองตาม QR พร้อมเพย์คืออะไร ตรงที่ gateway ผูก QR ไว้กับเลขคำสั่งซื้อ พอลูกค้าจ่าย gateway จึงรู้ว่าเงินก้อนนี้เป็นของคำสั่งซื้อไหน แล้วแจ้งผลกลับมาได้

2C2P เซ็นทุก request เป็น JWT ด้วย secret key

เอกสาร Payment Inquiry ของ 2C2P บอกว่า request และคำตอบทุกตัวอยู่ในรูป JWT เซ็นด้วย HMAC SHA-256 จาก secret key ของร้าน ข้อมูลทั้งก้อนเข้ารหัสเป็น JWT แล้วส่งในฟิลด์ payload ฟิลด์เดียว คนที่ไม่มี key จึงปลอมคำตอบไม่ได้ และ gateway ก็รู้ว่า request มาจากร้านจริง ฝั่ง PHP ใช้แพ็กเกจ firebase/php-jwt ได้เลย

จุดที่ติดกันบ่อยคือคำตอบที่เป็น error จะมาเป็น JSON ธรรมดาที่มีแค่ respCode กับ respDesc ถ้าระบบ decode ทุกคำตอบเป็น JWT ตรง ๆ โค้ดจะโยน exception ทุกครั้งที่ gateway ตอบ error และเราจะไม่เห็นรหัส error จริงเลย ส่วนส่งกับส่วนรับจึงหน้าตาแบบนี้

// ส่ง: เซ็นข้อมูลทั้งก้อนเป็น JWT แล้วใส่ในฟิลด์ payload ฟิลด์เดียว
$json = Http::post($url, ['payload' => JWT::encode($data, $secretKey, 'HS256')])->json();

// รับ: คำตอบที่สำเร็จเป็น JWT ส่วน error เป็น JSON ธรรมดา
$result = isset($json['payload'])
    ? JWT::decode($json['payload'], new Key($secretKey, 'HS256'))
    : (object) $json;

ทุก request ของระบบไปหา gateway ควรผ่านจุดนี้จุดเดียว จุดเดียวกันนี้เหมาะจะเก็บ log ของทุก request และคำตอบไว้ด้วย พอลูกค้าถามว่าจ่ายไปแล้วทำไมยังไม่ขึ้น เราเปิด log ดูได้ว่า gateway ตอบอะไรมา

ขอ payment token ต่อคำสั่งซื้อก่อนทุกครั้ง

ทุกการจ่ายเงินเริ่มจากขอ payment token ข้อมูลที่ส่งไปมีไม่กี่ช่อง แต่ละช่องกำหนดว่าการจ่ายครั้งนี้หน้าตาเป็นยังไง

ช่อง ใส่อะไร
invoiceNo เลขที่ gateway ใช้ผูกทุกอย่างของการจ่ายครั้งนี้ ใช้เลขคำสั่งซื้อได้ แต่ต้องไม่ซ้ำ
amount ยอดเงิน ทศนิยมไม่เกินสองตำแหน่ง
paymentChannel ช่องทางที่เปิดให้จ่าย เช่น CC บัตร PPQR พร้อมเพย์
backendReturnUrl URL ที่ gateway เรียกตรงจากเซิร์ฟเวอร์ของเขามาบอกผล
frontendReturnUrl หน้าที่พาเบราว์เซอร์ของลูกค้ากลับมาหลังจ่าย
paymentExpiry เวลาหมดอายุของลิงก์จ่ายเงิน เราใช้หนึ่งวันสำหรับหน้าชำระเงิน

ได้ token กลับมาแล้ว คำตอบจะมี webPaymentUrl สำหรับพาลูกค้าไปหน้าชำระเงิน ถ้าจะแสดง QR ในหน้าของร้านเอง ก็ส่ง token นั้นต่อให้ API payment พร้อมช่องทาง PPQR gateway จะตอบรหัส 1005 (รอลูกค้าสแกน) พร้อม URL ของรูป QR ระบบดาวน์โหลดรูปเก็บไว้กับคำสั่งซื้อ แล้วแสดงให้ลูกค้า QR แบบนี้มีอายุสั้น เอกสารที่เราใช้ตอนทำระบบบอกไว้ราว 15 นาที

URL สองช่องที่ชื่อคล้ายกันนั้นเชื่อได้ไม่เท่ากัน ช่องแรก gateway เรียกมาเองพร้อมลายเซ็น ช่องที่สองมากับเบราว์เซอร์ซึ่งลูกค้าแก้เองได้ เรื่องนี้คือหัวใจของโพสต์ถัดไป

ค่าที่ gateway ตรวจเข้มกว่าที่คิด

ช่องส่วนใหญ่ผ่านไปได้ง่าย แต่มีไม่กี่ช่องที่ gateway ตอบกลับด้วยรหัส error ตระกูล 9xxx ซึ่งหมายถึงค่าที่ส่งไปไม่ผ่านการตรวจ ตอนทำระบบเราต้องแก้อยู่สี่ช่อง

  • คำอธิบายรายการยาวได้ 250 ตัวอักษร เกินแล้วได้ 9006 คำอธิบายที่สร้างจากรายการสินค้าในตะกร้ายาวเกินได้ง่าย ต้องตัดก่อนส่ง โดยนับเป็นตัวอักษร ภาษาไทยจะได้ไม่โดนตัดกลางตัว
  • ยอดต้องเป็นทศนิยมไม่เกินสองตำแหน่ง ยอดที่คำนวณด้วย float อาจออกมาเป็น 1349.9999999 แล้วได้ 9009 ต้องปัดเป็นสองตำแหน่งก่อนส่งทุกครั้ง
  • ช่องที่ไม่บังคับ อย่าส่งเป็นค่าว่าง อีเมลลูกค้าที่ใช้กรอกล่วงหน้าในหน้าชำระเงินไม่บังคับ แต่ถ้าส่งเป็น null ไป gateway ตอบ error คำสั่งซื้อที่แอดมินเปิดให้ลูกค้าที่สั่งทางแชตมักไม่มีอีเมล ระบบจึงต้องตัดทั้งก้อนออกเมื่อไม่มีค่า
  • ช่องเดียวกันอาจบังคับในอีก API ตอนสร้าง QR ผ่าน Direct API อีเมลห้ามว่าง เราจึงใช้อีเมลกลางของร้านแทนเมื่อลูกค้าไม่มี

ทุกข้อควรมี test คุมไว้ เพื่อให้รู้ตั้งแต่ในเครื่องว่าข้อมูลที่ส่งออกไปตัดและปัดเรียบร้อยแล้ว วิธีเขียน test ชุดนี้อยู่ใน ทดสอบระบบรับชำระเงินโดยไม่ตัดเงินใครจริง

เก็บ key ใน env และให้ค่าตั้งต้นชี้ไป sandbox

merchant ID กับ secret key อยู่ใน .env ของแต่ละเครื่องเสมอ ไม่อยู่ในโค้ด ส่วน URL ของ gateway ใน config/services.php ให้ค่าตั้งต้นเป็นของ sandbox ถ้าเครื่องไหนลืมตั้งค่า เครื่องนั้นจะคุยกับ sandbox แทน production โดยไม่มีใครเสียเงิน

ระบบที่เราทำใช้ API เวอร์ชัน 4.3 ซึ่งยังใช้ได้ ส่วนเอกสารปัจจุบันของ 2C2P แสดงเวอร์ชัน 4.5 แล้ว ถ้าเริ่มใหม่ควรอ่านเวอร์ชันล่าสุดก่อน และถ้าอยากเก็บ .env ของ production ไว้ใน repo แบบเข้ารหัส ลองดูโพสต์ เข้ารหัสไฟล์ .env ใน Laravel

กรณีที่อาจเจอในงานจริง

  • ลูกค้ากดจ่ายซ้ำหลังจ่ายไปแล้ว ขอ token ด้วยเลขคำสั่งซื้อเดิม 2C2P ตอบ 9015 ว่าเลขนี้มีอยู่แล้ว ระบบควรถือรหัสนี้เป็นสัญญาณว่าอาจจ่ายสำเร็จไปแล้ว ถามผลจาก gateway ก่อน แล้วบอกลูกค้าว่าชำระเงินเรียบร้อยแล้ว แทนการโชว์ error
  • gateway ตอบช้า timeout ของ request ไปหา gateway ควรสั้นพอที่หน้าเว็บไม่ค้าง (เราตั้ง 10 วินาที) และไม่ควร retry การสร้างรายการจ่ายเงินแบบอัตโนมัติ เพราะ request ที่ช้าอาจสำเร็จไปแล้วฝั่ง gateway เรื่อง retry กับบริการที่ช้าอยู่ใน retry ตอนบริการปลายทางช้า
  • ยอดคำสั่งซื้อเปลี่ยนหลังขอ token เช่นแอดมินแก้ส่วนลดตอนลูกค้ายังไม่จ่าย ลิงก์เดิมยังเป็นยอดเก่า ควรล้าง token เดิมทิ้งทุกครั้งที่ยอดเปลี่ยน แล้วขอใหม่ตอนลูกค้ากดจ่าย
  • ร้านอยากเปิดแต่ละช่องทางไม่เท่ากัน ส่งเฉพาะรหัสช่องทางที่ลูกค้าเลือกในหน้าเว็บของร้านได้ ไม่ต้องเปิดทุกช่องทางทุกครั้ง

ตอนถัดไปของซีรีส์ รับเงินในเว็บขายของออนไลน์

อ่านต่อ

ถูกอ้างถึงใน

ล่าสุด

ดูทั้งหมด →