F 10 Verification Gate
Status: 🟢 ready to test Owner: Adilet Last updated: 2026-05-08 Last verified on production: —
Goal
When the verification gate is active for a cycle, an approved issue
cannot be paid until its issue status is explicitly set to verified.
This separates "the bug is real and we'll pay" (approval) from "the fix
landed and we re-tested" (verification). When the gate is inactive,
approval is sufficient — verification is skipped.
Actors
- Primary — Admin or Cycle lead (sets issue status to
verified) - Secondary — Reporter (sees pipeline strip update + bell ping)
Preconditions
- An approved issue exists (
payout_status='approved'from F-09) - Either the cycle's
payout_requires_verification = true, or the product's default is true and the cycle's flag isnull(inheriting)
Trigger
Admin/lead opens the issue detail page. The pipeline strip
(Triaged → Exported → Fix shipped → Verified → Paid) shows the current
stage. Mark Paid is gated until verification.
Happy path — verify a fix
| # | Actor | UI surface | Action | API call | DB writes | Side effects | Suggested event |
|---|---|---|---|---|---|---|---|
| 1 | Admin/Lead | issue detail ApprovalPayoutCard | Click "Mark verified" | PATCH /api/test-cycles/:id/issues/:issueId body { status: 'verified' } | UPDATE issues SET status='verified', resolved_at = NOW() | ISSUE_VERIFIED notification to reporter | issue.verified |
| 2 | Frontend | pipeline advances; "Mark paid" enabled | — | — | — | — | — |
Happy path — Mark Paid (per-issue, post-verification)
See F-11 for the manual per-issue payment path. This flow only documents the gate's effect on it.
| # | Actor | UI surface | Action | API call | DB writes | Side effects | Suggested event |
|---|---|---|---|---|---|---|---|
| 1 | Admin/Lead | issue detail | Click "Mark paid" (now enabled) | POST /api/test-cycles/:id/issues/:issueId/payments | INSERT INTO issue_payouts, UPDATE issues SET payout_status='paid' | ISSUE_PAID notification | payout.issue_paid |
Gate inactive (alternative happy path)
When the cycle's effective gate = false:
| # | Actor | UI surface | Action | API call | DB writes | Side effects | Suggested event |
|---|---|---|---|---|---|---|---|
| 1 | Admin | issue detail | Pipeline shows 3 stages (Triaged → Exported → Paid). "Mark paid" enabled directly on approved | (same as above) | (same) | (same) | (same) |
Gate-resolution rule
effective gate = cycle.payout_requires_verification
?? product.payout_requires_verification
?? falseCycle override beats product default. The product is the policy ("our internal app needs verification"); the cycle is the exception ("…except the maintenance sweep, just pay on approval").
Acceptance criteria
- AC-1 — Given the gate is active and the issue is approved but not verified, when admin opens the issue detail, then "Mark paid" is disabled with a hint explaining "Awaiting verification".
- AC-2 — Given the admin clicks "Mark verified", then
issues.statusflips toverified,resolved_atis set, and the reporter is notified. - AC-3 — Given the issue is verified, then "Mark paid" becomes enabled.
- AC-4 — Given the period batch payout runs (F-11),
when the gate is active and an approved issue is not verified,
then the run-batch endpoint excludes it (returns it in
unverifiedIssueIds). - AC-5 — Given the cycle override is
nulland the product default istrue, then the effective gate istrue. - AC-6 — Given the cycle override is
false(explicit) and the product default istrue, then the effective gate isfalse(cycle wins). - AC-7 — Given a pipeline strip is rendered, when the issue is approved, then the "Triaged" stage is filled, "Verified" is the next to-do (if gate active), and "Paid" is the last.
- AC-8 — Given an admin reverts a verified issue to
triagedorin_progress, then "Mark paid" disables again.
Test data / fixtures
- Baseline seed (Hackorda Web has
payout_requires_verification = trueby default — verify on prod) - An approved issue (F-09) on a gate-active cycle
Negative paths
| # | Scenario | Expected behavior |
|---|---|---|
| N-1 | Mark paid on approved-but-not-verified issue (direct API call bypassing UI) | API 400 "issue not verified" (gate enforcement in /payments POST) |
| N-2 | Mark paid via period batch | Issue excluded; returned in unverifiedIssueIds |
| N-3 | Mark verified on a non-approved issue | API allows but the UI flow makes this rare; verify behavior |
| N-4 | Toggle cycle gate off mid-cycle | Existing approved-unpaid issues become payable immediately |
Manual QA checklist
- Open a cycle whose gate is active. Confirm
VerificationGatePanelshows "Required by product" or "Required by this cycle" - Open an approved issue → pipeline shows "Approved" filled, "Verified" next, "Paid" last
- "Mark paid" is disabled with hint "Awaiting verification"
- Click "Mark verified" → status=
verified, resolved_at set, reporter bell pings - "Mark paid" becomes enabled
- Click "Mark paid" →
issue_payoutsrow,issues.payout_status='paid', pipeline fully filled - On a different cycle, override gate to OFF (
VerificationGatePanel) - Open an approved issue on that cycle → pipeline shows 3 stages, "Mark paid" enabled directly
- Run-batch endpoint dry-run: confirm gate-active unverified issues
appear in
unverifiedIssueIds, gate-inactive issues are eligible
Automated test outline
test.describe("F-10 verification gate", () => {
test("gate active → mark paid disabled until verified", async ({ page }) => { … });
test("gate inactive → mark paid enabled directly", async ({ page }) => { … });
test("cycle override beats product default", async ({ request }) => { … });
test("run-batch excludes unverified", async ({ request }) => { … });
test("revert verified disables mark paid again", async ({ page }) => { … });
});Code references
- UI:
src/components/test-cycles/ApprovalPayoutCard.tsx(pipeline strip + gating logic),src/components/test-cycles/admin/VerificationGatePanel.tsx - API:
src/app/api/test-cycles/[id]/issues/[issueId]/route.ts(PATCH for status changes)src/app/api/test-cycles/[id]/issues/[issueId]/payments/route.ts(per-issue payment)src/app/api/admin/payouts/batch-pay/route.tssrc/app/api/admin/payouts/run-batch/route.ts
- Lib:
src/lib/issue-payout.ts→requiresVerification(...) - Schema:
test_cycles.payout_requires_verification,products.payout_requires_verification,ISSUE_STATUSES.VERIFIED
Events emitted (proposed)
issue.verified—{ issue_id, cycle_id, verified_by }issue.unverified—{ issue_id, reverted_to_status }(if revert path is exercised)payout.gate_blocked—{ issue_id, source: 'run_batch' \| 'batch_pay' \| 'manual' }(when an attempt is denied by the gate)
Open questions / known gaps
- No "bulk verify" UI. Engineers verify one issue at a time.
- No timeline / audit beyond
resolved_at. If we want "who verified when" forensics, we need either averified_bycolumn or rely on events. - Pipeline strip currently only shows on issue detail; not on triage cards. Could add for triage too.