Hackorda Docs

Approval Process

The operator's runbook for taking a single bug from "tester just filed it" to "tester sees the money land". It accounts for the role split between admin and super_admin, the verification gate, and the self-dealing rules — all of which now sit between approval and a paid-out tester.

If you only need the per-step API surface for one of these stages, jump to the flow doc: F-09 triage, F-10 verification gate, F-11 payouts, F-12 tester balance. This doc stitches them into one procedure for the human running the cycle.


1. Who does what

The two tiers exist so that everyday QA work and money-moving actions need different levels of trust. Promotion happened automatically — every account that was admin before the role split is now super_admin. Anyone created after that ships as a plain admin unless a super-admin promotes them.

StepPlain admin CANPlain admin CANNOT
Triage — reject an issue
Triage — request info
Triage — reclassify severity
Triage — approve a payoutneeds super_admin
Verify a fix (fixed → verified)
Toggle cycle gate
Disburse payouts (per-issue / per-tester / period batch)needs super_admin
Change a user's roleneeds super_admin
Open /app/admin/auditsuper-admin only

Two rules hold for everyone — including super-admins:

  • Self-dealing block. You cannot approve OR pay out a bug you reported. The API returns 403 with selfReportedIssueIds populated. There is no override.
  • Closed-out auto-handling. If you close an issue out as duplicate, cant_reproduce, or wont_fix, any pending or approved payout auto-rejects. Paid payouts are never auto-reverted — you have to void them explicitly.

2. The shape of an "approval"

When the triage approve decision lands, the database row changes — nothing else moves. Specifically: issues.payoutStatus = 'approved', payoutDecidedBy = <you>, payoutDecidedAt = NOW(), and payoutAmountCents + payoutCurrency get set (either from your explicit override, or recomputed from the cycle's effective rates for the issue's severity). One row is appended to audit_log with action issue.payout_decision. The reporter gets a bell ping.

That is the entire effect of "approve". No money moves at this step. Disbursement is a separate, super-admin-only action (see step 4).


3. Step 1 — Tester files the bug

The tester runs through F-08: submit the form with title, body, repro steps, expected/actual, severity, type, and at least one attachment. On submit the issue lands with status = 'open' and payoutStatus = 'pending'. It now sits in the cross-cycle triage queue, waiting on you.


4. Step 2 — Triage

Open /app/admin/triage. You see a two-pane layout — the queue on the left, the full preview (title, body, attachments inline, AI hints, reporter context) on the right. On mobile the card opens as a fullscreen overlay.

Keyboard shortcuts (desktop)

KeyAction
jNext card
kPrevious card
sSkip (same as j without deciding)
eApprove (super-admin only — blocked on your own filings)
rReject (opens reason dialog)
oOpen issue detail in a new tab

The three decisions

DecisionWhoWhat it setsWhat the tester sees
Approvesuper-admin onlypayoutStatus = 'approved', payoutDecidedBy, payoutDecidedAt, payoutAmountCents, payoutCurrencyBell: "Issue approved: <title>. Payout: <amount>." Issue rolls into their Pending verification or Available bucket on /app/test-cycles/balance.
Rejectadmin OKpayoutStatus = 'rejected', status = 'rejected'Bell: "Issue rejected" + the reason you typed.
Request infoadmin OKpayoutStatus = 'info_requested', posts your question as a commentBell: "Clarification needed". When they reply, payoutStatus auto-flips back to pending and the card re-enters the queue.

Severity is reclassifiable inline from the right pane — pick a different severity and the payout amount recomputes from the cycle's effective rates for that severity (same source as the file-issue path). Amount and currency are also overridable directly in the approve dialog if you want to deviate from the rate.

What "approve" enforces under the hood

  • Tier check. API requires super_admin. A plain admin pressing e gets a 403 toast.
  • Self-dealing. If you reported the issue yourself, the approve path returns 403 with the message "You can't approve a payout for a bug you reported." There is no UI escape hatch. Pass it to another super-admin.
  • Allowed transitions. You cannot re-approve a paid issue — only paid → void is allowed once money has moved.
  • Audit. Every approve, reject, and request-info writes one issue.payout_decision row to audit_log with before and after snapshots.

Tip — reclassifying a high down to low on approve. When you change severity on the triage card, the amount silently recomputes for the new severity unless you also passed an explicit amountCents. So if you both reclassify and want a custom amount, set the amount in the same approve click — don't expect a stale high-tier number to survive.

See F-09 for the full API surface and the negative-path matrix.


5. Step 3 — Verification gate (when active)

If the cycle has payoutRequiresVerification = true — either by direct setting or by inheriting from the product — an approved issue is not eligible for payout until its status is moved to verified. This separates "the bug is real and we'll pay" (your approve at step 2) from "the fix landed and we re-tested" (step 3).

If the gate is inactive, skip this step — approved issues are immediately eligible for the next batch.

Where the work happens

Two surfaces feed the gate:

  1. Bulk verification queue on the cycle Overview tab. When at least one issue in the cycle sits at status = 'fixed', the Overview tab shows an amber "Awaiting fix verification (N)" card. Each row offers Fix verified (sets status = 'verified', posts an audit comment) or Still broken (requires a ≥5-character reason, posts the comment, flips the issue back to in_progress). Rows drop off the card immediately on action; the card hides itself when nothing is awaiting.
  2. The reporter's own verify-fix banner. The original reporter can also verify or reopen a fix from their issue detail page (see commit f5d2a9a for the recently-added reporter-verify path).

What the gate means for your payout queue

While the gate is active:

  • An approved-but-unverified issue counts in the tester's Pending verification bucket on /app/test-cycles/balance. It will not be picked up by a period batch payout — the run-batch endpoint excludes it and reports the id in unverifiedIssueIds.
  • Per-issue "Mark paid" is disabled with the hint "Awaiting verification" on the issue detail page.
  • Once you mark verified, the issue rolls into Available and becomes eligible for the next disbursement.

See F-10 for the gate-resolution rule and edge cases.


6. Step 4 — Disburse

Open /app/admin/payouts. The page is super-admin only — a plain admin gets the page chrome but every action returns 403.

The pay queue

The queue groups eligible issues by tester. "Eligible" means payoutStatus = 'approved' and (gate inactive OR status = 'verified'). Aggregate totals (by currency) sit in the header card; per-tester totals are on each row.

Three disbursement paths

In order of preference, smallest-blast-radius first when correcting:

  1. Period batch (RunBatchPanelPOST /api/admin/payouts/run-batch) — preferred. Pick a date range (Today / Last 7 days / This month / Last month / custom), pick a method (bank_transfer / kaspi / cash / other), reference, notes. Hit dry-run first — the preview lists eligibleCount, totals by currency, tester count, failedIssueIds, unverifiedIssueIds. When it looks right, commit. One payout_batches row is created, N issue_payouts rows are inserted under it, N issues flip to paid, and N bell pings fan out.
  2. Per-tester batch (BatchPayDialogPOST /api/admin/payouts/batch-pay) — pay one tester's outstanding queue in one click. Same dialog (method, reference, notes) but scoped to one set of issueIds.
  3. Per-issue manual payment (ApprovalPayoutCardPOST /api/test-cycles/:id/issues/:issueId/payments) — for one-off corrections.

What disburse enforces under the hood

  • Tier check. Both run-batch and batch-pay require super_admin.
  • Self-payment block. The endpoint scans the candidate set for reportedBy === <you>. If any match, it returns 403 with selfReportedIssueIds. No partial pay — the whole call is refused so you can re-form the set. (The idempotency key is released so you can retry cleanly.)
  • Gate enforcement. Approved-unverified issues are filtered out of run-batch and surfaced in unverifiedIssueIds. Per-issue payment on an unverified issue returns 400.
  • Idempotency key. run-batch and batch-pay both require an Idempotency-Key header. The UI generates one per click; if you retry the same call with the same key, the server returns the original response instead of paying twice.
  • Audit. Every batch writes one payout.batch_paid row to audit_log.

What the tester sees after the batch commits

  • Their payoutStatus flips to paid for each issue.
  • A row appears in issue_payouts with the method + reference (and batch_id linking it to the batch).
  • A bell notification per paid issue: "Paid: <title> via Kaspi · ref TST-2026-W19" (or whatever method/reference you used).

See F-11 for the full API surface, dry-run shape, and the void path.


7. Step 5 — Tester sees it

The tester opens /app/test-cycles/balance. The 5-column header now shows the paid issue in Total paid instead of Available; their issue history table picks up paid_at, payment method, and reference. They can hit Export CSV to download a personal-accounting file (balance.csv — one row per non-draft issue, with payment method and reference rendered as friendly labels).

See F-12 for the bucket definitions and the multi-currency rendering.


8. The audit trail

Every sensitive action above writes one row to audit_log:

Action verbFired by
issue.payout_decisionTriage approve / reject / request_info
issue.status_changedMark verified / mark fixed / reopen / etc.
payout.batch_paidPeriod batch + per-tester batch commits
cycle.status_changedCycle lifecycle transitions
user.role_changedRole changes via the users admin page

Open /app/admin/audit (super-admin only). The screen shows newest-first rows with a coloured action tint, the actor's name, the entity (issue / cycle / user), and a before → after diff of the fields that changed. The filter row lets you scope by action and by actor — useful when an owner wants "every payout decision in the last 7 days" or "everything Babur did yesterday".

Audit writes are fire-and-forget from the request path — they never block or fail the primary action. If you don't see an entry within a few seconds, refresh the audit page; if it still doesn't show, the underlying mutation may have rolled back.


9. One-page checklist — super-admin runbook

A compact tick-through for the operator who's settling a cycle.

  • Open /app/admin/triage — work the queue. For each card:
    • Read the body and attachments. Reclassify severity if needed.
    • Approve (e) for real bugs. Note: you'll be blocked on your own filings — pass those to another super-admin.
    • Reject (r) with a one-sentence reason for the noise.
    • Request info for the ambiguous ones — the reporter's reply will requeue the card automatically.
  • If the cycle's verification gate is active:
    • Open the cycle Overview tab → "Awaiting fix verification" card.
    • Fix verified for each issue where the fix actually lands; Still broken with a ≥5-char reason for the ones that don't.
  • Open /app/admin/payouts:
    • Skim the aggregate header — totals match what you expected?
    • Open RunBatchPanel, pick the period, fill method + reference + notes.
    • Dry-run first. Read the preview. Check failedIssueIds is empty. Check unverifiedIssueIds is the set you expect.
    • Commit the batch. Note the batchId from the success toast.
  • Spot-check /app/admin/audit:
    • Filter by action = "Payout batch paid". Confirm the new batch row.
    • Filter by action = "Payout decided" + actor = you. Confirm the triage decisions you made today are in the trail.
  • (Optional) Sign in as one of the paid testers — confirm /app/test-cycles/balance shows the new "Total paid" total and the bell carries the payout notification.

Cross-references

On this page