# API Contract: Update Customer

**Endpoint**: `PUT /api/v1/customers/{id}`
**Permission**: `customers.update` (Spatie, `admin` guard)
**Auth**: `Authorization: Bearer {token}` (Sanctum)

## Request

### Headers

| Header | Required | Value |
|---|---|---|
| `Authorization` | Yes | `Bearer {sanctum_token}` |
| `Accept` | Yes | `application/json` |
| `Content-Type` | Yes (for file upload) | `multipart/form-data` |

### Path Parameters

| Param | Type | Description |
|---|---|---|
| `id` | integer | The `client_id` in the `clients` table. |

### Body (multipart/form-data)

All fields are **optional** in PUT (use `sometimes` validation), but at least one must be present (enforced in the FormRequest's `withValidator` rule).

| Field | Type | Required | Constraints |
|---|---|---|---|
| `name` | string | No | 1–150 characters |
| `phone` | string | No | Syrian local format: `^0?9\d{8}$`; unique across `clients`, ignoring the current record's id |
| `description` | string | No | Up to 1000 characters; explicitly nullable (can be cleared) |
| `avatar` | file (image) | No | One of `jpg`, `jpeg`, `png`, `webp`; ≤ 5 MB; nullable (can be cleared by passing empty) |

**Explicitly forbidden in the request body (and silently ignored if sent)**:
- `client_type_flags` — never modifiable through this endpoint
- `id`, `created_at`, `updated_at` — never modifiable

### Example Request

```http
PUT /api/v1/customers/42 HTTP/1.1
Authorization: Bearer {token}
Content-Type: multipart/form-data; boundary=----FormBoundary

------FormBoundary
Content-Disposition: form-data; name="phone"

0998765432
------FormBoundary
Content-Disposition: form-data; name="description"

تم تحديث البيانات
------FormBoundary--
```

## Success Response

### 200 OK

```json
{
  "success": true,
  "message": "تم تحديث بيانات العميل بنجاح.",
  "data": {
    "id": 42,
    "name": "أحمد محمد",
    "phone": "0998765432",
    "description": "تم تحديث البيانات",
    "avatar": "avatars/abc123.jpg",
    "avatar_url": "http://host/storage/avatars/abc123.jpg",
    "client_type_flags": ["customer"],
    "created_at": "2026-06-07T10:30:00+03:00",
    "updated_at": "2026-06-07T11:45:00+03:00"
  }
}
```

## Error Responses

### 401 Unauthorized

```json
{ "success": false, "message": "Unauthenticated." }
```

### 403 Forbidden (missing `customers.update` permission)

```json
{ "success": false, "message": "You do not have permission to perform this action." }
```

### 404 Not Found (id doesn't exist)

```json
{ "success": false, "message": "The requested Client was not found." }
```

### 422 Unprocessable Entity (validation error)

```json
{
  "success": false,
  "message": {
    "phone": ["رقم الهاتف مستخدم مسبقاً."]
  }
}
```

### 422 Unprocessable Entity (no fields provided)

```json
{
  "success": false,
  "message": "يجب تقديم حقل واحد على الأقل للتحديث."
}
```

## Server-Side Behavior

1. Validate input via `UpdateCustomerRequest` (rules in `data-model.md`).
2. `Client::findOrFail($id)` — throws `ModelNotFoundException` if not found; mapped to 404.
3. Open a DB transaction.
4. `$client->fill($request->only(['name', 'phone', 'description']))` — `client_type_flags` is NOT in the fill list, so it cannot be modified even by accident.
5. `$client->save()`.
6. If avatar provided:
   - Compute old path: `$oldAvatar = $client->avatar;` (BEFORE the new save)
   - `$newPath = Storage::disk('public')->putFile('avatars', $request->file('avatar'));`
   - `$client->avatar = $newPath;`
   - `$client->save();`
   - If `$oldAvatar` was non-null: `Storage::disk('public')->delete($oldAvatar);` (best-effort cleanup)
7. Commit transaction.
8. Return `success(data: $clientResource->toArray($client->fresh()), msg: 'تم تحديث بيانات العميل بنجاح.')`.

## Important Business Rules

| Rule | Notes |
|---|---|
| Updates are allowed even if the customer has active or completed contracts. | Customer is not subject to the contract immutability rule. |
| `client_type_flags` is NEVER modified. | Verified by `fill()` taking only `[name, phone, description]`. |
| Avatar replacement deletes the old file. | `Storage::disk('public')->delete($oldAvatar)` after the new file is stored. |
| Avatar can be cleared. | Sending `avatar` as an empty file or null removes the existing one. (Implementation detail: `sometimes|nullable` rule.) |
| At least one field must be present. | Enforced via `withValidator()` in the FormRequest. |

## Test Cases (Feature Tests)

| # | Scenario | Expected |
|---|---|---|
| 1 | PUT with only `name` | 200, name updated, other fields unchanged |
| 2 | PUT with only `phone` (valid + unique) | 200, phone updated |
| 3 | PUT with only `description` | 200, description updated |
| 4 | PUT with `description=""` (empty) | 200, description becomes null/empty |
| 5 | PUT with new avatar | 200, avatar column updated, new file on disk, old file removed |
| 6 | PUT with no avatar change | 200, avatar column unchanged, no file operations |
| 7 | PUT with no fields at all | 422, "at least one field required" |
| 8 | PUT with `phone = "12345"` (bad format) | 422, phone field error |
| 9 | PUT with `phone = another client's phone` | 422, phone field error "مستخدم مسبقاً" |
| 10 | PUT with `avatar > 5 MB` | 422, avatar field error |
| 11 | PUT with `avatar = .gif` | 422, avatar field error |
| 12 | PUT to a customer with 1+ active contracts | 200 (allowed by business rule) |
| 13 | PUT to a customer with 1+ completed contracts | 200 (allowed by business rule) |
| 14 | PUT non-existent id | 404 |
| 15 | PUT as a user without `customers.update` | 403 |
| 16 | PUT without `Authorization` | 401 |
| 17 | PUT and verify `client_type_flags` is unchanged | After update, `client_type_flags == ["customer"]` (or whatever it was before) |
| 18 | PUT with attempt to send `client_type_flags=["admin"]` in the body | 200, `client_type_flags` is unchanged (field is ignored) |
| 19 | PUT and verify response time < 1 s | Manual or `assertLessThan(1000, $elapsedMs)` |
