# Mobile Bill Payment Feature - Development Report

**Date**: March 31, 2025
**Status**: Implemented
**Related Features**: Unified Bill Payment System, Bill Payment Webhook Integration, Sequential Payment Order Enforcement

---

## Background / Problem

The legacy `parentPayment()` endpoint was tightly coupled to the dashboard bill payment flow, making it difficult to extend for mobile clients. Key issues:

1. **Legacy endpoint limitations**: The `POST /api/bill/parent-payment` endpoint used a flat parameter structure (`bill_ids` array + single `installment` value), preventing granular per-bill payment control.
2. **Mobile-specific needs**: Mobile clients needed to support the same flexible payment array format as the dashboard (`bill_payments` array with per-bill amounts), enabling:
   - Pay full multiple bills in one request
   - Pay some bills in full, others partial (mixed payment)
   - Express different installment amounts per bill
3. **Code reuse opportunity**: The unified `TransactionService::payBills()` was designed to handle all payment scenarios, but was not accessible from the mobile layer.
4. **Webhook inconsistency**: The VA payment webhook (`PaymentController::updateBillByPaymentV2()`) was using an older pattern and needed alignment with the unified service for consistency.

---

## Goals

1. **New mobile endpoint**: Create `POST /api/mobile/bill/pay` that mirrors the dashboard flexibility
2. **Same bill_payments format**: Accept the unified `bill_payments` array format (matching dashboard)
3. **Two payment branches**: Support both immediate payment (Balance) and async payment (VA)
4. **Backward compatibility**: Keep legacy `POST /api/bill/parent-payment` untouched
5. **Webhook alignment**: Update VA webhook to use the unified service
6. **Sequential order enforcement**: Implement `BILL_PAY_BY_ORDER` setting to enforce chronological payment order
7. **Mobile-safe admin context**: Handle the absence of `admin_employee_id` in mobile sessions gracefully

---

## Design Decisions

### 1. Separate Controller vs Legacy Extension

**Decision**: Created new `MobileBillController::payBills()` instead of extending `parentPayment()`.

**Reasoning**:
- Prevents coupling mobile API versioning to legacy dashboard API
- Allows independent request/response contracts
- Makes mobile-specific validation and error responses clearer
- Easier to deprecate legacy endpoint later if needed
- Mobile clients can evolve without affecting dashboard integrations

### 2. bill_payments Array Format

**Decision**: Use `[{bill_id, payAmount}]` format (not flat `bill_ids + installment`).

**Reasoning**:
- Dashboard API already uses this format; single contract reduces frontend confusion
- Enables per-bill amount specification (required for "pay some full, some partial" scenarios)
- Easier to validate: can check each bill independently
- Transaction payload is self-describing and audit-friendly
- Matches webhook payload structure that's already stored in Payment records

### 3. Delegating VA to Legacy requestPaymentCharge()

**Decision**: Mobile VA branch builds a legacy-compatible Request object and calls `BillController::requestPaymentCharge()`.

**Reasoning**:
- Reuses proven charge creation flow (Payment, BillPayment, BankSettlement records)
- Prevents duplicate business logic for payment gateway integration
- Keeps VA infrastructure centralized
- Mobile doesn't need to understand gateway details
- Actual payment happens in webhook using unified service (transactional safety)

**Implementation detail**: The `bill_payments_json` is stored in the Payment record so the webhook can reconstruct exact per-bill amounts when processing confirmation.

### 4. admin_employee_id = 0 for Mobile

**Decision**: Mobile calls `TransactionService::payBills()` with `admin_employee_id = 0` (mobile user, no admin).

**Reasoning**:
- Mobile sessions have no admin context (no employee ID in JWT)
- Setting to 0 is semantically clear: "system/mobile user"
- The unified service doesn't assume admin_employee_id > 0; it's just logged
- Allows transaction history to distinguish mobile vs admin payments
- Backward safe: existing code doesn't depend on admin_employee_id being valid

### 5. updateBillByPaymentV2() for VA Webhook

**Decision**: Keep webhook endpoint unchanged; update its internals to call unified service.

**Reasoning**:
- Payment gateway webhooks expect stable endpoint contracts (external dependency)
- Using unified service inside the webhook ensures Bill, BillPayment, BillTransaction tables match exactly what dashboard flow produces
- Backward safe: old Payment records (from before this change) still work
- Consolidates all bill payment logic in one place (TransactionService)
- Webhook can use `bill_payments_json` from Payment record if available, fallback to legacy logic

### 6. payAmount Field Naming

**Decision**: Use `payAmount` in request body (not `amount`).

**Reasoning**:
- Aligns with dashboard API contract already documented
- Avoids ambiguity with other "amount" fields
- Clear intent: this is the payment amount, not the bill total
- Existing client code already uses this naming

### 7. BILL_PAY_BY_ORDER Setting

**Decision**: Add configurable setting to enforce sequential monthly payment order.

**Reasoning**:
- Schools may have strict payment policies (e.g., "must pay January before March")
- Makes enforcement optional; schools can disable if not needed
- Validation runs early in TransactionService::payBills(), before any database writes
- Same validation applies to both mobile and dashboard endpoints
- Error message clearly indicates which bill must be paid first

---

## Architecture

### File Changes

| File | Role | Change Type |
|------|------|-------------|
| `app/Http/Controllers/Mobile/MobileBillController.php` | New endpoint | Created |
| `app/Services/TransactionService.php` | Core service | Updated `payBills()` to include sequential order validation |
| `app/Http/Controllers/PaymentController.php` | Webhook handler | Updated `updateBillByPaymentV2()` to use unified service |
| `database/seeders/PayByOrderSettingSeeder.php` | Configuration | Created |
| `app/Helpers/ResponseHelper.php` | Error responses | Already has `mobileError()` and `mobileSuccess()` |

### Control Flow

```
POST /api/mobile/bill/pay
  └─ MobileBillController::payBills()
     ├─ Validation (bill_payments, payment_method, required fields)
     ├─ Route to payment method
     │
     ├─ [If Balance - Method 1]
     │  ├─ Load Balance
     │  ├─ Calculate total needed
     │  ├─ Check sufficiency
     │  └─ TransactionService::payBills(
     │      mode=BALANCE,
     │      balance_id set,
     │      admin_employee_id=0
     │    )
     │
     └─ [If VA - Method 2]
        ├─ Build legacy-compatible Request
        ├─ Store bill_payments_json in Request
        └─ BillController::requestPaymentCharge()
           (Creates Payment record, delegates to gateway)

POST /api/webhook/payment (when VA confirmed)
  └─ PaymentController::updateBillByPaymentV2()
     └─ TransactionService::payBills(
        mode=VA,
        payment_id set,
        uses bill_payments_json from Payment record
      )
```

### Service Hierarchy

```
TransactionService::payBills()
  ├─ validateBillPaymentOrder()              [NEW - sequential check]
  ├─ BillService::loadBillsForPayment()      [Existing]
  ├─ BillService::categorizeBills()          [Existing]
  ├─ BillService::payFull()                  [Existing]
  ├─ BillService::payInstallment()           [Existing]
  ├─ BillService::handlePsbFlowAfterPayment() [Existing]
  ├─ TransactionService::buildJournalEntriesForBill() [Existing]
  └─ ... (all other existing helpers)
```

---

## API Contract Summary

### Endpoint

```
POST /api/mobile/bill/pay
```

### Request Format

```json
{
  "payment_method_value": 1,
  "bill_payments": [
    {"bill_id": 123, "payAmount": 500000},
    {"bill_id": 124}
  ],
  "balance_id": 45,
  "user_id": null,
  "payment_channel_id": null,
  "additionalInfo": "Optional notes"
}
```

**Method Values**:
- `1` = Balance (requires `balance_id`)
- `2` = VA/Channel (requires `user_id`, `payment_channel_id`)

**bill_payments Fields**:
- `bill_id` (required): Bill to pay
- `payAmount` (optional): Amount to pay. Omit = pay full remaining

### Response Format

**Success (200)**:
```json
{
  "message": "Pembayaran berhasil",
  "data": {
    "transaction_id": 12345
  }
}
```

**Error (400 / 422)**:
```json
{
  "message": "Error description",
  "data": { /* details */ }
}
```

---

## Backward Compatibility

### Legacy Endpoint Preserved

**Endpoint**: `POST /api/bill/parent-payment`

**Unchanged**:
- Request/response contract identical
- Routing to `BillController::parentPayment()`
- No breaking changes

**Why untouched**:
- External integrations may depend on it
- Mobile endpoint can coexist
- Eventually deprecate with versioning (e.g., `/api/v2/bill/parent-payment`)

### Payment Records Schema

The VA webhook uses the same Payment/BillPayment/BankSettlement creation as before. To support per-bill amounts in webhook, the `bill_payments_json` field is stored in Payment record if available, but the webhook gracefully falls back to flat installment logic if not present.

---

## Configuration & Seeding

### BILL_PAY_BY_ORDER Setting

**Location**: `settings` table

| Column | Value | Meaning |
|--------|-------|---------|
| `code` | `BILL_PAY_BY_ORDER` | Setting key |
| `booleanValue` | `0` | Disabled (any order allowed) |
| `booleanValue` | `1` | Enabled (strict order required) |

**Seeder**: `PayByOrderSettingSeeder`
- Creates setting with default value `0` (disabled)
- Schools enable manually if needed
- Run during initial setup or migration

**Usage in Code**:
```php
$enforceOrder = Setting::where('code', 'BILL_PAY_BY_ORDER')->value('booleanValue');
if ($enforceOrder) {
  $this->validateBillPaymentOrder($billPayments);
}
```

---

## Validation Flows

### Mobile Bill Payment Validation

```
1. Validate request format
   ├─ payment_method_value in [1, 2]
   ├─ bill_payments array, min 1 item
   ├─ bill_payments[].bill_id (exists in bills)
   └─ bill_payments[].payAmount (nullable, >= 1 if set)

2. If method = 1 (Balance):
   ├─ balance_id required and exists
   ├─ Load balance
   └─ Check balance sufficiency

3. If method = 2 (VA):
   ├─ user_id required and > 0
   ├─ payment_channel_id required and > 0
   └─ [Charge request will validate further]

4. Unified service validation:
   ├─ validateBillPaymentOrder()      [If enabled]
   ├─ Check no duplicate bill_ids
   ├─ Check no already-paid bills
   └─ Check all required bill data
```

### Sequential Order Validation (When Enabled)

```
For each student in the payment:
  1. Fetch all unpaid bills (ordered by year_value, month_value)
  2. Walk through ordered list:
     - If bill in submission: OK, continue
     - If bill NOT in submission: mark gap
     - If later bill in submission after gap: ERROR

Error response: 422 with message
"Tagihan harus dibayar berurutan. [BillType] [Month] [Year] harus dibayar terlebih dahulu."
```

---

## Implementation Details

### Mobile Controller Logic

**Balance Payment** (`payWithBalance()`):
1. Load Balance (404 if not found)
2. Calculate total needed across all bills
3. Check balance sufficiency (400 if insufficient, with details)
4. Call `TransactionService::payBills()` with `admin_employee_id = 0`
5. Return transaction_id or error

**VA Payment** (`payWithVA()`):
1. Determine if mixed (has any partial payment → CICIL, else LUNAS)
2. Build legacy-compatible Request for `requestPaymentCharge()`
3. Store `bill_payments_json` so webhook can reconstruct amounts
4. Return gateway response (may be pending, awaiting confirmation)

### Service Integration

**TransactionService::payBills()** now includes early step:
```php
// After input validation, before DB transaction
if (Setting::where('code', 'BILL_PAY_BY_ORDER')->value('booleanValue')) {
  $this->validateBillPaymentOrder($billPayments, $billData);
}
```

---

## Testing Checklist

- [ ] Balance payment: single bill, full
- [ ] Balance payment: multiple bills, all full
- [ ] Balance payment: mixed (some full, some partial)
- [ ] Balance payment: insufficient balance error
- [ ] VA payment: creates pending Payment record
- [ ] VA payment: stores bill_payments_json correctly
- [ ] VA webhook: processes with unified service
- [ ] Sequential order: disabled (any order works)
- [ ] Sequential order: enabled, correct order works
- [ ] Sequential order: enabled, skip fails with proper error
- [ ] Backward compat: legacy parentPayment() still works
- [ ] Transaction logging: mobile payments logged as admin_employee_id=0
- [ ] Mobile response format: matches mobile API style

---

## Known Limitations & Future Work

### Current Limitations

1. **Mobile admin context**: Mobile always uses `admin_employee_id=0`. If future mobile clients need per-admin tracking, will need JWT enhancement.
2. **Receipt upload**: Mobile endpoint doesn't support receipt file upload (Dashboard supports it).
3. **Custom transaction date**: Mobile endpoint doesn't support custom transactionDate (always today).

### Future Enhancements

1. Add receipt upload to mobile endpoint
2. Support custom transactionDate in mobile API
3. Add payment verification/status endpoint for mobile clients
4. Support payment reversal (if business policy allows)
5. Implement parent-level payment scheduling (pay all children's bills on one date)
6. Real-time payment status via WebSocket or polling endpoint

---

## Related Documentation

- **Unified Bill Payment System**: `/docs/BILL_PAYMENT_UNIFIED_API.md`
- **Sequential Payment Order**: `/docs/BILL_PAY_BY_ORDER.md`
- **Legacy Endpoint**: Dashboard bill payment flow (unchanged)
- **Webhook Handler**: `PaymentController::updateBillByPaymentV2()`

---

## Questions for Product

1. Should mobile support receipt file upload in future versions?
2. Are there security concerns with `admin_employee_id=0` for audit purposes?
3. Should the legacy endpoint be versioned (`/api/v2/bill/parent-payment`) for cleaner deprecation path?
4. Any specific error messages for Indonesian locale that should be added?

---

## Sign-off

**Feature**: Mobile Bill Payment (Unified Service Integration)
**Implementation Date**: March 31, 2025
**Status**: Ready for QA
**Backward Compatible**: Yes (legacy endpoint untouched)
