# Task 16 — Payments & Collections

## Goal
Record customer payments against invoices, post them to finance.
Support cash, bank transfer, and cheque payment methods.
This task creates the payment record and links to finance posting (Task 19).

---

## Migration: `sales_payments`

```php
Schema::create('sales_payments', function (Blueprint $table) {
    $table->id();
    $table->string('payment_number')->unique();
    $table->date('payment_date');
    $table->unsignedBigInteger('customer_id');
    $table->unsignedBigInteger('invoice_id');
    $table->unsignedBigInteger('branch_id');
    $table->decimal('amount', 18, 2);
    $table->unsignedBigInteger('payment_method_id');  // finance_payment_methods
    $table->string('bank_reference')->nullable();      // cheque number or transfer ref
    $table->unsignedBigInteger('journal_entry_id')->nullable();
    $table->timestamp('posted_at')->nullable();
    $table->enum('status', ['draft', 'posted', 'reversed'])->default('draft');
    $table->text('notes')->nullable();
    $table->unsignedBigInteger('created_by')->nullable();
    $table->unsignedBigInteger('updated_by')->nullable();
    $table->timestamps();

    $table->foreign('customer_id')->references('id')->on('customers');
    $table->foreign('invoice_id')->references('id')->on('invoices');
    $table->foreign('branch_id')->references('id')->on('branches');
    $table->foreign('payment_method_id')->references('id')->on('finance_payment_methods');
    $table->foreign('journal_entry_id')->references('id')->on('journal_entries')->nullOnDelete();
    $table->index(['customer_id', 'status']);
    $table->index(['invoice_id', 'status']);
    $table->index('payment_date');
});
```

---

## Model: `app/Models/Sales/Payment/SalesPayment.php`

```php
protected $fillable = [
    'payment_number', 'payment_date', 'customer_id', 'invoice_id', 'branch_id',
    'amount', 'payment_method_id', 'bank_reference', 'journal_entry_id',
    'posted_at', 'status', 'notes', 'created_by', 'updated_by',
];

protected $casts = [
    'payment_date' => 'date',
    'amount'       => 'decimal:2',
    'posted_at'    => 'datetime',
];

public function customer(): BelongsTo { return $this->belongsTo(Customer::class); }
public function invoice(): BelongsTo { return $this->belongsTo(Invoice::class); }
public function paymentMethod(): BelongsTo { return $this->belongsTo(FinancePaymentMethod::class, 'payment_method_id'); }
public function journalEntry(): BelongsTo { return $this->belongsTo(JournalEntry::class); }
```

---

## Service: `app/Services/Sales/Payment/SalesPaymentService.php`

### `create(array $data): SalesPayment`
```php
1. Validate invoice is posted (cannot pay unposted invoice)
2. Validate amount > 0
3. Validate amount <= invoice.balance_amount (or allow overpayment with flag)
4. Generate payment_number via NumberSequenceService (module=sales, document=payment)
5. Set status = draft
6. Create payment
```

### `post(SalesPayment $payment): SalesPayment`
```php
1. Validate status = draft
2. Validate fiscal period open for payment_date
3. Call SalesPostingService::postPayment($payment)  ← creates journal entry (Task 19)
4. Set status = posted, posted_at = now()
5. Call InvoiceService::updatePaymentStatus($payment->invoice)
6. Return updated payment
```

### `reverse(SalesPayment $payment): SalesPayment`
```php
1. Validate status = posted
2. Reverse the journal entry
3. Set status = reversed
4. Call InvoiceService::updatePaymentStatus($payment->invoice)  ← re-opens invoice balance
```

### Validation: Amount vs Balance
```php
if ($data['amount'] > $invoice->balance_amount) {
    // Two options:
    // 1. Reject with error "Amount exceeds outstanding balance"
    // 2. Allow and mark overpayment as credit (advanced)
    // For MVP: reject
    throw new \Exception("Payment amount ({$data['amount']}) exceeds invoice balance ({$invoice->balance_amount})");
}
```

---

## Controller: `app/Http/Controllers/Admin/Sales/SalesPaymentController.php`

```php
// Quick payment from invoice show page
public function createForInvoice(Invoice $invoice): View
{
    return view('dashboard.admin.sales.payments.create', [
        'invoice'        => $invoice,
        'customer'       => $invoice->customer,
        'payment_methods'=> FinancePaymentMethod::active()->get(),
        'max_amount'     => $invoice->balance_amount,
    ]);
}
```

---

## Views

Path: `resources/views/dashboard/admin/sales/payments/`
- `index.blade.php` — all payments, filterable by customer/date/status
- `create.blade.php` — payment form (pre-filled with invoice data)
- `show.blade.php` — payment details + Post / Reverse buttons + journal entry link

---

## Permissions
```
sales.payments.view
sales.payments.create
sales.payments.post
sales.payments.reverse
```

---

## Routes
```php
Route::group(['prefix' => 'sales/payments', 'as' => 'sales.payments.'], function () {
    Route::get('/', [SalesPaymentController::class, 'index'])->name('index');
    Route::get('/create', [SalesPaymentController::class, 'create'])->name('create');
    Route::get('/invoice/{invoice}/create', [SalesPaymentController::class, 'createForInvoice'])->name('create-for-invoice');
    Route::post('/', [SalesPaymentController::class, 'store'])->name('store');
    Route::get('/{payment}', [SalesPaymentController::class, 'show'])->name('show');
    Route::post('/{payment}/post', [SalesPaymentController::class, 'post'])->name('post');
    Route::post('/{payment}/reverse', [SalesPaymentController::class, 'reverse'])->name('reverse');
});
```

---

## Test This Task

1. Post an invoice (Task 15 + Task 19 must be done first)
2. Create a payment for 50% of the invoice → verify invoice.payment_status = partial
3. Post the payment → verify journal entry created
4. Create second payment for remaining balance → verify invoice.payment_status = paid
5. Reverse first payment → verify invoice.balance_amount increases back
6. Try to pay more than balance → verify rejection
