ข้ามไปยังเนื้อหา

เช็กลิสต์ก่อนขึ้นจริง

ไล่ทีละข้อ ทุกข้อในนี้คือของที่ถ้าตอบผิดวันนี้ยังแก้ได้ถูก ๆ แต่ถ้าไปเจอตอนร้านกำลังขายจะแพงมาก

ข้อไหนยังไม่ได้ลองจริง อย่าเพิ่งติ๊ก

  • กุญแจอยู่ใน secret manager หรือ environment variable ที่ฉีดตอนรัน — ไม่ใช่ ในโค้ดที่ commit ไม่ใช่ในไฟล์ config ที่ขึ้น repo ไม่ใช่ในเอกสารที่แชร์กัน
  • เรียก API จากเซิร์ฟเวอร์ของคุณเท่านั้น · เราปิดการเรียกข้ามโดเมนไว้โดยเจตนา ⇒ เบราว์เซอร์หรือแอปมือถือเรียกตรงไม่ได้ และไม่ควรถือกุญแจที่อ่านยอดขายของร้านอยู่แล้ว
  • ไม่เอากุญแจไปใส่ใน URL, query string หรือ body · เราอ่านจาก Authorization: Bearer <key> ที่เดียวเท่านั้น
  • ระบบแต่ละตัวมีกุญแจของตัวเองพร้อมป้ายชื่อ ⇒ เพิกถอนใบหนึ่งได้โดยไม่ทำให้ตัวอื่นหยุด
  • ขอบเขตของกุญแจแคบที่สุดเท่าที่ทำงานได้ — ต่อสาขาเดียวก็ใช้กุญแจร้านเดียว · ใช้กุญแจแฟรนไชส์เฉพาะตอนที่ต้องการทุกสาขาจริง ๆ
  • ยิงไปที่ https://api.scanfood.co/ext/v1/… เท่านั้น · ไม่มีโฮสต์อื่นที่รองรับ และโฮสต์ที่เคยได้ไปตอนพัฒนาไม่ใช่ตัวสำรอง
  • มีคนอื่นนอกจากคนเขียนโค้ดที่รู้ว่ากุญแจเก็บไว้ที่ไหน และต้องติดต่อใครเวลาจะขอเปลี่ยน
  • วันที่ขาออกไม่มีขีด ขากลับมีขีด — คำขอใช้ from=20260901 ส่วนคำตอบคืน billDate: "2026-09-01" · อย่าเอารูปหนึ่งไปป้อนอีกรูปหนึ่ง
  • ทุกอย่างเป็นเวลาไทย (ICT) — from/to เป็นวันปฏิทินของโซนร้าน · businessDate/billDate เป็นวันบนผนังนาฬิกาของร้าน · โควตาวันรีเซ็ต 00:00 ICT · มีค่าเดียวที่เป็น UTC คือ serverTime ซึ่งลงท้ายด้วย Z
  • กระทบยอดด้วย businessDate ไม่ใช่ billDate — ร้านที่ปิดยอดหลังเที่ยงคืน บิลตี 2 สังกัดวันขายก่อนหน้า · การกรองเกิดบน billDate แต่การกระทบยอดใช้ businessDate ซึ่งทุกแถวส่งมาให้ด้วยเหตุผลนี้พอดี
  • ซอยช่วงวันไม่เกิน 31 วัน — กว้างกว่านั้นถูกปัดด้วย 400 bad_query ไม่ได้ตัดให้เงียบ ๆ
  • ไล่หน้าจน nextCursor เป็น null โดยส่ง nextCursor ของรอบก่อนกลับมาเป็น cursor · ถ้าหน้าสุดท้ายเต็มพอดี คุณจะได้ cursor ที่หน้าถัดไปว่าง — เป็นเรื่องปกติ ไม่ใช่บั๊ก
  • จัดการ cursor ที่ตายแล้ว — ถ้าแถวที่ cursor ชี้หายไป จะได้ 400 bad_cursor ⇒ ให้เริ่มหน้านั้นใหม่โดยไม่ส่ง cursor แทนที่จะล้มทั้งรอบ
  • บิล: ดึงย้อนหลังด้วย from/to ให้ครบก่อน แล้วค่อยสลับไป updatedSince — บิลที่ปิดก่อนระบบติดตามการเปลี่ยนแปลงจะไม่มี updatedAt และไม่มีวันโผล่ในโหมดนั้น · ตั้งโหมดนี้บนประวัติที่ว่างเปล่า = บิลหายถาวร
  • สมาชิก: full sync เท่านั้น — updatedSince บน /v1/members ตอบ 400 not_supported โดยเจตนา ⇒ ไล่หน้าด้วย cursor แล้วไปเทียบเอาฝั่งคุณ
  • ดึงซ้ำแล้วผลไม่เพี้ยน — ทุกเส้นเป็น GET ยิงซ้ำช่วงเดิมได้ปลอดภัย ตราบใดที่ฝั่งคุณ upsert ด้วย id (บิล) หรือ memberId (สมาชิก) ไม่ใช่ insert ซ้ำ
  • แตกสาขาที่ error ไม่ใช่ที่ message — error คือสัญญา ส่วนถ้อยคำใน message เปลี่ยนได้โดยไม่แจ้ง
  • รองรับบิลที่ถูกยกเลิก — status มี completed กับ void · บิลที่ยกเลิกยังอยู่และมี voidReason เพิ่มมา · ไม่มีก้อนข้อมูลคืนเงินแยก เพราะการคืนเงินหน้าร้านคือการยกเลิกบิล
  • อ่านส่วนลดที่ระดับบิล — amounts.discount, campaign, coupon, discountTotal เป็นของทั้งบิล · รายการสินค้าไม่มีช่องส่วนลดต่อบรรทัด
  • เจอช่องที่ไม่รู้จักให้ข้าม อย่าตีตก — ช่องใหม่โผล่มาในคำตอบได้ตลอด · parser ที่ throw ใส่ช่องแปลกจะพังในรุ่นที่ไม่ทำให้ใครพังเลย
  • ลองใหม่แบบถอยเป็นทวีคูณ และเคารพ Retry-After — หัวนี้โผล่กับ 429 rate_limited และบอกตรง ๆ ว่าให้รอกี่วินาที · นอน 1 วินาทีคงที่แล้วยิงรัวต่อ คือวิธีเปลี่ยนการรอ 1 นาทีให้กลายเป็น 1 ชั่วโมง
  • แยก 429 quota_exceeded ออกจาก 429 rate_limited — โควตาวันไม่ฟื้นด้วยการรอไม่กี่วินาที มันรีเซ็ต 00:00 ICT ⇒ หยุดรอบ แจ้งคน แล้วค่อยไปต่อพรุ่งนี้ หรือขอเพิ่มโควตา
  • 503 auth_unavailable ให้ลองใหม่ ไม่ใช่ไปไล่ดูกุญแจ — แปลว่าตัวตรวจกุญแจฝั่งเราไม่พร้อมชั่วคราว กุญแจคุณไม่ได้มีปัญหา ⇒ ลองใหม่แบบถอย อย่าปลุกคนมาออกกุญแจใหม่ตอนตี 2 ฟรี ๆ
  • 500 internal ลองใหม่ 2-3 ครั้งแล้วค่อยรายงาน — เป็นฝั่งเรา และมักหายเอง
  • 401 ให้หยุดรอบแล้วปลุกคน — กุญแจที่ถูกเพิกถอนหรือพิมพ์ผิดไม่มีวันหายเองจากการลองใหม่ · ลูปลองใหม่ถี่ ๆ บน 401 มีแต่จะเผาโควตาวันทิ้ง
  • ทุกคำขอมี timeout และมีเพดานจำนวนครั้งที่ลองใหม่ · ลองใหม่ไม่จำกัด + มีเพดานอัตรา = คุณทำระบบตัวเองล่มเอง
  • บันทึก X-RateLimit-Remaining และตั้งเตือนตั้งแต่ยังเหลือที่ว่าง ไม่ใช่ตอนเหลือศูนย์
  • รู้ว่าคำขอที่ล้มก็ถูกนับ — มิเตอร์ทั้ง 2 ตัวนับคำขอที่รับเข้ามา ไม่ใช่คำขอที่สำเร็จ ⇒ พายุลองใหม่กินโควตาเท่างานจริง
  • อย่าพึ่งพาว่าเพดานต่อนาทีจะเป๊ะ — มันนับแยกตามเครื่องที่กำลังทำงาน และถ้ากลไกของมันเองมีปัญหาจะปล่อยผ่าน ⇒ ปล่อยทะลุได้เล็กน้อยกว่าตัวเลขที่ประกาศ · ให้ถือว่าเป็นเบรก ไม่ใช่ตัวคุมจังหวะของคุณ
const BASE = 'https://api.scanfood.co/ext/v1';
async function apiGet(path, { attempts = 5 } = {}) {
for (let attempt = 1; ; attempt++) {
const res = await fetch(BASE + path, {
headers: { Authorization: `Bearer ${process.env.SCANFOOD_API_KEY}` },
signal: AbortSignal.timeout(30_000),
});
if (res.ok) return res.json();
const body = await res.json().catch(() => ({}));
// ไม่ลองใหม่: คำตอบไม่เปลี่ยนถ้าไม่มีคนมาแก้
if (res.status === 401 || res.status === 403 || res.status === 400) {
throw new Error(`${res.status} ${body.error}: ${body.message}`);
}
// โควตาวันหมดจนถึงเที่ยงคืน ICT — หยุด อย่าวน
if (body.error === 'quota_exceeded') {
throw new Error('โควตาวันหมด · เริ่มใหม่ 00:00 ICT');
}
if (attempt >= attempts) throw new Error(`ยอมแพ้หลัง ${attempts} ครั้ง: ${body.error}`);
const retryAfter = Number(res.headers.get('Retry-After'));
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000 // เซิร์ฟเวอร์บอกมาแล้ว ทำตาม
: Math.min(2 ** attempt * 500, 30_000); // ไม่บอก = ถอยเอง
await new Promise((r) => setTimeout(r, waitMs + Math.random() * 250));
}
}
  • บันทึกทุกคำขอไว้ฝั่งคุณ — path, query, สถานะ HTTP, รหัส error ถ้ามี, จำนวนแถวที่ได้ และเวลา · อีก 3 สัปดาห์ตอนของหาย นี่คือบันทึกชุดเดียวที่ผูกรอบดึงของคุณเข้ากับของเรา
  • เก็บ id ของบิล และ memberId ไว้กับทุกแถวที่นำเข้า — 2 ค่านี้คือมือจับที่เสถียรสำหรับ /v1/transactions/{id} กับ /v1/members/{memberToken} และเป็นทางเดียวที่จะถามเราถึงรายการใดรายการหนึ่งได้
  • บอก keyId กับเวลาแบบ ICT ได้เวลาติดต่อเรา — สมุดการใช้งานของเราเรียงตามกุญแจ path และเวลา · keyId คือท่อนกลางของ sf_live_<keyId>_<secret> และบอกเราได้อย่างปลอดภัย ส่วนท่อนความลับห้ามบอก
  • รอบที่ได้ 0 แถวต้องมีสัญญาณ — คำตอบว่างเป็นเรื่องถูกต้อง (สาขาปิด · คืนที่เงียบ) แต่รอบที่เงียบติดกัน 3 วันไม่ควรมารู้เอาตอนดูรายงานสิ้นเดือน
  • มีคนได้รับแจ้งเมื่อรอบดึงหยุดเดิน ไม่ใช่เฉพาะตอนที่มันล้มแบบเสียงดัง
  • ยืนยันแล้วว่าการเพิกถอนทำงานจริงตลอดเส้น — ขอให้เราเพิกถอนกุญแจที่ใช้ลอง แล้วดูระบบตัวเอง · ภายใน 5 นาทีมันต้องเริ่มเห็น 401 key_revoked และต้องแจ้งเตือน ไม่ใช่ลองใหม่ไปเรื่อย ๆ
  • รู้เพดานของกุญแจตัวเอง — ยืนยันตัวเลขต่อนาทีและต่อวันที่ออกให้กุญแจใบนั้น แล้ววางรอบดึงตามตัวเลขนั้น ไม่ใช่ตามค่าตั้งต้นในเว็บนี้
  • ให้รอบแรกวิ่งตอนมีคนเฝ้า ไม่ใช่ปล่อยข้ามคืน
  • กระทบยอด 1 วันขายเต็ม ๆ กับรายงานปิดวันของร้าน โดยจัดกลุ่มด้วย businessDate ก่อนจะเชื่อทั้งท่อ
  • ดู X-RateLimit-Remaining ตอนจบวันแรก — ตัวเลขนั้นบอกว่าคุณมีที่ว่างพอจะเพิ่มความถี่ทีหลังแค่ไหน
  • ขอให้เพิกถอนกุญแจที่ใช้ลอง เมื่อกุญแจของระบบจริงพิสูจน์ตัวเองแล้ว
  • ตามอ่าน บันทึกการเปลี่ยนแปลง — ช่องใหม่ ๆ ลงที่นั่น และเป็นหน้าที่จะบอกคุณเมื่อของที่คุณพึ่งพากำลังจะเปลี่ยน