# Farmer Login / Onboarding Routing Fix Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Stop completed farmers from being sent back into the onboarding wizard on login, and allow skipping the wizard after Step 3 (KYC) with resume-from-login later.

**Architecture:** Centralize post-auth routing in `lib/farmer-routing.ts` with explicit handling for every `OnboardingStatus`, fix session resume logic so completed/submitted/scoring sessions are never replaced by `createOrResume(false)`, and add a `wizardDeferred` flag on the session (stored in `stepData.ACCOUNT` or a dedicated meta field) to distinguish “skipped early” from “must finish wizard first”. Login page becomes the hub for “Continue application” when deferred.

**Tech Stack:** Next.js App Router, TypeScript, Vitest unit tests, Playwright e2e, dev mock in `lib/dev-api-mock.ts`, OpenAPI contract in `specs/openapi/bff-api.v1.yaml`

---

## Root Cause Analysis (Login → Onboarding Loop)

| # | Issue | Location | Effect |
|---|-------|----------|--------|
| R1 | `!status` routes to onboarding | `lib/farmer-routing.ts:8` | Any `activeSession` missing `status` always sends user to wizard |
| R2 | `createOrResume(false)` only resumes `in_progress` | `lib/dev-api-mock.ts:235`, OpenAPI spec | Completed/submitted/scoring sessions ignored → **new** `in_progress` session created |
| R3 | Legacy mock session IDs skip `loadSession` | `app/farmers/onboarding/page.tsx:32-35` | Calls `createOrResume(false)` instead of `loadSession(active.id)` for all `session_*` ids |
| R4 | `registered=1` blocks auto-redirect | `app/farmers/login/page.tsx:39` | Authenticated users with completed sessions stuck on login banner until manual OTP |
| R5 | In-memory mock store reset | `lib/dev-api-mock.ts` global store | Dev server restart drops sessions → login looks like new user |
| R6 | Onboarding init only short-circuits `completed` | `app/farmers/onboarding/page.tsx:28-30` | `submitted`/`scoring` users hitting onboarding page can spawn duplicate sessions (R2) |

E2e passes today because Playwright completes wizard + login in one process without restart, and uses consistent phone normalization.

---

## Security Review (Login Flow — Static)

**Score: 9/10 (dev), 8/10 (prod considerations)**

| Check | Result |
|-------|--------|
| Privilege escalation / shell patterns | None in login/onboarding client code |
| Credential exfiltration | None; OTP over same-origin `/api/auth/*` |
| Cookie handling | `httpOnly`, `sameSite: lax` in `lib/auth-cookie.ts` |
| Legacy dev token rejection | Middleware clears stale `dev.*` tokens when not in mock mode |
| Dev OTP displayed in UI | Expected for local dev; gate behind `NODE_ENV !== 'production'` before prod |

No malicious patterns found. Production hardening: hide dev OTP + seeded farmer list on login page when `NODE_ENV === 'production'`.

---

## Feature: Skip Wizard After Step 3 (KYC)

**Behavior:**
1. After KYC verified + saved, show secondary action: **“Finish later — go to login”**.
2. Persist `wizardDeferred: true` and `deferredAfterStep: "KYC"` on session (via PATCH `KYC` step data or dedicated PATCH meta endpoint).
3. `resolveFarmerDestination`: `in_progress` + `wizardDeferred` → `/farmers/dashboard` (partial modules).
4. Login page: when authenticated or after OTP verify, if `in_progress` + `wizardDeferred`, show prominent **“Continue application”** → `/farmers/onboarding` (resumes at `FARM_PROFILE`).
5. Dashboard modules without data show existing empty states (`lib/i18n/dashboard-labels.ts` already has copy).

---

## Task 1: Expand routing helpers + unit tests

**Files:**
- Modify: `lib/farmer-routing.ts`
- Modify: `lib/farmer-routing.test.ts`
- Modify: `lib/api-types.ts` (add optional `wizardDeferred` on step data meta if needed)

**Step 1: Write failing tests**

Add cases to `lib/farmer-routing.test.ts`:

```typescript
it("routes deferred in_progress session to dashboard", () => {
  expect(
    resolveFarmerDestination({
      activeSession: {
        status: "in_progress",
        stepData: { ACCOUNT: { wizardDeferred: true } },
      },
    }),
  ).toBe("/farmers/dashboard");
});

it("routes non-deferred in_progress session to onboarding", () => {
  expect(resolveFarmerDestination({ activeSession: { status: "in_progress" } })).toBe(
    "/farmers/onboarding",
  );
});

it("does not treat submitted/scoring as onboarding", () => {
  expect(resolveFarmerDestination({ activeSession: { status: "submitted" } })).toBe(
    "/farmers/dashboard",
  );
});
```

Add helper:

```typescript
export function isWizardDeferred(auth: FarmerRoutingAuth): boolean {
  const sd = auth.activeSession?.stepData as { ACCOUNT?: { wizardDeferred?: boolean } } | undefined;
  return Boolean(sd?.ACCOUNT?.wizardDeferred);
}

export function canContinueOnboarding(auth: FarmerRoutingAuth): boolean {
  const status = auth.activeSession?.status;
  return status === "in_progress" || status === "submitted" || status === "scoring";
}
```

**Step 2: Run tests — expect FAIL**

Run: `npm run test -- lib/farmer-routing.test.ts`

**Step 3: Implement `resolveFarmerDestination`**

```typescript
export function resolveFarmerDestination(auth: FarmerRoutingAuth): string {
  const session = auth.activeSession;
  if (!session?.status) return "/farmers/onboarding";

  const { status } = session;
  if (status === "in_progress") {
    return isWizardDeferred(auth) ? "/farmers/dashboard" : "/farmers/onboarding";
  }
  if (status === "submitted" || status === "scoring" || status === "completed") {
    return "/farmers/dashboard";
  }
  return "/farmers/apply";
}
```

**Step 4: Run tests — expect PASS**

**Step 5: Commit**

```bash
git add lib/farmer-routing.ts lib/farmer-routing.test.ts
git commit -m "fix: correct farmer post-login routing for deferred and terminal sessions"
```

---

## Task 2: Fix onboarding page session initialization

**Files:**
- Modify: `app/farmers/onboarding/page.tsx`

**Step 1: Write failing Playwright test**

Add to `e2e/tests/onboarding.spec.ts`:

```typescript
test("completed session does not spawn new session on /farmers/onboarding visit", async ({ page }) => {
  const phone = uniqueTestPhone();
  await completeFullWizard(page, phone);
  await loginAfterWizard(page, phone);
  await page.goto("/farmers/onboarding");
  await page.waitForURL("**/farmers/dashboard", { timeout: 20_000 });
});
```

**Step 2: Fix init logic**

Replace init block with:

```typescript
const active = auth.activeSession;
if (!active?.id) {
  await onboarding.createOrResume(false);
} else if (active.status === "completed") {
  router.replace("/farmers/dashboard");
  return;
} else if (active.status === "submitted" || active.status === "scoring") {
  router.replace("/farmers/dashboard");
  return;
} else {
  await onboarding.loadSession(active.id); // always load by id, including dev mock ids
}
```

Remove `isLegacyMockSessionId` gate for init (keep it only in saveStep recovery if still needed).

**Step 3: Run e2e**

Run: `npx playwright test e2e/tests/onboarding.spec.ts --grep "completed session"`

**Step 4: Commit**

```bash
git add app/farmers/onboarding/page.tsx e2e/tests/onboarding.spec.ts
git commit -m "fix: load existing onboarding session instead of createOrResume on revisit"
```

---

## Task 3: Fix dev mock `createOrResume` resume rules

**Files:**
- Modify: `lib/dev-api-mock.ts`
- Test: add unit test file `lib/dev-api-mock.onboarding.test.ts` (optional) or extend contract test

**Step 1: Update POST `/onboarding` handler**

When `!forceNew`, return existing session for **any** non-abandoned status, not only `in_progress`:

```typescript
if (!forceNew) {
  const existingId = sessionsByUser.get(userId);
  const existing = existingId ? sessions.get(existingId) : undefined;
  if (existing && existing.status !== "abandoned") {
    return { status: 200, data: existing };
  }
}
```

**Step 2: Manual verify**

1. Complete wizard with mock enabled
2. Call `POST /api/onboarding` without `forceNew`
3. Confirm same session id returned, status still `completed`

**Step 3: Commit**

```bash
git add lib/dev-api-mock.ts
git commit -m "fix: dev mock createOrResume returns terminal sessions instead of resetting"
```

---

## Task 4: Fix login page redirect logic

**Files:**
- Modify: `app/farmers/login/page.tsx`

**Step 1: Write failing e2e**

```typescript
test("authenticated completed user on login?registered=1 redirects to dashboard", async ({ page }) => {
  const phone = uniqueTestPhone();
  await completeFullWizard(page, phone);
  await page.goto("/farmers/login?registered=1");
  await page.waitForURL("**/farmers/dashboard", { timeout: 20_000 });
});
```

**Step 2: Update useEffect**

```typescript
useEffect(() => {
  if (auth.loading || !auth.isAuthenticated) return;
  const dest = resolveFarmerDestination(auth);
  if (justRegistered && dest === "/farmers/onboarding") return; // still need to finish wizard
  router.replace(dest);
}, [auth.loading, auth.isAuthenticated, justRegistered, auth, router]);
```

**Step 3: Add “Continue application” CTA**

When `canContinueOnboarding(auth) && isWizardDeferred(auth)` (or any in_progress), show button:

```tsx
{auth.isAuthenticated && canContinueOnboarding(auth) && (
  <button onClick={() => router.push("/farmers/onboarding")} ...>
    Continue your application
  </button>
)}
```

Also show after successful OTP when destination is dashboard but session is deferred incomplete (step < RESULT).

**Step 4: Run e2e login tests**

Run: `npx playwright test e2e/tests/farmer-dashboard.spec.ts e2e/tests/onboarding.spec.ts`

**Step 5: Commit**

```bash
git add app/farmers/login/page.tsx e2e/tests/onboarding.spec.ts
git commit -m "fix: login redirects completed farmers to dashboard; add continue-application CTA"
```

---

## Task 5: Add “Finish later” on StepKyc (after step 3)

**Files:**
- Modify: `components/onboarding/steps/StepKyc.tsx`
- Modify: `components/onboarding/OnboardingWizard.tsx` (pass `onDefer` callback)
- Modify: `lib/i18n/wizard-labels.ts` (uz/ru/en strings)
- Modify: `lib/dev-api-mock.ts` (persist `wizardDeferred` in stepData)

**Step 1: Add i18n keys**

```typescript
"kyc.finishLater": { uz: "...", ru: "...", en: "Finish later — go to login" }
"kyc.finishLaterHint": { uz: "...", ru: "...", en: "You can complete the rest from the login page." }
```

**Step 2: Add handler in StepKyc**

After verified, show secondary button:

```typescript
const handleFinishLater = async () => {
  await onSave({ /* kyc fields */, wizardDeferred: true, deferredAt: new Date().toISOString() });
  router.push("/farmers/login?deferred=1");
};
```

Pass `router` via prop or use `useRouter` in StepKyc.

Also PATCH `ACCOUNT` step meta OR store `wizardDeferred` on KYC payload and copy to ACCOUNT in mock handler when KYC completes with flag.

**Step 3: Write e2e test**

```typescript
test("farmer can defer wizard after KYC and resume from login", async ({ page }) => {
  const phone = uniqueTestPhone();
  await authenticateViaApply(page, phone);
  await completeAccountStep(page, phone);
  await completeConsentStep(page);
  await completeKycStep(page);
  await page.getByRole("button", { name: /Finish later/i }).click();
  await page.waitForURL("**/farmers/login?deferred=1");
  // login + land dashboard
  await loginAfterWizard(page, phone); // may need variant for deferred routing
  await page.getByRole("button", { name: /Continue your application/i }).click();
  await expect(page.locator("h2")).toContainText(/Farm Profile|Ferma profili/i);
});
```

**Step 4: Run e2e — expect PASS**

**Step 5: Commit**

```bash
git add components/onboarding/steps/StepKyc.tsx components/onboarding/OnboardingWizard.tsx lib/i18n/wizard-labels.ts
git commit -m "feat: allow deferring onboarding after KYC with resume from login"
```

---

## Task 6: Align apply page routing

**Files:**
- Modify: `app/farmers/apply/page.tsx`

**Step 1:** Use same `resolveFarmerDestination` / deferred helpers (already imported) — verify apply page does not send deferred users back to onboarding.

**Step 2:** Add e2e smoke test for apply → defer path.

**Step 3: Commit**

```bash
git add app/farmers/apply/page.tsx
git commit -m "fix: apply page respects deferred onboarding routing"
```

---

## Task 7: Regression + accessibility

**Files:**
- Modify: `e2e/tests/accessibility.spec.ts` (optional axe run on login with new CTA)

**Step 1:** Run full farmer e2e suite

```bash
npx playwright test e2e/tests/onboarding.spec.ts e2e/tests/farmer-dashboard.spec.ts
npm run test -- lib/farmer-routing.test.ts
```

**Step 2:** Run axe on login page if CTA added

**Step 3: Commit**

```bash
git commit -m "test: cover login routing and KYC defer regression"
```

---

## Verification Checklist

- [ ] Complete full wizard → login → lands on `/farmers/dashboard` (not onboarding)
- [ ] Complete wizard → refresh dev server (mock) → login still routes correctly OR shows clear “start new application” (document R5 limitation)
- [ ] Defer after KYC → login → dashboard with “Continue application”
- [ ] Continue application → resumes at `FARM_PROFILE`
- [ ] In-progress (non-deferred) login → onboarding
- [ ] Seeded demo farmers (`+998901110001`) still land on dashboard
- [ ] `registered=1` with completed session auto-redirects to dashboard

---

## Out of Scope (Document Only)

- Persisting dev mock sessions to disk (R5) — recommend PostgreSQL via core-api for real persistence
- Upstream core-api changes if production API differs from OpenAPI — add contract test against `/auth/me` activeSession shape

---

## Execution Handoff

Plan complete and saved to `docs/plans/2026-06-28-farmer-login-onboarding-routing.md`.

**Two execution options:**

1. **Subagent-Driven (this session)** — fresh subagent per task, review between tasks
2. **Parallel Session (separate)** — new session with `@executing-plans`, batch execution with checkpoints

Which approach?
