# Scopes: CompanyScope Implementation

## Overview

**CompanyScope is MANDATORY for all models** that contain company-specific data. This global scope ensures automatic filtering of data by the authenticated user's company, providing multi-tenant isolation.

## The Rule

Every model with a `company_id` column must have CompanyScope applied to ensure data isolation between companies.

## How CompanyScope Works

### Automatic Filtering

```php
// Without CompanyScope (DANGEROUS):
Product::all(); // Returns ALL products from ALL companies!

// With CompanyScope (SAFE):
Product::all(); // Returns only products for current user's company
```

### Behind the Scenes

```php
// CompanyScope automatically adds:
// WHERE company_id = {authenticated_user_company_id}

// So this query:
Product::where('status', 'active')->get();

// Becomes:
// SELECT * FROM products
// WHERE status = 'active'
// AND company_id = 'user-company-uuid'
```

## CompanyScope Implementation

### The Scope Class

```php
<?php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class CompanyScope implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     */
    public function apply(Builder $builder, Model $model): void
    {
        if (auth()->check()) {
            $builder->where(
                $model->getTable() . '.company_id',
                auth()->user()->company_id
            );
        }
    }
}
```

### Applying in BaseModel

CompanyScope is applied in BaseModel's boot method:

```php
protected static function boot()
{
    parent::boot();

    static::addGlobalScope(new CompanyScope);
}
```

## When CompanyScope is Applied

| Scenario | Scope Applied | Notes |
|----------|---------------|-------|
| User authenticated | ✅ Yes | Filters by user's company |
| No authentication | ❌ No | Query runs without filter |
| Console commands | ❌ No | Must manually filter |
| Queue jobs | ⚠️ Depends | Need to set auth context |

## Handling Special Cases

### 1. Console Commands

When running commands that need company filtering:

```php
// In a console command
public function handle()
{
    $companies = Company::all();

    foreach ($companies as $company) {
        // Option 1: Manual filtering
        $products = Product::withoutGlobalScope(CompanyScope::class)
            ->where('company_id', $company->id)
            ->get();

        // Option 2: Set auth context
        $admin = $company->users()->first();
        auth()->login($admin);

        $products = Product::all(); // Now filtered by company
    }
}
```

### 2. Queue Jobs

Ensure company context is preserved in jobs:

```php
class ProcessCompanyData implements ShouldQueue
{
    public function __construct(
        public string $companyId
    ) {}

    public function handle()
    {
        // Option 1: Query without scope
        $data = SomeModel::withoutGlobalScope(CompanyScope::class)
            ->where('company_id', $this->companyId)
            ->get();

        // Option 2: Set auth context
        $user = User::where('company_id', $this->companyId)->first();
        auth()->login($user);

        $data = SomeModel::all(); // Filtered automatically
    }
}
```

### 3. Cross-Company Queries (Admin Only)

For super-admin operations that need all company data:

```php
// Temporarily bypass CompanyScope
$allProducts = Product::withoutGlobalScope(CompanyScope::class)->get();

// Or for specific company
$companyProducts = Product::withoutGlobalScope(CompanyScope::class)
    ->where('company_id', $specificCompanyId)
    ->get();
```

### 4. Relationships Across Companies

Be careful with relationships that cross company boundaries:

```php
// This is SAFE - same company
$product->category; // Category belongs to same company

// This needs attention - shared resources
$product->globalCategory; // Might be shared across companies
```

## Additional Query Scopes

Beyond CompanyScope, add feature-specific scopes to models:

### Status Scopes

```php
class Product extends BaseModel
{
    public function scopeActive($query)
    {
        return $query->where('status', 'active');
    }

    public function scopeInactive($query)
    {
        return $query->where('status', 'inactive');
    }

    public function scopeDraft($query)
    {
        return $query->where('status', 'draft');
    }
}

// Usage
Product::active()->get();
Product::inactive()->get();
```

### Date Scopes

```php
class Order extends BaseModel
{
    public function scopeCreatedBetween($query, $start, $end)
    {
        return $query->whereBetween('created_at', [$start, $end]);
    }

    public function scopeThisMonth($query)
    {
        return $query->whereMonth('created_at', now()->month)
                     ->whereYear('created_at', now()->year);
    }

    public function scopeThisYear($query)
    {
        return $query->whereYear('created_at', now()->year);
    }
}

// Usage
Order::thisMonth()->get();
Order::createdBetween('2024-01-01', '2024-12-31')->get();
```

### Filter Scopes

```php
class Invoice extends BaseModel
{
    public function scopeForCustomer($query, $customerId)
    {
        return $query->where('customer_id', $customerId);
    }

    public function scopePaid($query)
    {
        return $query->where('payment_status', 'paid');
    }

    public function scopeUnpaid($query)
    {
        return $query->where('payment_status', 'unpaid');
    }

    public function scopeOverdue($query)
    {
        return $query->where('payment_status', 'unpaid')
                     ->where('due_date', '<', now());
    }
}

// Usage - Scopes chain together
Invoice::forCustomer($customerId)->unpaid()->overdue()->get();
```

## Scope Naming Conventions

| Type | Prefix | Example |
|------|--------|---------|
| Status filter | `scope{Status}` | `scopeActive`, `scopePending` |
| Relationship filter | `scopeFor{Relation}` | `scopeForCustomer`, `scopeForUser` |
| Date filter | `scope{Period}` | `scopeThisMonth`, `scopeThisYear` |
| Boolean filter | `scopeIs{State}` / `scopeHas{Thing}` | `scopeIsPublished`, `scopeHasItems` |

## Testing CompanyScope

### Test Isolation

```php
class ProductTest extends TestCase
{
    public function test_products_are_scoped_to_company()
    {
        // Arrange
        $company1 = Company::factory()->create();
        $company2 = Company::factory()->create();

        $user1 = User::factory()->for($company1)->create();

        Product::factory()->for($company1)->create(['name' => 'Product 1']);
        Product::factory()->for($company2)->create(['name' => 'Product 2']);

        // Act
        $this->actingAs($user1);
        $products = Product::all();

        // Assert
        $this->assertCount(1, $products);
        $this->assertEquals('Product 1', $products->first()->name);
    }
}
```

## Verification Checklist

- [ ] Model extends `BaseModel` (which includes CompanyScope)
- [ ] All queries return only current company's data
- [ ] Console commands handle company context properly
- [ ] Queue jobs preserve company context
- [ ] Tests verify scope isolation
- [ ] Cross-company operations use `withoutGlobalScope()` explicitly

## Common Mistakes

### 1. Forgetting Scope in Raw Queries

```php
// ❌ WRONG - Bypasses scope
DB::table('products')->where('status', 'active')->get();

// ✅ CORRECT - Uses Eloquent with scope
Product::where('status', 'active')->get();
```

### 2. Not Handling Unauthenticated Context

```php
// ❌ DANGEROUS - May return all data if not authenticated
$products = Product::all();

// ✅ SAFE - Check authentication
if (auth()->check()) {
    $products = Product::all();
}
```

### 3. Accidentally Exposing Data in APIs

```php
// ❌ WRONG - Could expose other company's data
Route::get('/products/{id}', function ($id) {
    return Product::withoutGlobalScope(CompanyScope::class)->find($id);
});

// ✅ CORRECT - Scope ensures ownership
Route::get('/products/{id}', function ($id) {
    return Product::findOrFail($id); // 404 if not in user's company
});
```

## Related Documentation

- [01-models.md](01-models.md) - BaseModel inheritance
- [04-validation.md](04-validation.md) - Request validation standards
