---
name: pe-signoff-history-schema-review
description: PE sign-off 6-decision design review (S111, pre-fanout, prod-live) — schema/history lens; opinion-history mechanism pick + Decision-3 hard-lock caller-safety + A1 test breakage
metadata:
  type: project
---

# PE sign-off change — schema/history lens review (GO-WITH-ADJUSTMENTS)

Base = investigation `wf_fd8f2bfe-9f6` (PASS_WITH_CONCERNS). Design review BEFORE fan-out + prod deploy. My lens = SCHEMA/HISTORY (Decision 5 opinion-history + Decision 6 no-migrate).

**Why (context):** anh Kiệt chốt 6 decisions (A1 bypass-only-below-creator; no self-approve-guard; EDGE-5 hard-lock DaDuyet/TuChoi; 1-button; opinion-history keep+edit+save-old; new-phiếu-only). Reviewing HOW, not re-debating WHAT.

**How to apply (findings that held):**
- **Decision 5 mechanism — RECOMMEND (a) append-only `PurchaseEvaluationLevelOpinionHistory` table.** Reject (b) round/version-col: DESTRUCTIVE — must DROP existing UNIQUE `(PEId, ApprovalWorkflowLevelId)` at `PurchaseEvaluationLevelOpinionConfiguration.cs:30` + break UPSERT read `FirstOrDefault(PEId,LevelId)` at `PurchaseEvaluationWorkflowService.cs:766-768` (returns arbitrary version) at every read site. (c) Changelog = zero-migration fallback IF audit-tab-only display OK, but overloads `PurchaseEvaluationEntityType` (no LevelOpinion value; would reuse Workflow=5 already holding return-mode+transition logs). (a) = only additive CreateTable (safest mig class, no ALTER/backfill/index-change), reversible-by-drop, clean per-cell FE history. FK: Cascade on PEId (mirror parent), LOOSE-Guid on LevelId (avoid FK-Restrict-forever like parent `.cs:28`).
- **History hook = ApproveV2Async UPSERT else-branch `:784-790`** (capture existingOpinion BEFORE mutate). Bypass-path overwrite `:635-641` becomes DEAD under A1 (creator own-slot no longer auto-signed).
- **A1 × Decision-5 interaction (KEY):** under A1 the creator's own slot is NO LONGER auto-signed at submit → ONLY overwrite path = re-sign (TraLai-resubmit / return-mode-backward re-approve). History belongs at approve-UPSERT, not submit-bypass.
- **Decision 3 caller-safety VERIFIED:** only production caller of PE `TransitionAsync` = MediatR handler `PurchaseEvaluationFeatures.cs:523` via controller `/transitions`. `SlaExpiryJob.cs:100` uses `IContractWorkflowService` on `db.Contracts` — NOT PE. So top-of-method hard-lock guard `fromPhase∈{DaDuyet,TuChoi}→throw` is safe (no background-loop crash). EDGE-5 root cause = `EnsureCanRejectV2Async:321` early-returns when Phase!=ChoDuyet (does NOT block) → reject branch `:92-146` then mutates DaDuyet→TraLai. Guard must sit BEFORE reject branch + admin-override `:290` (block even Admin).
- **Decision 6 schema-safe:** new history table empty for old phiếu (auto no-backfill). Terminal old phiếu hard-locked post-D3 → never overwrite → never accrue history. BUT code changes are GLOBAL (no per-phiếu scheme flag) → "phiếu mới only" not literally enforced for in-flight old phiếu → clarify = no-backfill (auto) vs old-retain-old-semantics (needs flag = scope+).
- **Test breakage (spec change, §7 update-with-code):** `PeSubmitGuardAndBypassTests.cs` L465/L510/L580/L642 encode old S60 auto-sign-own-slot → BREAK under A1 (method names literally wrong e.g. `BypassesAllLevels_MovesToStep2`). L545 (drafter-not-in-step1) stays green.

**Open (owner):** "cho sửa/cập-nhật" = re-sign-overwrite only (covered) vs NEW standalone edit-opinion endpoint (adds command+authz+own history-capture, NOT in lens scope). Verdict = GO-WITH-ADJUSTMENTS.
