wal: flush 20260727T1312
This commit is contained in:
@ -57,7 +57,11 @@ chain:
|
|||||||
[ ] ⚠️ Deploy này mang **migration mới** `20260727033522_AddPeAllowApproverDelete` lên PROD — AddColumn bit default false, additive an-toàn, nhưng LÀ đổi schema prod thật
|
[ ] ⚠️ Deploy này mang **migration mới** `20260727033522_AddPeAllowApproverDelete` lên PROD — AddColumn bit default false, additive an-toàn, nhưng LÀ đổi schema prod thật
|
||||||
[~] wf: A-S4-đợt1 hmw — run-id engine `wf_f1bfea65-ed9` · run-folder `.claude/workflows/runs/2026-07-27-S155-pe-delete-approver/` · 2 task: implementer-backend (F6 schema+mig+UpdateCommand+validator T1/T2+authz+F-5a+F-6+F-10) · implementer-frontend (F-11 5 dây, bẫy :605). Chết giữa chừng ⇒ /tiep §4 relaunch ĐÃ-CẮT-GỌT, đọc sub-*.md trên đĩa trước
|
[~] wf: A-S4-đợt1 hmw — run-id engine `wf_f1bfea65-ed9` · run-folder `.claude/workflows/runs/2026-07-27-S155-pe-delete-approver/` · 2 task: implementer-backend (F6 schema+mig+UpdateCommand+validator T1/T2+authz+F-5a+F-6+F-10) · implementer-frontend (F-11 5 dây, bẫy :605). Chết giữa chừng ⇒ /tiep §4 relaunch ĐÃ-CẮT-GỌT, đọc sub-*.md trên đĩa trước
|
||||||
[ ] A-S4-đợt1 wave 2 (sau khi wave 1 xanh): test-specialist F-12..F-16 + reviewer soi diff
|
[ ] A-S4-đợt1 wave 2 (sau khi wave 1 xanh): test-specialist F-12..F-16 + reviewer soi diff
|
||||||
[!] wf: **A-ĐỢT-2 W1** hmw — run-id `wf_55f6201f-a52` · anh lệnh "làm tiếp luôn" 2026-07-27 (KHÔNG chờ UAT đợt 1). 2 task SONG SONG: BE (xóa-by-approver + list /deleted + menu seed + changelog + 10 test) · FE (nút xóa panel + 3 mục menu + màn Đã xóa + md5-mirror)
|
[~] A-ĐỢT-2 W1 hmw XONG-MỘT-PHẦN — cả 2 sub mất return. Đo đĩa: **BE code LAND** (file mới `PeSoftDeleteFeatures.cs` + controller `:163` `by-approver` + `:174` `deleted` + `MenuKeys:145-146` + `DbInitializer`) build 0E — **NHƯNG 0 test mới** (551 y nguyên) · **FE gần như CHƯA làm** (chỉ `types/purchaseEvaluation.ts` ×2)
|
||||||
|
[x] ✅ **HỢP-ĐỒNG LIÊN-LANE GIỮ ĐƯỢC ở phía BE** — route + menu-key đúng canonical từng ký tự. FE chưa gọi (0 hit) vì chưa làm tới
|
||||||
|
[!] 🔴 **LEAD SAI, sub FE bác ĐÚNG**: lead bảo "vá lệch `fe-admin:107` thiếu `WfView`" — sub đo ra tiền-đề KHÔNG ĐỨNG (`WorkflowMatrixViewPage.tsx` CHỈ có ở fe-user; admin không page/route/types) ⇒ thêm regex một mình = **đẻ link chết** "Trang này chưa được build" = regression NHÌN THẤY ĐƯỢC, tệ hơn hiện trạng. Lead chép kết-luận của investigator đợt 1 mà không hỏi trang đích có tồn tại không. **LEAD CHỐT đường (c): GIỮ NGUYÊN**, gỡ khỏi phạm vi (admin đã có Designer sửa được, matrix chỉ-xem là thừa)
|
||||||
|
[!] wf: A-ĐỢT-2 W1-bis — 2 spawn TRỰC TIẾP song song (KHÔNG qua hmw): `implementer-frontend` làm phần chính FE (`sub-d2-fe-3.md`) · `test-specialist` viết 10 test T21-T26 (`sub-d2-test-4.md`)
|
||||||
|
[~] wf: A-ĐỢT-2 W1 hmw — run-id `wf_55f6201f-a52` · anh lệnh "làm tiếp luôn" 2026-07-27 (KHÔNG chờ UAT đợt 1). 2 task SONG SONG: BE (xóa-by-approver + list /deleted + menu seed + changelog + 10 test) · FE (nút xóa panel + 3 mục menu + màn Đã xóa + md5-mirror)
|
||||||
[ ] 🔴 **HỢP-ĐỒNG LIÊN-LANE đã chốt TRƯỚC khi phóng** (bài học đợt 1: 2 nửa build sạch mà không nối được nhau — F3) — canonical ở `run.md` §ĐỢT 2: `DELETE /api/purchase-evaluations/{id}/by-approver` (policy `PurchaseEvaluations.Delete`) · `GET /api/purchase-evaluations/deleted` (policy `.Read`) · giữ NGUYÊN `DELETE /{id}` xóa-nháp KHÔNG policy · key `Pe_{code}_Approved` + `Pe_{code}_Deleted` (KHÔNG vào `MenuKeys.All`) · route FE `?phase=7` và `?deleted=1`
|
[ ] 🔴 **HỢP-ĐỒNG LIÊN-LANE đã chốt TRƯỚC khi phóng** (bài học đợt 1: 2 nửa build sạch mà không nối được nhau — F3) — canonical ở `run.md` §ĐỢT 2: `DELETE /api/purchase-evaluations/{id}/by-approver` (policy `PurchaseEvaluations.Delete`) · `GET /api/purchase-evaluations/deleted` (policy `.Read`) · giữ NGUYÊN `DELETE /{id}` xóa-nháp KHÔNG policy · key `Pe_{code}_Approved` + `Pe_{code}_Deleted` (KHÔNG vào `MenuKeys.All`) · route FE `?phase=7` và `?deleted=1`
|
||||||
[ ] A-ĐỢT-2 W2: test + reviewer → deploy (theo đúng gate đợt 1: test xanh ∧ reviewer không ĐỪNG-DEPLOY ∧ F8 `git add` đích danh)
|
[ ] A-ĐỢT-2 W2: test + reviewer → deploy (theo đúng gate đợt 1: test xanh ∧ reviewer không ĐỪNG-DEPLOY ∧ F8 `git add` đích danh)
|
||||||
[ ] A-ràng-buộc CỨNG cho spec: `IgnoreQueryFilters()` chỉ mở ĐÚNG 1 endpoint list "Đã xóa" — rộng tay = phiếu xóa lọt lại `PeBudgetAccumulator` = phá đúng ý (3) của anh
|
[ ] A-ràng-buộc CỨNG cho spec: `IgnoreQueryFilters()` chỉ mở ĐÚNG 1 endpoint list "Đã xóa" — rộng tay = phiếu xóa lọt lại `PeBudgetAccumulator` = phá đúng ý (3) của anh
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
# sub-d2-be-1 — ĐỢT 2 BACKEND (xóa-by-approver + list "Đã xóa" + menu + test)
|
||||||
|
|
||||||
|
> run `2026-07-27-S155-pe-delete-approver` · vai `implementer-backend` · 2026-07-27
|
||||||
|
> Luật ghi: flush TỪNG PHẦN + tự `wc -c` (run này đã mất 1 diary 0-byte + 7 return).
|
||||||
|
|
||||||
|
## 0. Checklist tự theo dõi
|
||||||
|
|
||||||
|
- [x] đọc `run.md` mục ĐỢT 2 (hợp-đồng liên-lane) + `spec` PHẦN I mục 4-13, 21-28
|
||||||
|
- [x] đo nền từ ĐĨA (không tin spec): controller · handler xóa · list IDOR · MenuKeys · DbInitializer
|
||||||
|
- [ ] A. đường xóa MỚI `/by-approver`
|
||||||
|
- [ ] B. list `/deleted`
|
||||||
|
- [ ] C. menu (MenuKeys +2 factory · tree +2 · labelBackfill · 2 danh sách permission)
|
||||||
|
- [ ] D. test 21-26 + 22b/22c/25b
|
||||||
|
- [ ] build + test, dán SỐ THẬT
|
||||||
|
|
||||||
|
## 1. Nền ĐO TỪ ĐĨA (verify lại, không chép spec)
|
||||||
|
|
||||||
|
| Thứ | Vị trí đọc được | Khớp MEMORY-PACK? |
|
||||||
|
|---|---|---|
|
||||||
|
| Endpoint xóa CŨ | `PurchaseEvaluationsController.cs:146-151` — `[HttpDelete("{id:guid}")]`, class `[Authorize]` trần `:15` | ✅ |
|
||||||
|
| Handler xóa CŨ | `PurchaseEvaluationFeatures.cs:1396-1411` — ctor CHỈ `IApplicationDbContext`, guard allow-list `:1404-1406` | ✅ |
|
||||||
|
| Khối IDOR list | `PurchaseEvaluationFeatures.cs:596-617` (3 điều kiện: Drafter ∥ eligiblePhases ∥ V2-approver + `!= DangSoanThao`) | ✅ |
|
||||||
|
| Khuôn per-ROW | `PurchaseEvaluationWorkflowService.cs:744-745` `matchingLevel` (fallback `.First()`), guard lượt `:694-720` | ✅ (spec ghi `:859` cho F5 — đây là chỗ ĐỌC cờ) |
|
||||||
|
| `LogTransitionAsync` | `PurchaseEvaluationWorkflowService.cs:1169-1196` | ✅ |
|
||||||
|
| `MenuKeys.All` | `MenuKeys.cs:147-165` — 0 `Pe_*` (chỉ root `PurchaseEvaluations` `:153`) | ✅ |
|
||||||
|
| factory `Pe_*` | `MenuKeys.cs:134-141` (Group/List/Create/Pending/WfView) | ✅ |
|
||||||
|
| tree menu | `DbInitializer.cs:1867-1878` trong `foreach PurchaseEvaluationTypeCodes` | ✅ |
|
||||||
|
| upsert chỉ update Order | `DbInitializer.cs:1893-1905` (`continue` sau Order, KHÔNG đụng Label) | ✅ |
|
||||||
|
| `labelBackfill` | `DbInitializer.cs:1918-1924` | ✅ |
|
||||||
|
| permission seed ×2 | `DbInitializer.cs:2092-2096` (grant-all-roles) **VÀ** `:2492-2496` (PE defaults) | ✅ |
|
||||||
|
|
||||||
|
(đang cập nhật — phần thi công ghi tiếp bên dưới)
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
# sub-d2-fe-2 — ĐỢT 2 FRONTEND (nút Xóa màn duyệt + menu + màn "Đã xóa")
|
||||||
|
|
||||||
|
- **role:** implementer-frontend · **run:** `2026-07-27-S155-pe-delete-approver` · **đợt:** 2
|
||||||
|
- **scope:** `fe-admin/src/**` + `fe-user/src/**` ONLY. KHÔNG chạm `src/Backend/**`, KHÔNG chạm `tests/`.
|
||||||
|
- **ghi TRONG LÚC LÀM** (flush từng phần, tự `wc -c` sau mỗi flush).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Nền đã ĐO (trước khi sửa) — số thật, không phải trí nhớ
|
||||||
|
|
||||||
|
| Đo | Kết quả |
|
||||||
|
|---|---|
|
||||||
|
| `PeWorkflowPanel.tsx` md5 ×2 app | `02671be6334110028c55fe82f5e70dee` (IDENTICAL, 942 dòng) |
|
||||||
|
| `PurchaseEvaluationsListPage.tsx` md5 ×2 app | `99e38e167d2f745ea7628174f1d2d6bf` (IDENTICAL — **bonus**, spec không nói) |
|
||||||
|
| `usePermission.ts` md5 ×2 app | `2b210b753e8fe2704407c787cb82779d` (IDENTICAL) |
|
||||||
|
| `Layout.tsx` | **DIVERGE** admin `ca59f56c…` / user `cab674f4…` (đúng như thiết kế — 2 app khác staticMap) |
|
||||||
|
| `lib/menuKeys.ts` | DIVERGE đúng 2 dòng (`Off_AttendanceReport` admin-only) |
|
||||||
|
| `git status` lúc bắt đầu | sạch (chỉ `.session-counter.json`) ⇒ **lane BE chưa land gì** |
|
||||||
|
|
||||||
|
## 1. 🔴 PHỤ THUỘC LIÊN-LANE — FE gọi gì, BE phải có gì
|
||||||
|
|
||||||
|
Hợp-đồng `run.md:139-154` chốt 3 route + 2 menu-key. FE bám ĐÚNG. **NHƯNG** hợp-đồng
|
||||||
|
**không** đặt tên cho 2 thứ FE bắt buộc phải có — khai ở đây để lead verify, KHÔNG tự chế lung tung:
|
||||||
|
|
||||||
|
### (D1) Cờ F6 per-NGƯỜI trong bundle chi tiết phiếu
|
||||||
|
- FE đọc: `evaluation.currentLevelOptions.allowApproverDelete`
|
||||||
|
- BE PHẢI thêm: `bool AllowApproverDelete` làm **member positional thứ 9** của
|
||||||
|
`ApprovalWorkflowOptionsDto` (`src/Backend/…/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs:123-131`)
|
||||||
|
+ gán từ `curLevel.AllowApproverDelete` tại `PurchaseEvaluationFeatures.cs:1088-1096`.
|
||||||
|
- Vì sao chỗ này mà không phải chỗ khác: `currentLevelOptions` là kênh **DUY NHẤT** per-NGƯỜI —
|
||||||
|
nó resolve bằng `l.ApproverUserId == currentUser.UserId` (`PurchaseEvaluationFeatures.cs:1083-1085`).
|
||||||
|
Kênh còn lại `approvalFlow.steps[].levels[].allowApproverFinalize` là **OR-of-N per-CẤP**
|
||||||
|
(`:1182 g.Any(...)`) — dùng nó là vi phạm B2 (`run.md:159` per-ROW).
|
||||||
|
- **Nếu BE quên:** field về `undefined` → gate `=== true` false → **nút KHÔNG BAO GIỜ HIỆN**,
|
||||||
|
build 2 bên vẫn xanh. Đúng dạng silent-drop đợt 1. ⇒ lead phải grep xác nhận.
|
||||||
|
|
||||||
|
### (D2) Chỗ chứa "lý do xóa" trong request
|
||||||
|
- Hợp-đồng chỉ ghi `DELETE /api/purchase-evaluations/{id}/by-approver`, **không** nói payload.
|
||||||
|
- FE gửi **CẢ HAI** đường cho tương thích mọi kiểu binding BE:
|
||||||
|
`api.delete(url, { params: { reason }, data: { reason } })`
|
||||||
|
→ BE bind `[FromQuery] string? reason` ✅ · bind `[FromBody] {Reason}` ✅ · không nhận gì ✅ (bị bỏ qua).
|
||||||
|
- Chủ ý: thà thừa 1 query-param còn hơn mất vết audit vì lệch tên. Ghi rõ để reviewer không tưởng là cẩu thả.
|
||||||
|
|
||||||
|
### (D3) DTO list "Đã xóa"
|
||||||
|
- FE khai `PeDeletedListItem = PeListItem + { deletedAt?, deletedBy?, deletedByName? }` — **tất cả optional**,
|
||||||
|
render phòng thủ (`—` khi thiếu). BE trả thêm/thiếu đều không vỡ. Tên bám `AuditableEntity`
|
||||||
|
(`src/Backend/SolutionErp.Domain/Common/AuditableEntity.cs:5-7`: `IsDeleted`/`DeletedAt`/`DeletedBy`).
|
||||||
|
|
||||||
|
## 2. 🚩 FLAG — mục "vá `WfView` cho fe-admin" TÔI KHÔNG LÀM (tiền-đề của lệnh sai)
|
||||||
|
|
||||||
|
Lệnh: *"vá lệch sẵn có: `fe-admin:107` THIẾU `WfView` so với `fe-user:120`"*.
|
||||||
|
Đo trước khi làm thì tiền-đề **không đứng**:
|
||||||
|
|
||||||
|
| Bằng chứng | fe-user | fe-admin |
|
||||||
|
|---|---|---|
|
||||||
|
| Page `pages/pe/WorkflowMatrixViewPage.tsx` | CÓ (323 dòng) | **KHÔNG CÓ** |
|
||||||
|
| Route `/purchase-evaluations/workflow-matrix` | `App.tsx:60` | **KHÔNG CÓ** (`App.tsx:78-81` chỉ 4 route PE) |
|
||||||
|
| Types `types/approvalWorkflowV2.ts` | CÓ | **KHÔNG CÓ** (admin để type inline trong `ApprovalWorkflowsV2Page.tsx`) |
|
||||||
|
| Catch-all | — | `App.tsx:107-114` render *"Trang này chưa được build"* |
|
||||||
|
|
||||||
|
⇒ Thêm `WfView` vào regex admin **một mình** = đẻ link chết dẫn tới "Trang này chưa được build".
|
||||||
|
Đó là **regression nhìn thấy được**, tệ hơn trạng-thái-hiện-tại (leaf bị drop im lặng).
|
||||||
|
Sửa cho đúng cần **3 file mới bên admin** (page + types + route) = ngoài phạm vi đợt 2, và là
|
||||||
|
lựa-chọn UX (admin nên thấy matrix read-only hay Designer sửa được?) ⇒ **thuộc quyền owner/lead**.
|
||||||
|
|
||||||
|
**3 đường cho lead chọn (đều ≤ 1 lượt):**
|
||||||
|
- **(a)** mirror hẳn: copy `WorkflowMatrixViewPage.tsx` + `types/approvalWorkflowV2.ts` sang admin + route + regex.
|
||||||
|
- **(b)** rẻ nhất: admin map `Pe_{code}_WfView` → `/system/approval-workflows-v2/{code}` (Designer đã sống, `Layout.tsx:130-136`).
|
||||||
|
- **(c)** giữ nguyên: coi lệch này là CỐ Ý (admin có Designer rồi, không cần bản read-only) → ghi 1 dòng comment cho người sau khỏi "vá" lại.
|
||||||
|
|
||||||
|
Tôi để nguyên trạng (đường **c** *de facto*) + không chèn comment tránh đụng byte thừa. Chờ lead.
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
# sub-d2-test-4 — ĐỢT 2: test đường xóa-khi-đang-duyệt + màn "Đã xóa"
|
||||||
|
|
||||||
|
> Diary GHI TRONG LÚC LÀM (run này đã mất 1 diary 0-byte + 9 return). Flush từng phần, tự `wc -c` sau mỗi lần ghi.
|
||||||
|
|
||||||
|
## 0. Trạng thái nhận việc
|
||||||
|
- Baseline lead đo: **551 PASS / 0 FAIL** (Domain 45 + Infra 506). Code đợt 2 đã land, 0 test.
|
||||||
|
- Mục tiêu: 10 test (T21, T22, T22b, T22c, T23, T24, T24b, T25, T25b, T26) ⇒ đích > 551.
|
||||||
|
|
||||||
|
## 1. Đọc vật cần test — ĐÃ XONG
|
||||||
|
|
||||||
|
### `PeSoftDeleteFeatures.cs` (file mới, 249 dòng) — 2 vật
|
||||||
|
**A. `DeletePurchaseEvaluationByApproverCommandHandler`** (`:50-145`) — thứ tự rào:
|
||||||
|
1. `!IsAuthenticated || UserId is null` → `UnauthorizedException`
|
||||||
|
2. PE không tồn tại → `NotFoundException`
|
||||||
|
3. `Phase != ChoDuyet` → `ConflictException`
|
||||||
|
4. `ApprovalWorkflowId is not Guid` → `ConflictException`
|
||||||
|
5. steps rỗng / `CurrentWorkflowStepIndex` ngoài biên → `ConflictException`
|
||||||
|
6. `pendingLevelGroup.Count == 0` (Bước không có Cấp đang chờ) → `ConflictException`
|
||||||
|
7. **KHÔNG match `ApproverUserId == actorId`** → `ForbiddenException` ("chưa tới lượt") ← **T24**
|
||||||
|
8. **`!matchingLevel.AllowApproverDelete`** → `ForbiddenException` ("chưa được bật quyền") ← **T23, T24b**
|
||||||
|
9. Ghi `PurchaseEvaluationChangelog` (Action=`ChangelogAction.Delete`, EntityType=Workflow, EntityId=`matchingLevel.Id`, PhaseAtChange=ChoDuyet, ContextNote=Reason.Trim() hoặc null) ← **T26**
|
||||||
|
10. `db.PurchaseEvaluations.Remove(pe)` = XÓA MỀM qua `AuditingInterceptor` (Deleted→Modified + IsDeleted=true)
|
||||||
|
|
||||||
|
🔴 Điểm sống-còn cho test: rào (7) đọc **PER-ROW** `pendingLevelGroup.FirstOrDefault(l => l.ApproverUserId == actorId)`, rào (8) đọc `matchingLevel.AllowApproverDelete` — **không** `g.Any(...)`. Đây đúng là thứ **T24b** phải ghim.
|
||||||
|
|
||||||
|
**B. `ListDeletedPurchaseEvaluationsQueryHandler`** (`:153-248`):
|
||||||
|
- `IgnoreQueryFilters().Where(x => x.IsDeleted)` ← **T22b** ghim vế `.Where` (gỡ nó ⇒ list toàn phiếu sống)
|
||||||
|
- INNER join `db.Projects` (❗ phiếu phải có Project THẬT, không Guid ngẫu nhiên — nếu không row rơi khỏi kết quả và test xanh giả)
|
||||||
|
- IDOR-scope khi `!Roles.Contains(Admin)`: `DrafterUserId == userId` ∥ `eligiblePhases.Contains(Phase)` ∥ (Phase != DangSoanThao ∧ pinned wf ∈ workflow mà user là approver) ← **T22c**
|
||||||
|
|
||||||
|
### `PeBudgetAccumulator.cs` — số học cho T21 (🔴 KHÔNG phải "cả 4 cùng giảm")
|
||||||
|
| Đại lượng | Hàm | Điều kiện phase | Phiếu `ChoDuyet` có winner đang xét |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `PrevSubmitted*` | `ComputeAsync :53-59` | `ChoDuyet ∥ DaDuyet` | ✅ CÓ vào ⇒ xóa thì **↓** |
|
||||||
|
| `PrevSelected*` | `ComputeAsync :61-62` | `DaDuyet` ∧ có winner | ❌ không (đòi DaDuyet) ⇒ **KHÔNG đổi** |
|
||||||
|
| `PendingSubmitted*` | `ComputePendingAsync :104-108` | loại tường minh `ChoDuyet` | ❌ không ⇒ **KHÔNG đổi** |
|
||||||
|
| `PendingSelected*` | `ComputePendingAsync :114-117` | ∉{DangSoanThao,DaDuyet,TuChoi} ∧ winner | ✅ CÓ vào ⇒ xóa thì **↓** |
|
||||||
|
| `PriorPes` | `:128-133` | ∉{DaDuyet,TuChoi} | ✅ CÓ ⇒ **mất đúng 1 phần tử** |
|
||||||
|
|
||||||
|
Cơ chế triệt tiêu: `Remove` ⇒ `IsDeleted=true` ⇒ global filter `PurchaseEvaluationConfiguration.cs:84` loại khỏi `db.PurchaseEvaluations` ⇒ CẢ 4 phép cộng đọc hụt. Accumulator KHÔNG sửa.
|
||||||
|
|
||||||
|
### Controller (`PurchaseEvaluationsController.cs`)
|
||||||
|
- `:163-169` `DELETE {id}/by-approver` + `[Authorize(Policy = "PurchaseEvaluations.Delete")]` ← **T25**
|
||||||
|
- `:174-182` `GET deleted` + `[Authorize(Policy = "PurchaseEvaluations.Read")]`
|
||||||
|
- `:151-156` `DELETE {id}` (nháp) — **KHÔNG policy**, đừng nhầm ⇒ T25 phải assert cả vế này (nếu ai đó "sửa" bằng cách gắn policy lên method nháp thì T25 vẫn phải phân biệt được 2 method).
|
||||||
|
|
||||||
|
## 2. Kế hoạch file test
|
||||||
|
- File MỚI `tests/SolutionErp.Infrastructure.Tests/Application/PeDeleteByApproverTests.cs` — T21..T24b, T25b, T26 (handler-level, SQLite thật).
|
||||||
|
- T25 (403 policy) = Pattern 10 reflection, thêm vào file mới cùng chỗ (đọc attribute trên **method**, không phải class — 2 endpoint này gắn method-level).
|
||||||
|
|
||||||
|
<!-- flush 1 -->
|
||||||
Reference in New Issue
Block a user