15 KiB
sub-implementer-backend-3 — S161 W3 ĐỢT-3 IMPLEMENT (6 test RED → GREEN)
Ghi TỪNG MỤC trong lúc làm (bài #53: chỉ thứ trên đĩa mới sống). Index 3 — KHÔNG đè
sub-implementer-backend-0.md(đợt-1 skeleton). Scope MINE:Infrastructure/Services/ContractSigningPlanWorkflowService.cs(điền thân)
- 1 dòng seed trong
tests/.../ContractSigningPlanApprovalTests.cs(lead UỶ QUYỀN riêng lượt này, chỉ khi lỗi ở SEED). KHÔNG chạm:fe-*/**· slnx · commit/push · agent-memory · store_memory.
§0 — ĐO RED TRƯỚC KHI GÕ (tự chạy, không tin báo cáo)
dotnet test tests/SolutionErp.Infrastructure.Tests --filter FullyQualifiedName~ContractSigningPlanApprovalTests
| # | Test | RED vì | Bằng chứng |
|---|---|---|---|
| T1 | ApproveV2_OneApproverInLevel_Advances_WithoutRequiringBothSignatures |
NotImplementedException: W3 đợt-3: approve |
ContractSigningPlanWorkflowService.cs:113 |
| T2 | ApproveV2_NewStep_ResetsLevelOrderToOne |
NotImplementedException: W3 đợt-3: approve |
:113 (stack ...Tests.cs:422) |
| T3 | Finalize_SetsApprovedAmount_OnlyForLinesOfThatPlan |
NotImplementedException: W3 đợt-3: approve |
:113 (stack ...Tests.cs:501) |
| T4 | Inbox_OnlyPendingLevelApprover_SeesPlan |
ASSERT — Expected ... to be a collection with 1 item(s) ..., but found an empty collection (Tests.cs:574) |
KHÔNG chạm service — xem §4 |
| T5 | ApproveV2_TwiceSameLevel_UpsertsSingleOpinionRow |
NotImplementedException: W3 đợt-3: approve |
:113 |
| T6 | SendBack_Resubmit_RestartsFromStepOneLevelOne |
NotImplementedException: W3 đợt-3: submit |
:108 (stack ...Tests.cs:689) |
Failed: 6, Passed: 0, Total: 6 — khớp RED-evidence lead đưa (5 con NotImplemented + 1 con đỏ assertion).
§1 — CẤU TRÚC ĐÃ VIẾT (file Infrastructure/Services/ContractSigningPlanWorkflowService.cs)
2 dòng #pragma warning disable/restore CS9113 ĐÃ XOÁ — cả 4 dependency giờ dùng thật:
db (mọi truy vấn) · currentUser (actor + roles + fallback tên) · dateTime (SignedAt/ApprovedAt) · notifications (2 đường báo).
| Thành phần | Vai trò | Khuôn nguồn |
|---|---|---|
TransitionAsync |
rào action → rào độ dài ý kiến → rào đăng nhập → nạp phiếu (Include(Lines)) → switch 4 nhánh → 1 SaveChangesAsync → dựng response 3 field từ chính entity |
— |
SubmitAsync |
phase-guard {Nháp, Trả lại} · workflow-pin-guard · EnsureCanSubmitAsync · reset con-trỏ (0,1) · log · notify Cấp 1 |
spec §②-1 |
ApproveV2Async |
ResolvePointer → ResolveActingLevel (OR-of-N) → UpsertLevelOpinionAsync → advance/finalize → log → notify Cấp kế |
ContractWorkflowService.cs:217-394 |
ApplyApprovedValuesOnFinalize |
ApprovedAmount ??= ProposedAmount mọi Line của phiếu |
spec §②-3(a) |
ReturnOrRejectAsync |
1 thân dùng chung 2 action, khác nhau đúng targetPhase + chữ thông báo |
spec §②-3 |
LogSigningPlanTransitionAsync |
ghi 2 bảng KHKK-riêng + notify người soạn | spec §②-4 + ContractWorkflowService.cs:407-427 |
NotifyPendingApproversAsync |
vế THÊM spec §②-6 — báo đích danh NV Cấp đang chờ | PurchaseEvaluationWorkflowService.cs:1243-1289 |
ResolvePointer / LoadStepsAsync / ResolveActingLevel / LevelsOf / ResolveActorFullNameAsync / Truncate |
helper | :228-266, :433-442 |
4 điểm GIỮ của hợp-đồng — thi hành đủ 4:
- Con-trỏ ĐÔI:
ResolvePointerđọcCurrentWorkflowStepIndexnhư INDEX vào list đã sort (steps[idx], không phảiOrder) +CurrentApprovalLevelOrder; sang Bước mới gán thẳng= 1. GroupBy(l => l.Order)—ResolvePointer, y khuôn:246. KHÔNGSelectMany/flatten (bẫy Proposal).- OR-of-N —
ResolveActingLevel:pendingLevelGroup.FirstOrDefault(l => l.ApproverUserId == actorId); có tên là qua, KHÔNG đòi đủ chữ ký cả nhóm. - UPSERT ý kiến —
UpsertLevelOpinionAsync, 1 row/cặp (Plan × Level),SignedByUserId= người ký THẬT.
2 lệnh CẤM — tuân thủ, kiểm được:
grep -n "IChangelogService\|LogWorkflowTransitionAsync" ContractSigningPlanWorkflowService.cs→ 0 hit (dep vẫn CỐ Ý vắng ctor = rào FK-547 bằng vắng-mặt, không bằng trí nhớ).grep -n "SelectMany" ContractSigningPlanWorkflowService.cs→ 0 hit (không dính Proposal-flatten).
KHÔNG port (khai rõ): AllowApproverFinalize · CeoApprovalThreshold · AllowApproverSkipToFinal (:318-352 nguồn) · 4 return-mode per-level (ApprovalWorkflow.cs:116-129) → grep -n "AllowApprover\|CeoApprovalThreshold\|skipToFinal" trong file = 0 hit. W3 chỉ đường THƯỜNG (anh phán "PHÁ VỠ" @S155).
§2 — 🔴 CHOKE-POINT FINALIZE: 1 SITE, KHÔNG CÓ SITE THỨ 2 (đo, không đoán)
Lead dặn: "nếu mày không port admin-override thì KHAI RÕ 0-site-thứ-2". Khai:
- Không port nhánh admin-override kiểu nguồn copy (
ContractWorkflowService.cs:190-193gáncontract.Phase = targetPhaseQUA BIẾN). Lý do cấu trúc, không phải quên: hợp-đồng transition KHKK chỉ nhận 4 action cố định, KHÔNG có tham sốtargetPhase⇒ không tồn tại đường "Admin gán thẳng phase tuỳ ý". Admin muốn kết thúc phiếu vẫn phải điapprove(chỉ được miễn vế "có tên trong Cấp" tạiResolveActingLevel) ⇒ đi xuyên đúng choke-point. Đó CHÍNH LÀ write-path 2 mà T3 đo — và nó xanh. - Đo trên đĩa (2 lệnh, khớp nhau):
grep -rn "Phase = ContractSigningPlanPhase.DaDuyet" src/Backend --include=*.cs→ 1 hit =ContractSigningPlanWorkflowService.cs(nhánh terminal, ngay SAUApplyApprovedValuesOnFinalize).grep -rn "ApplyApprovedValuesOnFinalize" src/Backend --include=*.cs→ 2 hit = 1 khai báo helper + 1 call-site.- Đối chứng toàn module:
grep -rn "Phase = ContractSigningPlanPhase\." src/Backend --include=*.cs→ 4 hit:Features.cs:373(tạo phiếu → DangSoanThao) + 3 hit trong service (ChoDuyet / DaDuyet /= targetPhasecủa return-reject, màtargetPhaseở đó CHỈ nhận TraLai|TuChoi — 2 call-site literal ởTransitionAsync).
- RULE-comment dựng theo khuôn
ContractFeatures.cs:356-360(LUẬT +grep -rntự kiểm + KHÔNG đếm số site) — KHÔNG dùng khuônPEWS:1008(dòng đó tự đếm "4 site" trong khi thực 5). - Vế
AND PlanId = @id: bảo đảm bằng QUAN HỆ, không bằng nhớ viếtWhere.plan.Linesnạp quaIncludetrên đúng 1 phiếu ⇒ trong service không tồn tại câudb.ContractSigningPlanLines.Where(...)nào để mà quên vế lọc (grep -n "ContractSigningPlanLines"trong file = 0 hit). T3 có phiếu chứng nhânplanBbất động qua CẢ HAI write-path.
§3 — SAVE-ĐIỂM-NÀO (khai theo yêu cầu lead)
ĐÚNG MỘT await db.SaveChangesAsync(ct), ở CUỐI TransitionAsync, sau khi nhánh action chạy xong.
- Khác nguồn copy:
ContractWorkflowService.TransitionAsynckhông tự save (:537"caller có downstream save") vì nó được gọi từ MediatR handler. Ở đây service LÀ điểm vào — controller gọi thẳng (ContractSigningPlansController.cs:211), không có caller nào save hộ ⇒ không save = transition rơi vào hư không. - 1 transition = 1 unit-of-work: con-trỏ +
ContractSigningPlanApprovals+ContractSigningPlanChangelogs+LevelOpinion+ giá chốt +Notificationscùng sống hoặc cùng chết. Được phép làm vậy vìNotificationService.cs:7-8CỐ Ý không tựSaveChanges("caller's unit of work flushes both"). - Hệ quả đo được: mọi nhánh ném (Forbidden/Conflict) xảy ra TRƯỚC lượt save duy nhất ⇒ bị chặn thì không để lại vết. T1 assert đúng điều này: sau 2 lần bị chặn,
p3con-trỏ bất động +LevelOpinionsrỗng. - KHÔNG port khối
ExecuteUpdateAsyncmark-read chuông cũ của PE (PurchaseEvaluationWorkflowService.cs:1227-1241): nó ghi thẳng DB NGOÀI unit-of-work (transition ném sau đó thì mark-read đã commit rồi). Ngoài spec §②-6. Khai để lead quyết có kéo về sau không.
§4 — 🔧 SỬA TEST: ĐÚNG 1 THAM SỐ SEED (khai từng chỗ + lý do)
Chỗ sửa duy nhất: tests/SolutionErp.Infrastructure.Tests/Application/ContractSigningPlanApprovalTests.cs, phiếu pendingLevel1 trong Inbox_OnlyPendingLevelApprover_SeesPlan:
- ContractSigningPlanPhase.ChoDuyet, 0, 2, oneLine); // FAULT-INJECT tạm
+ ContractSigningPlanPhase.ChoDuyet, 0, 1, oneLine);
- Lỗi ở SEED, không ở assert. Chính comment trong test tự khai
// FAULT-INJECT tạm— vết của lượt tự-thử-lưới còn sót lại. Với0, 2, cả hai phiếu cùng đứng Cấp 2 ⇒ hộp thư A/B (approver Cấp 1) rỗng và C thấy 2 phiếu. Tên biếnpendingLevel1/pendingLevel2cũng nói rõ ý định gốc là Cấp 1 / Cấp 2. - KHÔNG nới một assert nào — 0 ký tự nào trong các câu
Should()bị đổi. Kiểm được:git difffile test chỉ có 1 dòng đổi giá trị2→1+ 1 khối comment giải thích thêm. - KHÔNG sửa mã sản phẩm để chiều test này:
GetMyContractSigningPlanInboxQueryHandler/ResolveV2InboxIdsAsync(W2) 0 dòng đổi ở đợt này. - Chứng minh 2 chiều: sau khi sửa seed, T4 xanh mà 5 test kia không đụng gì đến nó — và trước khi sửa, 5 test kia đã xanh sẵn với logic mới (xem §5 lượt-1), tức T4 đỏ hoàn toàn độc lập với logic tôi viết.
§5 — SỐ ĐO (build + test)
| Lượt | Lệnh | Kết quả |
|---|---|---|
| Build | dotnet build SolutionErp.slnx |
Build succeeded · 0 Warning(s) · 0 Error(s) · 42.93s · 6/6 project (pragma CS9113 đã xoá mà vẫn 0 warning ⇒ 4 dep dùng THẬT) |
| Test filter lượt-1 (trước khi sửa seed) | --filter ...ContractSigningPlanApprovalTests |
Failed: 1, Passed: 5 — con đỏ duy nhất = T4 Inbox (assert Tests.cs:574), 5 con logic mới XANH ngay lượt đầu |
| Test filter lượt-2 (sau khi sửa seed) | như trên | Failed: 0, Passed: 6, Total: 6 ✔ |
Bảng RED → GREEN từng con
| # | Test | RED (đầu đợt) | GREEN | Vế acceptance được nó khoá |
|---|---|---|---|---|
| T1 | ApproveV2_OneApproverInLevel_Advances_WithoutRequiringBothSignatures |
NotImplemented :113 |
✔ | OR-of-N 4 vế (a)(b)(c)(d) + phiếu bị chặn bất động |
| T2 | ApproveV2_NewStep_ResetsLevelOrderToOne |
NotImplemented :113 |
✔ | con-trỏ ĐÔI, reset Cấp ×2 lần, Order 5/15/25 ≠ index, terminal 2 con-trỏ null |
| T3 | Finalize_SetsApprovedAmount_OnlyForLinesOfThatPlan |
NotImplemented :113 |
✔ | choke-point 2 write-path (approver + Admin) · ??= giữ 300 · phiếu chứng nhân sạch |
| T4 | Inbox_OnlyPendingLevelApprover_SeesPlan |
assert (seed sai) | ✔ | đúng-Cấp / khác-Cấp / ngoài-quy-trình / người-soạn / phiếu nháp |
| T5 | ApproveV2_TwiceSameLevel_UpsertsSingleOpinionRow |
NotImplemented :113 |
✔ | UPSERT 1 row · placeholder ý kiến rỗng · SignedAt từ IDateTime · latest-write-wins |
| T6 | SendBack_Resubmit_RestartsFromStepOneLevelOne |
NotImplemented :108 |
✔ | trả lại toàn phần + gửi lại chạy LẠI từ (Bước 1, Cấp 1) + vòng mới là thật |
§6 — FULL SUITE
[LEAD điền on-behalf] dotnet test SolutionErp.slnx → Domain 45/45 + Infra 535/535 = 580/0 (574 + 6, đích ĐẠT). Build 0W/0E.
§7 — QUYẾT ĐỊNH KHAI ĐỂ LEAD PHÂN XỬ (không tự nuốt)
| # | Quyết | Vì sao | Đảo lại tốn gì |
|---|---|---|---|
| Q1 | Guard trình nhận thêm vế DrafterUserId == actor (spec chữ đen: CreatedBy == actor ∨ DeptManager ∨ Admin) |
2 cột trỏ CÙNG một người ở đường tạo W2 (Features.cs:372 gán DrafterUserId = actorId; CreatedBy do AuditingInterceptor ghi từ cùng ICurrentUser) ⇒ không mở thêm ai. Nhưng phiếu sinh ngoài CQRS (seed/import chạy dưới danh nghĩa hệ thống) có CreatedBy = null ⇒ đọc mỗi cột đó thì chính người soạn bị chặn trình phiếu của mình |
xoá 1 vế ` |
| Q2 | KHÔNG đặt SlaDeadline |
W3 không định nghĩa số ngày SLA cho KHKK; bê "7 ngày" của Contract sang = bịa một hạn chót rồi cho FE hiển thị như thật. Cột vẫn null y như sau W2 | thêm 1 dòng/nhánh |
| Q3 | Trần ý kiến 1000 ký tự → ConflictException (409) |
đường này KHÔNG qua MediatR ⇒ 0 FluentValidation; cột chặt nhất chứa nguyên văn là ContractSigningPlanApprovals.Comment = 1000 (...ApprovalConfiguration.cs:26). Để lọt = SQL Server 2628 (chuỗi bị cắt) → 500 khó hiểu. Chọn 409 cho ĐỒNG BỘ với "Hành động không hợp lệ." của đợt-1 (đúng nghĩa hơn là 400, nhưng đổi thì đổi cả 2 chỗ) |
đổi sang ValidationException (→400) 1 chỗ |
| Q4 | return/reject dùng CÙNG vế quyền với approve (NV Cấp đang chờ ∨ Admin) |
trả lại/từ chối là quyết định của người đang giữ lượt; rào bằng CHÍNH hàm ResolveActingLevel ⇒ 2 đường không thể lệch nhau về sau. (Khuôn Contract nới hơn: FE cho mọi user bấm Trả lại/Từ chối — cố ý KHÔNG bắt chước) |
nới 1 hàm |
| Q5 | return/reject KHÔNG ghi LevelOpinion |
bảng ý kiến là chữ ký DUYỆT (ContractSigningPlanLevelOpinion.cs:11 "Reject (TraLai/TuChoi) KHÔNG sync"); lý do trả lại nằm ở Approvals + Changelogs. T5 phụ thuộc vế này (nếu return đẻ row thì ContainSingle vỡ) |
— (đổi = vỡ T5) |
| Q6 | Notify người soạn loại trường hợp chính họ bấm; notify approver Cấp kế loại actor + người soạn | mirror bài UAT S86 ở PE (PEWS:1269-1274): người tạo phiếu không nhận chuông "cần bạn duyệt" cho phiếu của mình |
bỏ 1 vế Where |
Ambiguity còn lại (KHÔNG tự quyết):
- Chuông "cần bạn duyệt" CŨ của Cấp trước không được mark-read khi phiếu rời Cấp (PE đã vá S96 bằng
ExecuteUpdateAsync). KHKK sẽ gặp đúng bug đó khi UAT — nhưng vá nó = ghi ngoài unit-of-work, ngoài spec §②-6 ⇒ để lead quyết. ContractSigningPlanApproval.ApprovedByUserIdluôn có giá trị (không có đường "system/SLA auto" ở W3) — cột nullable giữ nguyên cho tương lai.
§8 — FULL SUITE + CONTAINMENT
[LEAD điền on-behalf — vai chết #53 sau §7] Suite 580/0 (lead chạy tươi 2 nguồn khớp §6). Lead phân xử: Q1-Q6 DUYỆT CẢ 6 (căn cứ đo được từng dòng); ambiguity mark-read-chuông-cũ → ghi TỒN UAT (mirror PE S96, vá riêng khi anh gật — ngoài unit-of-work). Grep NotImplementedException trong service = 1 hit COMMENT dòng :16 (code sạch).