Private Docs

Customer Onboarding (Shorten) — FE Integration Spec

Spec สำหรับ FE dev ที่จะต่อ Customer Onboarding (end-user ที่ Requestor เชิญ) เข้ากับ frontend จริง — @exim/onboarding lib + Host shell. BE เสร็จ+merged+seed(dev)แล้ว เหลือ FE + integration test. ครอบคลุม token/invite mechanics, API contract ครบ, gap analysis ราย step, และ acceptance criteria

อัปเดต: 2026-07-09

เอกสารนี้เป็น spec ให้ FE dev/AI ต่องาน Customer Onboarding (เรียกภายในว่า “Shorten Onboarding”) เข้ากับ frontend จริง — ฝั่ง BE ทำเสร็จ merged + seed ลง dev DB แล้ว เหลือแค่ต่อ FE จริง + integration test


1. Shorten Onboarding คืออะไร

Onboarding มี 2 ฝั่ง — Requestor (ฟอร์มยาว) ทำเสร็จแล้ว, ฝั่งนี้คือ end-user (ลูกค้าที่ Requestor เชิญ):

  • Requestor กรอกชื่อ-สกุล + role ของลูกค้าไว้แล้วตอน DesignateCustomersStep → ระบบ mint InvitationToken → ส่งอีเมล link ?code={tokenId}
  • end-user คลิก link → verify OTP ครั้งเดียว → login ใช้งานได้เลย (ข้อมูล/สิทธิ์ดึงจาก designee อัตโนมัติ ไม่ต้องกรอกซ้ำ) = “shorten”

2. สอง Path = สอง FlowDefinition (BE)

Path 1Path 2
FlowCodeCUSTOMER_FIXED_ONBOARDINGRETURNING_CUSTOMER
ใครใช้อีเมลใหม่ ยังไม่มีบัญชี (anonymous)อีเมลมีบัญชี FX อยู่แล้ว (logged-in)
Interactive step1 (OTP)0 (automatic ล้วน)
ระบุตัวตนOTP → mint sessionJWT + email-match gate
จบที่Draft → chain → FinalizedFinalized ระหว่าง Start เลย
AuthRequirementRequiresAnonymousRequiresLogin

การสลับ path เป็น data-drivenCUSTOMER_FIXED_ONBOARDING ตั้ง LoggedInRedirectFlowCode = RETURNING_CUSTOMER. ถ้าคนคลิก link ขณะ login อยู่ → BE (/access) สั่ง swap ไป RETURNING_CUSTOMER. FE ไม่ตัดสินเอง — ถาม BE เสมอ

BE chain (เพื่อ context — FE ไม่ต้องรู้ราย step เพราะ engine ขับให้):

  • Path 1: OtpVerificationStepCreatePlatformUserCreateUserCompanyMappingProvisionCustomerAccessAssignEndUserRoleActivateEntraUser
  • Path 2: เหมือนกันแต่ตัด OtpVerificationStep (5 automatic ล้วน)

3. Token / Invite mechanics ★ (หัวใจของ FE งานนี้)

นี่คือส่วนที่ FE ต้อง implement — flow ตั้งแต่ ?code จนถึง login:

flowchart TD
  A["เข้า /onboarding?code=TOKEN"] --> B["POST /onboarding/validate-invite { code }"]
  B -->|status != VALID| E["แสดง error: EXPIRED / REVOKED / SESSION_EXPIRED / 404"]
  B -->|"VALID → { email, flowCode }"| C{login อยู่ไหม?}
  C -->|"anonymous"| D1["POST /onboarding/instances<br/>{ flowCode, email, code }"]
  C -->|"authenticated"| AC["POST /onboarding/flows/{flowCode}/access<br/>{ inviteCode: code }"]
  AC -->|"SESSION_READY → effectiveFlowCode"| D2["POST /onboarding/instances<br/>{ flowCode: effectiveFlowCode, code }"]
  AC -->|REQUIRES_LOGIN| L["redirect loginUrl แล้วกลับมา"]
  AC -->|NOT_ALLOWED| E2["error view (reasonCode/message)"]
  D1 --> OTP["OTP loop: SaveDraft=ส่ง OTP · Submit=verify"]
  OTP -->|"verify → nav.stepOutputJson มี exchangeCode"| X["POST Sentinel /exchange → mint session cookie"]
  X --> R["resume ?instanceId → chain จบ → Finalized"]
  D2 --> R2["nav กลับมา Finalized ทันที (0 interactive) → result"]

จุดที่พลาดบ่อย (mechanical ที่ต้องใส่ให้ครบ):

  1. ?code ต้อง resolve ก่อน — FE มีแค่ token ยังไม่รู้ flowCode/email → ต้องเรียก validate-invite ก่อน start (ห้าม start ด้วย code เปล่า)
  2. ต้อง thread code เข้า start bodyPOST /onboarding/instances ต้องมี field code (ไม่ใช่แค่ flowCode+email) มิฉะนั้น BE ไม่ consume token → ไม่มี token lineageCreatePlatformUser/AssignEndUserRole หา designee ไม่เจอ (ชื่อ/role/company หาย)
  3. logged-in ต้องผ่าน /access ก่อน — ห้าม start CUSTOMER_FIXED_ONBOARDING ตรงเมื่อ login อยู่ (BE จะ block ด้วย RequiresAnonymous) ต้องถาม /access เพื่อรับ effectiveFlowCode = RETURNING_CUSTOMER
  4. OTP = Submit ไม่ใช่ Next ที่ flow นี้ (ดู §6)
  5. exchangeCode → session — หลัง verify, nav.stepOutputJson มี exchangeCode → FE ต้อง redeem ผ่าน Sentinel Gateway (config.onIdentityVerified) เพื่อ mint session cookie ก่อน resume

4. API contract reference

ทุก endpoint base = {apiBaseUrl}/{userService}/v1/onboarding (มีใน OnboardingHttpService แล้ว):

EndpointBodyResponseหมายเหตุ
POST /validate-invite{ code }{ status, email, flowCode, flowName, invitationExpiresAt }status: VALID|EXPIRED|REVOKED|SESSION_EXPIRED · 404=ไม่พบ · ไม่ consume
POST /flows/{flowCode}/access{ inviteCode }{ decision, effectiveFlowCode, loginUrl, reasonCode, message }decision: SESSION_READY|REQUIRES_LOGIN|NOT_ALLOWED · side-effect-free · email อ่านจาก session
POST /instances{ flowCode, email?, code? }StepNavigationV2 (nav)anonymous start ได้ · code = invite token (ต้องส่ง)
POST /instances/{id}/steps/{stepType}/actions/{actionCode}{ stepData? }StepNavigationV2actionCode: SaveDraft|Next|Back|Submit|Resubmit
POST /instances/{id}/open{}StepNavigationV2resume (owner JWT / session cookie)
GET /flows/{code}PublicFlowDto { steps[] }วาด sidebar (step order)

OTP verify output (nav.stepOutputJson, JSON string): { status:'VERIFIED', userId, exchangeCode } → redeem exchangeCode ที่ Sentinel Gateway POST /sentinel-gateway-api/sentinel/auth/onboarding/exchange เพื่อ mint session cookie

5. สิ่งที่ FE ต้องทำ (Gap analysis)

สถานะ @exim/onboarding lib ปัจจุบัน (verify กับ code จริง): เป็น engine runner ครบ (nav/action loop, OTP step, sidebar, rework) — mode fx/admin/platform. Host page (onboarding.ts) อ่าน ?code → ส่งเป็น [inviteCode]OnboardingComponent set entry.code แล้ว แต่ state ไม่เคยอ่าน entry.code → invite path ยังไม่ทำงาน

#Gapไฟล์ต้องทำreference (demo)
G1entry.code ไม่ถูกอ่าน → ไม่เรียก validate-inviteengine/onboarding.state.ts (init/startFill)ถ้ามี entry.code → เรียก validateInvite(code) ก่อน → ได้ flowCode+email → ค่อยเข้า email/OTP phaseonboarding-runner startFromInvite()
G2startFlow() ไม่ส่ง codeservices/.../onboarding.service.ts (startFlow) + stateเพิ่ม param code → body { flowCode, email?, code? } + thread จาก entrydemo startFlow(flowCode,email,code)
G3ไม่มี resolveFlowAccess เลย → Path 2 ทำไม่ได้services/.../onboarding.service.ts (เพิ่ม method) + stateเพิ่ม resolveFlowAccess(flowCode, inviteCode?) → ถ้า login อยู่ตอนคลิก invite → /access → SESSION_READY → start effectiveFlowCodedemo startInviteViaAccess()
G4OTP emit Next ตายตัว + redeem เฉพาะ Nextsteps/otp-verification/ + state submit()translate Next→Submit ตาม availableActions + redeem exchangeCode ทั้ง Next/Submit (ดู §6)memory fix: onStepAction()
G5Path 2 (0-interactive) flowstateflow ที่ไม่มี OTP + finalize-during-start → startFlowDirect คืน nav FINALIZEDfinish() (มี path นี้แล้ว แต่ต้องได้ flowCode จาก G3 ก่อน)

ลำดับแนะนำ: G2 (service signature) → G1 (invite resolve, anonymous ก่อน) → G4 (OTP submit — ไม่งั้น Path 1 พังทุกครั้ง) → G3+G5 (existing-account) → integration test

6. OTP Next/Submit action translation (บั๊กที่ทดสอบจับยาก)

🔴 นี่คือบั๊กที่ advisor จับได้ 2 รอบในฝั่ง demo — จะเกิดซ้ำใน lib จริงถ้าไม่ระวัง:

  • OnboardingOtpVerificationComponent.onSubmitOtp() hardcode emit({ code: 'Next' }) (otp-verification.component.ts:103) — component ไม่รู้ตำแหน่งตัวเองใน flow
  • แต่ CUSTOMER_FIXED_ONBOARDING มี OTP เป็น interactive step เดียว/สุดท้าย → engine บังคับ Submit ที่ last-interactive → ส่ง Next มา engine throw Session.InvalidAction: ขั้นตอนสุดท้ายต้องส่งใบสมัครด้วย Submit ก่อนถึง response
  • ซ้ำร้าย OnboardingState.submit() redeem exchangeCode เฉพาะ code === 'Next' (onboarding.state.ts:536) → ต่อให้แปลงเป็น Submit ได้ก็ยังไม่ redeem

Fix (ทำที่ orchestration layer ไม่ใช่ component):

  • ก่อนส่ง action ของ OTP: เช็ค availableActions (จาก nav.currentStep.availableActions = ค่าจริงจาก server AvailableActionsResolver) — ถ้าไม่มี Next แต่มี Submit → แปลง Next → Submit
  • คง Next ไว้สำหรับ NEW_REQUESTOR (OTP ไม่ใช่ step สุดท้าย) — อย่า hardcode Submit
  • redeem exchangeCode ให้ครอบทั้ง Next และ Submit

7. Invariants / อย่าทำ

  • อย่าให้ FE เดา path เอง — logged-in + invite ต้องผ่าน /access เสมอ (BE = single authority ของ NEW→RETURNING swap)
  • อย่า start CUSTOMER_FIXED_ONBOARDING ตอน login อยู่ — จะโดน RequiresAnonymous block
  • อย่า drop invite code ตอน start — token lineage พังเงียบ (ชื่อ/role/company หาย, ไม่ error ตรงๆ)
  • exchange ล้ม ห้าม crash flow — mint session fail → fallback ไป login ไม่ใช่ throw
  • T&C: backend ตัด T&C step ออกแล้ว — ถ้า product อยากได้จอ T&C ลูกค้า จะเป็น FE-only (⚠️ ไม่มี server-side consent record — EXIM เป็นธนาคาร อาจมี audit requirement, เป็น open governance item อย่าเงียบ)

8. Acceptance criteria (integration test)

  1. Path 1 (anonymous): เปิด ?code=<valid> (ไม่ login) → validate → กรอก email → รับ OTP → verify (ส่ง Submit) → redeem exchange → session → chain จบ → Finalized + login ได้จริง
  2. Path 2 (existing account): login แล้วเปิด ?code=<valid>/access คืน SESSION_READY + effectiveFlowCode=RETURNING_CUSTOMER → start → nav กลับ Finalized ทันที (0 interactive) → result
  3. invite invalid: EXPIRED/REVOKED/SESSION_EXPIRED/404 → error view ที่สื่อความหมาย (ไม่ค้าง loading)
  4. NEW_REQUESTOR ไม่ regression — OTP ที่ไม่ใช่ step สุดท้ายยังส่ง Next ได้ปกติ

9. สถานะ + อ้างอิง

สถานะปัจจุบัน (2026-07-09)

  • BE: เสร็จ merged เข้า development (PR #1149, afc52c8) · full suite เขียว (1206 pass) · seed apply แล้วเฉพาะ dev DB
  • FE: @exim/onboarding lib ยังไม่ทำ customer/invite path (G1–G5 ข้างบน) · demo (sa-onboarding-demo) ทำครบเป็น reference
  • เหลือ: FE implement + integration test (§8) + seed ขึ้น env สูง

File map

FE (ต้องแก้ — Frontend_SuperAppLibraryUi/libs/onboarding/src/lib/):

  • engine/onboarding.state.tsinit/startFill (G1), submit OTP redeem (G4), Path 2 (G5)
  • services/user-service/onboarding/onboarding.service.tsstartFlow +code (G2), เพิ่ม resolveFlowAccess (G3)
  • steps/otp-verification/otp-verification.component.ts — action emit (G4)
  • modes/fx/fx-step-registry.ts — (ไม่ต้องเพิ่ม; OTP register แล้ว)

FE (host, plumbing พร้อมแล้ว): Frontend_HostAppSuperApp/apps/shell/src/app/pages/onboarding/onboarding.ts (อ่าน ?code[inviteCode])

Reference (อย่าแก้/อย่าส่งมอบ): sa-onboarding-demo/src/app/pages/onboarding-runner/onboarding-runner.component.ts (startFromInvite, startInviteViaAccess, continueAfterVerify) + core/services/invite.service.ts

BE (เสร็จแล้ว — อ้างอิง contract): Backend_UserService/.../Controllers/v1/OnboardingController.cs · .../Queries/ResolveFlowAccess/ · seed Backend_UserService/seed/*.sql

เอกสาร/memory

  • SoT design เดิม (ส่วน T&C ล้าสมัยหลัง revert): Atlas/docs/user-service/07072026/DESIGN_CUSTOMER_FIXED_ONBOARDING_ENDUSER_COMPLETION.md
  • Handoff BE: Atlas/docs/user-service/07072026/HANDOFF_CUSTOMER_FIXED_ONBOARDING_ENDUSER_COMPLETION.md
  • engine docs: Flows · Frontend Flow · Backend Process · DB Schema