# Implementation Plan: Customer Management — SP-04 Re-execution

**Branch**: `005-customer-management-reexec` | **Date**: 2026-06-07 | **Spec**: [spec.md](./spec.md)

**Input**: Feature specification from `/specs/005-customer-management-reexec/spec.md`

## Summary

Re-execute the Customer Management domain (SP-04) end-to-end to replace the existing faulty implementation. The target behavior is fully defined in the spec: customers live in the unified `clients` table with the `"customer"` flag added to `client_type_flags` on creation; listing reads from the `customer_listing_mv` materialized view (cursor-paginated); details compute the financial summary live from `installments` joined to `contracts`; updates are allowed without touching flags; no delete endpoint exists; the Client model is hard-immutable at the Eloquent level. Three Spatie permissions (`customers.view`, `customers.create`, `customers.update`) are seeded and attached to the existing Admin.

The technical approach is a **delete-and-rewrite of the entire Customer domain directory** because the existing code uses patterns the constitution now forbids (Repository pattern, DTOs, custom Pipelines, custom ExceptionHandler, non-standard ApiResponseHelper response shape, missing flag management, wrong route paths, no permission middleware). The plan also updates the cross-cutting helpers (`ApiResponseHelper`, `Client` model, exception handling) to align with the constitution's mandated pattern.

## Technical Context

**Language/Version**: PHP 8.3+ (project requirement; verified in `composer.json`)

**Primary Dependencies**:
- Laravel 11+ (PHP framework, `^11.0`)
- `spatie/laravel-permission` (RBAC, already installed per `bootstrap/providers.php` / `config/permission.php`)
- `laravel/sanctum` (authentication, already configured per `config/sanctum.php`)
- PostgreSQL 15+ (database, per constitution Section 1)
- `brick/math` (`bcmath` polyfill, available via `vendor/brick/math`)
- PHPUnit (testing, already configured per `phpunit.xml`)

**Storage**: PostgreSQL 15+ (database); Laravel Storage on the `public` disk (avatar files under `avatars/`, see FR-006)

**Testing**: PHPUnit — Feature tests for every API endpoint, Unit tests for any pure logic; coverage target 80% per constitution Section 2.V

**Target Platform**: Linux server inside Docker (`docker-compose.yml`, `Dockerfile` in repo root)

**Project Type**: Web service — REST API only (consumed by mobile app, per constitution Section 1)

**Performance Goals** (from spec SC-001 / SC-002):
- Create: 201 within 500 ms
- List (first page): within 1 s for ≥ 10,000 customers
- Update: 200 within 1 s
- Details: 200 within 1 s

**Constraints** (per constitution):
- No business logic in Controller or Model (everything in Service)
- All input via FormRequest
- All output via `ApiResponseHelper`
- Domain routes in `app/Domains/Customer/Routes/v1/api.php`
- Cursor pagination, not offset
- `client_type_flags` updates via `save()` only (no `saveQuietly()`)
- Client entity hard-immutable (delete protection at the Model level)
- No Repository pattern (use Model Scopes)
- No DTOs (use Eloquent + Resources directly)
- All monetary math via `bcmath`
- `Asia/Damascus` timezone

**Scale/Scope**: 10,000+ customers, 100,000+ contracts, 1,000,000+ installments; list data may be 5 minutes stale; details must be live

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

| # | Constitutional Rule | Current State | Target | Status |
|---|---|---|---|---|
| 2.I | No business logic in Controller or Model | Controller and Service mix concerns; Model has minimal logic | Service-only business logic; thin Controller; Model only Eloquent + Scopes | ❌ Violation — fixed by rewrite |
| 2.I | No Repository pattern (use Model Scopes) | Customer domain uses `EloquentCustomerRepository` + `CustomerRepositoryInterface` + `BaseEloquentRepository` | Delete all `app/Shared/Repositories/`, `app/Domains/Customer/Repositories/`; use Model Scopes | ❌ Violation — fixed by delete+rewrite |
| 2.I | No DTOs (use Eloquent + Resources) | `app/Domains/Customer/DTO/CustomerProfile.php` and `CustomerFinancialSummary.php` exist | Delete entire `app/Domains/Customer/DTO/` directory | ❌ Violation — fixed by delete |
| 4.1 | Response format: `success` / `message` / `data` / `meta` | `ApiResponseHelper` uses `status` instead of `success`; error `message` is array-wrapped | Update helper to match constitution: top-level `success: true|false`, string `message` for errors | ❌ Violation — fixed by helper rewrite |
| 4.5 / 4.6 | All responses via `ApiResponseHelper`; no `JsonResource` direct | `CustomerController` returns raw `new CustomerResource(...)` and `JsonResponse` | Wrap every Resource return through `success()` / `resourceCreatedResponse()` | ❌ Violation — fixed by rewrite |
| 4.7 | Routes in `app/Domains/Customer/Routes/v1/api.php`; `routes/api.php` empty; Service Provider in `bootstrap/providers.php` | Routes exist in correct file but paths are wrong (`/customer` instead of `/customers`); no Service Provider registration check; `routes/api.php` not yet empty | Rewrite routes with `/customers` prefix; verify Service Provider registered | ⚠ Partial — fixed by rewrite |
| 5 | `auth:sanctum` + `permissions:customers.<action>` on every route | Only `auth:sanctum` is applied; no permission middleware | Add `permissions:customers.view` etc. to every route | ❌ Violation — fixed by rewrite |
| 6.2 (Section 12) | `ExceptionMappings.php` single source for exception → response mapping | No `ExceptionMappings.php`; uses `ExceptionHandler.php` (custom), `config/ExceptionClassToMethod.php`, `CostumErrorResponse.php` | Create `app/Exceptions/Business/ClientImmutableException`; create `app/Exceptions/ExceptionMappings.php`; register in `bootstrap/app.php` via `withExceptions()`; delete `ExceptionHandler.php`, `config/ExceptionClassToMethod.php`, `CostumErrorResponse.php` | ❌ Violation — fixed by delete+rewrite |
| 6.4 | Client hard-immutable; override `boot()`, `delete()`, `destroy()` to throw | `Client` model only overrides `booted()`; uses wrong exception class; English default message | Override `boot()`, `delete()`, `destroy()` on Client model; throw `ClientImmutableException` with Arabic message; remove old `ImmutabilityViolationException` | ❌ Violation — fixed by rewrite |
| 6.4 (Section 9 NEVER) | No soft delete; no `SoftDeletes` trait | `Client` uses `Authenticatable` (not `Model`); no `SoftDeletes` trait present (clean) | Change `Client` base to `Model`; verify no `SoftDeletes` | ⚠ Wrong base class — fixed by rewrite |
| 9 (ALWAYS) | `client_type_flags` updated with `save()`, not `saveQuietly()` | Current `ClientService` uses `Client::create()` and `$client->update()` — never touches `client_type_flags` at all | Add a `markAsCustomer()` helper on Client that uses `$this->save()`; call it in the same DB transaction as creation | ❌ Violation (flag never set) — fixed by rewrite |
| 9 (ALWAYS) | Use Model Scopes for reusable query logic | Customer domain uses custom Pipelines (`DateFilterPipeline`, `SearchPipeline`, `SortPipeline`, `CustomerQueryPipeline`) | Delete `Pipelines/`; use Model Scopes on the MV (or on `Client`) for search/filter/sort | ❌ Violation — fixed by delete+rewrite |
| 9 (ALWAYS) | Use `installment_id` to link payments to installments | Out of SP-04 scope, but verified present in `Payment` model | n/a (no change) | ✅ Already correct |
| 3.2 | Unified `clients` table with `client_type_flags` JSONB | Table exists; flags column exists; current `CustomerService` never sets the flag | Update flag via `markAsCustomer()` in same transaction as `create()` | ❌ Violation — fixed by rewrite |
| 4.3 | Cursor pagination, not offset | Current list uses `currentPage()` / `lastPage()` (offset semantics) | Use `->cursorPaginate(20)` and read `next_cursor` / `prev_cursor` | ❌ Violation — fixed by rewrite |
| 14 | List reads from `customer_listing_mv`; details compute live | Repository uses a live JOIN+GROUP BY query for both list and detail; no MV usage | List from `CustomerListingMv` Model; details via live `installments` JOIN `contracts` query in `CustomerService` | ❌ Violation — fixed by rewrite |
| 14 | Time filter on `first_contract_date` | Repository pipeline filters on a different field (not stored) | Use the MV's `first_contract_date` field for filter | ❌ Violation — fixed by rewrite |
| Domain: Customer | Required file structure: `Models/`, `Services/`, `Http/{Controllers,Requests,Resources}/`, `Routes/v1/api.php`, Service Provider | Exists with extra forbidden directories (`Repositories/`, `Pipelines/`, `DTO/`, `Models/Query/`, `Models/Views/`) and wrong name for MV model | Strip to required structure only; rename `Models/Views/CustomerFinancialSummaryView` to `Models/CustomerListingMv` | ❌ Violation — fixed by delete+rewrite |
| Authorization | Three permissions assigned to Admin | No seeder; no permission records; no Admin assignment | Create database seeder (or `php artisan db:seed` step) that creates `customers.view`, `customers.create`, `customers.update` and attaches them to the current Admin user | ❌ Violation — added in scope |

**Verdict: 14 violations and 2 partial conformances.** Per the re-execution guidelines, the current code is the "faulty execution artifact" and **must be deleted and rewritten**, not patched.

## Project Structure

### Documentation (this feature)

```text
specs/005-customer-management-reexec/
├── plan.md              # This file (/speckit.plan command output)
├── research.md          # Phase 0 output (/speckit.plan command)
├── data-model.md        # Phase 1 output (/speckit.plan command)
├── quickstart.md        # Phase 1 output (/speckit.plan command)
├── contracts/           # Phase 1 output (/speckit.plan command)
│   ├── create-customer.md
│   ├── list-customers.md
│   ├── get-customer.md
│   └── update-customer.md
├── checklists/
│   └── requirements.md
└── spec.md              # The feature specification
```

### Source Code (repository root)

The Laravel application source is under `src/`. Only files relevant to this feature are listed.

```text
src/
├── app/
│   ├── Domains/
│   │   └── Customer/
│   │       ├── CustomerServiceProvider.php           [REWRITE]
│   │       ├── Http/
│   │       │   ├── Controllers/
│   │       │   │   └── CustomerController.php        [REWRITE]
│   │       │   ├── Requests/
│   │       │   │   ├── StoreCustomerRequest.php      [RENAME from CreateCustomerRequest; UPDATE]
│   │       │   │   ├── UpdateCustomerRequest.php     [UPDATE]
│   │       │   │   └── ListCustomersRequest.php      [KEEP, UPDATE]
│   │       │   └── Resources/
│   │       │       ├── CustomerListResource.php      [KEEP, UPDATE]
│   │       │       └── CustomerDetailResource.php    [KEEP, UPDATE]
│   │       ├── Models/
│   │       │   ├── Client.php                        [MOVE from app/Models; REWRITE]
│   │       │   └── CustomerListingMv.php             [RENAME from CustomerFinancialSummaryView; REWRITE]
│   │       ├── Routes/
│   │       │   └── v1/
│   │       │       └── api.php                       [REWRITE]
│   │       ├── Services/
│   │       │   └── CustomerService.php               [REWRITE]
│   │       ├── DTO/                                  [DELETE entire directory]
│   │       ├── Pipelines/                            [DELETE entire directory]
│   │       ├── Repositories/                         [DELETE entire directory]
│   │       ├── Models/
│   │       │   ├── Query/                            [DELETE]
│   │       │   └── Views/                            [DELETE; content moves to top-level Models/]
│   │       ├── Http/Resources/CustomerResource.php   [DELETE; superseded by List+Detail]
│   │       └── Services/ClientLookupService.php      [DELETE; not needed]
│   ├── Exceptions/
│   │   ├── Business/
│   │   │   └── ClientImmutableException.php          [CREATE NEW]
│   │   ├── ExceptionMappings.php                     [CREATE NEW]
│   │   ├── Custom/                                   [DELETE entire directory]
│   │   ├── ApiResponseException.php                  [DELETE; replaced by ExceptionMappings + Laravel 11]
│   │   ├── ApiRateLimitException.php                 [DELETE]
│   │   ├── ApiConnectionException.php                [DELETE]
│   │   └── ExceptionHandler.php                      [DELETE; replaced by bootstrap/app.php withExceptions()]
│   ├── Helpers/
│   │   ├── ApiResponseHelper.php                     [REWRITE to match constitution 4.1/4.5]
│   │   └── CostumErrorResponse.php                   [DELETE; forbidden by constitution]
│   ├── Models/
│   │   └── Client.php                                [DELETE; moved to Customer/Models/Client.php]
│   └── Shared/
│       └── Repositories/                             [DELETE entire directory; forbidden]
├── bootstrap/
│   ├── app.php                                       [UPDATE: add ExceptionMappings::map() loop]
│   └── providers.php                                 [UPDATE: register CustomerServiceProvider]
├── routes/
│   └── api.php                                       [UPDATE: ensure empty except guidance comment]
├── database/
│   └── seeders/
│       └── CustomerPermissionsSeeder.php             [CREATE NEW]
└── tests/
    ├── Feature/
    │   └── Customer/
    │       ├── CreateCustomerTest.php                [CREATE NEW]
    │       ├── ListCustomersTest.php                 [CREATE NEW]
    │       ├── ShowCustomerTest.php                  [CREATE NEW]
    │       └── UpdateCustomerTest.php                [CREATE NEW]
    └── Unit/
        └── Domains/Customer/
            ├── ClientImmutabilityTest.php            [CREATE NEW]
            └── ClientTypeFlagsTest.php               [CREATE NEW]
```

**Structure Decision**: This is **Option 2 (Web service / API)** from the template — Laravel API inside the existing `src/` directory. The Customer domain lives in `src/app/Domains/Customer/` per the constitution's mandated pattern (Section 4.7), with cross-cutting helpers (`ApiResponseHelper`, exceptions) at the top level.

## Implementation Approach (Phased)

### Phase 0 — Cleanup & Foundations (must happen before any new code is written)

1. **Delete forbidden code** (no functional replacement; pure removal):
   - `src/app/Domains/Customer/Repositories/` (Repository pattern forbidden)
   - `src/app/Domains/Customer/DTO/` (DTOs removed by constitution Session 5)
   - `src/app/Domains/Customer/Pipelines/` (replaced by Model Scopes)
   - `src/app/Domains/Customer/Models/Query/` (Pipeline-style query builder)
   - `src/app/Domains/Customer/Models/Views/CustomerFinancialSummaryView.php` (renamed + moved)
   - `src/app/Domains/Customer/Services/ClientLookupService.php` (not needed)
   - `src/app/Domains/Customer/Http/Resources/CustomerResource.php` (superseded)
   - `src/app/Domains/Customer/Http/Requests/ExportCustomerRequest.php` (export feature removed in clarification)
   - `src/app/Domains/Customer/Http/Requests/CreateCustomerRequest.php` (renamed to `StoreCustomerRequest`)
   - `src/app/Shared/Repositories/` (forbidden)
   - `src/app/Exceptions/Custom/` (replaced by `App\Exceptions\Business\`)
   - `src/app/Exceptions/ApiResponseException.php`, `ApiRateLimitException.php`, `ApiConnectionException.php`, `ExceptionHandler.php` (replaced by Laravel 11 `withExceptions()` + `ExceptionMappings.php`)
   - `src/app/Helpers/CostumErrorResponse.php` (forbidden)
   - `src/config/ExceptionClassToMethod.php` (forbidden)
   - `src/app/Models/Client.php` (moved to Customer domain)

2. **Create cross-cutting exception infrastructure**:
   - `src/app/Exceptions/Business/ClientImmutableException.php` (Arabic message: "لا يمكن حذف العميل")
   - `src/app/Exceptions/ExceptionMappings.php` (single source of truth for exception → response mapping, per constitution Section 12)
   - Update `src/bootstrap/app.php` to call `withExceptions()` and iterate over `ExceptionMappings::map()`

3. **Rewrite `ApiResponseHelper`** to match the constitution (Section 4.1):
   - Top-level `success: true|false` (not `status`)
   - String `message` for errors (not array-wrapped)
   - Keep all the existing helper function names (`success`, `resourceCreatedResponse`, `error`, `unprocessableResponse`, `forbiddenResponse`, `modelNotFoundResponse`, `notFoundResponse`, `conflictResponse`, `badRequestResponse`, `unauthorizedResponse`, `generalFailureResponse`, `noContentResponse`, `successWithTokenUser`, `successWithToken`)

### Phase 1 — Customer Domain

4. **Move + rewrite `Client` model** (`src/app/Models/Client.php` → `src/app/Domains/Customer/Models/Client.php`):
   - Extend `Illuminate\Database\Eloquent\Model` (not `Authenticatable`)
   - Use `HasFactory` only
   - Add `client_type_flags` to `$fillable` (with `'array'` cast to `JSONB`)
   - Override `boot()`, `delete()`, `destroy()` — all three throw `ClientImmutableException`
   - Add `markAsCustomer()` helper that mutates `$this->client_type_flags` and calls `$this->save()` (not `saveQuietly()`)
   - Add semantic accessors: `isCustomer()`, `isAgent()`, `isInvestor()`
   - Add relationships: `contractsAsCustomer()`, `contractsAsAgent()`, `sharesLogs()`

5. **Create `CustomerListingMv` model** (`src/app/Domains/Customer/Models/CustomerListingMv.php`):
   - `$table = 'customer_listing_mv'`
   - `$primaryKey = 'client_id'`, `$incrementing = false`
   - `$timestamps = false`
   - Casts: `first_contract_date` → `date`, `next_due_date` → `date`, monetary fields → `decimal:2`, counts → `integer`
   - Scopes: `search(string $term)`, `firstContractFrom(date $from)`, `firstContractTo(date $to)`, `orderByNextDue()` (with `NULLS LAST`)

6. **Rewrite `CustomerService`** (no Repository injection):
   - `createCustomer(StoreCustomerRequest $request): array` — opens DB transaction; creates Client; uploads avatar to `public` disk under `avatars/`; calls `markAsCustomer()`; commits. Returns the Client model array.
   - `updateCustomer(int $id, UpdateCustomerRequest $request): array` — finds Client; updates only `name`, `phone`, `description`; if new avatar, deletes old file from storage then stores new one. **Never touches `client_type_flags`.**
   - `getCustomerList(ListCustomersRequest $request): CursorPaginator` — runs query on `CustomerListingMv` with the Scopes; cursor-paginates.
   - `getCustomerDetail(int $id): array` — fetches Client from `clients`; computes financial summary via live `installments` JOIN `contracts` (status IN active/completed). Throws `ModelNotFoundException` if not found.

7. **Rewrite `CustomerController`** (thin):
   - `store(StoreCustomerRequest)` → `resourceCreatedResponse(msg: '...', data: $service->createCustomer(...))`
   - `update(UpdateCustomerRequest, int $id)` → `success(data: $service->updateCustomer(...), msg: '...')`
   - `index(ListCustomersRequest)` → `success(data: $service->getCustomerList(...))` (cursor pagination meta in `meta`)
   - `show(int $id)` → `success(data: $service->getCustomerDetail(...), msg: '...')`
   - No `destroy()` method.

8. **Rewrite routes** (`src/app/Domains/Customer/Routes/v1/api.php`):
   ```php
   Route::middleware('auth:sanctum')->prefix('customers')->group(function () {
       Route::get('/',         [CustomerController::class, 'index'])->middleware('permissions:customers.view');
       Route::post('/',        [CustomerController::class, 'store'])->middleware('permissions:customers.create');
       Route::get('{id}',      [CustomerController::class, 'show'])->middleware('permissions:customers.view');
       Route::put('{id}',      [CustomerController::class, 'update'])->middleware('permissions:customers.update');
   });
   ```
   No `DELETE` route.

9. **Rewrite `CustomerServiceProvider`**:
   - In `boot()`: `Route::middleware('api')->prefix('api/v1')->group(__DIR__ . '/Routes/v1/api.php');`
   - No Repository binding (forbidden).
   - Register in `src/bootstrap/providers.php`.

10. **Update `src/routes/api.php`** to be empty except a comment pointer to domain providers (already standard; verify).

11. **Create `CustomerPermissionsSeeder`** (`src/database/seeders/CustomerPermissionsSeeder.php`):
    - Use Spatie's `Permission::firstOrCreate(['name' => $perm, 'guard_name' => 'admin'])` for `customers.view`, `customers.create`, `customers.update`.
    - Find the existing admin (by `email = 'admin@example.com'` or whichever env value; document the convention).
    - Use `$admin->givePermissionTo(...)` for all three.
    - Add a database seeder entry to `DatabaseSeeder.php`.

### Phase 2 — Tests

12. **Feature tests** under `src/tests/Feature/Customer/`:
    - `CreateCustomerTest`: 201 happy path, 422 on missing fields, 422 on duplicate phone, 422 on bad phone format, asserts `client_type_flags` contains `customer` post-creation, asserts response is `resourceCreatedResponse` shape.
    - `ListCustomersTest`: 200 with MV data, search filter, date range filter, cursor pagination, ordering by `next_due_date ASC NULLS LAST`, 403 without `customers.view`.
    - `ShowCustomerTest`: 200 with live-computed financial summary, 200 with zeroed summary when no contracts, 404 for missing id, 403 without `customers.view`.
    - `UpdateCustomerTest`: 200 with name/phone/description updates, avatar file replacement (old file deleted from storage), 200 when customer has active contracts (no immutability), 422 on bad phone format, 404 for missing id, `client_type_flags` unchanged after update, 403 without `customers.update`.

13. **Unit tests** under `src/tests/Unit/Domains/Customer/`:
    - `ClientImmutabilityTest`: calling `$client->delete()`, `Client::destroy(1)`, or a model event during delete throws `ClientImmutableException`.
    - `ClientTypeFlagsTest`: `markAsCustomer()` adds flag and uses `save()` (assert `saving` event fires).

14. **Permission/role tests** in `src/tests/Feature/Auth/`:
    - Login as Admin → 200 for all customer endpoints.
    - Login as a user without `customers.view` → 403 on list/show.
    - Login as a user with only `customers.view` → 403 on create/update.

### Phase 3 — Verification

15. Run the linter (`composer pint` or project-defined lint command) and the test suite (`php artisan test`). The constitution requires all tests pass before any commit.

## Complexity Tracking

> Fill ONLY if Constitution Check has violations that must be justified

| Violation | Why Needed | Simpler Alternative Rejected Because |
|---|---|---|
| None — all violations are eliminated by the plan | n/a | n/a |

The plan produces a **net reduction in complexity**: it removes the Repository pattern, the DTOs, the Pipelines, the custom ExceptionHandler, the `config/ExceptionClassToMethod.php`, the `CostumErrorResponse.php`, and the non-standard `ApiResponseHelper` response shape, and replaces them with the constitution-mandated patterns (Model Scopes, Eloquent + Resources, Laravel 11 `withExceptions()`, single `ExceptionMappings.php`, constitution-shaped helper).
