แดชบอร์ด
GET /v1/bo/dashboardคืนตัวเลขรวมของบัญชีคุณ — ยอดรวมที่ทำรายการได้ อัตราจับคู่สำเร็จ และอัตราสำเร็จโดยรวม
ตัวเลขบนแดชบอร์ดอ่านจากฐานข้อมูลสำรอง ห้ามใช้ตัดสินใจเรื่องเงิน
แดชบอร์ดถูกออกแบบให้ เร็ว ไม่ใช่ให้ เป๊ะวินาทีนั้น ตัวเลขที่ผูกพันทางการเงินมี 2 ที่เท่านั้น: GET /v1/pay/balance และชุดกระทบยอด 4 ตัวใน รายงานและการกระทบยอด
ตัวเลขที่ควรอยู่บนหน้าจอแรก
จัดเป็น 3 กลุ่ม — กลุ่มแรกคือเงิน กลุ่มที่สองคือคุณภาพของ integration กลุ่มที่สามคือของค้าง
กลุ่ม 1 — เงิน
| ตัวเลข | คำนวณจาก |
|---|---|
| ยอดพร้อมใช้ | available_minor |
| ยอดรอพ้นหน้าต่างข้อพิพาท | pending_clearing_minor |
| ยอดล็อกไว้ให้รายการถอน | reserved_minor |
| ยอดฝากที่รับเข้าสำเร็จวันนี้ | ผลรวมของ amount_received_minor ของใบที่ settled |
| ยอด settlement evidence ที่ธนาคารยืนยันวันนี้ | ผลรวมของ transferred_amount_minor · สูงกว่ายอดรับเข้าได้เมื่อ same-slip overpay; transfer เพิ่มหลัง settle อยู่ใน overpayment.amount_minor ไม่ถูกรวมซ้ำ |
| มูลค่าเคสยอดเกินที่ยังไม่จบ | ผลรวม Claim.amount_minor ของ overpayment claim สถานะ open, under_review, refund_pending และ return_pending_payout · เป็น operational exposure ไม่ใช่ยอด liability จาก ledger |
| ค่าธรรมเนียมวันนี้ | ผลรวมของ fee_total_minor |
| ยอดเข้ากระเป๋าวันนี้ | ผลรวมของ credited_amount_minor |
| ยอดจ่ายออกวันนี้ | ผลรวมของ filled_minor ของรายการถอน |
| ยอดคืนกลับวันนี้ | ผลรวมของ refunded_minor |
ยอดโอนจริง ยอดรับเข้า fee และยอดเข้ากระเป๋าต้องแสดงแยกกัน
amount_received_minor = min(transferred_amount_minor, ordered_amount_minor) และ amount_received_minor − fee_total_minor = credited_amount_minor ทุกใบ แดชบอร์ดที่แสดงแค่ตัวเดียวคือแดชบอร์ดที่ฝ่ายบัญชีเอาไปกระทบยอดไม่ได้ และเป็นต้นเหตุของการหยิบตัวเลขผิดไปเครดิตผู้ใช้
กลุ่ม 2 — คุณภาพของ integration
ตัวเลขชุดนี้บอกว่า UI ฝั่งคุณ มีปัญหาหรือไม่ ไม่ได้บอกว่าระบบเรามีปัญหา
| ตัวเลข | สูตร | เกณฑ์ |
|---|---|---|
| อัตราเข้าคิวตรวจสอบ | ใบที่เคยผ่าน under_review ÷ ใบทั้งหมด | ควร ต่ำกว่า 5% · เกิน 10% = ฟอร์มเลือกบัญชีผู้โอนมีปัญหา |
| สัดส่วนที่ใช้บัญชีที่เคยยืนยันแล้ว | ใบที่ส่ง sender_id ÷ ใบทั้งหมด | ยิ่งสูงยิ่งดี · ใบที่ใช้ sender_id เข้าคิวเกือบเป็นศูนย์ |
| อัตรารายการฝากสำเร็จ | ใบที่ settled ÷ ใบที่สร้างทั้งหมด | ดูคู่กับอัตราหมดอายุ |
| อัตราหมดอายุ | ใบที่ expired ÷ ใบทั้งหมด | สูงผิดปกติ = countdown ในหน้าเว็บไม่ตรง หรือ hardcode อยู่ |
| อัตรารับสลิปย้อนหลัง | ใบที่มี slip.is_late เป็น true | สูง = ผู้ใช้ปิดเบราว์เซอร์ก่อนอัปสลิปเยอะ |
| อัตราจับคู่สำเร็จของรายการถอน | ใบที่ settled ÷ ใบที่เข้าสู่การจับคู่ | ใบที่ expired โดย filled_minor มากกว่า 0 นับเป็นได้บางส่วน |
| อัตรา webhook ตรวจลายเซ็นไม่ผ่าน | นับจากฝั่งคุณเอง | 🔴 ต้องเป็น 0 ถ้าไม่ใช่ 0 คือมีคนยิงปลอมหรือคุณเก็บกุญแจไม่ทัน |
| อัตรา handler ตอบไม่ใช่ 2xx | นับจากฝั่งคุณเอง | เกิน 1% ใน 5 นาที ⇒ alert |
อัตราเข้าคิวตรวจสอบคือ KPI ที่สำคัญที่สุดของ integration
มันไม่ใช่ตัวเลข "คุณภาพของระบบเรา" แต่เป็นตัวเลข "ผู้ใช้กรอกบัญชีผู้โอนถูกหรือไม่" วิธีลดที่ได้ผลที่สุดคือให้ผู้ใช้ เลือก จาก GET /v1/pay/senders แทนการให้ พิมพ์ ระหว่างย้ายระบบ ถ้าอัตรานี้เกิน 10% ⇒ 🔴 อย่าเพิ่มสัดส่วน traffic จนกว่าจะแก้ฟอร์มก่อน
กลุ่ม 3 — ของค้างที่ต้องมีคนดู
| ตัวเลข | ทำไมต้องมี |
|---|---|
รายการถอนที่ค้าง pending_approval | เกิน 24 ชั่วโมงถูกยกเลิกอัตโนมัติ ⇒ alert ตั้งแต่ 4 ชั่วโมง |
รายการฝากที่ค้าง under_review เกิน 4 ชั่วโมง | ดู review.escalated ประกอบ |
| ข้อพิพาทที่ยังไม่ตอบ | เทียบกับ dispute.respond_by — ไม่ตอบ = ยอมรับผล |
| เรื่องร้องเรียนที่ยังเปิดอยู่ | เทียบกับ expected_response_at |
ช่องว่างของ sequence ที่ค้างเกิน 4 วัน | ช่องว่างสด ๆ ปกติ · ค้างเกิน 4 วัน = event หายจริง |
delivery ที่ failed ใน 24 ชั่วโมงล่าสุด | ถ้าไม่มี success เลยและล้มเกิน 50 ใบ ระบบจะปิด endpoint ให้อัตโนมัติ |
คำนวณเองให้ตรงกับ ledger
ตัวเลขบนแดชบอร์ดของคุณควรคำนวณจากชุดเดียวกับที่ใช้กระทบยอด ไม่ใช่จาก event ที่สะสมไว้เฉย ๆ
// ยอดรวมรายวัน — คำนวณจากชุดกระทบยอด ไม่ใช่จากตาราง event ที่อาจมาไม่ครบ
async function dailyTotals(dayIso) {
const from = `${dayIso}T00:00:00.000Z`
const to = new Date(Date.parse(from) + 86_400_000).toISOString().replace(/\.\d{3}Z$/, '.000Z')
const totals = {
deposit_count: 0,
transferred_amount_minor: 0,
amount_received_minor: 0,
fee_total_minor: 0,
credited_amount_minor: 0,
under_review_count: 0
}
let cursor = null
for (;;) {
const params = { limit: 200, status: 'settled', updated_after: from }
if (cursor) params.cursor = cursor
const body = await call('/v1/pay/deposits', params)
for (const d of body.data) {
if (d.settled_at < from || d.settled_at >= to) continue
const amounts = [d.ordered_amount_minor, d.transferred_amount_minor,
d.amount_received_minor, d.fee_total_minor, d.credited_amount_minor]
if (!amounts.every(Number.isSafeInteger) || d.ordered_amount_minor <= 0 ||
d.transferred_amount_minor <= 0 || d.amount_received_minor <= 0 ||
d.amount_received_minor !== Math.min(d.transferred_amount_minor, d.ordered_amount_minor) ||
d.fee_total_minor < 0 || d.fee_total_minor > d.amount_received_minor) {
throw new Error(`ยอดของ ${d.id} ผิด invariant X/V/A/fee`)
}
if (d.amount_received_minor - d.fee_total_minor !== d.credited_amount_minor) {
throw new Error(`ยอดของ ${d.id} ไม่เป็น A − fee = credited`)
}
totals.deposit_count += 1
totals.transferred_amount_minor += d.transferred_amount_minor
totals.amount_received_minor += d.amount_received_minor
totals.fee_total_minor += d.fee_total_minor
totals.credited_amount_minor += d.credited_amount_minor
if (d.settled_via === 'review') totals.under_review_count += 1
}
if (!body.has_more) break
cursor = body.next_cursor
}
// 🔴 ตรวจยอดรวมซ้ำหลังจากตรวจ A − fee = credited ทีละใบแล้ว
if (totals.amount_received_minor - totals.fee_total_minor !== totals.credited_amount_minor) {
throw new Error('ยอดรวมไม่ลงตัว — ดึงข้อมูลมาไม่ครบหรือมีใบที่ผิดปกติ')
}
if (totals.transferred_amount_minor < totals.amount_received_minor) throw new Error('ยอดโอนจริงต่ำกว่ายอดรับเข้า')
return totals
}
// backlog ปัจจุบันต้องอ่าน Claim; Deposit.overpayment เป็น immutable snapshot ตอนเปิด flag
async function unresolvedOverpaymentMinor() {
let total = 0
for (const status of ['open', 'under_review', 'refund_pending', 'return_pending_payout']) {
let cursor = null
for (;;) {
const params = { kind: 'overpayment', status }
if (cursor) params.cursor = cursor
const body = await call('/v1/bo/claims', params)
for (const claim of body.data) {
if (!Number.isSafeInteger(claim.amount_minor) || claim.amount_minor <= 0) {
throw new Error(`ยอด claim ${claim.claim_id} ไม่ถูกต้อง`)
}
total += claim.amount_minor
}
if (!body.has_more) break
cursor = body.next_cursor
}
}
return total
}ห้ามรวม Deposit.overpayment.amount_minor เพื่อหายอดค้างปัจจุบัน: flag นี้ไม่เปลี่ยนตามผลของ Claim และ deposit เดียวอาจมี claim จาก transfer เพิ่มได้มากกว่าหนึ่งใบ. ใช้ Claim เป็น source of truth เสมอ ตัวเลขนี้เป็นมูลค่าเคส ไม่ใช่ยอด OVERPAYMENT_REFUND_PAYABLE: ระหว่าง return_pending_payout original-transfer return อาจดับ liability แล้ว แต่ยังมี payout/recovery race ที่ต้องติดตาม
แสดงเวลาที่คำนวณเสมอ
ทุกตัวเลขบนจอควรมีบรรทัด "ข้อมูล ณ …" ที่มาจาก as_of ของ GET /v1/pay/balance หรือเวลาที่ job ของคุณรันเสร็จ แดชบอร์ดที่ไม่บอกเวลา คือแดชบอร์ดที่คนจะเถียงกันว่าใครถูกตอนตัวเลขไม่ตรง
ตัวเลขที่คุณจะไม่ได้ — และเหตุผล
| อยากเห็น | ได้ไหม | เพราะ |
|---|---|---|
| ปริมาณสภาพคล่องในระบบ / จำนวนผู้ถอนที่รออยู่ | ❌ | เป็นข้อมูลการแข่งขันของผู้ให้บริการชั้นบน · สิ่งที่คุณเห็นได้คือ liquidity.estimated_wait_ms เฉพาะออร์เดอร์ของคุณเอง |
| เวลาที่เจ้าหน้าที่ต้องตัดสินให้เสร็จ | ❌ | คิว support เป็นของชั้นบน · ส่งให้คุณคือ review.estimated_resolution_bucket |
| ค่าธรรมเนียมแยกตามว่าใครได้ส่วนแบ่ง | ❌ | fee_breakdown[] แยกตาม component (ประเภทค่าธรรมเนียม) เท่านั้น |
| ตัวตนของช่องทางสำรองที่ใช้ | ❌ | คำเดียวที่คุณเห็นคือ liquidity_source ที่มีค่าได้แค่ internal หรือ external_gateway |
| ข้อมูลของคู่กรณีอีกฝั่ง | ❌ | ระบบไม่เปิดเผยข้อมูลข้ามบัญชีหรือข้ามผู้ให้บริการ · id ของ fill ที่คุณเห็น (leg_ref) ทั้งสองฝั่งได้ค่าคนละตัวและ correlate ข้ามกันไม่ได้ |
liquidity_source ห้ามเอาไปตัดสินใจเรื่องเงิน
มันมีไว้ทำสถิติและตอบ support เท่านั้น เงื่อนไขการเครดิตมีข้อเดียวคือ status = 'settled'
เกณฑ์ alert ที่ควรมีตั้งแต่วันแรก
- อัตราเข้าคิวตรวจสอบเกินเกณฑ์ที่คุณตั้งไว้ (สัญญาณว่าฟอร์มผู้โอนมีปัญหา)
- ตรวจลายเซ็น webhook ไม่ผ่าน แม้แต่ครั้งเดียว
- handler ตอบไม่ใช่ 2xx เกิน 1% ใน 5 นาที
- job กระทบยอดรายวันออก exit code ไม่เป็น 0
- ช่องว่างของ
sequenceค้างเกิน 4 วัน - รายการถอนค้าง
pending_approvalเกิน 4 ชั่วโมง - ข้อพิพาทที่เหลือเวลาไม่ถึงครึ่งของหน้าต่างและยังไม่ตอบ
- ได้
deposit.reversedหรือbalance.debited(ควรแจ้งคนทันที ไม่ใช่แค่บันทึก)
ขั้นต่อไป
- รายงานและการกระทบยอด — ชุดข้อมูลที่ตัวเลขทั้งหมดในหน้านี้ควรคำนวณมาจาก
- กระเป๋าเงินและยอดคงเหลือ — 3 ช่องที่ต้องแสดงแยกกัน
- บันทึกและ audit log — ตัวเลขของ webhook มาจากที่นี่
- บัญชีผู้โอน (บังคับ) — วิธีลดอัตราเข้าคิวตรวจสอบ
- เช็กลิสต์ก่อน go-live
