Skip to content

Instantly share code, notes, and snippets.

@agoiabel
Created January 30, 2026 20:18
Show Gist options
  • Select an option

  • Save agoiabel/0f16b459d7022353d30a6b6677491502 to your computer and use it in GitHub Desktop.

Select an option

Save agoiabel/0f16b459d7022353d30a6b6677491502 to your computer and use it in GitHub Desktop.
# Community Commission Framework
> A complete guide to the community commission system in Propel - for Project Managers and Developers.
---
## Quick Links
| Audience | Section |
|----------|---------|
| **Project Managers** | [Executive Summary](#executive-summary) \| [User Guide](#user-guide-for-project-managers) \| [FAQ](#frequently-asked-questions) |
| **Developers** | [Technical Architecture](#technical-architecture) \| [API Reference](#api-endpoints) \| [Integration Guide](#integration-guide) \| [Testing Guide](#testing-guide) |
---
# Part 1: For Project Managers
## Executive Summary
### What is the Commission Framework?
The Commission Framework is a revenue-sharing system that allows **community administrators** to earn money when their community members use Propel's services. Think of it as a referral bonus - when community members take loans, get jobs, or complete tasks, the community earns a percentage of Propel's earnings.
### How Does It Work?
```
Member Activity --> Propel Earns --> Community Gets Commission
| | |
(Loan, Job, (Service fees, (20% of Propel's
Quest, etc.) interest, etc.) earnings)
```
**Example:**
1. A community member takes a loan through Propel
2. When they repay, Propel earns interest (e.g., NGN 50,000)
3. The community automatically earns 20% commission (NGN 10,000)
4. Once the loan is fully repaid, the community can withdraw this money
### Key Benefits
| Benefit | Description |
|---------|-------------|
| **Passive Income** | Communities earn money automatically from member activities |
| **Multiple Sources** | Earn from loans, job placements, quests, and referrals |
| **Easy Withdrawals** | Direct bank transfer to community accounts |
| **Full Transparency** | Dashboard shows all earnings in real-time |
---
## User Guide for Project Managers
### Accessing the Commission Dashboard
1. **Who can access:** Community Admins and Managers only
2. **URL:** `/community-commissions`
3. **Navigation:** Go to your community dashboard > Click "Commissions" in the sidebar
### Understanding the Dashboard
The commission dashboard has four main sections:
#### 1. Stats Cards (Top Section)
| Card | What It Shows |
|------|---------------|
| **Wallet Balance** | Money available for withdrawal RIGHT NOW |
| **Potential Earnings** | Money earned but not yet available (waiting for loans to be fully repaid) |
| **Total Withdrawn** | All money withdrawn to date |
| **Commission Rate** | The percentage your community earns (typically 20%) |
#### 2. Earnings by Type (Breakdown Section)
Shows how much you've earned from each source:
- **Loans** - From member loan repayments
- **Jobs** - From member job placements
- **Quests** - From member quest completions
- **Referrals** - From member referrals
Each type shows:
- **Available** - Ready to withdraw
- **Pending** - Earned but waiting (e.g., loan not fully repaid yet)
#### 3. Commission History (Table)
A detailed list of all commission records showing:
- Member name
- Type (Loan, Job, etc.)
- Amount earned
- Status (Pending, Available, Withdrawn)
- Date
**Filtering Options:**
- By earning type (Loans only, Jobs only, etc.)
- By status
- By date range
#### 4. Actions
| Button | What It Does |
|--------|--------------|
| **Request Withdrawal** | Withdraw money to your bank account |
| **View Commission Rules** | See how commissions are calculated |
### Making a Withdrawal
**Prerequisites:**
- Must have funds in "Wallet Balance" (Available balance)
- Must have a Nigerian bank account
**Steps:**
1. Click **"Request Withdrawal"**
2. Enter the amount to withdraw (or click "Max" for full balance)
3. Select your bank from the dropdown
4. Enter your 10-digit account number
5. Wait for automatic account verification
6. Review the summary (shows EUR amount and NGN equivalent)
7. Click **"Request Withdrawal"**
**What Happens Next:**
- Status changes to "Processing"
- Funds are transferred via Paystack (1-2 business days)
- You'll receive the NGN amount in your bank account
### Commission Status Explained
| Status | Meaning | When It Happens |
|--------|---------|-----------------|
| **PENDING** | Earned but locked | Loan not fully repaid yet |
| **AVAILABLE** | Ready to withdraw | Loan fully repaid, or instant for jobs/quests |
| **WITHDRAWN** | Already paid out | Included in a withdrawal |
**Important:** Loan commissions become AVAILABLE only after the member fully repays their loan. This protects against defaults.
### Commission Rates
Default rates (may vary by community):
| Earning Type | Commission Rate | Example |
|--------------|-----------------|---------|
| Loan Repayments | 20% | Propel earns EUR 100 interest → Community gets EUR 20 |
| Job Placements | 15% | Propel earns EUR 500 placement fee → Community gets EUR 75 |
| Quests | 10% | Propel earns EUR 50 from quest → Community gets EUR 5 |
| Referrals | 5% | Propel earns EUR 100 from referral → Community gets EUR 5 |
### Currently Active Commission Types
> **Important:** Not all commission types are currently active. See the table below for what's currently earning commissions.
| Earning Type | Status | Notes |
|--------------|--------|-------|
| **Loan Repayments** | **Active** | Commissions are automatically recorded when members repay loans |
| Job Placements | *Coming Soon* | Framework ready, pending integration with hiring workflow |
| Quests | *Coming Soon* | Framework ready, pending integration with quest completion |
| Referrals | *Coming Soon* | Framework ready, pending integration with referral system |
**What does "Coming Soon" mean?**
- The system is fully capable of tracking these commission types
- The dashboard will display them once they're activated
- Propel is working on integrating these into the respective workflows
---
## Frequently Asked Questions
### General Questions
**Q: How often can I withdraw?**
A: Anytime, as long as you have available balance.
**Q: Is there a minimum withdrawal amount?**
A: The minimum is typically EUR 10 (varies based on bank transfer limits).
**Q: Why is my commission showing as "Pending"?**
A: For loans, commissions are pending until the member fully repays their loan. This protects the community from earning commissions on defaulted loans.
**Q: How long does a withdrawal take?**
A: 1-2 business days via Paystack bank transfer.
**Q: Can regular members see the commission dashboard?**
A: No, only Community Admins and Managers can access it.
### Technical Questions
**Q: What currency are commissions stored in?**
A: All commissions are stored in EUR for consistency. Local currency amounts are shown for reference.
**Q: What happens if a withdrawal fails?**
A: The funds are returned to your wallet balance, and you can retry with corrected bank details.
**Q: Can I change the commission rate?**
A: Commission rates are set by Propel. Contact support for rate adjustments.
---
## Propel Admin Management
### Commission Settings Management (Propel Admin Only)
**Who can access:** Propel Back Office Admins only
**Purpose:** Allows Propel administrators to view and configure commission rates for all communities across the platform.
#### Accessing Admin Commission Settings
1. Log in as a Propel Back Office Admin
2. Navigate to **Financial Services** in the admin sidebar
3. Click **"Commission Settings"**
#### Features
**View All Communities**
- See all communities and their commission rates in a searchable table
- Filter by earning type (LOAN, JOB, QUEST, REFERRAL)
- Search communities by name
**Individual Rate Management**
- Edit commission rates for specific community/earning type combinations
- View rate change history for each community
- See when rates were last updated and by whom
**Bulk Updates**
- Select multiple communities
- Update commission rates for a specific earning type across all selected communities
- Efficient management of rate changes
**Rate History**
- View historical commission rate changes for any community
- Track who made changes and when
- See effective dates for each rate
#### Default Commission Rates
Communities without custom settings use these defaults:
| Earning Type | Default Rate |
|--------------|--------------|
| LOAN | 20% |
| JOB | 0% (pending activation) |
| QUEST | 0% (pending activation) |
| REFERRAL | 0% (pending activation) |
---
# Part 2: For Developers
## Technical Architecture
### System Overview
```
+-------------------------------------------------------------------+
| Frontend (Vue.js) |
| +-------------+ +-------------+ +-------------+ +--------------+ |
| | Dashboard | | Stats Cards | |History Table| | Modals | |
| +-------------+ +-------------+ +-------------+ +--------------+ |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| API Layer (Laravel) |
| +---------------------------------------------------------------+ |
| | CommunityCommissionController | |
| | * stats() * history() * withdraw() * getSettings() | |
| +---------------------------------------------------------------+ |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| Service Layer |
| +---------------+ +--------------+ +------------------------+ |
| |CommissionSvc | |LoanCommission| |CommissionWithdrawalSvc | |
| | (Generic) | | Service | | | |
| +---------------+ +--------------+ +------------------------+ |
| | | | |
| +---------------+ +--------------+ |
| |JobCommission | |QuestCommission| (Future services) |
| | Service | | Service | |
| +---------------+ +--------------+ |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| Data Layer |
| +-------------------+ +-------------------+ +------------------+ |
| |CommunityCommission| |CommunityCommission| |CommunityCommission| |
| | Model | | Setting | | Withdrawal | |
| +-------------------+ +-------------------+ +------------------+ |
+-------------------------------------------------------------------+
```
### Key Design Decisions
1. **Single Table for All Commission Types**: Using `earning_type` column for differentiation
2. **Polymorphic Sources**: `source_type` and `source_id` link to any model (Loan, ProjectApplication, etc.)
3. **EUR as Base Currency**: All amounts stored in EUR for consistency
4. **FIFO Withdrawals**: Oldest available commissions are marked as withdrawn first
5. **Backward Compatibility**: `CommunityLoanCommission` model uses global scope for legacy code
6. **Centralized Rate Management**: Propel admins can configure rates for all communities from a single interface
7. **Rate History Tracking**: All rate changes are audited with timestamps and admin attribution
---
## Database Schema
### community_commissions
The main table storing all commission records.
```sql
CREATE TABLE community_commissions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
community_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
earning_type VARCHAR(255) DEFAULT 'LOAN',
loan_id BIGINT NULL,
-- Financial amounts (EUR)
repayment_amount_eur DECIMAL(15,2),
propel_earnings_eur DECIMAL(15,2),
commission_amount_eur DECIMAL(15,2),
commission_rate DECIMAL(5,4),
-- Local currency reference
repayment_amount_local DECIMAL(15,2) NULL,
commission_amount_local DECIMAL(15,2) NULL,
local_currency VARCHAR(3) DEFAULT 'NGN',
-- Status
status ENUM('PENDING', 'AVAILABLE', 'WITHDRAWN') DEFAULT 'PENDING',
-- Metadata
provider VARCHAR(255) NULL,
provider_transaction_id VARCHAR(255) NULL,
source_type VARCHAR(255) NULL,
source_id BIGINT NULL,
metadata JSON NULL,
description VARCHAR(255) NULL,
repayment_date TIMESTAMP NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP NULL,
INDEX idx_community_earning_status (community_id, earning_type, status),
FOREIGN KEY (community_id) REFERENCES communities(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
```
### community_commission_settings
Stores configurable commission rates per community and earning type.
```sql
CREATE TABLE community_commission_settings (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
community_id BIGINT NOT NULL,
earning_type VARCHAR(255) NOT NULL,
commission_rate DECIMAL(5,4) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
effective_from TIMESTAMP NULL,
effective_to TIMESTAMP NULL,
set_by BIGINT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
INDEX idx_community_earning_active (community_id, earning_type, is_active),
FOREIGN KEY (community_id) REFERENCES communities(id) ON DELETE CASCADE
);
```
### community_commission_withdrawals
Tracks withdrawal requests and their status.
```sql
CREATE TABLE community_commission_withdrawals (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
community_id BIGINT NOT NULL,
requested_by BIGINT NOT NULL,
-- Amounts
amount_eur DECIMAL(15,2) NOT NULL,
amount_local DECIMAL(15,2) NULL,
currency VARCHAR(3) DEFAULT 'NGN',
-- Bank details
bank_name VARCHAR(255) NOT NULL,
bank_code VARCHAR(255) NOT NULL,
account_number VARCHAR(255) NOT NULL,
account_name VARCHAR(255) NOT NULL,
-- Status
status ENUM('PENDING', 'PROCESSING', 'SUCCESSFUL', 'FAILED', 'CANCELLED') DEFAULT 'PENDING',
-- Provider
provider VARCHAR(255) DEFAULT 'paystack',
provider_reference VARCHAR(255) NULL,
transfer_code VARCHAR(255) NULL,
failure_reason TEXT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP NULL,
INDEX idx_community_status (community_id, status),
FOREIGN KEY (community_id) REFERENCES communities(id) ON DELETE CASCADE,
FOREIGN KEY (requested_by) REFERENCES users(id) ON DELETE CASCADE
);
```
---
## Models
### CommunityCommission
**Location:** `app/Models/CommunityCommission.php`
```php
use App\Models\CommunityCommission;
// Constants
CommunityCommission::STATUS_PENDING // 'PENDING'
CommunityCommission::STATUS_AVAILABLE // 'AVAILABLE'
CommunityCommission::STATUS_WITHDRAWN // 'WITHDRAWN'
CommunityCommission::EARNING_TYPE_LOAN // 'LOAN'
CommunityCommission::EARNING_TYPE_JOB // 'JOB'
CommunityCommission::EARNING_TYPE_QUEST // 'QUEST'
CommunityCommission::EARNING_TYPE_REFERRAL // 'REFERRAL'
// Relationships
$commission->community; // Community model
$commission->user; // User who generated commission
$commission->loan; // Loan model (if type is LOAN)
$commission->source; // Polymorphic source (Loan, ProjectApplication, etc.)
// Scopes
CommunityCommission::forCommunity($id)->get();
CommunityCommission::forEarningType('LOAN')->get();
CommunityCommission::available()->get();
CommunityCommission::pending()->get();
// Accessors
$commission->earning_type_label; // "Loan Repayment", "Job Placement", etc.
```
### CommunityLoanCommission (Backward Compatible)
**Location:** `app/Models/CommunityLoanCommission.php`
Extends `CommunityCommission` with a global scope for `earning_type = 'LOAN'`.
```php
use App\Models\CommunityLoanCommission;
// Automatically scoped to LOAN type only
$loanCommissions = CommunityLoanCommission::where('community_id', $id)->get();
// Creating automatically sets earning_type = 'LOAN'
CommunityLoanCommission::create([...]);
```
### CommunityCommissionSetting
**Location:** `app/Models/CommunityCommissionSetting.php`
Manages commission rate configurations for each community and earning type. Supports rate history and audit tracking.
```php
use App\Models\CommunityCommissionSetting;
// Constants
CommunityCommissionSetting::DEFAULT_LOAN_COMMISSION_RATE; // 0.20 (20%)
// Get active rate for a community and earning type
$rate = CommunityCommissionSetting::getActiveRate($communityId, 'LOAN');
// Returns: 0.20 (20%), or default if not set
// Set a new rate (used by admin interface)
CommunityCommissionSetting::setRate($communityId, 'LOAN', 0.25, $userId);
// - Deactivates previous rate
// - Creates new active rate
// - Records who made the change
// Relationships
$setting->community; // Community model
$setting->createdBy; // User who created
$setting->updatedBy; // User who last updated
```
**How Rate Changes Work:**
1. When a new rate is set, the previous active rate is deactivated
2. A new record is created with `is_active = true`
3. The old record is updated with `effective_until = now()`
4. All changes are tracked with timestamps and user attribution
### CommunityCommissionWithdrawal
**Location:** `app/Models/CommunityCommissionWithdrawal.php`
```php
use App\Models\CommunityCommissionWithdrawal;
// Constants
CommunityCommissionWithdrawal::STATUS_PENDING
CommunityCommissionWithdrawal::STATUS_PROCESSING
CommunityCommissionWithdrawal::STATUS_SUCCESSFUL
CommunityCommissionWithdrawal::STATUS_FAILED
// Relationships
$withdrawal->community;
$withdrawal->requestedBy; // User who requested
// Methods
$withdrawal->markAsSuccessful($reference);
$withdrawal->markAsFailed($reason);
```
---
## Services
### CommissionService (Base Service)
**Location:** `app/Classes/CommissionService.php`
```php
use App\Classes\CommissionService;
$service = app(CommissionService::class);
// Record a commission
$commission = $service->recordCommission(
communityId: 123,
userId: 456,
earningType: 'JOB',
propelEarningsLocal: 50000,
localCurrency: 'NGN',
source: $projectApplication,
options: [
'transaction_amount' => 100000,
'provider' => 'propel',
'transaction_id' => 'TXN123',
'metadata' => ['project_id' => 789],
'description' => 'Job placement commission',
'status' => CommunityCommission::STATUS_AVAILABLE,
]
);
// Get statistics
$stats = $service->getStats($communityId, $earningType = null);
// Get earnings breakdown by type
$breakdown = $service->getEarningsByType($communityId);
// Balance methods
$available = $service->getAvailableBalance($communityId);
$pending = $service->getPotentialEarnings($communityId);
$withdrawn = $service->getTotalWithdrawn($communityId);
```
### LoanCommissionService
**Location:** `app/Classes/LoanCommissionService.php`
```php
use App\Classes\LoanCommissionService;
$service = app(LoanCommissionService::class);
// Record commission from a loan repayment
$commission = $service->recordRepaymentCommission(
loan: $loan,
repaymentAmount: 5000,
localCurrency: 'NGN',
provider: 'klump',
transactionId: 'KLP_123',
metadata: ['webhook_event' => 'repayment.success']
);
// Finalize commissions when loan is fully repaid
$service->finalizeCommissionsForLoan($loan);
```
### CommunityCommissionWithdrawalService
**Location:** `app/Classes/CommunityCommissionWithdrawalService.php`
```php
use App\Classes\CommunityCommissionWithdrawalService;
$service = app(CommunityCommissionWithdrawalService::class);
// Process a withdrawal request
$withdrawal = $service->processWithdrawal(
community: $community,
requestedBy: $user,
amountEur: 100.00,
bankDetails: [
'bank_name' => 'First Bank',
'bank_code' => '011',
'account_number' => '0123456789',
'account_name' => 'John Doe',
]
);
// Handle Paystack webhook
$service->handleTransferWebhook($reference, $status, $reason);
```
---
## API Endpoints
All endpoints are prefixed with `/api/community/{communityId}/commissions`
### GET /stats
Get commission statistics for a community.
**Query Parameters:**
- `earning_type` (optional): Filter by type (LOAN, JOB, QUEST, REFERRAL)
**Response:**
```json
{
"data": {
"wallet_balance_eur": 150.00,
"potential_earnings_eur": 75.00,
"total_withdrawn_eur": 500.00,
"total_earnings_eur": 725.00,
"current_commission_rate": 0.20,
"commission_rates": {
"LOAN": 0.20,
"JOB": 0.15
},
"earnings_by_type": {
"LOAN": { "pending": 50.00, "available": 100.00, "withdrawn": 200.00 }
}
}
}
```
### GET /history
Get paginated commission history.
**Query Parameters:**
- `earning_type`: Filter by type
- `status`: Filter by status (PENDING, AVAILABLE, WITHDRAWN)
- `from_date`: Filter from date
- `to_date`: Filter to date
- `page`: Page number
- `per_page`: Items per page (default: 20)
### GET /earning-types
Get available earning types with descriptions.
### GET /breakdown
Get earnings breakdown by type.
### POST /withdraw
Request a withdrawal.
**Request Body:**
```json
{
"amount_eur": 100.00,
"bank_name": "First Bank",
"bank_code": "011",
"account_number": "0123456789",
"account_name": "Community Name"
}
```
### GET /withdrawals
Get withdrawal history.
### GET /settings
Get commission settings for all earning types.
### PUT /settings
Update commission rate for an earning type.
---
## Admin API Endpoints (Propel Admin Only)
All endpoints are prefixed with `/api/admin/commission-settings` and require admin authentication.
### GET /
Get commission settings for all communities.
**Query Parameters:**
- `search` (optional): Search by community name
- `earning_type` (optional): Filter by type (LOAN, JOB, QUEST, REFERRAL)
- `page`: Page number (default: 1)
- `per_page`: Items per page (default: 20)
**Response:**
```json
{
"data": {
"data": [
{
"community_id": 123,
"community_name": "Tech Community",
"community_logo": "https://...",
"community_country": "Nigeria",
"earning_type": "LOAN",
"commission_rate": 0.20,
"is_active": true,
"effective_from": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-15T10:30:00Z",
"has_custom_setting": true
}
],
"current_page": 1,
"per_page": 20,
"total": 100,
"last_page": 5
}
}
```
### PUT /{communityId}
Update commission rate for a specific community.
**Request Body:**
```json
{
"earning_type": "LOAN",
"commission_rate": 0.25
}
```
**Validation:**
- `earning_type`: Required, must be one of: LOAN, JOB, QUEST, REFERRAL
- `commission_rate`: Required, numeric, between 0 and 1 (0% to 100%)
**Response:**
```json
{
"data": null,
"message": "Commission rate updated successfully"
}
```
### GET /{communityId}/history
Get commission rate change history for a community.
**Query Parameters:**
- `earning_type` (optional): Filter by earning type
- `per_page`: Items per page (default: 20)
**Response:**
```json
{
"data": {
"data": [
{
"id": 1,
"community_id": 123,
"earning_type": "LOAN",
"commission_rate": 0.20,
"is_active": false,
"effective_from": "2026-01-01T00:00:00Z",
"effective_until": "2026-01-15T10:30:00Z",
"created_by": {
"id": 1,
"name": "Admin User"
},
"updated_by": {
"id": 1,
"name": "Admin User"
},
"created_at": "2026-01-01T00:00:00Z"
}
]
}
}
```
### POST /bulk-update
Update commission rates for multiple communities at once.
**Request Body:**
```json
{
"earning_type": "LOAN",
"commission_rate": 0.25,
"community_ids": [123, 456, 789]
}
```
**Validation:**
- `earning_type`: Required, must be one of: LOAN, JOB, QUEST, REFERRAL
- `commission_rate`: Required, numeric, between 0 and 1
- `community_ids`: Required, array of valid community IDs
**Response:**
```json
{
"data": null,
"message": "Commission rates updated successfully for 3 communities"
}
```
---
## Frontend Components
### Page Access
**URL:** `/community-commissions`
**Route:** Defined in `routes/web.php`
```php
Route::get('/community-commissions', [CommunityAdminController::class, 'commissions'])
->name('community-commissions.index');
```
**Controller:** `app/Http/Controllers/CommunityAdminController.php`
```php
public function commissions()
{
$community = Community::getCurrent();
$admin = Auth::user();
return view('community.commissions', compact('community', 'admin'));
}
```
**Blade View:** `resources/views/community/commissions.blade.php`
```blade
@extends('layouts.master')
@section('content')
<new-dashboard-layout page-title="Commissions">
<commission-dashboard communityidp="{{ $community->id }}" />
</new-dashboard-layout>
@endsection
```
### Component Hierarchy
```
CommissionDashboard
|-- CommissionStatsCards
|-- CommissionHistoryTable
|-- CommissionWithdrawalModal
|-- CommissionRulesModal
|-- CommissionDetailModal
```
### Component Locations
| Component | File | Purpose |
|-----------|------|---------|
| CommissionDashboard | `resources/js/components/admin/commission-dashboard.vue` | Community admin view |
| CommissionStatsCards | `resources/js/components/admin/commission-stats-cards.vue` | Stats display |
| CommissionHistoryTable | `resources/js/components/admin/commission-history-table.vue` | History table |
| CommissionWithdrawalModal | `resources/js/components/admin/commission-withdrawal-modal.vue` | Withdrawal form |
| CommissionRulesModal | `resources/js/components/admin/commission-rules-modal.vue` | Rules display |
| CommissionDetailModal | `resources/js/components/admin/commission-detail-modal.vue` | Detail view |
| AdminCommissionSettings | `resources/js/components/admin/AdminCommissionSettings.vue` | Propel admin settings |
### Navigation Links
**Sidebar (Old Layout):** `resources/js/layouts/dashboard/sidebar.vue`
**Navbar (New Layout):** `resources/js/components/reusables/navbar.vue`
Both show "Commissions" link only for `COMMUNITY_ADMIN` and `MANAGER` roles.
### Admin Commission Settings (Propel Admin Only)
**URL:** `/admin/commission-settings`
**Route:** Defined in `routes/web.php` under `isBackOfficeAdmin` middleware
```php
Route::middleware('isBackOfficeAdmin')->group(function () {
Route::get('/commission-settings', [AdminCommissionSettingsController::class, 'index'])
->name('commission-settings.index');
});
```
**Controller:** `app/Http/Controllers/AdminCommissionSettingsController.php`
```php
public function index()
{
return view('admin.commission-settings.index');
}
```
**Blade View:** `resources/views/admin/commission-settings/index.blade.php`
```blade
@extends('layouts.admin')
@section('title', 'Commission Settings Management')
@section('content')
<main>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h1>Commission Settings Management</h1>
<div class="card mb-4">
<div class="card-body">
<admin-commission-settings />
</div>
</div>
</div>
</div>
</div>
</main>
@endsection
```
**Vue Component:** `resources/js/components/admin/AdminCommissionSettings.vue`
A comprehensive admin interface featuring:
- **Search & Filter**: Find communities by name, filter by earning type
- **Data Table**: Shows all communities with their commission rates, status, and last update
- **Bulk Operations**: Select multiple communities and update rates simultaneously
- **Edit Modal**: Individual rate editing for each community/earning type
- **History Modal**: View complete rate change history with timestamps and admin attribution
- **Pagination**: Navigate large datasets efficiently
- **Responsive Design**: Mobile-friendly layout
**Navigation:** Admin sidebar > Financial Services > Commission Settings
#### Authorization & Access Control
**Community Commission Dashboard:**
- Requires: `COMMUNITY_ADMIN` or `MANAGER` role
- Middleware: Standard auth
- Scope: User sees only their own community's data
**Admin Commission Settings:**
- Requires: Propel Back Office Admin account
- Middleware: `isBackOfficeAdmin`
- Scope: User sees all communities across the platform
- API endpoints validate admin status before allowing rate modifications
---
## Integration Status
> **Current State:** The commission framework supports multiple earning types, but not all are currently integrated into the application workflows.
| Earning Type | Framework Ready | Triggered in Code | Integration Location | Testing Endpoint |
|--------------|-----------------|-------------------|---------------------|------------------|
| **LOAN** | Yes | **Yes** | `app/Classes/Klump.php` (webhook handler) | `POST /api/simulate-klump-webhook` |
| **JOB** | Yes | **No** | Needs integration in hiring workflow | N/A |
| **QUEST** | Yes | **No** | Needs integration in quest completion | N/A |
| **REFERRAL** | Yes | **No** | Needs integration in referral system | N/A |
> **Testing**: See the [Testing Guide](#testing-guide) for detailed instructions on testing loan commissions using the simulation endpoint.
### What's Built vs What's Integrated
**Built (Ready to Use):**
- Database schema supports all earning types
- `CommissionService` base class can record any type
- `JobCommissionService` exists with `recordHiringCommission()` method
- API endpoints support filtering by any earning type
- Frontend dashboard displays all earning types
**Not Yet Integrated:**
- JOB: `JobCommissionService::recordHiringCommission()` is not called when a member is hired
- QUEST: No `QuestCommissionService` or trigger exists
- REFERRAL: No `ReferralCommissionService` or trigger exists
### Integration Checklist for New Types
To fully integrate a commission type, you need to:
1. **Find the trigger point** - Where in the code does the qualifying event happen?
2. **Get community context** - Determine which community the user belongs to
3. **Calculate Propel's earnings** - What did Propel earn from this transaction?
4. **Call the service** - Use `CommissionService::recordCommission()` or a dedicated service
5. **Handle finalization** - If status should be PENDING first, implement finalization logic
---
## Integration Guide
### Recording a Loan Commission
```php
// In Klump webhook handler
$service = app(LoanCommissionService::class);
$service->recordRepaymentCommission(
$loan,
$repaymentAmount,
'NGN',
'klump',
$transactionId,
['webhook_event' => $event]
);
// When loan is fully repaid
if ($loan->isFullyRepaid()) {
$service->finalizeCommissionsForLoan($loan);
}
```
### Recording a Job Commission
```php
use App\Classes\JobCommissionService;
$service = app(JobCommissionService::class);
$service->recordHiringCommission(
$projectApplication,
$propelPlacementFee,
'EUR',
[
'placement_fee' => $totalPlacementFee,
'metadata' => ['hired_date' => now()],
]
);
```
### Adding a New Earning Type
1. Add constant to `CommunityCommission` model
2. Create dedicated service extending `CommissionService`
3. Update API controller's `getEarningTypes()` method
4. Update frontend labels in Vue components
### Updating Commission Rates (Admin)
**Via Admin Interface (Recommended):**
1. Navigate to `/admin/commission-settings`
2. Search/filter for the target community
3. Click edit icon on the desired community/earning type row
4. Enter new rate percentage (0-100)
5. Save changes
**Via Code:**
```php
use App\Models\CommunityCommissionSetting;
// Update rate for a specific community
CommunityCommissionSetting::setRate(
$communityId,
'LOAN',
0.25, // 25%
auth()->id()
);
// Bulk update via service
$service = app(\App\Http\Controllers\Api\AdminCommissionSettingsController::class);
// Use the bulk-update API endpoint
```
**Rate Change Flow:**
1. Admin submits new rate via UI or API
2. System deactivates current active rate
3. New rate record is created with `is_active = true`
4. Change is audited with admin ID and timestamp
5. New rate takes effect immediately for future commissions
6. Existing commissions retain their original rates
### Integrating JOB Commissions (TODO)
**Trigger Point:** When a community member is marked as "hired" for a job.
**Likely Location:**
- `app/Http/Controllers/Api/ProjectApplicationController.php` (status change)
- `app/Http/Requests/` (application status update handler)
- Or via an Observer on `ProjectApplication` model
**Integration Example:**
```php
// In the hiring logic (e.g., when application status changes to HIRED)
use App\Classes\JobCommissionService;
$commissionService = app(JobCommissionService::class);
$commissionService->recordHiringCommission(
$projectApplication,
$propelPlacementFee, // Propel's earnings from this placement
'EUR',
[
'placement_fee' => $totalPlacementFee,
'metadata' => [
'hired_date' => now(),
'project_id' => $projectApplication->project_id,
],
]
);
```
### Integrating QUEST Commissions (TODO)
**Trigger Point:** When a community member completes a quest.
**Likely Location:**
- Quest completion handler/controller
- Or via an Observer on Quest completion model
**Integration Example:**
```php
use App\Classes\CommissionService;
use App\Models\CommunityCommission;
$service = app(CommissionService::class);
$service->recordCommission(
communityId: $user->community_id,
userId: $user->id,
earningType: CommunityCommission::EARNING_TYPE_QUEST,
propelEarningsLocal: $propelQuestRevenue,
localCurrency: 'EUR',
source: $quest,
options: [
'description' => "Quest completion: {$quest->title}",
'status' => CommunityCommission::STATUS_AVAILABLE, // Immediate availability
]
);
```
### Integrating REFERRAL Commissions (TODO)
**Trigger Point:** When a referral leads to a qualifying action (e.g., referred user takes a loan, gets hired).
**Likely Location:**
- Referral tracking system
- After qualifying action is completed
**Integration Example:**
```php
use App\Classes\CommissionService;
use App\Models\CommunityCommission;
$service = app(CommissionService::class);
$service->recordCommission(
communityId: $referrer->community_id,
userId: $referrer->id,
earningType: CommunityCommission::EARNING_TYPE_REFERRAL,
propelEarningsLocal: $propelReferralBonus,
localCurrency: 'EUR',
source: $referral,
options: [
'description' => "Referral bonus for {$referredUser->name}",
'status' => CommunityCommission::STATUS_AVAILABLE,
]
);
```
---
## Webhook Handling
### Paystack Transfer Webhooks
```php
// In Paystack webhook controller
public function handleWebhook(Request $request)
{
$event = $request->input('event');
$data = $request->input('data');
if (in_array($event, ['transfer.success', 'transfer.failed'])) {
$service = app(CommunityCommissionWithdrawalService::class);
$service->handleTransferWebhook(
$data['reference'],
$event === 'transfer.success' ? 'success' : 'failed',
$data['reason'] ?? null
);
}
}
```
---
## Troubleshooting
### Commission Not Recording
1. Check commission rate exists:
```php
$rate = CommunityCommissionSetting::getActiveRate($communityId, $earningType);
```
2. Check user belongs to community:
```php
$communityUser = CommunityUser::where('user_id', $userId)
->where('community_id', $communityId)
->first();
```
3. Check application logs for errors
### Withdrawal Failed
1. Check Paystack balance
2. Verify bank details
3. Check `failure_reason` in `community_commission_withdrawals` table
4. Review Paystack API logs
### Commission Status Not Updating
For loans, commissions stay PENDING until loan is fully repaid:
```php
// Manually finalize if needed
$service = app(LoanCommissionService::class);
$service->finalizeCommissionsForLoan($loan);
```
---
## File Reference
### Backend Files
| File | Purpose |
|------|---------|
| `app/Models/CommunityCommission.php` | Main commission model |
| `app/Models/CommunityLoanCommission.php` | Loan-specific model (backward compatible) |
| `app/Models/CommunityCommissionSetting.php` | Rate settings model |
| `app/Models/CommunityCommissionWithdrawal.php` | Withdrawal model |
| `app/Classes/CommissionService.php` | Base commission service |
| `app/Classes/LoanCommissionService.php` | Loan commission service |
| `app/Classes/JobCommissionService.php` | Job commission service |
| `app/Classes/CommunityCommissionWithdrawalService.php` | Withdrawal service |
| `app/Http/Controllers/Api/CommunityCommissionController.php` | API controller (community) |
| `app/Http/Controllers/Api/AdminCommissionSettingsController.php` | API controller (admin) |
| `app/Http/Controllers/CommunityAdminController.php` | Web controller (community) |
| `app/Http/Controllers/AdminCommissionSettingsController.php` | Web controller (admin) |
| `routes/api.php` | API routes |
| `routes/web.php` | Web routes |
### Frontend Files
| File | Purpose |
|------|---------|
| `resources/js/components/admin/commission-dashboard.vue` | Main dashboard |
| `resources/js/components/admin/commission-stats-cards.vue` | Stats display |
| `resources/js/components/admin/commission-history-table.vue` | History table |
| `resources/js/components/admin/commission-withdrawal-modal.vue` | Withdrawal form |
| `resources/js/components/admin/commission-rules-modal.vue` | Rules display |
| `resources/js/components/admin/commission-detail-modal.vue` | Detail view |
| `resources/js/cass.js` | Component registration |
| `resources/js/layouts/dashboard/sidebar.vue` | Sidebar nav (community) |
| `resources/js/components/reusables/navbar.vue` | Navbar nav (community) |
| `resources/views/community/commissions.blade.php` | Blade template (community) |
| `resources/js/components/admin/AdminCommissionSettings.vue` | Admin settings interface |
| `resources/views/admin/commission-settings/index.blade.php` | Blade template (admin) |
| `resources/views/layouts/admin.blade.php` | Admin sidebar nav |
### Database Migrations
| File | Purpose |
|------|---------|
| `2026_01_30_113357_create_community_loan_commissions_table.php` | Create main table |
| `2026_01_30_113358_create_community_commission_settings_table.php` | Create settings table |
| `2026_01_30_113358_create_community_commission_withdrawals_table.php` | Create withdrawals table |
| `2026_01_30_113358_add_commission_fields_to_loans_table.php` | Add loan fields |
| `2026_01_30_114417_refactor_community_commissions_for_multiple_types.php` | Multi-type support |
---
## Running Migrations
```bash
php artisan migrate
```
This will:
1. Create `community_loan_commissions` table (or skip if exists)
2. Create `community_commission_settings` table
3. Create `community_commission_withdrawals` table
4. Add commission fields to `loans` table
5. Rename to `community_commissions` and add multi-type support
All migrations are **idempotent** - safe to run multiple times.
---
## Testing Guide
> **Quick Start**: Use `POST /api/simulate-klump-webhook` to simulate loan repayments and test commission recording without real Klump integration.
### Testing Loan Commissions
Since loan commissions are the only currently active commission type, here's a complete guide to testing the commission system end-to-end.
**What You'll Test:**
- ✅ Commission creation on loan repayment
- ✅ Commission rate calculation (default 20%)
- ✅ Status transitions (PENDING → AVAILABLE)
- ✅ Community admin dashboard display
- ✅ Propel admin rate management
#### Prerequisites
1. Have a community set up with members
2. Have the commission rate configured (default is 20% for loans)
3. Have a Klump loan created for a community member
4. Access to API testing tool (Postman, Insomnia, etc.)
#### Step 1: Create a Test Loan
First, create a loan for testing. You can either:
- Use the regular loan application flow through the UI
- Or use the Klump integration to create a loan
**Important**: Note the loan's `internal_ref_number` - you'll need this for the webhook simulation.
```sql
-- Find a recent loan for testing
SELECT id, internal_ref_number, amount, status, user_id
FROM loans
WHERE status = 'DISBURSED'
ORDER BY created_at DESC
LIMIT 1;
```
#### Step 2: Simulate a Partial Repayment
Use the simulation endpoint to trigger a repayment webhook event.
**Endpoint:** `POST /api/simulate-klump-webhook`
**Headers:**
```
Content-Type: application/json
Accept: application/json
```
**Request Body (Partial Payment Example):**
```json
{
"event": "klump.loan.repayment.successful",
"data": {
"merchant_reference": "LOAN_INTERNAL_REF_123456",
"amount": 50000,
"transaction": {
"id": "klump_txn_test_001",
"merchant_reference": "LOAN_INTERNAL_REF_123456",
"created_at": "2026-01-30T10:00:00Z",
"loan_is_paid_off": false
},
"loan_plan": {
"id": "plan_001",
"loan_is_paid_off": false
}
}
}
```
**Request Body (Final Payment Example):**
```json
{
"event": "klump.loan.repayment.successful",
"data": {
"merchant_reference": "LOAN_INTERNAL_REF_123456",
"amount": 50000,
"transaction": {
"id": "klump_txn_test_002",
"merchant_reference": "LOAN_INTERNAL_REF_123456",
"created_at": "2026-01-30T11:00:00Z",
"loan_is_paid_off": true
},
"loan_plan": {
"id": "plan_001",
"loan_is_paid_off": true
}
}
}
```
**Expected Response:**
```json
{
"message": "Webhook processed successfully."
}
```
#### Step 3: Verify Commission Recording
**Check Commission was Created:**
```sql
SELECT
cc.id,
cc.user_id,
cc.community_id,
cc.earning_type,
cc.repayment_amount_local,
cc.commission_amount_eur,
cc.commission_rate,
cc.status,
cc.repayment_date,
u.email as user_email,
c.name as community_name
FROM community_commissions cc
JOIN users u ON cc.user_id = u.id
JOIN communities c ON cc.community_id = c.id
WHERE cc.loan_id = YOUR_LOAN_ID
ORDER BY cc.created_at DESC;
```
**Expected Results:**
- `earning_type`: "LOAN"
- `status`: "PENDING" (for partial payments)
- `status`: "AVAILABLE" (after final payment when `loan_is_paid_off = true`)
- `commission_rate`: 0.20 (or your configured rate)
- `commission_amount_eur`: Calculated as (propel_earnings_eur * commission_rate)
#### Step 4: Test Commission Dashboard
**View Community Commission Dashboard:**
1. Log in as a community admin
2. Navigate to `/community-commissions`
3. Check the stats:
- **Wallet Balance**: Should show EUR 0 (commissions pending)
- **Potential Earnings**: Should show the commission amount
- **Commission Rate**: Should show 20%
**View Commission History:**
1. Scroll to the commission history table
2. Find your test commission
3. Verify:
- Status shows as "PENDING"
- Amount matches calculated commission
- Member name is correct
#### Step 5: Finalize Loan & Test Withdrawal
**Simulate Final Payment:**
Send another webhook with `loan_is_paid_off: true` (see Final Payment Example above).
**Verify Commission Status Changed:**
```sql
SELECT status, commission_amount_eur
FROM community_commissions
WHERE loan_id = YOUR_LOAN_ID;
```
Expected: All commissions for this loan should now have `status = 'AVAILABLE'`
**Check Wallet Balance Updated:**
1. Refresh the commission dashboard
2. **Wallet Balance** should now show the commission amount
3. **Potential Earnings** should be reduced by that amount
**Test Withdrawal (Optional):**
1. Click "Request Withdrawal"
2. Enter an amount (up to wallet balance)
3. Enter bank details
4. Submit the withdrawal request
5. Check `community_commission_withdrawals` table for the record
#### Step 6: Verify in Admin Settings
**As Propel Admin:**
1. Navigate to `/admin/commission-settings`
2. Search for the test community
3. Click the history icon
4. Verify the commission rate history is shown correctly
#### Troubleshooting
**Commission Not Created:**
- Check logs: `storage/logs/laravel.log`
- Search for "Klump commission recording failed"
- Verify the community has a commission rate set
- Ensure the loan's user belongs to a community
**Commission Status Not Changing:**
```php
// Manually finalize if needed (in tinker)
$loan = \App\Models\Loan::find(YOUR_LOAN_ID);
$service = app(\App\Classes\LoanCommissionService::class);
$service->finalizeCommissionsForLoan($loan);
```
**Webhook Simulation Fails:**
- Verify loan exists with the provided `merchant_reference`
- Check the `internal_ref_number` matches exactly
- Ensure the loan status is not already "COMPLETED"
#### Testing Different Scenarios
**Scenario 1: Multiple Partial Payments**
```bash
# First payment - 25,000 NGN
curl -X POST http://localhost/api/simulate-klump-webhook \
-H "Content-Type: application/json" \
-d '{"event":"klump.loan.repayment.successful","data":{"merchant_reference":"LOAN_REF","amount":25000,"transaction":{"id":"txn_001","merchant_reference":"LOAN_REF","loan_is_paid_off":false},"loan_plan":{"id":"plan_001","loan_is_paid_off":false}}}'
# Second payment - 25,000 NGN
curl -X POST http://localhost/api/simulate-klump-webhook \
-H "Content-Type: application/json" \
-d '{"event":"klump.loan.repayment.successful","data":{"merchant_reference":"LOAN_REF","amount":25000,"transaction":{"id":"txn_002","merchant_reference":"LOAN_REF","loan_is_paid_off":false},"loan_plan":{"id":"plan_001","loan_is_paid_off":false}}}'
# Final payment - 50,000 NGN (marks as paid off)
curl -X POST http://localhost/api/simulate-klump-webhook \
-H "Content-Type: application/json" \
-d '{"event":"klump.loan.repayment.successful","data":{"merchant_reference":"LOAN_REF","amount":50000,"transaction":{"id":"txn_003","merchant_reference":"LOAN_REF","loan_is_paid_off":true},"loan_plan":{"id":"plan_001","loan_is_paid_off":true}}}'
```
**Expected**:
- 3 commission records created
- All start as PENDING
- All change to AVAILABLE when final payment is made
**Scenario 2: Different Commission Rates**
1. As admin, change the commission rate for a community to 25%
2. Create a new loan for that community
3. Simulate a repayment
4. Verify commission is calculated at 25% instead of 20%
**Scenario 3: Cross-Currency Testing**
The system stores all commissions in EUR. Test with different local currencies:
- Repayments in NGN are converted to EUR
- Commission calculations happen on EUR amounts
- Dashboard displays in EUR
#### Expected Commission Calculation
```
Repayment Amount (Local): 100,000 NGN
Exchange Rate: 1 EUR = 1,700 NGN
Propel Interest Earned (EUR): (100,000 / 1,700) * 0.30 = 17.65 EUR (30% interest)
Commission Rate: 20%
Commission Amount (EUR): 17.65 * 0.20 = 3.53 EUR
```
**Note**: The actual Propel earnings calculation depends on the loan terms and interest structure. The commission service extracts this from the loan repayment data.
---
## Change Log
### January 30, 2026
- Added Propel Admin Commission Settings Management interface
- Added bulk rate update functionality
- Added commission rate history tracking
- Added admin API endpoints for rate management
- Updated documentation with admin management section
### January 2026 (Initial Release)
- Initial commission framework implementation
- Community admin dashboard
- Loan commission integration
- Withdrawal functionality
---
## Quick Reference
### For Community Admins
- **Dashboard URL**: `/community-commissions`
- **Who Can Access**: Community Admins & Managers
- **Default Commission Rate**: 20% for loans
- **Withdrawal**: Direct bank transfer via Paystack (1-2 business days)
### For Propel Admins
- **Settings URL**: `/admin/commission-settings`
- **Who Can Access**: Propel Back Office Admins
- **Features**: View all communities, edit rates, bulk updates, view history
### For Developers Testing
```bash
# Simulate loan repayment
POST /api/simulate-klump-webhook
Content-Type: application/json
{
"event": "klump.loan.repayment.successful",
"data": {
"merchant_reference": "LOAN_INTERNAL_REF",
"amount": 50000,
"transaction": {
"id": "test_txn_001",
"merchant_reference": "LOAN_INTERNAL_REF",
"loan_is_paid_off": false
},
"loan_plan": {
"id": "plan_001",
"loan_is_paid_off": false
}
}
}
```
### Key Database Tables
- `community_commissions` - Main commission records
- `community_commission_settings` - Rate configurations
- `community_commission_withdrawals` - Withdrawal requests
### Commission Status Flow
```
PENDING (loan not fully paid) → AVAILABLE (loan paid off) → WITHDRAWN (paid out)
```
---
*Last updated: January 30, 2026*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment