Hackorda Docs
Flows

F 05 Test Cycle Create

Status: 🟢 ready to test Owner: Adilet Last updated: 2026-05-08 Last verified on production:

Goal

An admin creates a new test cycle on top of an existing product+version, then configures it — payout rates, verification gate, self-join policy, and member assignments — before flipping it live.

Actors

  • Primary — Admin
  • Secondary — Testers who get added (notified per F-13)

Preconditions

  • An organization, product, and product version exist (F-04).
  • Admin is signed in.
  • (Optional) Users with role_id = 4 (Tester) exist if you want to assign members manually instead of relying on self-join.

Trigger

Admin clicks "New cycle" on /app/admin/test-cycles.

Happy path — Step 1: create cycle (planned)

#ActorUI surfaceActionAPI callDB writesSide effectsSuggested event
1Admin/app/admin/test-cyclesClick "New cycle"
2Admin/app/admin/test-cycles/newFill name, pick org/product/version, dates, description (markdown w/ inline media), open-to-self-join togglePOST /api/admin/test-cyclesINSERT INTO test_cycles (status=planned)cycle.created
3Frontendredirect to /app/admin/test-cycles/:idrender new cycle

Happy path — Step 2: configure cycle

Configuration happens on /app/admin/test-cycles/:id panels. Each panel is a separate write.

#PanelActionAPI callDB writesNotificationEvent
2aMembersPanelAdd tester(s) with role tester / lead / observerPOST /api/admin/test-cycles/:id/testersINSERT INTO testersTESTER_ASSIGNED to each new membercycle.tester_added
2bJoinPolicyPanelToggle "Open to self-join"PATCH /api/admin/test-cycles/:id body { openToSelfJoin: true }UPDATE test_cycles SET open_to_self_join = truecycle.join_policy_changed
2cPayoutRatesPanelOverride per-severity ratesPATCH /api/admin/test-cycles/:id body { metadata: { payout_rates: { rates: {...}, currency: 'KZT' } } }UPDATE test_cycles SET metadata = …cycle.rates_overridden
2dVerificationGatePanelForce gate ON / OFF / use product defaultPATCH /api/admin/test-cycles/:id body { payoutRequiresVerification: true | false | null }UPDATE test_cycles SET payout_requires_verification = …cycle.gate_changed

Happy path — Step 3: cycle goes live

#ActorUI surfaceActionAPI callDB writesSide effectsSuggested event
1AdminStatusControl on cycle detailClick "Activate"PATCH /api/admin/test-cycles/:id body { status: 'active' }UPDATE test_cycles SET status = 'active'CYCLE_STATUS_CHANGED to all memberscycle.status_changed

Acceptance criteria

  • AC-1 — Given the admin fills the create form, when they submit, then the cycle row exists with status='planned' and the admin lands on the cycle detail page.
  • AC-2 — Given the admin adds a tester via MembersPanel, when they click Add, then a testers row exists with the chosen role and the added user receives a TESTER_ASSIGNED notification (see F-13).
  • AC-3 — Given openToSelfJoin = true, when a non-member with role Tester opens /app/test-cycles/browse, then this cycle is visible as joinable.
  • AC-4 — Given per-cycle payout rates are saved, when a tester files an issue with severity high, then the suggested payout matches the cycle's override (not the system default).
  • AC-5 — Given the cycle is activated, when an existing member loads any page, then they receive a CYCLE_STATUS_CHANGED bell notification.

Test data / fixtures

  • Baseline seed (org + product + version)
  • One additional tester user (role=Tester) for member assignment

Negative paths

#ScenarioExpected behavior
N-1Submit create form without picking a productUI validation blocks
N-2Add same user twice as testerAPI enforces unique (test_cycle_id, user_id) → 400
N-3endsAt < startsAtUI validation blocks
N-4Activate cycle with zero testers AND openToSelfJoin = falseCycle activates but is unreachable for testers — warn in UI (open question)
N-5Lead removes themselvesAPI allows; UI permission falls back to read-only on next load

Manual QA checklist

  • Create new cycle "QA Test Cycle" tied to baseline product + version, status=planned
  • Land on cycle detail page; status badge shows "Planned"
  • Add a Tester user via MembersPanel (role=tester); verify row in testers
  • Toggle "Open to self-join" ON; verify test_cycles.open_to_self_join = true
  • Override high severity rate to 1500 KZT; verify metadata.payout_rates.rates.high = 150000 (cents)
  • Force verification gate ON; verify payout_requires_verification = true
  • Click "Activate" → status flips to "Active"
  • Sign in as the assigned tester → cycle appears in their /app/test-cycles list
  • As a non-member with Tester role, sign in → cycle appears in /app/test-cycles/browse

Automated test outline

test.describe("F-05 cycle create+configure", () => {
  test("create planned cycle", async ({ page }) => { … });
  test("configure rates + gate + members", async ({ page, request }) => { … });
  test("activate fires status change notifications", async ({ request }) => { … });
});

Code references

  • Pages:
    • src/app/app/admin/test-cycles/page.tsx (list)
    • src/app/app/admin/test-cycles/new/page.tsx (create)
    • src/app/app/admin/test-cycles/[id]/page.tsx (detail w/ panels)
    • src/app/app/admin/test-cycles/[id]/edit/page.tsx
  • Components: MembersPanel, JoinPolicyPanel, PayoutRatesPanel, VerificationGatePanel, StatusControl
  • API:
    • src/app/api/admin/test-cycles/route.ts (GET, POST)
    • src/app/api/admin/test-cycles/[id]/route.ts (GET, PATCH, DELETE)
    • src/app/api/admin/test-cycles/[id]/testers/route.ts (POST)
    • src/app/api/admin/test-cycles/[id]/testers/[testerId]/route.ts (DELETE)
  • Hooks: src/hooks/test-cycles/cycles.ts
  • Schema: test_cycles, testers, TEST_CYCLE_STATUSES, TESTER_ROLES, metadata.payout_rates

Events emitted (proposed)

  • cycle.created{ cycle_id, org_id, product_id, version_id, created_by }
  • cycle.tester_added{ cycle_id, target_user_id, cycle_role, added_by }
  • cycle.tester_removed{ cycle_id, target_user_id, removed_by }
  • cycle.join_policy_changed{ cycle_id, open_to_self_join, changed_by }
  • cycle.rates_overridden{ cycle_id, currency, rates }
  • cycle.gate_changed{ cycle_id, payout_requires_verification }
  • cycle.status_changed{ cycle_id, from_status, to_status }

Open questions / known gaps

  • Cycles can be activated with zero members + open_to_self_join=false — no UI warning. (Add a guard?)
  • No bulk member import.
  • No "duplicate this cycle" shortcut for periodic shake-downs.

On this page