# SPECIFICATIONS/03_ARCHITECTURE/04_DECISION_RECORDS.md

**PURPOSE:** Documents the historical "why" behind system and data model choices. This file explains WHAT the system is built on and WHY certain technologies or patterns were chosen over alternatives.

**ARCHITECTURAL NOTE:** Code Design Decisions that dictate "HOW to write code" have been separated into `04_DECISION_RECORDS_HISTORICAL.md`. That file documents the historical context only — the implementation rules themselves live as canonical law in the `CONSTITUTIONS/` folder.

> **For HOW to implement these decisions:** See `../../CONSTITUTIONS/*.md`

---

## System & Data Design Decisions

### ADR-001: No Soft Deletes
No entity uses soft deletion. Deletion is either hard delete (draft contracts) or prohibited entirely (clients, agents).
**Rationale:** Financial history must be preserved, not hidden. Agents and customers are immutable entities. Draft contracts have no history worth keeping.
**Consequences:** No "restore" functionality, no "trash" views, simpler queries.

### ADR-002: No Agent Deletion
Agents cannot be deleted — neither soft nor hard.
**Rationale:** Agent is a historical entity tied to financial records. Deleting would corrupt contract history.
**Consequences:** Enforced at model and database levels.

### ADR-003: LIFO for Payments
Only the most recent payment can be deleted (Last-In, First-Out).
**Rationale:** Preserves ledger sequence and accuracy. Matches physical ledger behavior. Prevents cascading reversals.
**Consequences:** Must delete in reverse order. Limits admin flexibility but ensures integrity.

### ADR-004: Computed Profits
Investor profits are calculated dynamically, not stored in a separate table.
**Rationale:** YAGNI — no current need for stored profits. Avoids synchronization issues. Simple formula: `SUM(contract_margin) × 5%`.
**Consequences:** Profit query requires aggregation at request time. No historical profit snapshots.

### ADR-005: Cursor Pagination
Use cursor-based pagination, not offset-based.
**Rationale:** Offset pagination degrades at scale. Cursor pagination starts directly at position. Essential for customer list with complex sorting.
**Consequences:** No "jump to page N" functionality. Clients must follow cursor chains.

### ADR-006: payment_date for Analytics Filters
Time filters in analytics use `payment_date` (when money was collected), not `due_date` (when it was expected).
**Rationale:** Analytics measure actual cash flow patterns, not obligation patterns.

### ADR-007: Share Value Fixed at $200
Each share has a fixed value of 200 USD.
**Rationale:** Simplifies calculations. No need for price history table.
**Consequences:** If share price changes in future, migration required.

### ADR-008: Investor Flag Independent of Shares
The `investor` flag is assigned when a client is designated as an investor — it is permanent and never removed, regardless of whether shares are ever added or later withdrawn. Shares are managed independently through the Shares domain.
**Rationale:** Separates identity (being an investor) from activity (owning shares). Allows simpler UI flows where an investor is created first, then shares are added separately. Permanent flag simplifies permission checks.
**Consequences:** An investor may appear in investor lists with zero shares balance.

### ADR-009: PostgreSQL 15+
Use PostgreSQL instead of MySQL.
**Rationale:** Superior analytics via Materialized Views. JSONB with GIN indexing. `FILTER (WHERE ...)` clause. `BOOL_OR()` aggregate. MVCC for financial data.
**Consequences:** Team must know PostgreSQL-specific syntax. No MySQL compatibility.

### ADR-010: Unified Clients Table
Single table for Customers, Agents, and Investors.
**Rationale:** Prevents data duplication. A person can be all three roles simultaneously. JSONB flags enable O(1) role inference. Simplifies referral tracking.
**Alternatives rejected:** Separate tables (duplication), single `type` column (cannot represent multiple roles), polymorphic relations (unnecessary complexity).

### ADR-011: Installments as State Table
Pre-generate all installments as physical rows at contract creation.
**Rationale:** Enables efficient sorting by `due_date`. Supports direct status filtering. Enables index on `(contract_id, status, installment_number)`. Clear audit trail.
**Alternatives rejected:** Dynamic calculation (complex date math on every query), event sourcing (overkill).
**Consequences:** More storage (minimal per row). Installment data is immutable after creation.

### ADR-012: Ledger + Audit Log (Not Tree Versioning)
Separate `payment_audit_logs` table with independent narrative text, instead of tree-versioning the payments table.
**Rationale:** Tree versioning degrades aggregation performance. Separate table keeps main `payments` table lean. Narrative preserved independently even after payment deletion.
**Consequences:** Two tables to maintain. Narrative must be self-contained (cannot join to payments after deletion).

### ADR-013: Hard Delete for Draft Contracts
Draft contracts (even with initial payment) are hard deleted with CASCADE.
**Rationale:** No history needed for cancelled drafts. Cleaner database. Business requirement: cancelled drafts leave no trace.
**CASCADE scope:** Contract, all installments, all payments (including company_payout and initial_payment), all audit logs.
**Consequences:** No "restore draft" functionality. Irreversible operation.

### ADR-014: 30-Day Lock on Shares
Share log entries can only be modified or deleted within 30 days of creation.
**Rationale:** Business requirement for share transaction stability. Prevents manipulation of historical records.
**Implementation constraint:** Check uses the record creation date (never changes). Only the most recent `active` record can be modified.

### ADR-015: No Database-Level ENUMs
Use `VARCHAR` for status columns, not PostgreSQL ENUMs.
**Rationale:** ENUMs require `ALTER TABLE` to add values (locks table). VARCHAR with application-level constants is more flexible.
**Consequences:** No database-level validation of status values. Application must enforce valid values.

### ADR-016: Spatie Laravel Permission for RBAC
Use `spatie/laravel-permission` for role-based access control.
**Rationale:** Future-proofing. Dynamic permission assignment without code changes. Built-in caching.

### ADR-017: Async Report Export via Queues
All report exports processed via Laravel Queues. API returns immediately with job ID.
**Rationale:** Prevent API timeout on large data extraction. Non-blocking. Scales with queue workers.

### ADR-018: LIFO Edit on Payments
Allow editing `payment_date` and `notes` of the most recent payment only.
**Rationale:** Fix typos without breaking chain. LIFO ensures no ambiguity. Only metadata edited, not financial data.
**Constraint:** Amount modification still prohibited.

### ADR-019: Validation Not Calculation
Backend validates math instead of calculating installments.
**Rationale:** Mobile app handles all calculation complexity. Backend role: verify correctness and reject invalid data. Prevents fractional payments at source.
**Consequences:** Mobile must implement correct math. Backend rejects if fractions detected.

### ADR-020: Exact Match Payments
Payment amount must exactly equal one installment amount. No partial payments, no multiple installments in one request.
**Rationale:** Simplifies payment engine drastically. 1 payment = 1 installment exactly. O(1) payment processing.
**Consequences:** No "pay what you can" scenarios. Amount modification becomes logically impossible.

### ADR-021: Contract Margin as Investor Profit Base
Investor profit is 5% of contract margin (`total_after_profit - purchase_amount`), not 5% of total collected.
**Rationale:** Contract margin represents what the company actually earned from the product. Only applies to `active` or `completed` contracts.
**Consequences:** Investor profit reflects the company's actual gain, not the customer's total payment.

### ADR-023: JSONB client_type_flags
Use JSONB array column for role inference instead of runtime queries or separate type column.
**Rationale:** O(1) role inference without JOINs. GIN index for fast filtering. Supports multiple simultaneous roles. Updated in same transaction as triggering operation.

### ADR-024: Status Column Not Boolean
Use explicit `status` column with state machine (`draft` → `active` → `completed`) instead of `is_signed` boolean.
**Rationale:** Explicit states prevent ambiguity. Enables simple filtering. Clear transition rules.
**Alternatives rejected:** `is_signed` boolean (ambiguous for completed), derived from payments (fragile).

### ADR-026: Status and updated_at on Shares Logs
Track modification status and timestamp for share log entries.
**Rationale:** Proper modification tracking within 30-day window. Records marked as `modified` or `deleted`, not physically removed. Creation date never changes (used for 30-day lock).

### ADR-027: Materialized Views + Caching for Analytics
Use PostgreSQL Materialized Views with application-level caching.
**Rationale:** Leverage PostgreSQL for sub-second analytics at scale. Pre-compute daily aggregations during off-hours. Application cache reduces MV reads.
**Consequences:** Data up to 24 hours stale. No on-demand refresh.

### ADR-029: Audit Logs CASCADE on Contract Delete
`payment_audit_logs.contract_id` uses `ON DELETE CASCADE` — logs deleted when draft contract is deleted.
**Rationale:** No history needed for cancelled drafts. Contrasts with `payment_id` which uses `SET NULL`.

### ADR-031: Zero Initial Payment Allowed
Zero-down-payment contracts are valid.
**Rationale:** Business requirement. Full amount spread across installments. Validation: `0 ≤ initial_payment ≤ purchase_amount`.

### ADR-032: Payment Sequencing Rules
Installments paid by ascending sequential order. Cannot skip overdue to pay later. Early payment allowed. Due dates never change. All installments have identical amounts.

### ADR-033: Payment Lookup Index
Composite index `(contract_id, status, installment_number)` for O(1) next-installment lookup.
**Rationale:** Critical for payment registration performance. Covers next-installment lookup and completion check.

### ADR-034: Customer Listing via Materialized View
Pre-compute customer list with 5-minute refresh instead of complex JOIN+GROUP BY on every request.
**Rationale:** Sub-second response for sorted customer list. Acceptable 5-minute staleness for list pages. Detail pages use live data.

### ADR-035: Shares Balance Formula
Explicit formula: sum `active` records only — `add` as positive, `withdraw` as negative. `modified` and `deleted` excluded.
**Rationale:** Single source of truth. Prevents inconsistent implementations.

### ADR-036: installment_id in Payments
Explicit linking of each payment to its installment. The sole source of truth for payment-installment mapping.
**Rationale:** When amounts are identical across installments, cannot identify which was paid by amount alone. Enables direct LIFO reversal.

### ADR-037: company_payout Payment Type
Record company's purchase cost as a payment type (`company_payout`).
**Rules:** Created automatically with every contract. Amount equals `purchase_amount`. Not linked to any installment. Deleted only via CASCADE when draft deleted.
**Purposes:** Verify mobile's `purchase_amount`, internal accounting, contract timeline narrative.

### ADR-042: Analytics 365-Day Cap
Maximum 1-year range for analytics requests.
**Rationale:** Prevent unbounded queries. 1 year sufficient for management reports.

### ADR-043: first_contract_date in Customer Listing MV
Pre-compute first contract date in the customer listing MV for time-based filtering.
**Rationale:** Enables time filtering without complex queries at request time.

### ADR-054: Shares as Independent Domain
Shares management extracted from Agent domain into its own domain (`app/Domains/Shares/`).
**Rationale:** Shares have their own entity, validation rules, and state machine. Separation of concerns. Endpoints operate on any client with investor flag, not just agents.
**Consequences:** Agent detail still displays shares data but doesn't manage them.

### ADR-059: Investor as Independent Domain
Separate Investor domain (`app/Domains/Investor/`) with its own CRUD endpoints.
**Rationale:** Investor creation has unique constraints (permanent flag, independent from shares). Detail returns different data than agent detail (includes profit calculations and referred customers with margin data). Without separation, Agent domain accumulates investor-specific logic.

### ADR-061: Rename agent_shares_logs to client_shares_logs
Shares table renamed to reflect that shares are not agent-specific — any client with investor flag can have shares.
**Rationale:** Consistency with ADR-054 (shares domain independence).