---
description: "Task list for SP-07 — Contract Management"
---

# Tasks: SP-07 — Contract Management

**Input**: Design documents from `/specs/008-contract-management/`
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
**Tests**: Test-First is required by Constitution §III (NON-NEGOTIABLE) for the calculator/algorithm (`DueDateGenerator`) and the financial-engine endpoints. Test tasks are included.

**Organization**: Tasks are grouped by user story (8 user stories — 7× P1 + 1× P2) to enable independent implementation and testing of each story.

**Source Paths**: Production code lives under `src/app/Domains/Contract/`; tests under `src/tests/Unit/Domains/Contract/` and `src/tests/Feature/Contract/`. All paths in this document are relative to the repository root and include the `src/` prefix.

## Format: `[ID] [P?] [Story] Description`

- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (US1–US8)
- Include exact file paths in descriptions

---

## Phase 1: Setup (Shared Infrastructure)

**Purpose**: Project initialization, prerequisite verification, translation files. No domain code in this phase.

- [x] T001 [P] Verify SP-06 baseline: `src/app/Domains/Contract/Services/ContractMathValidator.php` exists and exposes a public `validate(...)` method that throws `PaymentMismatchException` or `InvalidInitialPaymentException` on math failure
- [x] T002 [P] Verify SP-03 baseline: `src/app/Domains/Customer/Models/Client.php` exposes `markAsCustomer()` and `markAsAgent()` instance methods. **Hard fail** if either method body contains `saveQuietly(` (Constitution §V NEVER rule; ADR-041). Verification steps: (a) read the method bodies, (b) run `grep -nE 'function\s+markAs(Customer|Agent|Investor)\s*\(' src/app/Domains/Customer/Models/Client.php` to enumerate the methods, (c) inside each method body, run `grep -n 'saveQuietly' <method body>` — if any match, this task fails and SP-03 must be fixed before SP-07 can proceed. Flag-add methods MUST use `save()` so Eloquent `saving`/`saved` events fire (model observers and audit hooks depend on this).
- [x] T003 [P] Verify SP-03 baseline: `App\Shared\Traits\HasReferenceNumber` (or equivalent) exists and is used by the `Contract` model to auto-generate `CTR-XXXXXXXXXX`
- [x] T004 [P] Verify `RefreshCustomerListingJob` (or equivalent queueable job) exists at `src/app/Domains/Customer/Jobs/RefreshCustomerListingJob.php`; if missing, create a minimal stub that runs `DB::statement('REFRESH MATERIALIZED VIEW CONCURRENTLY customer_listing_mv')`
- [x] T005 [P] Verify `ContractFactory` and `InstallmentFactory` exist under `src/database/factories/` with `draft()` state (per `07_04_TESTING_STANDARDS.md`)
- [x] T006 [P] Verify `ClientFactory` exposes `asCustomer()` and `asAgent()` states (per `04_05_FACTORIES.md`)
- [x] T007 [P] Verify Spatie permissions exist for `contracts.create`, `contracts.view`, `contracts.sign`, `contracts.delete` (run `php artisan tinker` query or inspect `src/database/seeders/RolePermissionSeeder.php`); add any missing ones to the seeder
- [x] T008 [P] Create `src/lang/ar/errors/contract.php` with keys: `not_found`, `cannot_sign`, `cannot_delete`, `immutable` (merge into existing file if SP-06 already created it)
- [x] T009 [P] Create `src/lang/ar/success/contract.php` with keys: `created`, `signed`, `deleted`
- [x] T010 [P] Create `src/lang/en/errors/contract.php` with English mirrors of T008 keys
- [x] T011 [P] Create `src/lang/en/success/contract.php` with English mirrors of T009 keys
- [x] T012 Create the directory skeleton under `src/app/Domains/Contract/` with subdirs: `Services/`, `Http/Controllers/`, `Http/Requests/`, `Http/Resources/`, `Providers/`, `Routes/v1/`

---

## Phase 2: Foundational (Blocking Prerequisites)

**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented. These tasks are framework- and routing-level concerns shared by all 4 endpoints.

**⚠️ CRITICAL**: No user story work can begin until this phase is complete

- [x] T013 Register 4 named rate limiters in `src/app/Providers/AppServiceProvider.php` (or wherever `RateLimiter::for(...)` calls live in this project): `contracts.create` (60/min), `contracts.view` (120/min), `contracts.sign` (60/min), `contracts.delete` (60/min) — all keyed by `$req->user()->id`
- [x] T014 Create `src/app/Domains/Contract/Providers/ContractServiceProvider.php` that extends `Illuminate\Support\ServiceProvider` and loads `src/app/Domains/Contract/Routes/v1/api.php` from its `boot()` method
- [x] T015 Register `App\Domains\Contract\Providers\ContractServiceProvider::class` in `src/bootstrap/providers.php` (append to the returned array)
- [x] T016 Create `src/app/Domains/Contract/Routes/v1/api.php` with 4 routes under `prefix('v1')` and `middleware(['auth:admin'])`: `POST contracts` (throttle:contracts.create, can:contracts.create), `GET contracts/{id}` (throttle:contracts.view, can:contracts.view), `PATCH contracts/{id}/sign` (throttle:contracts.sign, can:contracts.sign), `DELETE contracts/{id}` (throttle:contracts.delete, can:contracts.delete) — all bound to `ContractController` methods `store`, `show`, `sign`, `destroy`
- [x] T017 [P] Create `src/app/Domains/Contract/Http/Controllers/ContractController.php` skeleton with constructor injecting `ContractService` and 4 method stubs (`store`, `show`, `sign`, `destroy`) that each return `success(data: null)` temporarily

**Checkpoint**: Foundation ready — user story implementation can now begin in parallel

---

## Phase 3: User Story 1 — Happy Path Contract Creation (Priority: P1) 🎯 MVP

**Goal**: Authenticated admin with `contracts.create` permission can POST a fully valid contract payload (customer + agent + non-zero `initial_payment`) and receive 200 with the full contract body, with 1 contract row, N installments, 1 `company_payout`, 1 `initial_payment`, 2 audit logs, customer/agent flags set, and `RefreshCustomerListingJob` dispatched — all in a single atomic transaction.

**Independent Test**: A feature test that POSTs a valid contract payload with an agent and a non-zero `initial_payment`, then asserts: 1 contract row, N installment rows where N = `months`, 1 `company_payout` row, 1 `initial_payment` row, 2 audit log rows, customer has `customer` flag, agent has `agent` flag, and `RefreshCustomerListingJob` was dispatched exactly once.

### Tests for User Story 1 (Test-First per Constitution §III)

> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**

- [x] T018 [P] [US1] Create `src/tests/Unit/Domains/Contract/Services/DueDateGeneratorTest.php` with the full 33-cell exhaustive matrix from `data-model.md` Section 3.4: leap-year day-31 starts, leap-year day-30 starts, leap-year day-29 starts, non-leap year day-31 starts, non-leap year January 30/29/28 with Feb overflow, no-overflow baselines, and `months=1` and `months=60` boundary cases — using `assertSame` for exact string equality on `Y-m-d` formatted dates
- [x] T019 [P] [US1] Add invalid-input tests to `src/tests/Unit/Domains/Contract/Services/DueDateGeneratorTest.php`: invalid date format throws `InvalidArgumentException`, `months=0` throws, `months=61` throws
- [x] T020 [P] [US1] Create `src/tests/Feature/Contract/CreateContractTest.php` with the happy-path test (AC-001 to AC-003): POSTs valid payload with agent and `initial_payment > 0`, asserts 200, response body shape, DB row counts in `contracts` (1), `installments` (N=`months`), `payments` with `type='company_payout'` (1), `payments` with `type='initial_payment'` (1), `payment_audit_logs` (2)

### Implementation for User Story 1

- [x] T021 [US1] Implement `src/app/Domains/Contract/Services/DueDateGenerator.php` with `generate(string $startDate, int $months): array` — pure function, uses `\DateTimeImmutable::createFromFormat('Y-m-d', ...)` then `modify('+1 month')` in a loop, returns array of `Y-m-d` strings; throws `InvalidArgumentException` on bad input (depends on T018, T019 tests passing)
- [x] T022 [P] [US1] Create `src/app/Domains/Contract/Http/Resources/InstallmentResource.php` with fields: `id`, `installment_number`, `due_date` (`Y-m-d` format), `amount`, `status`, `paid_amount`, `paid_at` (ISO-8601 or null)
- [x] T023 [P] [US1] Create `src/app/Domains/Contract/Http/Resources/ContractResource.php` with all fields from FR-020, embedding `InstallmentResource::collection(...)` for installments, deriving `narrative_timeline` from `auditLogs` sorted by `created_at` ASC and plucking `narrative_text`, and projecting `customer` to `{id, name, phone}` and `agent` to `{id, name}` or `null` (NEVER exposing `client_type_flags`)
- [x] T024 [US1] Create `src/app/Domains/Contract/Http/Requests/StoreContractRequest.php` with `authorize()` returning `$this->user()->can('contracts.create')` and `rules()` covering all 11 fields per `research.md` Decision 10 (including `'agent_id' => ['nullable', 'integer', 'exists:clients,id', 'different:customer_id']`, `'months' => ['required', 'integer', 'min:1', 'max:60']`) plus `messages()` resolving all keys via `__()` from `lang/ar/validation.php` and `lang/ar/errors/contract.php`
- [x] T025 [US1] Create `src/app/Domains/Contract/Services/ContractService.php` with constructor injecting `ContractMathValidator` (SP-06) and `DueDateGenerator`, and 4 method stubs (`create`, `view`, `sign`, `delete`) — all returning type-hinted `Contract` or `void` (depends on T021)
- [x] T026 [US1] Implement `ContractService::create(array $data): Contract` — wraps all sub-operations in `DB::transaction(function () { ... })` per the sequence in `data-model.md` Section 4: (1) `ContractMathValidator::validate(...)` (throws on failure), (2) `Contract::create([...$data, 'status' => 'draft'])` (HasReferenceNumber auto-generates reference), (3) bulk insert `installments` using `DueDateGenerator::generate($data['start_date'], $data['months'])` with `installment_number` 1..N, `amount = monthly_installment_amount`, `status='pending'`, `paid_amount=0`, `paid_at=null`, (4) create `Payment` with `type='company_payout'`, `amount=purchase_amount`, `installment_id=NULL`, `payment_date=start_date` + matching `PaymentAuditLog` with `narrative_text="تم دفع مبلغ من الشركة بقيمة {bcadd(amount, '0', 2)}"`, (5) **if `bccomp((string)$data['initial_payment'], '0', 2) > 0`** create `Payment` with `type='initial_payment'` + matching audit log, (6) `$contract->customer->markAsCustomer()` (idempotent), (7) **if `$contract->agent_id !== null`** `$contract->agent->markAsAgent()`. AFTER the transaction returns: `RefreshCustomerListingJob::dispatch()`. Return the contract eager-loaded with `customer`, `agent`, `installments`, `auditLogs` (depends on T022, T023, T024, T025)
- [x] T027 [US1] Implement `ContractController::store(StoreContractRequest $request): JsonResponse` — calls `$this->contractService->create($request->validated())` and returns `success(data: ContractResource::make($contract), msg: __('success/contract.created'))` using named parameters (depends on T026)
- [x] T028 [US1] Run `DueDateGeneratorTest` and `CreateContractTest`; fix any failures until all pass; add a queue-dispatch assertion test to `CreateContractTest` covering AC-013: `Queue::fake()` then `Queue::assertPushed(RefreshCustomerListingJob::class, 1)` after a successful POST

**Checkpoint**: At this point, User Story 1 (happy path creation) is fully functional and testable independently. The 12-month contract, 12 installments, 2 payments, 2 audit logs, and the queue dispatch are all verified.

---

## Phase 4: User Story 2 — Zero Down-Payment Contract (Priority: P1)

**Goal**: Admin can create a contract with `initial_payment = 0`. The system still creates the `company_payout` row + audit log, but does NOT create an `initial_payment` row or an `initial_payment` audit log. The narrative timeline has exactly one entry (the `company_payout` one).

**Independent Test**: POST a valid contract with `initial_payment = 0`, assert: response is 200, exactly 1 `company_payout` row exists, 0 `initial_payment` rows exist, 0 `initial_payment` audit log rows exist, and the `narrative_timeline` contains only the `company_payout` entry.

### Tests for User Story 2

> **NOTE: Write these tests FIRST, ensure they FAIL before implementation (or before they pass)**

- [x] T029 [P] [US2] Add zero-down-payment test to `src/tests/Feature/Contract/CreateContractTest.php` covering AC-004: POST with `initial_payment=0`, assert 200, exactly 1 `company_payout` row, 0 `initial_payment` rows, **`assertDatabaseCount('payment_audit_logs', 1)` — confirming only the `company_payout` audit log exists and no `initial_payment` audit log was created when `initial_payment = 0`**, and `narrative_timeline` has 1 entry starting with "تم دفع مبلغ من الشركة"

### Implementation for User Story 2

- [x] T030 [US2] Verify (no code change expected) that the `if (bccomp((string)$data['initial_payment'], '0', 2) > 0)` guard in `ContractService::create` correctly skips both the `initial_payment` Payment row and the corresponding `PaymentAuditLog` row when `initial_payment=0`; if a logic bug is found, fix the guard in `src/app/Domains/Contract/Services/ContractService.php` (depends on T029)

**Checkpoint**: At this point, User Story 2 (zero-down-payment) is testable independently. AC-004 is verified.

---

## Phase 5: User Story 3 — Contract Without Agent (Priority: P1)

**Goal**: Admin can create a contract with no `agent_id` (omitted from payload). The `agent` column on the contract row is `NULL`, and no client's `agent` flag is added. The customer's `customer` flag may be added (idempotent) but no agent flag update occurs.

**Independent Test**: POST a valid contract without `agent_id`, assert: response is 200, `data.agent` is `null`, `contracts.agent_id` is `NULL`, and no client had the `agent` flag added (only the customer's `customer` flag was added if not already present).

### Tests for User Story 3

- [x] T031 [P] [US3] Add no-agent test to `src/tests/Feature/Contract/CreateContractTest.php` covering AC-005: POST with `agent_id` omitted, assert 200, `data.agent` is `null`, `contracts.agent_id` is `NULL` in DB
- [x] T032 [P] [US3] Add self-referral rejection test to `src/tests/Feature/Contract/CreateContractTest.php`: POST with `agent_id == customer_id`, assert 422 with `validation.different` error key and zero new rows in any table

### Implementation for User Story 3

- [x] T033 [US3] Verify the `'different:customer_id'` rule in `StoreContractRequest::rules()` is present; verify the `if ($contract->agent_id !== null)` guard in `ContractService::create` skips `markAsAgent()` when `agent_id` is null; fix either if missing (depends on T031, T032)

**Checkpoint**: At this point, User Story 3 (no-agent + self-referral rejection) is testable independently. AC-005, AC-037 are verified.

---

## Phase 6: User Story 4 — Sign Contract (Priority: P1)

**Goal**: Admin PATCHes `/contracts/{id}/sign` on a draft contract, transitioning it to `status='active'` and setting `signed_at` to `now()`. Subsequent sign attempts on non-draft contracts return 403 with the Arabic `cannot_sign` message. The contract becomes immutable.

**Independent Test**: Create a draft, PATCH `/sign`, assert `status='active'`, `signed_at` is not null. Then attempt to PATCH `/sign` again, assert 403 with the Arabic message.

### Tests for User Story 4

- [x] T034 [P] [US4] Create `src/tests/Feature/Contract/SignContractTest.php` with the happy-path test (AC-015): seed a draft contract, PATCH `/sign`, assert 200, `data.status='active'`, `data.signed_at` is a non-null ISO-8601 timestamp within 1 second of `now()`
- [x] T035 [P] [US4] Add the **non-draft transitions rejected** test to `SignContractTest` (covers AC-016 and AC-017 cross-link): seed an `active` contract (via sign or factory), then (a) PATCH `/sign` and assert 403 with `errors/contract.cannot_sign` message, AND (b) DELETE `/contracts/{id}` and assert 403 with `errors/contract.cannot_delete` message. Both assertions live in the same test method (`test_non_draft_transitions_are_rejected`) and share the same seeded active contract.
- [x] T036 [P] [US4] Add the not-found test to `SignContractTest`: PATCH `/sign` on a non-existent id, assert 404

### Implementation for User Story 4

- [x] T037 [US4] Create `src/app/Domains/Contract/Http/Requests/SignContractRequest.php` with `authorize()` returning `$this->user()->can('contracts.sign')` and no body rules
- [x] T038 [US4] Implement `ContractService::sign(int $id): Contract` in `src/app/Domains/Contract/Services/ContractService.php` — wraps in `DB::transaction(function () use ($id) { ... })`: (1) `Contract::lockForUpdate()->findOrFail($id)`, (2) guard `if ($contract->status !== 'draft') throw new ContractImmutableException(__('errors/contract.cannot_sign'))`, (3) `$contract->update(['status' => 'active', 'signed_at' => now()])`, (4) return `$contract->fresh(['customer', 'agent', 'installments', 'auditLogs'])` (depends on T034, T035, T036)
- [x] T039 [US4] Implement `ContractController::sign(SignContractRequest $request, int $id): JsonResponse` — calls `$this->contractService->sign($id)` and returns `success(data: ContractResource::make($contract), msg: __('success/contract.signed'))` (depends on T038)
- [x] T040 [US4] Run `SignContractTest`; fix any failures until all pass (depends on T035)

**Checkpoint**: At this point, User Story 4 (signing + immutability guard) is testable independently. AC-015, AC-016, AC-017, AC-039 are verified.

---

## Phase 7: User Story 5 — Delete Draft Contract (Priority: P1)

**Goal**: Admin DELETEs `/contracts/{id}` on a draft contract with all dependents (installments, payments, audit logs); the system hard-deletes the contract and CASCADE deletes all children in a single transaction. DELETE on non-draft returns 403 with the Arabic `cannot_delete` message.

**Independent Test**: Create a draft with 12 installments, 1 `company_payout`, 1 `initial_payment`, and 2 audit log rows; DELETE it; assert 200 success message; query all 4 tables and assert 0 rows reference the deleted contract id.

### Tests for User Story 5

- [x] T042 [P] [US5] Create `src/tests/Feature/Contract/DeleteDraftContractTest.php` with the CASCADE happy-path test (AC-018): seed a draft contract with 12 installments + 1 `company_payout` + 1 `initial_payment` + 2 audit log rows, DELETE it, assert 200, then `assertDatabaseMissing('contracts', ['id' => $id])`, `assertDatabaseCount('installments', 0)`, `assertDatabaseCount('payment_audit_logs', 0)`, and **`assertDatabaseCount('payments', 0)` — a general count that covers ALL payment types** (`company_payout`, `initial_payment`, `installment_payment`, or any future type) rather than type-specific assertions. The CASCADE chain must zero out the `payments` table entirely for the deleted contract.
- [x] T043 [P] [US5] Add the non-draft rejection test (AC-019): sign a contract, then DELETE it, assert 403 with `errors/contract.cannot_delete`
- [x] T044 [P] [US5] Add the create-then-immediately-delete test (AC-020): create a draft, immediately DELETE, assert 200 and zero rows in all 4 tables
- [x] T045 [P] [US5] Add the not-found test: DELETE a non-existent id, assert 404

### Implementation for User Story 5

- [x] T046 [US5] Create `src/app/Domains/Contract/Http/Requests/DeleteContractRequest.php` with `authorize()` returning `$this->user()->can('contracts.delete')` and no body rules
- [x] T047 [US5] Implement `ContractService::delete(int $id): void` in `src/app/Domains/Contract/Services/ContractService.php` — wraps in `DB::transaction(function () use ($id) { ... })`: (1) `Contract::lockForUpdate()->findOrFail($id)`, (2) guard `if ($contract->status !== 'draft') throw new ContractImmutableException(__('errors/contract.cannot_delete'))`, (3) `$contract->delete()` — the database CASCADE chain removes installments + payments + audit logs (depends on T042, T043, T045)
- [x] T048 [US5] Implement `ContractController::destroy(DeleteContractRequest $request, int $id): JsonResponse` — calls `$this->contractService->delete($id)` and returns `success(msg: __('success/contract.deleted'))` (depends on T047)
- [x] T049 [US5] Run `DeleteDraftContractTest`; fix any failures until all pass (depends on T044)

**Checkpoint**: At this point, User Story 5 (draft deletion + CASCADE + non-draft rejection) is testable independently. AC-018, AC-019, AC-020, AC-040 are verified.

---

## Phase 8: User Story 6 — Math Validation Delegation (Priority: P1)

**Goal**: Admin submits a contract with figures that fail `ContractMathValidator` (e.g., `total_after_profit` mismatch, fractional payment, `initial_payment` out of bounds). The system rejects with 422 and the appropriate error message key, and ZERO new rows are inserted (transaction rolled back).

**Independent Test**: POST a payload with `monthly_installment_amount` off by 0.01 from the correct `remaining / months`, assert HTTP 422, assert the response message corresponds to the math error, and assert the database has zero new rows in any of the 4 tables.

### Tests for User Story 6

- [x] T050 [P] [US6] Add `total_after_profit` mismatch test to `CreateContractTest`: POST with `total_after_profit` differing from the formula `purchase_amount + purchase_amount × profit / 100`, assert 422 with `errors/contract.total_mismatch`, then `assertDatabaseCount('contracts', 0)`, `assertDatabaseCount('installments', 0)`, `assertDatabaseCount('payments', 0)`, `assertDatabaseCount('payment_audit_logs', 0)` (zero new rows in all 4 tables)
- [x] T051 [P] [US6] Add fractional-payment test to `CreateContractTest`: POST with `monthly_installment_amount` that produces a non-terminating fraction (e.g., `1000 / 3 = 333.33...` × 3 = 999.99, leaving 0.01), assert 422 with `errors/contract.fractional_payment`, then `assertDatabaseCount('contracts', 0)`, `assertDatabaseCount('installments', 0)`, `assertDatabaseCount('payments', 0)`, `assertDatabaseCount('payment_audit_logs', 0)`
- [x] T052 [P] [US6] Add the atomicity-when-mid-flow-throws test to `CreateContractTest` (AC-014): use a `PaymentAuditLog::creating` event listener that throws `\RuntimeException('Forced failure')`, POST valid payload, assert 500, then `assertDatabaseCount('contracts', 0)`, `assertDatabaseCount('installments', 0)`, `assertDatabaseCount('payments', 0)`, `assertDatabaseCount('payment_audit_logs', 0)`
- [x] T053 [P] [US6] Add `initial_payment` out-of-bounds test: POST with `initial_payment > purchase_amount`, assert 422 with `errors/contract.initial_payment_bounds` (or equivalent), then `assertDatabaseCount('contracts', 0)`, `assertDatabaseCount('installments', 0)`, `assertDatabaseCount('payments', 0)`, `assertDatabaseCount('payment_audit_logs', 0)`

### Implementation for User Story 6

- [x] T054 [US6] Verify `ContractService::create` calls `ContractMathValidator::validate(...)` as the FIRST sub-operation inside `DB::transaction(...)` so any throw rolls back everything. **Additionally** assert the math-validator delegation contract end-to-end: (a) `ContractMathValidator::validate(...)` throws an exception whose class name is **exactly** `App\Domains\Contract\Exceptions\PaymentMismatchException` (for total / monthly / fractional mismatches) or `App\Domains\Contract\Exceptions\InvalidInitialPaymentException` (for `initial_payment` bounds) — assert via `try { $validator->validate(...); $this->fail('expected exception not thrown'); } catch (\Throwable $e) { $this->assertSame(PaymentMismatchException::class, get_class($e)); }`, (b) `src/app/Exceptions/ExceptionMappings.php` maps both exception classes to an HTTP **422** response (read the file; assert the mapping entry exists and returns 422), (c) the API response JSON contains the expected translation key in the `errors` object (e.g., `assertJsonValidationErrors(['total_after_profit'])` with the key `errors/contract.total_mismatch` resolved server-side into the Arabic message per `07_02_LOCALIZATION.md`). Keep the existing delegation verification — these are additions, not replacements (depends on T050, T051, T052, T053)
- [x] T055 [US6] Run all four math-validation tests; fix any failures until all pass (depends on T054)

**Checkpoint**: At this point, User Story 6 (math delegation + atomicity) is testable independently. AC-012, AC-014 are verified.

---

## Phase 9: User Story 7 — View Contract Detail (Priority: P1)

**Goal**: Admin GETs `/contracts/{id}` and receives 200 with the full contract body: all contract fields, the customer (id, name, phone), the agent (or null), all installments with all 7 fields, and the chronological `narrative_timeline` ordered by `created_at` ASC. `client_type_flags` is NEVER returned.

**Independent Test**: Create a contract, sign it, record 2 payments (SP-08 dependency out of scope; simulate via factory), delete 1 payment (simulate via factory), then GET detail and assert the timeline has all expected entries in order, each entry is self-contained (no `payment_id` reference), and `client_type_flags` is absent from the response.

### Tests for User Story 7

- [x] T056 [P] [US7] Create `src/tests/Feature/Contract/ViewContractTest.php` with the happy-path test (AC-021, AC-022): seed a contract with 12 installments, GET `/contracts/{id}`, assert 200, `data.customer` has `id/name/phone` only, `data.agent` has `id/name` or `null`, `data.installments` array has length 12 and each entry has all 7 fields (`id, installment_number, due_date, amount, status, paid_amount, paid_at`)
- [x] T057 [P] [US7] Add the `narrative_timeline` ordering test to `ViewContractTest` (AC-023): seed 2 audit log rows with different `created_at` timestamps, GET detail, assert the `narrative_timeline` array order matches `created_at` ASC
- [x] T058 [P] [US7] Add the self-contained-narrative test to `ViewContractTest` (AC-024): seed audit log rows whose `payment_id` is NULL (simulating a payment that was deleted), GET detail, then assert **`assertStringContainsString((string) $payment->amount, $timeline[last_index])`** — verifying the self-containment property per BR-004-9 (the narrative text must contain the amount as a string, so it remains readable without joining the `payments` table even after `payment_id` is nullified by `ON DELETE SET NULL`). Additionally assert the timeline text has no `payment_id` placeholder.
- [x] T059 [P] [US7] Add the `client_type_flags` absence test to `ViewContractTest` (AC-025): GET detail, assert the response JSON does NOT contain the key `client_type_flags` at any level
- [x] T060 [P] [US7] Add the not-found test to `ViewContractTest` (AC for `contract.not_found`): GET a non-existent id, assert 404 with `errors/general.not_found` (or `contract.not_found` per `06_08_ERROR_CODES.md`)

### Implementation for User Story 7

- [x] T061 [US7] Create `src/app/Domains/Contract/Http/Requests/ViewContractRequest.php` with `authorize()` returning `$this->user()->can('contracts.view')` and no body rules
- [x] T062 [US7] Implement `ContractService::view(int $id): Contract` in `src/app/Domains/Contract/Services/ContractService.php` — `Contract::with(['customer:id,name,phone', 'agent:id,name', 'installments', 'auditLogs'])->findOrFail($id)`; return the loaded model (depends on T056, T058, T059, T060)
- [x] T063 [US7] Implement `ContractController::show(ViewContractRequest $request, int $id): JsonResponse` — calls `$this->contractService->view($id)` and returns `success(data: ContractResource::make($contract))` (depends on T062)
- [x] T064 [US7] Run `ViewContractTest`; fix any failures until all pass (depends on T057, T060)

**Checkpoint**: At this point, User Story 7 (view detail with timeline + data minimization) is testable independently. AC-021 to AC-025, AC-038 are verified.

---

## Phase 10: User Story 8 — Installment Due-Date Edge Cases (Priority: P2)

**Goal**: Exhaustive verification that `DueDateGenerator` produces correct due dates for every month-end overflow combination, including Jan-31 → Feb-28/29, Mar-31 → Apr-30, May-31 → Jun-30, Aug-31 → Sep-30, Oct-31 → Nov-30, Dec-31 → Jan-31 (year boundary), Jan-30 → Feb-28/29, Jan-29 → Feb-28/29, leap-year boundary (2023-12-31 → 2024-02-29), and no-overflow baselines.

**Independent Test**: `DueDateGeneratorTest` parameterized dataProvider generates test cases for every `(start_day, start_month, is_leap)` combination listed in `research.md` Section 3.3, asserting the exact `Y-m-d` strings. A separate end-to-end feature test creates a 12-month contract starting `2024-01-31` and asserts the installment `due_date` column values match the expected matrix (AC-026 to AC-031).

### Tests for User Story 8

- [x] T065 [P] [US8] Add a `dataProvider` to `src/tests/Unit/Domains/Contract/Services/DueDateGeneratorTest.php` for the 60-months boundary: 60 test cases asserting `generate($anyValidStartDate, 60)` returns exactly 60 dates and the first date is `startDate + 1 month`
- [x] T066 [P] [US8] Add an end-to-end integration test in `src/tests/Feature/Contract/CreateContractTest.php` for the leap-year 12-month case (AC-026): POST a contract with `start_date='2024-01-31'` and `months=12`, assert the response's 12 installment `due_date` values are `2024-02-29, 2024-03-31, 2024-04-30, 2024-05-31, 2024-06-30, 2024-07-31, 2024-08-31, 2024-09-30, 2024-10-31, 2024-11-30, 2024-12-31, 2025-01-31` in that exact order
- [x] T067 [P] [US8] Add an end-to-end integration test for the non-leap year case (AC-027): POST with `start_date='2023-01-31'`, `months=12`, assert first installment `due_date` is `2023-02-28` (NOT `2023-02-29`)
- [x] T068 [P] [US8] Add an end-to-end integration test for the Mar-31 → Apr-30 case (AC-028): POST with `start_date='2024-03-31'`, `months=3`, assert installment dates `2024-04-30, 2024-05-30, 2024-06-30`
- [x] T069 [P] [US8] Add an end-to-end integration test for the Jan-30 → Feb-29 leap case (AC-029): POST with `start_date='2024-01-30'`, `months=3`, assert dates `2024-02-29, 2024-03-29, 2024-04-29`
- [x] T070 [P] [US8] Add an end-to-end integration test for the no-overflow baseline (AC-030): POST with `start_date='2024-01-15'`, `months=3`, assert dates `2024-02-15, 2024-03-15, 2024-04-15`
- [x] T071 [P] [US8] Add an end-to-end integration test for the Dec-31 year-boundary (AC-031): POST with `start_date='2024-12-31'`, `months=2`, assert dates `2025-01-31, 2025-02-28`
- [x] T072 [P] [US8] Add a future-start-date test (AC-042): POST with `start_date` 1 year in the future, assert 200, `data.installments[0].due_date` equals `start_date + 1 month` (also in the future)

### Implementation for User Story 8

- [x] T073 [US8] Run all new tests (T065 to T072); fix any failures — likely no code change since `DueDateGenerator` is already implemented in T021; if a test fails, fix the algorithm and re-verify (depends on T065, T066, T067, T068, T069, T070, T071, T072)

**Checkpoint**: At this point, all 8 user stories are fully testable. The due-date algorithm is exhaustively verified across every month-end overflow case. AC-026 to AC-031, AC-042 are verified.

---

## Phase 11: Polish & Cross-Cutting Concerns

**Purpose**: Improvements that affect multiple user stories, plus the project-wide Definition-of-Done checklist.

- [x] T074 [P] Verify the 4 rate limiters from T013 are reachable: hit each of the 4 endpoints and assert the 61st (or 121st for view) request from the same admin within 60 seconds returns 429 with `errors/general.too_many_requests` (AC-041) — add an integration test in `src/tests/Feature/Contract/RateLimitTest.php` if missing
- [x] T075 [P] Verify the 4 routes in `src/app/Domains/Contract/Routes/v1/api.php` from T16 include the middleware stack: `auth:admin` + `throttle:{name}` + `can:{permission}` — assert via a feature test that requests without the Sanctum token get 401, requests with a token but missing permission get 403 (AC-037, AC-038, AC-039, AC-040)
- [x] T076 [P] Verify the 4 permission keys (`contracts.create`, `contracts.view`, `contracts.sign`, `contracts.delete`) exist in the `permissions` table and are assigned to `super-admin`; add them to `src/database/seeders/RolePermissionSeeder.php` if missing
- [x] T077 [P] Verify `client_type_flags` is NEVER returned in any contract response — add a `DAMA\DataType` schema snapshot test in `src/tests/Feature/Contract/ContractResponseSchemaTest.php` that asserts the full JSON shape matches the contract from `contracts/02_view.md` (no `client_type_flags` key anywhere)
- [x] T078 [P] Verify all error responses use translation keys (AC-035) — assert every error response's `message` field is a translation key like `errors/contract.cannot_sign` (NOT a raw Arabic/English string)
- [x] T079 [P] Verify all responses use the unified format (AC-036) — assert every response has the keys `success`, `message`, `data` (and optionally `meta`, `errors`)
- [x] T080 [P] Verify all `success()` and `error()` calls in `ContractController` and `ContractService` use **named parameters** (no positional args) per NFR-018
- [x] T081 [P] Verify `ReferenceNumber` regex match (AC-011, AC-034): create 2 contracts in sequence, assert both `reference_number` match `^CTR-\d{10}$` and the second's numeric suffix is the first's plus one
- [x] T082 [P] Verify `markAsCustomer` / `markAsAgent` idempotency (AC-032, AC-033): create a contract for a customer who already has the `customer` flag, then assert **`assertSame(['customer'], $client->fresh()->client_type_flags)`** — verifying the exact array contents (single entry, correct value, no duplicates, no extras). The JSONB column must deserialize to a single-element array with exactly the string `"customer"`.
- [x] T083 [P] Run the full test suite: `php artisan test` — fix any regression in SP-01/SP-03/SP-04/SP-05/SP-06 caused by this spec
- [x] T084 [P] Run coverage check: `php artisan test --coverage` — assert ≥80% overall coverage; 100% for `DueDateGenerator`
- [x] T085 [P] Run static analysis / lint per project conventions (PHPStan / Pint / PHP-CS-Fixer) — fix any reported issues in the new `src/app/Domains/Contract/` and `src/tests/{Unit,Feature}/Contract/` files
- [x] T086 Run the `quickstart.md` Section 6 Definition of Done checklist manually (or via a smoke-test script): verify the 14 items are all checked

---

## Dependencies & Execution Order

### Phase Dependencies

- **Setup (Phase 1)**: No dependencies — can start immediately
- **Foundational (Phase 2)**: Depends on Setup completion — BLOCKS all user stories
- **User Stories (Phase 3–10)**: All depend on Foundational phase completion
  - US1 must complete before US2/US3 (US2/US3 are extensions of US1's `create` flow)
  - US4 (sign) is independent of US1's `create` flow once `ContractService` skeleton (T025) exists
  - US5 (delete) is independent of US1's `create` flow once `ContractService` skeleton (T025) exists
  - US6 (math delegation) tests are added to the US1 `CreateContractTest` file, so US6 logically extends US1
  - US7 (view) is independent of US1's `create` flow once `ContractService` skeleton (T025) exists
  - US8 (due-date edge cases) extends the US1 unit tests in `DueDateGeneratorTest`; can be done in parallel with US2–US7
  - User stories can proceed in parallel (if staffed) once their respective dependencies are met
- **Polish (Phase 11)**: Depends on all desired user stories being complete

### User Story Dependencies

- **US1 (P1)**: Can start after Foundational (Phase 2) — No dependencies on other stories
- **US2 (P1)**: Can start after US1 (extends `create` flow + tests in same file)
- **US3 (P1)**: Can start after US1 (extends `create` flow + tests in same file)
- **US4 (P1)**: Can start after Foundational (Phase 2) — independent test file `SignContractTest.php`; only needs `ContractService` skeleton (T025)
- **US5 (P1)**: Can start after Foundational (Phase 2) — independent test file `DeleteDraftContractTest.php`; only needs `ContractService` skeleton (T025)
- **US6 (P1)**: Can start after US1 (extends `CreateContractTest.php` with math-rejection tests)
- **US7 (P1)**: Can start after Foundational (Phase 2) — independent test file `ViewContractTest.php`; only needs `ContractService` skeleton (T025)
- **US8 (P2)**: Can start after US1 (extends `DueDateGeneratorTest.php` and `CreateContractTest.php`); no production code change expected

### Within Each User Story

- Tests (per Constitution §III Test-First) MUST be written and FAIL before implementation
- Pure algorithm (`DueDateGenerator`) tests before `ContractService` tests
- `ContractService` before `ContractController`
- `ContractController` before route registration verification
- `FormRequest` classes before the controller method that consumes them
- Story complete before moving to the next priority

### Parallel Opportunities

- All Setup tasks marked [P] can run in parallel (T001–T012, excluding T012 which is the skeleton that others depend on)
- All Foundational tasks T013–T017 are largely sequential (each builds on the previous); T013, T014 can run in parallel with the T008–T011 translation files from Setup
- Within US1, T018/T019 (unit tests) and T020 (feature test) can be written in parallel; T022 (InstallmentResource) and T023 (ContractResource) can run in parallel
- US2 (Phase 4) and US3 (Phase 5) can run in parallel with each other and with US4/US5/US7 (each has its own test file)
- US8 (Phase 10) can run in parallel with US4/US5/US7 since it only adds tests, no code change
- All Polish tasks T074–T082 marked [P] can run in parallel
- Different user stories can be worked on in parallel by different team members once Foundational is done

---

## Parallel Example: User Story 1

```bash
# Launch unit tests + feature test + resource classes in parallel (all different files, no mutual deps):
Task: "Create src/tests/Unit/Domains/Contract/Services/DueDateGeneratorTest.php with full 33-cell matrix"
Task: "Create src/tests/Feature/Contract/CreateContractTest.php with happy-path test"
Task: "Create src/app/Domains/Contract/Http/Resources/InstallmentResource.php"
Task: "Create src/app/Domains/Contract/Http/Resources/ContractResource.php"
Task: "Create src/app/Domains/Contract/Http/Requests/StoreContractRequest.php"

# Then sequentially (each depends on the previous):
Task: "Implement DueDateGenerator in src/app/Domains/Contract/Services/DueDateGenerator.php"
Task: "Create ContractService skeleton in src/app/Domains/Contract/Services/ContractService.php"
Task: "Implement ContractService::create with full transaction"
Task: "Implement ContractController::store"
```

## Parallel Example: User Story 4 + 5 + 7 (after US1 is done)

```bash
# Three developers work on three different test files in parallel:
Developer A: "SignContractTest.php + SignContractRequest + ContractService::sign + ContractController::sign"
Developer B: "DeleteDraftContractTest.php + DeleteContractRequest + ContractService::delete + ContractController::destroy"
Developer C: "ViewContractTest.php + ViewContractRequest + ContractService::view + ContractController::show"
```

---

## Implementation Strategy

### MVP First (User Story 1 Only)

1. Complete Phase 1: Setup (T001–T012)
2. Complete Phase 2: Foundational (T013–T017)
3. Complete Phase 3: User Story 1 (T018–T028)
4. **STOP and VALIDATE**: Run `CreateContractTest` and `DueDateGeneratorTest`; manually POST a contract via Postman/curl; verify the response body, DB rows, and `Queue::assertPushed(RefreshCustomerListingJob::class)` — MVP is ready
5. Deploy/demo if ready

### Incremental Delivery

1. Complete Setup + Foundational → Foundation ready
2. Add User Story 1 → Test independently → Deploy/Demo (MVP! — happy-path contract creation)
3. Add User Story 2 + User Story 3 → Test independently → Deploy/Demo (zero-down-payment + no-agent variants)
4. Add User Story 4 → Test independently → Deploy/Demo (signing)
5. Add User Story 5 → Test independently → Deploy/Demo (draft deletion)
6. Add User Story 6 → Test independently → Deploy/Demo (math validation rejection)
7. Add User Story 7 → Test independently → Deploy/Demo (view detail with timeline)
8. Add User Story 8 → Test independently → Deploy/Demo (exhaustive date edge cases)
9. Polish (Phase 11) → Final regression run + coverage check

### Parallel Team Strategy

With multiple developers:

1. Team completes Setup + Foundational together (small, ~1 hour)
2. Once Foundational is done:
   - Developer A: User Story 1 (the heaviest — includes `ContractService::create` + `DueDateGenerator` + 3 Resource/Request classes)
   - Developer B (starts after A's `ContractService` skeleton from T025): User Story 4 (sign)
   - Developer C (starts after A's `ContractService` skeleton from T025): User Story 5 (delete)
   - Developer D (starts after A's `ContractService` skeleton from T025): User Story 7 (view)
3. After US1 completes, Developers B/C/D continue with US2/US3/US6/US8 (US6 and US8 extend `CreateContractTest.php`; US2/US3 also extend it)
4. Stories complete and integrate independently

---

## Notes

- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability (US1–US8)
- Each user story is independently completable and testable
- Tests are NOT optional in this spec — Constitution §III is NON-NEGOTIABLE for the SP-07 financial engine
- `DueDateGenerator` is the user's precision focus: the unit test matrix in `DueDateGeneratorTest.php` must cover every month-end overflow combination, not just Jan/Feb (per `research.md` Section 3.3)
- `ContractService::create` is the heart of the spec: order of sub-operations inside `DB::transaction(...)` matters (math validation FIRST, flag updates LAST)
- `RefreshCustomerListingJob::dispatch()` is called AFTER the transaction returns — never inside the closure
- All financial string assertions use `assertSame('5000.00', $value)`, never `assertEquals(5000.00, $value)`
- `client_type_flags` is NEVER exposed in any response (FR-023, data minimization)
- Commit after each task or logical group
- Stop at any checkpoint to validate story independently
- Avoid: vague tasks, same-file conflicts, cross-story dependencies that break independence
