# Core Module — Code Implementation Reference

This document is a thorough code-level reference for the Core module of this Laravel 12 ERP system. It covers every route, every controller method, every service method, every model, and every migration column for the three Core domain objects: **Branch**, **Setting**, and **NumberSequence**.

---

## Table of Contents

1. [Module Overview](#1-module-overview)
2. [Routes](#2-routes)
3. [Form Requests (Validation)](#3-form-requests-validation)
4. [Controllers](#4-controllers)
   - 4.1 [BranchController](#41-branchcontroller)
   - 4.2 [SettingController](#42-settingcontroller)
5. [Services](#5-services)
   - 5.1 [BranchService](#51-branchservice)
   - 5.2 [SettingService](#52-settingservice)
   - 5.3 [NumberSequenceService](#53-numbersequenceservice)
6. [Models](#6-models)
   - 6.1 [Branch](#61-branch)
   - 6.2 [Setting](#62-setting)
   - 6.3 [NumberSequence](#63-numbersequence)
7. [Migrations](#7-migrations)
   - 7.1 [branches table](#71-branches-table)
   - 7.2 [settings table](#72-settings-table)
   - 7.3 [number_sequences table](#73-number_sequences-table)
8. [Dependency Injection](#8-dependency-injection)

---

## 1. Module Overview

The Core module provides the foundational infrastructure that every other module in the ERP depends on:

| Component | Purpose |
|---|---|
| **Branch** | Multi-branch entity management. All operational data carries a `branch_id`. |
| **Setting** | Key-value configuration store with scope (system vs. branch), type casting, and optional encryption. |
| **NumberSequence** | Atomic document-number generation per module, document type, branch, and calendar year. |

The Core module has no routes for `NumberSequence` itself — it is a service used internally by other modules. Only Branch and Setting have HTTP endpoints.

---

## 2. Routes

All Core routes live inside `routes/admin.php` under the route group:

```php
Route::group(['prefix' => 'core', 'as' => 'core.'], function () { ... });
```

The outer wrapper applies locale prefix, localization middleware, `admin` auth prefix, and `auth:admin` guard middleware. All core routes therefore have the effective URI pattern:

```
/{locale}/admin/core/{resource}
```

### Full route table

| # | HTTP Method | URI | Route Name | Controller Method | Permission Middleware |
|---|---|---|---|---|---|
| 1 | GET | `/admin/core/branches` | `admin.core.branches.index` | `BranchController@index` | `core.branches.view` |
| 2 | GET | `/admin/core/branches/create` | `admin.core.branches.create` | `BranchController@create` | `core.branches.create` |
| 3 | POST | `/admin/core/branches` | `admin.core.branches.store` | `BranchController@store` | `core.branches.create` |
| 4 | GET | `/admin/core/branches/{branch}` | `admin.core.branches.show` | `BranchController@show` | `core.branches.view` |
| 5 | GET | `/admin/core/branches/{branch}/edit` | `admin.core.branches.edit` | `BranchController@edit` | `core.branches.edit` |
| 6 | PUT | `/admin/core/branches/{branch}` | `admin.core.branches.update` | `BranchController@update` | `core.branches.edit` |
| 7 | DELETE | `/admin/core/branches/{branch}` | `admin.core.branches.destroy` | `BranchController@destroy` | `core.branches.delete` |
| 8 | PUT | `/admin/core/branches/{branch}/set-as-main` | `admin.core.branches.setAsMain` | `BranchController@setAsMain` | `core.branches.edit` |
| 9 | PUT | `/admin/core/branches/{branch}/toggle-status` | `admin.core.branches.toggleStatus` | `BranchController@toggleStatus` | `core.branches.edit` |
| 10 | POST | `/admin/core/branches/{id}/restore` | `admin.core.branches.restore` | `BranchController@restore` | `core.branches.edit` |
| 11 | GET | `/admin/core/settings` | `admin.core.settings.index` | `SettingController@index` | `core.settings.view` |
| 12 | PUT | `/admin/core/settings` | `admin.core.settings.update` | `SettingController@update` | `core.settings.update` |

**Notes:**
- Routes 1–7 are the standard CRUD set. Routes 8–10 are extra actions.
- Route 10 uses `POST` and receives a plain integer `{id}` (not model binding) because soft-deleted records are not resolvable via standard route model binding.
- The `{branch}` parameter on routes 1–9 uses implicit route model binding against `App\Models\Core\Branch`. Laravel resolves the model by primary key.
- All routes sit behind `auth:admin` at the group level. Individual routes further restrict access with Spatie `permission:` middleware.

---

## 3. Form Requests (Validation)

### 3.1 StoreBranchRequest

**File:** `app/Http/Requests/Admin/Core/StoreBranchRequest.php`

Applied to: `BranchController@store` (POST `/admin/core/branches`)

**`prepareForValidation()`** runs before validation and coerces `is_active` and `is_main` to proper PHP booleans using `$this->boolean()`, preventing string `"0"` / `"1"` from causing type mismatches.

| Field | Rules | Notes |
|---|---|---|
| `code` | required, string, max:20, unique:branches,code | Must be globally unique across all branches |
| `name` | required, string, max:255 | |
| `slug` | nullable, string, max:255, unique:branches,slug | Auto-generated by BranchService if omitted |
| `email` | nullable, email, max:255 | |
| `phone` | nullable, string, max:20 | |
| `mobile` | nullable, string, max:20 | |
| `fax` | nullable, string, max:20 | |
| `address` | nullable, string, max:500 | |
| `city` | nullable, string, max:100 | |
| `state` | nullable, string, max:100 | |
| `postal_code` | nullable, string, max:20 | |
| `country` | nullable, string, max:100 | |
| `description` | nullable, string, max:1000 | |
| `manager_name` | nullable, string, max:255 | |
| `manager_email` | nullable, email, max:255 | |
| `manager_phone` | nullable, string, max:20 | |
| `is_active` | boolean | Pre-cast by `prepareForValidation` |
| `is_main` | boolean | Pre-cast by `prepareForValidation` |

### 3.2 UpdateBranchRequest

**File:** `app/Http/Requests/Admin\Core\UpdateBranchRequest.php`

Applied to: `BranchController@update` (PUT `/admin/core/branches/{branch}`)

Identical field set to `StoreBranchRequest` with one key difference: the `unique` rules on `code` and `slug` use `Rule::unique(...)->ignore($branchId)` to exclude the current record from the uniqueness check, preventing false conflicts when saving unchanged values.

```php
$branchId = $this->route('branch')?->id ?? $this->branch?->id;

'code' => [
    'required', 'string', 'max:20',
    Rule::unique('branches', 'code')->ignore($branchId),
],
```

The `$branchId` is resolved from the route-model-bound `Branch` object.

### 3.3 UpdateSettingRequest

**File:** `app/Http/Requests/Admin/Core/UpdateSettingRequest.php`

Applied to: `SettingController@update` (PUT `/admin/core/settings`)

This request validates a fixed set of known system-level settings. The field names encode the group and key using the pattern `{group}_{key}`:

| Field | Rules | Derived group | Derived key |
|---|---|---|---|
| `company_name_ar` | required, string, max:255 | `company` | `name_ar` |
| `company_name_en` | required, string, max:255 | `company` | `name_en` |
| `system_default_currency` | required, string, size:3 | `system` | `default_currency` |
| `system_default_language` | required, string, size:2 | `system` | `default_language` |
| `sales_default_tax_rate` | required, numeric, min:0, max:100 | `sales` | `default_tax_rate` |

The `SettingController@update` method splits each validated key on the first underscore to extract `group` and `key` before persisting.

---

## 4. Controllers

### 4.1 BranchController

**File:** `app/Http/Controllers/Admin/Core/BranchController.php`

**Namespace:** `App\Http\Controllers\Admin\Core`

**Extends:** `App\Http\Controllers\Controller`

#### Dependency Injection

```php
private BranchService $branchService;

public function __construct(BranchService $branchService)
{
    $this->branchService = $branchService;
}
```

Laravel's service container resolves `BranchService` automatically via constructor injection when the controller is instantiated. No manual `new BranchService()` call is needed anywhere.

---

#### Method: `index(Request $request)`

**Route:** GET `/admin/core/branches`

**Parameters received:**
- `$request->get('search')` — free-text search term (optional)
- `$request->get('is_active')` — filter by active status: `"1"`, `"0"`, or empty (optional)
- `$request->get('is_main')` — filter by main-branch flag: `"1"`, `"0"`, or empty (optional)
- `$request->get('sort_by', 'created_at')` — column to sort by, defaults to `created_at`
- `$request->get('sort_order', 'desc')` — sort direction, defaults to `desc`

**Step-by-step logic:**
1. Builds a `$filters` array from the five optional query parameters.
2. Calls `$this->branchService->list(15, $filters)` to get a paginated result (15 per page).
3. Returns the Blade view `dashboard.admin.core.branches.index` with `$branches` (paginator) and `$filters` (array) available in the template.

**Returns:** `Illuminate\View\View`

---

#### Method: `create()`

**Route:** GET `/admin/core/branches/create`

**Parameters received:** None

**Step-by-step logic:**
1. Returns the Blade view for the create form with no additional data.

**Returns:** `Illuminate\View\View` — renders `dashboard.admin.core.branches.create`

---

#### Method: `store(StoreBranchRequest $request)`

**Route:** POST `/admin/core/branches`

**Parameters received:**
- `StoreBranchRequest` — Form request object carrying `$request->validated()` (all whitelisted, validated fields).

**Step-by-step logic:**
1. Reads the currently authenticated admin's ID via `auth('admin')->user()?->id`.
2. Calls `$this->branchService->create($request->validated(), $adminId)`.
3. The service returns the newly created `Branch` model instance.
4. Redirects to the show page for the new branch (`admin.core.branches.show`) with a success flash message.

**Returns:** `Illuminate\Http\RedirectResponse` — redirect to `admin.core.branches.show` with `success` session flash.

---

#### Method: `show(Branch $branch)`

**Route:** GET `/admin/core/branches/{branch}`

**Parameters received:**
- `Branch $branch` — route-model-bound instance resolved from `{branch}` URL segment.

**Step-by-step logic:**
1. Passes the bound `$branch` model to the Blade view.

**Returns:** `Illuminate\View\View` — renders `dashboard.admin.core.branches.show` with `$branch`.

---

#### Method: `edit(Branch $branch)`

**Route:** GET `/admin/core/branches/{branch}/edit`

**Parameters received:**
- `Branch $branch` — route-model-bound instance.

**Step-by-step logic:**
1. Passes the bound `$branch` model to the edit form view.

**Returns:** `Illuminate\View\View` — renders `dashboard.admin.core.branches.edit` with `$branch`.

---

#### Method: `update(UpdateBranchRequest $request, Branch $branch)`

**Route:** PUT `/admin/core/branches/{branch}`

**Parameters received:**
- `UpdateBranchRequest $request` — validated form data.
- `Branch $branch` — route-model-bound instance.

**Step-by-step logic:**
1. Reads the currently authenticated admin's ID.
2. Calls `$this->branchService->update($branch, $request->validated(), $adminId)`.
3. The service returns the refreshed `Branch` model.
4. Redirects to the show page with a success flash message.

**Returns:** `Illuminate\Http\RedirectResponse` — redirect to `admin.core.branches.show` with `success` flash.

---

#### Method: `destroy(Branch $branch)`

**Route:** DELETE `/admin/core/branches/{branch}`

**Parameters received:**
- `Branch $branch` — route-model-bound instance.

**Step-by-step logic:**
1. Calls `$this->branchService->delete($branch)`, which performs a **soft delete** (sets `deleted_at` timestamp; the row remains in the database).
2. Redirects to the branches index with a success flash.

**Returns:** `Illuminate\Http\RedirectResponse` — redirect to `admin.core.branches.index` with `success` flash.

---

#### Method: `toggleStatus(Branch $branch)`

**Route:** PUT `/admin/core/branches/{branch}/toggle-status`

**Parameters received:**
- `Branch $branch` — route-model-bound instance.

**Step-by-step logic:**
1. Reads the admin's ID.
2. Calls `$this->branchService->toggleStatus($branch, $adminId)`, which flips `is_active` and updates `activated_at` or `deactivated_at` accordingly.
3. After the service call, inspects `$branch->is_active` (the **original** value before toggle, since `$branch` is the pre-update object) to build the status word.

> **Note:** The status message is derived from `$branch->is_active` before the service updates the model — so the message reads the original state. This is a known minor inversion in the wording logic; functionally the toggle still works correctly.

4. Redirects back with a success flash.

**Returns:** `Illuminate\Http\RedirectResponse` — redirect back with `success` flash.

---

#### Method: `setAsMain(Branch $branch)`

**Route:** PUT `/admin/core/branches/{branch}/set-as-main`

**Parameters received:**
- `Branch $branch` — route-model-bound instance.

**Step-by-step logic:**
1. Reads the admin's ID.
2. Calls `$this->branchService->setAsMain($branch, $adminId)`, which sets all other branches to `is_main = false` then sets this branch to `is_main = true`.
3. Redirects back with a success flash.

**Returns:** `Illuminate\Http\RedirectResponse` — redirect back with `success` flash.

---

#### Method: `restore(int $id)`

**Route:** POST `/admin/core/branches/{id}/restore`

**Parameters received:**
- `int $id` — raw integer from the URL segment. No model binding here because soft-deleted records cannot be resolved by the default binding.

**Step-by-step logic:**
1. Calls `$this->branchService->restore($id)`.
2. The service looks up the branch including trashed records via `Branch::withTrashed()->find($id)`.
3. If not found or not trashed, returns `false` — the controller redirects back with an `error` flash.
4. On success, redirects back with a `success` flash.

**Returns:** `Illuminate\Http\RedirectResponse` — redirect back with either `error` or `success` flash.

---

### 4.2 SettingController

**File:** `app/Http/Controllers/Admin/Core/SettingController.php`

**Namespace:** `App\Http\Controllers\Admin\Core`

**Extends:** `App\Http\Controllers\Controller`

#### Dependency Injection

```php
public function __construct(
    protected SettingService $settingService
) {}
```

Uses PHP 8 constructor property promotion. `SettingService` is resolved by the service container and stored as a protected property automatically.

---

#### Method: `index(): View`

**Route:** GET `/admin/core/settings`

**Parameters received:** None

**Step-by-step logic:**
1. Returns the settings index view. The view is responsible for fetching and rendering the current settings (the controller does not pass any data).

**Returns:** `Illuminate\View\View` — renders `dashboard.admin.core.settings.index`.

---

#### Method: `update(UpdateSettingRequest $request): RedirectResponse`

**Route:** PUT `/admin/core/settings`

**Parameters received:**
- `UpdateSettingRequest $request` — carries the five validated setting fields.

**Step-by-step logic:**
1. Iterates over every key-value pair in `$request->validated()`.
2. For each key (e.g., `company_name_ar`):
   - Splits the key on the **first underscore** using `explode('_', $key, 2)`.
   - `$parts[0]` becomes the `$group` (e.g., `company`).
   - `$parts[1]` becomes the `$settingKey` (e.g., `name_ar`).
3. Calls `$this->settingService->set(key: $settingKey, value: $value, scope: 'system', group: $group, adminId: auth('admin')->id())`.
4. After all keys are processed, redirects back with a success flash.

**Returns:** `Illuminate\Http\RedirectResponse` — redirect back with `success` flash.

---

## 5. Services

### 5.1 BranchService

**File:** `app/Services/Core/BranchService.php`

**Namespace:** `App\Services\Core`

This service encapsulates all branch business logic. It is the only place that issues queries on the `Branch` model.

---

#### `list(int $perPage = 15, array $filters = []): LengthAwarePaginator`

Builds a paginated query on `Branch::query()` with optional filters.

**Parameters:**
- `$perPage` — number of results per page (default `15`).
- `$filters` — associative array with any of: `is_active`, `search`, `is_main`, `sort_by`, `sort_order`.

**Step-by-step logic:**
1. Starts a fresh Eloquent query builder on `Branch`.
2. If `$filters['is_active']` is set and not an empty string, adds `WHERE is_active = ?` (cast to bool).
3. If `$filters['search']` is not empty, adds a grouped OR WHERE on `code`, `name`, `email`, `phone` using `LIKE %term%`.
4. If `$filters['is_main']` is set and not an empty string, adds `WHERE is_main = ?`.
5. Applies `ORDER BY {sort_by} {sort_order}` (defaults: `created_at DESC`).
6. Calls `->paginate($perPage)` and returns the paginator.

**Return value:** `Illuminate\Pagination\LengthAwarePaginator`

---

#### `getAllActive(): Collection`

Returns all active branches without pagination.

**Step-by-step logic:**
1. Calls the `active` scope on the model: `Branch::active()->get()`.

**Return value:** `Illuminate\Support\Collection` of `Branch` instances.

---

#### `getById(int $id): ?Branch`

Fetches a single branch by primary key.

**Step-by-step logic:**
1. Calls `Branch::find($id)`. Returns `null` if not found.

**Return value:** `Branch|null`

---

#### `getByCode(string $code): ?Branch`

Fetches a branch by its unique code.

**Step-by-step logic:**
1. Calls `Branch::where('code', $code)->first()`.

**Return value:** `Branch|null`

---

#### `create(array $data, ?int $createdBy = null): Branch`

Creates a new branch record.

**Parameters:**
- `$data` — validated field array from `StoreBranchRequest`.
- `$createdBy` — the admin's ID to record in the audit trail (nullable).

**Step-by-step logic:**
1. If `$data['slug']` is absent or empty, generates a slug via `Str::slug($data['name'])`.
2. Calls `$this->generateUniqueSlug($data['slug'])` to ensure no collision.
3. If `$createdBy` is provided, sets `$data['created_by']` and `$data['updated_by']`.
4. If `$data['is_active']` is truthy, sets `$data['activated_at'] = now()`.
5. Calls `Branch::create($data)` and returns the new model.

**Return value:** `Branch`

---

#### `update(Branch $branch, array $data, ?int $updatedBy = null): Branch`

Updates an existing branch.

**Parameters:**
- `$branch` — the existing model instance to update.
- `$data` — validated field array from `UpdateBranchRequest`.
- `$updatedBy` — the admin's ID for the audit trail (nullable).

**Step-by-step logic:**
1. If `$data['name']` is provided and differs from the current name, regenerates the slug (calls `generateUniqueSlug` with `$branch->id` excluded to avoid self-collision).
2. If `$updatedBy` is provided, sets `$data['updated_by']`.
3. If `$data['is_active']` is set:
   - Activating (was inactive → now active): sets `activated_at = now()` and clears `deactivated_at`.
   - Deactivating (was active → now inactive): sets `deactivated_at = now()`.
4. Calls `$branch->update($data)`.
5. Calls `$branch->fresh()` to re-fetch the updated model from the database.

**Return value:** `Branch` (freshly fetched)

---

#### `delete(Branch $branch): bool`

Soft-deletes a branch (sets `deleted_at`; row remains in the database).

**Step-by-step logic:**
1. Calls `$branch->delete()` (which is `SoftDeletes::delete()`).
2. Returns the boolean result cast from the delete return value.

**Return value:** `bool`

---

#### `restore(int $id): bool`

Restores a soft-deleted branch.

**Parameters:**
- `$id` — primary key of the branch (integer, not model binding).

**Step-by-step logic:**
1. Looks up `Branch::withTrashed()->find($id)`.
2. If no record found, or the found record is not trashed (`!$branch->trashed()`), returns `false`.
3. Calls `$branch->restore()` and returns the boolean result.

**Return value:** `bool`

---

#### `forceDelete(Branch $branch): bool`

Permanently removes a branch from the database. Not currently wired to any HTTP route; available for administrative scripts.

**Step-by-step logic:**
1. Calls `$branch->forceDelete()`.

**Return value:** `bool`

---

#### `toggleStatus(Branch $branch, ?int $updatedBy = null): Branch`

Flips the active/inactive state of a branch.

**Step-by-step logic:**
1. Computes `$newStatus = !$branch->is_active`.
2. Delegates to `$this->update($branch, ['is_active' => $newStatus], $updatedBy)`, which handles `activated_at` / `deactivated_at` timestamps automatically.

**Return value:** `Branch`

---

#### `setAsMain(Branch $branch, ?int $updatedBy = null): Branch`

Designates one branch as the main branch (there can be only one).

**Step-by-step logic:**
1. Runs a bulk update: `Branch::where('id', '!=', $branch->id)->update(['is_main' => false])` to clear the flag on all other branches.
2. Delegates to `$this->update($branch, ['is_main' => true], $updatedBy)`.

**Return value:** `Branch`

---

#### `generateUniqueSlug(string $slug, ?int $excludeId = null): string` *(private)*

Ensures the generated slug does not already exist in the database.

**Parameters:**
- `$slug` — the base slug to check (e.g., `"cairo-branch"`).
- `$excludeId` — if updating, the current branch's ID is excluded from the collision check.

**Step-by-step logic:**
1. Stores `$originalSlug` and initializes `$counter = 1`.
2. Queries `Branch::where('slug', $slug)` (plus `->where('id', '!=', $excludeId)` if updating).
3. While the slug exists, appends `-{counter}` (e.g., `cairo-branch-1`, `cairo-branch-2`) and increments the counter.
4. Returns the first non-colliding slug.

**Return value:** `string`

---

### 5.2 SettingService

**File:** `app/Services/Core/SettingService.php`

**Namespace:** `App\Services\Core`

Provides a typed, encrypted-aware key-value store backed by the `settings` table.

---

#### `get(string $key, ?int $branchId = null, $default = null): mixed`

Retrieves a setting value with branch-level override support.

**Parameters:**
- `$key` — the setting key (e.g., `"default_currency"`).
- `$branchId` — if provided, looks for a branch-scoped override first.
- `$default` — value to return if no setting is found.

**Step-by-step logic:**
1. If `$branchId` is provided, queries `settings` for `scope = 'branch' AND branch_id = ? AND key = ?`.
2. If no branch-scoped row found (or `$branchId` was null), queries `settings` for `scope = 'system' AND key = ?`.
3. If still no row, returns `$default`.
4. If the found setting has `is_encrypted = true` and a non-null value, decrypts via `Crypt::decryptString($setting->value)`.
5. Passes the raw string value through `$this->castValue($value, $setting->type)` to return the correctly typed PHP value.

**Return value:** `mixed` (typed according to the setting's `type` column)

---

#### `set(string $key, $value, string $scope = 'system', ?int $branchId = null, string $type = 'string', ?string $group = null, ?int $adminId = null): Setting`

Creates or updates a setting entry.

**Parameters:**
- `$key` — setting key.
- `$value` — new value to store (any PHP type; will be cast to string for storage).
- `$scope` — `'system'` or `'branch'` (defaults to `'system'`).
- `$branchId` — required when `$scope = 'branch'`.
- `$type` — the PHP type to cast on retrieval: `'string'`, `'boolean'`, `'integer'`, `'float'`, `'json'` (defaults to `'string'`).
- `$group` — logical grouping label (e.g., `'company'`, `'sales'`).
- `$adminId` — ID of the admin making the change (for audit trail).

**Step-by-step logic:**
1. Checks whether the existing row has `is_encrypted = true` (queries the `value` for `is_encrypted` from the database; defaults to `false` if not found).
2. If encrypted and value is non-null, encodes via `Crypt::encryptString((string) $value)`.
3. If not encrypted, coerces value to string (or `null`).
4. Calls `Setting::updateOrCreate()` matching on `[scope, branch_id, key]`.
5. For the `created_by` field: checks whether the row already exists; if it does, passes `null` (preserves original creator); otherwise passes `$adminId`.
6. Always updates `updated_by` with `$adminId`.

**Return value:** `Setting`

---

#### `castValue($value, string $type): mixed`

Converts a raw string value from the database into the correct PHP type.

**Parameters:**
- `$value` — the raw string (or null) from the `settings.value` column.
- `$type` — the type string stored in `settings.type`.

**Step-by-step logic:**
1. Returns `null` immediately if `$value` is null.
2. Uses a `match` expression on `$type`:
   - `'boolean'` / `'bool'` → `filter_var($value, FILTER_VALIDATE_BOOLEAN)`
   - `'integer'` / `'int'` → `(int) $value`
   - `'float'` / `'double'` → `(float) $value`
   - `'json'` → `json_decode($value, true)` (returns associative array)
   - default → returns `$value` as-is (string)

**Return value:** `mixed`

---

### 5.3 NumberSequenceService

**File:** `app/Services/Core/NumberSequenceService.php`

**Namespace:** `App\Services\Core`

This service is used by all other modules to generate formatted, sequential, collision-free document numbers. It is never directly called from HTTP routes — it is injected into other services (e.g., `InvoiceService`, `PurchaseOrderService`).

---

#### `getOrCreate(string $module, string $document, ?int $branchId = null, ?int $year = null): NumberSequence`

Fetches or lazily creates the sequence record for a given combination.

**Parameters:**
- `$module` — module identifier (e.g., `'sales'`, `'inventory'`, `'finance'`).
- `$document` — document type identifier (e.g., `'invoice'`, `'purchase_order'`, `'receipt'`).
- `$branchId` — branch scope (null = company-wide sequence).
- `$year` — calendar year (defaults to `Carbon::now()->year`).

**Step-by-step logic:**
1. If `$year` is null, resolves to the current calendar year.
2. Calls `NumberSequence::firstOrCreate()` matching on `[module, document, branch_id, year]`.
3. Default creation values: `prefix = null`, `padding = 5`, `current_number = 0`.

**Return value:** `NumberSequence`

---

#### `getNextNumber(string $module, string $document, ?int $branchId = null, ?int $year = null): string`

**The primary method used by all modules for document numbering.** Atomically increments the counter and returns a formatted string.

**Step-by-step logic:**
1. Calls `$this->getOrCreate(...)` to get or create the sequence record.
2. Calls `$sequence->incrementAndGet()` on the model, which uses Eloquent's `->increment('current_number')` (a single atomic `UPDATE ... SET current_number = current_number + 1`) and then `->fresh()` to read back the new value.
3. Pads the new number to `$sequence->padding` digits with leading zeros using `str_pad($issued, $sequence->padding, '0', STR_PAD_LEFT)`.
4. If the sequence has a `prefix`, prepends it: `$sequence->prefix . $padded`.
5. Returns the final formatted string (e.g., `"INV00042"`, `"00042"`).

**Return value:** `string`

> **Important:** `incrementAndGet()` relies on `INCREMENT` SQL semantics. In high-concurrency environments this is safe because `INCREMENT` is atomic at the database row level.

---

#### `peekNextNumber(string $module, string $document, ?int $branchId = null, ?int $year = null): string`

Returns what the next number *would be* without consuming it (no counter increment). Used for preview display on create forms.

**Step-by-step logic:**
1. Calls `$this->getOrCreate(...)`.
2. Calls `$sequence->getNextNumber()` on the model, which returns `$this->current_number + 1` (no DB write).
3. Pads and applies prefix the same way as `getNextNumber`.

**Return value:** `string`

---

#### `create(string $module, string $document, ?int $branchId = null, ?int $year = null, ?string $prefix = null, int $padding = 5): NumberSequence`

Creates a new sequence with explicit configuration. Intended for admin setup or seeding, not for runtime document generation.

**Step-by-step logic:**
1. Resolves `$year` to current year if null.
2. Calls `NumberSequence::create([...])` with all provided values and `current_number = 0`.

**Return value:** `NumberSequence`

---

#### `update(NumberSequence $sequence, array $data): NumberSequence`

Updates a sequence's configuration (prefix, padding, or counter value).

**Step-by-step logic:**
1. Calls `$sequence->update($data)`.
2. Returns `$sequence->fresh()`.

**Return value:** `NumberSequence`

---

#### `reset(NumberSequence $sequence): NumberSequence`

Resets the counter to zero.

**Step-by-step logic:**
1. Delegates to `$this->update($sequence, ['current_number' => 0])`.

**Return value:** `NumberSequence`

---

#### `resetTo(NumberSequence $sequence, int $value): NumberSequence`

Resets the counter to a specific value (useful for corrections or migrations).

**Step-by-step logic:**
1. Delegates to `$this->update($sequence, ['current_number' => $value])`.

**Return value:** `NumberSequence`

---

#### `getCurrentNumber(string $module, string $document, ?int $branchId = null, ?int $year = null): int`

Returns the current counter value without incrementing.

**Step-by-step logic:**
1. Calls `$this->getOrCreate(...)`.
2. Returns `$sequence->current_number`.

**Return value:** `int`

---

#### `getById(int $id): ?NumberSequence`

Fetches a sequence by primary key.

**Return value:** `NumberSequence|null`

---

#### `get(string $module, string $document, ?int $branchId = null, ?int $year = null): ?NumberSequence`

Fetches a sequence by its four-part composite key without creating it if absent.

**Step-by-step logic:**
1. Resolves `$year` if null.
2. Queries `WHERE module = ? AND document = ? AND branch_id = ? AND year = ?`.
3. Returns the first match or `null`.

**Return value:** `NumberSequence|null`

---

#### `getAllForModule(string $module): array`

Returns all sequences for a given module (all documents, branches, and years).

**Step-by-step logic:**
1. Uses `NumberSequence::forModule($module)->get()->toArray()`.

**Return value:** `array`

---

#### `getAllForModuleDocument(string $module, string $document): array`

Returns all sequences for a specific module+document pair (all branches and years).

**Step-by-step logic:**
1. Chains `forModule` and `forDocument` scopes, then calls `->get()->toArray()`.

**Return value:** `array`

---

#### `getAllForBranch(int $branchId): array`

Returns all sequences scoped to a specific branch.

**Step-by-step logic:**
1. Uses `NumberSequence::forBranch($branchId)->get()->toArray()`.

**Return value:** `array`

---

#### `delete(NumberSequence $sequence): bool`

Hard-deletes a sequence record. (NumberSequence does not use SoftDeletes.)

**Return value:** `bool`

---

#### `exists(string $module, string $document, ?int $branchId = null, ?int $year = null): bool`

Checks whether a sequence row exists without fetching it.

**Step-by-step logic:**
1. Resolves `$year` if null.
2. Queries with `->exists()`.

**Return value:** `bool`

---

#### `getNextNumbers(string $module, string $document, int $count = 1, ?int $branchId = null, ?int $year = null): array`

Generates multiple consecutive numbers in one call. Useful for batch document creation.

**Parameters:**
- `$count` — how many numbers to generate.

**Step-by-step logic:**
1. Calls `$this->getOrCreate(...)` once.
2. Loops `$count` times: each iteration calls `$sequence->increment('current_number')`, then `$sequence->refresh()` (re-reads from DB), pads, and prepends prefix.
3. Appends each formatted number to `$numbers[]`.
4. Returns the full array.

> **Note:** Unlike `getNextNumber`, this method uses `increment()` + `refresh()` in a loop rather than `incrementAndGet()`. Each iteration still issues a separate atomic `INCREMENT` SQL statement.

**Return value:** `array` of formatted number strings

---

## 6. Models

### 6.1 Branch

**File:** `app/Models/Core/Branch.php`

**Namespace:** `App\Models\Core`

**Table:** `branches`

**Traits:** `HasFactory`, `SoftDeletes`

#### Fillable Fields

```php
protected $fillable = [
    'code', 'name', 'slug',
    'email', 'phone', 'mobile', 'fax',
    'address', 'city', 'state', 'postal_code', 'country',
    'description',
    'manager_name', 'manager_email', 'manager_phone',
    'is_active', 'is_main',
    'activated_at', 'deactivated_at',
    'created_by', 'updated_by',
];
```

#### Casts

| Attribute | Cast Type | Reason |
|---|---|---|
| `is_active` | `boolean` | Prevents `"1"`/`"0"` string values leaking into PHP |
| `is_main` | `boolean` | Same as above |
| `activated_at` | `datetime` | Returns Carbon instance |
| `deactivated_at` | `datetime` | Returns Carbon instance |
| `created_at` | `datetime` | Standard timestamp |
| `updated_at` | `datetime` | Standard timestamp |
| `deleted_at` | `datetime` | SoftDeletes timestamp |

#### Relationships

| Method | Type | Target Model | Foreign Key | Notes |
|---|---|---|---|---|
| `creator()` | `BelongsTo` | `App\Models\Admin` | `created_by` | Admin who created the branch |
| `updater()` | `BelongsTo` | `App\Models\Admin` | `updated_by` | Admin who last updated the branch |

#### Query Scopes

| Scope | Method Signature | Behavior |
|---|---|---|
| `active` | `scopeActive($query)` | Adds `WHERE is_active = 1` |
| `main` | `scopeMain($query)` | Adds `WHERE is_main = 1` |
| `inactive` | `scopeInactive($query)` | Adds `WHERE is_active = 0` |

**Usage examples:**
```php
Branch::active()->get();           // All active branches
Branch::main()->first();           // The main branch
Branch::inactive()->paginate(10);  // Inactive branches
```

---

### 6.2 Setting

**File:** `app/Models/Core/Setting.php`

**Namespace:** `App\Models\Core`

**Table:** `settings`

**Traits:** `HasFactory`

No soft deletes — settings are updated in-place, never deleted via the standard UI.

#### Fillable Fields

```php
protected $fillable = [
    'scope', 'branch_id', 'key', 'value',
    'type', 'group', 'is_encrypted',
    'created_by', 'updated_by',
];
```

#### Casts

| Attribute | Cast Type |
|---|---|
| `is_encrypted` | `boolean` |
| `branch_id` | `integer` |
| `created_by` | `integer` |
| `updated_by` | `integer` |

Note: `value` is stored as `longText` (raw string). Type coercion happens in `SettingService::castValue()` at read time, not as an Eloquent cast, because the type varies per row.

#### Relationships

| Method | Type | Target Model | Foreign Key |
|---|---|---|---|
| `branch()` | `BelongsTo` | `App\Models\Core\Branch` | `branch_id` |
| `creator()` | `BelongsTo` | `App\Models\Admin` | `created_by` |
| `updater()` | `BelongsTo` | `App\Models\Admin` | `updated_by` |

No query scopes are defined on this model. The `SettingService` handles all query-building logic directly.

---

### 6.3 NumberSequence

**File:** `app/Models/Core/NumberSequence.php`

**Namespace:** `App\Models\Core`

**Table:** `number_sequences`

**Traits:** `HasFactory`

No soft deletes.

#### Fillable Fields

```php
protected $fillable = [
    'module', 'document', 'branch_id', 'year',
    'prefix', 'padding', 'current_number',
];
```

#### Casts

| Attribute | Cast Type |
|---|---|
| `branch_id` | `integer` |
| `year` | `integer` |
| `padding` | `integer` |
| `current_number` | `integer` |
| `created_at` | `datetime` |
| `updated_at` | `datetime` |

#### Relationships

| Method | Type | Target Model | Foreign Key |
|---|---|---|---|
| `branch()` | `BelongsTo` | `App\Models\Core\Branch` | `branch_id` |

#### Instance Methods

| Method | Signature | Behavior |
|---|---|---|
| `getNextNumber()` | `(): int` | Returns `current_number + 1` without modifying the database. Safe to call as preview. |
| `incrementAndGet()` | `(): int` | Calls `$this->increment('current_number')` (atomic DB increment), then `$this->fresh()->current_number`. Returns the new value. |
| `generateFormattedNumber()` | `(): string` | Returns `str_pad(current_number + 1, padding, '0', STR_PAD_LEFT)` with optional prefix. Does not increment. |

#### Query Scopes

| Scope | Method Signature | SQL Effect |
|---|---|---|
| `forModule` | `scopeForModule($query, string $module)` | `WHERE module = ?` |
| `forDocument` | `scopeForDocument($query, string $document)` | `WHERE document = ?` |
| `forBranch` | `scopeForBranch($query, int $branchId)` | `WHERE branch_id = ?` |
| `forYear` | `scopeForYear($query, int $year)` | `WHERE year = ?` |
| `companyWide` | `scopeCompanyWide($query)` | `WHERE branch_id IS NULL` |

---

## 7. Migrations

### 7.1 branches table

**File:** `database/migrations/2026_02_26_000001_create_branches_table.php`

**Runs:** very early in the migration stack (timestamp `000001` of that date), as branches are a prerequisite for settings and number sequences.

| Column | Type | Nullable | Default | Index | Purpose |
|---|---|---|---|---|---|
| `id` | bigint unsigned (auto-increment) | No | — | PK | Primary key |
| `code` | varchar(255) | No | — | UNIQUE + index | Short identifier (e.g., `BR001`). Used for display and lookup. Must be globally unique. |
| `name` | varchar(255) | No | — | — | Human-readable branch name |
| `slug` | varchar(255) | Yes | NULL | UNIQUE + index | URL-friendly version of name, auto-generated by `BranchService`. Used in routes. |
| `email` | varchar(255) | Yes | NULL | — | Primary contact email for the branch |
| `phone` | varchar(255) | Yes | NULL | — | Main phone number |
| `mobile` | varchar(255) | Yes | NULL | — | Mobile/cell number |
| `fax` | varchar(255) | Yes | NULL | — | Fax number (legacy support) |
| `address` | text | Yes | NULL | — | Street address |
| `city` | varchar(255) | Yes | NULL | — | City name |
| `state` | varchar(255) | Yes | NULL | — | State or province |
| `postal_code` | varchar(255) | Yes | NULL | — | Postal/ZIP code |
| `country` | varchar(255) | Yes | NULL | — | Country name |
| `description` | text | Yes | NULL | — | Free-text description of the branch |
| `manager_name` | varchar(255) | Yes | NULL | — | Name of the branch manager |
| `manager_email` | varchar(255) | Yes | NULL | — | Branch manager's email |
| `manager_phone` | varchar(255) | Yes | NULL | — | Branch manager's phone |
| `is_active` | tinyint(1) | No | `true` | index | Whether the branch accepts operational data. Inactive branches are excluded from dropdowns. |
| `is_main` | tinyint(1) | No | `false` | index + composite | Designates the primary/head office branch. Only one row should have this as `true`. |
| `activated_at` | timestamp | Yes | NULL | — | When `is_active` last changed to `true`. Maintained by `BranchService`. |
| `deactivated_at` | timestamp | Yes | NULL | — | When `is_active` last changed to `false`. Maintained by `BranchService`. |
| `created_by` | bigint unsigned | Yes | NULL | index | FK to `admins.id` (soft reference, no FK constraint). Records which admin created the branch. |
| `updated_by` | bigint unsigned | Yes | NULL | index | FK to `admins.id` (soft reference). Records which admin last updated the branch. |
| `created_at` | timestamp | Yes | NULL | — | Standard Laravel timestamp |
| `updated_at` | timestamp | Yes | NULL | — | Standard Laravel timestamp |
| `deleted_at` | timestamp | Yes | NULL | — | SoftDeletes. Non-null means the branch is trash. Query builder excludes these by default. |

**Composite index:** `(is_active, is_main)` — optimizes queries that filter branches by both status flags simultaneously (common in dropdowns).

---

### 7.2 settings table

**File:** `database/migrations/2026_03_06_000001_create_settings_table.php`

| Column | Type | Nullable | Default | Index | Purpose |
|---|---|---|---|---|---|
| `id` | bigint unsigned | No | — | PK | Primary key |
| `scope` | varchar(255) | No | — | index | `'system'` or `'branch'`. Controls lookup priority in `SettingService::get()`. |
| `branch_id` | bigint unsigned | Yes | NULL | FK + index | If `scope = 'branch'`, identifies which branch this setting applies to. NULL for system-wide settings. Foreign key references `branches(id)` with `nullOnDelete`. |
| `key` | varchar(255) | No | — | index | The setting's name within its group (e.g., `default_currency`). |
| `value` | longtext | Yes | NULL | — | Stored as a raw string. May be encrypted. Type coercion happens in `SettingService::castValue()`. |
| `type` | varchar(255) | No | `'string'` | — | PHP type hint for retrieval: `string`, `boolean`, `integer`, `float`, `json`. |
| `group` | varchar(255) | Yes | NULL | index | Logical category for UI grouping (e.g., `company`, `system`, `sales`). |
| `is_encrypted` | tinyint(1) | No | `false` | — | When `true`, the `value` column is stored as a Laravel `Crypt::encryptString()` ciphertext. |
| `created_by` | integer | Yes | NULL | — | ID of the admin who created this entry. |
| `updated_by` | integer | Yes | NULL | — | ID of the admin who last updated this entry. |
| `created_at` | timestamp | Yes | NULL | — | Standard Laravel timestamp |
| `updated_at` | timestamp | Yes | NULL | — | Standard Laravel timestamp |

**Unique constraint:** `(scope, branch_id, key)` — ensures each `key` is unique per scope+branch combination. This is the lookup key used by `SettingService::get()` and `SettingService::set()`.

---

### 7.3 number_sequences table

**File:** `database/migrations/2026_02_26_000002_create_number_sequences_table.php`

| Column | Type | Nullable | Default | Index | Purpose |
|---|---|---|---|---|---|
| `id` | bigint unsigned | No | — | PK | Primary key |
| `module` | varchar(255) | No | — | index | Module name (e.g., `sales`, `inventory`, `finance`). Identifies which part of the system owns this counter. |
| `document` | varchar(255) | No | — | index | Document type (e.g., `invoice`, `quotation`, `purchase_order`). Identifies the specific document type within the module. |
| `branch_id` | bigint unsigned | Yes | NULL | FK + index | Branch scope. NULL means a company-wide sequence shared across branches. References `branches(id)` with `nullOnDelete`. |
| `year` | int unsigned | No | — | index | Calendar year. Sequences can be reset annually by creating a new row for the new year. |
| `prefix` | varchar(255) | Yes | NULL | — | Optional string prepended to every generated number (e.g., `"INV"`, `"PO"`). If null, only the padded number is returned. |
| `padding` | int unsigned | No | `5` | — | How many digits the numeric part should be zero-padded to. Default `5` produces numbers like `00001`. |
| `current_number` | bigint unsigned | No | `0` | — | The last-issued sequence number. Incremented atomically by `incrementAndGet()`. The next issued number will be `current_number + 1`. |
| `created_at` | timestamp | Yes | NULL | — | Standard Laravel timestamp |
| `updated_at` | timestamp | Yes | NULL | — | Standard Laravel timestamp |

**Unique constraint:** `(module, document, branch_id, year)` — the four-part composite key that uniquely identifies a counter. `NumberSequenceService::getOrCreate` matches on this tuple.

**Formatted number example:** with `prefix = 'INV'`, `padding = 5`, `current_number = 41`, the next call to `getNextNumber` produces `"INV00042"`.

---

## 8. Dependency Injection

The Core module uses Laravel's built-in IoC container for dependency injection throughout.

### How it works

Laravel automatically resolves type-hinted constructor dependencies without any manual binding in a service provider, because all three service classes (`BranchService`, `SettingService`, `NumberSequenceService`) are concrete classes with no interface abstraction — they are auto-resolved by the container using **autowiring**.

### BranchController

```php
// app/Http/Controllers/Admin/Core/BranchController.php

private BranchService $branchService;

public function __construct(BranchService $branchService)
{
    $this->branchService = $branchService;
}
```

When Laravel routes a request to `BranchController`, it instantiates the controller via the container. The container sees `BranchService` in the constructor signature, instantiates `BranchService` (which itself has no dependencies), and injects it.

### SettingController

```php
// app/Http/Controllers/Admin/Core/SettingController.php

public function __construct(
    protected SettingService $settingService
) {}
```

Uses PHP 8 constructor property promotion. The `protected` keyword both declares the property and assigns the injected value in one step. Functionally identical to the explicit assignment used in `BranchController`.

### NumberSequenceService (used by other modules)

`NumberSequenceService` is not injected into any Core controller. Other module services (e.g., `InvoiceService` in the Sales module) declare it as a constructor dependency:

```php
// Example from another module's service
public function __construct(
    private NumberSequenceService $numberSequenceService
) {}
```

The container resolves this automatically when that service is first requested, instantiating `NumberSequenceService` once and sharing the instance within the request lifecycle.

### No manual binding required

There are no `App::bind()` or `App::singleton()` calls for Core services in any service provider. The entire resolution chain relies on Laravel's reflection-based autowiring. If interface abstraction is added in the future, bindings would need to be registered in `AppServiceProvider` or a dedicated `CoreServiceProvider`.
