# Multi-Tenancy / Company Isolation

This guide explains how data is separated by company in our multi-tenant architecture and how to implement company isolation in new features.

## Overview

The application uses a **single-database multi-tenancy** approach where all companies share the same database, but data is isolated using a `company_id` column and automatic query scoping.

## Architecture Components

### 1. Company Context Resolution

The current company is determined from the authenticated user's `company_id` and made available throughout the application via service provider bindings.

**Location:** `app/Providers/AppServiceProvider.php`

```php
// Singleton that resolves current company from authenticated user
$this->app->singleton('company', function ($app) {
    return new class {
        protected $company = null;

        public function get() {
            if ($this->company === null) {
                return $this->loadCompany();
            }
            return $this->company;
        }

        protected function loadCompany() {
            if (!Auth::check()) {
                return $this->company = app(CompanyServices::class)->find(1);
            }
            return $this->company = app(CompanyServices::class)->find(Auth::user()->company_id);
        }
    };
});
```

### 2. Helper Function

**Location:** `app/Core/Helpers/helper.php`

```php
function company(): \App\Models\Company
{
    return app('company')->get();
}
```

**Usage:**
```php
// Get current company
$company = company();

// Get company ID
$companyId = company()->id;

// Access company properties
$companyName = company()->name;
```

### 3. Global Scope

**Location:** `app/Models/Scopes/CompanyScope.php`

```php
class CompanyScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $table = $model->getTable();
        $builder->where($table . '.company_id', company()->id);
    }
}
```

This scope automatically filters all queries to return only records belonging to the current company.

## Implementing Company Isolation

### Step 1: Add company_id Column to Migration

```php
Schema::create('your_table', function (Blueprint $table) {
    $table->id();
    // ... other columns
    $table->foreignId('company_id')->constrained()->cascadeOnDelete();
    $table->timestamps();
});
```

### Step 2: Enable Company Scope in Model

**Option A: Automatic Company Scope (Recommended)**

```php
<?php

namespace App\Domains\YourDomain\Models;

use App\Models\BaseModel;

class YourModel extends BaseModel
{
    protected static bool $applyCompanyScope = true;  // Enable automatic filtering

    protected $fillable = [
        'name',
        'company_id',
        // ... other fields
    ];
}
```

When `$applyCompanyScope = true`:
- All queries automatically filter by `company_id`
- New records automatically get `company_id` assigned on creation
- No manual filtering required

**Option B: Manual Company Scope**

If you need more control, use the manual scope method:

```php
// In your model (extends BaseModel)
// The scopeCompany method is already available from BaseModel

// Usage in queries
YourModel::query()->company()->get();
YourModel::query()->company()->where('status', 'active')->get();
```

### Step 3: Handle Company Assignment in Services

For models with `$applyCompanyScope = true`, company assignment is automatic. For manual control:

```php
public function store(YourDTO $dto): YourModel
{
    return DB::transaction(function () use ($dto) {
        return YourModel::create([
            'company_id' => company()->id,  // Explicit assignment
            'name' => $dto->name,
            // ... other fields
        ]);
    });
}
```

### Step 4: Company-Scoped Validation Rules

When validating unique constraints, scope them to the current company:

```php
// In your FormRequest
public function rules(): array
{
    $modelId = $this->route('model')?->id;

    return [
        'code' => [
            'required',
            'string',
            Rule::unique('your_table', 'code')
                ->where('company_id', company()->id)
                ->ignore($modelId),
        ],
    ];
}
```

## Patterns by Scenario

### Pattern 1: Standard Model with Full Isolation

```php
class Department extends BaseModel
{
    protected static bool $applyCompanyScope = true;

    public static array $filtersCols = ['company_id', 'status'];
    public static array $searchCols = ['name', 'code'];

    protected $fillable = ['name', 'code', 'company_id'];
}
```

### Pattern 2: Model Without Company Isolation

Some models (like User, Currency, Country) may not need company isolation:

```php
class Currency extends BaseModel
{
    protected static bool $applyCompanyScope = false;  // No automatic filtering

    // This model is shared across all companies
}
```

### Pattern 3: Conditional Company Access

When you need to access data across companies (admin functions):

```php
// Bypass the global scope
YourModel::withoutGlobalScope(CompanyScope::class)->get();

// Or bypass all global scopes
YourModel::withoutGlobalScopes()->get();
```

### Pattern 4: Explicit Company Filtering in Services

```php
public function getByCompany($companyId): Collection
{
    return YourModel::withoutGlobalScope(CompanyScope::class)
        ->where('company_id', $companyId)
        ->get();
}
```

## Company Settings Access

### Company-Specific Settings

```php
// Access company settings
$setting = cs('setting_key');

// Access additional settings (JSON field)
$additionalSetting = csa('additional_setting_key');
```

### Caching Strategy

Company settings are cached per company:

```php
// Cache key format: cs.{company_id}
Cache::rememberForever("cs." . company()->id, function () {
    return CompanySetting::query()->company()->first();
});
```

## Best Practices

### DO:

1. **Always extend `BaseModel`** for models that need company isolation
2. **Set `$applyCompanyScope = true`** for tenant-specific data
3. **Include `company_id` in migrations** with proper foreign key constraints
4. **Scope unique validations** to the current company
5. **Use the `company()` helper** for consistent company access

### DON'T:

1. **Don't hardcode company IDs** - always use `company()->id`
2. **Don't forget to add `company_id`** to fillable array if manually assigning
3. **Don't bypass global scopes** without explicit business justification
4. **Don't assume company context** in console commands or queue jobs

## Queue Jobs and Console Commands

For background jobs, pass the company ID explicitly:

```php
// Dispatching
ProcessReport::dispatch($reportId, company()->id);

// In the job
public function __construct(
    public int $reportId,
    public int $companyId
) {}

public function handle()
{
    // Manually set company context or filter
    $report = Report::withoutGlobalScope(CompanyScope::class)
        ->where('company_id', $this->companyId)
        ->find($this->reportId);
}
```

## Relationships Across Companies

When defining relationships, be aware of company scope implications:

```php
// This relationship respects company scope
public function departments()
{
    return $this->hasMany(Department::class);
}

// For cross-company relationships (rare), disable scope
public function allDepartments()
{
    return $this->hasMany(Department::class)->withoutGlobalScope(CompanyScope::class);
}
```

## Debugging Tips

1. **Check if scope is applied:**
   ```php
   YourModel::query()->toSql();
   // Should include: WHERE company_id = ?
   ```

2. **Verify company context:**
   ```php
   dd(company()->id, Auth::user()->company_id);
   ```

3. **Log scope application:**
   ```php
   \DB::enableQueryLog();
   YourModel::all();
   dd(\DB::getQueryLog());
   ```
