All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m28s
Session 22 final docs: - STATUS Last updated S22 + S21 chốt cuối preserved row (§6.5 KEEP narrative) - HANDOFF Last updated S22 + S21 row preserved - Session log mới `2026-05-13-1800-s22-plan-cde-test-strict-v2.md` (260 LOC) + narrative 4 plan + pre-flight evidence + lessons learned Cross-agent sync (start-of-session): 3 agent MEMORY.md drift patch (KHÔNG cắt narrative — chỉ count update): - investigator/MEMORY.md: 27→29 mig + 81→84 test + 44→46 gotcha + 16→19 memory + Mig 28/29 narrative ngắn + Gitea API discovery cross-ref - implementer/MEMORY.md: test baseline 81→84 - reviewer/MEMORY.md: 81→84 test + 44→46 gotcha + Mig 29 per-NV scope line CICD Monitor MEMORY.md đã fresh từ S21 t5 — KHÔNG đụng. Plan F ABORTED reason: - Contract entity HOÀN TOÀN V1 (no ApprovalWorkflowId column) - Prod 23 PE + 4 V1-only PE + 7 Contract pin V1 - Drop V1 BE crash startup → defer sau Plan B Contract V2 wire Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
232 lines
11 KiB
Markdown
232 lines
11 KiB
Markdown
# Session 22 — 2026-05-13 18:00 — Plan D + C + E (Plan F ABORTED)
|
||
|
||
**Dev:** Claude Opus 4.7 1M Max (em main solo — Implementer REFUSE cross-stack reasoning chain)
|
||
**Duration:** ~2.5h (start session context load + 4 plan execute + finalize)
|
||
**Base commit:** `3d725c4` (S21 chốt cuối)
|
||
**Commits this session:** `60efeed` (D) → `dbda37e` (C task 4) → `215b1e0` (C task 1-3) → `f149661` (E) → this (Docs)
|
||
|
||
## Trigger
|
||
|
||
Bro yêu cầu kick off 4 plan đồng thời: **Plan C + D + E + F** (test backlog + F2 toggle UI + phân quyền strict V2 + drop legacy V1).
|
||
|
||
Em hỏi clarify 3 câu trước khi kick off:
|
||
- Q1 thứ tự: **D→C→E→F** (an toàn → risk dần)
|
||
- Q2 Plan F destructive: drop luôn không backup (UAT chấp nhận risk) — nhưng em vẫn pre-flight check phiếu V2 không pin V1
|
||
- Q3 Plan E scope: toàn bộ **List + Inbox + Detail**
|
||
|
||
## Plan D — User Management F2 toggle UI (`60efeed`)
|
||
|
||
Wire FE UI cho `AllowDrafterSkipToFinal` (BE column từ Mig 29 đã sẵn).
|
||
|
||
### BE — UserFeatures.cs + UsersController.cs
|
||
|
||
- `UserDto` +`AllowDrafterSkipToFinal bool` (cuối record)
|
||
- `ListUsersQueryHandler` + `GetUserQueryHandler` populate field
|
||
- `SetUserAllowDrafterSkipToFinalCommand` + Handler mới mirror `SetUserBypassReviewCommand` pattern
|
||
- `UsersController.cs` +endpoint `PATCH /api/users/{id}/allow-skip-final` body `{allowDrafterSkipToFinal:bool}` Policy=`Users.Update`
|
||
|
||
### FE Admin — users.ts + UsersPage.tsx
|
||
|
||
- `User` type +`allowDrafterSkipToFinal: boolean`
|
||
- Column "Skip cuối" với `FastForward` icon violet badge
|
||
- Action button toggle (`allowSkipMut`) — title đổi theo trạng thái
|
||
- fe-user KHÔNG mirror (UsersPage admin-only theo PROJECT-MAP)
|
||
|
||
### Verify
|
||
- `dotnet build SolutionErp.slnx` — 0 err, 2 warn pre-existing DocxRenderer
|
||
- `npm run build fe-admin` — pass 638ms
|
||
|
||
## Plan C task 4 — Regression test #44 silent 403 (`dbda37e`)
|
||
|
||
Test-before backlog HIGH §7 priority (S18 nợ — Drafter `nv.test` Workspace dropdown empty silent).
|
||
|
||
### File mới `tests/.../Api/AuthorizePolicyRegressionTests.cs`
|
||
|
||
5 reflection-based tests verify `ApprovalWorkflowsV2Controller` policy split:
|
||
1. Class-level `[Authorize]` only, NO Policy
|
||
2. GET Overview inherits class-level (no action-level Policy)
|
||
3. POST Create require `Policy="Workflows.Create"`
|
||
4. DELETE require `Policy="Workflows.Create"`
|
||
5. PATCH user-selectable require `Policy="Workflows.Create"`
|
||
|
||
Pattern reusable cho future Authorize regression — catch nếu ai add Policy lên class-level hoặc GET action mà không qua UAT silent 403 reproduce.
|
||
|
||
Add ProjectReference `SolutionErp.Api` → `SolutionErp.Infrastructure.Tests` cho reflection access Controller types.
|
||
|
||
### Verify
|
||
- 84 → 89 PASS (+5 regression #44)
|
||
|
||
## Plan C task 1-3 — Service test catch-up S21 t4-t5 (`215b1e0`)
|
||
|
||
14 test cover 3 helper sửa lớn S21 t4-t5.
|
||
|
||
### `Services/PurchaseEvaluationWorkflowServiceReturnModeTests.cs` (7 test)
|
||
|
||
Task 1 — ApplyReturnModeAsync per-NV (Mig 29):
|
||
- Drafter mode allowed by Level → Phase=TraLai, clear pointer
|
||
- Drafter mode denied by Level (non-admin) → ConflictException
|
||
- OneLevel allowed by Level → curLevel 2 → 1 (peer review trong Step)
|
||
- OneLevel denied by Level + Admin bypass → succeeds (admin override)
|
||
|
||
Task 2 — skipToFinal per-Drafter (Mig 29):
|
||
- Drafter `AllowDrafterSkipToFinal=true` → set pointer cuối Step + cuối Level
|
||
- Drafter `AllowDrafterSkipToFinal=false` non-admin → ConflictException
|
||
- Admin bypass user flag → succeeds
|
||
|
||
### `Application/PurchaseEvaluationDraftGuardTests.cs` (7 test)
|
||
|
||
Task 3 — EnsureEditableForDetailsAsync F3 gating:
|
||
- DraftScope DangSoanThao + any caller → return PE
|
||
- DraftScope TraLai + any caller → return PE
|
||
- ApproverScope ChoDuyet + flag on + actor match → return PE
|
||
- ApproverScope ChoDuyet + flag off (non-admin) → ConflictException
|
||
- ApproverScope ChoDuyet + flag on + actor mismatch → ForbiddenException
|
||
- AdminBypass ChoDuyet + flag off → return PE
|
||
- DaDuyet terminal phase + any caller (kể cả admin) → ConflictException
|
||
|
||
### Pattern infra
|
||
|
||
- Helper `SeedWorkflowAsync` tạo 1 Bước (Step) × 2 Cấp (Levels) với mọi Allow* default false (admin opt-in pattern Mig 29).
|
||
- Helper `SeedApproversAsync` create approver users qua `IdentityFixture.CreateUserAsync` để satisfy FK `ApproverUserId` constraint.
|
||
- `DepartmentId = null` trên Step để skip FK Department.
|
||
- `FakeCurrentUser` implement `ICurrentUser` minimal cho Guard tests.
|
||
- `InternalsVisibleTo("SolutionErp.Infrastructure.Tests")` thêm vào `SolutionErp.Application.csproj` để test access `internal static class PurchaseEvaluationDraftGuard`.
|
||
|
||
### Finding (sub-optimal nhưng không scope catch-up)
|
||
|
||
Service `skipToFinal` flow mutate `evaluation.Phase = ChoDuyet` TRƯỚC validate user flag. Throw chặn SaveChanges nên DB không persist nhưng in-memory entity dirty. Test phải relax secondary assertion `pe.Phase` → assert pointer chưa init thay vì rollback. Note trong test comment cho future refactor.
|
||
|
||
### Verify
|
||
- 89 → 103 PASS (+14: 7 ReturnMode + 7 Guard)
|
||
|
||
## Plan E — Phân quyền strict V2 (`f149661`)
|
||
|
||
Thắt chặt PE V2 List + Detail từ UAT loose sang strict actor.UserId scope (Inbox đã strict từ S17 — KHÔNG đụng).
|
||
|
||
### Change 1: `ListPurchaseEvaluationsQueryHandler`
|
||
|
||
Trước (UAT loose):
|
||
```csharp
|
||
q = q.Where(x =>
|
||
x.e.DrafterUserId == userId
|
||
|| eligiblePhases.Contains(x.e.Phase)
|
||
|| x.e.ApprovalWorkflowId != null); // V2 loose UAT
|
||
```
|
||
|
||
Sau (strict):
|
||
```csharp
|
||
var userApprovalWfIds = await db.ApprovalWorkflowLevels.AsNoTracking()
|
||
.Where(l => l.ApproverUserId == userId.Value)
|
||
.Select(l => l.Step!.ApprovalWorkflowId)
|
||
.Distinct()
|
||
.ToListAsync(ct);
|
||
q = q.Where(x =>
|
||
x.e.DrafterUserId == userId
|
||
|| eligiblePhases.Contains(x.e.Phase)
|
||
|| (x.e.ApprovalWorkflowId != null && userApprovalWfIds.Contains(x.e.ApprovalWorkflowId.Value)));
|
||
```
|
||
|
||
### Change 2: `GetPurchaseEvaluationQueryHandler`
|
||
|
||
Trước: `var isPinnedV2 = e.ApprovalWorkflowId is not null` (loose).
|
||
|
||
Sau:
|
||
```csharp
|
||
var isV2Approver = false;
|
||
if (e.ApprovalWorkflowId is Guid awIdForCheck && currentUser.UserId is Guid uidForCheck)
|
||
{
|
||
isV2Approver = await db.ApprovalWorkflowLevels.AsNoTracking()
|
||
.AnyAsync(l => l.Step!.ApprovalWorkflowId == awIdForCheck
|
||
&& l.ApproverUserId == uidForCheck, ct);
|
||
}
|
||
if (!isDrafter && !eligiblePhases.Contains(e.Phase) && !isV2Approver)
|
||
throw new ForbiddenException("Bạn không có quyền xem phiếu này.");
|
||
```
|
||
|
||
### Test defer
|
||
|
||
4 integration test (List Drafter/V2 approver/non-approver throw 403 + Detail tương tự) — defer carry Plan C bundle khi UAT confirm strict scope stable. Hiện 103/103 PASS regression-free.
|
||
|
||
### Verify
|
||
- `dotnet build SolutionErp.slnx` — 0 err, 2 warn pre-existing
|
||
- `dotnet test SolutionErp.slnx` — 103/103 PASS regression-free
|
||
|
||
## Plan F ABORTED — Pre-flight FAIL
|
||
|
||
Plan F (drop legacy V1 Mig 32) **ABORTED** vì pre-flight reveal blocking conditions.
|
||
|
||
### Pre-flight evidence (Prod DB qua SSH vietreport-vps)
|
||
|
||
| Entity | Prod count | Pin status |
|
||
|---|---:|---|
|
||
| PurchaseEvaluations với `WorkflowDefinitionId` | **23** | V1 ref |
|
||
| PurchaseEvaluations V1-ONLY (no V2 fallback) | **4** | mất workflow nếu drop |
|
||
| Contracts với `WorkflowDefinitionId` | **7** | V1 |
|
||
| Contracts với `ApprovalWorkflowId` | **error** | column không tồn tại — Contract V2 chưa wire |
|
||
|
||
### Blocking conditions
|
||
|
||
1. **Contract entity HOÀN TOÀN dùng V1** — `Contract.cs` không có property `ApprovalWorkflowId`. Plan B Contract V2 wire CHƯA kick off. Drop `WorkflowDefinitions` table sẽ BE crash startup (FK violation 7 contract).
|
||
|
||
2. **4 phiếu PE V1-only thật trong prod** — drop V1 = workflow data loss.
|
||
|
||
3. **19 phiếu PE V1+V2 mix carry V1 ref** — drop column = NULL data, loose audit.
|
||
|
||
### Decision
|
||
|
||
ABORT Plan F. Defer S22+ sau Plan B Contract V2 wire (Mig 30+31). Order đúng:
|
||
1. Plan B Contract V2 wire (Mig 30+31)
|
||
2. Migrate 4 phiếu PE V1-only → V2 (admin script)
|
||
3. UAT V2 cả 2 module (PE + Contract) 2-3 tuần
|
||
4. THEN Plan F drop legacy V1 (Mig 32)
|
||
|
||
User confirm via AskUserQuestion (4 option) — answer trống → em apply Recommended default ABORT.
|
||
|
||
## Stats cumulative S22
|
||
|
||
| Metric | Trước (S21 chốt) | Sau (S22) | Δ |
|
||
|---|---|---|---|
|
||
| DB tables | 59 | 59 | 0 |
|
||
| Migrations | 29 | 29 | 0 |
|
||
| Endpoints | ~143 | **~144** | +1 (PATCH /users/{id}/allow-skip-final) |
|
||
| FE pages | 34 | 34 | 0 (UsersPage extend column) |
|
||
| **Unit tests** | 84 | **103** | **+19** (+5 reg #44 + 7 ReturnMode + 7 Guard) |
|
||
| Gotchas | 46 | 46 | 0 |
|
||
| Memory entries | 19 | 19 | 0 |
|
||
| Skills | 6 | 6 | 0 |
|
||
| Sub-agents | 4 (3 seeds + cicd) | 4 same | 0 (em main solo) |
|
||
| Commits S22 | — | **5** | D + C×2 + E + Docs |
|
||
|
||
## Lessons learned
|
||
|
||
1. **Pre-flight prod check BẮT BUỘC cho destructive migration.** SSH sqlcmd verify data state TRƯỚC khi drop schema. Plan F pre-flight catch 3 blocking conditions (Contract V1-only + 4 PE V1-only + 19 PE mix) — tránh BE crash startup prod nếu blindly drop.
|
||
|
||
2. **Schema dependency check toàn bộ entity.** Plan F focus drop PE V1 nhưng Contract entity cùng V1 schema → liên đới. Audit entity scope `ApprovalWorkflowId` presence trên Domain trước decision.
|
||
|
||
3. **Test infra reusable Cookie-cutter.** Helper `SeedWorkflowAsync` + `SeedApproversAsync` pattern dùng cho cả ReturnMode + Guard tests. Future Service test PE/Contract V2 reuse được. Tổng test catch-up 14 test trong ~1.5h, ROI clear.
|
||
|
||
4. **InternalsVisibleTo cleaner than make public.** Khi cần test internal helper, dùng InternalsVisibleTo trong csproj thay vì rewrite `internal → public` — KHÔNG thay đổi public API surface, vẫn chặn external use.
|
||
|
||
5. **Reflection-based regression test cho Authorize policy.** Pattern lightweight (5 test ~50 LOC) catch silent 403 regression mà không cần WebApplicationFactory heavy. ROI cực cao cho project có nhiều controller.
|
||
|
||
## Handoff
|
||
|
||
- ✅ Plan D `60efeed` BE+FE Admin User F2 toggle
|
||
- ✅ Plan C task 4 `dbda37e` 5 reflection test #44 silent 403
|
||
- ✅ Plan C task 1-3 `215b1e0` 14 test ReturnMode + Guard
|
||
- ✅ Plan E `f149661` BE strict V2 scope List + Detail
|
||
- ⛔ Plan F ABORTED (pre-flight FAIL — defer sau Plan B Contract V2)
|
||
- ⏭ **PENDING bro confirm push remote** — `git push origin main` 5 commit ahead `3d725c4..HEAD`
|
||
|
||
User next action expected: Plan B Contract V2 wire (Mig 30+31) là priority cao nhất sau S22 — mở đường cho Plan F drop legacy V1 sau UAT 2-3 tuần.
|
||
|
||
## References
|
||
|
||
- BE feature: `src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs` (List + Get strict scope)
|
||
- BE User: `src/Backend/SolutionErp.Application/Users/UserFeatures.cs` + `SolutionErp.Api/Controllers/UsersController.cs`
|
||
- FE Admin: `fe-admin/src/types/users.ts` + `fe-admin/src/pages/system/UsersPage.tsx`
|
||
- Tests: `tests/SolutionErp.Infrastructure.Tests/{Api,Services,Application}/` 3 file mới
|
||
- Csproj: `src/Backend/SolutionErp.Application/SolutionErp.Application.csproj` +InternalsVisibleTo
|
||
- Pre-flight: `ssh vietreport-vps "sqlcmd -S .\SQLEXPRESS -d SolutionErp -E -Q \"...\""`
|
||
- Rules: §3.9 mirror 2 FE, §6.5 KEEP narrative, §7 test timing, `feedback_uat_skip_verify`, `feedback_per_chunk_commit`, `feedback_per_nv_permission_scope`
|