Private Docs

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); raise FlowInstanceStatusChangedEvent
  • แก้ TransitionToSubmitted (
    ):
    จาก Rework → set Resubmitted (ไม่ใช่ 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 → set AssignedReviewerUsername

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-staleAbandon() (local, ไม่ publish — ไม่มี saga)
  • post-submit-staleCancel(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)