Private Docs

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-documentmulti-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 นี้)
  1. FE เรียก /terms/current → ได้ หลายเอกสาร (noticeList[]) แต่ละเอกสารมี T&C content + รายการ consent (purpose)
  2. FE render ทุกเอกสาร + checkbox ต่อ purpose → เก็บสถานะ agreed
  3. กด “ส่งใบสมัคร” → FE POST array documents[] ไปที่ step-action ของ UserService
  4. 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)
languageCodeth-TH (ปัจจุบันรองรับ th-TH เท่านั้น)
AuthAllowAnonymous — เป็น 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ชนิดความหมาย / วิธีใช้
documentIdGuidคีย์หลักของเอกสาร — ต้องส่งกลับตอน submit (field ใหม่ round นี้)
noticeIdGuid (string)= TermsVersionId ภายใน — ใช้เป็น React/Angular list key ได้ แต่ห้ามส่งกลับตอน submit (ดู warning ด้านล่าง)
noticeNamestringชื่อเอกสาร (หัวข้อกล่อง)
versionstringเช่น "v3.0" — ต้องส่งกลับตอน submit คู่กับ documentId
sections[]arrayเนื้อหา T&C — render content (อาจเป็น HTML), เรียงตาม order
purposeList[]arrayรายการ consent (checkbox) ของเอกสารนี้
purposeList[].purposeIdstringเช่น TC-0000001-MAINค่านี้แหละที่ต้องส่งเป็น consentCode ตอน submit
purposeList[].purposeNamestringlabel ของ checkbox
purposeList[].requirebooltrue = บังคับติ๊ก ถึงจะ submit ผ่าน
purposeList[].requireScrollboolจับมาจาก contract จริง — ยังไม่ enforce scroll-to-enable round นี้ (FE จะ implement UX นี้หรือไม่ก็ได้)

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 ที่รันอยู่)
stepTypeFxRequestorTcConsentStep (ค่า literal ตายตัว)
actionCodeSubmit
AuthAllowAnonymous + 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 = สถานะ checkbox
  • lang = 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
documentIdnoticeList[].documentId
versionnoticeList[].version
langlanguageCode ที่ request (th-TH)
consents[].consentCodenoticeList[].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) ที่ส่งไม่มีใน Centralizedre-fetch /terms/current (version ถูก unpublish/เปลี่ยน)
Step.TcDuplicateDocumentมี documentId ซ้ำใน documents[]bug ฝั่ง FE — อย่าส่ง documentId ซ้ำ
Step.TcServiceUnavailableUserService เรียก Centralized ไม่ได้ให้ผู้ใช้ ลองใหม่ (ปัญหาชั่วคราว, ไม่ใช่ข้อมูลผิด)
Step.DependencyMissingstep ก่อนหน้ายังไม่ครบไม่ควรเกิดถ้าเดิน 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/currentversion + 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.tsTypeScript interfaces (TermsNotice, FxRequestorTcConsent*)
pages/corporate-application/components/fx-requestor-tc-consent-step.component.tsrender N เอกสาร + checkbox + mapping → submit payload
core/services/corporate-application-http.service.tsgetCurrentTerms() (→ gateway Centralized) + action() (→ step-action submit)

Endpoint reference (สรุป)

#MethodPathAuthผู้เรียก
1GET…/centralized-api/api/centralized-service/v1/applications/fx/terms/current?languageCode=th-THAnonymousFE
2POST…/userservice-api/api/user-service/v1/onboarding/instances/{id}/steps/FxRequestorTcConsentStep/actions/SubmitAnonymous + cookieFE
3POST…/centralized-api/api/centralized-service/v1/applications/terms/notices/batchAnonymousUserService (ไม่ใช่ 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