# Phase 4 — Banking Operations API Documentation

## Overview

Phase 4 exposes the first banking operations APIs on top of the double-entry ledger engine built in Phase 3. All financial movement goes through `TransactionService`, which calls `Ledger::recordEntry()` and `BalanceEngine::updateBalance()`.

## Base URL

```
https://api.abcbank.example/api/v1
```

All routes require the `Authorization: Bearer <token>` header unless marked as public.

---

## Transfers

### POST /transfers/internal

Transfer between two accounts within the same bank.

**Request body:**
```json
{
  "from_account_uuid": "acc-uuid-1",
  "to_account_uuid": "acc-uuid-2",
  "amount": 50000.00,
  "currency": "NGN",
  "description": "Rent payment"
}
```

**Response:**
```json
{
  "status": "success",
  "message": "Transfer completed successfully",
  "data": {
    "transaction_uuid": "txn-uuid",
    "reference": "TXN-20260702-A1B2C3D4",
    "from_account": "...",
    "to_account": "...",
    "amount": 50000.00,
    "currency": "NGN",
    "transfer_type": "INTERNAL",
    "status": "COMPLETED",
    "created_at": "2026-07-02 14:30:00"
  }
}
```

### POST /transfers/domestic

Transfer to a domestic beneficiary (via clearing account).

**Request body:**
```json
{
  "from_account_uuid": "acc-uuid-1",
  "beneficiary_uuid": "ben-uuid-1",
  "amount": 25000.00,
  "currency": "NGN",
  "description": "Vendor payment"
}
```

**Response:** Same shape as internal transfer, with `transfer_type: "DOMESTIC"`.

### POST /transfers/wire

International wire transfer (via clearing account).

**Request body:**
```json
{
  "from_account_uuid": "acc-uuid-1",
  "beneficiary_uuid": "ben-uuid-1",
  "amount": 5000.00,
  "currency": "USD",
  "description": "International invoice",
  "swift_code": "ABCDUS33",
  "iban": "US12345678901234567890"
}
```

**Response:** Same shape, with `transfer_type: "WIRE"`.

### GET /transfers

List transfers with optional filters.

**Query parameters:**
- `from_account_uuid` (optional)
- `transfer_type` (optional): `INTERNAL`, `DOMESTIC`, `WIRE`
- `currency` (optional)
- `page` (default: 1)
- `per_page` (default: 20)

### GET /transfers/{uuid}

Get a single transfer by UUID.

---

## Deposits

### POST /deposits

Create a deposit. Admin types (`ADMIN`, `OPENING_BALANCE`) require staff/admin role.

**Request body:**
```json
{
  "account_uuid": "acc-uuid-1",
  "amount": 100000.00,
  "currency": "NGN",
  "deposit_type": "CASH",
  "description": "Opening deposit"
}
```

**Valid deposit types:** `CASH`, `MANUAL`, `ADMIN`, `OPENING_BALANCE`

### GET /deposits

List deposits with optional filters.

**Query parameters:**
- `account_uuid` (optional)
- `deposit_type` (optional)
- `currency` (optional)
- `page` (default: 1)
- `per_page` (default: 20)

### GET /deposits/{uuid}

Get a single deposit by UUID.

---

## Withdrawals

### POST /withdrawals

Create a withdrawal. Admin type (`ADMIN`) requires staff/admin role. ATM type returns `501 Not Implemented`.

**Request body:**
```json
{
  "account_uuid": "acc-uuid-1",
  "amount": 5000.00,
  "currency": "NGN",
  "withdrawal_type": "CASH",
  "description": "ATM withdrawal"
}
```

**Valid withdrawal types:** `CASH`, `MANUAL`, `ADMIN`, `ATM`

### GET /withdrawals

List withdrawals with optional filters.

**Query parameters:**
- `account_uuid` (optional)
- `withdrawal_type` (optional)
- `currency` (optional)
- `page` (default: 1)
- `per_page` (default: 20)

### GET /withdrawals/{uuid}

Get a single withdrawal by UUID.

---

## Beneficiaries

### GET /beneficiaries

List beneficiaries. Defaults to the authenticated user's beneficiaries.

**Query parameters:**
- `name` (optional)
- `account_number` (optional)
- `is_favourite` (optional): 0 or 1
- `is_internal` (optional): 0 or 1
- `page` (default: 1)
- `per_page` (default: 20)

### POST /beneficiaries

Create a new beneficiary.

**Request body:**
```json
{
  "name": "John Doe",
  "account_number": "1234567890",
  "bank_name": "First Bank",
  "bank_code": "011",
  "branch_code": "001",
  "account_type": "SAVINGS",
  "currency": "NGN",
  "country": "NG",
  "is_internal": false
}
```

### GET /beneficiaries/{uuid}

Get a single beneficiary.

### PUT /beneficiaries/{uuid}

Update a beneficiary. All fields are optional.

### DELETE /beneficiaries/{uuid}

Soft-delete a beneficiary (sets status to `DELETED`).

### PATCH /beneficiaries/{uuid}/favourite

Toggle the favourite flag on a beneficiary.

---

## Statements

### POST /statements

Generate a JSON account statement with running balance.

**Request body:**
```json
{
  "account_uuid": "acc-uuid-1",
  "start_date": "2026-01-01",
  "end_date": "2026-06-30"
}
```

**Constraints:**
- Maximum date range: 1 year (365 days)
- `start_date` must be before `end_date`

**Response:**
```json
{
  "status": "success",
  "message": "Statement generated successfully",
  "data": {
    "statement_uuid": "stmt-uuid",
    "account_uuid": "acc-uuid-1",
    "account_number": "1000000001",
    "account_type": "SAVINGS",
    "currency": "NGN",
    "start_date": "2026-01-01",
    "end_date": "2026-06-30",
    "opening_balance": 50000.00,
    "closing_balance": 120000.00,
    "total_credits": 100000.00,
    "total_debits": 30000.00,
    "net_change": 70000.00,
    "entry_count": 15,
    "entries": [
      {
        "date": "2026-01-15 10:00:00",
        "reference": "TXN-20260115-XXXXXXXX",
        "transaction_uuid": "txn-uuid",
        "description": "Salary deposit",
        "entry_type": "DEPOSIT",
        "debit": 0.00,
        "credit": 50000.00,
        "balance": 100000.00
      }
    ]
  }
}
```

### GET /statements

List statement requests for the authenticated user.

### GET /statements/{uuid}

Get a single statement request record.

---

## Notifications

### GET /notifications

List in-app notifications for the authenticated user.

**Query parameters:**
- `unread_only` (default: `false`): set to `true` to show only unread
- `limit` (default: 50)
- `offset` (default: 0)

### GET /notifications/unread-count

Return the count of unread notifications.

**Response:**
```json
{
  "status": "success",
  "data": { "unread_count": 7 }
}
```

### PATCH /notifications/{uuid}/read

Mark a single notification as read.

### PATCH /notifications/read-all

Mark all notifications as read for the authenticated user.

**Response:**
```json
{
  "status": "success",
  "message": "All notifications marked as read",
  "data": { "marked_count": 7 }
}
```

---

## Authentication

All banking operation routes require the same JWT Bearer token as Phase 2.

```
Authorization: Bearer <jwt_token>
```

Staff and admin roles are enforced for `ADMIN`/`OPENING_BALANCE` deposit types and `ADMIN` withdrawal types.

---

## Error Codes

| Code | Meaning | Typical Cause |
|------|---------|---------------|
| 400 | Bad Request | Validation error, invalid amount/currency |
| 403 | Forbidden | Insufficient role for admin operation |
| 404 | Not Found | Account, beneficiary, or transaction not found |
| 409 | Conflict | Insufficient balance, account frozen, daily limit exceeded |
| 422 | Unprocessable | Invalid date range, transfer to same account |
| 501 | Not Implemented | ATM withdrawal, cheque deposit, draft |

---

## Event Flow

1. User calls a banking operation (e.g., `POST /transfers/internal`)
2. `TransferController` validates input via `BaseController::validate()`
3. `TransferService` calls `TransactionService::createTransfer()`
4. `TransactionService` calls `Ledger::recordDoubleEntry()` + `BalanceEngine::updateBalance()`
5. `TransactionService` dispatches `TransferCompleted` event
6. `NotificationService` listener creates in-app notification + queues email job
7. `AuditService` logs the action
8. Controller returns success response

---

## Ledger-First Rules

- **Never** run `UPDATE accounts SET balance = balance + amount` directly.
- **Always** route through `TransactionService` → `Ledger` → `BalanceEngine`.
- **Always** create at least one DR and one CR ledger entry per transaction.
- **BalanceEngine** is the ONLY component that writes to `accounts.balance`.

---

## Testing Checklist

### Transfer Tests
- [ ] Internal transfer between two accounts
- [ ] Internal transfer to same account (should reject)
- [ ] Domestic transfer via beneficiary
- [ ] Wire transfer with SWIFT/IBAN
- [ ] Insufficient balance rejection
- [ ] Frozen account rejection
- [ ] Currency mismatch rejection
- [ ] Daily limit exceeded rejection
- [ ] Reference collision (should auto-retry)

### Deposit Tests
- [ ] Cash deposit to account
- [ ] Manual deposit by staff
- [ ] Admin deposit (admin role required)
- [ ] Opening balance deposit
- [ ] Invalid currency rejection
- [ ] Frozen account rejection

### Withdrawal Tests
- [ ] Cash withdrawal
- [ ] Manual withdrawal by staff
- [ ] Admin withdrawal (admin role required)
- [ ] ATM withdrawal (should return 501)
- [ ] Insufficient balance rejection
- [ ] Frozen account rejection
- [ ] Daily limit exceeded rejection

### Beneficiary Tests
- [ ] Create internal beneficiary (auto-resolves account)
- [ ] Create external beneficiary
- [ ] Update beneficiary details
- [ ] Delete beneficiary (soft-delete)
- [ ] Toggle favourite
- [ ] Search by name, account_number, is_favourite

### Statement Tests
- [ ] Generate statement with running balance
- [ ] Opening balance correctness
- [ ] Closing balance matches live balance
- [ ] Date range > 1 year rejection
- [ ] Invalid date range (start > end) rejection
- [ ] Empty period (no entries)

### Notification Tests
- [ ] In-app notification on transfer completion
- [ ] In-app notification on deposit completion
- [ ] In-app notification on withdrawal completion
- [ ] Statement ready notification
- [ ] Beneficiary added notification
- [ ] Unread count accurate
- [ ] Mark single as read
- [ ] Mark all as read
- [ ] Email job queued (check storage/queue/)

### Integration Tests
- [ ] Double-entry balance: sum(DR) == sum(CR) per transaction
- [ ] BalanceEngine rebuild matches live balance
- [ ] Reference uniqueness (no duplicates in ref_registry)
- [ ] Audit log entries for every operation
- [ ] Event dispatcher does not throw on listener failure
- [ ] Queue job file created for email notifications
