keyboard_tabSkip to content
arrow_back All Posts

Aug 30, 2023

ลองเล่น Stripe payment gateway กัน

แนะนำ Stripe payment gateway ที่สามารถทำให้เราทำเว็บชำระเงินออกมาได้

สารบัญ

stripe-payment-example สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video

Content นี้สนับสนุนโดย เหล่าผู้ตามของ Mikelopster เช่นเดิม (มีคนทักมาถามเรื่อง Payment เลยรู้สึกอยากแชร์เรื่องนี้ไว้ให้เป็นความรู้เพิ่มเติม) เลยรู้สึกว่า อยากลอง Stripe (ปกติเราเคยลอง Omise) และจะได้มาเล่าเรื่อง payment flow ไปเลย

Stripe คืออะไร ?

Stripe คือ Platform ชำระเงินที่ออกแบบมาเพื่อให้รับชำระเงินและส่งเงินได้ทั่วโลก ซึ่งโจทย์ของ Stripe ก็คือการเป็นตัวกลางในการชำระเงินระหว่าง user และธนาคาร (Payment gateway)

อ่านเพิ่มเติมกันได้ https://stripe.com/th-be/payments

เราจะลองเล่น Stripe กันยังไงดี

ทีนี้ Stripe เองก็มี API ที่ให้นักพัฒนามาลองใช้เพื่อใช้สำหรับพัฒนาช่องทางชำระเงินของตัวเอง ซึ่งมีทั้ง การ implement โดยการทำ form ขึ้นมาและใช้การสร้าง token ผ่าน Stripe หรือการใช้ redirect payment อย่างหน้า checkout ของ Stripe ก็สามารถทำได้

เคสที่เราจะมาลองเล่นกันเป็นเคสแบบ one time checkout คือ เป็นการชำระเงินแบบจ่ายเงินครั้งเดียวจบ ซึ่งเป็นตัวอย่างที่เล่นง่ายที่สุด และผมคิดว่าเห็นภาพง่ายที่สุด

ตามเอกสารนี้เลย (ช่องทาง Online Payment) https://stripe.com/docs/payments/checkout/how-checkout-works

ซึ่งจริงๆ มันมีรูปแบบ payment ที่หลากหลายมากทั้งจะ จ่ายปกติ (ในตัวอย่างที่เราจะทำ) หรือ subscription https://stripe.com/docs/development/quickstart

จะมาทำ

  • payment api (ผ่าน node) สำหรับสร้าง order และรับชำระเงิน
  • ทำหน้า UI สำหรับชำระเงิน
  • และทำหน้า success, fail เพื่อมารองรับหลังจาก payment เสร็จ

เป็นการจำลอง flow ชำระเงินอย่างง่ายกัน

stripe-demo

Config เริ่มต้น เตรียมตัว project กันก่อน

strip-sidemap

1. setup docker สำหรับ project นี้ (docker-compose.yml)

  • สำหรับ project นี้เราจะ setup docker 2 ตัวคือ mysql และ phpmyadmin เท่านั้น
  • โดยโจทย์ของ mysql คือการเก็บข้อมูล user และ order คู่กันไว้ เป็นการเก็บ status ที่ได้รับจาก Stripe มาว่าส่ง status กลับมาถูกต้องหรือไม่

docker-compose.yml

plain
version: "3.7"

services:
  db:
    image: mysql:latest
    container_name: mysql_db
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: tutorial
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - my_network

  phpmyadmin:
    image: phpmyadmin/phpmyadmin:latest
    container_name: phpmyadmin
    environment:
      PMA_HOST: db
      PMA_PORT: 3306
      PMA_USER: root
      PMA_PASSWORD: root
    ports:
      - "8080:80"
    depends_on:
      - db
    networks:
      - my_network

networks:
  my_network:
    driver: bridge

volumes:
  mysql_data:
    driver: local

2. setup folder project

  • ./src เป็นสถานที่เก็บไฟล์ html (ฝั่ง Frontend) ของหน้าเว็บไว้ประกอบด้วย
    • checkout.html = หน้าสำหรับรับชำระเงิน
    • main.js = สำหรับเก็บ function สำหรับการชำระเงินและยิง API เอาไว้
    • cancel.html = หน้าที่บอกว่าชำระเงินไม่สำเร็จ
    • success.html = หน้าที่บอกว่าชำระเงินสำเร็จแล้ว
  • index.js จะใช้สำหรับเก็บ nodejs ของฝั่ง API เอาไว้ เพื่อใช้สำหรับสร้าง API Placeorder, Webhook และ API สำหรับ check status ของ order

3. code index.js

เราจะทำการ setup code ไว้เริ่มต้นเป็นแบบนี้

javascript
const cors = require("cors");
const express = require("express");
const mysql = require("mysql2/promise");
const { v4: uuidv4 } = require("uuid");

require("dotenv").config();

const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);

const app = express();
const port = 8000;

// This is your Stripe CLI webhook secret for testing your endpoint locally.
const endpointSecret = "xxxx"; // เอาได้จากเว็บของ Stripe

// Middlewares here
app.use(cors());

let conn = null;

const initMySQL = async () => {
  conn = await mysql.createConnection({
    host: "localhost",
    user: "root",
    password: "root",
    database: "tutorial",
  });
};

/* code ที่เขียนด้านล่างนี้จะเป็นการเพิ่มเติมส่วนจากตรงนี้ */

// Listen
app.listen(port, async () => {
  await initMySQL();
  console.log("Server started at port 8000");
});

พาเล่น Dashboard Stripe ก่อนว่ามีอะไรบ้าง

ให้ทุกคนทำการสมัคร Stripe ผ่าน https://stripe.com/

  • Stripe มี sandbox เอาไว้ให้ developer เล่น = ไม่ต้องจ่ายเงินจริงก็สามารถยิงจ่ายเงินทดสอบได้

หน้าที่สำคัญ

  1. เมนู การชำระเงิน: หน้าสำหรับดูการชำระเงิน (สามารถดู log การชำระเงินได้) stripe-01

  2. หน้านักพัฒนา เป็นการดูการ Call API ว่ามีการยิงมาเท่าไหร่บ้าง stripe-02

  3. หน้านักพัฒนา > คีย์ API สำหรับดู Public, Secret key สำหรับการยิง Payment Stripe stripe-03

  4. หน้านักพัฒนา > Webhook สำหรับการใส่ API webhook

  • ตรงหัวข้อ "เพิ่มตัวรอรับเหตุการณ์ในระบบ" จะเป็นการบอกวิธีการ proxy API webhook จาก server มาสู่ local ได้ stripe-04

  • ให้ทำการลง stripe CLI ตามคำแนะนำในนี้เลย (เดี๋ยวเราจะใช้กันตอนทดสอบ webhook กัน) stripe-05

เริ่ม implement !

เรามาดูภาพรวมก่อนว่าเราจะทำอะไรกันบ้าง

Loading diagram…

เราจะแบ่งงานออกเป็น 2 ฝั่งคือ Backend และ Frontend โดยเราจะเริ่มทำจาก Backend ก่อน

1. Backend (API)

  1. /api/checkout (Placeorder) สำหรับเก็บข้อมูล order และสร้าง Payment
javascript
app.post("/api/checkout", express.json(), async (req, res) => {
  const { product, information } = req.body;
  try {
    // create payment session
    const orderId = uuidv4();
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ["card"],
      line_items: [
        {
          price_data: {
            currency: "thb",
            product_data: {
              name: product.name,
            },
            unit_amount: product.price * 100,
          },
          quantity: product.quantity,
        },
      ],
      mode: "payment",
      success_url: `http://localhost:8888/success.html?id=${orderId}`,
      cancel_url: `http://localhost:8888/cancel.html?id=${orderId}`,
    });

    // create order in database (name, address, session id, status)
    console.log("session", session);

    const data = {
      name: information.name,
      address: information.address,
      session_id: session.id,
      status: session.status,
      order_id: orderId,
    };

    const [result] = await conn.query("INSERT INTO orders SET ?", data);

    res.json({
      message: "Checkout success.",
      id: session.id,
      result,
    });
  } catch (error) {
    console.error("Error creating user:", error.message);
    res.status(400).json({ error: "Error payment" });
  }
});
  1. /api/webhook สำหรับรับข้อมูลจาก Stripe มา update ที่ database ของเรา
javascript
app.post("/webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const sig = req.headers["stripe-signature"];

  let event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    res.status(400).send(`Webhook Error: ${err.message}`);
    return;
  }

  // Handle the event
  switch (event.type) {
    case "checkout.session.completed":
      const paymentSuccessData = event.data.object;
      const sessionId = paymentSuccessData.id;

      const data = {
        status: paymentSuccessData.status,
      };

      const result = await conn.query("UPDATE orders SET ? WHERE session_id = ?", [
        data,
        sessionId,
      ]);

      console.log("=== update result", result);

      // event.data.object.id = session.id
      // event.data.object.customer_details คือข้อมูลลูกค้า
      break;
    default:
      console.log(`Unhandled event type ${event.type}`);
  }

  // Return a 200 response to acknowledge receipt of the event
  res.send();
});
  1. /api/order/:id สำหรับรับ order_id มาเพื่อ recheck status ก่อนแสดงว่า success หรือไม่
javascript
app.get("/order/:id", async (req, res) => {
  const orderId = req.params.id;
  try {
    const [result] = await conn.query("SELECT * from orders where order_id = ?", orderId);
    const selectedOrder = result[0];
    if (!selectedOrder) {
      throw {
        errorMessage: "Order not found",
      };
    }
    res.json(selectedOrder);
  } catch (error) {
    console.log("error", error);
    res.status(404).json({ error: error.errorMessage || "System error" });
  }
});

ก่อนเริ่มให้ทำการ binding webhook เข้า local ก่อน stripe-cli

เมื่อทุกอย่างเรียบร้อยลองทดสอบยิงข้อมูลด้วย API กันดูก่อน Test card สามารถดูได้จาก https://stripe.com/docs/testing

stripe-postman

ลองทดสอบการเช็ค status order ดูว่าถูกไหม

stripe-postman-2

2. Frontend

เราจะทำทั้งหมด 4 อย่างคือ

  1. สร้าง main.js ก่อนเพื่อรวมคำสั่งสำหรับการส่ง payment เอาไว้ (และเรียกใช้ stripe library)
javascript
const stripe = Stripe("...");

const placeorder = async (data) => {
  try {
    const requestData = {
      product: {
        name: "test",
        price: 200,
        quantity: 1,
      },
      information: {
        name: data.name,
        address: data.address,
      },
    };

    const response = await axios.post("http://localhost:8000/api/checkout", requestData);
    const session = response.data;

    stripe.redirectToCheckout({
      sessionId: session.id,
    });
  } catch (error) {
    console.log("error", error);
  }

  return null;
};
  1. checkout.html จำลองหน้าสำหรับเตรียมจ่ายเงิน
html
... (ย่อ head)
<body>
  <div>
    Checkout
    <div>Name <input type="text" name="name" /></div>
    <div>
      Address
      <div>
        <textarea name="address"></textarea>
      </div>
    </div>

    <button id="checkout">Checkout</button>
  </div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.4.0/axios.min.js"></script>
  <script src="https://js.stripe.com/v3/"></script>
  <script src="./main.js"></script>
  <script>
    checkout.addEventListener("click", async () => {
      const name = document.querySelector("input[name=name]").value;
      const address = document.querySelector("textarea[name=address]").value;

      await placeorder({
        name,
        address,
      });
    });
  </script>
</body>
...
  1. success.html จำลองหน้าสำหรับ success
html
... (ย่อ head)
<body>
  จ่ายเงินสำเร็จ

  <div>
    <div>ชื่อ: <span id="name"></span></div>
    <div>ที่อยู่: <span id="address"></span></div>
  </div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.4.0/axios.min.js"></script>

  <script>
    window.onload = async () => {
      const urlParams = new URLSearchParams(window.location.search);
      const orderId = urlParams.get("id");
      const response = await axios.get(`http://localhost:8000/order/${orderId}`);
      const orderData = response.data;
      if (orderData.status !== "complete") {
        window.location.href = "http://localhost:8888/cancel.html";
      }
      document.getElementById("name").innerText = orderData.name;
      address.innerText = orderData.address;
    };
  </script>
</body>
  1. cancal.html จำลองหน้ากรณี fail หรือ cancel
  • ไม่มีอะไร เพราะหน้านี้เป็น fallback
  • การถึงหน้านี้ = ไม่สามารถไปไหนต่อได้นอกจากต้องชำระเงินใหม่ (ต่อให้เช็คท้ายที่สุดก็ต้องพาไปชำระเงินใหม่)
  • เว้นแต่ อยาก handle case ว่า ถ้า success จริงๆ พากลับไปหน้า success
html
... (ย่อ head)
<body>
  จ่ายเงินล้มเหลว
</body>

forumComments (0)

lockLog in to join the conversation.

Log In

Loading comments…