# Error Boundaries & Exceptions

**NUMBER:** ADR-ERR-001
**STATUS:** CANONICAL AUTHORITY
**SCOPE:** Every class thrown via `throw`, every `try-catch` block, and every class that handles errors or builds error responses in this project.
**RULE:** This is the ONLY definitive source for Exception architecture, Mapping, and Formatting. Any other document mentioning this topic is a REFERENCE, not a definition.
**VIOLATION:** Implementing exception handling patterns based on any other source constitutes an architectural breach.

> **Response JSON structure:** `07_BOUNDARY_LAYERS.md` → Controller and Resource Rules
> **Translation file structure and key naming:** `07_BOUNDARY_LAYERS.md` → Localization Confinement
> **Error codes and HTTP status catalog (What to return):** `SPECIFICATIONS/06_API/08_ERROR_CODES.md`
> **Where Domain Validators throw exceptions:** `08_FORM_REQUESTS.md` → Domain Validators

---

## 1. Philosophy

**Throw Data, Not Texts.**

A Domain Exception is an **abstract data carrier** — not a user notification tool. Its sole function is to stop execution and announce a "failure fact" carrying raw context (e.g., expected amount, received amount). 

The Domain layer is completely blind to HTTP (Status Codes) and Presentation (Localization). The conversion from "abstract fact" to "formatted HTTP response" happens at exactly one point: the Mapping Layer.

The Formatter is a **blind builder**. It constructs JSON structure without understanding the semantic meaning of the error (e.g., it does not know what "forbidden" means, it only knows it must build an error payload).

---

## 2. Absolute Prohibitions

```text
❌ PROHIBITED: Putting HTTP Status Codes (e.g., 403, 422) as Properties inside Domain Exceptions.
❌ PROHIBITED: Using __() or any translated text inside Domain Exception constructors.
❌ PROHIBITED: Using try-catch in Actions/Services to convert Business Exceptions to JsonResponse.
❌ PROHIBITED: Building responses using Global Helpers (e.g., error(), success()) inside Mappings.
❌ PROHIBITED: Putting semantic shortcuts (e.g., forbidden(), unprocessable()) inside ApiResponseFormatter.
❌ PROHIBITED: Passing Request objects into the Exception.
❌ PROHIBITED: Using response()->json() inside Exception Mappings (must use ApiResponseFormatter).
❌ PROHIBITED: Creating Generic Exceptions that handle multiple unrelated business contexts (See Section 4).
❌ PROHIBITED: Using string parsing (like str_contains) on log messages in ExceptionMappings to determine context.
```

---

## 3. Domain Exception Architecture

Every exception in `App\Exceptions\Business` MUST follow this strict structure to ensure data purity.

### 3.1 The Single Context Rule (CANONICAL)

One Exception class = One specific business failure context.

```text
❌ PROHIBITED: DataIntegrityException used for "Duplicate Phone", "Missing Relation", and "Invalid State".
✅ MANDATORY: DuplicatePhoneException, MissingRelationException, InvalidStateException as separate classes.
```

**Reason:** A generic exception forces the Mapping layer to guess the context via string parsing or conditional checks, which is fragile and violates clean architecture. Specialized exceptions carry exact getters for their exact context.

### 3.2 Construction Rules

1. Extends `RuntimeException`.
2. Accepts NO translated text in the Constructor.
3. Accepts **raw data** representing error context (numbers, names, states).
4. Builds an English technical message for `parent::__construct` (appears in Logs/CLI only).
5. Provides public Getters for the raw data so the Mapping layer can extract them.

### 3.3 Template: With Dynamic Context

Used when the error message needs to inject raw data (e.g., amounts, IDs).

```php
namespace App\Exceptions\Business;

use RuntimeException;

class PaymentMismatchException extends RuntimeException
{
    public function __construct(
        private readonly string $expectedAmount,
        private readonly string $receivedAmount,
        ?\Throwable $previous = null,
    ) {
        $logMessage = "Payment mismatch: Expected {$expectedAmount}, Got {$receivedAmount}";
        parent::__construct($logMessage, 0, $previous);
    }

    public function getExpectedAmount(): string { return $this->expectedAmount; }
    public function getReceivedAmount(): string { return $this->receivedAmount; }
}
```

### 3.4 Template: Simple (No Dynamic Context)

Used when the error is a static state violation.

```php
class ContractImmutableException extends RuntimeException
{
    public function __construct(?string $logMessage = null, ?\Throwable $previous = null)
    {
        parent::__construct(
            $logMessage ?? 'Attempted to modify an immutable contract.',
            0,
            $previous
        );
    }
}
```

---

## 4. Domain Exception Catalog (CANONICAL)

This is the single source of truth for all domain exceptions. No other file should define these classes or their structural purpose.

### Simple Exceptions (No dynamic context required)

| Exception Class | Log Message | Thrown When |
|:---|:---|:---|
| `ClientImmutableException` | `Attempted to delete a client.` | Any client deletion attempt |
| `ContractImmutableException` | `Attempted to modify an immutable contract.` | Modify/delete non-draft contract |
| `NoPendingInstallmentException` | `No pending installments found for contract {id}.` | No remaining installments to pay |

### Context-Aware Exceptions (Carry raw data via Getters)

| Exception Class | Raw Data (Getters) | Thrown When |
|:---|:---|:---|
| `DuplicatePhoneException` | `getPhone(): string` | Phone number already exists |
| `MissingRelationException` | `getEntity(): string`, `getId(): int` | Required linked resource not found |
| `InvalidStateException` | `getCurrentState(): string`, `getRequiredState(): string` | Entity is in wrong state for operation |
| `PaymentMismatchException` | `getExpectedAmount()`, `getReceivedAmount()` | Amount does not match installment |
| `LifoViolationException` | `getAttemptedPaymentId()`, `getLatestPaymentId()` | Operation on non-latest payment |
| `InsufficientShareBalanceException` | `getRequestedWithdrawal()`, `getCurrentBalance()` | Withdrawal results in negative balance |
| `ShareNotLatestException` | `getAttemptedLogId()`, `getLatestActiveLogId()` | Modify/delete non-latest active share |
| `ShareLockPeriodExpiredException` | `getLogId()`, `getCreatedAt()`, `getDaysElapsed()` | Share record older than 30 days |

---

## 5. The Mapping Layer

The only permitted bridge between the Domain world and the HTTP world. Invoked automatically by the framework when any exception is thrown.

### 5.1 Rules

1. Registered centrally via `withExceptions()` in `bootstrap/app.php`.
2. The **ONLY** place that decides the HTTP Status Code.
3. Extracts raw data via the Exception's Getters.
4. Uses `__()` with `:placeholders` for translated user-facing text.
5. Calls `ApiResponseFormatter::error()` to build the JSON.

### 5.2 Framework Registration (Laravel 11+)

```php
// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
    foreach (\App\Exceptions\ExceptionMappings::map() as $exception => $handler) {
        $exceptions->map($exception, $handler);
    }
})
```

### 5.3 Standard Mapping Template

```php
namespace App\Exceptions;

use App\Services\ApiResponseFormatter;
use App\Exceptions\Business\DuplicatePhoneException;
use App\Exceptions\Business\ContractImmutableException;

class ExceptionMappings
{
    public function __construct(
        private readonly ApiResponseFormatter $formatter
    ) {}

    public function map(): array
    {
        return [
            // Context-Aware Exception (Specific Mapping, no str_contains)
            DuplicatePhoneException::class => function (DuplicatePhoneException $e) {
                return $this->formatter->error(
                    statusCode: 422,
                    errorKey: 'duplicate_phone',
                    message: __('errors/client.phone_unique', ['phone' => $e->getPhone()]),
                );
            },

            // Simple Exception
            ContractImmutableException::class => function (ContractImmutableException $e) {
                return $this->formatter->error(
                    statusCode: 403,
                    errorKey: 'immutable_contract',
                    message: __('errors/contract.cannot_modify')
                );
            },

            // Framework Exceptions
            \Illuminate\Validation\ValidationException::class => function ($e) {
                return $this->formatter->error(
                    statusCode: 422,
                    errorKey: 'validation_error',
                    message: __('errors/general.validation_failed'),
                    context: ['errors' => $e->errors()]
                );
            },

            // Fallback — catches any unmapped exceptions safely
            \Throwable::class => function (\Throwable $e) {
                if (app()->isProduction()) {
                    return $this->formatter->error(
                        statusCode: 500,
                        errorKey: 'server_error',
                        message: __('errors/general.server')
                    );
                }
                return null; // Lets Laravel show Whoops in local dev
            }
        ];
    }
}
```

---

## 6. The Formatting Layer (ApiResponseFormatter)

Responsible for JSON structure ONLY. A blind builder.

### 6.1 Rules

1. Contains an internal `_response()` method, and two public methods: `success()` and `error()`.
2. **NO semantic shortcuts** — no `forbidden()`, `notFound()`, `unprocessable()`, etc.
3. Registered as a Singleton and accessed via `HasApiResponse` trait in Controllers.

---

## 7. The Catch Rules

```text
PROHIBITED (Absolutely):
   Catching Business Exceptions to convert them to JSON inside Service or Action.
   Reason: Breaks isolation. Let the exception bubble up to the central Handler.

PERMITTED (Resource Cleanup Only):
   try-catch ONLY when dealing with a shared resource that needs release
   (e.g., Locks) outside the DB::transaction scope.

   Strict Rules for Permitted Catch:
   1. Catches \Throwable (locks must be released even on system errors).
   2. Contains exactly one cleanup line (e.g., $lock->release()).
   3. Must re-throw immediately (throw $e;) without modification.
   4. NO logging, NO returning a Response.
```

### The ONLY Accepted Pattern

```php
public function execute(PaymentDTO $dto): Payment
{
    $lock = Cache::lock("contract_{$dto->contractId}", 10);

    try {
        return DB::transaction(fn () => $this->paymentService->process($dto));
    } catch (\Throwable $e) {
        $lock->release();
        throw $e;
    }
}
```
**Note:** If there is no Lock or external resource requiring cleanup, `try-catch` is strictly PROHIBITED.

---

## 8. Edge Cases

### 8.1 Execution Outside HTTP (CLI / Queue Workers)

When an Exception is thrown in a CLI environment (Artisan Commands) or Queue Worker:
- The `renderable` mapping in the Handler **WILL NOT be invoked**.
- The Exception will reach the default CLI error handler.
- Because we put a clear English technical message in `parent::__construct`, the Terminal will print it clearly for the developer.
- **Result:** The system works seamlessly across all environments without modifying Domain code.

### 8.2 Dynamic Translation Keys

When an error key needs to accept parameters for dynamic content, use a `_detail` suffix to distinguish it from the base key.

```php
// Base key (no parameters)
'phone_unique' => 'This phone number is already registered.'

// Detail key (with parameters) — used in ExceptionMappings.php ONLY
'phone_unique_detail' => 'Phone number :phone is already registered by another user.'
```

---

## 9. Adding a New Exception

Follow this exact sequence:

1. **Define Context:** Is this a new specific failure? Create a new Exception class.
2. **Create Class:** Create in `app/Exceptions/Business/` following the templates in Section 3.
3. **Add Mapping:** Add one closure in `ExceptionMappings.php`.
4. **Add Translation:** Add the translation key in `lang/ar/errors/{domain}.php`.

---

## 10. Decision Cheat Sheet

When writing code that throws or catches an exception:

1. Am I about to throw a generic exception for two different reasons?
   └─ YES → ERROR. Create two specific Exception classes.

2. Do I need to tell the user dynamic numbers (amounts, balances)?
   └─ YES → Exception carries numbers as Properties + Getters.
   └─ NO  → Simple Exception without extra data.

3. Am I using __() inside the Exception Constructor?
   └─ YES → ERROR. Remove it. Put an English message for developers only.

4. Am I using str_contains in ExceptionMappings to find out what went wrong?
   └─ YES → ERROR. Create a specific Exception class.

5. Am I using try-catch in the Action to return response()->json()?
   └─ YES → ERROR. Remove the catch, let the exception bubble up.

6. Where do I decide the HTTP Status Code?
   └─ Inside the Exception Closure in ExceptionMappings.php ONLY.

7. Am I adding a forbidden() or notFound() method to ApiResponseFormatter?
   └─ YES → ERROR. The Formatter must remain blind. Use error(statusCode: 404).