# Admin Money Center — Backend Shipped (FE Changelog)

**Audience:** Frontend engineers wiring the admin Money Center pages.
**Status:** ✅ All 9 endpoints from `ADMIN_WALLETS_LEDGER_FRONTEND.md` are now live on `main` + `staging` (commits `10ed2817` + `73dca96e`). The amber "endpoint pending" cards should swap to live data the moment WHM pulls.
**Date:** 2026-04-29

---

## TL;DR

| # | Method | Path | Powers | Status |
|---|---|---|---|---|
| 1 | GET  | `/api/admin/marketplace/money/overview` | KPIs + counts + top 5 spenders/earners | ✅ |
| 2 | GET  | `/api/admin/marketplace/brand-wallets` | Paginated brand wallet list | ✅ |
| 3 | GET  | `/api/admin/marketplace/brand-wallets/{brandId}` | Brand wallet detail header | ✅ |
| 4 | GET  | `/api/admin/marketplace/brand-wallets/{brandId}/transactions` | Paginated transaction history | ✅ |
| 5 | GET  | `/api/admin/marketplace/transfers` | Collaboration Ledger explorer | ✅ |
| 6 | GET  | `/api/admin/marketplace/collaboration-requests/{crId}/transfers` | Per-CR money flow + 4-card summary | ✅ |
| 7 | GET  | `/api/admin/marketplace/reports/platform-revenue` | Platform Revenue chart + total | ✅ |
| 8 | GET  | `/api/admin/marketplace/reports/vat-collected` | VAT Collected chart + total | ✅ |
| 9 | POST | `/api/admin/marketplace/brand-wallets/{brandId}/adjust` | Admin "Adjust balance" modal | ✅ |

**Auth:** `Authorization: Bearer <sanctum_token>` on every request.
**Response wrapper:** every endpoint wraps payloads in `{ status: true, message: "Success", data: { … } }` (the standard `ApiResponseTrait` format used everywhere else in this API). Read `data.<field>` in the FE adapter — examples below show the unwrapped `data` block.

---

## Pagination convention

All paginated endpoints (#2, #4, #5) return:

```json
{
  "items": [ … ],
  "meta": {
    "current_page": 1,
    "last_page": 3,
    "per_page": 25,
    "total": 67
  }
}
```

This matches the **pre-normalized** shape (option 3) listed in the FE contract — no adapter changes needed if you already accept this shape via `wallets.types.ts`.

`per_page` accepts 1–100, default 25. Pass `?page=N` to navigate.

---

## 1. `GET /api/admin/marketplace/money/overview`

**Query params:**

| Param | Type | Default |
|---|---|---|
| `from` | `YYYY-MM-DD` | first day of current month |
| `to`   | `YYYY-MM-DD` | today |

**Response `data`:**

```json
{
  "period":   { "from": "2026-04-01", "to": "2026-12-31" },
  "currency": "SAR",
  "totals": {
    "brand_wallets_balance":   1992.50,
    "brand_wallets_pending":   0,
    "platform_revenue_period": 0,
    "vat_collected_period":    0,
    "creator_earnings_period": 1500,
    "creator_withdrawals_paid": 600,
    "creator_outstanding":     850
  },
  "counts": {
    "brands_with_wallet":  1,
    "active_collabs":      10,
    "completed_collabs":   1,
    "pending_withdrawals": 1
  },
  "top_brand_spenders": [
    { "brand_id": 2, "brand_name": "Test Brand", "brand_email": "brand@test.com", "total_spent": 8015 }
  ],
  "top_earning_creators": [
    { "creator_id": 21, "creator_name": "Wd Test", "total_earned": 1500 }
  ]
}
```

**Notes:**
- All `*_period` totals are scoped to the `from`–`to` range.
- `creator_outstanding` is **all-time** (`creator_credits − withdrawals_paid − locked`), clamped to ≥ 0.
- `top_*` arrays are limited to 5 rows.

---

## 2. `GET /api/admin/marketplace/brand-wallets`

**Query params:**

| Param | Type | Notes |
|---|---|---|
| `q` | string | matches `users.name`, `users.brand_name`, `users.email` (LIKE) |
| `sort` | `field:dir` | accepts `balance:asc`, `balance:desc`, `pending:asc`, `pending:desc`, `last_activity:asc`, `last_activity:desc`. Default `balance:desc` |
| `min_balance`, `max_balance` | number | optional |
| `page`, `per_page` | int | standard |

**Each `items[]` row:**

```json
{
  "brand_id":           2,
  "brand_name":         "Test Brand",
  "brand_email":        "brand@test.com",
  "balance":            1992.50,
  "pending":            0,
  "currency":           "SAR",
  "lifetime_topped_up": 1000,
  "lifetime_spent":     8015,
  "last_activity_at":   "2026-04-25 23:45:27"
}
```

---

## 3. `GET /api/admin/marketplace/brand-wallets/{brandId}`

Same row shape as #2 but a single object (not paginated). 404 if no wallet exists for that brand.

---

## 4. `GET /api/admin/marketplace/brand-wallets/{brandId}/transactions`

**Query params:**

| Param | Type | Values |
|---|---|---|
| `type` | string | `credit` \| `debit` |
| `source` | string | `top_up` \| `collaboration_payment` \| `refund` \| `admin_adjustment` |
| `from`, `to` | date | `YYYY-MM-DD` |
| `page`, `per_page` | int | standard |

**Each `items[]` row:**

```json
{
  "id":            12,
  "type":          "credit",
  "source":        "admin_adjustment",
  "amount":        42.50,
  "balance_after": 1992.50,
  "currency":      "SAR",
  "note":          "smoke-test admin top-up",
  "related": {
    "type":  "collaboration_request",
    "id":    47,
    "title": "Summer Collab"
  },
  "created_at": "2026-04-30 02:21:09"
}
```

`related` is `null` when the transaction isn't tied to a CR (e.g. top-ups, admin adjustments).

---

## 5. `GET /api/admin/marketplace/transfers`

The Collaboration Ledger explorer — paginated read of `mp_collaboration_transfers`.

**Query params:**

| Param | Type | Values |
|---|---|---|
| `account_type` | string | `creator` \| `platform` \| `vat` |
| `creator_id` | int | filter to one creator (forces `account_type=creator`) |
| `brand_id` | int | filter to all transfers for CRs from this brand |
| `cr_id` | int | filter to one CR |
| `from`, `to` | date | `YYYY-MM-DD` |
| `page`, `per_page` | int | standard |

**Each `items[]` row:**

```json
{
  "id":            15,
  "account_type":  "creator",
  "account_id":    21,
  "account_label": "Wd Test",
  "type":          "credit",
  "amount":        1500,
  "currency":      "SAR",
  "posted_at":     "2026-04-30 00:11:15",
  "note":          null,
  "collaboration_request": {
    "id":         10,
    "title":      "WD Test Collab",
    "brand_id":   2,
    "brand_name": "Test Brand"
  }
}
```

`account_label` is human-readable: creator name for `account_type=creator`, `"Platform"` for `account_type=platform`, `"VAT payable"` for `account_type=vat`. `account_id` is `0` for the platform/VAT single-bucket accounts.

---

## 6. `GET /api/admin/marketplace/collaboration-requests/{crId}/transfers`

Per-CR drilldown with the 4-card summary block included so you don't have to recompute client-side.

**Response `data`:**

```json
{
  "collaboration_request": {
    "id":           10,
    "title":        "WD Test Collab",
    "status":       "Completed",
    "completed_at": "2026-04-30 00:11:15"
  },
  "brand_paid": {
    "wallet_transaction_id": 8,
    "amount":                1500,
    "currency":              "SAR",
    "paid_at":               "2026-04-25 23:45:27"
  },
  "transfers": [
    { "account_type": "creator",  "account_id": 21, "creator_name": "Wd Test", "type": "credit", "amount": 1500, "currency": "SAR", "posted_at": "2026-04-30 00:11:15" },
    { "account_type": "platform", "account_id": 0,  "creator_name": null,      "type": "credit", "amount": 150,  "currency": "SAR", "posted_at": "2026-04-30 00:11:15" },
    { "account_type": "vat",      "account_id": 0,  "creator_name": null,      "type": "credit", "amount": 22.5, "currency": "SAR", "posted_at": "2026-04-30 00:11:15" }
  ],
  "summary": {
    "brand_paid":     1500,
    "creator_earned": 1500,
    "platform_fee":   150,
    "vat_collected":  22.5,
    "currency":       "SAR"
  }
}
```

`brand_paid` may be `null` if the CR was paid before the wallet integration (legacy data). `transfers[]` may be empty for CRs that aren't `Completed` yet — the summary will show zeros in that case.

---

## 7 & 8. Reports — `platform-revenue` and `vat-collected`

Identical shape, different account type.

**Query params (both):**

| Param | Type | Default |
|---|---|---|
| `from` | `YYYY-MM-DD` | first day of current month |
| `to`   | `YYYY-MM-DD` | today |
| `group_by` | `day` \| `week` \| `month` | `day` |

**Response `data`:**

```json
{
  "period":   { "from": "2026-01-31", "to": "2026-04-15" },
  "currency": "SAR",
  "group_by": "month",
  "total":    275.50,
  "series": [
    { "date": "2026-01", "amount": 0 },
    { "date": "2026-02", "amount": 100 },
    { "date": "2026-03", "amount": 175.50 },
    { "date": "2026-04", "amount": 0 }
  ]
}
```

**Date format in `series[].date`:**
- `group_by=day`   → `YYYY-MM-DD`
- `group_by=week`  → `YYYY-MM-DD` (Monday of that week)
- `group_by=month` → `YYYY-MM`

The series is **zero-filled** across the full range so chart x-axes have no gaps.

---

## 9. `POST /api/admin/marketplace/brand-wallets/{brandId}/adjust`

The "Adjust balance" modal.

**Request body:**

```json
{
  "type":   "credit",
  "amount": 42.50,
  "note":   "Compensation for delayed shipping #INC-2031"
}
```

**Validation rules:**

| Field | Rule |
|---|---|
| `type` | required, one of `credit` \| `debit` |
| `amount` | required, numeric, **strictly greater than 0** |
| `note` | required, 3–500 chars |

**Authz:** rejects creators (403), sub-users (403 unless impersonating their owner — see note below), and unauthenticated requests (401). Brand-owner accounts pass for now (see "Known limitation" below).

**Response `data` on 200:**

```json
{
  "transaction_id": 12,
  "type":           "credit",
  "amount":         42.50,
  "balance_after":  1992.50,
  "currency":       "SAR",
  "note":           "Compensation for delayed shipping #INC-2031"
}
```

Behind the scenes this calls the same `WalletService::credit` / `debit` used everywhere else, so:
- The wallet row is `SELECT … FOR UPDATE` locked → no race conditions.
- A row is appended to `mp_wallet_transactions` with `source = "admin_adjustment"` and `actor_id = <admin_user_id>`.
- The new balance is reflected immediately on the next `GET /brand-wallets/{brandId}` call.

After a successful adjust, the FE should refetch the wallet header (#3) and the transactions table (#4) to show the new row.

---

## Bug fixes shipped alongside

- **Reports month-bucket overflow** (was a Carbon overflow bug): `group_by=month` with `from=2026-01-31` now correctly produces `[Jan, Feb, Mar, Apr]` buckets instead of skipping February.
- **Adjust authorization** tightened from `return true` to require a top-level brand-owner account that is not a creator.

---

## Known limitations / open follow-ups

1. **Admin-role gating gap (pre-existing across the entire admin surface).** The whole `/api/admin/marketplace/*` route group is currently only `auth:sanctum`-gated — same as offers approve/reject, withdrawals approve/mark-paid, categories CRUD, etc. Until BE adds an `admin.only` middleware on this whole block, **the FE must continue to hide the admin nav from non-admin users**. The new `POST /adjust` endpoint is the only one with an additional narrow gate (rejects creators and unauthenticated requests), so even if the nav leaks it can't actually mutate balances.

2. **Sub-user impersonation.** Per the existing `ImpersonateBrandOwner` middleware, any sub-user request is automatically swapped to the brand owner across the API. This is intentional architectural behavior and applies to these endpoints too — i.e. a sub-user with brand-owner delegation will see the same admin Money Center data as the brand owner does. If you need the FE to distinguish "actual admin user" vs "delegated sub-user", flag it and BE will add a separate `is_acting_as_admin` boolean.

3. **`brand_paid` semantics in #6** currently picks the latest single wallet debit linked to the CR. If we ever support partial refunds in the future, that field will need to net all CR-linked wallet lines — flagged as a future improvement, not blocking today.

---

## Deployment

Already pushed to `main` + `staging`. Deploy on WHM:

```bash
git pull
# no migrations needed — all backing tables already exist
```

The amber "endpoint pending" cards on the Money Center pages should flip to live data automatically.
