# Oikka Capital - Technical Documentation

Oikka Capital is a samity and investment management platform built with a modern, high-performance web stack. This document details the system architecture, database schema, role-isolation rules, and core mathematical formulas.

---

## 🏗️ System Architecture

* **Backend:** Laravel 11 Framework (PHP)
* **Frontend:** Inertia.js with Vue 3 (Composition API, `<script setup>`) and TypeScript
* **Styling:** TailwindCSS
* **State & Validation:** Laravel Precognition (live form validation) and Inertia Forms (`useForm`)
  > [!IMPORTANT]
  > For Laravel Precognition validations (`form.validate()`) to function correctly without executing the store/update logic as duplicate requests, the `HandlePrecognitiveRequests::class` middleware must be attached to the respective routes (e.g., `members`, `payments.store`, and `roles` in `web.php`).
* **Routing:** Laravel Wayfinder (generates type-safe client-side TypeScript routing helper helper files)

---

## 🗄️ Database Schema & Models

The database features a fully normalized schema designed for relation-integrity and cascade deletion safety:

```mermaid
erDiagram
    users ||--o{ role_user : "pivot"
    roles ||--o{ role_user : "pivot"
    users ||--o{ payments : "makes"
    users ||--o{ businesses : "manages"
    businesses ||--o{ investments : "contains"
    businesses ||--o{ collections : "receives"
    investments ||--o{ investment_shares : "distributes"
    users ||--o{ investment_shares : "holds"
```

### 1. Normalized Roles Pivot
* **`roles` table:** Stores system roles: `'super_admin'`, `'admin'`, `'founder'`, and `'member'`.
* **`role_user` table:** Pivot table mapping users to multiple roles (`user_id` and `role_id`).
* **Cascade Deletes:** Deleting a user automatically purges pivot relations, payments, and investment records.

### 2. Business Operational Scope
* **`businesses` table:** tracks operational businesses. Managed by a `'founder'` role user (`user_id`).
* **`investments` table:** tracks capital raised for a business.
* **`investment_shares` table:** stores members' frozen share allocations.
* **`collections` table:** logs returned revenue returns by the managing founder.

### 3. Payments Ledger & Enums
* **`payments` table:** logs all transactions with cast Enums (`PaymentType` and `PaymentStatus`).
  * `type` (`PaymentType` enum): `PaymentType::INSTALLMENT` (`'installment'`), `PaymentType::WITHDRAWAL` (`'withdrawal'`), or `PaymentType::COLLECTION_RETURN` (`'collection_return'`).
  * `status` (`PaymentStatus` enum): `PaymentStatus::PENDING` (`'pending'`), `PaymentStatus::RECEIVED` (`'received'`), or `PaymentStatus::REJECTED` (`'rejected'`).
  * `method`: Payment/payout method (`'bank'`, `'bkash'`, or `'cash'`). Defaults to `'bank'`.
  * `ref`: Optional transaction reference or reference ID.

---

## 🧮 Proportional Share Allocation & Balance Enforcement

### 1. Member Available Balance & Effective Balance
A member's uncommitted funds are computed dynamically:

$$\text{Member Available Balance} = \max(0, \sum (\text{Received Installments and Distributed Returns}) - \sum (\text{Wallet-Funded Shares}) - \sum (\text{Approved Withdrawals}))$$

Wallet-funded shares belong to investments with no `collection_id`. Directly reinvested collection returns increase total invested funds without crediting or debiting the member's available balance.

$$\text{Effective Available Balance} = \max(0.0, \text{Member Available Balance} - \sum (\text{Pending Withdrawals}))$$

### 2. Proportional Share Calculation
When raising capital via a new investment opportunity, each member's default percentage contribution is computed based on their **Effective Available Balance** relative to the sum of all members' effective available balances:

$$\text{Member Share \%} = \frac{\text{Member Effective Balance}}{\text{Total Effective Balance of All Members}} \times 100$$

Default percentages are apportioned in 0.01 percentage-point increments using `MoneyMath::distribute('100.00', $balances)`, rather than rounding each percentage independently. Their displayed total is exactly 100.00% whenever the member pool is positive; members with no effective balance receive 0.00%. If the entire pool is empty, every default remains 0.00%.

Two-decimal percentage weights may still exceed an individual member's balance when investing the entire pool (for example, 33.34% of Tk 300 exceeds a Tk 100 balance). The final contribution balance checks remain enforced; exact percentage totals do not override them.

For new investments, percentages are allocation weights with up to two decimal places and must total exactly **100.00%**. Both server validation and the investment form reject totals such as 99.99% or 100.01%, without a rounding tolerance. `InvestmentAllocation` calculates contributions through `MoneyMath::distribute()` and assigns leftover paisa using the largest remainder method (ties follow submitted member order). Final contributions always total exactly the investment capital.

The server recalculates contribution amounts instead of trusting submitted `shares.*.amount` values, which are optional for compatibility. Member IDs must be distinct, capital accepts up to two decimal places, and balance checks use the final calculated contributions. The browser preview follows the same allocation rule. Existing investments and shares are not recalculated by this change.

### 3. Individual Balance Enforcement
To prevent accounts from going into negative balances or over-committing reserved funds, the system enforces the following constraints:

$$\text{Requested Withdrawal} \le \text{Effective Available Balance}$$
$$\text{Allocated Share Amount} \le \text{Effective Available Balance}$$

### Edge Case Handling:
* **Zero System Deposits:** If the sum of all available balances is `0.00`, the default share percentage defaults to `0.00%` for all members.
* **Soft Reservation:** Pending withdrawal requests reserve funds immediately from `Effective Available Balance`. If an Admin rejects a withdrawal request, the soft reservation is released back to the member's balance.
* **Frozen Allocations:** Once an investment is saved, member percentages and calculated amounts are written to `investment_shares`—freezing them permanently.

### 4. Historical Tk 3 Reconciliation and Reinvestment Replay

Migration `2026_10_14_000001_reconcile_initial_investment_share_rounding.php` corrects the audited original investment #1 in business #1. Its capital remains Tk 30,000, while its member allocations are reduced proportionally from Tk 30,003 to Tk 30,000 using the largest remainder method. Display percentages are separately apportioned to total 100.00%.

| Member ID | Original contribution | Corrected contribution | Released balance | Corrected percentage |
|-----------|----------------------:|-----------------------:|-----------------:|---------------------:|
| 4 | 11,580.00 | 11,578.84 | 1.16 | 38.60% |
| 5 | 3,684.00 | 3,683.63 | 0.37 | 12.28% |
| 6 | 11,580.00 | 11,578.84 | 1.16 | 38.59% |
| 7 | 3,159.00 | 3,158.69 | 0.31 | 10.53% |
| 8 | 0.00 | 0.00 | 0.00 | 0.00% |
| **Total** | **30,003.00** | **30,000.00** | **3.00** | **100.00%** |

The initial migration locks the target investment and its shares in a transaction and checks the audited member IDs, contribution amounts, and percentages before updating anything. A missing target, different target capital, or already reconciled allocation is left untouched; an unexpected allocation for the matching target aborts for review. Repeated execution does not apply another adjustment. This first migration only changes the original investment's shares. Corrected contribution amounts remain authoritative; two-decimal percentages are rounded representations of ownership.

The follow-up migration `2026_10_14_000002_reconcile_reinvestment_share_sequence.php` requires the original correction and replays investments #2, #3, and #4 in their **recording order** (`created_at`, then ID). Collection dates can be backdated and do not determine ownership for this replay. Each corrected return is added to the running ownership before calculating the next return.

| Reinvestment ID | Collection ID | Profit total preserved | Contribution changes |
|-----------------|---------------|-----------------------:|----------------------|
| 2 | 1 | 2,024.00 | None |
| 3 | 2 | 2,473.00 | None |
| 4 | 3 | 2,069.00 | Member #4: 798.56 → 798.55; member #7: 217.84 → 217.85 |

Each reinvestment's percentages become 38.60%, 12.28%, 38.59%, and 10.53% for members #4–#7, totaling exactly 100.00%. The three recorded profits still total Tk 6,566 and the historical investment sequence totals Tk 36,566. Replaying reinvested returns does not credit or debit member wallets. The later Tk 10,000 investment (#5), its member allocations, payments, and collection/investment headers remain unchanged.

The follow-up validates the complete historical sequence before updating any shares, accepts either the audited legacy allocation or the fully corrected allocation for each reinvestment, and applies changes atomically. Unexpected shares, changed profit totals, linked payout payments, additional collections, duplicate reinvestments, or intervening capital require review instead of automatic correction. Later ordinary investments are excluded from the replay. Repeating the migration on the same history leaves values and timestamps unchanged. Future reinvestments and their browser previews apportion percentages to exactly 100.00% from their final allocated amounts, with equal remainders resolved in member ID order. The preview mirrors the canonical allocator's eight-decimal monetary remainder precision so nearly equal weights produce the same recipients on both sides.

Back up the database before applying this financial correction. Apply it with:

```bash
php artisan migrate
```

Laravel runs the pending corrections in timestamp order. If the initial correction has already run, it is skipped and the follow-up runs next. Both migrations' `down()` methods intentionally leave corrected financial records intact. Automatic migration rollback does not restore the previous allocations; any reversal requires a separately reviewed correction or a suitable backup.

---

## 🛣️ Key API Routes

All endpoints are registered inside [web.php](file:///home/mehedi/Apps/mahedi/oikka-capital/routes/web.php) under the `auth` middleware group:

* **Users CRUD:** `Route::resource('users', UserController::class)`
* **Members CRUD:** `Route::resource('members', MemberController::class)->middleware([HandlePrecognitiveRequests::class])`
* **Installments & Withdrawals:**
  * `Route::get('/payments', [PaymentController::class, 'index'])`
  * `Route::post('/payments', [PaymentController::class, 'store'])->middleware([HandlePrecognitiveRequests::class])`
  * `Route::post('/payments/withdraw', [PaymentController::class, 'withdraw'])->middleware([HandlePrecognitiveRequests::class])`
  * `Route::patch('/payments/{payment}', [PaymentController::class, 'update'])`
  * `Route::patch('/payments/{payment}/reject', [PaymentController::class, 'reject'])`
* **Roles management:** `Route::resource('roles', RoleController::class)->middleware([HandlePrecognitiveRequests::class])`
* **Businesses & Investments:** 
  * `Route::resource('businesses', BusinessController::class)`
  * `Route::post('/investments', [InvestmentController::class, 'store'])`
  * `Route::post('/collections', [CollectionController::class, 'store'])`

---

## 🧪 Testing Layout
Located in [tests/Feature](file:///home/mehedi/Apps/mahedi/oikka-capital/tests/Feature):
* **`InvestmentAllocationTest.php`:** Verifies default percentages total exactly 100.00% for positive pools, zero-balance defaults, rejection of percentage totals above or below 100.00%, exact investment allocations, server-calculated contributions, member balance limits after rounding, and duplicate-member rejection.
* **`InvestmentShareReconciliationMigrationTest.php`:** Verifies the targeted Tk 3 correction, exact contribution and percentage totals, released member balances, unchanged historical returns, repeatability, and guards against unexpected records.
* **`ReinvestmentSequenceReconciliationMigrationTest.php`:** Verifies chronological reinvestment replay, the one-paisa ownership adjustment, exact percentage totals, unchanged wallets and later capital, atomic rejection of unexpected history, and repeatability.
* **`WithdrawalTest.php`:** Verifies deposit withdrawal requests, soft-reservation limits, double withdrawal guards, investment lock protections, admin approval/rejection flows, and role isolation.
* **`PaymentTest.php`:** Verifies installment flows, self-pay actions, payment methods/references, admin record entries, and receipt approvals.
* **`MemberRecordIsolationTest.php`:** Verifies record-level isolation for member roles to ensure members only view their own payments, investments, collections, and businesses.
* **`CollectionAndInvestmentTest.php`:** Verifies the investment cap pool checks, collection payouts (distribute and reinvest modes), and individual member balance enforcement limits.
* **`UserTest.php`:** Verifies users listing and multiple role pivot syncing.
* **`BusinessTest.php`:** Verifies business ownership, collection authorization boundaries, and investment percentage validation.
* **`MonthlyPaymentReminderTest.php`:** Verifies automated monthly payment reminder email triggers on the 4th of every month for unpaid members.

The investment preview's rounding cases are covered by `npm run test:frontend` (Node.js 22.6+), alongside the backend suite (`php artisan test`).

---

## ⏰ Automated Scheduled Tasks
* **`payments:send-monthly-reminder`** (`App\Console\Commands\SendMonthlyPaymentReminder`):
  * **Schedule**: `0 9 4 * *` (Runs on the 4th day of every month at 9:00 AM).
  * **Target**: Queries active members who have no payments or payment requests recorded in the current calendar month.
  * **Action**: Dispatches `MonthlyPaymentReminderMail` with a responsive HTML template directing members to log in and pay their installment.
  * **Options**: Use `--force` flag to execute manually on any day for testing.
