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 1 | Path 2 | |
|---|---|---|
| FlowCode | CUSTOMER_FIXED_ONBOARDING | RETURNING_CUSTOMER |
| ใครใช้ | อีเมลใหม่ ยังไม่มีบัญชี (anonymous) | อีเมลมีบัญชี FX อยู่แล้ว (logged-in) |
| Interactive step | 1 (OTP) | 0 (automatic ล้วน) |
| ระบุตัวตน | OTP → mint session | JWT + email-match gate |
| จบที่ | Draft → chain → Finalized | Finalized ระหว่าง Start เลย |
| AuthRequirement | RequiresAnonymous | RequiresLogin |
การสลับ path เป็น data-driven — CUSTOMER_FIXED_ONBOARDING ตั้ง LoggedInRedirectFlowCode = RETURNING_CUSTOMER. ถ้าคนคลิก link ขณะ login อยู่ → BE (/access) สั่ง swap ไป RETURNING_CUSTOMER. FE ไม่ตัดสินเอง — ถาม BE เสมอ
BE chain (เพื่อ context — FE ไม่ต้องรู้ราย step เพราะ engine ขับให้):
- Path 1:
OtpVerificationStep→CreatePlatformUser→CreateUserCompanyMapping→ProvisionCustomerAccess→AssignEndUserRole→ActivateEntraUser - 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 ที่ต้องใส่ให้ครบ):
?codeต้อง resolve ก่อน — FE มีแค่ token ยังไม่รู้flowCode/email→ ต้องเรียกvalidate-inviteก่อน start (ห้าม start ด้วย code เปล่า)- ต้อง thread
codeเข้า start body —POST /onboarding/instancesต้องมี fieldcode(ไม่ใช่แค่flowCode+email) มิฉะนั้น BE ไม่ consume token → ไม่มี token lineage →CreatePlatformUser/AssignEndUserRoleหา designee ไม่เจอ (ชื่อ/role/company หาย) - logged-in ต้องผ่าน
/accessก่อน — ห้าม startCUSTOMER_FIXED_ONBOARDINGตรงเมื่อ login อยู่ (BE จะ block ด้วยRequiresAnonymous) ต้องถาม/accessเพื่อรับeffectiveFlowCode=RETURNING_CUSTOMER - OTP =
Submitไม่ใช่Nextที่ flow นี้ (ดู §6) - 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 แล้ว):
| Endpoint | Body | Response | หมายเหตุ |
|---|---|---|---|
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? } | StepNavigationV2 | actionCode: SaveDraft|Next|Back|Submit|Resubmit |
POST /instances/{id}/open | {} | StepNavigationV2 | resume (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) |
|---|---|---|---|---|
| G1 | entry.code ไม่ถูกอ่าน → ไม่เรียก validate-invite | engine/onboarding.state.ts (init/startFill) | ถ้ามี entry.code → เรียก validateInvite(code) ก่อน → ได้ flowCode+email → ค่อยเข้า email/OTP phase | onboarding-runner startFromInvite() |
| G2 | startFlow() ไม่ส่ง code | services/.../onboarding.service.ts (startFlow) + state | เพิ่ม param code → body { flowCode, email?, code? } + thread จาก entry | demo startFlow(flowCode,email,code) |
| G3 | ไม่มี resolveFlowAccess เลย → Path 2 ทำไม่ได้ | services/.../onboarding.service.ts (เพิ่ม method) + state | เพิ่ม resolveFlowAccess(flowCode, inviteCode?) → ถ้า login อยู่ตอนคลิก invite → /access → SESSION_READY → start effectiveFlowCode | demo startInviteViaAccess() |
| G4 | OTP emit Next ตายตัว + redeem เฉพาะ Next | steps/otp-verification/ + state submit() | translate Next→Submit ตาม availableActions + redeem exchangeCode ทั้ง Next/Submit (ดู §6) | memory fix: onStepAction() |
| G5 | Path 2 (0-interactive) flow | state | flow ที่ไม่มี OTP + finalize-during-start → startFlowDirect คืน nav FINALIZED → finish() (มี 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()hardcodeemit({ code: 'Next' })(otp-verification.component.ts:103) — component ไม่รู้ตำแหน่งตัวเองใน flow- แต่
CUSTOMER_FIXED_ONBOARDINGมี OTP เป็น interactive step เดียว/สุดท้าย → engine บังคับSubmitที่ last-interactive → ส่งNextมา engine throwSession.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= ค่าจริงจาก serverAvailableActionsResolver) — ถ้าไม่มี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 อยู่ — จะโดนRequiresAnonymousblock - อย่า 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)
- Path 1 (anonymous): เปิด
?code=<valid>(ไม่ login) → validate → กรอก email → รับ OTP → verify (ส่ง Submit) → redeem exchange → session → chain จบ → Finalized + login ได้จริง - Path 2 (existing account): login แล้วเปิด
?code=<valid>→/accessคืนSESSION_READY+effectiveFlowCode=RETURNING_CUSTOMER→ start → nav กลับ Finalized ทันที (0 interactive) → result - invite invalid:
EXPIRED/REVOKED/SESSION_EXPIRED/404 → error view ที่สื่อความหมาย (ไม่ค้าง loading) - 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/onboardinglib ยังไม่ทำ 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.ts—init/startFill(G1),submitOTP redeem (G4), Path 2 (G5)services/user-service/onboarding/onboarding.service.ts—startFlow+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