# Employer Reputation & Risk Management System

## Overview

A complete data-driven system for rating, monitoring, and moderating employers on the CuraHealthLine platform. Nurses can rate and report employers, while the system automatically computes reputation scores, risk scores, and assigns status levels for moderation.

---

## Architecture

### 🗄️ Database Schema

#### `employers` table (updated)
- `is_verified` (boolean) - KYC/contract verification status
- `reputation_score` (decimal 5,2) - **0-100** (higher = better)
- `risk_score` (decimal 5,2) - **0-100** (higher = more dangerous)
- `status` (string) - `active`, `watch`, `under_review`, `suspended`, `banned`

#### `employer_ratings` table (new)
Stores nurse ratings/reviews across 5 dimensions:
- `employer_id`, `nurse_id` (unique per nurse)
- `overall_score`, `fairness_score`, `communication_score`, `conditions_score`, `support_score` (1-5)
- `would_recommend` (boolean)
- `is_verified_interaction` (boolean) - +1.5x weight if true
- `rating_weight` (cached weight for analytics)
- `comment` (optional text review)

**Indexes**: `(employer_id, nurse_id)` unique, `(employer_id, created_at)`, `is_verified_interaction`

#### `employer_reports` table (new)
Stores serious complaints/issues:
- `employer_id`, `nurse_id` (nullable for anonymous), `job_id` (optional link)
- `type` (string) - fraud_scams, non_payment, contract_breach, visa_misuse, discrimination, unsafe_conditions, harassment_abuse, ghosting_unprofessional, other
- `severity` (1-5, auto-assigned by type, modifiable)
- `description`, `evidence_url`
- `status` (string) - `open`, `in_review`, `resolved_valid`, `resolved_invalid`
- `moderator_notes`, `resolved_at`

**Indexes**: `(employer_id, status, created_at)`, `(type, severity)`, `(nurse_id, created_at)`

---

## Algorithms

### 🏆 Reputation Score (0-100)

**Purpose**: Rate employer quality based on nurse feedback

**Formula**:
```
For each rating i:
  weight_i = (1.0 + 1.5*verified) * exp(-0.02 * age_days)
  
Weighted mean: R̄ = Σ(weight_i * overall_score_i) / Σ(weight_i)

Bayesian smoothing: R_bayes = (10*4.0 + n*R̄) / (10 + n)

Normalize [1,5] → [0,100]: reputation = ((R_bayes - 1) / 4) * 100
```

**Parameters** (tunable in `config/employer_reputation.php`):
- `weight_base = 1.0` - Base weight for all ratings
- `weight_verified = 1.5` - Bonus multiplier for verified interactions
- `time_decay_lambda = 0.02` - Decay rate (~50% after 35 days)
- `bayesian_prior_ratings = 10` - Minimum ratings to fully trust
- `bayesian_prior_score = 4.0` - Global average (1-5 scale)

**Key Features**:
- **Time Decay**: Older ratings count less (keeps reputation current)
- **Verified Bonus**: Verified interactions (applied/worked via Cura) weighted 2.5x more
- **Bayesian Smoothing**: Prevents extreme scores from small sample sizes
- **Banned Override**: Banned employers forced to 0.0

---

### ⚠️ Risk Score (0-100)

**Purpose**: Quantify danger/problems from reports

**Formula**:
```
For each active report i (status: open, in_review, resolved_valid):
  risk_raw += severity_i * exp(-0.03 * age_days)

Normalize: risk = min(100, (risk_raw / 50.0) * 100)
```

**Severity Defaults** (by report type):
- fraud_scams: **5**
- non_payment: **5**
- visa_misuse: **5**
- harassment_abuse: **5**
- contract_breach: **4**
- unsafe_conditions: **4**
- discrimination: **4**
- ghosting_unprofessional: **2**
- other: **3**

**Parameters**:
- `time_decay_mu = 0.03` - Faster decay than ratings (~50% after 23 days)
- `risk_cap = 50.0` - Normalization cap (tune based on observed max)

---

### 🚦 Status Classification

**Purpose**: Auto-assign employer status for moderation workflows

| Status | Conditions |
|--------|-----------|
| **active** | Default, good standing |
| **watch** | `risk >= 40` OR (`reputation < 40` AND has active reports) |
| **under_review** | `risk >= 70` OR (≥3 severe reports in last 60 days) |
| **suspended** | Manual override by moderator |
| **banned** | Manual override by moderator (permanent) |

**Auto-Review Trigger**:
- ≥3 reports with `severity >= 4` from different nurses in last 60 days
- Automatically moves employer to `under_review`

---

## Service Layer

### 📦 Service Classes (located in `app/Services/`)

#### `EmployerReputationService`
- `computeReputation(Employer)` → float
- `getRatingStats(Employer)` → array (averages, counts, etc.)
- `cacheRatingWeight(EmployerRating)` → void

#### `EmployerRiskService`
- `computeRisk(Employer)` → float
- `getDefaultSeverity(string $type)` → int
- `getRiskStats(Employer)` → array (report breakdowns)
- `shouldAutoReview(Employer)` → bool

#### `EmployerStatusService`
- `classify(Employer, reputation, risk)` → string
- `getStatusLabel(string)` → string (human-readable)
- `getStatusColor(string)` → string (Tailwind color)
- `canPostJobs(Employer)` → bool
- `isPubliclyVisible(Employer)` → bool
- `getRecommendedActions(Employer)` → array

#### `EmployerScoreService` (Orchestrator) ⭐
Main entry point for all operations:
- `refreshEmployerScores(Employer)` → array
  - **Call this after every rating/report change**
  - Recomputes reputation, risk, and status
  - Saves to database
  
- `recomputeRecentScores(int $days)` → int
  - Batch recompute for time decay
  - Used by scheduled command
  
- `getEmployerScoreSummary(Employer)` → array
  - Complete data for employer profile
  
- `getTopEmployers(int $limit, ?string $country)` → Collection
- `getEmployersNeedingModeration()` → Collection
- `syncAllScores()` → int (full system recompute)

---

## Integration Guide

### 🔧 When Nurse Submits Rating

```php
use App\Services\EmployerScoreService;

public function storeRating(Request $request, Employer $employer)
{
    // Validate input
    // Create/update EmployerRating model
    $rating = EmployerRating::create([...]);
    
    // CRITICAL: Refresh scores
    $scoreService = app(EmployerScoreService::class);
    $scoreService->refreshEmployerScores($employer);
    
    return response()->json(['rating' => $rating]);
}
```

### 🔧 When Nurse Submits Report

```php
use App\Services\EmployerScoreService;
use App\Services\EmployerRiskService;

public function storeReport(Request $request, Employer $employer)
{
    $riskService = app(EmployerRiskService::class);
    
    // Auto-assign severity based on type
    $severity = $riskService->getDefaultSeverity($request->type);
    
    // Create report
    $report = EmployerReport::create([
        'employer_id' => $employer->id,
        'type' => $request->type,
        'severity' => $severity,
        'status' => 'open',
        ...
    ]);
    
    // CRITICAL: Refresh scores
    $scoreService = app(EmployerScoreService::class);
    $scoreService->refreshEmployerScores($employer);
    
    return response()->json(['report' => $report]);
}
```

### 🔧 When Moderator Resolves Report

```php
public function updateReport(Request $request, EmployerReport $report)
{
    // Update report status
    $report->resolve($request->status, $request->moderator_notes);
    
    // CRITICAL: Refresh scores
    $scoreService = app(EmployerScoreService::class);
    $scoreService->refreshEmployerScores($report->employer);
    
    return response()->json(['report' => $report]);
}
```

---

## Scheduled Tasks

### Command: `employers:recompute-scores`

**Purpose**: Apply time decay to all ratings/reports (scores naturally decrease as content ages)

**Usage**:
```bash
# Default: last 180 days
php artisan employers:recompute-scores

# Custom timeframe
php artisan employers:recompute-scores --days=30

# Recompute ALL employers (caution: resource intensive)
php artisan employers:recompute-scores --all
```

**Scheduler** (in `routes/console.php`):
```php
Schedule::command('employers:recompute-scores')->daily();
```

**Performance**:
- Processes in batches of 100
- Only updates employers with activity (unless `--all` flag used)
- Logs results to Laravel log

---

## API Routes (Example)

Add to `routes/web.php` or `routes/api.php`:

```php
use App\Http\Controllers\EmployerReputationController;

// Nurse actions
Route::middleware('auth')->group(function() {
    Route::post('employers/{employer}/ratings', [EmployerReputationController::class, 'storeRating']);
    Route::post('employers/{employer}/reports', [EmployerReputationController::class, 'storeReport']);
});

// Public views
Route::get('employers/{employer}/reputation', [EmployerReputationController::class, 'show']);
Route::get('employers/top-rated', [EmployerReputationController::class, 'topRated']);

// Moderator actions
Route::middleware(['auth', 'role:moderator'])->group(function() {
    Route::patch('employers/reports/{report}', [EmployerReputationController::class, 'updateReport']);
    Route::get('moderator/employers/needs-attention', [EmployerReputationController::class, 'needsModeration']);
});
```

---

## UI Display

### Employer Profile

```blade
<div class="employer-reputation">
    <div class="score-badge {{ $employer->reputation_score >= 70 ? 'bg-green-500' : 'bg-yellow-500' }}">
        {{ number_format($employer->reputation_score, 0) }}
    </div>
    
    @if($employer->risk_score > 40)
        <div class="warning-badge bg-red-500">
            ⚠️ Risk Level: {{ number_format($employer->risk_score, 0) }}
        </div>
    @endif
    
    <div class="status-badge text-{{ $statusService->getStatusColor($employer->status) }}-600">
        {{ $statusService->getStatusLabel($employer->status) }}
    </div>
</div>
```

### Rating Form

```blade
<form action="/employers/{{ $employer->id }}/ratings" method="POST">
    @csrf
    <label>Overall Experience (1-5 stars)</label>
    <input type="number" name="overall_score" min="1" max="5" required>
    
    <label>Fairness</label>
    <input type="number" name="fairness_score" min="1" max="5" required>
    
    <label>Communication</label>
    <input type="number" name="communication_score" min="1" max="5" required>
    
    <label>Working Conditions vs Promise</label>
    <input type="number" name="conditions_score" min="1" max="5" required>
    
    <label>Support & Care</label>
    <input type="number" name="support_score" min="1" max="5" required>
    
    <label>Would you recommend this employer?</label>
    <input type="checkbox" name="would_recommend" value="1">
    
    <label>Comments (optional)</label>
    <textarea name="comment" maxlength="1000"></textarea>
    
    <button type="submit">Submit Rating</button>
</form>
```

---

## Configuration

All parameters in `config/employer_reputation.php`:

```php
'reputation' => [
    'weight_base' => 1.0,
    'weight_verified' => 1.5,
    'time_decay_lambda' => 0.02,
    'bayesian_prior_ratings' => 10,
    'bayesian_prior_score' => 4.0,
],

'risk' => [
    'time_decay_mu' => 0.03,
    'risk_cap' => 50.0,
    'severity_map' => [...],
],

'status' => [
    'review_severe_report_count' => 3,
    'review_severe_report_days' => 60,
],
```

Tune these based on production data.

---

## Testing & Monitoring

### Initial Setup
```bash
# Run migrations
php artisan migrate

# Seed test data (if you have seeders)
php artisan db:seed --class=EmployerReputationSeeder

# Run initial score computation
php artisan employers:recompute-scores --all
```

### View Employer Summary
```php
use App\Services\EmployerScoreService;

$scoreService = app(EmployerScoreService::class);
$summary = $scoreService->getEmployerScoreSummary($employer);

// Returns:
// - reputation_score, risk_score, status
// - rating_stats (counts, averages)
// - risk_stats (report breakdowns)
// - can_post_jobs, is_publicly_visible
// - recommended_actions (for moderators)
```

### Check Scheduler
```bash
php artisan schedule:list
php artisan schedule:run  # Manual trigger
```

---

## Scalability Considerations

✅ **Database Indexes**: All critical query paths indexed
✅ **Batch Processing**: Commands process in chunks of 100
✅ **Selective Updates**: Only recompute employers with recent activity
✅ **Cached Weights**: `rating_weight` field for analytics without recomputation
✅ **Lazy Loading**: Use `with()` to eager-load relationships
✅ **Log Monitoring**: All batch operations logged for observability

### Future Optimizations
- Cache top employers list (Redis, 5-min TTL)
- Queue score updates for high-traffic periods
- Materialize aggregates for large datasets
- Read replicas for public employer queries

---

## Status

✅ **FULLY IMPLEMENTED AND TESTED**

All migrations run successfully. System is production-ready and automatically updating scores on every interaction.
