Private Docs

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)
OrchestratorServiceSaga กลางที่ประสาน 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 โดยตรง (แก้ตัวเหล่านี้)

ลำดับใน flowTemplate Keyจุดที่เกิดใน onboarding
1. เริ่มสมัคร (invite-only)invitation_defaultAdmin เชิญ requestor เข้าสมัคร
2. ยืนยันตัวตน (identity step)otp_spa_registrationOTP ตอนลงทะเบียน/ยืนยันอีเมลระหว่างกรอกใบสมัคร
3. ส่งงานให้ maker ตรวจmaker_task_assignedมอบหมาย/resubmit งานให้ maker คนใดคนหนึ่งตรวจใบสมัคร
4a. ผลลัพธ์ = อนุมัติfx_application_approvedrequestor ได้รับอนุมัติ
4b. ผลลัพธ์ = ต้องแก้ไขfx_application_reworkrequestor ถูกขอให้แก้ไขใบสมัคร
4c. ผลลัพธ์ = ไม่อนุมัติfx_application_rejectedrequestor ถูกปฏิเสธ
4d. ผลลัพธ์ = ยกเลิกfx_application_cancelledrequestor ยกเลิกใบสมัคร/หมดอายุ

สรุป: มี 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ผู้ publishTransportMacro หลัก
fx_application_approvedแจ้ง requestor ว่าใบสมัครอนุมัติแล้วsubject flow-approvedUserService (AsbFlowStatusNotificationPublisher)ASB topic flow-status-notificationFX_REF_NO
fx_application_reworkแจ้ง requestor ว่าใบสมัครต้องแก้ไขsubject flow-correction-requestedUserServiceASB topic flow-status-notificationFX_REF_NO, FX_CORRECTION_NOTE
fx_application_rejectedแจ้ง requestor ว่าใบสมัครไม่อนุมัติsubject flow-rejectedUserServiceASB topic flow-status-notificationFX_REF_NO (ไม่มีเหตุผล — CorrectionNote ส่ง null เสมอ)
fx_application_cancelledแจ้ง requestor ว่าใบสมัครถูกยกเลิกsubject flow-cancelledUserServiceASB topic flow-status-notificationFX_REF_NO, FX_CORRECTION_NOTE (= เหตุผลยกเลิก)
maker_task_assignedแจ้ง maker ว่ามีงานมอบหมายให้ตรวจ (resubmit/resume)EmailTemplateKey="maker_task_assigned" เมื่อมี AssigneeUserId+AssigneeEmailWorkflowServiceASB topic workflow-maker-notificationMAKER_TASK_SUMMARY, FX_REF_NO, FX_DEEP_LINK
invitation_defaultส่งลิงก์เชิญเข้าสมัคร (invite-only onboarding)Admin สร้าง Invitation Token ใน UserServiceUserService (AsbInvitationEventPublisher)ASB topic invitation-eventINVITATION_LINK, INVITATION_EXPIRY_DATE, INVITATION_FLOW_NAME, RECIPIENT_EMAIL (จริง ๆ ใช้ templateKey ที่ผูกกับ FlowDefinition ได้ เช่น invitation_fxinvitation_default คือค่า placeholder เริ่มต้น)
otp_spa_registrationOTP ตอนลงทะเบียน/ยืนยันอีเมลระหว่าง onboardingwebhook Entra, module=onboardingMicrosoft Entra → HTTP webhookPOST .../auth-extension/email-otp-sendOTP_CODE, REF_CODE, EXPIRY_MINUTES
⚠️otp_spa_loginOTP ตอน login ทั่วไป (ไม่ใช่แค่ onboarding)webhook เดียวกัน, module=loginMicrosoft Entra → HTTP webhookendpoint เดียวกันOTP_CODE, EXPIRY_MINUTES
⚠️otp_spa_news_subscriptionOTP ยืนยัน email contact channel (ไม่เกี่ยวกับ onboarding)module=email-contact-channelNotificationService (internal)Internal callOTP_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 แบบเก่า vs otp_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)NotificationTypeEmail Template
flow-approvedใบสมัคร FX Online ได้รับการอนุมัติSuccessfx_application_approved
flow-correction-requestedใบสมัคร FX Online ต้องการการแก้ไขWarningfx_application_rework
flow-rejectedใบสมัคร FX Online ไม่ได้รับการอนุมัติDangerfx_application_rejected
flow-cancelledใบสมัคร FX Online ถูกยกเลิกInfofx_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:

  1. เช็คก่อนว่ามี onboarding session ที่รอ OTP อยู่หรือไม่ (GetRefCodeAwaitingOtpAsync)
  2. ถ้ามี → module = onboarding → template otp_spa_registration
  3. ถ้าไม่มี → module = login → template otp_spa_login
  4. (แยกอีกทาง) module = email-contact-channel → template otp_spa_news_subscription — สร้างภายในระบบ ไม่ผ่าน Entra webhook

Email เป็น best-effort (log เฉยๆ ถ้า fail — Entra ต้องได้ 200 เสมอไม่ว่า email จะสำเร็จหรือไม่)


7. Known Caveats / จุดที่ต้องระวัง

  1. Subject naming ไม่ตรงกันในเอกสารเก่าflow-rework (เอกสาร) vs flow-correction-requested (โค้ดจริง) → ยึดโค้ด
  2. flow-rejected ไม่มีเหตุผล — ถ้า business ต้องการเพิ่ม ต้องแก้ทั้ง publisher (RejectAsync ส่ง correctionNote) และ template (fx_application_rejected เพิ่ม {{FX_CORRECTION_NOTE}})
  3. flow-cancelled in-app อาจไร้ความหมาย — requestor ถูก soft-delete/teardown ก่อน อาจ login ไม่ได้มาดู in-app (email ยังถึงปกติ) — ยังเป็น open decision ระหว่าง NS/Lego team (เก็บ in-app ต่อ หรือปรับเป็น email-only)
  4. OTP template ซ้ำซ้อน 2 ชุด — ชุด otp_login/otp_onboarding/otp_email_contact seed ไว้แต่ไม่มี trigger จริง (ของจริงคือ otp_spa_*) ควร cleanup ทีหลัง
  5. role_assignment และ notification_general — seed ไว้ล่วงหน้า ยังไม่มี publisher เรียกใช้งานจริง (reserved สำหรับ feature ที่ยังไม่ทำ)
  6. OrchestratorService ไม่ส่ง email — เดิมเคย raise AdminNotification เอง แต่ปัจจุบัน WorkflowService เป็นเจ้าของ notification ของ maker ทั้งหมดแล้ว (comment ในโค้ด OnboardingApprovalSaga.OnResubmit)