Skip to main content

กรอกฟอร์ม (Form Builder)

ภาพรวม

หน้ากรอกฟอร์มคือช่องทางให้ลูกค้ากรอกข้อมูลตามแบบฟอร์มที่แอดมินออกแบบไว้จากฝั่ง CMS (form builder) รองรับ field มากกว่า 20 ชนิด พร้อมความสามารถระดับสูงหลายอย่าง ได้แก่ เงื่อนไขการแสดงผลแบบ conditional logic, ธีมสีที่กำหนดเองต่อฟอร์ม, การบังคับล็อกอิน LINE, การจำกัดให้ส่งได้เพียงครั้งเดียวต่อผู้ใช้ และการยืนยันตัวตนด้วย OTP

สถาปัตยกรรมของหน้านี้แยกความรับผิดชอบชัดเจน ตัว page ทำหน้าที่โหลดข้อมูลฟอร์มและจัดการสถานะการเข้าถึง แล้วส่งโครงสร้างฟอร์มต่อให้ container ซึ่งเป็นผู้เรนเดอร์ field แต่ละตัวตามชนิดที่กำหนดไว้

Business Flow

  1. ผู้ใช้เข้าสู่ path /:hash/form/:id โดย id คือ hash ของฟอร์ม
  2. ระบบดึงโครงฟอร์มด้วย GET /form-builder/:hash ได้ข้อมูลครบชุดทั้งหัวข้อ คำอธิบาย รายการคำถาม ธีม การตั้งค่าบังคับล็อกอิน การจำกัดการส่งครั้งเดียว ข้อความหน้าขอบคุณ และการ mapping ข้อมูลโปรไฟล์
  3. resolve ธีมของฟอร์มแล้วติดตั้งเป็น CSS variable (เช่น --form-page และ --form-card) ลงในหน้า พร้อมตั้งชื่อ document title ตามชื่อฟอร์ม
  4. หากฟอร์มตั้งค่าให้บังคับล็อกอิน LINE ระบบจะบล็อกฟอร์มไว้จนกว่า LIFF จะพร้อมใช้งาน โดยเลือกใช้ formLiffId ก่อนและ fallback ไปที่ lineLiffId เมื่อพร้อมแล้วจึงดึง ID token เก็บไว้ใน state
  5. หากฟอร์มจำกัดให้ส่งได้ครั้งเดียว ระบบจะยิง POST /form-builder/:hash/is-submitted พร้อม LINE user ID เพื่อตรวจสอบ หากพบว่าเคยส่งแล้วจะ redirect ไปหน้าขอบคุณทันที ระหว่างการตรวจสอบจะแสดงหน้า loading ตลอดโดยไม่แสดงฟอร์มให้เห็น
  6. เมื่อหน้าโหลดเสร็จ ระบบจะบันทึกว่า "เปิดฟอร์มแล้วแต่ยังไม่ส่ง" ผ่าน POST /form-submission/:hash/form-submit-incompleted เพื่อให้ฝั่ง CMS นำไปทำรายงานอัตราการกรอกไม่สำเร็จ
  7. ระหว่างที่ผู้ใช้กรอก ระบบจะเก็บค่าที่เปลี่ยนแปลงไว้ใน state เพื่อประเมินเงื่อนไข conditional logic แบบเรียลไทม์
  8. เมื่อกดส่ง ระบบจะ normalize ค่าก่อนส่งจริง ตามกฎที่อธิบายในหัวข้อถัดไป
  9. หากฟอร์มเปิดใช้ OTP กระบวนการจะหยุดไว้ก่อนแล้วสลับไปหน้ายืนยัน OTP (ดู ยืนยันตัวตนด้วย OTP ในฟอร์ม)
  10. ส่งข้อมูลจริงด้วย POST /form-submission/:hash ในรูปแบบ FormData พร้อมแนบ header x-liff-token
  11. เมื่อสำเร็จ ระบบจะพาผู้ใช้ไปยังหน้าขอบคุณของฟอร์มนั้น
  12. path /:hash/form ที่ไม่ระบุ id ทำหน้าที่เป็น catch page คือรับค่า liff.state ที่ LINE ส่งกลับมาแล้ว redirect ไปยังฟอร์มที่ถูกต้อง โดยคง query parameter เดิมไว้

กฎการ normalize ค่าก่อนส่ง

ชนิด fieldการแปลงค่า
อัปโหลดไฟล์ส่ง path ของไฟล์ที่อัปโหลดสำเร็จแล้ว
วันที่จัดรูปแบบเป็น YYYY-MM-DD เพื่อป้องกันวันเลื่อนจาก timezone
วันเกิดรวมค่าวัน เดือน ปี ที่แยกกันให้เป็น YYYY-MM-DD
ตัวเลือกเดี่ยวและหลายตัวเลือกหากผู้ใช้เลือก "อื่น ๆ" ระบบจะแทนที่ด้วยข้อความที่ผู้ใช้พิมพ์เอง

หลังการแปลงค่า ระบบจะตัดคีย์ช่วยทั้งหมดที่ลงท้ายด้วย _other ออกก่อนส่ง

การจัดการข้อผิดพลาดกรณีพิเศษ

  • เมื่อ API ตอบ HTTP 400 พร้อมรหัสข้อผิดพลาดที่หมายถึงไม่พบโปรไฟล์หรือสิทธิ์ถูกใช้ไปแล้ว หน้าเว็บจะแสดงข้อความที่แอดมินตั้งค่าไว้สำหรับกรณีนั้น
  • เมื่อได้ข้อความว่าส่งครบจำนวนสูงสุดแล้ว ระบบจะพาผู้ใช้ไปหน้าขอบคุณแทนการแสดง error โดยเซิร์ฟเวอร์เป็นผู้ควบคุมกฎจริง ส่วนฝั่ง client ทำเพียงปรับ UX ให้เนียน

หน้าจอและองค์ประกอบหลัก

หน้าเพจ

  • หน้ากรอกฟอร์มหลัก (src/app/[hash]/form/[id]/page.tsx) รับผิดชอบการโหลดข้อมูล การตรวจสอบสิทธิ์ และการติดตั้งธีม
  • หน้า redirect (src/app/[hash]/form/page.tsx) ทำหน้าที่รับ liff.state และส่งต่อไปยังฟอร์มปลายทาง

Container และส่วนประกอบร่วม

  • FormBuilderContainer (src/components/form-builder/layout/form.container.tsx) เป็นแกนกลางที่จัดการการ submit การประกอบ FormData การเลือกเรนเดอร์ field ตามชนิด และการรับผลยืนยัน OTP
  • ส่วนหัวของฟอร์ม (form-header.tsx) แสดงภาพปก หัวข้อ และคำอธิบาย
  • มีสถานะ loading และ not found แยกเป็นคอมโพเนนต์เฉพาะภายในโฟลเดอร์เดียวกัน

ระบบธีม

  • โมดูล resolve ธีม (src/components/form-builder/theme/resolve-theme.ts) รับผิดชอบการหาค่าธีมที่ใช้จริง แปลงเป็นชุด CSS variable ตรวจสอบความถูกต้องของค่าสี และกำหนดธีมเริ่มต้นเมื่อไม่มีการตั้งค่า

คอมโพเนนต์ของ field

field ทั้งหมดอยู่ใน src/components/form-builder/ ครอบคลุมทั้งช่องข้อความบรรทัดเดียวและหลายบรรทัด ช่อง URL การยอมรับเงื่อนไข เส้นคั่น ช่องกำหนดเอง ตัวเลือกเดี่ยวและหลายตัวเลือก dropdown แบบเลือกเดี่ยวและเลือกหลายค่า ตัวเลือกวันที่ ตัวเลือกวันเกิด และการอัปโหลดไฟล์

Hook และ service

  • Hook เฉพาะของหน้านี้ (src/app/[hash]/form/[id]/hooks/) ประกอบด้วยการดึงโครงฟอร์ม การตรวจว่าเคยส่งแล้วหรือยัง และการบันทึกสถานะเปิดฟอร์มแต่ยังไม่ส่ง
  • ฟังก์ชันประเมิน conditional logic (src/utils/logic.utils.ts) รองรับเงื่อนไขหลายข้อทั้งแบบ AND และ OR
  • Service ที่ใช้คือ form builder service และ form submission service ส่วนนิยามชนิดข้อมูลและ enum ของชนิด field อยู่ใน src/service/types/ และ src/service/enums/

Endpoint ที่ใช้

MethodPathใช้ทำอะไร
GET/form-builder/:hashดึงโครงฟอร์ม
POST/form-builder/:hash/is-submittedตรวจว่าผู้ใช้เคยส่งแล้วหรือยัง
POST/form-submission/:hashส่งคำตอบ
POST/form-submission/:hash/form-submit-incompletedบันทึกว่ามีการเปิดฟอร์ม

จุดเชื่อมต่อกับฟีเจอร์อื่น

รายละเอียดฝั่ง Backend (Client API)

pipeline การส่งฟอร์มเป็น flow ที่ยาวและซับซ้อนที่สุดใน client-api ทั้ง service ประกอบด้วยการตรวจหลายชั้นที่เรียงลำดับอย่างจงใจ และ side effect หลายอย่างหลังบันทึกสำเร็จ หัวข้อนี้อธิบายว่าเกิดอะไรขึ้นจริงหลังจากหน้าเว็บกดส่ง

ตอนโหลดโครงฟอร์ม (GET /api/form-builder/:hash)

  • ค้นฟอร์มที่ active ด้วย hash — ไม่พบจะได้ 400 (ไม่ใช่ 404) พร้อมข้อความสั้นๆ ว่า Bad Request เท่านั้น (parity กับระบบเดิมที่ไม่ได้ใส่ข้อความ) ฝั่งเว็บจึงต้องตีความ 400 ของ endpoint นี้ว่า "ไม่พบฟอร์ม" ไม่ใช่ "ข้อมูลที่ส่งไปผิด"
  • รูปปกของธีม ถูกแปลงจาก storage path เป็น public URL โดย backend จะ เช็คก่อนว่าไฟล์นั้นมีอยู่จริง ถ้าไฟล์หายหรือไม่ได้ตั้งค่า storage จะเขียนค่าเป็น null — หน้าเว็บจึงไม่เคยได้ URL ที่ชี้ไปไฟล์ที่ไม่มีอยู่ และต้องเผื่อกรณีปกเป็น null ไว้เสมอ
  • การ merge กฎกลาง (common rule) เป็นสิ่งที่ทำให้ validation ฝั่งเว็บทำงานได้: แอดมินสร้าง "กฎกลาง" ไว้ใน CMS (เช่น รูปแบบเบอร์มือถือไทย, เลขบัตรประชาชน, รหัสพนักงาน) ซึ่งเก็บ regex, ความยาวต่ำสุด/สูงสุด และข้อความ error ทั้งภาษาไทยและอังกฤษ — เวลาโหลดฟอร์ม backend จะดึงกฎที่ active มา แนบเพิ่มเป็น key commonRule ไว้ในแต่ละคำถาม ที่ผูกกฎนั้น หน้าเว็บจึงมี regex พร้อมใช้ validate ฝั่ง client ได้เลย
  • กฎกลางถูกอ่าน สดจากฐานข้อมูลโดยไม่ผ่าน cache ในเส้นทางนี้โดยเจตนา — เพราะถ้าใช้ cache แอดมินแก้กฎแล้วฟอร์มจะยังใช้ของเก่า (endpoint GET /api/form-builder-rule ที่ดึงกฎแยกนั้นมี cache แต่ไม่ใช่เส้นทางที่ฟอร์มใช้)
  • backend รักษาลำดับ key ของ object ธีมและคำถามไว้ตามเดิม (ordered JSON) จึงเปรียบเทียบผลลัพธ์กับระบบเดิมได้ตรงตัว

ตอนเช็คว่าเคยส่งแล้วหรือยัง (POST /api/form-builder/:hash/is-submitted)

จุดนี้มีข้อสำคัญด้าน security ที่หน้าเว็บควรรู้:

  • backend ไม่เชื่อ lineUserId ที่ส่งมาใน body เลย — ค่านั้นถูกทิ้งทั้งหมด ผู้ใช้ถูกระบุจากการ verify x-liff-token เท่านั้น เหตุผลคือถ้าเชื่อ body ใครก็จะ probe ได้ว่า LINE user คนไหนส่งฟอร์มนี้ไปแล้ว (กลายเป็นเครื่องมือสอดส่องผู้ใช้รายคน)
  • ถ้า verify ไม่ผ่านหรือไม่มี token → คืน false ไม่ใช่ error ผู้ใช้จะเห็นฟอร์มเปล่าเสมือนยังไม่ส่ง แต่การกันส่งซ้ำจริงยังทำงานอยู่ที่ขั้นตอน submit
  • response เป็น boolean แบบ text ล้วน ("true"/"false") และตอบด้วย status 201 ไม่ใช่ 200 เพราะเป็น POST ตาม parity ของ framework เดิม
  • ยังมี endpoint POST /api/form-builder/:hash/form-submission ค้างอยู่ในระบบซึ่งเป็น stub ที่ไม่ทำอะไร (คืน hash กลับไปเฉยๆ) port ไว้เพื่อ parity ไม่ควรเรียกใช้

ตอนบันทึกร่าง (POST /api/form-submission/:hash/form-submit-incompleted)

  • ถ้าฟอร์มตั้งค่าบังคับล็อกอิน LINE แต่ verify token ไม่ผ่าน → 401 Authentication required for this form
  • ถ้ามีแถวร่างของผู้ใช้คนนี้อยู่แล้ว จะ คืนแถวเดิมไม่สร้างซ้ำ — ผู้ใช้เปิดฟอร์มซ้ำหลายครั้งจึงไม่ทำให้ตัวเลข funnel เพี้ยน

ลำดับการตรวจตอนส่งจริง (POST /api/form-submission/:hash)

ลำดับนี้สำคัญมาก เพราะแต่ละขั้นเป็นด่านของขั้นถัดไป — error ที่หน้าเว็บได้รับจะบอกว่าติดอยู่ขั้นไหน backend รับได้ทั้ง multipart, urlencoded และ JSON

  1. โหลดฟอร์ม → verify token → เช็คการบังคับล็อกอิน
  2. ลบ lineUserId ออกจาก body (ใช้ค่าจาก token เท่านั้น เหมือน endpoint ก่อนหน้า) และแยก otpRef ออกไปเพราะไม่ใช่คำตอบของคำถาม
  3. จับคู่แต่ละคำตอบกับชนิดคำถามตามนิยามฟอร์ม
  4. ตรวจ question id ที่ไม่รู้จัก — key ใดที่ไม่ตรงกับคำถามในฟอร์มจะทำให้ทั้ง request ล้มด้วย 400 พร้อมรายชื่อ id ที่ไม่ถูกต้อง
  5. validate คำตอบ ด้วย regex และความยาว (รายละเอียดด้านล่าง)
  6. profile mapping — จับคู่ผู้กรอกกับฐานข้อมูลลูกค้า
  7. ด่าน OTP — เฉพาะฟอร์มที่เปิด OTP
  8. ย้ายไฟล์แนบ จากที่พักชั่วคราวไปที่เก็บถาวร
  9. บันทึก พร้อมกฎกันส่งซ้ำ
  10. ทำ side effect หลายอย่าง

รูปแบบ error ที่ต่างจากปกติ (ต้องรู้เพื่อจับ error ให้ถูก)

pipeline นี้มี error 2 รูปแบบที่ต่างกัน:

  • error ทั่วไปมาในรูป envelope มาตรฐาน {statusCode, message, error}
  • แต่ error ของ validation, profile mapping และ OTP มาเป็น object ดิบๆ ไม่ใช่ envelope — เช่น {message: "Form validation failed", errors: [...], details: "..."} หรือ {code: "PROFILE_NOT_FOUND"} / {code: "RECORD_ALREADY_CLAIMED"} / {code: "OTP_REQUIRED"} ทั้งหมดมาพร้อม status 400
  • นี่เป็นพฤติกรรมที่ port มาจากระบบเดิม (เกิดจากการโยน exception ด้วย object) ฝั่งเว็บจึงต้องอ่าน code จาก body ตรงๆ ไม่ใช่จาก message ของ envelope

เครื่องยนต์ validation ฝั่ง server (ด่านจริง ไม่ใช่แค่ UI hint)

การ validate ฝั่งเว็บช่วยเรื่อง UX แต่ request ที่ทำมือข้ามด่านนั้นได้ ด่านจริงอยู่ที่ backend ซึ่งมีพฤติกรรมที่ควรรู้:

  • ตรวจเฉพาะคำตอบที่ส่งมาเท่านั้น — field ที่ตั้ง required ไว้แต่ ไม่ได้ส่ง key มาเลย จะไม่ถูกตรวจ (parity กับระบบเดิม) การบังคับกรอกจึงพึ่งฝั่งเว็บเป็นหลัก จุดนี้เป็นข้อจำกัดที่ควรรู้ ไม่ใช่ช่องโหว่ที่จะได้ error ให้เห็น
  • คำถามที่ไม่ required และค่าว่างจะถูกข้าม; ที่ required แต่ว่างจะได้ This field is required
  • commonRule.pattern มีความสำคัญเหนือกฎตามชนิด field — ถ้าคำถามผูกกฎกลางไว้ ระบบใช้ regex ของกฎกลางแทนกฎประจำชนิด พร้อมเช็คความยาวของกฎกลาง และเลือกข้อความ error ตามลำดับ ภาษาไทย → ภาษาอังกฤษ → ข้อความ default
  • กฎ default ตามชนิด (ใช้เมื่อไม่มีกฎกลาง): อีเมลและเบอร์โทร (ตัวเลข 10–15 หลัก), ตัวเลขล้วน, URL, เลขบัตรประชาชน 13 หลัก, วันที่รูปแบบ YYYY-MM-DD, ข้อความบรรทัดเดียว 1–255 ตัวอักษร, ข้อความยาว 1–5000 ตัวอักษร
  • วันเกิด ไม่ได้ตรวจแค่รูปแบบ แต่ คำนวณอายุแล้วเทียบกับอายุขั้นต่ำที่แอดมินตั้งไว้ ถ้าต่ำกว่าจะได้ข้อความบอกว่าต้องมีอายุอย่างน้อยกี่ปี
  • การยอมรับเงื่อนไข (terms) ต้องเป็น true จริงๆ ไม่งั้นได้ You must agree to the terms and conditions
  • field อัปโหลดไฟล์ผ่านการ validate เสมอ ในขั้นนี้ (การตรวจไฟล์อยู่ที่ขั้นตอนอัปโหลดแยก)
  • ชนิดตัวเลือกทั้งหมด (single choice, multiple choice, dropdown, multi-select) ยังปิดการตรวจไว้ — backend ไม่เช็คว่าค่าที่ส่งมาอยู่ในรายการตัวเลือกจริงหรือไม่ เป็น parity กับระบบเดิม จุดนี้ควรรู้ไว้เพราะค่าที่ไม่อยู่ในตัวเลือกจะถูกบันทึกได้
  • การนับความยาวนับเป็น UTF-16 code unit (เหมือน String.length ของ JavaScript) ไม่ใช่ byte — เพื่อให้ข้อความไทยผ่าน/ไม่ผ่านเหมือนเดิมทุกกรณี
  • ข้อสังเกตสำคัญ: regex บางแบบที่ JavaScript รองรับ (lookahead/lookbehind/backreference) Go compile ไม่ได้ ถ้าแอดมินตั้ง pattern แบบนั้น ผู้ใช้จะได้ข้อความ Invalid validation pattern configured for this field ซึ่งเป็น error ของการตั้งค่า ไม่ใช่ความผิดของผู้กรอก — เจอกรณีนี้ต้องไปแก้กฎที่ CMS ไม่ใช่บอกผู้ใช้กรอกใหม่

Profile mapping — เมื่อฟอร์มกลายเป็นฟอร์มยืนยันตัวตนสมาชิก

แอดมินสามารถผูกฟอร์มกับฐานข้อมูลลูกค้าหนึ่งชุด แล้วกำหนดคำถามชนิด "ตรวจกับฐานข้อมูล" ที่ผูกกับคอลัมน์ในฐานนั้น ผู้กรอกต้องตอบให้ตรงกับแถวใดแถวหนึ่ง ทุกข้อพร้อมกัน จึงถือว่า match (ยิงเป็น query เดียวที่บังคับให้ทุกเงื่อนไขอยู่ในแถวเดียวกัน)

  • ข้อสังเกตด้าน security: ชื่อคอลัมน์ที่ใช้ค้นหามาจาก การตั้งค่าของแอดมินเท่านั้น ไม่ใช่จาก body ของผู้ใช้ และทั้งชื่อ key และค่าถูกส่งเป็น query parameter ไม่มีการต่อสตริง — เป็นการกัน SQL/JSONB key injection โดยตรง
  • ค่าที่ส่งมาถูก trim ก่อนเทียบ และค่าว่างถูกข้าม
  • match ไม่ได้ จะแยกเป็น 2 ทางตามการตั้งค่า: ถ้าแอดมินเปิด "ให้คนใหม่สมัครได้" จะผ่านไปแบบ "ยังไม่ยืนยัน" ไม่ error; ถ้าไม่เปิดจะได้ 400 {code: "PROFILE_NOT_FOUND"} พร้อมข้อความที่แอดมินตั้งเอง (หรือค่า default)
  • match ได้แต่แถวนั้นถูกอ้างสิทธิ์ไปแล้ว: ถ้าแอดมินเปิด "หนึ่งบัญชีต่อหนึ่งระเบียน" ระบบจะค้นว่ามี submission ที่ส่งสำเร็จผูกกับแถวนี้อยู่แล้วหรือไม่ ถ้ามีและเป็นของ LINE บัญชีอื่น → 400 {code: "RECORD_ALREADY_CLAIMED"}
  • ฐานข้อมูลที่ถูกลบไปแล้วจะถือว่า match ไม่ได้ (เข้าเส้นทาง PROFILE_NOT_FOUND) ไม่ได้ error แยก
  • เมื่อ match สำเร็จและบันทึกเสร็จ ระบบจะ ติดแท็กให้ LINE user โดย merge ค่าลงใน custom attribute (คีย์ default คือ verified) — ถ้าแอดมินตั้งชื่อคีย์เป็นสตริงว่างโดยเจตนา ระบบจะไม่ติดแท็กเลย
  • matchedRowId ที่ match ได้ถูกเก็บไว้ใน metadata ของ submission และเป็นค่าที่ใช้กันการอ้างสิทธิ์ซ้ำในครั้งถัดไป

ด่าน OTP (สำหรับฟอร์มที่เปิดใช้)

  • backend ไม่เชื่อธง "ยืนยันแล้ว" ที่ฝั่ง client ส่งมาเลย ต้องมี session OTP ที่ verified อยู่จริงฝั่ง server
  • ยิ่งกว่านั้น matchedRowId ของ session ต้องตรงกับแถวที่ match ใหม่ตอน submit ไม่งั้นได้ 400 {code: "OTP_REQUIRED"} — ป้องกันการเอา OTP ที่ยืนยันไว้กับข้อมูลชุดหนึ่งไปใช้กับข้อมูลอีกชุด
  • ดูรายละเอียดที่ ยืนยันตัวตนด้วย OTP ในฟอร์ม

การย้ายไฟล์แนบและกฎกันส่งซ้ำ

  • คำตอบชนิดอัปโหลดไฟล์ที่ยังอยู่ในโฟลเดอร์ชั่วคราวจะถูก copy ไปยังที่เก็บถาวรของฟอร์มนั้น โดยชื่อไฟล์ถูกตรวจให้มีเฉพาะตัวอักษร ตัวเลข จุด ขีดล่างและขีดกลาง — path ที่ไม่ได้อยู่ในโฟลเดอร์ชั่วคราวจะถูกข้ามไปเงียบๆ (ไม่ error)
  • กฎกันส่งซ้ำ ทำงานตอนบันทึก: ถ้ามีแถวร่างอยู่แล้วจะ update แถวนั้น; ถ้าไม่มีจะนับจำนวนที่ส่งสำเร็จแล้ว และถ้าฟอร์มตั้งค่า "ส่งได้ครั้งเดียว" แล้วนับได้มากกว่า 0 → 400 พร้อมข้อความว่าส่งครบจำนวนสูงสุดแล้ว (ซึ่งหน้าเว็บแปลงเป็นการพาไปหน้าขอบคุณ)
  • หมายเหตุ parity: ตัวนับจำนวนคำตอบบนตัวฟอร์ม (responses_count) ไม่เคยถูกเพิ่มค่า — ตัวเลขนี้เชื่อถือไม่ได้ ถ้าต้องรู้จำนวนจริงต้องนับจากตาราง submission

Side effect หลังบันทึกสำเร็จ

  • แปลง guest เป็น member (ถ้าฟอร์มตั้งค่าไว้): publish งานสลับ rich menu สำหรับสมาชิก และข้อสำคัญคือถ้า publish งานนี้ล้มเหลว ทั้ง request จะล้มตามไปด้วย (ต่างจาก side effect อื่นที่กลืน error) พร้อมกับอัปเดตประเภทผู้ใช้เป็น member แบบ best-effort
  • สลับ rich menu ตาม action ของหน้าขอบคุณ (ถ้าตั้งไว้): publish งานแบบ best-effort ล้วน ล้มเหลวก็ไม่กระทบผลการส่งฟอร์ม
  • เขียนโปรไฟล์กลับไปที่ LINE user: คำตอบชนิดชื่อ นามสกุล อีเมล และเบอร์โทรถูกเขียนลงคอลัมน์ของผู้ใช้; ส่วน เลขบัตรประชาชนและวันเกิดถูกทิ้งโดยเจตนา เพราะไม่มีคอลัมน์รองรับ (ระบบเดิมก็ ignore เช่นกัน) — error ทั้งหมดในขั้นนี้ถูกกลืน
  • field attribute mapping: แอดมินกำหนดได้ว่าคำตอบข้อไหนไปลงคอลัมน์ใดของผู้ใช้ (จำกัดเฉพาะ allowlist คือชื่อที่แสดง ชื่อ นามสกุล อีเมล เบอร์มือถือ และภาษา) และ/หรือ merge ลง custom attribute โดยแปลงชนิดค่าตามที่ตั้งไว้ (ค่าที่แปลงเป็นตัวเลขไม่ได้จะถูกข้าม) — error ถูกกลืนทั้งหมด
  • ผลของการกลืน error เหล่านี้คือ ผู้ใช้จะเห็นว่าส่งฟอร์มสำเร็จเสมอ แม้การอัปเดตโปรไฟล์บางส่วนจะล้มเหลว ถ้าพบว่าข้อมูลผู้ใช้ไม่อัปเดตตามคาด ต้องไปดู log ฝั่ง backend ไม่ใช่หาจาก response
  • เมื่อสำเร็จ backend คืน body ที่มีคำตอบที่บันทึก ข้อมูลฟอร์ม และข้อความ Form submitted successfully ด้วย status 201