# Legacy Import Loan Flag Update

## Overview

This update introduces a loan-level flag to distinguish loans created through the legacy CSV import flow from loans created through the normal application workflow.

The goal is to preserve the original CSV `Paid` value as the authoritative paid amount for historical legacy-imported loans, without interfering with the normal repayment logic for new application-created loans.

---

## Why this update is needed

Legacy CSV imports are not live repayment events. They are historical data ingestion events. Because of that, the imported `Paid` value should remain the source of truth for loans created through that pathway.

Without a flag, the system may later recalculate the loan's paid total using:

```php
$loan->repayments()->sum('amount_paid')
```

This creates a mismatch where the DB-paid summary drifts away from the original imported CSV `Paid` value.

---

## New field

Add the following field to the `loans` table:

```php
legacy_import BOOLEAN NOT NULL DEFAULT false
```

or integer form if preferred by the project:

```php
legacy_import TINYINT(1) NOT NULL DEFAULT 0
```

### Meaning

- `true` / `1` = this loan was created during a legacy CSV import
- `false` / `0` = this loan was created normally through the app

---

## Business rule

When `legacy_import = true` for a loan:

- the imported CSV `Paid` value becomes the authoritative paid amount for that record
- the system must not overwrite that value using the runtime sum of repayments
- reconciliation logic for `amount_paid` must short-circuit for that loan

This is the key protection against historical value drift.

---

## Repayment allocation for legacy imports

When a legacy-imported loan has a non-zero `Paid` amount, the system must distribute that amount across the repayment schedule so that individual installment records reflect the historical payment pattern.

### Mechanism

1. **Payment Allocation Service**: Uses `RepaymentService::recordLoanPayment()` to intelligently allocate the `legacy_paid` amount across pending and overdue repayments in installment order.

2. **Audit Trail**: Creates `PaymentAllocation` records to track which repayments received which portions of the historical payment.

3. **Total Repaid Accessor**: The Loan model's `total_repaid` accessor returns:
   - `legacy_paid` if `legacy_import = true`
   - `repayments()->sum('amount_paid')` otherwise

This ensures all views and reports show the correct authoritative paid amount for legacy-imported loans.

### Storage

Add the `legacy_paid` field to preserve the historical CSV `Paid` value:

```php
Schema::table('loans', function (Blueprint $table) {
    if (! Schema::hasColumn('loans', 'legacy_paid')) {
        $table->decimal('legacy_paid', 15, 2)->default(0.00)->after('legacy_import');
    }
});
```

Add to Loan model:

```php
protected $fillable = [
    // ...
    'legacy_import',
    'legacy_paid',
    // ...
];

protected $casts = [
    'legacy_import' => 'boolean',
    'legacy_paid' => 'decimal:2',
];

public function getTotalRepaidAttribute(): float
{
    // For legacy imports: use historical snapshot
    if ((bool) ($this->legacy_import ?? false) && Schema::hasColumn('loans', 'legacy_paid')) {
        return (float) ($this->legacy_paid ?? 0);
    }
    // For normal loans: calculate from live repayments
    return (float) $this->repayments()->sum('amount_paid');
}
```

### Idempotency

To prevent duplicate allocations when the import runs multiple times on the same data, the importer checks for existing payment allocations before re-allocating:

```php
if ($paid > 0) {
    // Only allocate if no historical payment allocations exist yet
    $existingAllocations = PaymentAllocation::where('loan_id', $loan->id)
        ->where('batch_reference', 'like', '%Historical%')
        ->sum('amount');
    
    if ($existingAllocations == 0) {
        $this->applyHistoricalPayment($loan, $paid);
    }
}
```

This ensures that re-importing the same CSV data does not create duplicate payment records.

---

## Implementation pattern

### 1) Migration

Add two non-destructive migrations:

**Migration 1: Add legacy_import flag**
```php
Schema::table('loans', function (Blueprint $table) {
    if (! Schema::hasColumn('loans', 'legacy_import')) {
        $table->boolean('legacy_import')->default(false)->after('status');
    }
});
```

**Migration 2: Add legacy_paid snapshot field**
```php
Schema::table('loans', function (Blueprint $table) {
    if (! Schema::hasColumn('loans', 'legacy_paid')) {
        $table->decimal('legacy_paid', 15, 2)->default(0.00)->after('legacy_import');
    }
});
```

Both migrations are intentionally non-destructive and safe to apply on an existing database when preceded by a backup.

### 2) Loan model

Add the field to the model fillable list and cast it to boolean:

```php
protected $fillable = [
    // ...
    'status',
    'legacy_import',
    // ...
];

protected $casts = [
    'legacy_import' => 'boolean',
];
```

### 3) Legacy importer

When a loan is created via the legacy import flow:

```php
$loan = Loan::create([
    'user_id' => $user->id,
    'tier_id' => $tier->id,
    'amount' => $principal,
    'legacy_import' => true,  // Mark as legacy-imported
    // ... other fields
]);

// After loan is created, set the historical paid amount
if ($paid > 0) {
    $loan->update(['legacy_paid' => round((float) $paid, 2)]);
    
    // Allocate the paid amount across repayments (idempotent)
    $existingAllocations = PaymentAllocation::where('loan_id', $loan->id)
        ->where('batch_reference', 'like', '%Historical%')
        ->sum('amount');
    
    if ($existingAllocations == 0) {
        $this->repaymentService->recordLoanPayment($loan, $paid, 'Historical import payment');
    }
}
```

For standard app-created loans, leave `legacy_import` untouched so it stays `false`.

### 4) Reconciliation and display safeguard

All calculations of total repaid must use the legacy-aware accessor instead of direct repayment sums:

**In Controllers:**
```php
$totalRepaid = $loan->total_repaid;  // Uses legacy_paid for legacy imports
$outstandingBalance = $loan->total_payable - $totalRepaid;
```

**In Blade Views:**
```blade
{{ number_format($loan->total_repaid, 2) }}  <!-- Legacy-aware -->
```

**In Resources/APIs:**
```php
'total_paid' => $this->total_repaid,  <!-- Uses accessor -->
```

This ensures:
- Legacy-imported loans consistently display their historical `legacy_paid` amount
- Normal loans continue to use live repayment calculations
- All display layers (admin, user, officer, API) show the correct value
- No manual conditionals scattered throughout the codebase

---

## Safe rollout

The rollout should be staged as follows:

1. Add the migration.
2. Deploy the model and importer changes.
3. Run a dry validation on a small sample of legacy-imported records.
4. Confirm that `legacy_import = true` loans keep CSV `Paid` as the authoritative paid value.
5. Only then extend to full production usage.

---

## Acceptance criteria

- a new `legacy_import` column exists on `loans` (boolean flag)
- a new `legacy_paid` column exists on `loans` (decimal snapshot field)
- legacy-imported loans are flagged as `1` or `true`
- regular loans remain `0` or `false`
- the CSV `Paid` value is preserved in `legacy_paid` for legacy-imported loans
- legacy paid amounts are distributed across the repayment schedule via `PaymentAllocation` records
- subsequent API/C2B payments allocated to a legacy loan increase `legacy_paid` by the allocated loan amount
- all displays use the `total_repaid` accessor instead of direct `repayments()->sum()`
- the CSV `Paid` value remains the authoritative figure for legacy-imported loans (not recalculated)
- re-importing the same CSV data does not create duplicate payment allocations (idempotent)
- normal repayment logic remains unchanged for non-legacy loans

---

## Implementation note

This feature is intentionally narrow and explicit. It does not change the standard loan lifecycle for normal loans; it only adds a legacy-import control point for historical CSV records.
