# Unified Bill Payment System

## Overview

The unified bill payment system consolidates all bill payment scenarios (full, installment, single, multiple, mixed) into a single, cohesive transaction-safe function. It supports all payment modes (balance, bank transfer, cash, virtual account) and handles complex workflows like PSB flow advancement and optional service subscriptions.

---

## Architecture

### Service Layer Hierarchy

```
TransactionService::payBills()                    [Orchestrator]
    ├── BillService::loadBillsForPayment()        [Load bills with relations]
    ├── BillService::categorizeBills()            [Metadata building]
    ├── BillService::payFull()                    [Update bill + BillTransaction]
    ├── BillService::payInstallment()             [Installment update + BillTransaction]
    ├── BillService::handlePsbFlowAfterPayment()  [PSB advancement + cleanup]
    ├── BillService::markOptionalServicesAsPaid() [Update subscriptions]
    ├── TransactionService::generateTransactionNumber()
    ├── TransactionService::uploadReceiptImage()
    ├── TransactionService::resolveCoaTabungan()
    ├── TransactionService::buildJournalEntriesForBill()
    ├── TransactionService::createPaymentBankSettlementJournals()
    ├── TransactionService::deductBalanceAndRecordMutation()
    ├── TransactionService::handleVaPaymentCleanup()
    ├── TransactionService::logBillPayment()
    ├── TransactionService::sendBillPaymentNotification()
    └── TransactionService::sendInstallmentPaymentNotification()
```

### Transaction Safety

- **Database Transaction Guard**: `DB::beginTransaction()` → `DB::commit()` with `DB::rollBack()` on any `\Throwable`
- **Idempotency**: Route protected by `idempotent` middleware
- **WA Notification**: Runs after commit, failures are logged but don't rollback the payment

---

## API Endpoint

### POST `/api/bill/pay`

Unified endpoint for all bill payment scenarios.

**Middleware**: `idempotent` (prevents duplicate submissions)

**Authentication**: Required (Bearer token)

---

## Request Format

### Headers
```
Content-Type: application/json
Authorization: Bearer {token}
```

### Body

```json
{
  "bill_payments": [
    {
      "bill_id": integer,
      "amount": integer | null
    },
    ...
  ],
  "transaction_mode_value": integer,
  "admin_employee_id": integer,
  "balance_id": integer | null,
  "coa_bank_id": integer | null,
  "coa_kas_id": integer | null,
  "additionalInfo": string | null,
  "transactionDate": string (Y-m-d) | null,
  "file": UploadedFile | null
}
```

### Field Documentation

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `bill_payments` | array | ✓ | Array of payment instructions. Min 1 item. |
| `bill_payments[].bill_id` | int | ✓ | Bill ID to pay. Must exist in bills table. |
| `bill_payments[].amount` | int | ✗ | Amount to pay. Omit or null = pay full remaining. If amount ≥ remaining, treated as full pay. |
| `transaction_mode_value` | int | ✓ | Payment mode: 1=Balance, 2=Cash/Bank, 3=VA |
| `admin_employee_id` | int | ✓ | Employee ID processing the payment |
| `balance_id` | int | ✗ | Balance ID (required if mode=1) |
| `coa_bank_id` | int | ✗ | Bank COA ID (required if mode=2 and bank payment) |
| `coa_kas_id` | int | ✗ | Cash COA ID (required if mode=2 and cash payment) |
| `additionalInfo` | string | ✗ | Additional notes (max 250 chars) |
| `transactionDate` | string | ✗ | Transaction date. Defaults to today (Y-m-d format) |
| `file` | file | ✗ | Receipt image (jpg/png) |

---

## Response Format

### Success (200 OK)

```json
{
  "message": "success",
  "transaction_id": 12345
}
```

### Validation Error (422 Unprocessable Entity)

```json
{
  "message": "Validation failed",
  "errors": {
    "bill_payments": ["The bill payments field is required"],
    "bill_payments.*.bill_id": ["The bill_payments.0.bill_id must be an integer"]
  }
}
```

### Business Logic Error (400 Bad Request)

```json
{
  "message": "Saldo tidak mencukupi",
  "error": "Saldo tidak mencukupi",
  "required": 1500000,
  "available": 500000
}
```

Or:

```json
{
  "message": "Bills not found: 999, 1000",
  "error": "Bills not found: 999, 1000"
}
```

Or:

```json
{
  "message": "Bill #1 is already fully paid.",
  "error": "Bill #1 is already fully paid."
}
```

### System Error (500 Internal Server Error)

```json
{
  "message": "Payment failed",
  "error": "Error message details..."
}
```

---

## Request Scenarios

### Scenario 1: Pay Full (Single Bill)

**Use Case**: Student pays in full for one bill

```json
{
  "bill_payments": [
    {
      "bill_id": 1
    }
  ],
  "transaction_mode_value": 2,
  "admin_employee_id": 5,
  "coa_kas_id": 10,
  "additionalInfo": "Pembayaran tunai kas"
}
```

**Process**:
1. Load bill #1
2. Calculate remaining: `amount - paidAmount`
3. Update bill: `paidAmount = amount`, `pay_status_value = LUNAS`
4. Create BillTransaction with `outstanding = 0`
5. Create journal: credit piutang, debit kas
6. Mark optional services as paid
7. Log transaction

---

### Scenario 2: Pay Full (Multiple Bills)

**Use Case**: Student pays multiple bills in one transaction

```json
{
  "bill_payments": [
    { "bill_id": 1 },
    { "bill_id": 2 },
    { "bill_id": 3 }
  ],
  "transaction_mode_value": 2,
  "admin_employee_id": 5,
  "coa_bank_id": 15,
  "additionalInfo": "Transfer bank BCA"
}
```

**Process**: Same as Scenario 1, repeated for each bill

---

### Scenario 3: Pay Installment (Single Bill)

**Use Case**: Student pays partial amount for one bill

```json
{
  "bill_payments": [
    {
      "bill_id": 1,
      "amount": 250000
    }
  ],
  "transaction_mode_value": 2,
  "admin_employee_id": 5,
  "coa_kas_id": 10,
  "additionalInfo": "Cicilan pertama"
}
```

**Process**:
1. Load bill #1
2. Check if 250000 < remaining
3. Update bill: `paidAmount += 250000`
4. If now fully paid: `pay_status_value = LUNAS`, mark optional services
5. Else: `pay_status_value = CICIL`
6. Handle PSB flow (advance if fully paid OR if `bill_type.isCicil = 1`)
7. Create journal entries

---

### Scenario 4: Mixed Full + Installment

**Use Case**: Pay multiple bills, some full, some partial

```json
{
  "bill_payments": [
    { "bill_id": 1 },
    { "bill_id": 2, "amount": 500000 },
    { "bill_id": 3 }
  ],
  "transaction_mode_value": 2,
  "admin_employee_id": 5,
  "coa_kas_id": 10,
  "additionalInfo": "Pembayaran campuran"
}
```

**Process**: For each bill, determine if full or installment, then process accordingly

---

### Scenario 5: Pay from Student Balance

**Use Case**: Student pays using their balance

```json
{
  "bill_payments": [
    { "bill_id": 1 },
    { "bill_id": 2, "amount": 100000 }
  ],
  "transaction_mode_value": 1,
  "admin_employee_id": 5,
  "balance_id": 25,
  "additionalInfo": "Bayar dari saldo siswa"
}
```

**Validation**:
- Balance must have sufficient funds
- Total needed = sum of actual payments
- Returns 400 if insufficient with details

**Process**:
1. Validate balance sufficiency
2. Process bills
3. Deduct from balance
4. Create balance mutation record
5. Journal: credit piutang, debit tabungan siswa

---

### Scenario 6: With Receipt File & Custom Date

**Use Case**: Offline payment with receipt photo

```bash
curl -X POST http://localhost:8000/api/bill/pay \
  -H "Authorization: Bearer {token}" \
  -F "bill_payments=[{\"bill_id\":1}]" \
  -F "transaction_mode_value=2" \
  -F "admin_employee_id=5" \
  -F "coa_kas_id=10" \
  -F "additionalInfo=Pembayaran tunai" \
  -F "transactionDate=2025-02-28" \
  -F "file=@receipt.jpg"
```

**Process**:
1. Upload image to `public/receipts/{trxNumber}.jpg`
2. Resize to 300px wide, aspect ratio maintained
3. Store path in transaction record

---

## Payment Modes

### Mode 1: Balance (`TRANSACTION_MODE_BALANCE`)

```json
{
  "bill_payments": [...],
  "transaction_mode_value": 1,
  "balance_id": 25,
  ...
}
```

**Validation**: Balance must have `amount >= totalPayment`

**Journals Created**:
- Credit: Piutang (per bill)
- Debit: Tabungan Siswa

**Side Effects**:
- `balance.balance` decremented
- `BalanceMutation` record created

---

### Mode 2: Cash/Bank (`TRANSACTION_MODE_CASH` / `TRANSACTION_MODE_BANK`)

```json
{
  "bill_payments": [...],
  "transaction_mode_value": 2,
  "coa_kas_id": 10,
  ...
}
```

Or with bank:

```json
{
  "bill_payments": [...],
  "transaction_mode_value": 2,
  "coa_bank_id": 15,
  ...
}
```

**Journals Created**:
- Credit: Piutang (per bill)
- Debit: Kas or Bank COA

---

### Mode 3: Virtual Account (`TRANSACTION_MODE_VA`)

```json
{
  "bill_payments": [...],
  "transaction_mode_value": 3,
  "payment_id": 999,
  ...
}
```

**Journals Created**:
- Credit: Piutang (per bill)
- Debit: Bank (per settlement in payment)

**Side Effects**:
- Duplicate VA payments cleaned up
- Bank settlement journals created

---

## Service Methods

### BillService

#### `loadBillsForPayment(array $billIds): Collection`

Loads bills with all required relations for payment processing.

```php
$bills = $billService->loadBillsForPayment([1, 2, 3]);
```

**Relations Loaded**:
- `bill_type`
- `student` (name, phone, parents)
- `student.parallel` (class)
- `new_student` (PSB student, name, parents, school)
- `new_student.school`

---

#### `categorizeBills(Collection $allBills): array`

Categorizes bills and builds transaction metadata.

**Returns**:
```php
[
  'cashBills' => [],              // Bills with pay_status_value === 0
  'installmentBills' => [],       // Bills with pay_status_value !== 0
  'totalBill' => 1500000,
  'student_id' => 123,
  'new_student_id' => 456,
  'school_id' => 10,
  'user_type_value' => 2,
  'parent_ids' => [1, 2, 3],
  'new_parent_ids' => [5, 6],
  'studentNames' => ['Adi (X-A)', 'Budi (XI-B)'],
  'monthBills' => ['Januari', 'Februari'],
  'stringBillTypes' => '- Uang Sekolah Januari 2025 Rp500.000<br>...',
  'description' => '- Adi (X-A) Uang Sekolah ... Rp500.000 ...'
]
```

---

#### `payFull(Bill $bill, int $transaction_id): array`

Marks a bill as fully paid and creates BillTransaction.

```php
$result = $billService->payFull($bill, $transaction_id);
// Returns: ['pay' => 500000]
```

**DB Changes**:
- `bills.paidAmount = bill.amount`
- `bills.pay_status_value = 2` (LUNAS)
- `bills.billed_status_value = 1`
- Creates `BillTransaction`

---

#### `payInstallment(Bill $bill, int $installment, int $transaction_id): array`

Records a partial payment (installment) for a bill.

```php
$result = $billService->payInstallment($bill, 250000, $transaction_id);
// Returns: [
//   'pay' => 250000,
//   'paid' => 250000,        // (or higher if already had partial payment)
//   'outstanding' => 250000, // (or 0 if now fully paid)
//   'is_fully_paid' => false
// ]
```

**DB Changes**:
- `bills.paidAmount += installment`
- Updates `pay_status_value` (CICIL or LUNAS)
- Updates `billed_status_value` if fully paid
- Creates `BillTransaction`

---

#### `handlePsbFlowAfterPayment(Bill $bill, PsbService $psbService, array $excludeBillIds = []): bool`

Handles PSB flow advancement after full payment.

```php
$advanced = $billService->handlePsbFlowAfterPayment($bill, $psbService, [1, 2, 3]);
```

**Logic**:
- If bill has no PSB flow: returns false
- If `paymentInstruction === 1`: Advance only if all other bills in flow are paid
- If `paymentInstruction !== 1` (optional bills): Delete unpaid optional bills, always advance
- Calls `psbService->nextPsbFlow()` if advancing

**Returns**: true if PSB was advanced, false otherwise

---

#### `markOptionalServicesAsPaid(array $billIds): void`

Marks optional service subscriptions as paid for fully paid bills.

```php
$billService->markOptionalServicesAsPaid([1, 2, 3]);
```

---

### TransactionService

#### `payBills(array $billPayments, int $transaction_mode_value, ...): array`

**Signature**:
```php
public function payBills(
    array $billPayments,
    int $transaction_mode_value,
    int $admin_employee_id,
    ?Balance $balance = null,
    ?Payment $payment = null,
    ?int $coa_bank_id = null,
    ?int $coa_kas_id = null,
    ?string $additionalInfo = null,
    ?string $transactionDate = null,
    $file = null
): array
```

**Input**: `$billPayments`
```php
[
  ['bill_id' => 1, 'amount' => null],   // pay full
  ['bill_id' => 2, 'amount' => 250000], // installment
]
```

**Returns**:
```php
['transaction_id' => 12345]
```

**Execution Steps**:
1. Validate input (no duplicates, all required fields)
2. `DB::beginTransaction()`
3. Load bills, calculate actual amounts, build metadata
4. Resolve payment mode and debit COA
5. Generate transaction number, upload receipt
6. Create `Transaction` record
7. For each bill: payFull/payInstallment, build journals, handle PSB
8. Bulk insert journals, mark optional services, handle VA cleanup
9. Bank settlement journals, balance deduction, logging
10. `DB::commit()`
11. Send WA notification (non-blocking)
12. Return transaction_id

**Throws**:
- `InvalidArgumentException` — bad input
- `RuntimeException` — missing data, business rules violated
- `\Throwable` — re-thrown after rollback

---

#### `generateTransactionNumber(): string`

```php
$trxNumber = $transactionService->generateTransactionNumber();
// Returns: "1234567890123" (time() + count)
```

---

#### `uploadReceiptImage($file, string $trxNumber): ?string`

```php
$receiptUrl = $transactionService->uploadReceiptImage($file, $trxNumber);
// Returns: "receipts/1234567890123.jpg" or null
```

**Process**:
- Resizes image to 300px wide, maintains aspect ratio
- Saves to `public/receipts/`
- Returns relative URL path

---

#### `resolveCoaTabungan(int $schoolId): ?Coa`

```php
$coaTabungan = $transactionService->resolveCoaTabungan(5);
```

**Logic**:
- Looks for "Tabungan Siswa %" at school level
- Falls back to foundation-level "Tabungan Siswa"

---

#### `buildJournalEntriesForBill(string $mode, int $amount, Coa $coaPiutang, ...): array`

```php
$entries = $transactionService->buildJournalEntriesForBill(
  'balance',           // mode: 'payment', 'balance', 'bank', 'kas'
  250000,             // amount
  $coaPiutang,        // Coa object
  1,                  // tahun_id
  123,                // transaction_id
  '2025-02-28',       // today
  $coaDebit           // Coa for debit side
);
// Returns: [
//   [
//     'transaction_id' => 123,
//     'coa_id' => 5,
//     'debit' => 0,
//     'credit' => 250000,
//     ...
//   ],
//   [
//     'transaction_id' => 123,
//     'coa_id' => 10,
//     'debit' => 250000,
//     'credit' => 0,
//     ...
//   ]
// ]
```

**Mode Logic**:
- `payment` → only credit side (bank settlement journals separate)
- `balance`, `bank`, `kas` → credit + debit sides

---

#### Other helpers

- `createPaymentBankSettlementJournals(Payment $payment, ...)` — creates bank debit journals per settlement
- `deductBalanceAndRecordMutation(Balance $balance, ...)` — decrement balance + create mutation record
- `handleVaPaymentCleanup(array $billIds, Payment $payment)` — delete duplicate VA payments
- `sendBillPaymentNotification(...)` — send WA notification for full/cash payments
- `sendInstallmentPaymentNotification(...)` — send WA notification for installments
- `logBillPayment(int $transactionId, ...)` — create audit log entry

---

## PSB Flow Integration

### Full Payment with PSB

When a bill with `psb_flow_id` is paid in full:

1. Check if all other bills in the same PSB flow are paid
2. If `paymentInstruction === 1`: Advance only if all are paid
3. If `paymentInstruction !== 1`: Delete unpaid optional bills, always advance
4. Call `psbService->nextPsbFlow()` to advance to next step
5. One advancement per new_student per transaction (tracked via set)

### Installment Payment with PSB

When a bill with `psb_flow_id` is paid as installment:

1. If bill is now fully paid: Advance PSB flow
2. OR if `bill_type.isCicil === 1` (even partial payment): Advance PSB flow
3. Only advance once per new_student per transaction

---

## Error Handling

### Validation Errors (422)

- Missing required fields
- Invalid field types
- Duplicate bill IDs
- Invalid date format

### Business Logic Errors (400)

- Bill not found
- Bill already fully paid
- Insufficient balance
- Payment mode mismatch (e.g., balance_id missing when mode=1)
- No active tahun pelajaran
- BillType or COA piutang not found

### System Errors (500)

- Database errors
- File upload failures
- Any uncaught exception

**All errors rollback the transaction and return appropriate HTTP status codes.**

---

## Testing

### cURL Examples

**Scenario 1: Full pay, single bill, cash**

```bash
curl -X POST http://localhost:8000/api/bill/pay \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {token}" \
  -d '{
    "bill_payments": [
      {"bill_id": 1}
    ],
    "transaction_mode_value": 2,
    "admin_employee_id": 5,
    "coa_kas_id": 10,
    "additionalInfo": "Pembayaran tunai kas"
  }'
```

**Scenario 2: Mixed with balance check**

```bash
curl -X POST http://localhost:8000/api/bill/pay \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer {token}" \
  -d '{
    "bill_payments": [
      {"bill_id": 1},
      {"bill_id": 2, "amount": 250000}
    ],
    "transaction_mode_value": 1,
    "admin_employee_id": 5,
    "balance_id": 25,
    "additionalInfo": "Bayar dari saldo"
  }'
```

**Scenario 3: With receipt file**

```bash
curl -X POST http://localhost:8000/api/bill/pay \
  -H "Authorization: Bearer {token}" \
  -F "bill_payments=[{\"bill_id\":1}]" \
  -F "transaction_mode_value=2" \
  -F "admin_employee_id=5" \
  -F "coa_kas_id=10" \
  -F "additionalInfo=Pembayaran tunai" \
  -F "file=@receipt.jpg"
```

### Postman Setup

1. Create POST request to `{{base_url}}/api/bill/pay`
2. Set Bearer token in Authorization
3. Body → raw (JSON) → paste request payload
4. For files: Body → form-data → add "file" as File type
5. Send

### Expected Responses

**Success**:
```json
{
  "message": "success",
  "transaction_id": 12345
}
```

**Error (insufficient balance)**:
```json
{
  "message": "Saldo tidak mencukupi",
  "error": "Saldo tidak mencukupi",
  "required": 1500000,
  "available": 500000
}
```

---

## Database Impact

### Tables Modified

| Table | Operation | Condition |
|-------|-----------|-----------|
| `bills` | UPDATE | All bills being paid |
| `bill_transactions` | INSERT | One row per bill |
| `journals` | INSERT | Multiple per bill (per mode) |
| `transactions` | INSERT | One per API call |
| `optional_service_subscriptions` | UPDATE | If bill fully paid |
| `balances` | UPDATE | If mode = balance |
| `balance_mutations` | INSERT | If mode = balance |
| `payments` | DELETE | If mode = VA (duplicates) |
| `bill_payments` | DELETE | If mode = VA (duplicates) |
| `logs` | INSERT | Audit trail |

### Transaction Properties

- **Mode**: Online (uses `DB::beginTransaction()`)
- **Rollback**: Any `\Throwable` rolls back all changes
- **Idempotency**: Middleware prevents duplicate submissions
- **Atomicity**: All-or-nothing guarantee

---

## Integration Points

### PSB Service
- `nextPsbFlow($new_student_id)` — advances to next PSB step

### Balance Service
- `recordUserBalanceMutation($data)` — logs balance changes

### Notification Service
- `SendWAMessagesJob::dispatch($phones, $message)` — sends WA notifications

---

## Audit & Logging

All transactions create:
1. **Log entry** (`logs` table) with:
   - Transaction ID
   - Amount
   - Description

2. **Balance mutation** (if balance payment):
   - Before/after balance
   - Amount deducted
   - Transaction reference

3. **Bill transaction entries** (per bill):
   - Amount paid
   - Outstanding remaining

---

## Future Enhancements

- [ ] Support for payment installment schedules
- [ ] Automatic PSB flow completion based on parent-level requirements
- [ ] Real-time payment status webhooks
- [ ] Bulk payment via CSV upload
- [ ] Payment reversal/cancellation
- [ ] Multi-currency support
- [ ] Advance payment (overpay) handling

---

## Troubleshooting

### Issue: "Saldo tidak mencukupi"
**Solution**: Check balance sufficiency. Return value shows required vs available.

### Issue: "Bills not found"
**Solution**: Verify bill IDs exist in database and belong to correct school/period.

### Issue: "Bill already fully paid"
**Solution**: Check `bills.paidAmount` against `bills.amount`. Cannot pay an already-paid bill.

### Issue: WA notification not sent
**Solution**: Check `Setting::WA_NOTIF_BILL_PAY` is enabled. Notification failures don't rollback payment.

### Issue: PSB not advancing
**Solution**: Verify `psb_flow_id` on bill. Check `NewStudent.psb_flow_id` matches. Inspect `PsbFlow.paymentInstruction`.

---

## Changelog

### v1.0.0 (2025-03-09)
- Initial release
- Unified payment function supporting all scenarios
- Service layer extraction for reusability
- Transaction-safe with proper error handling
- PSB flow integration
- Balance deduction support
- Multi-mode payment support

