รายการขาย
รายการขาย คือบิลที่ปิดแล้ว 1 ใบ — สั่งอะไรบ้าง · ลดไปเท่าไหร่ · คิดภาษียังไง · ลูกค้าจ่ายด้วยอะไร และบิลนั้นถูกยกเลิกทีหลังหรือเปล่า · นี่คือทรัพยากรที่ระบบบัญชี · ระบบ BI หรืองานกระทบยอดรายวัน ใช้เวลาอยู่กับมันเกือบทั้งหมด
มี 2 เส้น:
GET /v1/transactions— บิลของร้านหนึ่งร้าน ทีละหน้าGET /v1/transactions/{id}— บิลใบเดียวจาก id
ทั้ง 2 เส้นเป็นการอ่านอย่างเดียว — สิ่งที่คุณทำที่นี่ไม่เปลี่ยนอะไรในระบบ ScanFood เลย
รายการขายคืออะไร
หัวข้อที่มีชื่อว่า “รายการขายคืออะไร”บิลถูกสร้างตอนแคชเชียร์ปิดการขาย · บิลใบเดียวรวมได้หลายออเดอร์ (โต๊ะที่สั่ง 3 รอบ
จ่ายครั้งเดียว) นี่คือเหตุผลที่ orderIds เป็น array
สิ่งที่ API คืนกลับมา คือชุดฟิลด์ที่ถูกคัดไว้ตายตัว — เขียนชื่อไว้ทีละช่องบนเซิร์ฟเวอร์ · บิลจริงในฐานข้อมูลถือข้อมูลมากกว่านี้มาก รวมถึงต้นทุนและสูตรวัตถุดิบ ⇒ ของที่ไม่ได้อยู่ ในรายการข้างล่างนี้ โผล่ออกมาไม่ได้ ทั้งวันนี้และหลังการแก้ไขในอนาคต
ใครใช้ข้อมูลนี้:
| งาน | ดึงอะไร |
|---|---|
| กระทบยอดรายวันกับรายงานของร้าน | ช่วงวันปฏิทินของร้านนั้น แล้วจัดกลุ่มด้วย businessDate |
| ซิงก์ต่อเนื่องเข้าคลังข้อมูล | updatedSince ทุก 15 นาที |
| วิเคราะห์เมนู/สินค้า | items[] ตลอดช่วงวัน |
| เชื่อมยอดกับสมาชิก | memberId บนบิล แล้ว join กับ สมาชิก |
| กำไรขาดทุนแยกช่องทาง (หน้าร้าน vs เดลิเวอรี) | channel.code + amounts |
ฟิลด์ของบิล
หัวข้อที่มีชื่อว่า “ฟิลด์ของบิล”รูป response ของเส้นลิสต์:
{ "ok": true, "data": [ { "…": "หนึ่งก้อนต่อหนึ่งบิล" } ], "nextCursor": null, "serverTime": "2026-09-01T06:30:00.000Z"}เส้นบิลใบเดียวคืนก้อนเดียวกันใต้ data และไม่มี nextCursor
| ฟิลด์ | ชนิด | เป็น null ได้ไหม | ความหมาย | ตัวอย่าง |
|---|---|---|---|---|
id |
string | ไม่ | รหัสบิล · เสถียร ใช้เป็นคีย์หลักได้ และเป็นค่าที่ใช้เป็น cursor ด้วย |
"tx9Bd2LmQ7sVfR4KpN1C" |
orderIds |
string[] | ไม่ (เป็น [] ได้) |
ออเดอร์ที่ถูกรวมมาปิดเป็นบิลใบนี้ | ["or4Tq8ZmB2vNs6WkY9Hd"] |
receiptNumber |
string | ได้ | เลขใบเสร็จที่พิมพ์ให้ลูกค้า | "RC-001" |
taxNumber |
string | ได้ | เลขใบกำกับภาษีเต็มรูป · มีเฉพาะบิลที่ออกใบกำกับภาษี | "TIV-2026-000144" |
timestamp |
string | ได้ | เวลาที่ปิดบิล · ISO-8601 พร้อม offset ของโซนเวลาร้าน | "2026-09-01T13:30:00.000+07:00" |
businessDate |
string | ได้ | วันธุรกิจที่บิลนี้ถูกนับ รูป YYYY-MM-DD (ดูด้านล่าง) |
"2026-09-01" |
billDate |
string | ได้ | วันปฏิทินบนนาฬิกาของร้าน รูป YYYY-MM-DD — นี่คือช่องที่ from/to กรอง |
"2026-09-01" |
updatedAt |
string | ได้ | ครั้งล่าสุดที่บิลเปลี่ยน · ISO-8601 พร้อม offset ร้าน · null บนบิลที่ปิดก่อนมีระบบติดตามการแก้ไข |
"2026-09-01T13:30:00.000+07:00" |
franchiseId |
string | ได้ | แฟรนไชส์ที่ร้านสังกัด | "fr5Ns8CtJ4vHqZ2WbY7K" |
shopId |
string | ไม่ | ร้านที่ออกบิล | "sh7Kq2mVbN4tRxZ0Lp8W" |
deviceId |
string | ได้ | เครื่องที่ปิดบิล | "dv6Mk1PqR8tZbN3WxL5J" |
stationId |
string | ได้ | สถานี POS ที่ปิดบิล | "st2Wc7YnV5qLpK8ZmR4T" |
channel |
object | ไม่ | ช่องทางการขาย — { "code", "name" } (ดูด้านล่าง) |
{ "code": "dine_in", "name": "โต๊ะ 3" } |
serviceType |
string | ได้ | "alacarte" สำหรับการขายปกติ · นอกนั้นคือ รหัส ของแพ็กเกจบุฟเฟ่ต์ที่ขาย ไม่ใช่ชื่อที่แสดง · ไม่ใช่ชุดค่าปิด เพราะแต่ละร้านตั้งแพ็กเกจเองได้ |
"alacarte" |
tableName |
string | ได้ | ชื่อโต๊ะ สำหรับบิลที่กินที่ร้าน | "โต๊ะ 3" |
memberId |
string | ได้ | โทเคนสมาชิกแบบทึบ (m1_…) เมื่อบิลผูกกับสมาชิก · ไม่ผูก = null |
"m1_ZmQ3YjJkNGE1…" |
items |
array | ไม่ (เป็น [] ได้) |
รายการบนบิล (ดูหัวข้อ “รายการสินค้า”) | — |
amounts |
object | ไม่ | ยอดเงินท้ายบิล (ดูหัวข้อ “ยอดเงินและภาษี”) | — |
payments |
array | ไม่ (เป็น [] ได้) |
ลูกค้าจ่ายด้วยอะไร (ดูหัวข้อ “การชำระเงิน”) | — |
status |
"completed" | "void" |
ไม่ | "void" = บิลถูกยกเลิกทั้งใบหลังปิดบิลแล้ว |
"completed" |
voidReason |
string | ได้ | เหตุผลที่บันทึกตอนยกเลิก · ไม่ได้ยกเลิก = null |
"กดผิดเมนู" |
cashier |
string | ได้ | ชื่อพนักงานที่ปิดบิล · ไม่ใช่รหัสพนักงาน | "สมชาย ใจดี" |
channel
หัวข้อที่มีชื่อว่า “channel”channel.code บอกว่าการขายเข้ามาทางไหน:
code |
ความหมาย |
|---|---|
"dine_in" |
นั่งกินที่ร้าน สั่งที่โต๊ะ |
"take_away" |
สั่งที่เคาน์เตอร์แล้วซื้อกลับบ้าน |
"pickup" |
ลูกค้าสั่งเอง (QR / ลิงก์สั่งอาหาร) แล้วมารับ |
| รหัสช่องทางที่ร้านตั้งเอง | ช่องทางที่ร้านสร้างขึ้น — แพลตฟอร์มเดลิเวอรี · โทรสั่ง · มาร์เกตเพลส |
null |
บิลไม่มีข้อมูลโต๊ะหรือช่องทางติดมา |
channel.name คือชื่อที่คนอ่าน: ชื่อช่องทางที่ร้านตั้งไว้ถ้าจับคู่ได้ · ถ้าไม่ได้ก็เป็นชื่อโต๊ะ ·
ถ้าไม่มีทั้งคู่ = null
การกรอง
หัวข้อที่มีชื่อว่า “การกรอง”คุณต้องเลือก 1 ใน 2 โหมดต่อคำขอ · ส่งทั้ง 2 โหมด หรือไม่ส่งเลย = 400 bad_query
เพราะ API จะไม่เดาว่าคุณหมายถึงอันไหน
| โหมด A — ช่วงวันปฏิทิน | โหมด B — ส่วนที่เปลี่ยน | |
|---|---|---|
| พารามิเตอร์ | from + to |
updatedSince |
| กรองที่ช่อง | billDate |
updatedAt |
| รูปแบบ | YYYYMMDD ไม่มีขีด |
ISO-8601 datetime |
| ช่วงสูงสุด | 31 วัน (นับรวมหัวท้าย) | ไม่จำกัด |
| การเรียง | billDate จากน้อยไปมาก |
updatedAt จากน้อยไปมาก |
| เหมาะกับ | ดึงย้อนหลัง · ปิดยอดสิ้นเดือน | ซิงก์ต่อเนื่องทุกไม่กี่นาที |
ที่เหมือนกันทั้ง 2 โหมด: shopId บังคับ (ร้านนอกขอบเขตกุญแจ = 403 shop_not_in_scope) ·
limit ค่าตั้งต้น 100 · cursor ใช้อ่านหน้าถัดไป
limit เป็นค่าที่ระบบ แก้ให้ ไม่ใช่ปฏิเสธ: ค่าเกิน 200 จะถูกลดเหลือ 200 เงียบ ๆ
และค่าที่ไม่ใช่จำนวนบวก (0 · ติดลบ · ตัวอักษร) จะตกกลับไปที่ 100 ⇒ ให้ดูที่ nextCursor
ไม่ใช่จำนวนแถว เพื่อรู้ว่าดึงครบหรือยัง
billDate เทียบกับ businessDate
หัวข้อที่มีชื่อว่า “billDate เทียบกับ businessDate”ความต่างข้อนี้คือสาเหตุอันดับหนึ่งของอาการ “ยอดไม่ตรง”
businessDateคือวันที่บิลถูกนับเป็น รายได้ ของร้าน · วันธุรกิจของร้านจบตามเวลา ปิดยอดที่ร้านตั้งเอง ซึ่งมักเลยเที่ยงคืนไปแล้ว ·billDateคือวันตามปฏิทินธรรมดา · ร้านที่ปิดยอดตอน 06:00 บิลที่จ่ายตอนตี 2 จะมีbillDateเป็น วันพรุ่งนี้ แต่businessDateเป็น เมื่อวาน
การกรองใช้ billDate เสมอ · businessDate กรองไม่ได้ — มันถูกส่งมาให้คุณ
จัดกลุ่มเอง หลังดึงข้อมูลมาแล้ว
⇒ การกระทบยอด 1 วันธุรกิจ ต้องดึงช่วงวันปฏิทินที่กว้างกว่า แล้วค่อยจัดกลุ่ม:
- ขอ
from= วันก่อนหน้า ·to= วันถัดไป ของวันธุรกิจที่ต้องการ (รวม 3 วันปฏิทิน) - จัดกลุ่มแถวที่ได้ด้วย
businessDate - เก็บเฉพาะกลุ่มที่ต้องการ
updatedAt ถูกประทับตอนไหน
หัวข้อที่มีชื่อว่า “updatedAt ถูกประทับตอนไหน”โหมด B คืนเฉพาะบิลที่ updatedAt ขยับ · ตราประทับลงเมื่อ:
- ปิดบิล
- ออกเลขใบกำกับภาษีให้บิลนั้น
- ยกเลิก / กลับรายการบิล
- แก้ไขข้อมูลบนบิล
- สมาชิกเคลมบิล
- เขียนโน้ตบนบิล
และ จงใจไม่ประทับ ตอนที่สถานะการส่งออกไปยังระบบบัญชีเปลี่ยน — เพราะนั่นไม่ใช่ การเปลี่ยนเนื้อบิล · ถ้าประทับ บิลจะโผล่ซ้ำในรอบซิงก์ของคุณทั้งที่ไม่มีอะไรต่างเลย
บิลที่ยกเลิกก็ถูกส่งมาด้วย
หัวข้อที่มีชื่อว่า “บิลที่ยกเลิกก็ถูกส่งมาด้วย”บิลที่ยกเลิกยังถูกคืนมา พร้อม status: "void" ⇒ หน้าที่ตัดออกจากยอดขายเป็นของคุณ ·
และเพราะการยกเลิกประทับ updatedAt บิลที่คุณเคยนำเข้าไปแล้วเป็น completed จะกลับมา
อีกครั้งในโหมด B เป็น void ⇒ ให้ upsert ด้วย id ห้าม append
รายการสินค้า
หัวข้อที่มีชื่อว่า “รายการสินค้า”แต่ละแถวใน items[]:
| ฟิลด์ | ชนิด | เป็น null ได้ไหม | ความหมาย |
|---|---|---|---|
id |
string | ได้ | รหัสสินค้า — คีย์สำหรับ join กับแคตตาล็อกของคุณเอง |
rootId |
string | ได้ | รหัสสินค้าแม่ สำหรับตัวเลือกย่อยและสินค้าในชุด |
name |
string | ได้ | ชื่อสินค้าตามที่พิมพ์บนบิล |
qty |
number | ได้ | จำนวน |
unitPrice |
number | ได้ | ราคาต่อหน่วยที่บันทึกไว้บนบรรทัด · ถ้า POS ไม่ได้บันทึกไว้จะคิดจากยอดรวมบรรทัด ÷ จำนวน · เป็น null เมื่อคิดแบบนั้นไม่ได้ (อ่านจำนวนไม่ได้ · จำนวนเป็น 0 · ไม่มียอดรวมบรรทัด) |
lineTotal |
number | ได้ | ยอดรวมของบรรทัดนั้น |
options |
array | ไม่ (เป็น [] ได้) |
ตัวเลือกที่ลูกค้าเลือก: { "choiceId", "choiceName", "qty" } · qty ค่าตั้งต้น 1 |
category |
array ของ string | ได้ | หมวดที่สินค้าอยู่ ณ เวลาที่ขาย เป็น รหัสหมวด เรียงจากหมวดบนสุดลงไปหมวดย่อยสุด · ส่วนใหญ่มีรหัสเดียว · สินค้าไม่มีหมวด = [] |
cancelled |
boolean | ไม่ | true เมื่อบรรทัดนี้ถูกยกเลิกระหว่างสั่ง |
vat |
boolean | ไม่ | บรรทัดนี้คิด VAT ไหม |
isTip |
boolean | ไม่ | true เมื่อบรรทัดนี้เป็นทิป ไม่ใช่สินค้า |
แถวที่ cancelled: true และแถวที่ isTip: true อยู่ใน array ทั้งคู่ ⇒ กรองออกก่อน
คำนวณยอดขายรายสินค้า
บิล ไม่มี SKU และไม่มีบาร์โค้ด ⇒ ใช้ items[].id เป็นคีย์ join กับแคตตาล็อกสินค้าของคุณ
ส่วนลดไม่ถูกกระจายลงรายบรรทัด · lineTotal เป็นยอดก่อนหักส่วนลด · ส่วนลดทุกตัว
อยู่ที่ระดับบิลใน amounts ⇒ ถ้าต้องการยอดรายสินค้าหลังหักส่วนลด ให้เฉลี่ย
amounts.discountTotal ลงแต่ละบรรทัดเองตามกติกาที่คุณเลือก
ยอดเงินและภาษี
หัวข้อที่มีชื่อว่า “ยอดเงินและภาษี”ทุกค่าเป็น เงินบาท (THB)
| ฟิลด์ | ชนิด | เป็น null ได้ไหม | ความหมาย |
|---|---|---|---|
subtotal |
number | ได้ | ผลรวมรายการก่อนหักส่วนลดและค่าธรรมเนียม โดยไม่นับบรรทัดทิป — ดูหมายเหตุใต้ตาราง |
discount |
number | ได้ | ส่วนลดที่แคชเชียร์กดเอง |
campaign |
array | ไม่ (เป็น [] ได้) |
โปรโมชันที่ใช้ แถวละ 1 โปรโมชัน — ดู campaign[] และ coupon[] |
coupon |
array | ไม่ (เป็น [] ได้) |
คูปองที่ใช้ แถวละ 1 ใบ — รูปแถวเดียวกัน |
discountTotal |
number | ได้ | ส่วนลดรวม ที่บันทึกไว้บนบิล — มือ + แคมเปญ + คูปอง · ไม่ถูกจำกัดด้วยยอดที่ต้องจ่าย — ดูหมายเหตุใต้ตาราง |
serviceCharge |
number | ได้ | ค่าบริการ |
deliveryFee |
number | ได้ | ค่าส่งที่เก็บจากลูกค้า |
cardSurcharge |
number | ได้ | ค่าธรรมเนียมบัตร |
credit |
number | ได้ | เครดิตร้านที่ใช้ตัดในบิลนี้ |
vat |
number | ได้ | ยอด VAT |
vatType |
string | ได้ | โหมด VAT (ดูด้านล่าง) |
rounding |
number | ได้ | ปัดเศษ |
tip |
number | ได้ | ทิป |
net |
number | ได้ | ยอดที่ลูกค้าจ่ายจริง — รวมทิปแล้ว |
campaign[] และ coupon[]
หัวข้อที่มีชื่อว่า “campaign[] และ coupon[]”ทั้งสองลิสต์ใช้รูปแถวเดียวกัน · บิลที่ไม่มีโปรโมชันหรือไม่มีคูปองจะได้ [] ไม่ใช่ null
| ฟิลด์ | ชนิด | เป็น null ได้ไหม | ความหมาย |
|---|---|---|---|
id |
string | ได้ | รหัสของโปรโมชันหรือคูปอง ตามที่ร้านตั้งค่าไว้ |
name |
string | ได้ | ชื่อของโปรโมชันหรือคูปองที่คนอ่านรู้เรื่อง |
amount |
number | ไม่ | ยอดส่วนลดที่แถวนี้ให้ หน่วยบาท · เป็น 0 เมื่อแถวนั้นไม่มียอดลด |
vatType
หัวข้อที่มีชื่อว่า “vatType”| ค่า | ความหมาย |
|---|---|
"Include" |
ราคาบนเมนูรวม VAT แล้ว · ยอด vat ถูกถอดออกจากยอดรวม |
"Exclude" |
VAT ถูกบวกเพิ่มท้ายบิล |
"" |
ร้านยังไม่เคยตั้งค่า VAT |
null |
บิลไม่มีข้อมูลการตั้งค่า VAT ติดมา |
ข้อมูลต้นทุนไม่ถูกส่งออกเลย — ต้นทุนรายบรรทัด · ต้นทุนรวม · สูตรวัตถุดิบ และการตัดสต็อก ถูกกันออกจาก API นี้โดยนโยบาย ทุกระดับกุญแจ ⇒ คำนวณกำไรขั้นต้น จากข้อมูลชุดนี้อย่างเดียวไม่ได้
การชำระเงิน
หัวข้อที่มีชื่อว่า “การชำระเงิน”payments[] บันทึกว่าบิลถูกชำระด้วยอะไร · บิลใบเดียวแยกจ่ายหลายช่องทางได้ จึงเป็น array:
| ฟิลด์ | ชนิด | เป็น null ได้ | ความหมาย |
|---|---|---|---|
name |
string | ได้ | ชื่อช่องทางรับเงินตามที่ร้านตั้งไว้ — เช่น "เงินสด" · "โอน" · "บัตรเครดิต" |
amount |
number | ได้ | ยอดที่ลูกค้า ยื่นให้ ผ่านช่องทางนั้น หน่วยบาท — คือเงินที่ยื่นมา ไม่ใช่ยอดที่ตัดจริง |
change |
number | ได้ | เงินทอนของรายการชำระแถวนี้ หน่วยบาท · ช่องทางที่ไม่มีเงินทอน = 0 |
รหัสอ้างอิงภายในของรายการชำระ ไม่ถูกส่งออก · และเพราะ amount คือเงินที่ลูกค้ายื่นมา
ผลรวมของ payments[].amount จึงมากกว่า amounts.net ได้ เมื่อจ่ายเงินสดแล้วรับเงินทอน ⇒
ยอดที่ตัดจริงของแต่ละแถวคือ amount - change และตัวเลขชุดนั้นคือตัวที่รวมได้เท่ากับ amounts.net
บิลที่ยกเลิกและคืนเงิน
หัวข้อที่มีชื่อว่า “บิลที่ยกเลิกและคืนเงิน”ScanFood ไม่มีรายการคืนเงินแยกต่างหาก — การคืนเงินหน้าร้านทำโดยการยกเลิกบิล ⇒
statusมีแค่ 2 ค่า:"completed"และ"void"- ไม่มีแนวคิดคืนเงินบางส่วน และไม่มีบิลติดลบ
voidReasonเก็บสิ่งที่พนักงานพิมพ์ไว้- การยกเลิกประทับ
updatedAt⇒ การเปลี่ยนแปลงไปถึงรอบซิงก์แบบต่อเนื่อง
ระบบนำเข้าข้อมูลของคุณจึงควร:
- upsert ด้วย
idไม่ใช่ insert ตรง ๆ - คำนวณยอดรวมของงวดใหม่โดยตัด
status: "void"ออก แทนที่จะสมมติว่าบิลที่นำเข้าไป เมื่อวานยังใช้ได้อยู่ - เก็บแถวที่ยกเลิกไว้ อย่าลบทิ้ง จะได้อธิบายได้ว่าทำไมยอดของวันนั้นถูกแก้
การซิงก์ข้อมูล
หัวข้อที่มีชื่อว่า “การซิงก์ข้อมูล”กระทบยอดรายวัน
หัวข้อที่มีชื่อว่า “กระทบยอดรายวัน”ดึง 1 วันธุรกิจ แล้วจัดกลุ่มด้วย businessDate
# ดึงหน้าต่างวันปฏิทิน 3 วันคร่อมวันธุรกิจ 2026-09-01# แล้วค่อยจัดกลุ่มด้วย businessDate ที่ฝั่งคุณcurl -s -G https://api.scanfood.co/ext/v1/transactions \ -H "Authorization: Bearer sf_live_4b8f2c1e9d07_3f9a2c1b7e4d8506a1b2c3d4e5f60718" \ --data-urlencode "shopId=sh7Kq2mVbN4tRxZ0Lp8W" \ --data-urlencode "from=20260831" \ --data-urlencode "to=20260902" \ --data-urlencode "limit=200"const BASE = 'https://api.scanfood.co/ext/v1';const KEY = process.env.SCANFOOD_API_KEY;
async function getPage(params) { const url = new URL(`${BASE}/transactions`); for (const [k, v] of Object.entries(params)) { if (v != null) url.searchParams.set(k, v); } const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } }); const body = await res.json(); if (!res.ok) throw new Error(`${res.status} ${body.error}: ${body.message}`); return body;}
/** ทุกบิลในช่วงวันปฏิทิน โดยไล่ cursor จนหมด */async function fetchRange(shopId, from, to) { const rows = []; let cursor = null; do { const page = await getPage({ shopId, from, to, limit: 200, cursor }); rows.push(...page.data); cursor = page.nextCursor; } while (cursor); return rows;}
// วันธุรกิจ 2026-09-01 — ดึงหน้าต่างวันปฏิทินที่กว้างกว่า แล้วจัดกลุ่มconst bills = await fetchRange('sh7Kq2mVbN4tRxZ0Lp8W', '20260831', '20260902');
const day = bills.filter( (b) => b.businessDate === '2026-09-01' && b.status === 'completed',);
const revenue = day.reduce((sum, b) => sum + ((b.amounts.net ?? 0) - (b.amounts.tip ?? 0)), 0);console.log('ยอดขาย 2026-09-01 (ไม่รวมทิป):', revenue.toFixed(2), 'บาท');import osimport requests
BASE = "https://api.scanfood.co/ext/v1"HEADERS = {"Authorization": f"Bearer {os.environ['SCANFOOD_API_KEY']}"}
def get_page(params): query = {k: v for k, v in params.items() if v is not None} res = requests.get(f"{BASE}/transactions", headers=HEADERS, params=query, timeout=60) body = res.json() if res.status_code != 200: raise RuntimeError(f"{res.status_code} {body['error']}: {body['message']}") return body
def fetch_range(shop_id, date_from, date_to): """ทุกบิลในช่วงวันปฏิทิน โดยไล่ cursor จนหมด""" rows, cursor = [], None while True: # "from" เป็นคำสงวนของ Python จึงประกอบ query เป็น dict page = get_page({ "shopId": shop_id, "from": date_from, "to": date_to, "limit": 200, "cursor": cursor, }) rows.extend(page["data"]) cursor = page["nextCursor"] if not cursor: return rows
# วันธุรกิจ 2026-09-01 — ดึงหน้าต่างวันปฏิทินที่กว้างกว่า แล้วจัดกลุ่มbills = fetch_range("sh7Kq2mVbN4tRxZ0Lp8W", "20260831", "20260902")
day = [b for b in bills if b["businessDate"] == "2026-09-01" and b["status"] == "completed"]revenue = sum((b["amounts"]["net"] or 0) - (b["amounts"]["tip"] or 0) for b in day)
print(f"ยอดขาย 2026-09-01 (ไม่รวมทิป): {revenue:.2f} บาท")ซิงก์ทุก 15 นาที
หัวข้อที่มีชื่อว่า “ซิงก์ทุก 15 นาที”เก็บค่า updatedAt ที่สูงที่สุดที่นำเข้าไปแล้ว แล้วส่งกลับมาเป็น updatedSince
ในรอบถัดไป · เพราะข้อมูลเรียงตาม updatedAt จากน้อยไปมาก การส่งค่าเดิมซ้ำจึงปลอดภัย:
คุณจะได้บิลที่คาบเส้นกลับมาอีกใบแล้ว upsert ทับตัวมันเอง
curl -s -G https://api.scanfood.co/ext/v1/transactions \ -H "Authorization: Bearer sf_live_4b8f2c1e9d07_3f9a2c1b7e4d8506a1b2c3d4e5f60718" \ --data-urlencode "shopId=sh7Kq2mVbN4tRxZ0Lp8W" \ --data-urlencode "updatedSince=2026-09-01T06:15:00.000Z" \ --data-urlencode "limit=200"/** * ดึงเฉพาะส่วนที่เปลี่ยน · `since` = ค่า updatedAt สูงสุดที่นำเข้าไปแล้ว * รอบแรกสุดให้ทำ backfill ด้วย from/to ก่อน แล้วค่อยมาใช้เส้นนี้ */async function syncSince(shopId, since) { let cursor = null; let highest = since;
do { const page = await getPage({ shopId, updatedSince: since, limit: 200, cursor });
for (const bill of page.data) { upsertBill(bill); // ที่เก็บของคุณ — คีย์ที่ bill.id if (bill.updatedAt && (!highest || bill.updatedAt > highest)) { highest = bill.updatedAt; } }
cursor = page.nextCursor; } while (cursor);
return highest; // เก็บค่านี้ไว้ใช้เป็น `since` รอบหน้า}
const watermark = await syncSince('sh7Kq2mVbN4tRxZ0Lp8W', loadWatermark());saveWatermark(watermark);def sync_since(shop_id, since): """ดึงเฉพาะส่วนที่เปลี่ยน · `since` = ค่า updatedAt สูงสุดที่นำเข้าไปแล้ว ให้ทำ backfill ด้วย from/to ก่อนรอบแรกเสมอ""" cursor, highest = None, since
while True: page = get_page({ "shopId": shop_id, "updatedSince": since, "limit": 200, "cursor": cursor, })
for bill in page["data"]: upsert_bill(bill) # ที่เก็บของคุณ — คีย์ที่ bill["id"] stamp = bill.get("updatedAt") if stamp and (highest is None or stamp > highest): highest = stamp
cursor = page["nextCursor"] if not cursor: return highest # เก็บไว้ใช้เป็น `since` รอบหน้า
watermark = sync_since("sh7Kq2mVbN4tRxZ0Lp8W", load_watermark())save_watermark(watermark)ดึงบิลใบเดียวจาก id
หัวข้อที่มีชื่อว่า “ดึงบิลใบเดียวจาก id”curl -s https://api.scanfood.co/ext/v1/transactions/tx9Bd2LmQ7sVfR4KpN1C \ -H "Authorization: Bearer sf_live_4b8f2c1e9d07_3f9a2c1b7e4d8506a1b2c3d4e5f60718"บิลที่ไม่มีอยู่จริง กับบิลของร้านที่อยู่นอกขอบเขตกุญแจ คืน 404 not_found เหมือนกัน —
API จะไม่ยืนยันว่า id นั้นมีอยู่จริงนอกขอบเขตของคุณ
ตัวอย่าง response
หัวข้อที่มีชื่อว่า “ตัวอย่าง response”{ "ok": true, "data": [ { "id": "tx9Bd2LmQ7sVfR4KpN1C", "orderIds": ["or4Tq8ZmB2vNs6WkY9Hd", "orH5nP2wQ8tLcZ3RvK7M"], "receiptNumber": "RC-001", "taxNumber": null, "timestamp": "2026-09-01T13:30:00.000+07:00", "businessDate": "2026-09-01", "billDate": "2026-09-01", "updatedAt": "2026-09-01T13:30:00.000+07:00", "franchiseId": null, "shopId": "sh7Kq2mVbN4tRxZ0Lp8W", "deviceId": "dv6Mk1PqR8tZbN3WxL5J", "stationId": "st2Wc7YnV5qLpK8ZmR4T", "channel": { "code": "ch4Rn9WsT2kLqZ6BvY8M", "name": "แกร็บฟู้ด" }, "serviceType": "alacarte", "tableName": null, "memberId": "m1_ZmQ3YjJkNGE1ZTZmNzg5MGExYjJjM2Q0ZTVmNjc4OTBhYmNkZWYxMjM", "items": [ { "id": "pd8Zt3QvL6mNc1KwB9Ys", "rootId": "rt1Kp5MwZ9bQn7VxL3Cd", "name": "สปาเก็ตตี้ขี้เมา", "qty": 3, "unitPrice": 137.55, "lineTotal": 412.65, "options": [], "category": ["9369601f-fef5-4a61-bfb8-26f25eab3dad"], "cancelled": false, "vat": true, "isTip": false } ], "amounts": { "subtotal": 412.65, "discount": 20, "campaign": [], "coupon": [], "discountTotal": 20, "serviceCharge": 0, "deliveryFee": 0, "cardSurcharge": 0, "credit": 0, "vat": 0, "vatType": "Include", "rounding": 0, "tip": 0, "net": 392.65 }, "payments": [{ "name": "เงินสด", "amount": 400, "change": 7.35 }], "status": "completed", "voidReason": null, "cashier": "สมชาย ใจดี" } ], "nextCursor": null, "serverTime": "2026-09-01T06:30:00.000Z"}สิ่งที่ไม่มีวันส่งออก
หัวข้อที่มีชื่อว่า “สิ่งที่ไม่มีวันส่งออก”เป็นนโยบาย และถูกบังคับด้วยรูปร่างของ response เอง:
| กลุ่ม | ไม่มีให้ |
|---|---|
| ต้นทุน | ต้นทุนรายบรรทัด · ต้นทุนรวม · สูตรวัตถุดิบ · การตัดสต็อก |
| ตัวตนลูกค้า | ชื่อลูกค้า · ชื่อสมาชิก · เบอร์โทร · อีเมล · ที่อยู่ · ข้อมูลผู้ขอใบกำกับภาษี |
| หลักฐานและโน้ต | สลิป/ภาพหลักฐานการโอน · โน้ตบนบิล · รูปที่อัปโหลด |
| ร่องรอยพนักงาน | รหัสพนักงาน · ผู้ยกเลิกบิล · รหัสกะและผู้จัดการ |
| ปฏิบัติการหน้าร้าน | เลขติดตามพัสดุ/เดลิเวอรี · รายละเอียดการรับของ · พนักงานที่ผูกรายสินค้า |
| อ้างอิงภายใน | รหัสอ้างอิงเกตเวย์รับชำระ · สถานะการส่งออกไประบบบัญชี · ข้อมูลการแก้ไข |
ถ้ารายงานที่ต้องทำจำเป็นต้องใช้ของพวกนี้ = ทำจาก API นี้ไม่ได้ ⇒ บอกกันตั้งแต่ต้น ดีกว่าออกแบบระบบรอบ ๆ ช่องที่จะไม่มีวันมา
อ่านต่อ
หัวข้อที่มีชื่อว่า “อ่านต่อ”- สมาชิก — แปลง
memberIdบนบิลเป็นข้อมูลสมาชิก - การแบ่งหน้าและการซิงก์ — กติกา cursor แบบละเอียด
- โซนเวลาและวันธุรกิจ
- ข้อผิดพลาด — รหัสทุกตัวที่เส้นนี้คืนได้
- ขีดจำกัดอัตราการเรียก — ดึงข้อมูลได้เร็วแค่ไหน