01 — UserService (Lego Engine) — Work Breakdown
รายละเอียดงานฝั่ง Lego: mirror ของ saga ช่วง in-flight, เป็นเจ้าของเอง Draft + automation chain หลัง Approved (branch feature/sprint9/SA-1557, code-only ไม่มี migration)
อัปเดต: 2026-06-15
อ่าน 00-OVERVIEW.md ก่อน — ไฟล์นี้คือรายละเอียดสำหรับ dev ฝั่ง Lego Branch:
feature/sprint9/SA-1557(code-only, ไม่มี migration) Lego = mirror ของ saga ในช่วง in-flight; เป็นเจ้าของเองเฉพาะ Draft + automation chain หลัง Approved
บทบาทของ Lego ในระบบนี้
- Publish:
flow-submitted-for-approval(submit/resubmit),flow-cancel-requested(admin/sweep cancel) - Consume:
flow-progress-update(InReview/Reviewed),flow-approval-result(Approve/Reject/Rework/Cancel) - Set status เองตอน: Draft, Submitted, Resubmitted, Finalized, Abandoned + Cancelled (optimistic local)
- Mirror จาก orchestrator: InReview, Reviewed (progress) + Approved/Rejected/Rework/Cancelled (decision applied)
- Notify requestor (เจ้าของ — grill 2026-06-15): Lego ยิง notification หา requestor เองตอน Approve/Reject/Rework/Cancel (มี email + deep link resume); orchestrator ไม่ทำ notification
Tasks
L1 — เพิ่ม status ใหม่ (no migration)
ไฟล์: UserService04.Domain/Ports/Enums/FlowInstanceStatus.cs
InReview = 8, Reviewed = 9, Resubmitted = 10, Closed = 11
Closedใส่ enum ไว้ก่อน แต่ยังไม่ wire (เผื่ออนาคต)- verify column
Status varchar(30)รองรับ (Resubmitted= 11 ตัว) — ✅ จากFlowInstanceConfiguration.cs:17
L2 — transition methods + resubmit landing
ไฟล์: UserService04.Domain/Entities/Onboarding/FlowInstance.cs
- เพิ่ม
ApplyProgress(FlowInstanceStatus target, string? makerUsername, DateTime occurredAt)— set status + เก็บ maker +LastProgressAt(field ใหม่บน entity); raiseFlowInstanceStatusChangedEvent - แก้
TransitionToSubmitted(): จากRework→ setResubmitted(ไม่ใช่Submitted); first submit คงSubmitted- ปัจจุบัน:
Status = FlowInstanceStatus.Submittedตายตัว → แยก case ตามfrom - HistoryAction ยังเป็น
Resubmit(มีอยู่แล้ว )
- ปัจจุบัน:
- เพิ่ม metadata
LastProgressAt+AssignedMakerUsername+AssignedReviewerUsername(2-actor — grill) — เก็บในFlowSnapshotJson(jsonb เดิม) ไม่ใช่ column ใหม่ → คง code-only ไม่มี migration (แนะนำ)- InReview projection → set
AssignedMakerUsername; Reviewed projection → setAssignedReviewerUsername
- InReview projection → set
L3 — IsActiveSlot
ไฟล์: FlowInstance.cs:254-257
- เพิ่ม
InReview, Reviewed, Resubmittedเข้า active set (กิน uniqueness slot เหมือน Submitted)
L4 — ขยาย FlowTransitionValidator
ไฟล์: UserService03.Application/Features/Onboarding/Engine/FlowTransitionValidator.cs
in-flight set = { Submitted, InReview, Reviewed, Resubmitted }
Approve/Reject/Rework → current ∈ in-flight set (เดิม == Submitted)
Cancel → current ∈ in-flight set ∪ {Rework} (เดิม {Submitted, Rework})
L5 — NEW: Progress consumer
ไฟล์ใหม่: UserService02.Infrastructure/Messaging/FlowProgressUpdateConsumer.cs (sub topic flow-progress-update)
- dedup ด้วย
eventId(idempotent store เดิมที่ใช้กับ consumer อื่น) - โหลด flow → terminal guard: ถ้า status ∈ {Approved, Rejected, Cancelled, Finalized, Abandoned, Rework} → ignore
- ordering guard: ถ้า
occurredAt <= LastProgressAt→ ignore - ไม่งั้น
instance.ApplyProgress(progressStatus, maker, occurredAt)→ save - ไม่ผ่าน
FlowTransitionValidator, ไม่จุด automation
L6 — ApprovalWireMapper + CANCEL
ไฟล์: UserService02.Infrastructure/Messaging/ApprovalWireMapper.cs (+ FlowApprovalResultConsumer.cs)
- เพิ่ม case
"CANCEL"→TransitionAction.Cancel - consumer ต้องเรียก path cancel (teardown identity เหมือน admin cancel) — reuse
TransitionFlowInstanceCommand.CancelAsync
L7 — NEW: Cancel publisher
ไฟล์ใหม่: AsbFlowCancelRequestedPublisher.cs + port IFlowCancelRequestedPublisher ใน UserService04.Domain/Ports/Onboarding/
- publish
flow-cancel-requested { eventId, flowInstanceId, refNo, initiatedBy=Lego, mode=Manual|Expiry, reason }
L8 — wire cancel command ให้ publish
ไฟล์: TransitionFlowInstanceCommand.cs (CancelAsync ~
- หลัง set Cancelled local + teardown → publish L7 (เฉพาะ post-submit; Draft owner-cancel ไม่ publish)
- gate:
from ∈ post-submit set→ publish;from == Draft→ local only
L9 — Expiry sweep แยก Abandoned vs Cancelled
ไฟล์: UserService02.Infrastructure/Onboarding/FlowInstanceOrphanCleanupService.cs (verify/extend)
- Draft-stale →
Abandon()(local, ไม่ publish — ไม่มี saga) - post-submit-stale →
Cancel(mode=Expiry)+ publish L7 (มี saga ต้อง teardown Task/Workflow) - ⚠️ อย่า publish cancel สำหรับ Draft (ไม่มี saga รับ) และอย่า local-abandon post-submit (Task/Workflow จะค้าง)
L10 — Lego เป็นเจ้าของ requestor notification (grill 2026-06-15)
ไฟล์: IFlowStatusNotificationPublisher.cs + decision-channel consumer
- คงไว้ + ขยาย (ไม่ retire) — Lego ยิง notification หา requestor เอง เพราะถือ email + deep link resume
- เดิมมี
PublishApprovedAsync+PublishReworkAsync→ เพิ่มPublishRejectedAsync+PublishCancelledAsync(Reject/Cancel ยังไม่มี requestor notif ทั้งคู่) - เรียกจาก decision-channel consumer หลัง apply Approve/Reject/Rework/Cancel
- orchestrator ไม่ ทำ notification → ไม่มีปัญหายิงซ้ำ
L11 (Maker directory endpoint)+L12 (requestorUserId ใน submit event)— ตัดออก (grill): orchestrator ไม่ notify maker/requestor แล้ว → ไม่ต้องมี maker directory; Lego notify requestor เองมี requestorUserId ใน flow context อยู่แล้ว ไม่ต้องส่งผ่าน event
Test checklist (ดู 00 §Testing)
- Validator widening (decision จาก InReview/Reviewed/Resubmitted)
- IsActiveSlot รวม 3 status ใหม่
- Resubmit → status
Resubmitted(ไม่ใช่ Submitted) - Progress consumer: idempotent (eventId ซ้ำ) + ordering (occurredAt เก่า ignore) + terminal guard
- reviewer bounce: Reviewed→InReview ด้วย occurredAt ใหม่กว่า → apply
- Wire mapper CANCEL → ไม่ dead-letter
- Cancel: Draft=local only / post-submit=publish
- Sweep: Draft→Abandoned, post-submit→Cancelled+publish
- requestor notify: เพิ่ม PublishRejected/PublishCancelled ยิงถูกตอน Reject/Cancel
- คง coverage ≥90% (onboarding suite เดิม 94/94)