# ✅ FINAL PAYMENT FLOW - Invoice-Based Pricing

## Complete Flow Confirmed

### The Correct Flow:

```
1. Customer Accepts Quotation
   ↓
2. Sales Order Created in Odoo
   ↓
3. Invoice Generated in Odoo (with pricing from Odoo)
   ↓
4. Invoice Synced to Portal Database
   ↓
5. Customer Sees Invoice with Amount
   ↓
6. Customer Chooses Payment Method
   ├─ Pesepay: Amount = invoice.total_amount
   └─ Bank Transfer: Amount = invoice.total_amount
```

---

## Pricing Source: INVOICE (Not Quotation, Not Manual Entry)

### Where Prices Come From:

1. **Quotation Stage**:
   - Portal calculates estimated prices (can be simulated if `SIMULATE_PACKAGE_PRICING=true`)
   - Quotation sent to Odoo
   - Odoo team reviews and adjusts pricing

2. **Sales Order Stage**:
   - Quotation converted to sales order in Odoo
   - Odoo pricing is now locked

3. **Invoice Stage** ✅ THIS IS THE SOURCE:
   - Invoice generated from sales order in Odoo
   - Invoice contains FINAL pricing from Odoo
   - `invoice.total_amount` = The amount customer must pay
   - This amount includes:
     - Base price
     - Installation fees
     - Equipment costs
     - Extension costs (if any)
     - Taxes
     - Any Odoo adjustments

4. **Payment Stage**:
   - Amount is READ from invoice
   - Amount is NOT editable by customer
   - Amount is NOT manually entered
   - Amount comes directly from `invoice.total_amount`

---

## Code Implementation

### 1. Invoice Generation (QuotationController.php)

```php
// After sales order is created in Odoo
$invoiceId = $odooService->generateInvoice($odooOrderId);
$invoiceDetails = $odooService->getInvoice($invoiceId);

// Create local invoice with Odoo pricing
$invoice = Invoice::create([
    'invoice_number' => $invoiceDetails['name'],
    'customer_id' => $customer->id,
    'sales_order_id' => $salesOrder->id,
    'amount' => $invoiceDetails['amount_total'] - ($invoiceDetails['amount_tax'] ?? 0),
    'tax_amount' => $invoiceDetails['amount_tax'] ?? 0,
    'total_amount' => $invoiceDetails['amount_total'],  // ✅ FINAL PRICE FROM ODOO
    'status' => 'sent',
    'invoice_date' => $invoiceDetails['invoice_date'] ?? now(),
    'due_date' => $invoiceDetails['invoice_date_due'] ?? now()->addDays(7),
    'odoo_invoice_id' => $invoiceId,
    'odoo_synced' => true,
]);
```

### 2. Pesepay Payment (PaymentController.php)

```php
public function initiatePayment(Request $request)
{
    $validator = Validator::make($request->all(), [
        'invoice_id' => 'required|exists:invoices,id',  // ✅ REQUIRES INVOICE
        'amount' => 'required|numeric|min:0.01',
        'payment_method' => 'required|in:pesepay',
    ]);

    $invoice = Invoice::where('id', $request->invoice_id)
        ->where('customer_id', $customer->id)
        ->first();

    // Validate amount matches invoice
    if ($request->amount != $invoice->total_amount) {
        return response()->json([
            'success' => false,
            'message' => 'Payment amount must match invoice amount',
        ], 400);
    }

    // Create payment with invoice amount
    $payment = Payment::create([
        'invoice_id' => $invoice->id,
        'amount' => $invoice->total_amount,  // ✅ AMOUNT FROM INVOICE
        // ...
    ]);
}
```

### 3. Bank Transfer Payment (PaymentController.php)

```php
public function uploadProofOfPayment(Request $request)
{
    $validator = Validator::make($request->all(), [
        'invoice_id' => 'required|exists:invoices,id',  // ✅ REQUIRES INVOICE
        'amount' => 'required|numeric|min:0.01',
        // ...
    ]);

    $invoice = Invoice::where('id', $request->invoice_id)
        ->where('customer_id', $customer->id)
        ->first();

    // Validate amount matches invoice
    if ($request->amount != $invoice->total_amount) {
        return response()->json([
            'success' => false,
            'message' => 'Payment amount must match invoice amount',
        ], 400);
    }

    // Create payment with invoice amount
    $payment = Payment::create([
        'invoice_id' => $invoice->id,
        'amount' => $invoice->total_amount,  // ✅ AMOUNT FROM INVOICE
        // ...
    ]);
}
```

### 4. Frontend - Pesepay Payment (PaymentOptions.jsx)

```jsx
const handlePesepayPayment = async () => {
  const response = await api.payments.initiate({
    invoice_id: invoice.id,           // ✅ INVOICE ID
    amount: invoice.total_amount,     // ✅ AMOUNT FROM INVOICE
    payment_method: 'pesepay',
  });

  if (response.success) {
    window.location.href = response.data.redirect_url;
  }
};
```

### 5. Frontend - Bank Transfer (ProofOfPaymentUpload.jsx)

```jsx
// Amount is pre-filled and read-only
const [formData, setFormData] = useState({
  payment_reference: '',
  payment_date: new Date().toISOString().split('T')[0],
  amount: invoice?.total_amount || '',  // ✅ PRE-FILLED FROM INVOICE
  notes: '',
});

// Amount field is read-only
<input
  type="number"
  name="amount"
  value={formData.amount}
  readOnly  // ✅ CANNOT BE CHANGED
  className="bg-gray-50 cursor-not-allowed"
/>
```

---

## Simulated Pricing Integration

### Environment Variable:
```env
SIMULATE_PACKAGE_PRICING=true
```

### How It Works:

1. **Quotation Stage** (Portal):
   - If `SIMULATE_PACKAGE_PRICING=true`:
     - Portal generates estimated prices for display
     - Prices are shown to customer for quotation
   - If `SIMULATE_PACKAGE_PRICING=false`:
     - Only "Request Quote" is shown
     - No prices displayed until Odoo provides them

2. **Odoo Stage**:
   - Quotation sent to Odoo (with or without simulated prices)
   - Odoo team reviews and sets FINAL pricing
   - Odoo pricing overrides any simulated prices

3. **Invoice Stage** ✅ FINAL PRICING:
   - Invoice generated from Odoo sales order
   - Invoice contains REAL pricing from Odoo
   - Simulated prices are no longer relevant
   - Customer pays the INVOICE amount (Odoo pricing)

### Key Point:
**Simulated pricing is ONLY for quotation display. Invoice pricing ALWAYS comes from Odoo.**

---

## Payment Amount Validation

### Backend Validation:

```php
// In both initiatePayment() and uploadProofOfPayment()

// Get invoice
$invoice = Invoice::find($request->invoice_id);

// Validate amount matches invoice
if (abs($request->amount - $invoice->total_amount) > 0.01) {  // Allow 1 cent tolerance
    return response()->json([
        'success' => false,
        'message' => 'Payment amount must match invoice amount of $' . number_format($invoice->total_amount, 2),
    ], 400);
}
```

### Frontend Prevention:

```jsx
// Amount field is read-only
<input
  type="number"
  value={invoice.total_amount}
  readOnly
  className="bg-gray-50 cursor-not-allowed"
/>

// API call uses invoice amount
api.payments.initiate({
  invoice_id: invoice.id,
  amount: invoice.total_amount,  // Not user input
});
```

---

## API Request/Response Examples

### 1. After Quotation Acceptance:

**Response:**
```json
{
  "success": true,
  "data": {
    "quotation": {
      "id": 123,
      "total_amount": 1500.00  // Simulated or estimated
    },
    "sales_order": {
      "id": 456,
      "total_amount": 1500.00
    },
    "invoice": {
      "id": 789,
      "invoice_number": "INV/2026/0001",
      "total_amount": 1650.00,  // ✅ FINAL PRICE FROM ODOO (may differ from quotation)
      "amount": 1500.00,
      "tax_amount": 150.00,
      "status": "sent",
      "due_date": "2026-02-23"
    }
  }
}
```

### 2. Pesepay Payment Request:

**Request:**
```json
{
  "invoice_id": 789,
  "amount": 1650.00,  // Must match invoice.total_amount
  "payment_method": "pesepay"
}
```

**Response:**
```json
{
  "success": true,
  "data": {
    "payment_id": 999,
    "redirect_url": "https://pesepay.com/pay/...",
    "invoice": {
      "id": 789,
      "invoice_number": "INV/2026/0001",
      "amount": 1650.00
    }
  }
}
```

### 3. Bank Transfer POP Upload:

**Request (FormData):**
```
invoice_id: 789
amount: 1650.00  // Must match invoice.total_amount
payment_reference: "BANK-REF-12345"
payment_date: "2026-02-16"
proof_file: <file>
notes: "Payment via Standard Bank"
```

**Response:**
```json
{
  "success": true,
  "message": "Proof of payment uploaded successfully...",
  "data": {
    "payment_id": 999,
    "payment_reference": "BANK-REF-12345",
    "status": "pending",
    "invoice": {
      "id": 789,
      "invoice_number": "INV/2026/0001",
      "amount": 1650.00
    }
  }
}
```

---

## Summary

### ✅ Pricing Source Hierarchy:

1. **Quotation**: Estimated/simulated prices (for display only)
2. **Sales Order**: Odoo pricing (locked)
3. **Invoice**: FINAL pricing from Odoo ✅ THIS IS THE SOURCE
4. **Payment**: Amount = Invoice amount (not editable)

### ✅ Key Rules:

1. **Customers CANNOT pay without an invoice**
2. **Payment amount MUST match invoice amount**
3. **Invoice amount comes from Odoo (not portal)**
4. **Simulated pricing is ONLY for quotation display**
5. **Final pricing is ALWAYS from Odoo invoice**

### ✅ Benefits:

1. **Accurate pricing**: Always uses Odoo's final pricing
2. **No discrepancies**: Payment amount matches invoice exactly
3. **Proper accounting**: Invoice is the billing document
4. **Audit trail**: Complete tracking from quotation to payment
5. **Odoo control**: Pricing is managed in Odoo, not portal

---

## Testing Checklist

- [ ] Quotation with simulated pricing creates correct invoice
- [ ] Invoice amount comes from Odoo (not quotation)
- [ ] Pesepay payment uses invoice amount
- [ ] Bank transfer uses invoice amount
- [ ] Amount field is read-only in POP upload
- [ ] Payment validation rejects incorrect amounts
- [ ] Invoice amount includes all fees and taxes
- [ ] Multiple payments can be made for one invoice (partial payments)

---

## 🎉 CONFIRMED

**YES, payment amount comes from the invoice.**

**YES, invoice pricing comes from Odoo.**

**YES, simulated pricing is only for quotation display.**

**YES, customers cannot pay without an invoice.**

**All pricing flows through the invoice!** ✅
