---
feature: Payments monitor
slug: payments
surface: admin
consumer: react
version: 1.2.0
status: ready
brd_version: "1.3"
brd_ids: [ADMIN-015]
business_rules: [BR-14, BR-24, BR-29]
decisions: [DEC-06, DEC-09, DEC-10, TD-19, TD-27, TD-151, TD-164, TD-165, TD-168, TD-179]
schema_tables: [payments, orders, provider_subscriptions, featured_plans, invoices, refunds]
related_docs: [orders.md, cancellations-refunds.md, coupons.md, settings.md, ../client/payment.md, ../provider/featured-subscription.md]
last_updated: 2026-11-10
---

# Payments monitor — React Dashboard Integration Guide

> **Base URL:** `{HOST}/api/v1/admin` · Shared rules (headers, envelope, errors,
> pagination, axios setup, TS types): [API_CONVENTIONS.md](../API_CONVENTIONS.md)

> **Read-only.** This section watches money; it never moves it. Refunds live in
> [cancellations-refunds.md](cancellations-refunds.md), payouts in the payouts guide. Payments run on the
> simulated gateway until the real vendor is chosen (DEC-06, TD-151); the screens do not change when it arrives.

## Version history

Newest first. Never edit an old row.

| Version | Date | Type | Summary | Dashboard action required |
|---------|------|------|---------|---------------------------|
| 1.2.0 | 2026-11-10 | MINOR | Customer references in admin payment rows and the customer filter list now also carry `customer.user_id`, the **user** uuid that opens the customer at `GET /admin/users/{id}`; `customer.id` is unchanged (the customer-profile uuid, still what `filters[customer_id]` takes). Fixes the order → customer link that returned 404 (QA-03). | Link to `/users/{customer.user_id}` instead of `/users/{customer.id}`; ignore the field if absent. |
| 1.1.0 | 2026-11-08 | MINOR | `GET /payments/customers`: customer picker whose `id` is the profile uuid `filters[customer_id]` validates (TD-179). | Fill the customer filter from it; never pass a `/users` row id. |
| 1.0.0 | 2026-11-04 | MINOR | New: `GET /payments` and `GET /payments/{payment}` (ADMIN-015, TD-168). | Build the Payments page and details view. |

## Changelog

### 1.2.0 — 2026-11-10

- **Added:** Customer references in admin payment rows and the customer filter list now also carry `customer.user_id`, the **user** uuid that opens the customer at `GET /admin/users/{id}`; `customer.id` is unchanged (the customer-profile uuid, still what `filters[customer_id]` takes). Fixes the order → customer link that returned 404 (QA-03).
- **Note:** the JSON examples in this guide were captured before `customer.user_id` existed and do not show it; the current captured responses are in the Postman collection.
- **Breaking:** No. **App action required:** Link to `/users/{customer.user_id}` instead of `/users/{customer.id}`; ignore the field if absent.

### 1.1.0 — 2026-11-08

- **Added:** `GET /payments/customers` (route `admin.payments.customers`, section `payments` · view; no customer-management cell needed). Section 3.1.
- **Unchanged on purpose:** `filters[customer_id]` still takes the customer **profile** uuid. `GET /users` row ids are *user* uuids and are refused by this filter (422); general notifications keep using user uuids for `customer_ids[]`.
- **Breaking:** No. **Dashboard action required:** use the lookup for the customer filter.
- **BRD / decisions:** ADMIN-015, TD-179.

### 1.0.0 — 2026-11-04

Added the payments monitor: one list for customer order payments and provider featured-plan payments, with
filters, and a details view with the invoice, failure reason, refund history and the subscription period.

## 1. Business summary

Admins watch every payment on the platform: customers paying for orders and packages, and providers paying for
a featured plan. Each row shows who paid, what for, how the money was split between wallet and gateway, and the
result. Details add the invoice, the transaction number, the refund history and, for plans, the subscription dates.

## 2. Pages and permissions

| Page / component | Purpose | Endpoint(s) | RBAC permission (route name) |
|------------------|---------|-------------|------------------------------|
| `<PaymentsListPage>` | Table of all payments with filters | `GET /payments` | `admin.payments.index` |
| `<PaymentCustomerSelect>` (1.1.0) | Options for `filters[customer_id]` | `GET /payments/customers` | `admin.payments.customers` |
| `<PaymentDetailsPage>` | One payment with invoice and refunds | `GET /payments/{payment}` | `admin.payments.show` |

Sidebar entry: Payments (section `payments`, with Coupons). Hide it when the admin lacks `admin.payments.index`.
Section `payments` / action `view`; supervisors need that cell.

## 3. List / table

### 3.0 Customer lookup — `GET /api/v1/admin/payments/customers` (1.1.0, TD-179)

Customers who have **at least one order payment** (any payment status: the filter shows failed ones too), found the way the filter finds them: `payments.order_id → orders.customer_id`. Provider subscription payments have no customer and add nobody. Accounts that are disabled, banned or deleted stay listed, because their payments remain in the history; no notification-reachability rule applies.

| Query | Type | Notes |
|-------|------|-------|
| `search` | string ≤ 100 | Partial match on name or phone (LIKE-escaped). |
| `page`, `per_page` (1–100, default 15) | int | Rows under `data.customers` with `links` and `meta`. |
| `paginate=false` | | Direct `data` array, at most 500 rows. |

Row: `id` — the **customer profile uuid** (send as `filters[customer_id]`) — `user_id` (the user uuid; opens `GET /users/{id}`), `full_name`, `phone_code`, `phone`. Ordered by name. 422 on bad paging; 403 without `payments.view`.

```json
{"id": "01a1bb7c-04f2-73c9-a671-30914a6c5ca8", "full_name": "Nora Payer", "phone_code": "966", "phone": "502220002"}
```

Postman: `admin.payments.customers` › "200 Payment Customers Lookup" · "200 Payment Customers Lookup Unpaginated" · "422 Payment Customers Lookup Invalid".


`GET /payments` — paginated, newest first. Query: `page`, `per_page` (max 100), `paginate=false` (plain array,
500 rows), `search` (order code, gateway transaction number or payer name), and:

| Filter | Values |
|--------|--------|
| `filters[status]` | `initiated`, `authorized`, `paid`, `failed`, `voided`, `refunded`, `partially_refunded` |
| `filters[method]` | `card`, `mada`, `apple_pay`, `tabby`, `wallet`, `wallet_plus_gateway`, `earnings_deduction` |
| `filters[purpose]` | `order` (customer order or package), `featured_subscription` (provider plan) |
| `filters[order_id]`, `filters[customer_id]`, `filters[provider_id]` | public uuids |
| `filters[created_from]`, `filters[created_to]` | `YYYY-MM-DD` |

Sort: `sort[created_at|amount|paid_at]=asc|desc`. Unknown filter keys and bad values are 422.

Columns: customer or payer, order kind (service / package), provider, order value, delivery fee, final total,
method, status, date, wallet amount, gateway amount, transaction type, subscription fee.

Postman: `admin.payments.index` › "200 Payments List"

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "payments": [
      {
        "id": "01a1a71a-de06-7347-84a6-7b3f0d740f6c",
        "transaction_type": {
          "value": "featured_subscription",
          "label": "Featured subscription payment"
        },
        "transaction_number": null,
        "payer": {
          "name": "Bria Rice",
          "phone_code": "966",
          "phone": "503190584"
        },
        "customer": null,
        "provider": {
          "id": "01a1a71a-de02-73e7-8872-473a469901d8",
          "display_name": "Mr. Gaetano Kohler"
        },
        "order": null,
        "subscription": {
          "id": "01a1a71a-ddff-7141-a53c-167d6c475d42",
          "plan_name": "Sample-cesi96413487",
          "subscription_fee": "100.00"
        },
        "method": {
          "value": "card",
          "label": "Bank card"
        },
        "status": {
          "value": "paid",
          "label": "Paid"
        },
        "amount": "149.00",
        "wallet_amount": "0.00",
        "gateway_amount": "149.00",
        "paid_at": null,
        "created_at": "2026-11-04T15:08:38+00:00"
      },
      {
        "id": "01a1a71a-ddfc-73ea-a337-ec131aaf4b68",
        "transaction_type": {
          "value": "order",
          "label": "Order payment"
        },
        "transaction_number": null,
        "payer": {
          "name": "Albina Koelpin",
          "phone_code": "966",
          "phone": "537298228"
        },
        "customer": {
          "id": "01a1a71a-ddf0-7270-ad62-5d5f01d217ec",
          "full_name": "Albina Koelpin"
        },
        "provider": {
          "id": "01a1a71a-ddf5-71cb-a2a1-0d051657001b",
          "display_name": "Princess Corwin"
        },
        "order": {
          "id": "01a1a71a-dde6-738f-aadb-0e449fe45a3a",
          "code": "JSH-ussc26430836",
          "kind": {
            "value": "service",
            "label": "Service"
          },
          "order_value": "130.00",
          "delivery_fee": "20.00",
          "final_total": "150.00"
        },
        "subscription": null,
        "method": {
          "value": "card",
          "label": "Bank card"
        },
        "status": {
          "value": "paid",
          "label": "Paid"
        },
        "amount": "150.00",
        "wallet_amount": "0.00",
        "gateway_amount": "150.00",
        "paid_at": "2026-11-04T15:08:38+00:00",
        "created_at": "2026-11-04T15:08:38+00:00"
      }
    ],
    "meta": {
      "current_page": 1,
      "last_page": 1,
      "per_page": 15,
      "total": 2
    }
  }
}
```

`order` is `null` for plan payments and `subscription` is `null` for order payments; `customer` is `null` for
plan payments. All keys are always present.

## 4. Details view

`GET /payments/{payment}` — the list row plus: `gateway`, `gateway_fee`, `failure_reason`, `invoice`
(`id`, `invoice_no`, `total`, `vat_amount`, `issued_at`, or `null`), `refunds[]` (amount, status, reason,
processed_at) and, for plans, `subscription_period` (`duration_days`, `starts_at`, `ends_at`, `status`).

Postman: `admin.payments.show` › "200 Payment Details"

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "id": "01a1a71a-e265-71e4-aab5-b0aaee87b1ad",
    "transaction_type": {
      "value": "order",
      "label": "Order payment"
    },
    "transaction_number": null,
    "payer": {
      "name": "Alivia Lynch",
      "phone_code": "966",
      "phone": "511983337"
    },
    "customer": {
      "id": "01a1a71a-e25d-73c8-8230-3101d11e2bd3",
      "full_name": "Alivia Lynch"
    },
    "provider": {
      "id": "01a1a71a-e260-736c-87d8-d4f6bb80d9bf",
      "display_name": "Lemuel Little"
    },
    "order": {
      "id": "01a1a71a-e256-718f-b18a-ca0702af85ac",
      "code": "JSH-cwww20685418",
      "kind": {
        "value": "service",
        "label": "Service"
      },
      "order_value": "130.00",
      "delivery_fee": "20.00",
      "final_total": "150.00"
    },
    "subscription": null,
    "method": {
      "value": "card",
      "label": "Bank card"
    },
    "status": {
      "value": "paid",
      "label": "Paid"
    },
    "amount": "150.00",
    "wallet_amount": "0.00",
    "gateway_amount": "150.00",
    "paid_at": "2026-11-04T15:08:39+00:00",
    "created_at": "2026-11-04T15:08:39+00:00",
    "gateway": null,
    "gateway_fee": null,
    "failure_reason": null,
    "invoice": null,
    "subscription_period": null,
    "refunds": [
      {
        "id": "01a1a71a-e266-709e-9278-f07c1fa05998",
        "amount": "40.00",
        "status": {
          "value": "completed",
          "label": "Completed"
        },
        "reason": {
          "value": "provider_rejected",
          "label": "Provider rejected"
        },
        "processed_at": null
      }
    ]
  }
}
```

Postman: `admin.payments.show` › "200 Payment Details Subscription"

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "id": "01a1a71a-e27c-7171-957e-605c4b86df96",
    "transaction_type": {
      "value": "featured_subscription",
      "label": "Featured subscription payment"
    },
    "transaction_number": null,
    "payer": {
      "name": "Eula Kreiger",
      "phone_code": "966",
      "phone": "570218547"
    },
    "customer": null,
    "provider": {
      "id": "01a1a71a-e278-7386-83f7-6c979f8ca1f7",
      "display_name": "Kacie Haag"
    },
    "order": null,
    "subscription": {
      "id": "01a1a71a-e275-720d-8750-1521127d7994",
      "plan_name": "Sample-xaxq46913912",
      "subscription_fee": "100.00"
    },
    "method": {
      "value": "card",
      "label": "Bank card"
    },
    "status": {
      "value": "paid",
      "label": "Paid"
    },
    "amount": "100.00",
    "wallet_amount": "0.00",
    "gateway_amount": "100.00",
    "paid_at": null,
    "created_at": "2026-11-04T15:08:39+00:00",
    "gateway": null,
    "gateway_fee": null,
    "failure_reason": null,
    "invoice": null,
    "subscription_period": {
      "duration_days": 30,
      "starts_at": null,
      "ends_at": null,
      "status": "pending_payment"
    },
    "refunds": []
  }
}
```

Errors: 404 for an unknown uuid (an integer id is also a 404), 422 for a bad filter, 401 / 403 as on every admin route.

## 5. Forms (create / edit)

N/A — read-only.

## 6. Actions (non-CRUD)

N/A — refunds are issued from the Cancellations & refunds section.

## 7. Statuses and enums

`status`, `method` and `transaction_type` are `{value, label}` objects; branch on `value`.
Statuses follow the schema: `initiated`, `authorized`, `paid`, `failed` ("Payment failed"), `voided`,
`refunded`, `partially_refunded`.

## Data model

`payments` (`purpose` order or featured_subscription, `amount = wallet_amount + gateway_amount` except
`earnings_deduction`), joined to `orders` (customer, provider), `provider_subscriptions` (plan, dates),
`invoices` and `refunds`.

## 8. TypeScript types

```ts
type Enum = { value: string; label: string };
type PaymentRow = {
  id: string; transaction_type: Enum; transaction_number: string | null;
  payer: { name: string | null; phone_code: string | null; phone: string | null };
  customer: { id: string; user_id: string; full_name: string } | null;
  provider: { id: string; display_name: string } | null;
  order: { id: string; code: string; kind: Enum; order_value: string; delivery_fee: string; final_total: string } | null;
  subscription: { id: string; plan_name: string | null; subscription_fee: string } | null;
  method: Enum; status: Enum; amount: string; wallet_amount: string; gateway_amount: string;
  paid_at: string | null; created_at: string;
};
type PaymentDetails = PaymentRow & {
  gateway: string | null; gateway_fee: string | null; failure_reason: string | null;
  invoice: { id: string; invoice_no: string; total: string; vat_amount: string; issued_at: string } | null;
  subscription_period: { duration_days: number | null; starts_at: string | null; ends_at: string | null; status: string } | null;
  refunds: { id: string; amount: string; status: Enum; reason: Enum; processed_at: string | null }[];
};
```

## 9. Business rules enforced by the backend

A failed payment never confirms an order or activates a plan (BR-14). A successful payment confirms the order,
issues the invoice and activates the plan; this section only displays the outcome.

## 10. Notifications triggered

None. The payer's notifications are listed in [notifications.md](notifications.md).

## 11. Integration checklist

- [ ] Payments page with the filters above and the `payments` envelope key.
- [ ] Details page; show the invoice and refunds blocks only when present.
- [ ] Hide the sidebar item without `admin.payments.index`.

## 12. Acceptance scenarios

AC-ADMIN-015 — `PaymentManagementTest`: list shows order and plan payments with the wallet/gateway split;
filters narrow the list; details show invoice, failure reason, refunds and the plan period; bad filters are 422;
403 without the section.

## 13. Open decisions and known limitations

- DEC-06: the real gateway is not chosen. Rows carry the simulated gateway today; `gateway_fee` is empty until the vendor reports one.
- Exporting the list is not built.
