# Backend Development Rules

## DDD & Actions
- **Pure Domain:** Actions (`src/Domain`) MUST return Models/Collections. NEVER return HTTP arrays (`['success' => true]`).
- **Controllers:** Handle HTTP, try/catch, validation, and JSON responses.
- **Exceptions:** Actions throw specific exceptions (e.g., `ModelNotFoundException`); Controllers catch them. **NEVER swallow exceptions silently.** Any `catch (\Throwable $e)` block MUST either log the error contextually (`Log::error($e->getMessage())`) or re-throw it. Returning `false` or `null` silently masks critical system failures.

## Database & Models
- **Tenancy:** Tenant models MUST use `UsesTenantConnection` and `$connection = 'tenant_connection'`.
- **Dynamic Models:** NEVER resolve tenant models via string concat (e.g., `App\Models\$name`). ALWAYS use the `GetTenantModel($name)` helper to prevent cross-tenant leakage.
- **NO Tenant ID:** Do NOT add `where('tenant_id', $id)` clauses. The connection provides the scope.
- **Core DB:** `mysql` connection is for Jobs/Queues ONLY.
- **SQL Safety:** ALWAYS qualify columns in `join` or self-referential queries (e.g., `where('projecttasks.isparent', true)`).
- **Optimistic Locking Guard (409):** Standard update/save interfaces (PUT/PATCH) MUST require a client timestamp (`client_version` or `updated_at`). The backend MUST check this against the database record before updating. If mismatched, abort with HTTP 409 Conflict to prevent silent data overwrite.

## Filesystem Path Resolution
- **Disk Abstraction (MANDATORY):** File paths that are managed by a Laravel Storage disk MUST always be resolved using `Storage::disk('name')->path($relativePath)`. Never use `storage_path('app/' . $path)`, `base_path(...)`, or other framework helper assumptions to derive absolute paths for disk-managed files. The disk root is configurable — hardcoded helper paths become incorrect whenever `config/filesystems.php` changes.
    ```php
    // ❌ BAD — hardcodes disk root; breaks when config changes
    $absolute = storage_path('app/' . $relativePath);

    // ✅ GOOD — resolves via the configured disk
    $absolute = Storage::disk('local')->path($relativePath);
    ```
- **Consistency Rule:** Any method that uses `Storage::disk('local')->exists($path)` for existence checks MUST use the same disk abstraction (`Storage::disk('local')->path($path)`) for absolute path resolution. Mixing the two is always incorrect.

## Background Jobs & Concurrency
- **Idempotency:** Jobs MUST be safely retriable. When making DB insertions from a background job, prefer idempotent write patterns like `firstOrCreate()` or `upsert()` over raw `insert()` to prevent unique constraint violations on re-entry. When checking for an existing record via an external provider ID (e.g., a cloud file ID, a webhook event ID), always perform a `Model::where('provider_id', $id)->first()` guard at the start of the create action and return the existing record if found.
- **Non-idempotent multi-step jobs — `$tries` safety net (BUG-M):** If a job performs multiple non-idempotent side-effects in sequence (e.g., cloud file upload followed by DB record creation), it MUST either (a) make **every** step explicitly idempotent, or (b) set `public int $tries = 1;` to prevent automatic retries of a partially-executed sequence. Option (b) must be accompanied by a docblock comment explaining which steps are non-idempotent and why retries would be unsafe (duplicate cloud files, double-billing, etc.). Increasing `$tries` in the future requires first verifying idempotency for all steps under the target conflict strategy.
- **Race Condition Prevention:** Any multi-step/coordinator job that could be dispatched concurrently MUST use a distributed lock (`Cache::lock('key', 10)->get()`) / Redis SETNX to prevent duplicate coordination attempts.
- **Atomic Redis Operations:** NEVER execute `HSET` followed by an independent `EXPIRE` in concurrent environments. This creates non-atomic race conditions. ALWAYS use Redis Pipelines (`Redis::pipeline()`) or Lua scripts for atomic dual-operations.
- **Connection Safety & Fallbacks:** High-volume jobs or listener events writing critical data MUST wrap DB writes in a bounded retry loop (e.g., 3 retries with backoff) to survive connection pool exhaustion. Never allow silent data loss.
- **Temporary Resource Cleanup (`failed()` hook):** Any queued job that creates a temporary resource (temp file, temp directory, external lock, API session) in its constructor or dispatch path MUST declare `public function failed(Throwable $e): void` to clean up that resource when the job exhausts its retries. Without this hook, permanent job failure leaves the resource leaked indefinitely. The `failed()` method MUST delete/release the resource AND emit a `Log::warning` with the resource identifier and failure reason.
- **Controller-Side Dispatch Cleanup (Corollary to `failed()` hook — BUG-F-CTRL):** When a controller stores a temporary resource (e.g., `$file->store('temp/...', 'local')`) immediately before `dispatch()`-ing a job, it MUST also guard the dispatch with a `try/catch (Throwable $e)` that deletes the temp resource and re-throws. The `failed()` hook on the job only runs for jobs that were successfully queued — a dispatch-phase failure (queue connection error, serialization error) means the job is never queued and `failed()` never fires. The controller is the last line of defense for pre-dispatch resources.
    ```php
    // ✅ CORRECT — controller guards the dispatch, not just the job
    $tempPath = $file->store('temp/cloud_uploads', 'local');
    try {
        UploadFileToCloudJob::dispatch($tempPath, ...);
    } catch (Throwable $e) {
        Storage::disk('local')->delete($tempPath);
        throw $e;
    }
    ```
- **Orphan Recovery System:** Asynchronous multi-step dispatch chains (e.g., jobs dispatching jobs or API callback success processing) MUST NOT rely solely on "fire and forget". They must implement or hook into an Orphan Detection metric/recovery system to catch completed tasks that failed to trigger the subsequent background dispatch step.
- **Connection Pool Circuit Breakers:** Critical async listeners (like `BatchJobCompleted`) MUST NOT fail silently or mask "Connection Pool Limits Reached" PDO errors. `getConnection()` logic MUST be wrapped in a bounded retry loop with exponential backoff. If exhaustion persists, the event MUST be pushed to a durable fallback (Redis List / Emergency Storage) instead of discarding the payload.
- **External API Loop Cap (MANDATORY):** Every `while` or `do/while` loop whose continuation depends on an external API response (pagination token, async status, existence check) MUST define a hard maximum iteration limit before the loop. Breaching the limit MUST emit `Log::warning` with the iteration count and break the loop. There is no safe "infinite" loop over an external API — a misbehaving remote response (circular token, always-truthy condition) turns a missing cap into an infinite loop on the worker, causing process starvation.
    ```php
    // ❌ BAD — no cap; a circular nextLink halts the worker forever
    do {
        $response = $client->get($nextLink);
        $nextLink = $response['@odata.nextLink'] ?? null;
    } while ($nextLink);

    // ✅ GOOD — bounded with warning on cap breach
    $maxPages = 500;
    $page = 0;
    do {
        $response = $client->get($nextLink);
        $nextLink = $response['@odata.nextLink'] ?? null;
        if (++$page >= $maxPages) {
            Log::warning('External API pagination cap reached', ['page' => $page, 'url' => $nextLink]);
            break;
        }
    } while ($nextLink);
    ```
- **Scoped Buffered Queries:** When executing nested queries inside Observers, Event Listeners, or Loops traversing massive datasets, wrap the interaction using Scoped Buffered Queries (`$pdo->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true)`) inside a `try/finally` block to prevent "Cannot execute queries while other unbuffered queries are active" PDO conflicts. NEVER enable this globally.
- **Last-Resort Writer Pattern:** Any class whose primary function is to record failure evidence (e.g., audit log writers, error event recorders, dead letter dispatchers) MUST apply the same bounded-retry + durable-fallback contract as async listeners. Being called synchronously does not exempt a class — a single-attempt write failure in a failure-recorder silently destroys the only evidence of the original error, which is a worse outcome than the original failure itself.
- **Fallback Logger Serialization Safety:** Any durable fallback log call that captures an external payload MUST use a fault-tolerant encoding strategy. Bare `json_encode($data)` returns `false` when the payload contains non-serializable types (PHP resources, closures, circular references) — the log entry then records `false` instead of the payload, silently defeating its own purpose. Use `json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR) ?: '[non-serializable payload]'`: this preserves all serializable fields, substitutes `null` for unserializable ones, and guarantees the log entry always contains a non-empty string. A last-resort logger that fails on the same input that caused the original failure provides zero recovery value.

## Controllers
- **RESTful:** Use `store()`, `update()`, `destroy()`. Avoid dual-purpose methods like `create($id=null)`.
- **Validation:** NEVER use `$request->all()`. ALWAYS use specific FormRequests.
- **Response:** API routes return JSON; Web routes return View/Redirect.

- **Multi-Tenant:** ALWAYS use the `ProcessesMultipleTenants` trait for iteration instead of manually chunking models and purging connections. This provides standardized logs, connection rotation, and safe exception bubbling.
- **Batch Cleanup Safety Windows:** Any command/CRON deleting or recycling records using timestamp thresholds (e.g., `updated_at < X`) MUST enforce a `Safety Window` parallel condition (e.g., `updated_at <= DATE_SUB(NOW(), INTERVAL 10 MINUTE)`). NEVER delete up to the exact current second to prevent destroying transient data from concurrent processes. ALWAYS use `--batch` chunks to prevent massive lock contention.

## Redis & Integrations
- **Predis Eval Signature:** The project uses the `predis` driver. When calling `Redis::eval($script, ...)`, the second argument MUST be an integer representing the number of Redis keys, followed by the variadic keys and arguments (e.g., `Redis::eval($script, 2, $key1, $key2, $arg1)`). NEVER pass an array `[$key]` as the second argument; it will cause a silent TypeError.
- **Cursor Iterations (`SCAN` vs `KEYS`):** When writing agnostic cleanup commands analyzing thousands of Redis keys, IF using `SCAN` in PHP, the initial cursor MUST be serialized as `null`, NOT `0` (`0` causes immediate false returns). Because `SCAN` requires manual prefix-handling depending on the driver, preferred logic should be: Wrap SCAN in a `Restry/Backoff` loop, and if failures persist, provide an explicit fallback utilizing the generic `$redis->keys()` command.
- **Redis Key Lifecycle Contract (MANDATORY):** Every `Redis::set()` / `Redis::setex()` for a resource-scoped, unbounded, or dynamically generated key (e.g. `flow:{id}:run:{runId}`) MUST have a corresponding and verifiable cleanup mechanism to prevent Redis memory leaks. 
  - If a domain requires a periodic or terminal cleanup service (e.g., `CleanupFlowDataService::getRunKeyPatterns()`), **every** key pattern written by the domain's workers (e.g., `ProcessFlow`) MUST be explicitly registered there. 
  - A contract test (like `CleanupFlowDataServiceTest`) MUST enforce this invariant by asserting that every known writable key suffix appears in the cleanup pattern array. No new Redis write may be added to an entity without its corresponding cleanup registration.

## Return Value Ambiguity
- **Distinct Sentinel Values:** When a function, script, or subprocess can reach the **same return value** through **semantically different execution paths** (e.g., "no mutation occurred" vs "mutation succeeded and result is zero"), you MUST use **distinct return values** (sentinel values) for each path. This ensures downstream branching, logging, and error reporting accurately represent what actually happened. Returning the same value for different outcomes leads to misleading logs and incorrect conditional logic.

## Trait & Reflection Safety
- **Public Visibility for External Calls:** If a Service or Controller uses `method_exists($model, 'traitMethod')` and calls it dynamically, the trait method MUST be `public`. 
- **The `method_exists` trap:** `method_exists()` returns `true` for `protected` and `private` methods, leading to a PHP Fatal Error when called from an external scope. If a trait provides a contract for external resolution, its methods must be public.

## Eloquent Select Safety
- **Column Verification (MANDATORY):** Before adding any column to a `select()` call, verify it exists:
  `php artisan tinker --execute="Schema::connection('tenant_connection')->getColumnListing('table');"`
- **FK in select:** If a relation is eager-loaded via `->with('relation')`, its FK column MUST be in `select()`.
- **No Duplicates:** NEVER assign the same response key twice in the same method. Run before commit:
  `grep -n "\$varName\['" file.php | awk -F"'" '{print $2}' | sort | uniq -d`
- **Dead Code (Global Heuristic):** Any newly created or modified method, conditional, or configuration key MUST have a verifiable execution path. If a method or array key is defined but a `grep` search across `src/` yields zero invocations outside its own definition, it is Dead Code and MUST be deleted. Do not leave "orphan" code for future use.
- **See also:** `docs/ai-rules/600-eloquent-safe-patterns.md` for full examples and detection commands.
