Platform admin: tenant payments history
The platform-admin app gains a Payments tab on each tenant's detail page. It lets a platform operator audit a single tenant's payment history: read a monthly summary (net revenue, failure rate, why payments fail, what sells), then filter and page through the raw transactions and open any one for full detail. The goal is operational visibility — spotting failing payments and seeing how each tenant actually sells — without touching tenant-facing surfaces. Every endpoint is read-only and tenant-scoped, and the data below is seeded demo data on the local Docker stack.
What was built
Three read-only backend endpoints under /api/v1/platform (gated by the platform-admin guard),
surfaced through three Next.js proxy routes and one UI tab.
GET /tenants/{tenant_id}/transactions— paginated list (page/size, newest first). Filters:status,provider,search(nanoid / provider txn id / order code / user email + name),start_date/end_dateon created_at.GET /tenants/{tenant_id}/transactions/summary—month(YYYY-MM, default current). Zero-filled status counts, net revenue per currency, failure rate, top failure reasons, top courses.GET /tenants/{tenant_id}/transactions/{nanoid}— full detail incl. line items, provider ids/metadata, voucher, refund, and billing snapshot. 404 if the transaction is not in that tenant (cross-tenant isolation).- UI: a Payments tab (after Usage) with a month stepper + summary cards, a filter bar, a paginated table with coloured status badges, and a per-transaction detail sheet.
Seeded data
Two demo tenants were empty, so the seed created instructor + student users, courses, and memberships, then inserted transactions spread over the last 60 days (weighted to Aug – Sep 2026), each with 1–2 line items. All inserts ran in one transaction.
| Status | Tenant A | Tenant B |
|---|---|---|
| COMPLETED | 25 | 3 |
| FAILED | 9 | 1 |
| PENDING | 4 | 1 |
| PROCESSING | 2 | 0 |
| CANCELLED | 2 | 0 |
| REFUNDED | 2 | 0 |
| PARTIALLY_REFUNDED | 1 | 0 |
| Total transactions | 45 | 5 |
| Line items | 61 | 7 |
Mix: mostly SEPAY with some ALEPAY; BANK_TRANSFER / DIGITAL_WALLET; a couple of USD amounts; several rows carry a voucher + discount; failed rows use varied reasons (some null). Users, courses and memberships were created for both tenants (Tenant A: 1 instructor + 5 students + 4 courses; Tenant B: 1 instructor + 2 students + 2 courses).
Walkthrough
Signed in as the platform admin (skillpixeldeveloper@gmail.com). Click any image to enlarge. device_scale_factor = 2.
1. Payments tab — default view


2. Summary section close-up

3. Filter: status = FAILED

4. Search by user email

5. Date range filter

6. Pagination — page 2

7. Detail sheet — COMPLETED


8. Detail sheet — FAILED and REFUNDED


9. Month stepper — August 2026

10. Tenant B — isolation


11. Overview tab — context

API samples
Raw JSON captured by calling the backend directly with the minted id token.
GET /transactions/summary (2026-09)
{
"month": "2026-09",
"status_counts": {
"PENDING": 2,
"PROCESSING": 1,
"COMPLETED": 13,
"FAILED": 5,
"CANCELLED": 2,
"REFUNDED": 1,
"PARTIALLY_REFUNDED": 1
},
"revenue_by_currency": [
{
"currency": "VND",
"net_amount": 7340700,
"completed_count": 15
}
],
"failure_rate": 0.2777777777777778,
"top_failure_reasons": [
{
"reason": "Payment window expired",
"count": 2
},
{
"reason": "Bank timeout",
"count": 1
},
{
"reason": "Card declined by issuer",
"count": 1
},
{
"reason": "unknown",
"count": 1
}
],
"top_courses": [
{
"course_id": "ux-design-essentials",
"course_title": "UX Design Essentials",
"completed_count": 6,
"net_amount": 3231500,
"currency": "VND"
},
{
"course_id": "web-dev-bootcamp",
"course_title": "Web Development Bootcamp",
"completed_count": 5,
"net_amount": 2132500,
"currency": "VND"
},
{
"course_id": "ml-foundations",
"course_title": "Machine Learning Foundations",
"completed_count": 4,
"net_amount": 1143000,
"currency": "VND"
},
{
"course_id": "data-science-python",
"course_title": "Data Science with Python",
"completed_count": 3,
"net_amount": 744000,
"currency": "VND"
}
]
}GET /transactions/summary (2026-08)
{
"month": "2026-08",
"status_counts": {
"PENDING": 2,
"PROCESSING": 1,
"COMPLETED": 10,
"FAILED": 4,
"CANCELLED": 0,
"REFUNDED": 0,
"PARTIALLY_REFUNDED": 0
},
"revenue_by_currency": [
{
"currency": "VND",
"net_amount": 5213300,
"completed_count": 10
}
],
"failure_rate": 0.2857142857142857,
"top_failure_reasons": [
{
"reason": "Insufficient funds",
"count": 2
},
{
"reason": "Signature mismatch",
"count": 1
},
{
"reason": "unknown",
"count": 1
}
],
"top_courses": [
{
"course_id": "ml-foundations",
"course_title": "Machine Learning Foundations",
"completed_count": 3,
"net_amount": 1738500,
"currency": "VND"
},
{
"course_id": "web-dev-bootcamp",
"course_title": "Web Development Bootcamp",
"completed_count": 4,
"net_amount": 1684000,
"currency": "VND"
},
{
"course_id": "data-science-python",
"course_title": "Data Science with Python",
"completed_count": 4,
"net_amount": 1292500,
"currency": "VND"
},
{
"course_id": "ux-design-essentials",
"course_title": "UX Design Essentials",
"completed_count": 3,
"net_amount": 648000,
"currency": "VND"
}
]
}GET /transactions?status=FAILED&size=3
{
"items": [
{
"transaction_id": "McNfAQLKHu7qnQTupqziQ",
"user": {
"name": "An Le",
"email": "an.le@tenanta.demo"
},
"amount": 990000,
"discount_amount": 0,
"final_amount": 990000,
"refund_amount": 0,
"currency": "VND",
"status": "FAILED",
"provider": "SEPAY",
"payment_method": "BANK_TRANSFER",
"failure_reason": "Bank timeout",
"course_titles": [
"Machine Learning Foundations",
"Data Science with Python"
],
"created_at": "2026-09-07T08:00:00Z",
"completed_at": null,
"expires_at": null
},
{
"transaction_id": "suhaFVBliyIToGJ1QZwez",
"user": {
"name": "Minh Tran",
"email": "minh.tran@tenanta.demo"
},
"amount": 499000,
"discount_amount": 99800,
"final_amount": 399200,
"refund_amount": 0,
"currency": "VND",
"status": "FAILED",
"provider": "SEPAY",
"payment_method": "DIGITAL_WALLET",
"failure_reason": null,
"course_titles": [
"Data Science with Python"
],
"created_at": "2026-09-04T08:55:00Z",
"completed_at": null,
"expires_at": null
},
{
"transaction_id": "0ULru2p17fr4CpWDKNQyv",
"user": {
"name": "Minh Tran",
"email": "minh.tran@tenanta.demo"
},
"amount": 299000,
"discount_amount": 0,
"final_amount": 299000,
"refund_amount": 0,
"currency": "VND",
"status": "FAILED",
"provider": "SEPAY",
"payment_method": "DIGITAL_WALLET",
"failure_reason": "Card declined by issuer",
"course_titles": [
"Data Science with Python",
"Machine Learning Foundations"
],
"created_at": "2026-09-03T11:31:00Z",
"completed_at": null,
"expires_at": null
}
],
"total": 9,
"page": 1,
"size": 3,
"total_pages": 3,
"has_next": true,
"has_previous": false
}GET /transactions/{nanoid} (COMPLETED detail)
{
"transaction_id": "mTecQoXsf2o3gyrDO1xkx",
"user": {
"name": "An Le",
"email": "an.le@tenanta.demo"
},
"amount": 299000,
"discount_amount": 0,
"final_amount": 299000,
"refund_amount": 0,
"currency": "VND",
"status": "COMPLETED",
"provider": "SEPAY",
"payment_method": "BANK_TRANSFER",
"failure_reason": null,
"course_titles": [
"Web Development Bootcamp",
"Data Science with Python"
],
"created_at": "2026-09-03T08:38:00Z",
"completed_at": "2026-09-03T09:00:00Z",
"expires_at": null,
"items": [
{
"course_id": "web-dev-bootcamp",
"course_title": "Web Development Bootcamp",
"amount": 149500,
"currency": "VND"
},
{
"course_id": "data-science-python",
"course_title": "Data Science with Python",
"amount": 149500,
"currency": "VND"
}
],
"provider_transaction_id": "SEPAY-TXN-00001-Dyr7OS",
"provider_order_code": "ORD0000001",
"provider_payment_url": null,
"provider_metadata": {
"bank": "VPBank",
"ref": "FT2609034598"
},
"voucher_code": null,
"voucher_percent": null,
"voucher_fixed_amount": null,
"refund_reason": null,
"description": "Purchase: Web Development Bootcamp",
"full_name": "Le Van C",
"phone_number": "0903056413",
"address": null,
"school_name": "RMIT Vietnam",
"updated_at": "2026-09-03T08:38:00Z"
}Checks
Backend (apps/backend)
yarn format— pass (reformatted new files).yarn lint— "All checks passed!".yarn typecheck— no errors in the new/modified files (repo has a large pre-existing baseline elsewhere, unrelated).yarn test:unit— 1521 passed. The new endpoint tests hit the DB engine so they are auto-markeddband skipped by the-m 'not db'filter (same as the existing platform-stats tests); ran explicitly → 13 + 41 passed.
platform-admin (apps/platform-admin)
npm run lint(biome) — clean.npm run build— compiled successfully, TypeScript passed, all 3 new API routes present.
Browser walk (this run)
- Console errors: none.
- Failed network requests (status ≥ 400): none.
- Mobile horizontal scroll:
document.documentElement.scrollWidth = 360on the Payments tab for both Tenant A and Tenant B at a 360px viewport (no horizontal page scroll).
Open items / observations
- Compact money in the table and stat tiles. The shared
formatMoneyutil renders large VND amounts in compact form, so the Amount column and Net revenue tile show e.g.₫1Mfor 990,000 / 1,290,000 and₫7Mfor the monthly total. The detail sheet shows the exact value (₫990,000). This is existing shared-util behavior, not part of the new tab — flagged only as a readability note; not changed, per scope. - No functional bugs found in the Payments tab during the walk. Filters, search, date range, pagination, month stepper, detail sheet, and tenant isolation all behaved as expected.
- Not committed (per spec). App code was not modified.