Survey API และการฝังในใบเสร็จ
ฟีเจอร์ Survey ของ ReceiptRoller ให้ร้านค้ารวบรวมคำตอบผ่าน QR / ลิงก์ / การฝังในใบเสร็จดิจิทัล และแจกคูปองกลับได้ตามต้องการ บทความนี้ครอบคลุม consumer API สาธารณะ เพย์โหลดการฝังในใบเสร็จ และกฎการกำหนดกลุ่มเป้าหมาย (audience targeting) ที่ร้านค้าสามารถแนบเข้ากับแคมเปญได้
URL สาธารณะ
ทุกแคมเปญแบบสำรวจจะได้รับโทเคนแบบ URL-safe ความยาว 10 อักขระ หน้าแลนดิงสาธารณะคือ:
https://{host}/s/{token}
ค่า host มาจาก Survey:UniversalLinkHost ในไฟล์คอนฟิก หากไม่มีจะใช้ host ของ request แทน แอปเนทีฟจะเคลม /s/* ผ่าน Universal Link / App Link (มีการเพิ่มรายการลงใน apple-app-site-association และ assetlinks.json) ผู้ใช้ที่เปิดผ่านเบราว์เซอร์ในแอป LINE จะถูก 302 ไปยัง LIFF URL โดยต่อท้ายด้วย ?t={token} ส่วนผู้ใช้อื่น ๆ จะไปยังฟอร์มสำรองแบบ inline บนเว็บ
Consumer API
มีสามเอนด์พอยต์ ทั้งหมดอยู่ภายใต้ /api/v1/surveys เอนด์พอยต์ preview และ submit เป็น [AllowAnonymous] เพื่อให้ LIFF หรือฟอร์มสำรองบนเว็บเรียกได้โดยไม่ต้องมี bearer token เซิร์ฟเวอร์จะควบคุมแคมเปญที่ไม่อนุญาตผู้ไม่ระบุตัวตนด้วยสถานะ auth_required
GET /api/v1/surveys/campaign/{token}
คืนค่าเมทาดาทาของแคมเปญและรายการคำถามทั้งหมด ไคลเอนต์ใช้เพื่อเรนเดอร์ฟอร์มก่อนที่ผู้ใช้จะเริ่มตอบ
{
"status": "ok",
"campaign": {
"campaignId": "...",
"storeId": "...",
"title": "...",
"description": "...",
"validFrom": "2026-04-01T00:00:00Z",
"validUntil": "2026-05-31T23:59:59Z",
"isAnonymous": true,
"maxResponsesPerUser": 1,
"hasReward": true
},
"questions": [
{ "questionId": "...", "order": 0, "type": "single_choice",
"text": "...", "isRequired": true, "optionsJson": "[\"A\",\"B\"]" }
]
}
ค่า status ที่เป็นไปได้: ok, expired, inactive, unknown_token
POST /api/v1/surveys/responses
ส่งคำตอบ ผู้เรียกแบบไม่ระบุตัวตนต้องส่ง deviceId ที่คงที่ ส่วนแคมเปญที่ไม่อนุญาตผู้ไม่ระบุตัวตนต้องใช้ bearer token
POST /api/v1/surveys/responses
Content-Type: application/json
{
"token": "...",
"answers": [
{ "questionId": "...", "value": "A" },
{ "questionId": "...", "value": "5" },
{ "questionId": "...", "value": "OptionA,OptionC" }
],
"deviceId": "anon-..."
}
การตอบกลับใช้ตัวจำแนก status ที่ไคลเอนต์นำไปแตกเงื่อนไข:
| Status | ความหมาย |
|---|---|
recorded | บันทึกสำเร็จ ค่า couponIssuedId จะมีค่าเมื่อแคมเปญมีรางวัลและคูปองผ่านการตรวจสอบ |
response_limit | ผู้ใช้ส่งคำตอบครบ maxResponsesPerUser ครั้งแล้ว |
expired | อยู่นอกช่วงเวลาที่ใช้งานได้ |
inactive | แคมเปญถูกจัดเก็บ (archived) |
unknown_token | ไม่พบโทเคน |
validation_error | ฟิลด์ที่จำเป็นว่างหรือค่าอยู่นอกช่วง มี dict validationErrors โดยใช้ questionId เป็นคีย์ |
auth_required | แคมเปญต้องการผู้ใช้ที่ล็อกอิน ไคลเอนต์ควรเปิดหน้าล็อกอิน |
การเข้ารหัสคำตอบแบบเลือกได้หลายข้อ
สำหรับคำถามแบบ multi_choice ไคลเอนต์อาจส่งเป็นสตริงที่คั่นด้วยจุลภาค ("A,C") หรือสตริง JSON array ("[\"A\",\"C\"]") ก็ได้ ตัวรวบรวมข้อมูลการวิเคราะห์ (analytics aggregator) รับได้ทั้งสองแบบ
GET /api/v1/surveys/me
ต้องใช้ bearer เท่านั้น คืนค่าประวัติคำตอบของผู้เรียกเอง (response id, campaign id, store id, completed-at, coupon-issued id)
การฝังในใบเสร็จดิจิทัล
เมื่อร้านค้ามีแคมเปญที่กำลังทำงานอยู่โดยตั้ง showOnDigitalReceipt = true และกฎการกำหนดกลุ่มเป้าหมายตรงกับบริบทของใบเสร็จ transaction-receipt API จะแนบออบเจ็กต์ survey เข้าไปในเพย์โหลดของใบเสร็จ:
GET /api/v1/rx/{claimToken}
{
"claimToken": "...",
"storeName": "...",
"transaction": { ... },
"survey": {
"campaignId": "...",
"title": "...",
"description": "...",
"url": "https://{host}/s/{token}",
"hasReward": true
}
}
ฟิลด์ survey จะเป็น null เมื่อไม่มีแคมเปญที่ตรงกัน ไคลเอนต์เพียงซ่อนส่วนนั้นไว้ ความล้มเหลวในการค้นหาข้อมูลการฝังจะไม่ทำให้การตอบกลับใบเสร็จเสียหาย
กฎการกำหนดกลุ่มเป้าหมาย
แต่ละแคมเปญสามารถแนบเอกสาร JSON ที่กรองว่าใบเสร็จใดจะได้รับการฝัง เอกสารนี้เป็นรายการแบบ AND ที่ราบ (flat) — ใบเสร็จจะตรงก็ต่อเมื่อทุกกฎตรงกัน เอกสารที่ว่างเปล่า / ไม่มี = ตรงเสมอ
{
"rules": [
{ "field": "total_amount", "op": "gte", "value": 1000 },
{ "field": "item_count", "op": "gte", "value": 3 },
{ "field": "category", "op": "eq", "value": "drinks" },
{ "field": "product_name", "op": "contains", "value": "coffee" },
{ "field": "time_of_day", "op": "gte", "value": 18 },
{ "field": "day_of_week", "op": "eq", "value": "fri" },
{ "field": "customer_visit_count", "op": "gte", "value": 2 }
]
}
ฟิลด์
| Field | แหล่งที่มา | ชนิด |
|---|---|---|
total_amount | ยอดรวมของใบเสร็จ | decimal |
item_count | จำนวนรายการสินค้า | int |
category | หมวดหมู่ของรายการสินค้าใด ๆ | string |
product_name | ชื่อสินค้าของรายการใด ๆ | string |
time_of_day | ชั่วโมงของวันที่เกิดธุรกรรม (0-23) | int |
day_of_week | ตัวย่อภาษาอังกฤษ 3 ตัวอักษร (mon...sun) | string |
customer_visit_count | จำนวนครั้งการมาเยือนสะสมของผู้ใช้รายนี้ (โหลดแบบ lazy-loaded) | int |
ตัวดำเนินการ (Operators)
eq— ตรงกันแบบพอดี (สตริงหรือตัวเลข)gt/gte/lt/lte— การเปรียบเทียบเชิงตัวเลขcontains— สตริงย่อยแบบไม่สนใจตัวพิมพ์ (case-insensitive) (สำหรับฟิลด์สตริงและอาร์เรย์ของรายการสินค้า)
ความหมายแบบ fail-closed
JSON ที่ผิดรูปแบบ ฟิลด์ที่ไม่รู้จัก ตัวดำเนินการที่ไม่รู้จัก หรือค่าที่ขาดหาย จะทำให้กฎนั้น (และการจับคู่ทั้งหมด) ล้มเหลว นี่เป็นความตั้งใจ: ยอมไม่ฝังแบบสำรวจอย่างเงียบ ๆ ดีกว่าฝังลงในใบเสร็จที่ผิด
รางวัลคูปอง
การตั้ง RewardCouponId ให้กับแคมเปญจะทำให้ SubmitResponseAsync resolve คูปอง ณ เวลาที่ส่งคำตอบ คูปองต้องอยู่ในสถานะใช้งานได้และอยู่ในช่วงเวลาที่ใช้งานได้ของตนเอง หากผ่านเกณฑ์ แถวคำตอบจะถูกประทับด้วย coupon id และ DTO ผลลัพธ์จะคืนค่านั้นเป็น couponIssuedId คูปองใน ReceiptRoller เป็นเทมเพลตระดับร้านค้า — ไม่มีโมเดลการมอบสิทธิ์แบบต่อผู้ใช้ ไคลเอนต์ฝั่งผู้บริโภคจะถือว่า id ที่คืนมาเป็นรหัสที่นำไปแลกได้
การกันตอบซ้ำต่อผู้ใช้
แต่ละแคมเปญมีการตั้งค่า maxResponsesPerUser (ค่าเริ่มต้น 1) เซอร์วิสบังคับใช้เพดานนี้โดยนับคำตอบที่อ้างอิงกับ userId (การส่งแบบล็อกอิน) หรือ deviceId (การส่งแบบไม่ระบุตัวตน) ฟอร์มสำรองบนเว็บจะสร้าง device id ที่คงที่ใน localStorage ส่วนไคลเอนต์ LIFF / เนทีฟควรส่งของตนเองมา
Universal Link / App Link
พาธ /s/* ถูกรวมอยู่ใน apple-app-site-association และ assetlinks.json ควบคู่กับพาธที่มีอยู่แล้ว ได้แก่ terminal QR (/r/*), transaction QR (/rx/*) และ check-in (/c/*) แอปเนทีฟที่เคลมพาธเหล่านั้นจะได้รับ survey URL โดยตรงจาก OS และสามารถเรียก consumer API ได้โดยไม่ต้องผ่านหน้าแลนดิงบนเว็บ
เอกสารอ้างอิงเชิงวิศวกรรม
สเปกการออกแบบฉบับเต็ม รวมถึงรายการ PR แบบเฟสต่อเฟสและสิ่งที่เลื่อนไปเวอร์ชัน v2 อยู่บน engineering wiki: Engineering / Survey Feature — Design Spec