# Payment / SlipOK diagnostic — 7 ตุลาคม 2026

## ROOT CAUSE

รายการล่าสุด `pay_id=22`, `bid_id=56` ถูก SlipOK ปฏิเสธด้วย **HTTP 400 / provider code 1011** ไม่ใช่การตรวจสอบสำเร็จที่ Flutter map ผิด

ตาม [เอกสาร SlipOK](https://slipok.com/api-documentation/error-status-code/) รหัส 1011 หมายถึง **QR Code หมดอายุ หรือไม่มีรายการธุรกรรมอยู่จริง** รหัสนี้ไม่แยกสองสาเหตุย่อย จึงยังยืนยันไม่ได้ว่าเป็น QR หมดอายุหรือธนาคารไม่พบรายการ

Backend เดิมรวม 1005/1006/1007/1008/1011 เป็น `สลิปไม่ถูกต้อง กรุณาตรวจสอบรูปสลิป` จึงทำให้ผู้ใช้ไม่เห็นเหตุผลเฉพาะ Flutter อ่าน `failureReason` ถูกต้องแล้ว

หลักฐานจริงที่ตรวจ:

- Log ของ request ล่าสุดจาก Terminal ที่ผู้ใช้ส่งให้ หลังพบว่า Backend ปัจจุบันเป็นคนละ process กับ log เก่าในโปรเจกต์
- อ่านรายการชำระเงินจริงแบบ SELECT: amount=2.00, bid.price=2.00, status=verification_failed, provider=slipok, verification_attempts=1
- ไฟล์ที่ Backend เก็บเป็น JPEG 984×1426, 162,311 bytes เปิดอ่านได้
- SHA-256 ของ bytes ในไฟล์ตรงกับ evidence_sha256 ของรายการ
- อ่าน QR จากไฟล์ในเครื่องได้ 1 รายการ ไม่แสดง QR payload หรือข้อมูลส่วนตัว และผลนี้ไม่ได้พิสูจน์ความถูกต้องของธุรกรรม

## SLIPOK RESPONSE

Metadata ของ request ล่าสุด:

```text
HTTP 400
code=1011
rootSuccess=false
dataSuccess=false
receiverObject=false
hasReceiverAccount=false
hasReceiverProxy=false
hasDisplayName=false
hasReceiverName=false
hasTransRef=false
amountPresent=false
receivingBankPresent=false
```

ไม่มีการเก็บ raw provider response ไว้ในระบบเดิม รายงานนี้ใช้ metadata ของ request ล่าสุดที่ผู้ใช้ส่งให้ ไม่ใช่ response จากการส่งสลิปซ้ำ

เส้นทางที่ trace จาก source:

1. Flutter `PaymentPage._submit`: อ่าน XFile เป็น bytes แล้ว Base64 encode
2. `GroupService.submitPayment`: POST `/api/payments/submit`, JSON `bidId`, `method`, `evidenceBase64`, `evidenceMimeType` พร้อม Bearer session
3. Backend ตรวจ Base64, file signature, MIME, ขนาด ≤5 MiB และ duplicate evidence SHA ก่อนเก็บ bytes
4. Flutter โหลดสถานะแล้ว POST `/api/payments/verify` พร้อม `bidId` และ session
5. Backend ตรวจสิทธิ์และ payable bid อ่าน bytes จากไฟล์ และเทียบ amount ใน payments กับ bid.price
6. SlipOK adapter ส่ง POST HTTPS ไป `/api/line/apikey/<configured branch>` แบบ multipart: `files` image/jpeg, `log=true`, `amount=2.00`; API key อยู่เฉพาะ header ฝั่ง Backend
7. Provider ตอบ 400/1011 → adapter คืน invalid decision → Backend บันทึก verification_failed และส่ง HTTP 200, `ok=false`, `payment.failureReason`
8. Flutter รับ PaymentRecord และแสดง failureReason; ไม่แปลงเป็น paid

## EXPECTED VS ACTUAL

| รายการ | Expected / สิ่งที่ยืนยันได้ | Actual จาก request ล่าสุด |
|---|---|---|
| Amount | payments.amount และ bid.price = 2.00; adapter serialize multipart เป็น `2.00` | SlipOK ไม่คืน amount จึงเทียบยอดจาก provider ไม่ได้ |
| Bytes | JPEG 162,311 bytes | ไฟล์เปิดอ่านได้และ hash ตรงกับตอน upload |
| Receiver ใน QR กับ Backend config | launcher config ให้ทั้งสองค่า normalized ตรงกัน: `•••••••••••4816`; mode=live, SlipOK configured | response ไม่มี receiver และไม่มีผลยืนยันผู้รับ |
| Receiver ที่ผูกกับ SlipOK branch | ใช้ `log=true` ให้ SlipOK ตรวจบัญชีที่ผูกไว้ตาม [สัญญา API](https://slipok.com/api-documentation/check-slip/) | ไม่มีข้อมูลบัญชี branch จาก response นี้ จึงยังเทียบกับ QR โดยตรงไม่ได้ |
| Provider duplicate | ต้องปฏิเสธ 1012 | ได้ 1011 ไม่ใช่ 1012 |
| Local duplicate evidence | SHA ต้องไม่ถูกใช้กับรายการอื่น | จำนวน payments ที่มี SHA เดียวกัน = 1 คือรายการนี้ |
| Transaction reference | ต้องมี reference และไม่ซ้ำ | provider ไม่คืน transRef; ไม่มี reference ให้ตรวจ duplicate ต่อ |

`2.00` ใน request ยืนยันจากข้อมูลจริงใน DB และ serialization ของ source พร้อม regression test ตรวจ multipart; log รุ่นเดิมไม่ได้บันทึกยอดใน request จึงไม่มี wire capture ย้อนหลังของ field นี้

การปฏิเสธเกิดที่ provider code ก่อน Backend ตรวจ amount/receiver/reference/date จากผลสำเร็จ จึงไม่อ้างว่าการตรวจเหล่านั้นผ่านแล้ว และไม่จัดรายการนี้เป็น amount mismatch, receiver mismatch, duplicate หรือ auth error โดยไม่มีหลักฐาน

## FIX

แก้เฉพาะข้อความใน adapter และเพิ่ม metadata ที่จำเป็นสำหรับติดตาม request ครั้งถัดไป:

| Code | ข้อความใหม่ |
|---|---|
| 1005 | รูปแบบไฟล์สลิปไม่รองรับ กรุณาใช้ JPG, PNG หรือ WebP |
| 1006 | ไม่สามารถอ่านข้อมูลจากสลิปได้ กรุณาใช้ภาพสลิปที่ชัดเจน |
| 1007 | ไม่พบ QR ตรวจสอบในภาพสลิป กรุณาใช้ภาพสลิปเต็มและชัดเจน |
| 1008 | QR ในภาพไม่ใช่ QR ตรวจสอบการชำระเงิน กรุณาใช้สลิปจากธนาคาร |
| 1011 | QR ตรวจสอบสลิปหมดอายุหรือไม่พบรายการธุรกรรม กรุณาตรวจสอบสลิป |
| 1012 | สลิปนี้ถูกใช้ตรวจสอบไปแล้ว |
| 1013 | ยอดเงินในสลิปไม่ตรงกับยอดที่ต้องชำระ |
| 1014 | บัญชีผู้รับในสลิปไม่ตรงกับบัญชีของระบบ |

Provider unavailable ยังคงเป็น 503 พร้อมข้อความปลอดภัย ไม่ถูกนับเป็น invalid slip

Log เดิมเพิ่มเฉพาะ timestamp UTC, expectedAmount, actualAmount ถ้ามี, image byte count และ log=true ไม่มี API key, branch ID, account/proxy identifier, QR payload, transRef หรือชื่อบุคคล และไม่ log raw response เครื่องมือตรวจ config ชั่วคราวถูกลบแล้ว

### FILES CHANGED

- [server/slipok_verifier.dart](C:/Users/Admin/Downloads/TourAppProject/travelin_app/server/slipok_verifier.dart): แยกข้อความตาม provider code และเพิ่ม safe metadata
- [test/server/slipok_verifier_test.dart](C:/Users/Admin/Downloads/TourAppProject/travelin_app/test/server/slipok_verifier_test.dart): ข้อความแต่ละ code, bytes multipart ไม่เปลี่ยน, amount=2.00/log=true และ 1011 ต้องปฏิเสธแม้ response มี success fields
- [test/group_service_test.dart](C:/Users/Admin/Downloads/TourAppProject/travelin_app/test/group_service_test.dart): HTTP 200/ok=false ส่ง failureReason ผ่าน PaymentRecord ได้
- [test/payment_page_test.dart](C:/Users/Admin/Downloads/TourAppProject/travelin_app/test/payment_page_test.dart): เลือกรูป/ส่งหลักฐานและแสดงข้อความ 1011, duplicate, amount, receiver, unreadable และ unavailable โดยไม่แสดง payment success

ไม่ได้แก้ Flutter production source, Backend payment endpoint, QR generation, amount/receiver/transRef/date validation, duplicate SHA/reference protection หรือ verifier selection ไม่มี schema migration และไม่มีการแก้สถานะ paid ด้วยมือ

### TESTS

```text
flutter analyze — No issues found
dart analyze server — No issues found
flutter test test/server/slipok_verifier_test.dart test/server/slip_verification_service_test.dart test/server/slipok_configuration_test.dart test/server/payment_verification_state_test.dart test/server/promptpay_qr_test.dart test/group_service_test.dart test/payment_page_test.dart --reporter expanded
99 passed
```

Test fixtures ใช้เฉพาะ offline unit/widget tests ไม่ถูกติดตั้งแทน Live verifier และไม่ได้ส่งรูป test ให้ SlipOK จริง

### RESTART

Restart เฉพาะ Backend สำเร็จ: process ใหม่ PID 25756, `/api/health` ok=true, `/api/payments/verify` ไม่มี session ได้ 401 ตามเดิม โหมด Live โหลด SlipOK configured ไม่มีการ restart MariaDB, Emulator หรือ Flutter

Session เดิมอยู่ใน memory จึงต้องเข้าสู่ระบบใหม่ Backend ใหม่เก็บ log ที่ [work/payment_backend.stderr.log](C:/Users/Admin/Downloads/TourAppProject/travelin_app/work/payment_backend.stderr.log)

หลังตรวจและ restart: pay_id=22 ยังเป็น verification_failed, attempts=1; checksum ตาราง payments คงเดิม `641686647` ไม่ได้ส่งสลิปนี้ให้ provider ซ้ำ ไม่ได้โอนเงิน หรือเขียนแก้ payments ระหว่างการตรวจ

## RETEST

- **อย่าโอนใหม่ทันทีเพียงเพราะข้อความ error** ตรวจประวัติการโอนและการรับเงินของรายการเดิมก่อน เพราะ 1011 เพียงอย่างเดียวบอกไม่ได้ว่าเงินถูกหักหรือผู้รับได้เงินแล้วหรือไม่
- **ส่งไฟล์เดิมซ้ำในแอปจะไม่ได้ตรวจ SlipOK ใหม่**: ระบบพบ evidence SHA เดิมสำหรับ payment เดิมและคืนผลที่บันทึกไว้ รายการล่าสุดจึงยังมีข้อความ generic รุ่นเก่าอยู่ ไม่ได้ล้างผลเดิมหรือแก้ประวัติเพื่อบังคับ retry
- ถ้ามีรายการโอนจริง ให้ใช้สลิปต้นฉบับที่ธนาคารออกให้สำหรับรายการนั้นและตรวจว่ามี QR สำหรับตรวจสอบที่ยังใช้งานได้ หรือติดต่อธนาคาร/ผู้ดูแลเพื่อยืนยันรายการเดิมก่อนตัดสินใจโอนอีกครั้ง
- ถ้าได้ 1012 ต้องไม่ reuse สลิป ไม่แก้ภาพหรือ QR เพื่อหลบ duplicate; amount และ receiver ยังคงต้องตรงตามระบบ
- ข้อความใหม่จะปรากฏในการตรวจ request ใหม่ หลังเข้าสู่ระบบใหม่ โดยไม่มีการ bypass SlipOK
