# Quickstart: Customer Management Re-execution (SP-04)

**Date**: 2026-06-07
**Spec**: [./spec.md](./spec.md)
**Plan**: [./plan.md](./plan.md)
**Data Model**: [./data-model.md](./data-model.md)
**Contracts**: [./contracts/](./contracts/)

A step-by-step guide for executing the SP-04 re-execution. Each step lists the files touched, the expected outcome, and how to verify it.

---

## Prerequisites

1. **Environment**:
   - PHP 8.3+
   - PostgreSQL 15+ accessible
   - Docker (recommended) or local PHP/Postgres install
   - Composer

2. **Verify the constitution is read**:
   - `.specify/memory/constitution.md` (Sections 2.I, 3.2, 4.1, 4.5–4.7, 5, 6.4, 12, 14)
   - `doc/DB_Schema.md` (Sections 1, 3, 9.1)
   - `doc/API_Manifest.md` (Section 2: Customer domain)

3. **Verify the existing state**:
   - `git status` should be clean
   - `git branch --show-current` should be `005-customer-management-reexec`

4. **Confirm the Admin user exists**:
   - `php artisan tinker` → `App\Domains\Auth\Models\Admin::first()` should return one record.
   - If empty, run the Auth domain's admin seeder first (out of SP-04 scope; belongs to SP-01).

---

## Phase 0 — Cleanup (delete the forbidden code)

### Step 0.1 — Delete the forbidden Customer domain subdirectories

```bash
cd src
rm -rf app/Domains/Customer/Repositories
rm -rf app/Domains/Customer/DTO
rm -rf app/Domains/Customer/Pipelines
rm -rf app/Domains/Customer/Models/Query
rm -rf app/Domains/Customer/Models/Views
rm app/Domains/Customer/Models/Query/CustomerQueryPipeline.php 2>/dev/null || true
rm app/Domains/Customer/Services/ClientLookupService.php
rm app/Domains/Customer/Http/Resources/CustomerResource.php
rm app/Domains/Customer/Http/Requests/CreateCustomerRequest.php
rm app/Domains/Customer/Http/Requests/ExportCustomerRequest.php
```

**Verify**:
```bash
ls app/Domains/Customer/
# Expected: CustomerServiceProvider.php, Http/, Models/, Routes/, Services/
# Not expected: DTO/, Pipelines/, Repositories/, Models/Query/, Models/Views/, Services/ClientLookupService.php
```

### Step 0.2 — Delete the cross-cutting forbidden code

```bash
cd src
rm -rf app/Exceptions/Custom
rm app/Exceptions/ApiResponseException.php
rm app/Exceptions/ApiRateLimitException.php
rm app/Exceptions/ApiConnectionException.php
rm app/Exceptions/ExceptionHandler.php
rm app/Helpers/CostumErrorResponse.php
rm app/Models/Client.php  # will be re-created inside the Customer domain
rm -rf app/Shared/Repositories
rm config/ExceptionClassToMethod.php
```

**Verify**:
```bash
ls app/Exceptions/
# Expected: empty (or only ExceptionMappings.php if Step 0.3 already created it)
ls app/Helpers/
# Expected: ApiResponseHelper.php
ls app/Models/
# Expected: empty
ls app/Shared/
# Expected: empty
ls config/ExceptionClassToMethod.php 2>/dev/null
# Expected: "No such file"
```

### Step 0.3 — Create the new exception infrastructure

Create `src/app/Exceptions/Business/ClientImmutableException.php`:

```php
<?php

namespace App\Exceptions\Business;

use RuntimeException;

class ClientImmutableException extends RuntimeException
{
    public function __construct(string $message = 'لا يمكن حذف العميل')
    {
        parent::__construct($message, 403);
    }
}
```

Create `src/app/Exceptions/ExceptionMappings.php` per constitution Section 12. Copy the structure shown in the constitution, including the new `ClientImmutableException` mapping.

### Step 0.4 — Wire `ExceptionMappings` into `bootstrap/app.php`

Open `src/bootstrap/app.php` and add (or update) the `->withExceptions(...)` call to iterate over `ExceptionMappings::map()`:

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

### Step 0.5 — Rewrite `ApiResponseHelper`

Edit `src/app/Helpers/ApiResponseHelper.php`:
- Replace the `status` key in the response array with `success`.
- In `error()`, pass the string message directly (not array-wrapped).
- Keep all helper function names and signatures the same so callers don't break.

After the rewrite, smoke test:
```bash
php -r "require 'vendor/autoload.php'; require 'app/Helpers/ApiResponseHelper.php'; echo json_encode(json_decode((string) success(['x' => 1], 'ok')->getContent()));"
# Expected: {"success":true,"message":"ok","data":{"x":1}}
```

---

## Phase 1 — Customer Domain Build

### Step 1.1 — Create the `Client` model

Create `src/app/Domains/Customer/Models/Client.php` extending `Illuminate\Database\Eloquent\Model` (NOT `Authenticatable`).

Required:
- `$fillable = ['name', 'phone', 'description', 'avatar', 'client_type_flags']`
- `'client_type_flags' => 'array'` cast
- Override `boot()` → register `deleting` listener that throws `ClientImmutableException`
- Override `delete()` → throw `ClientImmutableException`
- Override `destroy($ids)` → throw `ClientImmutableException`
- Methods: `isCustomer()`, `isAgent()`, `isInvestor()`, `markAsCustomer()`, `markAsAgent()`, `markAsInvestor()`
- Relationships: `contractsAsCustomer()`, `contractsAsAgent()`, `sharesLogs()`

**Verify**:
```bash
php -r "
require 'vendor/autoload.php';
\$app = require 'bootstrap/app.php';
\$app->boot();
\$c = new App\Domains\Customer\Models\Client();
try { \$c->delete(); echo 'BUG: delete did not throw'; }
catch (App\Exceptions\Business\ClientImmutableException \$e) { echo 'OK: delete throws'; }
"
```

### Step 1.2 — Create the `CustomerListingMv` model

Create `src/app/Domains/Customer/Models/CustomerListingMv.php` with `$table = 'customer_listing_mv'`, `$primaryKey = 'client_id'`, `$incrementing = false`, `$timestamps = false`, and the four Scopes (`search`, `firstContractFrom`, `firstContractTo`, `orderByNextDue`).

**Verify**:
```bash
php artisan tinker --execute="echo get_class(App\Domains\Customer\Models\CustomerListingMv::query()->orderByNextDue()->first());"
# Expected: "App\Domains\Customer\Models\CustomerListingMv"
```

### Step 1.3 — Create the FormRequests

Create:
- `src/app/Domains/Customer/Http/Requests/StoreCustomerRequest.php`
- `src/app/Domains/Customer/Http/Requests/UpdateCustomerRequest.php`
- `src/app/Domains/Customer/Http/Requests/ListCustomersRequest.php`

With rules per `data-model.md`. Use Arabic error messages (the default Laravel messages stay English; override the `messages()` method on each FormRequest to return Arabic).

### Step 1.4 — Create the Resources

Create:
- `src/app/Domains/Customer/Http/Resources/CustomerListResource.php` — fields: `id`, `name`, `phone`, `avatar`, `avatar_url`, `first_contract_date`, `next_due_date`, `total_remaining_amount`
- `src/app/Domains/Customer/Http/Resources/CustomerDetailResource.php` — fields: `id`, `name`, `phone`, `description`, `avatar`, `avatar_url`, `client_type_flags`, `created_at`, `updated_at`, `financial_summary` (object)

### Step 1.5 — Create the `CustomerService`

Create `src/app/Domains/Customer/Services/CustomerService.php` with the four public methods (`createCustomer`, `updateCustomer`, `getCustomerList`, `getCustomerDetail`). No Repository injection. Uses `Storage` facade and `DB` facade directly.

### Step 1.6 — Create the `CustomerController`

Create `src/app/Domains/Customer/Http/Controllers/CustomerController.php` with the four methods (`store`, `update`, `index`, `show`). All return values via `ApiResponseHelper`. No `destroy` method.

### Step 1.7 — Rewrite the routes

Replace `src/app/Domains/Customer/Routes/v1/api.php` with the canonical route definitions (see `plan.md` Step 8).

### Step 1.8 — Rewrite the Service Provider

Update `src/app/Domains/Customer/CustomerServiceProvider.php` to load routes in `boot()`. Register the provider in `src/bootstrap/providers.php`.

### Step 1.9 — Create the permission seeder

Create `src/database/seeders/CustomerPermissionsSeeder.php`. Register in `src/database/seeders/DatabaseSeeder.php` (`$this->call(CustomerPermissionsSeeder::class);`).

Run the seeder:
```bash
php artisan db:seed --class=CustomerPermissionsSeeder
```

**Verify**:
```bash
php artisan tinker --execute="dump(\Spatie\Permission\Models\Permission::pluck('name')->toArray());"
# Expected: contains 'customers.view', 'customers.create', 'customers.update'
php artisan tinker --execute="dump(App\Domains\Auth\Models\Admin::first()->getAllPermissions()->pluck('name'));"
# Expected: contains all three customer permissions
```

### Step 1.10 — Ensure `routes/api.php` is empty

Open `src/routes/api.php`. It should contain only a comment pointing to domain Service Providers. No actual route definitions.

---

## Phase 2 — Tests

### Step 2.1 — Feature tests

Create:
- `src/tests/Feature/Customer/CreateCustomerTest.php`
- `src/tests/Feature/Customer/ListCustomersTest.php`
- `src/tests/Feature/Customer/ShowCustomerTest.php`
- `src/tests/Feature/Customer/UpdateCustomerTest.php`

In each test that needs MV data, call `DB::statement('REFRESH MATERIALIZED VIEW customer_listing_mv')` after seeding.

### Step 2.2 — Unit tests

Create:
- `src/tests/Unit/Domains/Customer/ClientImmutabilityTest.php`
- `src/tests/Unit/Domains/Customer/ClientTypeFlagsTest.php`

### Step 2.3 — Run the suite

```bash
cd src
php artisan test
```

**Verify**: all tests pass. Target coverage ≥ 80% on the Customer domain files (per constitution Section 2.V).

---

## Phase 3 — End-to-End Verification

### Step 3.1 — Manual API smoke test

Start the dev server:
```bash
cd src
php artisan serve
```

Get a token:
```bash
curl -X POST http://localhost:8000/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"password"}'
# Save the token from the response
```

Create a customer:
```bash
curl -X POST http://localhost:8000/api/v1/customers \
  -H "Authorization: Bearer {token}" \
  -F "name=أحمد محمد" \
  -F "phone=0912345678" \
  -F "description=عميل" \
  -F "avatar=@/path/to/avatar.jpg"
# Expected: 201 with success:true and client_type_flags:["customer"]
```

List customers:
```bash
curl -X GET "http://localhost:8000/api/v1/customers?per_page=5" \
  -H "Authorization: Bearer {token}"
# Expected: 200, paginated list
```

Get details:
```bash
curl -X GET http://localhost:8000/api/v1/customers/1 \
  -H "Authorization: Bearer {token}"
# Expected: 200, with financial_summary object
```

Update:
```bash
curl -X PUT http://localhost:8000/api/v1/customers/1 \
  -H "Authorization: Bearer {token}" \
  -F "phone=0998765432"
# Expected: 200, with updated phone
```

Attempt delete (should not exist):
```bash
curl -X DELETE http://localhost:8000/api/v1/customers/1 \
  -H "Authorization: Bearer {token}" -i
# Expected: 404 or 405 (route does not exist)
```

### Step 3.2 — Permission enforcement test

Create a second admin without customer permissions, log in as them, attempt list:
```bash
# Expected: 403 Forbidden
```

### Step 3.3 — Immutability test (unit)

```bash
php artisan tinker --execute="
\$c = App\Domains\Customer\Models\Client::first();
try { \$c->delete(); echo 'BUG'; }
catch (App\Exceptions\Business\ClientImmutableException \$e) { echo 'OK: ' . \$e->getMessage(); }
"
# Expected: "OK: لا يمكن حذف العميل"
```

---

## Phase 4 — Pre-Commit Checks

1. **Lint**: `composer pint` (or the project's linter).
2. **Static analysis**: `composer phpstan` if configured.
3. **Tests**: `php artisan test` — all green.
4. **No new violations of the constitution**:
   - No `saveQuietly()` on `client_type_flags` paths: `grep -r "saveQuietly" src/app/Domains/Customer` → empty
   - No Repository pattern: `find src/app/Domains/Customer -type d -name Repositories` → empty
   - No DTOs: `find src/app/Domains/Customer -type d -name DTO` → empty
   - No raw SQL in controllers: `grep -rn "DB::raw\|DB::select" src/app/Domains/Customer/Http/Controllers` → empty
   - No `response()->json()` outside ApiResponseHelper: `grep -rn "response()->json" src/app/Domains/Customer` → empty
   - `routes/api.php` empty: `cat src/routes/api.php` → only the guidance comment
5. **Constitution Check** in `plan.md` → all rows ✅.

---

## Rollback Strategy

If a regression is caught post-merge:

1. **Database is unchanged** — the migration count is identical to before this work (no new tables, no new columns).
2. **Git revert** the merge commit.
3. **Re-run the Auth domain seeder** if any changes touched Admin user data (they shouldn't have).
4. **Re-refresh the materialized view** if the index on `client_id` was somehow affected: `REFRESH MATERIALIZED VIEW CONCURRENTLY customer_listing_mv;`.

The plan is intentionally additive: every deleted file corresponds to a specific constitutional violation, and every created file corresponds to a specific FR in the spec. Reverting the diff should restore the previous (faulty) state, which is acceptable as a stopgap while the next iteration is planned.
