diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6ec83c5..1b17a58 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ and this project follows [Semantic Versioning](https://semver.org/).
- Cashup/reports: payments tagged with a digital wallet method (e.g. `apple_pay`, `google_pay` from online checkouts) are now bucketed as "card" for reconciliation instead of silently falling into "other".
- User dashboard payment history: `GET /api/payments/mypayments` now normalizes `method` to the fixed set cash/card/eft/voucher/other. Card-network wallet payments (`apple_pay`, `google_pay`) are reported and filterable as "card"; any other gateway-reported value falls under "other" — instead of exposing raw, inconsistent gateway strings the filter dropdown didn't know about.
+- User dashboard: the registration payment list (shown on the main dashboard when viewing a registration's bill) applies the same cash/card/eft/voucher/other normalization client-side, so it no longer shows a raw `apple_pay`/`google_pay` string. Staff-facing payment views (supervisor payments, reports, cashup) are unaffected — they still show the raw method, which is what reconciliation needs.
## [1.0.1] - 2026-07-23
diff --git a/frontend/src/app/dashboard/user/page.tsx b/frontend/src/app/dashboard/user/page.tsx
index 981b290..8180b5f 100644
--- a/frontend/src/app/dashboard/user/page.tsx
+++ b/frontend/src/app/dashboard/user/page.tsx
@@ -4,6 +4,7 @@ import { useAuth } from "@/hooks/useAuth";
import { apiFetch } from "@/lib/api";
import { useRouter } from "next/navigation";
import { formatDate } from "@/lib/date";
+import { formatPaymentMethod } from "@/lib/paymentMethod";
import { QrImage } from "@/components/shared/QrImage";
// Helper formatters
@@ -1008,7 +1009,7 @@ export default function UserDashboardPage() {
Payments
{activeBill.payments.map((p: any) => (
- - {new Date(p.createdAt).toLocaleString()} — {formatRand(p.amount)} ({p.method || "Payment"})
+ - {new Date(p.createdAt).toLocaleString()} — {formatRand(p.amount)} ({formatPaymentMethod(p.method)})
))}
diff --git a/frontend/src/app/dashboard/user/payments/page.tsx b/frontend/src/app/dashboard/user/payments/page.tsx
index 4208c3d..26af19e 100644
--- a/frontend/src/app/dashboard/user/payments/page.tsx
+++ b/frontend/src/app/dashboard/user/payments/page.tsx
@@ -4,6 +4,7 @@ import { useAuth } from "@/hooks/useAuth";
import { useRouter } from "next/navigation";
import { apiFetch } from "@/lib/api";
import { formatDateTime } from "@/lib/date";
+import { formatPaymentMethod } from "@/lib/paymentMethod";
interface PaymentItem {
id: string;
@@ -18,21 +19,6 @@ interface PaymentItem {
const formatRand = (n: number) => `R ${Math.abs(n).toFixed(2)}`;
-// The API normalizes Payment.method to this set before it ever reaches the dashboard — any
-// gateway-reported wallet type (apple_pay, google_pay, yoco, ...) is folded into "other".
-const METHOD_LABELS: Record = {
- cash: "Cash",
- card: "Card",
- eft: "EFT",
- voucher: "Voucher",
- other: "Other",
-};
-
-const formatMethod = (method: string | null | undefined) => {
- if (!method) return "Payment";
- return METHOD_LABELS[method] || method;
-};
-
export default function UserPaymentsPage() {
const { user, loading, token } = useAuth();
const router = useRouter();
@@ -163,7 +149,7 @@ export default function UserPaymentsPage() {
{formatDateTime(p.createdAt)}
- Method: {formatMethod(p.method)}
+ Method: {formatPaymentMethod(p.method)}
{eventTitle && Event: {eventTitle}
}
);
diff --git a/frontend/src/lib/paymentMethod.ts b/frontend/src/lib/paymentMethod.ts
new file mode 100644
index 0000000..3a2e4ea
--- /dev/null
+++ b/frontend/src/lib/paymentMethod.ts
@@ -0,0 +1,26 @@
+// Payment.method is free-text — online checkouts get tagged with whatever wallet type the
+// gateway reports (apple_pay, google_pay, ...), not just the manual-entry methods below.
+// Mirrors normalizeUserMethod in backend/src/controllers/paymentController.js: card-network
+// wallets count as "card" (same settlement, no separate float); anything else is "other".
+const USER_FACING_METHODS = ["cash", "card", "eft", "voucher"];
+const CARD_ALIASES = ["apple_pay", "google_pay"];
+
+const METHOD_LABELS: Record = {
+ cash: "Cash",
+ card: "Card",
+ eft: "EFT",
+ voucher: "Voucher",
+ other: "Other",
+};
+
+export function normalizePaymentMethod(method: string | null | undefined): string {
+ const m = String(method || "").toLowerCase();
+ if (USER_FACING_METHODS.includes(m)) return m;
+ if (CARD_ALIASES.includes(m)) return "card";
+ return "other";
+}
+
+export function formatPaymentMethod(method: string | null | undefined): string {
+ if (!method) return "Payment";
+ return METHOD_LABELS[normalizePaymentMethod(method)];
+}