diff --git a/docs/superpowers/specs/2026-09-24-evaluate-collect-approve-workflow-design.md b/docs/superpowers/specs/2026-09-24-evaluate-collect-approve-workflow-design.md new file mode 100644 index 0000000..a4b4dbd --- /dev/null +++ b/docs/superpowers/specs/2026-09-24-evaluate-collect-approve-workflow-design.md @@ -0,0 +1,262 @@ +# evaluate-collect → evaluate-follow: เพิ่มขั้นตอนอนุมัติ 2 ลำดับ + Central Review (Design) + +**สถานะ:** ผ่าน design review แล้ว รอเขียน implementation plan +**อ้างอิง:** +- `docs/superpowers/specs/2026-09-07-approve-state-workflow-reference.md` — กลไก approve_state 2 ลำดับ + ส่งอนุมัติ (code "04") ต้นแบบหลัก +- `docs/superpowers/specs/2026-09-07-invest-asset-self-service-approval-design.md` — ตัวอย่างการพอร์ต pattern เดียวกันไปยัง entity ใหม่ (invest_asset) ล่าสุด +- `RequestBudgetReviewStatusComponent`/`RequestBudgetReviewController` (คำขอเงินรายได้ type_4) — ต้นแบบ central-review stage หลัง 2 ลำดับอนุมัติผ่านครบ + +## 1. บริบทและปัญหา + +หน้า `/app/research-and-evaluation/evaluate-collect` (entity `evaluate_form`) ปัจจุบันเป็น flow ระดับเดียว: +กรอกข้อมูล (`onSubmit` → PUT) → "ส่งผล" (`onSend` → PUT แล้วเรียก `GET final_active/{uid}` ทันที ตั้ง +`is_active=true`) ไม่มีแนวคิดผู้อนุมัติหรือสถานะรออนุมัติเลย — ใครก็กด "ส่งผล" ให้ finalize ได้ทันที + +หน้า `/app/research-and-evaluation/evaluate-follow` (list จาก `agency_evaluate/final_active`) ปัจจุบันมีแค่ +ปุ่ม "ส่งกลับแก้ไข" (`onSendBack`) ที่เขียนคอมเมนต์ราย indicator ผ่าน `evaluate_follow_comment` แล้วเรียก +`GET send_back/{uid}` (ตั้ง `is_active=false`) — ไม่มีปุ่ม "อนุมัติ/ตรวจสอบแล้ว" ที่ปิดจบกระบวนการ + +ผู้ใช้ต้องการเพิ่ม: +1. ปุ่ม "กำหนดผู้อนุมัติ" ที่ evaluate-collect → popup เลือกผู้อนุมัติ 2 ลำดับ (label เดียวกับคำขอเงินรายได้) +2. ปุ่ม "ส่งอนุมัติ" +3. ผู้อนุมัติแต่ละลำดับ login เข้ามากด "อนุมัติ" หรือ "ส่งแก้ไข" (ต้องกรอกรายละเอียด) +4. เมื่อผู้อนุมัติลำดับสุดท้ายอนุมัติ → เข้า evaluate-follow โดยอัตโนมัติ ที่นั่นเจ้าหน้าที่กด "ส่งแก้ไข" + หรือ "ตรวจสอบแล้ว" (จบ process) + +## 2. ของเดิมที่ยังใช้ได้ (ไม่แตะ) + +- `evaluate_form_target_indicator_agency` และกลไกกรอกข้อมูลราย indicator/หน่วยงาน — ไม่เกี่ยวกับงานนี้ +- ปุ่ม "ส่งกลับหน่วยงาน (n)" (`onSendBackAgency` → `POST send_back_agency/{uid}`) ที่ evaluate-collect — + เป็นการส่งกลับเฉพาะบาง agency ให้กรอกใหม่ **ก่อน** เข้ากระบวนการอนุมัติทั้งฟอร์ม คนละระดับกับงานนี้ + ยังใช้ได้ปกติ แต่จะถูก disable ระหว่างฟอร์มอยู่ในสถานะรออนุมัติ (ดูข้อ 6) +- ตาราง/endpoint `evaluate_form_active`, `evaluate_form_display` และ CRUD สายที่สอง (`create_form`/ + `update_form`) — ไม่แตะ +- `GET final_active/{uid}` (ตั้ง `is_active=true, active_date=now`) และ `GET send_back/{uid}` (ตั้ง + `is_active=false, active_date=null`) — logic เดิม **ยังใช้อยู่** แต่จะถูกเรียกจากจุดใหม่ (อัตโนมัติตอน + อนุมัติผ่านครบ / ตอนกด "ส่งแก้ไข" ที่ evaluate-follow) แทนที่จะเรียกตรงจากปุ่ม "ส่งผล"/"ส่งกลับแก้ไข" เดิม +- `evaluate_follow_comment` (ตาราง + service) — ยังอยู่ในระบบ ไม่ลบ แต่จะไม่มี action ใหม่เขียนเข้าไปอีก + (ดูข้อ 8 เรื่อง dead code) + +## 3. โมเดลข้อมูลใหม่ + +### 3.1 `approve_state` / `remark_history` — เพิ่มคอลัมน์ FK ใหม่ + +ตาม pattern "wide nullable FK ต่อ entity" เดิม (ห้าม reuse `request_expense_uid` เพราะ `evaluate_form` +เป็นคนละตาราง): + +```csharp +// t_approve_state, t_remark_history — เพิ่มคอลัมน์เดียวกันทั้งสองตาราง +public Guid? evaluate_form_uid { get; set; } +``` + +อัปเดต `v_approve_state` (และ view ของ remark_history ถ้ามี) ให้รวมคอลัมน์ใหม่ + nav property +`evaluate_form` แบบเดียวกับ `invest_asset`/`invest_construct`/`request_expense` + +### 3.2 `evaluate_form` — เพิ่ม 3 ฟิลด์สถานะ + +ใช้ semantics แบบ `request_expense` code "04"/"08" (pristine = null/null) ไม่ใช้แบบ `invest_asset` +(`is_draft` ตรงข้าม) เพราะ `evaluate_form` ไม่มีแนวคิด draft เดิมอยู่แล้ว เพิ่มเองใหม่ทั้งหมดจึงเลือก +รูปแบบที่ตรงไปตรงมากว่า: + +```csharp +public bool? is_sent { get; set; } // true = กด "ส่งอนุมัติ" แล้วจริง +public bool? is_approve { get; set; } // true = อนุมัติผ่านครบทุกลำดับ (รอบปัจจุบัน) +public string current_approve_name { get; set; } // denormalize ชื่อผู้อนุมัติที่กำลังรอคิว (โชว์ในตาราง) +``` + +สถานะที่เป็นไปได้ (mirror ตารางในเอกสารอ้างอิงข้อ 2 ทุกช่อง — ไม่มี `is_draft` เพราะไม่ต้องใช้): + +| is_sent | is_approve | ความหมาย | +|---|---|---| +| null | null | ยังไม่กด "ส่งอนุมัติ" (ต่อให้ตั้งผู้อนุมัติไว้แล้วก็ตาม) | +| true | null | ส่งอนุมัติแล้ว กำลังรอผู้อนุมัติแถวที่ `is_state=true` | +| false | false | ถูกส่งแก้ไขจากลำดับ 1 กลับมาหาผู้กรอกข้อมูล — แก้แล้วกด "ส่งอนุมัติ" ใหม่ได้ | +| true | false | ถูกส่งแก้ไขจากลำดับ >1 กลับมาลำดับก่อนหน้า — ยังไม่ถึงมือผู้กรอกข้อมูล | +| — | true | อนุมัติผ่านครบทุกลำดับแล้ว (จุดที่ trigger `final_active` + สร้าง `evaluate_form_review`) | + +### 3.3 ตารางใหม่ `evaluate_form_review` — central review stage (mirror `request_budget_review`) + +```csharp +public class evaluate_form_review : base_table +{ + [Key] public Guid? evaluate_form_review_uid { get; set; } + public Guid? evaluate_form_uid { get; set; } + public short review_status_id { get; set; } // 1=รอตรวจสอบ, 2=ถูกส่งแก้ไข, 3=ตรวจสอบแล้ว + public string remark { get; set; } +} +``` + +Append-only log (เหมือนต้นแบบ) — สถานะปัจจุบันคือแถวล่าสุดตาม `created_datetime` ของ `evaluate_form_uid` +นั้น ไม่มีฟิลด์ status เดี่ยวบน `evaluate_form` เอง + +Migration: SQL ใหม่ `Migrations/add_evaluate_form_review.sql` (ตาราง + view) ตามสไตล์ migration อื่นๆ +ใน `Modules/Evaluate/` + +## 4. Flow 1 — กำหนดผู้อนุมัติ (`SetCustomApprovers` สำหรับ evaluate_form) + +Mirror `RequestExpenseService.SetCustomApprovers` (เอกสารอ้างอิงข้อ 3) ทุกจุด เปลี่ยนแค่ entity/FK: + +- โหลด `t_evaluate_form` ตาม uid, ไม่พบ → error "ไม่พบรายการ" +- Guard: `entity.is_sent != null` **หรือ** `entity.is_approve != null` → error "รายการนี้เข้าสู่ + กระบวนการอนุมัติไปแล้ว ไม่สามารถกำหนดผู้อนุมัติใหม่ได้" (pristine check — null/null เท่านั้น) +- Guard: มีแถว `t_approve_state` ที่ `evaluate_form_uid==uid && is_approve==true` อยู่แล้ว → error เดียวกัน +- กรองแถวว่างทิ้ง, ไม่มีเหลือ → error "กรุณาระบุผู้อนุมัติอย่างน้อย 1 คน" +- ลบคิวเก่า (ถ้ามี, เผื่อกำหนดซ้ำก่อนส่ง) → สร้างใหม่ทั้งหมด `row_order = idx+1`, + `is_state = idx==0`, `request_approve_type = "<รหัสใหม่ — ดูข้อ 9>"`, `evaluate_form_uid = uid` +- เซ็ต `entity.current_approve_name` เป็นชื่อคนแรก, save + +**Endpoint:** `POST /api/evaluate/evaluate_form/set_approvers/{uid}` +Body: `[{ personnel_uid, display_name_th }, ...]` + +**Frontend:** `evaluate-collect-send.component` เพิ่มปุ่ม "กำหนดผู้อนุมัติ" เปิด +`SetApproversDialogComponent` เดิม (reuse ตรงๆ ไม่ต้อง copy component ใหม่ — component นี้ generic อยู่แล้ว +รับ/คืนแค่ `{personnel_uid, display_name_th}[]`) `approverLabels = ['หัวหน้าหน่วยงาน', 'รองอธิการบดี']` +ลำดับ 1 required, ลำดับ 2 optional (ตามต้นแบบ) + +## 5. Flow 2 — ส่งอนุมัติ (`SendApprove` สำหรับ evaluate_form) + +Mirror `SendApprove2` (เอกสารอ้างอิงข้อ 4): + +- โหลด entity, ไม่พบ → error +- `is_approve == true` → error "รายการนี้เข้าสู่กระบวนการอนุมัติไปแล้ว" +- ส่งได้ 2 กรณีเท่านั้น: pristine (`is_sent==null && is_approve==null`) หรือเพิ่งถูกส่งแก้ไขกลับมาถึงผู้กรอก + ข้อมูล (`is_sent==false && is_approve==false`) — อย่างอื่น block +- ต้องมีคิว `row_order==1` ตั้งไว้แล้ว (`is_state==true`) ไม่งั้น error "ยังไม่พร้อมส่งอนุมัติ กรุณา + กำหนดผู้อนุมัติก่อน" +- เซ็ต `is_sent=true, is_approve=null`, save + +**Endpoint:** `POST /api/evaluate/evaluate_form/send_approve/{uid}` (body ว่าง, ใครก็เรียกได้) + +**Frontend:** ปุ่ม "ส่งอนุมัติ" แทนที่ปุ่ม "ส่งผล" เดิม (ปุ่มเดิมที่เรียก `final_active` ตรงๆ ถูกลบออก — +การ finalize จริงย้ายไปเกิดอัตโนมัติตอนอนุมัติผ่านครบทุกลำดับ ดูข้อ 6) + +## 6. Flow 3 — อนุมัติ 2 ลำดับ (ขยาย `ApproveGetEntity` ใน `ApproveState.cs`) + +เพิ่ม branch ใหม่ในคอนโทรลเลอร์กลาง `Modules/Setting/Controllers/ApproveState.cs` +(`GET /api/setting/approve_state/approve/{uid}`) คู่ขนานกับ branch `request_expense`/`invest_asset`/ +`invest_construct` เดิม โดย: + +```csharp +if (evaluate_form != null && getResult != null) +{ + // ต่างจากต้นแบบเดิม (ที่เช็คสิทธิ์เฉพาะ row_order==1): เช็คตัวตนผู้อนุมัติทุก row_order + // — ใช้ fix ล่าสุดที่ระบบทำกับ request_expense (commit c12b93bd) เป็นค่าเริ่มต้นตั้งแต่วันแรก + // ไม่ต้องรอเจอบั๊กเดียวกันซ้ำ + if (getResult.row_order == 1 && getResult.request_approve_type == "<รหัสใหม่>" + && evaluate_form.is_sent != true) + { + return BadRequest(new { description = "ยังไม่ได้ส่งอนุมัติ กรุณากด \"ส่งอนุมัติ\" ก่อน" }); + } + var currentDisplayName = (User.Identity as ClaimsIdentity)?.FindFirst("display_name")?.Value; + if (string.IsNullOrEmpty(currentDisplayName) || currentDisplayName != getResult.display_name_th) + return StatusCode(403, new { description = "ไม่มีสิทธิ์อนุมัติ (ต้องเป็นผู้ที่ถูกกำหนดเป็นผู้อนุมัติลำดับนี้เท่านั้น)" }); + + // ปิดคิวปัจจุบัน = อนุมัติผ่าน + getResult.is_state = null; getResult.is_approve = true; + evaluate_form.current_approve_name = null; + // ... save ... + + var remaining = /* แถวที่ evaluate_form_uid==uid && round_no==currentRound && is_approve!=true */; + if (!remaining.Any()) + { + evaluate_form.is_approve = true; + evaluate_form.is_active = true; // = logic เดิมของ final_active + evaluate_form.active_date = DateTime.Now; + // สร้าง evaluate_form_review ใหม่ review_status_id=1 (รอตรวจสอบ) + } + else + { + // เดินหน้า is_state=true ไปแถว row_order+1, เซ็ต current_approve_name + } +} +``` + +**Endpoint:** ใช้ตัวเดิม `GET /api/setting/approve_state/approve/{uid}` (endpoint กลาง ไม่เพิ่มใหม่) + +**Frontend:** `evaluate-collect-send.component` เพิ่มปุ่ม "อนุมัติ" / "ส่งแก้ไข" แสดงเฉพาะเมื่อ +`current_approve_name` ตรงกับชื่อผู้ login อยู่ (เทียบแบบ `request-expense-approval.ts`, port มาเป็นไฟล์ +util คู่ขนานสำหรับ `evaluate_form`) — ปุ่ม "อนุมัติ" เรียก endpoint approve ตรงๆ, ปุ่ม "ส่งแก้ไข" เปิด Swal +textarea บังคับกรอก แล้วบันทึก `t_remark_history` (`evaluate_form_uid`, `round_no` ปัจจุบัน) ก่อนเรียก +reject (ข้อ 7) — mirror `onApprove4`/`onReject4` ทุกจุด รวมตาราง remark history ใต้ปุ่มด้วย + +## 7. Flow 4 — ส่งแก้ไขระหว่างอนุมัติ (ขยาย `RejectGetEntity`) + +ตามที่เลือกไว้ ("ย้อนไปทีละลำดับ") — mirror `RejectGetEntity` branch `request_expense` เป๊ะ +(เอกสารอ้างอิงข้อ 6): + +- หา index ของแถว `is_state==true` ปัจจุบันในคิว (เรียงตาม `row_order`) +- ถ้า `index > 0`: ปิดแถวปัจจุบัน (`is_state=null, is_approve=null`), เปิดแถวก่อนหน้า + (`is_state=true, is_approve=null`) — ยังไม่ถึงมือผู้กรอกข้อมูล +- ถ้า `index == 0` (ลำดับ 1 ปฏิเสธเอง): **ไม่แตะ** `approve_state` (ตาม pattern เดิม ทิ้ง `is_state` + ของแถวลำดับ 1 ค้างไว้เป็น true — โค้ดฝั่งอ่านสถานะต้องเช็ค `is_sent` ควบคู่เสมอ ตามคำเตือนในเอกสาร + อ้างอิงข้อ 2/6/7) +- เซ็ต `evaluate_form.is_approve=false`; ถ้า `index==0` เพิ่ม `evaluate_form.is_sent=false` ด้วย + (คืนสถานะ "ยังไม่ได้ส่งอนุมัติ" ให้ส่งใหม่ได้หลังแก้ไข) ถ้า `index>0` ไม่แตะ `is_sent` (ยังเป็น true) +- อัปเดต `current_approve_name` จากแถวที่ `is_state==true` ใหม่ (ถ้ามี) + +**Endpoint:** ใช้ตัวเดิม `GET /api/setting/approve_state/reject/{uid}` + +## 8. Flow 5 — Central Review Stage (evaluate-follow) — mirror `request_budget_review` + +**Controller/Service ใหม่** `Modules/Evaluate/Controllers/EvaluateFormReview.cs` + +`Modules/Evaluate/Services/EvaluateFormReview.cs`, route `api/evaluate/evaluate_form_review`: + +| Endpoint | พฤติกรรม | +|---|---| +| `GET status/{evaluate_form_uid}` | derive `not_ready`/`pending`/`returned`/`reviewed` จาก `evaluate_form.is_approve` + แถวล่าสุดใน `evaluate_form_review`; คืน `history[]` จาก `remark_history` ที่ `evaluate_form_uid` นี้ (แบบ `by`=`updated_name`, `detail`=`remark_history_name_th`) | +| `GET dashboard` | group รายการที่ `is_approve==true` เป็น `pending`/`returned`/`reviewed` ตาม `evaluate_form_review` ล่าสุด | +| `POST return_for_edit/{uid}` (body `{remark}`) | **"ส่งแก้ไข"**: require `is_approve==true` && remark ไม่ว่าง → เปิด `is_state=true, is_approve=null` ที่แถว **`row_order` สูงสุด** (ผู้อนุมัติคนสุดท้าย — ไม่ย้อนกลับไปลำดับ 1) → เซ็ต `evaluate_form.is_approve=false, current_approve_name=<ชื่อแถวนั้น>` (ไม่แตะ `is_sent`, ยังเป็น true) → insert `t_remark_history` (comment) + `t_evaluate_form_review` (`review_status_id=2`) | +| `POST mark_reviewed/{uid}` | **"ตรวจสอบแล้ว"**: require `is_approve==true` && สถานะล่าสุดเป็น pending (หรือยังไม่มี log เลย สำหรับ record เก่า) → insert `t_evaluate_form_review` (`review_status_id=3`) — จบ process | + +**Frontend:** สร้าง `EvaluateFormReviewService` (มิเรอร์ `RequestBudgetReviewService` 4 endpoint) และ +คอมโพเนนต์ใหม่มิเรอร์ `RequestBudgetReviewStatusComponent` ฝังใน `evaluate-follow-send.component` — +**แทนที่** ปุ่ม "ส่งกลับแก้ไข" เดิมทั้งหมด (ลบการเรียก `evaluate_follow_comment.add` + `send_back` ออกจาก +ปุ่มนี้) ด้วย: +- ปุ่ม "ส่งแก้ไข" (แสดงเมื่อ status `pending`) → Swal textarea บังคับกรอก → `returnForEdit` +- ปุ่ม "ตรวจสอบแล้ว" (แสดงเมื่อ status `pending`) → confirm "ยืนยันว่าตรวจสอบแล้วใช่หรือไม่" → `markReviewed` +- ตาราง history ยุบ/ขยายได้ (mirror `toggleHistory()`) + +`evaluate-follow-index.component` เพิ่ม bucket/tab สถานะ (รอตรวจสอบ/ถูกส่งแก้ไข/ตรวจสอบแล้ว) มิเรอร์ +`RequestBudgetReviewDashboardComponent` — list source ยังคงอ่านจาก `agency_evaluate/final_active` เดิม +(รายการเข้ามาที่นี่เมื่อไรก็ต่อเมื่อ `evaluate_form.is_approve=true` ซึ่งตอนนี้ trigger `is_active=true` +ให้อัตโนมัติจาก flow อนุมัติแล้ว — ไม่ต้องเปลี่ยน query เดิม) + +## 9. รหัส `request_approve_type` ใหม่ — ต้องยืนยันสดก่อนเขียนโค้ด + +โค้ดที่ใช้แล้ว ณ ตอนเขียนเอกสารนี้: `"01"`-`"08"` (ดูตารางในเอกสารอ้างอิง + type_4 คำขอแผ่นดิน) คาดว่า +รหัสถัดไปคือ `"09"` แต่**ต้องเช็คแบบสดกับฐานข้อมูล production เป็น task แรกของ implementation plan** +(`SELECT DISTINCT request_approve_type FROM approve_state` + ตาราง `request_approve_type` ถ้ามี) ก่อน +เขียนโค้ดจริง เหมือนที่ทำกับ invest-asset self-service (ข้อ 4 ของสเปกนั้น) + +## 10. ความเสี่ยง/จุดต้องระวัง (สืบทอดจากเอกสารอ้างอิง) + +1. **`display_name_th` string matching แบบ exact** — ระบบเดิม match ตัวตนผู้อนุมัติด้วยการเทียบ string + ล้วนๆ (เจอเคสจริงที่ชื่อเดียวกันมี/ไม่มีวรรคแล้ว match ไม่ติด) เวอร์ชันใหม่นี้จะ**ตามระบบเดิมไปก่อน** + เพื่อความสอดคล้อง ไม่เปลี่ยนกลไกนี้เฉพาะจุด — ถ้าเจอปัญหาจริงหลัง deploy ค่อยพิจารณาย้ายไป match ด้วย + `personnel_uid` ทั้งระบบแยกเป็นงานอื่น (นอกขอบเขต) +2. **เช็คสิทธิ์ทุก `row_order` ตั้งแต่วันแรก** (ไม่ใช่แค่ row_order==1 แบบต้นแบบเดิมก่อนแก้) — ตามที่ระบุ + ในข้อ 6 ป้องกันบั๊กเดิมที่เพิ่งแก้ไปกับ `request_expense` +3. **ปุ่ม "ส่งกลับหน่วยงาน" ที่ evaluate-collect ต้อง disable ระหว่างรออนุมัติ** (`is_sent==true && + is_approve!=true`) — ไม่งั้นข้อมูลจะถูกแก้ระหว่างที่กำลังอยู่ในคิวอนุมัติ ทำให้ข้อมูลที่อนุมัติไปแล้ว + กับข้อมูลจริงไม่ตรงกัน +4. **`evaluate_follow_comment` กลายเป็น dead write-path** จากงานนี้ (ยังอ่านแสดงได้ ถ้ามีข้อมูลเก่า แต่ไม่มี + action ใหม่เขียนเพิ่ม) — ไม่ลบทิ้งในรอบนี้ ตามที่ตกลงไว้ นอกขอบเขต cleanup +5. **ต้องเพิ่ม guard `is_sent` ในจุดเช็คของ `ApproveGetEntity`** ให้ครอบคลุม code ใหม่ตั้งแต่แรก — ถ้าลืม + ระบบจะข้ามการเช็ค "ส่งอนุมัติหรือยัง" ไปเลย เพราะ `SetCustomApprovers` seed `is_state=true` ที่แถวแรก + ตั้งแต่ก่อนกด "ส่งอนุมัติ" ด้วยซ้ำ +6. **ห้ามไว้ใจฟิลด์ denormalize ชื่อหน่วยงาน/ปีงบบน `evaluate_form` เอง** ถ้ามี — join จากตารางต้นทางเสมอ + เวลาทำ dashboard (บทเรียนเดียวกับ `request_expense.faculty_name_th`) + +## 11. Testing + +Methodology เดียวกับที่ใช้ทำ income-budget-review-stage/request-budget-approve-workflow ใน session ก่อน +หน้า: สร้างข้อมูลทดสอบสังเคราะห์ผ่าน SQL (evaluate_form + agency data ที่ครบเงื่อนไข), ยิง endpoint จริง +ด้วย JWT ทดสอบของผู้ใช้ 3 บทบาท (ผู้กรอกข้อมูล, ผู้อนุมัติลำดับ 1, ผู้อนุมัติลำดับ 2/เจ้าหน้าที่ follow), +ตรวจผลผ่าน SQL หลังแต่ละ step (`approve_state`, `evaluate_form.is_sent/is_approve/is_active`, +`evaluate_form_review`, `remark_history`), ลบข้อมูลทดสอบทิ้งหลังตรวจเสร็จ — ไม่มี automated test suite +ในโปรเจกต์นี้ + +Path ที่ต้องครอบคลุม: (1) happy path ครบ 2 ลำดับ → evaluate-follow → ตรวจสอบแล้ว, (2) ลำดับ 2 ส่งแก้ไข +→ กลับลำดับ 1, (3) ลำดับ 1 ส่งแก้ไข (ปฏิเสธตัวเอง) → กลับผู้กรอกข้อมูล → ส่งอนุมัติใหม่, (4) evaluate-follow +ส่งแก้ไข → เปิดคิวลำดับสุดท้ายใหม่ → อนุมัติอีกรอบ → กลับเข้า evaluate-follow อีกครั้ง, (5) guard: กด +approve/reject โดยไม่ใช่ชื่อที่ตรงคิว → ต้อง 403, (6) กด approve ก่อนกด "ส่งอนุมัติ" → ต้อง 400