# Affiliate & Admin — Phased Implementation Roadmap

Concrete ticket breakdown for Admin + Affiliate features. Maps to existing code in `api/`, `admin/`, and `affiliate/`.

**Repos:** API (`nextpayday_borrower`), Admin (`nextpayday_admin`), Affiliate (`nextpayday_affiliates`)

---

## Current baseline (already shipped)

| Area | Exists today |
|------|----------------|
| Affiliate signup/login | `POST /auth/user/register`, `POST /auth/user/login` (`login_type=affiliate`) |
| Admin approval gate | `is_verified_affiliate`; affiliate `PendingApproval` screen |
| Custom commission | `custom_commission_rate`; admin `PartnerVettingCard` + `/partners/[id]` |
| Withdrawals | `POST /affiliate/withdraw` (inline bank fields); admin `/payouts` approve/reject |
| Permissions | JSON `permissions` on user; affiliate sidebar gating |
| Supervisor hierarchy | `supervisor_id`, `GET /affiliate/team`, admin assign supervisor |
| Global loans (affiliate) | `GET /affiliate/loans` with permission scoping |
| Employers (read-only affiliate) | `GET /affiliate/employers`; admin full CRUD |
| Email OTP (borrower) | `initiate-signup` → `verify-otp` pattern in Auth domain |
| BVN/bank (borrower) | `POST /profile/verify/stage-two`; Paystack resolve |

---

## Phase 1 — Trust & payouts foundation

**Goal:** Verified email + saved payout profile + clearer admin approval.  
**Ship independently:** Yes  
**Estimated effort:** 1–2 sprints

### 1.1 Email verification for affiliate registration

#### API tickets

| ID | Ticket | Details |
|----|--------|---------|
| P1-API-01 | Migration: ensure email fields | Confirm `users.email_verified`, `email_verification_code` (already exist). Add `email_verified_at` timestamp if missing. |
| P1-API-02 | `POST /auth/affiliate/send-verification-email` | Authenticated or post-register: generate OTP via `OtpService`, send via `EmailService::sendVerificationOtp`, store code + expiry on user. |
| P1-API-03 | `POST /auth/affiliate/verify-email` | Body: `{ email, code }`. Set `email_verified=true`, clear code. Return token if mid-registration. |
| P1-API-04 | Gate affiliate register | After `userRegister` with `type=affiliate`: require email verify before treating signup complete OR send OTP immediately and return `{ requires_email_verification: true }`. |
| P1-API-05 | Gate affiliate login (optional) | Block login if `!email_verified` with 403 + message (keep admin `is_verified_affiliate` as second gate). |
| P1-API-06 | Tests | `AffiliateEmailVerificationTest`: send, verify, invalid code, expired code. |

**Reuse:** `app/Services/Auth/OtpService.php`, `app/Domains/Auth/Actions/VerifyOtpAction.php`, `signup-auth-flow.md`

#### Affiliate UI tickets

| ID | Ticket | File(s) |
|----|--------|---------|
| P1-AFF-01 | Email verify step after register | `affiliate/src/app/page.tsx` — add step/modal: enter 6-digit OTP |
| P1-AFF-02 | Resend OTP + error states | Same page; link to resend endpoint |
| P1-AFF-03 | Settings: email status | `affiliate/src/app/dashboard/settings/page.tsx` — show verified badge or “Verify email” CTA |

#### Admin UI tickets

| ID | Ticket | File(s) |
|----|--------|---------|
| P1-ADM-01 | Show email verified on partner detail | `admin/src/app/partners/[id]/page.tsx` — badge next to email |

---

### 1.2 Bank account + BVN for withdrawal profile

#### API tickets

| ID | Ticket | Details |
|----|--------|---------|
| P1-API-07 | Migration: `affiliate_payout_profiles` | `user_id`, `bvn` (encrypted), `bank_code`, `bank_name`, `account_number`, `account_name`, `is_verified`, `verified_at`, timestamps |
| P1-API-08 | `GET /affiliate/payout-profile` | Return saved profile (mask BVN: `***1234`) |
| P1-API-09 | `POST /affiliate/payout-profile` | Body: `bvn`, `bank_code`, `account_number`. Resolve account via Paystack; optionally verify BVN (reuse `VerifyStageTwoAction` logic or Paystack dedicated API). |
| P1-API-10 | `PUT /affiliate/payout-profile` | Update bank; re-verify account name; reset `is_verified` until admin/auto confirm |
| P1-API-11 | Enforce profile on withdraw | `AffiliateController::withdraw()` — require active payout profile; prefill bank fields from profile instead of request body (or validate request matches profile) |
| P1-API-12 | `GET /affiliate/banks` | List Paystack banks for dropdown (or proxy existing admin/borrower bank list endpoint) |
| P1-API-13 | Tests | `AffiliatePayoutProfileTest`: create, update, withdraw without profile fails |

**Reuse:** `WithdrawalService.php`, `PaystackClient.php`, borrower `VerifyStageTwoAction.php`

#### Affiliate UI tickets

| ID | Ticket | File(s) |
|----|--------|---------|
| P1-AFF-04 | Payout profile section in Settings | `affiliate/src/app/dashboard/settings/page.tsx` — BVN, bank select (API-driven), account number, resolved account name |
| P1-AFF-05 | Refactor earnings withdraw modal | `affiliate/src/app/dashboard/earnings/page.tsx` — use saved profile; show “Update in Settings” if missing; remove hardcoded bank list |
| P1-AFF-06 | Block withdraw CTA if no profile | Earnings page — disable withdraw + link to settings |

#### Admin UI tickets

| ID | Ticket | File(s) |
|----|--------|---------|
| P1-ADM-02 | Show payout profile on partner detail | `admin/src/app/partners/[id]/page.tsx` — masked BVN, bank, account name |

---

### 1.3 Admin approve / decline with commission

#### API tickets

| ID | Ticket | Details |
|----|--------|---------|
| P1-API-14 | Extend `POST /admin/users/{id}/update-affiliate-status` | Add `action: approve \| decline`, `review_notes`, `decline_reason`. On decline: `is_verified_affiliate=false`, optional `affiliate_status=declined`. |
| P1-API-15 | Notification hooks (optional) | Email affiliate on approve/decline via `EmailService` |

#### Admin UI tickets

| ID | Ticket | File(s) |
|----|--------|---------|
| P1-ADM-03 | Approve / Decline buttons | `admin/src/app/partners/[id]/page.tsx` + consolidate `PartnerVettingCard.tsx` |
| P1-ADM-04 | Commission edit on approve modal | Inline rate + tier preview (Phase 2); notes field |
| P1-ADM-05 | Decline reason + notes | Required on decline; show in partner detail history |

---

### Phase 1 acceptance criteria

- [ ] Affiliate must verify email before full portal access (or before admin review queue)
- [ ] Affiliate can save BVN + bank account in Settings
- [ ] Withdrawal uses saved profile; cannot withdraw without verified profile
- [ ] Admin can approve or decline with commission rate and notes
- [ ] Feature tests pass for email verify + payout profile + withdraw guard

---

## Phase 2 — Commission tiers & auto progression

**Goal:** Configurable tiers with automatic rate assignment; admin override preserved.  
**Depends on:** Phase 1 (optional but recommended for clean approval UX)  
**Estimated effort:** 1–2 sprints

### Business rules (confirm with product)

```
Tier One:   Borrowers 0–100,   Loans 0–100,   Rate 1.5%
Tier Two:   Borrowers 100–300, Loans 100–300, Rate 2.0%
Tier Three: Borrowers 300–500, Loans 300–500, Rate 2.5%
```

**Decisions needed:**
- Boundary inclusivity (is exactly 100 Tier 1 or 2?)
- Both thresholds must be met, or max of the two?
- All-time counts vs rolling window?
- `custom_commission_rate` always wins over tier?

### API tickets

| ID | Ticket | Details |
|----|--------|---------|
| P2-API-01 | Migration: `commission_tiers` | `name`, `min_borrowers`, `max_borrowers`, `min_loans`, `max_loans`, `commission_rate`, `sort_order`, `is_active` |
| P2-API-02 | Migration: seed default tiers | Seeder matching Tier One/Two/Three above |
| P2-API-03 | `users.commission_tier_id` | FK nullable; set by auto job or admin |
| P2-API-04 | Admin CRUD tiers | `GET/POST/PUT/DELETE /admin/commission-tiers` |
| P2-API-05 | `CommissionTierService::resolveForUser($user)` | Count referrals + disbursed loans; return matching tier |
| P2-API-06 | Update `AffiliateService` commission calc | Use `custom_commission_rate ?? tier.rate ?? setting default` |
| P2-API-07 | Artisan command / scheduler | `affiliate:recalculate-tiers` — nightly or on loan disburse event |
| P2-API-08 | Expose tier in stats | `GET /affiliate/stats` — add `commission_tier`, `next_tier`, progress counts |
| P2-API-09 | Tests | Tier boundary tests, override tests, recalc command test |

### Admin UI tickets

| ID | Ticket | File(s) |
|----|--------|---------|
| P2-ADM-01 | Commission tiers settings tab | `admin/src/app/settings/page.tsx` — Financial tab or new “Commission Tiers” sub-tab |
| P2-ADM-02 | Tier CRUD table | Add/edit/delete tiers with borrower/loan ranges |
| P2-ADM-03 | Partner detail: tier + override | `admin/src/app/partners/[id]/page.tsx` — show current tier, manual tier assign, custom rate override |

### Affiliate UI tickets

| ID | Ticket | File(s) |
|----|--------|---------|
| P2-AFF-01 | Tier badge on dashboard | `affiliate/src/app/dashboard/page.tsx` — current tier, rate, progress to next |
| P2-AFF-02 | Tier info on earnings | `affiliate/src/app/dashboard/earnings/page.tsx` — effective rate explanation |

### Phase 2 acceptance criteria

- [ ] Admin can configure tier ranges and rates
- [ ] System auto-assigns tier based on counts (per agreed rules)
- [ ] `custom_commission_rate` overrides tier when set
- [ ] Affiliate sees current tier and progress on dashboard

---

## Phase 3 — Supervisor portfolio visibility

**Goal:** Supervisors see downstream affiliates’ loan books and repayment health.  
**Depends on:** Existing supervisor hierarchy (`supervisor_id`, `/affiliate/team`)  
**Estimated effort:** 1 sprint

### API tickets

| ID | Ticket | Details |
|----|--------|---------|
| P3-API-01 | `GET /affiliate/team/{memberId}/loans` | Paginated loans for supervised affiliate only; permission: `affiliate.manage_partners` or `type=supervisor` |
| P3-API-02 | `GET /affiliate/team/{memberId}/portfolio` | Summary: total outstanding, due this month, overdue count, recent repayments |
| P3-API-03 | `GET /affiliate/team/portfolio-logs` | Aggregated due/outstanding across all supervised members; filter by status |
| P3-API-04 | Scope existing `GET /affiliate/loans` | When user is supervisor without `view_all_loans`, return supervised tree only |
| P3-API-05 | Tests | Supervisor cannot see non-supervised affiliate data |

**Reuse:** `RepaymentSchedule` model, admin repayment views, `AffiliateController::allLoans()`

### Affiliate UI tickets

| ID | Ticket | File(s) |
|----|--------|---------|
| P3-AFF-01 | Team member drill-down | `affiliate/src/app/dashboard/team/page.tsx` — click member → `/dashboard/team/[id]` |
| P3-AFF-02 | Member loan book page | `affiliate/src/app/dashboard/team/[id]/page.tsx` — loans table |
| P3-AFF-03 | Portfolio logs page | `affiliate/src/app/dashboard/team/[id]/portfolio/page.tsx` or tab — due/outstanding |
| P3-AFF-04 | Page-level permission guards | `team/page.tsx`, `loans/page.tsx`, `employers/page.tsx` — redirect if missing permission |
| P3-AFF-05 | Sidebar: Portfolio link for supervisors | `affiliate/src/components/dashboard/Sidebar.tsx` |

### Admin UI tickets

| ID | Ticket | File(s) |
|----|--------|---------|
| P3-ADM-01 | Supervisor tree on partner detail | `admin/src/app/partners/[id]/page.tsx` — list supervised affiliates + link to their stats |

### Phase 3 acceptance criteria

- [ ] Supervisor sees only their team’s loans
- [ ] Portfolio view shows due payments and outstanding balances
- [ ] No cross-team data leakage (API + tests)

---

## Phase 4 — Affiliate employer submission workflow

**Goal:** Affiliate proposes employer; admin completes financial setup and approves.  
**Estimated effort:** 1–2 sprints

### API tickets

| ID | Ticket | Details |
|----|--------|---------|
| P4-API-01 | Migration: `employer_submissions` | `submitted_by_user_id`, org fields (`name`, `email`, `phone`, `state`, `address`, `sector`, `notes`), `status` (`pending`, `approved`, `rejected`), `employer_id` (nullable FK after approval), `reviewed_by`, `review_notes` |
| P4-API-02 | Permission: `employer.submit` | Add to `RolesAndPermissionsSeeder` + `AffiliateRolesSeeder` |
| P4-API-03 | `POST /affiliate/employer-submissions` | Create pending submission; permission middleware |
| P4-API-04 | `GET /affiliate/employer-submissions` | Affiliate’s own submissions + status |
| P4-API-05 | Admin list pending | `GET /admin/employer-submissions?status=pending` |
| P4-API-06 | Admin approve | `POST /admin/employer-submissions/{id}/approve` — create/update `Employer` with financial params from admin payload; link submission |
| P4-API-07 | Admin reject | `POST /admin/employer-submissions/{id}/reject` — notes required |
| P4-API-08 | Tests | Submit, approve creates employer, reject, permission denied |

### Affiliate UI tickets

| ID | Ticket | File(s) |
|----|--------|---------|
| P4-AFF-01 | Submit employer form | `affiliate/src/app/dashboard/employers/submit/page.tsx` or modal on employers page |
| P4-AFF-02 | My submissions list | Status badges: pending / approved / rejected |
| P4-AFF-03 | Sidebar permission | Show “Add Employer” when `employer.submit` |
| P4-AFF-04 | Wire employers search filter | Fix non-functional search on `employers/page.tsx` |

### Admin UI tickets

| ID | Ticket | File(s) |
|----|--------|---------|
| P4-ADM-01 | Pending submissions queue | `admin/src/app/employers/submissions/page.tsx` or tab on employers page |
| P4-ADM-02 | Review modal | Org details read-only + admin financial fields (rates, max amount, tenure) |
| P4-ADM-03 | Approve → opens employer editor pre-filled | Link to existing `admin/src/app/employers/page.tsx` create flow |

### Phase 4 acceptance criteria

- [ ] Affiliate with permission can submit employer org details
- [ ] Admin sees pending queue and completes financial parameters on approve
- [ ] Approved employer appears in affiliate employer list
- [ ] Rejected submissions show reason to affiliate

---

## Cross-cutting tickets (any phase)

| ID | Ticket | Details |
|----|--------|---------|
| X-01 | Consolidate partner vetting UI | Merge `PartnerVettingCard` logic into `/partners/[id]` patterns |
| X-02 | Admin sidebar permission filtering | Wire `Sidebar.tsx` `permission` keys to `useProfile()` permissions |
| X-03 | API bank list endpoint | Shared `GET /banks` for admin, affiliate, borrower |
| X-04 | Encrypt BVN at rest | Laravel `encrypted` cast on payout profile + user BVN |
| X-05 | Audit log | Log approve/decline, tier changes, payout profile updates |

---

## Suggested sprint order

| Sprint | Deliverables |
|--------|--------------|
| **Sprint A** | P1-API-01–06, P1-AFF-01–03, P1-ADM-01 (email verify) |
| **Sprint B** | P1-API-07–13, P1-AFF-04–06, P1-ADM-02 (payout profile) |
| **Sprint C** | P1-API-14–15, P1-ADM-03–05 (approve/decline) |
| **Sprint D** | Phase 2 full (tiers) |
| **Sprint E** | Phase 3 full (supervisor portfolio) |
| **Sprint F** | Phase 4 full (employer submission) |

---

## Key files reference

### API
```
routes/api/affiliate.php
routes/api/auth.php
routes/api/admin.php
app/Domains/Financial/Controllers/AffiliateController.php
app/Domains/Financial/Services/AffiliateService.php
app/Domains/Financial/Services/WithdrawalService.php
app/Domains/Auth/Controllers/AuthController.php
app/Domains/Auth/Models/User.php
app/Domains/Admin/Controllers/BorrowerController.php
app/Domains/Admin/Controllers/EmployerController.php
app/Domains/Borrower/Profile/Models/Employer.php
database/seeders/SettingSeeder.php
database/seeders/RolesAndPermissionsSeeder.php
```

### Admin
```
src/app/partners/[id]/page.tsx
src/components/borrowers/PartnerVettingCard.tsx
src/app/settings/page.tsx
src/app/payouts/page.tsx
src/app/employers/page.tsx
src/components/dashboard/Sidebar.tsx
```

### Affiliate
```
src/app/page.tsx
src/app/dashboard/settings/page.tsx
src/app/dashboard/earnings/page.tsx
src/app/dashboard/team/page.tsx
src/app/dashboard/loans/page.tsx
src/app/dashboard/employers/page.tsx
src/components/dashboard/Sidebar.tsx
src/lib/permissions.ts
src/components/PendingApproval.tsx
```

---

## Open product questions (resolve before Phase 2 & 4)

1. **Tier boundaries:** Inclusive or exclusive at 100, 300, 500?
2. **Tier metrics:** Count disbursed loans only, or all booked?
3. **Email verify vs admin verify:** Both required, or either order?
4. **BVN:** Must match affiliate legal name? Re-verify on bank change?
5. **Employer submission:** Can affiliate edit/resubmit after reject?
6. **Supervisor:** Is `type=supervisor` distinct from affiliate with `manage_partners` permission?

---

*Last updated: June 2026*
