Private Docs

08 — FE Onboarding Flow — API Contract & Sequence

Flow ที่ FE ต้องเรียกตั้งแต่ต้นจนจบ อ้างอิงจาก OnboardingRunnerComponent จริง: entry point decision → start → step loop → OTP → complete (BE PR #976, SA-1557)

อัปเดต: 2026-06-30

เอกสารนี้สำหรับ FE developer ที่ implement ส่วน onboarding — อ่านแล้วรู้ว่าต้องเรียก API อะไร ส่งอะไร รับอะไร ในแต่ละขั้นตอน

อ้างอิงจาก code จริง: OnboardingRunnerComponent.ngOnInit + CorporateApplicationHttpService

BE branch: feature/sprint9/SA-1557development · PR #976 (UserService)


ภาพรวม: Entry Decision Tree

Runner มี 5 entry path — ตัดสินตามลำดับความสำคัญ:

flowchart TD
  A[ngOnInit] --> AV{?adminView=}
  AV -->|มี| Z1[enterAdminView — read-only step map]
  AV -->|ไม่มี| ID

  ID{?id=}
  ID -->|มี| Z2["POST /instances/{id}/open → routeResume"]
  ID -->|ไม่มี| CODE

  CODE{?code= invite}
  CODE -->|มี| C["check GET /onboarding/session\n(กัน duplicate draft)"]
  C -->|200 nav + flowCode เดียวกัน| Z3[resume session เดิม]
  C -->|404 หรือ flowCode ต่าง| CINV["POST /validate-invite → startFlow(flowCode,email,code)"]

  CODE -->|ไม่มี| FLC
  FLC{?flowCode=}
  FLC -->|มี| RA["resolveFlowAccess(flowCode) → /access"]
  RA -->|SESSION_READY\n+ authed + RETURNING| RR["startReturningRequestor()\n→ POST /requestor/start"]
  RA -->|SESSION_READY\nอื่น| EE[email-entry → startFlow]
  RA -->|REQUIRES_LOGIN| LOGIN[redirect login]
  RA -->|NOT_ALLOWED| ERR[error view]

  FLC -->|ไม่มี| AUTH
  AUTH{isAuthenticated?}
  AUTH -->|ใช่| Z4[redirect /corporate-workspace]
  AUTH -->|ไม่ใช่| SES["GET /onboarding/session"]
  SES -->|200| Z5[resume session cookie]
  SES -->|404| Z6["email-entry → startFlow(STANDARD_CUSTOMER_ONBOARDING, email)"]

Quick Reference — Endpoints ทั้งหมด

MethodPathใช้เมื่อ
GET/onboarding/sessioncheck/resume cookie session
POST/onboarding/instances/{id}/openresume ด้วย ?id= param
POST/onboarding/validate-inviteresolve ?code= invite token
POST/onboarding/flows/{flowCode}/accessเฉพาะ ?flowCode= path — ขอ verdict
POST/onboarding/requestor/startstart requestor (authed)
POST/onboarding/instancesสร้าง FlowInstance (anonymous/invite)
POST/onboarding/instances/{id}/steps/{type}/actions/{code}ส่ง step action

Base path: /api/user-service/v1

/access endpoint เรียกเฉพาะ ?flowCode= path เท่านั้น — invite path (?code=) และ no-param path ไม่เรียก


Path A — Self-start ผ่าน ?flowCode= (NEW_REQUESTOR, anonymous)

ผู้ใช้ยังไม่ login เข้า URL ที่มี ?flowCode=NEW_REQUESTOR

sequenceDiagram
  participant FE
  participant BE as UserService

  FE->>BE: POST /flows/NEW_REQUESTOR/access  {}
  BE-->>FE: { decision:"SESSION_READY", effectiveFlowCode:"NEW_REQUESTOR" }

  Note over FE: แสดง email-entry form (FE ยังไม่รู้ email ของ anonymous user)
  Note over FE: user กรอก email แล้วกด start

  FE->>BE: POST /instances  { flowCode:"NEW_REQUESTOR", email:"user@example.com" }
  BE-->>FE: { nav, editSessionId, sessionTtlSeconds }
  Note over FE: set cookie __Host-onboarding=editSessionId

  Note over FE,BE: nav.currentStep.type = OtpVerificationStep → auto-send
  FE->>BE: POST …/OtpVerificationStep/actions/SaveDraft  { stepData:{email} }
  BE-->>FE: nav (outputJson มี refCode)

  Note over FE: user กรอก OTP
  FE->>BE: POST …/OtpVerificationStep/actions/Next  { stepData:{otp,refCode} }
  BE-->>FE: nav (step ถัดไป)

  loop step loop
    FE->>BE: POST …/{stepType}/actions/{code}  { stepData:{…} }
    BE-->>FE: nav
  end

ลำดับ API calls

A-0: Pre-flight (?flowCode= ทริกเกอร์)

POST /onboarding/flows/NEW_REQUESTOR/access
body: {}
→ { decision:"SESSION_READY", effectiveFlowCode:"NEW_REQUESTOR" }

สิ่งที่เปลี่ยนจาก spec เดิม:

  • field ชื่อ inviteCode (เดิมชื่อ code) — ส่งเฉพาะ flow ที่มี invite
  • ไม่ส่ง email ใน body — BE อ่านจาก session/JWT เอง (anonymous = null)

SESSION_READY (anonymous, ไม่ใช่ RETURNING) → FE แสดง email-entry form ก่อน

A-1: User กรอก email → สร้าง FlowInstance

POST /onboarding/instances
body: { "flowCode": "NEW_REQUESTOR", "email": "user@example.com" }
→ { nav, editSessionId, sessionTtlSeconds }

FE: set cookie __Host-onboarding=<editSessionId>; SameSite=Strict; Secure; Max-Age=<sessionTtlSeconds>

A-2: Auto-send OTP (OTP = step แรก → applyNav trigger sendOtp(false) อัตโนมัติ)

POST /onboarding/instances/{id}/steps/OtpVerificationStep/actions/SaveDraft
body: { "stepData": { "email": "user@example.com" } }
→ nav { outputJson: '{"refCode":"R1","status":"OTP_SENT","expirySeconds":300}' }

FE: เก็บ refCode จาก outputJson สำหรับ verify · เริ่ม countdown 300 วินาที

A-3: Verify OTP

POST /onboarding/instances/{id}/steps/OtpVerificationStep/actions/Next
body: { "stepData": { "otp": "123456", "refCode": "R1" } }
→ nav (step ถัดไป) หรือ error

dup-email guard อยู่ที่นี่: ถ้า email ซ้ำ (คนอื่น) → Otp.DuplicateEmail

A-4+: Step Loop (ดู §Phase 2 ด้านล่าง)


Path B — Self-start ผ่าน ?flowCode= (RETURNING_REQUESTOR, login แล้ว)

ผู้ใช้ login แล้ว เข้า URL ที่มี ?flowCode=NEW_REQUESTOR → BE สลับเป็น RETURNING_REQUESTOR

sequenceDiagram
  participant FE
  participant BE as UserService

  FE->>BE: POST /flows/NEW_REQUESTOR/access  {}
  BE-->>FE: { decision:"SESSION_READY", effectiveFlowCode:"RETURNING_REQUESTOR" }
  Note over FE: authed + effectiveFlowCode===RETURNING → startReturningRequestor()

  FE->>BE: POST /requestor/start  { email:"user@company.com" }
  BE-->>FE: { type:"SESSION_READY", flowInstanceId, nav }
  Note over FE: navigate ?id=flowInstanceId → routeResume(nav)

  loop step loop
    FE->>BE: POST …/{stepType}/actions/{code}  { stepData:{…} }
    BE-->>FE: nav
  end

ลำดับ API calls

B-0: Pre-flight (เหมือน A)

POST /onboarding/flows/NEW_REQUESTOR/access
body: {}
→ { decision:"SESSION_READY", effectiveFlowCode:"RETURNING_REQUESTOR" }

B-1: Start requestor (ไม่ต้องสร้าง FlowInstance ใหม่แยก — BE จัดการ new/returning swap เอง)

POST /onboarding/requestor/start
body: { "email": "<auth.userEmail()>" }   ← FE อ่านจาก JWT/auth service ฝั่ง FE
→ { type:"SESSION_READY", flowCode:"RETURNING_REQUESTOR", flowInstanceId:"...", nav:{…} }

FE: navigate ?id=flowInstanceIdrouteResume(nav) → render step โดยตรง ไม่มี OTP step

B-2+: Step Loop (ดู §Phase 2)

ไม่มี cookie __Host-onboarding สำหรับ returning requestor — auth ผ่าน JWT แทน


ลูกค้าได้รับลิงก์เชิญ ต้องเป็น anonymous (ไม่ login) ไม่เรียก /access — resolveInvite → startFlow ตรงๆ

sequenceDiagram
  participant FE
  participant BE as UserService

  Note over FE: URL: /onboarding?code=inv_abc123

  FE->>BE: GET /onboarding/session  (check cookie session ก่อน)
  BE-->>FE: 404 (ไม่มี session) หรือ 200 (session คนละ flow)

  FE->>BE: POST /validate-invite  { code:"inv_abc123" }
  BE-->>FE: { status:"VALID", flowCode:"CUSTOMER_FIXED_ONBOARDING", email:"customer@example.com" }

  FE->>BE: POST /instances  { flowCode:"CUSTOMER_FIXED_ONBOARDING", email:"customer@example.com", code:"inv_abc123" }
  BE-->>FE: { nav, editSessionId, sessionTtlSeconds }
  Note over FE: set cookie __Host-onboarding=editSessionId · flowEmail ถูก set จาก resolveInvite

  Note over FE,BE: OTP step (step แรก)
  FE->>BE: POST …/OtpVerificationStep/actions/SaveDraft  { stepData:{email} }
  BE-->>FE: nav (refCode)
  FE->>BE: POST …/OtpVerificationStep/actions/Next  { stepData:{otp,refCode} }
  BE-->>FE: nav

  loop step loop
    FE->>BE: POST …/{stepType}/actions/{code}  { stepData:{…} }
    BE-->>FE: nav
  end

ลำดับ API calls

C-0: Session check (กัน duplicate draft ถ้า reload ก่อน verify)

GET /onboarding/session
→ 200 nav + flowCode เดียวกัน → resume session เดิม (ไม่ start ใหม่)
→ 404 หรือ flowCode ต่าง → startFromInvite

C-1: Resolve invite

POST /onboarding/validate-invite
body: { "code": "inv_abc123" }
→ { status:"VALID", flowCode:"CUSTOMER_FIXED_ONBOARDING", email:"customer@example.com" }

C-2: สร้าง FlowInstance (code ไปใน body เพื่อให้ BE consume token)

POST /onboarding/instances
body: {
  "flowCode": "CUSTOMER_FIXED_ONBOARDING",
  "email": "customer@example.com",
  "code": "inv_abc123"
}
→ { nav, editSessionId, sessionTtlSeconds }

FE: set cookie __Host-onboarding=<editSessionId>

C-3+: OTP + step loop (เหมือน A-2/A-3 ต่อไป)


No-param Entry (Standard onboarding)

ผู้ใช้เข้า /onboarding โดยไม่มี query param

สถานะRunner ทำอะไร
login แล้วredirect ไป /corporate-workspaceไม่แสดง email-entry
ไม่ login + มี cookieGET /onboarding/session → 200 → resume session เดิม
ไม่ login + ไม่มี cookieGET /onboarding/session → 404 → email-entry → startFlow('STANDARD_CUSTOMER_ONBOARDING', email)

ไม่มีการเรียก /access สำหรับ no-param path

POST /onboarding/instances
body: { "flowCode": "STANDARD_CUSTOMER_ONBOARDING", "email": "user@example.com" }
→ { nav, editSessionId, sessionTtlSeconds }

Phase 2 — Step Loop

pattern เดียวสำหรับทุก step หลัง start:

POST /onboarding/instances/{instanceId}/steps/{stepType}/actions/{actionCode}
Content-Type: application/json
Cookie: __Host-onboarding=<editSessionId>   ← anonymous session เท่านั้น

body: { "stepData": { …payload จาก step component… } }

สังเกต: body ห่อด้วย stepData เสมอ — ไม่ส่ง payload ตรง

Response StepNavigationV2:

{
  "status": "IN_PROGRESS",
  "flowInstanceId": "abc-123",
  "flowCode": "NEW_REQUESTOR",
  "currentStep": {
    "type": "PersonalInfoStep",
    "index": 2,
    "canGoBack": true,
    "outputJson": null
  },
  "availableActions": ["Next", "Back"]
}
statusFE แสดงอะไร
IN_PROGRESSrender currentStep.type ผ่าน STEP_REGISTRY
SUBMITTEDnavigate ไป /application-result?ref=…&id=…
FINALIZEDinline result → redirect /home (4 วินาที)
TERMINALinline result + terminalStatus

Action codes

actionCodeความหมาย
Nextadvance + ส่งข้อมูล
Backย้อนกลับ
SaveDraftsave ไม่ advance (ใช้ส่ง OTP)
Submitsubmit สำหรับ review
Resubmitส่งใหม่ (rework path)

OTP Step — รายละเอียด

OTP เป็น step ปกติใน engine ไม่ใช่ gate แยก

FE behavior เมื่อ applyNav เห็น OtpVerificationStep

  1. Auto-send ทันที — sendOtp(false)SaveDraft โดยไม่รอ user
  2. เก็บ refCode จาก outputJson → ส่งกลับตอน Next
  3. Countdown 300 วินาที + resend cooldown 60 วินาที
  4. Resend — user กดปุ่ม → sendOtp(true)SaveDraft ซ้ำ
SaveDraft body: { "stepData": { "email": "<flowEmail>" } }
Next body:      { "stepData": { "otp": "123456", "refCode": "R1" } }

flowEmail ถูก set ก่อน startFlow เสมอ (email-entry form / invite email จาก resolveInvite)

dup-email guard (ตรวจที่ OTP verify ไม่ใช่ /access)

สถานการณ์ผล
ไม่มีบัญชีผ่าน → สร้าง user ใหม่
บัญชีเดิม + OID เดียวกัน (retry)ผ่าน (idempotent)
บัญชีเดิม + OID ต่างกันerror 4xx: { errorCode:"Otp.DuplicateEmail" }

FE: แสดงข้อความ + ปุ่มไป login


Resume Paths

GET /onboarding/session
Cookie: __Host-onboarding=<editSessionId>
→ 200: nav → applyNav → routeView
→ 404: ไม่มี session → email-entry
POST /onboarding/instances/{id}/open
body: {}
→ nav → routeResume
  → ถ้า nav.correctionContext → enterReworkView (editable map)
  → ไม่งั้น → applyNav → routeView

ใช้เฉพาะ anonymous session (Path A, C, no-param) — returning requestor (Path B) ใช้ JWT

Attributeค่า
Name__Host-onboarding
ValueeditSessionId (จาก BE response)
SameSiteStrict
Secure
Max-AgesessionTtlSeconds (default 86400)
Path/

cookie FE ตั้งเอง หลังได้ editSessionId — BE ไม่ Set-Cookie


Error Reference

errorCodeเกิดตอนFE ทำอะไร
HTTP 404 Flow.NotFound/accesserror view
verdict NOT_ALLOWED + Flow.MustNotBeLoggedIn/accessแจ้ง logout
verdict NOT_ALLOWED + Flow.InviteRequiredOrExpired/accesserror ขอ invite ใหม่
verdict NOT_ALLOWED + Flow.SelfStartNotAllowed/accesserror
verdict REQUIRES_LOGIN/accessredirect loginUrl
Otp.DuplicateEmailOTP Nextแสดง error + ปุ่ม login
Otp.DuplicateAccountOTP SaveDraftแสดง error + ปุ่ม login
Otp.ExpiredOTP Nextขอ OTP ใหม่
Otp.VerifyFailedOTP Nextแสดง error
HTTP 403 Flow.SelfStartNotAllowedPOST /instancesflow ต้องมี invite