# Task 15 — Invoice Expansion

## Goal
Expand invoices with due date, per-line discounts/tax, payment status tracking, and posting readiness.
This task does NOT include finance posting — that is Task 19.

---

## Migration: Alter `invoices`

```php
Schema::table('invoices', function (Blueprint $table) {
    $table->date('due_date')->nullable()->after('invoice_date');
    $table->decimal('discount_amount', 18, 2)->default(0)->after('tax_amount');
    $table->decimal('paid_amount', 18, 2)->default(0)->after('discount_amount');
    $table->decimal('balance_amount', 18, 2)->default(0)->after('paid_amount');
    $table->enum('payment_status', ['unpaid','partial','paid','overdue'])->default('unpaid')->after('balance_amount');
    $table->unsignedBigInteger('journal_entry_id')->nullable()->after('payment_status');
    $table->timestamp('posted_at')->nullable()->after('journal_entry_id');
    $table->string('delivery_note_ref')->nullable()->after('posted_at');

    $table->foreign('journal_entry_id')->references('id')->on('journal_entries')->nullOnDelete();
    $table->index(['customer_id', 'payment_status']);
    $table->index(['due_date', 'payment_status']);
    $table->index('posted_at');
});
```

## Migration: Alter `invoice_lines`

```php
Schema::table('invoice_lines', function (Blueprint $table) {
    $table->decimal('discount_percent', 5, 2)->default(0)->after('unit_price');
    $table->decimal('discount_amount', 18, 2)->default(0)->after('discount_percent');
    $table->unsignedBigInteger('tax_rate_id')->nullable()->after('discount_amount');
    $table->decimal('tax_amount', 18, 2)->default(0)->after('tax_rate_id');
    $table->decimal('line_subtotal', 18, 2)->default(0)->after('tax_amount');
    $table->unsignedBigInteger('warehouse_id')->nullable()->after('line_subtotal');
    $table->decimal('cogs_amount', 18, 2)->default(0)->after('warehouse_id');

    $table->foreign('tax_rate_id')->references('id')->on('sales_tax_rates')->nullOnDelete();
    $table->foreign('warehouse_id')->references('id')->on('warehouses')->nullOnDelete();
});
```

---

## Update Model: `app/Models/Sales/Invoice.php`

Add to `$fillable`:
```php
'due_date', 'discount_amount', 'paid_amount', 'balance_amount',
'payment_status', 'journal_entry_id', 'posted_at', 'delivery_note_ref',
```

Add to `$casts`:
```php
'due_date'       => 'date',
'paid_amount'    => 'decimal:2',
'balance_amount' => 'decimal:2',
'posted_at'      => 'datetime',
```

Add relationships:
```php
public function journalEntry(): BelongsTo { return $this->belongsTo(JournalEntry::class); }
public function payments(): HasMany { return $this->hasMany(SalesPayment::class); }
public function returns(): HasMany { return $this->hasMany(SalesReturn::class, 'original_invoice_id'); }
```

Add helper methods:
```php
public function isPosted(): bool { return $this->status === 'posted'; }
public function isEditable(): bool { return in_array($this->status, ['draft']); }
public function isPaid(): bool { return $this->payment_status === 'paid'; }
public function isOverdue(): bool {
    return $this->due_date && $this->due_date->isPast() && $this->balance_amount > 0;
}
```

---

## Update Service: `app/Services/Sales/InvoiceService.php`

### `create(array $data): Invoice`

Extended logic:
```php
1. Calculate line totals (same as sales order: subtotal, discount, tax, line_total)
2. Calculate invoice totals (subtotal, discount, tax, total)
3. Set due_date = invoice_date + customer.payment_terms_days
4. Set balance_amount = total_amount
5. Set payment_status = 'unpaid'
6. Check credit limit (CustomerService::checkCreditLimit)
7. Create invoice + lines
8. Update sales_order.invoiced_amount (if sales_order_id set)
```

### `confirm(Invoice $invoice): Invoice`
```php
if ($invoice->status !== 'draft') throw new \Exception('...');
$invoice->update(['status' => 'confirmed']);
return $invoice->fresh();
```

### `updatePaymentStatus(Invoice $invoice): void`
Called after every payment is posted/reversed.
```php
public function updatePaymentStatus(Invoice $invoice): void
{
    $totalPaid = $invoice->payments()->where('status', 'posted')->sum('amount');
    $balance   = (float) $invoice->total_amount - $totalPaid;

    $paymentStatus = match (true) {
        $totalPaid <= 0 => $invoice->isOverdue() ? 'overdue' : 'unpaid',
        $balance <= 0   => 'paid',
        default         => 'partial',
    };

    $invoiceStatus = match ($paymentStatus) {
        'paid'    => 'paid',
        'partial' => 'partially_paid',
        default   => $invoice->status,
    };

    $invoice->update([
        'paid_amount'    => $totalPaid,
        'balance_amount' => max(0, $balance),
        'payment_status' => $paymentStatus,
        'status'         => $invoiceStatus,
    ]);
}
```

---

## Status Machine (Invoice)
```
draft ──confirm──► confirmed ──post──► posted ──partial payment──► partially_paid ──► paid
  └──cancel──► cancelled
```

**Rules:**
- Cannot edit confirmed or posted invoice
- Cannot cancel posted invoice — must reverse journal entry first
- Cannot delete if payments exist

---

## Due Date Logic
- Default: `invoice_date + customer.payment_terms_days`
- Override: allow manual override in form
- Overdue check: scheduled job updates `payment_status = overdue` daily for unpaid invoices past due_date

---

## Update Views

Create/edit form additions:
- Due Date field (pre-populated, overridable)
- Per-line: Discount %, Tax Rate dropdown, warehouse selector (for stock items)
- Invoice summary footer: subtotal, discount, tax, **total**
- JS auto-calculation of totals

Show page additions:
- Payment status badge (color coded)
- Balance remaining
- Due date with overdue warning
- Payments tab (list of payments)
- Post button (confirmed status only)
- "Receive Payment" quick action button
