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)
| # | Actor | UI surface | Action | API call | DB writes | Side effects | Suggested event |
|---|---|---|---|---|---|---|---|
| 1 | Admin | /app/admin/test-cycles | Click "New cycle" | — | — | — | — |
| 2 | Admin | /app/admin/test-cycles/new | Fill name, pick org/product/version, dates, description (markdown w/ inline media), open-to-self-join toggle | POST /api/admin/test-cycles | INSERT INTO test_cycles (status=planned) | — | cycle.created |
| 3 | Frontend | redirect to /app/admin/test-cycles/:id | render new cycle | — | — | — | — |
Happy path — Step 2: configure cycle
Configuration happens on /app/admin/test-cycles/:id panels. Each panel
is a separate write.
| # | Panel | Action | API call | DB writes | Notification | Event |
|---|---|---|---|---|---|---|
| 2a | MembersPanel | Add tester(s) with role tester / lead / observer | POST /api/admin/test-cycles/:id/testers | INSERT INTO testers | TESTER_ASSIGNED to each new member | cycle.tester_added |
| 2b | JoinPolicyPanel | Toggle "Open to self-join" | PATCH /api/admin/test-cycles/:id body { openToSelfJoin: true } | UPDATE test_cycles SET open_to_self_join = true | — | cycle.join_policy_changed |
| 2c | PayoutRatesPanel | Override per-severity rates | PATCH /api/admin/test-cycles/:id body { metadata: { payout_rates: { rates: {...}, currency: 'KZT' } } } | UPDATE test_cycles SET metadata = … | — | cycle.rates_overridden |
| 2d | VerificationGatePanel | Force gate ON / OFF / use product default | PATCH /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
| # | Actor | UI surface | Action | API call | DB writes | Side effects | Suggested event |
|---|---|---|---|---|---|---|---|
| 1 | Admin | StatusControl on cycle detail | Click "Activate" | PATCH /api/admin/test-cycles/:id body { status: 'active' } | UPDATE test_cycles SET status = 'active' | CYCLE_STATUS_CHANGED to all members | cycle.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 atestersrow exists with the chosen role and the added user receives aTESTER_ASSIGNEDnotification (see F-13). - AC-3 — Given
openToSelfJoin = true, when a non-member with roleTesteropens/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_CHANGEDbell notification.
Test data / fixtures
- Baseline seed (org + product + version)
- One additional tester user (role=Tester) for member assignment
Negative paths
| # | Scenario | Expected behavior |
|---|---|---|
| N-1 | Submit create form without picking a product | UI validation blocks |
| N-2 | Add same user twice as tester | API enforces unique (test_cycle_id, user_id) → 400 |
| N-3 | endsAt < startsAt | UI validation blocks |
| N-4 | Activate cycle with zero testers AND openToSelfJoin = false | Cycle activates but is unreachable for testers — warn in UI (open question) |
| N-5 | Lead removes themselves | API 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
highseverity rate to 1500 KZT; verifymetadata.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-cycleslist - 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.