FX Requestor T&C — Multi-Document Consent (Frontend API Spec)
สัญญา API ระหว่าง Frontend ↔ UserService ↔ Centralized สำหรับ T&C แบบหลายเอกสาร (multi-document) ของ FX Requestor: fetch N เอกสารจาก Centralized, render พร้อม consent checkbox, submit เป็น array — พร้อม field mapping, error codes, TypeScript models
อัปเดต: 2026-07-02
เอกสารส่งมอบให้ทีม Frontend สำหรับ implement ขั้นตอน T&C (ข้อกำหนดและเงื่อนไข) ของ FX Requestor ที่เปลี่ยนจาก single-document → multi-document consent
FE ทำงานกับ 2 endpoint เท่านั้น: (1) ดึง T&C จาก Centralized เพื่อ render, (2) ส่ง consent กลับไปที่ UserService. batch endpoint ระหว่าง 2 service เป็น server-to-server — FE ไม่เรียกเอง
ภาพรวม flow
┌─────────────┐ 1. GET /terms/current ┌──────────────┐
│ │ ──────────────────────────────→ │ Centralized │
│ Frontend │ ←── noticeList[] (N เอกสาร) ──── │ (T&C มาสเตอร์)│
│ (FX apply) │ └──────────────┘
│ │ 2. POST submit (documents[]) ┌──────────────┐
│ │ ──────────────────────────────→ │ UserService │
│ │ ←── nav / errorCode ──────────── │ (onboarding)│
└─────────────┘ └──────┬───────┘
3. batch validate (server-to-server) │
POST .../terms/notices/batch ────┘→ Centralized
(FE ไม่เกี่ยวกับ step นี้)
- FE เรียก
/terms/current→ ได้ หลายเอกสาร (noticeList[]) แต่ละเอกสารมี T&C content + รายการ consent (purpose) - FE render ทุกเอกสาร + checkbox ต่อ purpose → เก็บสถานะ agreed
- กด “ส่งใบสมัคร” → FE POST array
documents[]ไปที่ step-action ของ UserService - UserService ตรวจ consent กับ Centralized เอง (batch) แล้วตอบ nav (สำเร็จ) หรือ
errorCode(validation ไม่ผ่าน)
Endpoint 1 — ดึง T&C (Centralized)
GET {gateway}/centralized-api/api/centralized-service/v1/applications/{applicationCode}/terms/current?languageCode=th-TH
| ส่วน | ค่า |
|---|---|
{gateway} (dev) | https://gateway-dev.exim.go.th |
{applicationCode} | fx (FX Requestor flow) |
languageCode | th-TH (ปัจจุบันรองรับ th-TH เท่านั้น) |
| Auth | AllowAnonymous — เป็น public T&C content ไม่ผูก user |
Response — { data, meta } envelope
{
"data": {
"applicationCode": "fx",
"noticeList": [
{
"documentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"noticeId": "8c1d0f2a-1111-2222-3333-444455556666",
"noticeName": "ข้อกำหนดการใช้บริการ FX Online",
"version": "v3.0",
"sections": [
{
"name": "ข้อกำหนดการใช้บริการ FX Online",
"description": "ข้อกำหนดการใช้บริการ FX Online",
"content": "<p>เนื้อหา T&C (อาจเป็น HTML)…</p>",
"sectionType": "GENERAL",
"order": 1
}
],
"purposeList": [
{
"purposeId": "TC-0000001-MAIN",
"purposeName": "ยอมรับข้อกำหนดและเงื่อนไขการใช้บริการ",
"description": "…",
"require": true,
"requireScroll": false
}
]
}
]
},
"meta": { "traceId": "…", "timestamp": "2026-07-02T…", "path": "…" }
}
แต่ละ element ใน data.noticeList[] = หนึ่งเอกสาร (1 กล่อง T&C ที่ต้อง render):
| field | ชนิด | ความหมาย / วิธีใช้ |
|---|---|---|
documentId | Guid | คีย์หลักของเอกสาร — ต้องส่งกลับตอน submit (field ใหม่ round นี้) |
noticeId | Guid (string) | = TermsVersionId ภายใน — ใช้เป็น React/Angular list key ได้ แต่ห้ามส่งกลับตอน submit (ดู warning ด้านล่าง) |
noticeName | string | ชื่อเอกสาร (หัวข้อกล่อง) |
version | string | เช่น "v3.0" — ต้องส่งกลับตอน submit คู่กับ documentId |
sections[] | array | เนื้อหา T&C — render content (อาจเป็น HTML), เรียงตาม order |
purposeList[] | array | รายการ consent (checkbox) ของเอกสารนี้ |
purposeList[].purposeId | string | เช่น TC-0000001-MAIN — ค่านี้แหละที่ต้องส่งเป็น consentCode ตอน submit |
purposeList[].purposeName | string | label ของ checkbox |
purposeList[].require | bool | true = บังคับติ๊ก ถึงจะ submit ผ่าน |
purposeList[].requireScroll | bool | จับมาจาก contract จริง — ยังไม่ enforce scroll-to-enable round นี้ (FE จะ implement UX นี้หรือไม่ก็ได้) |
Endpoint 2 — ส่ง consent (UserService)
T&C เป็น step สุดท้าย ของ NEW_REQUESTOR flow — กด “ส่งใบสมัคร” = ยิง step-action Submit ที่ step นี้
POST {gateway}/userservice-api/api/user-service/v1/onboarding/instances/{instanceId}/steps/FxRequestorTcConsentStep/actions/Submit
| ส่วน | ค่า |
|---|---|
{instanceId} | Guid ของ FlowInstance (ได้จาก onboarding flow ที่รันอยู่) |
| stepType | FxRequestorTcConsentStep (ค่า literal ตายตัว) |
| actionCode | Submit |
| Auth | AllowAnonymous + edit-session cookie (onboarding window เดิม — ต้องส่ง credentials/cookie) |
Request body — ห่อด้วย stepData
{
"stepData": {
"documents": [
{
"documentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"version": "v3.0",
"lang": "th-TH",
"consents": [
{ "consentCode": "TC-0000001-MAIN", "agreed": true }
]
}
]
}
}
- หนึ่ง element ใน
documents[]ต่อหนึ่ง notice ที่ได้จาก/terms/current consents[]= ทุกpurposeในเอกสารนั้น (ทั้งที่ติ๊กและไม่ติ๊ก) —consentCode=purposeId,agreed= สถานะ checkboxlang= languageCode ที่ใช้ตอน fetch (th-TH)- ห้ามส่ง
documentIdซ้ำ ใน array เดียว (จะโดนStep.TcDuplicateDocument)
Response สำเร็จ — { data, meta }
data = StepNavigationV2 (nav ไปหน้าถัดไป/สถานะ Submitted) + reportAccessToken (สำหรับดู application report แบบ anonymous หลัง submit — ดู FX Online — Report Access Token)
Field mapping — สร้าง submit payload จาก /terms/current
หัวใจของการ integrate — ทุก field ใน body มาจาก response ของ endpoint 1:
submit documents[] | ← มาจาก /terms/current |
|---|---|
documentId | noticeList[].documentId |
version | noticeList[].version |
lang | languageCode ที่ request (th-TH) |
consents[].consentCode | noticeList[].purposeList[].purposeId |
consents[].agreed | สถานะ checkbox ที่ผู้ใช้ติ๊ก |
Error handling
Validation ที่ไม่ผ่านทุกกรณี → HTTP 400 + envelope RFC 9457 ProblemDetails (ไม่ใช่ envelope { data, meta }):
{
"type": "/errors/validation",
"title": "Validation Error",
"status": 400,
"detail": "กรุณายอมรับข้อกำหนดที่จำเป็น: (3fa85f64-…, TC-0000001-MAIN)",
"instance": "/api/user-service/v1/onboarding/instances/…/steps/FxRequestorTcConsentStep/actions/Submit",
"errorCode": "Step.TcRequiredConsentMissing",
"traceId": "…",
"timestamp": "2026-07-02T…"
}
- ตัดสินใจด้วย
errorCode(string คงที่) — ไม่ใช่title/detail - แสดง
detailเป็นข้อความ error ให้ผู้ใช้ (เป็นภาษาไทยพร้อมแสดง; ใน Angular =err.error.detail)
errorCode | เกิดเมื่อ | FE ควรทำ |
|---|---|---|
Step.TcRequiredConsentMissing | มี consent ที่ require=true แต่ agreed=false | ชี้ checkbox ที่ยังไม่ติ๊ก (detail list (documentId, consentCode)) |
Step.TcRequiredDocumentMissing | ส่งไม่ครบเอกสารที่ Centralized กำหนด ณ ตอนนี้ | re-fetch /terms/current แล้ว render/submit ใหม่ (ดู TOCTOU ด้านล่าง) |
Step.TcVersionNotFound | (documentId, version) ที่ส่งไม่มีใน Centralized | re-fetch /terms/current (version ถูก unpublish/เปลี่ยน) |
Step.TcDuplicateDocument | มี documentId ซ้ำใน documents[] | bug ฝั่ง FE — อย่าส่ง documentId ซ้ำ |
Step.TcServiceUnavailable | UserService เรียก Centralized ไม่ได้ | ให้ผู้ใช้ ลองใหม่ (ปัญหาชั่วคราว, ไม่ใช่ข้อมูลผิด) |
Step.DependencyMissing | step ก่อนหน้ายังไม่ครบ | ไม่ควรเกิดถ้าเดิน flow ปกติ — กลับไปทำ step ที่ขาด |
ลำดับการตรวจ (backend)
Backend ตรวจตามลำดับนี้ — คืน error ตัวแรก ที่ fail:
1. DependencyMissing (step ก่อนหน้าครบ?)
2. TcDuplicateDocument (documentId ซ้ำ?)
3. TcServiceUnavailable ── เรียก Centralized: เอกสารที่ต้องมีตอนนี้
4. TcRequiredDocumentMissing (ส่งครบทุกเอกสารที่ต้องมี?)
5. TcServiceUnavailable ── เรียก Centralized: batch validate เนื้อหา
6. TcVersionNotFound (ทุก documentId+version มีจริง?)
7. TcRequiredConsentMissing (consent required ติ๊กครบ?)
→ OK
TypeScript models (พร้อมใช้)
interface เหล่านี้ตรงกับ contract จริง (ยกจาก reference FE):
// ── GET /terms/current → data.noticeList[] ──
export interface TermsPurpose {
purposeId: string; // → ใช้เป็น consentCode ตอน submit
purposeName: string; // label ของ checkbox
description: string;
require: boolean; // true = บังคับติ๊ก
requireScroll: boolean; // ยังไม่ enforce round นี้
}
export interface TermsSection {
name: string;
description: string;
content: string; // อาจเป็น HTML
sectionType: string;
order: number;
}
export interface TermsNotice {
documentId: string; // → ส่งกลับตอน submit
noticeId: string; // UI key เท่านั้น (ห้ามส่งกลับ)
noticeName: string;
version: string; // → ส่งกลับตอน submit
sections: TermsSection[];
purposeList: TermsPurpose[];
}
// ── POST submit → stepData.documents[] ──
export interface FxRequestorTcConsentItem {
consentCode: string; // = TermsPurpose.purposeId
agreed: boolean;
}
export interface FxRequestorTcConsentDocument {
documentId: string; // = TermsNotice.documentId
version: string; // = TermsNotice.version
lang: string; // 'th-TH'
consents: FxRequestorTcConsentItem[];
}
export interface FxRequestorTcConsentData {
documents: FxRequestorTcConsentDocument[];
}
ตัวอย่าง mapping (จาก notices → submit payload):
const documents: FxRequestorTcConsentDocument[] = notices.map(n => ({
documentId: n.documentId,
version: n.version,
lang: 'th-TH',
consents: n.purposeList.map(p => ({
consentCode: p.purposeId,
agreed: !!agreedByPurpose[p.purposeId], // สถานะ checkbox
})),
}));
// ส่ง: POST .../actions/Submit body = { stepData: { documents } }
v1 → v2 — สิ่งที่ FE ต้องเปลี่ยน
ถ้า FE เดิมยังส่งแบบ single-document (round ก่อน) — payload เปลี่ยนเป็น array และเพิ่มการอ่าน documentId:
| v1 (เดิม) | v2 (ปัจจุบัน) | |
|---|---|---|
| submit body | { stepData: { tcVersion, consents: [...] } } | { stepData: { documents: [{ documentId, version, lang, consents }] } } |
อ่านจาก /terms/current | version + purposeList | + documentId (field ใหม่) |
| จำนวนเอกสาร | 1 (แบน) | N (array) |
สิ่งที่ต้องแก้: (1) อ่าน documentId จาก noticeList[], (2) เปลี่ยน submit จาก object เดี่ยว → documents[], (3) handle error codes ชุดใหม่ (5 ตัว Step.Tc*)
Context: batch endpoint (FE ไม่เรียก)
เพื่อความเข้าใจว่าทำไมต้องมี documentId: หลัง FE submit, UserService เรียก Centralized เองแบบ server-to-server เพื่อ verify ว่าเนื้อหา/consent ที่ส่งมา match กับ master จริง —
POST {gateway}/centralized-api/api/centralized-service/v1/applications/terms/notices/batch
body: { items: [{ documentId, version, languageCode }] }
endpoint นี้คีย์ด้วย documentId → เป็นเหตุผลที่ documentId ต้องมีใน /terms/current เพื่อให้ FE ส่งกลับมาได้. FE ไม่ต้องเรียก batch endpoint นี้เอง — เป็นหน้าที่ของ UserService
Edge cases
noticeListว่าง (0 เอกสาร) — ไม่มี T&C ต้องยอมรับ; แสดงข้อความ “ไม่พบข้อกำหนด…” และ block submit (ควรติดต่อเจ้าหน้าที่ — สถานะผิดปกติของ config)- Multi-language — schema ฝั่ง Centralized รองรับแล้ว แต่ round นี้ FE ส่ง
th-THอย่างเดียว;languageServedเป็นเรื่องภายใน backend ไม่กระทบ FE contentเป็น HTML — render ด้วย sanitized innerHTML (ระวัง XSS ถ้าเนื้อห้ามาจาก admin input)
Reference implementation
sa-onboarding-demo (repo เดิมของทีม backend) implement contract นี้ครบแล้ว — ใช้อ้างอิงโค้ดจริงได้:
| ไฟล์ | เนื้อหา |
|---|---|
core/models/corporate-application.models.ts | TypeScript interfaces (TermsNotice, FxRequestorTcConsent*) |
pages/corporate-application/components/fx-requestor-tc-consent-step.component.ts | render N เอกสาร + checkbox + mapping → submit payload |
core/services/corporate-application-http.service.ts | getCurrentTerms() (→ gateway Centralized) + action() (→ step-action submit) |
Endpoint reference (สรุป)
| # | Method | Path | Auth | ผู้เรียก |
|---|---|---|---|---|
| 1 | GET | …/centralized-api/api/centralized-service/v1/applications/fx/terms/current?languageCode=th-TH | Anonymous | FE |
| 2 | POST | …/userservice-api/api/user-service/v1/onboarding/instances/{id}/steps/FxRequestorTcConsentStep/actions/Submit | Anonymous + cookie | FE |
| 3 | POST | …/centralized-api/api/centralized-service/v1/applications/terms/notices/batch | Anonymous | UserService (ไม่ใช่ FE) |
- APIM gateway prefix (dev): Centralized =
/centralized-api, UserService =/userservice-api(ต่อด้วย{gateway}=https://gateway-dev.exim.go.th)- path segment
v1กับv1.0ใช้แทนกันได้ (ASP.NET API versioning) — reference FE ใช้v1- หมายเหตุ:
sa-onboarding-demo(localhost) เรียก onboarding ผ่าน BFF ไม่ใช่ APIM ตรง — prefix/userservice-apiยืนยันจากenvironment.apim-dev.ts(env ที่ route ผ่าน APIM จริง). Production FE ควร confirm prefix กับทีม APIM/IaC อีกครั้งตาม env