FX Online — Lego Onboarding Email Templates & Event Triggers
สรุป email template ทั้งหมดที่เกี่ยวข้องกับ Lego Onboarding (UserService, WorkflowService, OrchestratorService, NotificationService) — template ไหนใช้กับ event ไหน และตัวไหนเป็น onboarding จริงบ้าง
อัปเดต: 2026-07-01
อ้างอิงจากโค้ดจริง:
Backend_UserService(Lego Engine),Backend_WorkflowService,Backend_OrchestratorService,Backend_NotificationServiceอ้างอิงเอกสาร:docs/user-service/lego-engine/,docs/orchestration-integration/,docs/orchestrator-service/,docs/notification-service/
1. ภาพรวม — ใครทำอะไรใน Lego Onboarding
Lego Engine (อยู่ใน UserService) เป็น workflow engine แบบ generic ที่ขับเคลื่อนกระบวนการ onboarding
(สมัครใช้บริการ FX / ลงทะเบียน user ใหม่ / ผูกบริษัท) ใบสมัคร 1 ใบ = FlowInstance เดินสถานะ
Draft → Submitted → (Approval loop) → Approved/Rejected/Cancelled → Finalized
4 service เกี่ยวข้องกับการ “ส่ง email” ดังนี้:
| Service | บทบาทต่อ email |
|---|---|
| UserService (Lego Engine) | เจ้าของ requestor identity + flow state — publish โดยตรง ไปหา NotificationService (invitation email, flow-status email) |
| OrchestratorService | Saga กลางที่ประสาน US ↔ WF (approval routing/state) — ไม่ส่ง email เอง (comment ในโค้ด: “Workflow notifies the maker/assignee itself now → no AdminNotification raised here”) |
| WorkflowService | เจ้าของ task/maker assignment — publish ไปหา NotificationService เมื่อมอบหมายงานให้ maker คนใดคนหนึ่ง |
| NotificationService | ปลายทางเดียวที่ยิง email จริง (ผ่าน ANE) — resolve template จาก event ที่รับเข้ามา |
sequenceDiagram
participant Req as Requestor
participant US as UserService (Lego Engine)
participant OC as OrchestratorService (Saga)
participant WF as WorkflowService (Task/Maker)
participant NS as NotificationService
participant ANE as ANE (Email provider)
Req->>US: สมัคร (Draft → Submit)
US->>OC: ASB flow-submitted-for-approval
OC->>WF: POST /instances (สร้าง workflow instance)
WF-->>OC: ASB workflow-pickup / workflow-decision
WF->>NS: ASB workflow-maker-notification (assigned) — EmailTemplateKey=maker_task_assigned
NS->>ANE: ส่ง email ให้ maker
OC->>US: ASB flow-approval-result (Approve/Reject/Rework/Cancel)
US->>NS: ASB flow-status-notification (subject ตามผลลัพธ์)
NS->>ANE: ส่ง email ให้ requestor
⭐ 1.5 Highlight — เฉพาะเรื่อง “Onboarding” คือ template ไหนบ้าง
ทั้งระบบมี email template 14 key แต่ ไม่ได้ทุกตัวเป็นส่วนหนึ่งของ onboarding flow — บางตัวใช้กับ login/contact ทั่วไปที่ไม่เกี่ยวกับการสมัคร ตารางนี้แยกให้ชัดว่าตัวไหนแก้ได้/ต้องแก้เมื่อพูดถึง onboarding
✅ เป็นส่วนหนึ่งของ Onboarding flow โดยตรง (แก้ตัวเหล่านี้)
| ลำดับใน flow | Template Key | จุดที่เกิดใน onboarding |
|---|---|---|
| 1. เริ่มสมัคร (invite-only) | invitation_default | Admin เชิญ requestor เข้าสมัคร |
| 2. ยืนยันตัวตน (identity step) | otp_spa_registration | OTP ตอนลงทะเบียน/ยืนยันอีเมลระหว่างกรอกใบสมัคร |
| 3. ส่งงานให้ maker ตรวจ | maker_task_assigned | มอบหมาย/resubmit งานให้ maker คนใดคนหนึ่งตรวจใบสมัคร |
| 4a. ผลลัพธ์ = อนุมัติ | fx_application_approved | requestor ได้รับอนุมัติ |
| 4b. ผลลัพธ์ = ต้องแก้ไข | fx_application_rework | requestor ถูกขอให้แก้ไขใบสมัคร |
| 4c. ผลลัพธ์ = ไม่อนุมัติ | fx_application_rejected | requestor ถูกปฏิเสธ |
| 4d. ผลลัพธ์ = ยกเลิก | fx_application_cancelled | requestor ยกเลิกใบสมัคร/หมดอายุ |
สรุป: มี 7 templates ที่เป็น onboarding จริง → invitation_default, otp_spa_registration, maker_task_assigned, fx_application_approved, fx_application_rework, fx_application_rejected, fx_application_cancelled
❌ ไม่ใช่ onboarding (อย่าไปแก้ปนถ้า scope คือ onboarding)
| Template Key | ใช้ที่ไหน | ทำไมไม่ใช่ onboarding |
|---|---|---|
otp_spa_login | ทุกครั้งที่ user login เข้าระบบ (ไม่ใช่แค่ตอนสมัคร) | เป็น auth ทั่วไป — ใช้ตลอดอายุ user ไม่ใช่แค่ระหว่าง onboarding |
otp_spa_news_subscription | ยืนยัน email contact channel (สมัครรับข่าวสาร) | ฟีเจอร์แยกจาก onboarding flow |
otp_login / otp_onboarding / otp_email_contact | ไม่มี trigger จริง (legacy/reserved key) | เป็น key ค้างจากของเก่า — ไม่ถูกเรียกใช้เลยในโค้ดปัจจุบัน (ดู §2.2) |
role_assignment | เตรียมไว้สำหรับแจ้ง role assignment | ไม่เกี่ยวกับ onboarding submission |
notification_general | เตรียมไว้สำหรับ general/batch notification | ไม่เกี่ยวกับ onboarding submission |
⚠️ ระวังชื่อชนกัน: มี key ที่ชื่อดูเหมือน onboarding แต่ ไม่ได้ถูกใช้จริง คือ
otp_onboarding(legacy, ไม่มี publisher เรียก) — ตัวที่ใช้จริงตอนสมัครคือotp_spa_registrationคนละ key กัน ห้ามแก้ผิดตัว
2. Email Template Catalog — ครบทุก key ที่ seed ไว้จริง
ที่มา: EmailTemplateDataSeeder.GetSystemTemplateKeys()
(Backend_NotificationService/src/Notification02.Infrastructure/Persistence/Seeding/EmailTemplateDataSeeder.cs)
2.1 กลุ่มที่ใช้งานจริงใน Lego Onboarding (มี trigger ต่อสาย)
คอลัมน์ Onboarding? — ✅ = ใช่ (แก้ตัวนี้ถ้า scope คือ onboarding) · ⚠️ = ใช้ร่วมกับเคสอื่นด้วย (ระวังตอนแก้)
| Onboarding? | Template Key | ใช้ทำอะไร | Event / Subject ที่ trigger | ผู้ publish | Transport | Macro หลัก |
|---|---|---|---|---|---|---|
| ✅ | fx_application_approved | แจ้ง requestor ว่าใบสมัครอนุมัติแล้ว | subject flow-approved | UserService (AsbFlowStatusNotificationPublisher) | ASB topic flow-status-notification | FX_REF_NO |
| ✅ | fx_application_rework | แจ้ง requestor ว่าใบสมัครต้องแก้ไข | subject flow-correction-requested | UserService | ASB topic flow-status-notification | FX_REF_NO, FX_CORRECTION_NOTE |
| ✅ | fx_application_rejected | แจ้ง requestor ว่าใบสมัครไม่อนุมัติ | subject flow-rejected | UserService | ASB topic flow-status-notification | FX_REF_NO (ไม่มีเหตุผล — CorrectionNote ส่ง null เสมอ) |
| ✅ | fx_application_cancelled | แจ้ง requestor ว่าใบสมัครถูกยกเลิก | subject flow-cancelled | UserService | ASB topic flow-status-notification | FX_REF_NO, FX_CORRECTION_NOTE (= เหตุผลยกเลิก) |
| ✅ | maker_task_assigned | แจ้ง maker ว่ามีงานมอบหมายให้ตรวจ (resubmit/resume) | EmailTemplateKey="maker_task_assigned" เมื่อมี AssigneeUserId+AssigneeEmail | WorkflowService | ASB topic workflow-maker-notification | MAKER_TASK_SUMMARY, FX_REF_NO, FX_DEEP_LINK |
| ✅ | invitation_default | ส่งลิงก์เชิญเข้าสมัคร (invite-only onboarding) | Admin สร้าง Invitation Token ใน UserService | UserService (AsbInvitationEventPublisher) | ASB topic invitation-event | INVITATION_LINK, INVITATION_EXPIRY_DATE, INVITATION_FLOW_NAME, RECIPIENT_EMAIL (จริง ๆ ใช้ templateKey ที่ผูกกับ FlowDefinition ได้ เช่น invitation_fx — invitation_default คือค่า placeholder เริ่มต้น) |
| ✅ | otp_spa_registration | OTP ตอนลงทะเบียน/ยืนยันอีเมลระหว่าง onboarding | webhook Entra, module=onboarding | Microsoft Entra → HTTP webhook | POST .../auth-extension/email-otp-send | OTP_CODE, REF_CODE, EXPIRY_MINUTES |
| ⚠️ | otp_spa_login | OTP ตอน login ทั่วไป (ไม่ใช่แค่ onboarding) | webhook เดียวกัน, module=login | Microsoft Entra → HTTP webhook | endpoint เดียวกัน | OTP_CODE, EXPIRY_MINUTES |
| ⚠️ | otp_spa_news_subscription | OTP ยืนยัน email contact channel (ไม่เกี่ยวกับ onboarding) | module=email-contact-channel | NotificationService (internal) | Internal call | OTP_CODE, EXPIRY_MINUTES |
หมายเหตุสำคัญ: ชื่อ subject ที่เอกสาร integration บางไฟล์เขียนว่า
flow-reworkเป็นชื่อเก่า/หลวม — โค้ดจริงใช้flow-correction-requested(FlowStatusNotificationMap.cs) ให้ยึดโค้ดเป็นหลัก
2.2 กลุ่มที่ seed ไว้แล้วแต่ยังไม่มี trigger ใช้งานจริง (reserved / legacy)
| Template Key | สถานะ | หมายเหตุ |
|---|---|---|
otp_login | ไม่มี publisher เรียกใช้ | ดูเหมือนถูกแทนที่ด้วย otp_spa_login (mapping จริงผ่าน OtpEmailTemplateKeys.Resolve() คืนค่า otp_spa_* เท่านั้น) |
otp_onboarding | ไม่มี publisher เรียกใช้ | ถูกแทนที่ด้วย otp_spa_registration เช่นกัน |
otp_email_contact | ไม่มี publisher เรียกใช้ | ถูกแทนที่ด้วย otp_spa_news_subscription |
role_assignment | ไม่มี publisher เรียกใช้ | เตรียมไว้สำหรับ “แจ้งการกำหนด Role” (company-specific) — ยังไม่มีจุดเรียก |
notification_general | ไม่มี publisher เรียกใช้ | เตรียมไว้สำหรับ general/batch notification |
จุดสังเกต: มี key ซ้ำซ้อน 2 ชุดสำหรับ OTP (
otp_login/otp_onboarding/otp_email_contactแบบเก่า vsotp_spa_*แบบที่ใช้จริงผ่าน ANE) — ถ้าจะ cleanup ควรลบชุดที่ไม่ใช้ทิ้ง เพื่อไม่ให้สับสนตอนดูรายการ template ใน Admin UI
3. Flow ที่ 1 — Requestor: อนุมัติ/แก้ไข/ปฏิเสธ/ยกเลิก (flow-status-notification)
เจ้าของ: UserService Lego Engine (AsbFlowStatusNotificationPublisher, topic flow-status-notification)
ผู้รับ (routing): FlowStatusNotificationMap.Resolve(subject) ฝั่ง NotificationService
| Subject (ASB) | Title (in-app) | NotificationType | Email Template |
|---|---|---|---|
flow-approved | ใบสมัคร FX Online ได้รับการอนุมัติ | Success | fx_application_approved |
flow-correction-requested | ใบสมัคร FX Online ต้องการการแก้ไข | Warning | fx_application_rework |
flow-rejected | ใบสมัคร FX Online ไม่ได้รับการอนุมัติ | Danger | fx_application_rejected |
flow-cancelled | ใบสมัคร FX Online ถูกยกเลิก | Info | fx_application_cancelled |
Payload (FlowStatusNotificationEvent): EventId, OccurredAt, FlowInstanceId, RefNo, RequestorUserId, RequestorEmail, DeepLinkUrl, CorrectionNote
CorrectionNoteมีค่าเฉพาะ rework และ cancel (เหตุผล); rejected ส่งnullเสมอ (ไม่มีเหตุผลใน email — ปัจจุบัน template ก็ไม่มี placeholder รองรับอยู่แล้ว)- ทั้ง 4 subject ส่งทั้ง in-app + email เหมือนกัน (uniform ตามที่ business กำหนด) ยกเว้นกรณี
flow-cancelledที่มี edge case ผู้ใช้ถูก soft-delete ก่อน → in-app เขียนไปแต่ requestor login ไม่ได้เพื่อมาเห็น (email ยังถึงปกติ เพราะ Lego capture email ไว้ก่อน teardown)
4. Flow ที่ 2 — Maker: มอบหมายงานตรวจสอบ (workflow-maker-notification)
เจ้าของ: WorkflowService (publisher) → NotificationService’s WorkflowMakerDispatcher (field-driven, ไม่ switch ตาม subject)
Payload (WorkflowMakerNotificationEvent): EventId, OccurredAt, RefNo, NotificationType, TaskSummary, DeepLinkUrl, MakerUserIds[]?, AssigneeUserId?, AssigneeEmail?, EmailTemplateKey?
Dispatch logic:
- pooled (งานเข้ากองกลาง ~20-30 maker): มี
MakerUserIdsเท่านั้น → fan-out in-app only ไม่มี email - assigned (resubmit/resume ให้ maker คนเดิม): มี
AssigneeUserId+AssigneeEmail+EmailTemplateKey="maker_task_assigned"→ in-app + email
Template maker_task_assigned ใช้ macro: MAKER_TASK_SUMMARY, FX_REF_NO, FX_DEEP_LINK (ปุ่ม “เปิดงาน” ลิงก์กลับ maker workspace)
future trigger ที่ยังไม่ publish (reviewer-kick-back / reassignment / SLA reminder) ใช้ contract เดียวกันได้เลยโดย NS ไม่ต้องแก้โค้ด — แค่ WF publish field ให้ครบ
5. Flow ที่ 3 — Invitation (invitation-event)
เจ้าของ: UserService (AsbInvitationEventPublisher) → NotificationService (InvitationEventConsumer)
Trigger: Admin สร้าง Invitation Token ใน UserService (ใช้กับ invite-only onboarding flow — ADR-0025) → publish ทันทีหลัง commit token ลง DB (fire-and-forget, publisher กลืน exception เพื่อไม่ให้ Admin เห็น error ทั้งที่ token สร้างสำเร็จแล้ว)
Payload: templateKey (มาจาก FlowDefinition, default = invitation_default), recipientEmail, invitationLink, expiryDate, flowName
Macro: INVITATION_LINK, INVITATION_EXPIRY_DATE, INVITATION_FLOW_NAME, RECIPIENT_EMAIL
Template ไม่พบ (TemplateNotFound) → dead-letter ทันที (ถือเป็น configuration error ไม่ใช่ transient)
6. Flow ที่ 4 — OTP (Entra Custom Authentication Extension)
ไม่ผ่าน UserService เลย — Microsoft Entra ยิง webhook ตรงมาที่ NotificationService:
POST /api/notification-service/v1/auth-extension/email-otp-send (AuthExtensionController, [AllowAnonymous], ต้องตอบ 200 เสมอเพื่อให้ Entra ทำงานต่อ)
Handler (HandleEmailEmailOtpSendHandler) ตัดสินใจ module ก่อนเลือก template:
- เช็คก่อนว่ามี onboarding session ที่รอ OTP อยู่หรือไม่ (
GetRefCodeAwaitingOtpAsync) - ถ้ามี → module =
onboarding→ templateotp_spa_registration - ถ้าไม่มี → module =
login→ templateotp_spa_login - (แยกอีกทาง) module =
email-contact-channel→ templateotp_spa_news_subscription— สร้างภายในระบบ ไม่ผ่าน Entra webhook
Email เป็น best-effort (log เฉยๆ ถ้า fail — Entra ต้องได้ 200 เสมอไม่ว่า email จะสำเร็จหรือไม่)
7. Known Caveats / จุดที่ต้องระวัง
- Subject naming ไม่ตรงกันในเอกสารเก่า —
flow-rework(เอกสาร) vsflow-correction-requested(โค้ดจริง) → ยึดโค้ด flow-rejectedไม่มีเหตุผล — ถ้า business ต้องการเพิ่ม ต้องแก้ทั้ง publisher (RejectAsyncส่งcorrectionNote) และ template (fx_application_rejectedเพิ่ม{{FX_CORRECTION_NOTE}})flow-cancelledin-app อาจไร้ความหมาย — requestor ถูก soft-delete/teardown ก่อน อาจ login ไม่ได้มาดู in-app (email ยังถึงปกติ) — ยังเป็น open decision ระหว่าง NS/Lego team (เก็บ in-app ต่อ หรือปรับเป็น email-only)- OTP template ซ้ำซ้อน 2 ชุด — ชุด
otp_login/otp_onboarding/otp_email_contactseed ไว้แต่ไม่มี trigger จริง (ของจริงคือotp_spa_*) ควร cleanup ทีหลัง role_assignmentและnotification_general— seed ไว้ล่วงหน้า ยังไม่มี publisher เรียกใช้งานจริง (reserved สำหรับ feature ที่ยังไม่ทำ)- OrchestratorService ไม่ส่ง email — เดิมเคย raise
AdminNotificationเอง แต่ปัจจุบัน WorkflowService เป็นเจ้าของ notification ของ maker ทั้งหมดแล้ว (comment ในโค้ดOnboardingApprovalSaga.OnResubmit)