ทำบอทเช็คสลิปใน LINE OA ของร้าน ด้วย Checkslip API

บอทเช็คสลิปใน LINE OA: โค้ดรับรูปสลิปจากแชตแล้วตอบผลพบรายการ

Checkslip ไม่มีบอท LINE สำเร็จรูปให้ติดตั้ง บทความนี้สอนนักพัฒนาของร้านเขียนบอทเองบนเซิร์ฟเวอร์ของร้าน แล้วต่อเข้ากับ Checkslip API ร้านที่ขายผ่าน LINE ได้สลิปจากลูกค้าในแชตทุกวัน ถ้าให้ LINE OA ของร้านเช็คสลิปกับธนาคารเองทันทีที่ลูกค้าส่งรูปมา แอดมินไม่ต้องเปิดแอปธนาคารเทียบทีละใบ

บอททำงานยังไง

บอทเช็คสลิปใน LINE OA: ลูกค้าส่งรูปสลิปเข้าแชต บอทของร้านส่งต่อ Checkslip API แล้วตอบผลพบรายการในแชต

1. ลูกค้าส่งรูปสลิปเข้าแชต LINE OA ของร้าน

2. LINE ส่ง webhook มาที่เซิร์ฟเวอร์ของร้าน

3. เซิร์ฟเวอร์ดึงรูปสลิปจาก LINE แล้วส่งต่อให้ Checkslip API

4. Checkslip ดึงข้อมูลรายการจากธนาคารต้นทาง แล้วตอบผล เช่น พบรายการ หรือไม่พบรายการ

5. บอทตอบผลกลับในแชต และร้านใช้ผลนั้นยืนยันออเดอร์

สิ่งที่ต้องมีก่อนเริ่ม

  • LINE OA ที่เปิด Messaging API แล้ว พร้อม channel secret และ channel access token จาก LINE Developers Console
  • API key ของ Checkslip ออกได้ตาม เริ่มต้นใช้งาน บัญชีใหม่ได้เครดิตทดลองฟรี
  • เซิร์ฟเวอร์ที่มี HTTPS สำหรับรับ webhook และ Node.js 18 ขึ้นไป

ตั้งค่าเป็นตัวแปรสภาพแวดล้อม 3 ตัว: LINE_CHANNEL_SECRET, LINE_CHANNEL_ACCESS_TOKEN และ CHECKSLIP_API_KEY ห้ามใส่ในโค้ด

โค้ดบอท (Node.js)

import crypto from "node:crypto";
import express from "express";

const app = express();
app.use(express.raw({ type: "application/json" }));

function signatureOk(rawBody, signature) {
  const expected = crypto
    .createHmac("sha256", process.env.LINE_CHANNEL_SECRET)
    .update(rawBody)
    .digest("base64");
  return signature === expected;
}

async function getSlipImage(messageId) {
  const res = await fetch(
    `https://api-data.line.me/v2/bot/message/${messageId}/content`,
    { headers: { Authorization: `Bearer ${process.env.LINE_CHANNEL_ACCESS_TOKEN}` } },
  );
  if (!res.ok) throw new Error(`LINE content ${res.status}`);
  return new Blob([await res.arrayBuffer()], { type: res.headers.get("content-type") });
}

async function checkSlip(image, messageId) {
  const form = new FormData();
  form.append("image", image, "slip.jpg");
  const res = await fetch("https://api.checkslip.com/api/slips/lookup", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.CHECKSLIP_API_KEY}`,
      "Idempotency-Key": `line-${messageId}`,
    },
    body: form,
  });
  return res.json();
}

function replyText(result) {
  switch (result.outcome) {
    case "FOUND": {
      const baht = (result.slip.amount.satang / 100).toFixed(2);
      return `พบรายการ ${baht} บาท ร้านจะตรวจยอดกับออเดอร์ให้นะครับ`;
    }
    case "DUPLICATE":
      return "สลิปนี้เคยส่งเข้ามาแล้ว แอดมินจะตรวจให้อีกครั้งครับ";
    case "NOT_FOUND":
      return "ยังไม่พบรายการนี้ที่ธนาคาร แอดมินจะติดต่อกลับครับ";
    default:
      return "ได้รับสลิปแล้ว แอดมินจะตรวจให้ครับ";
  }
}

async function reply(replyToken, text) {
  await fetch("https://api.line.me/v2/bot/message/reply", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.LINE_CHANNEL_ACCESS_TOKEN}`,
    },
    body: JSON.stringify({ replyToken, messages: [{ type: "text", text }] }),
  });
}

app.post("/line/webhook", async (req, res) => {
  if (!signatureOk(req.body, req.get("x-line-signature"))) {
    return res.sendStatus(401);
  }
  res.sendStatus(200);
  const { events } = JSON.parse(req.body.toString("utf8"));
  for (const event of events) {
    if (event.type !== "message" || event.message.type !== "image") continue;
    try {
      const image = await getSlipImage(event.message.id);
      const result = await checkSlip(image, event.message.id);
      await reply(event.replyToken, replyText(result));
    } catch (err) {
      console.error("slip check failed", err);
    }
  }
});

app.listen(3000);

ติดตั้งด้วย npm install express แล้วตั้ง Webhook URL ใน LINE Developers Console เป็น https://โดเมนของร้าน/line/webhook

สิ่งที่ควรรู้ก่อนใช้จริง

  • ตรวจลายเซ็น x-line-signature ทุกครั้ง คำขอที่ลายเซ็นไม่ตรงไม่ได้มาจาก LINE ให้ปฏิเสธ
  • reply token ใช้ได้ครั้งเดียวและต้องตอบภายใน 1 นาที ถ้าผลเป็น PENDING (รอธนาคาร) Checkslip จะส่งผลทีหลังทาง webhook ถึงตอนนั้น reply token หมดอายุแล้ว ให้ส่งข้อความหาลูกค้าด้วย push message แทน
  • Idempotency-Key ผูกกับ message id ของ LINE ถ้า LINE ส่ง webhook ซ้ำ จะได้คำตอบเดิมและไม่หักเครดิตเพิ่ม
  • ผล FOUND บอกว่าธนาคารมีรายการนี้ ร้านยังต้องเทียบยอดเงินและบัญชีผู้รับกับออเดอร์ก่อนยืนยัน ตัวอย่างการเทียบอยู่ใน เช็คสลิปโอนเงินด้วย PHP
  • หัก 1 เครดิตเฉพาะผล FOUND ผลอื่นไม่หัก ความหมายของทุกผลดูได้ที่ ผลตรวจรายการโอน

ต่อจากนี้