Apply payment method normalization consistently across user-facing views

Audited every place Payment.method reaches the UI. The main user
dashboard's per-registration payment list (fed by GET
/api/payments/registration/:id) was still showing the raw gateway
string, since that endpoint is shared with the staff-facing supervisor
payments page and wasn't touched by the earlier /mypayments fix.

Extracted the cash/card/eft/voucher/other normalization (matching
normalizeUserMethod on the backend) into frontend/src/lib/paymentMethod.ts
and applied it to both user-facing payment displays. Staff-facing views
(supervisor payments, at-the-door, reports, cashup) intentionally keep
showing the raw method for reconciliation and were left unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 09:41:43 +02:00
co-authored by Claude Sonnet 5
parent 12e5dfc643
commit 193739c042
4 changed files with 31 additions and 17 deletions
+1
View File
@@ -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". - 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 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 ## [1.0.1] - 2026-07-23
+2 -1
View File
@@ -4,6 +4,7 @@ import { useAuth } from "@/hooks/useAuth";
import { apiFetch } from "@/lib/api"; import { apiFetch } from "@/lib/api";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { formatDate } from "@/lib/date"; import { formatDate } from "@/lib/date";
import { formatPaymentMethod } from "@/lib/paymentMethod";
import { QrImage } from "@/components/shared/QrImage"; import { QrImage } from "@/components/shared/QrImage";
// Helper formatters // Helper formatters
@@ -1008,7 +1009,7 @@ export default function UserDashboardPage() {
<div className="font-medium mb-1">Payments</div> <div className="font-medium mb-1">Payments</div>
<ul className="text-sm list-disc pl-5 space-y-1"> <ul className="text-sm list-disc pl-5 space-y-1">
{activeBill.payments.map((p: any) => ( {activeBill.payments.map((p: any) => (
<li key={p.id}>{new Date(p.createdAt).toLocaleString()} {formatRand(p.amount)} ({p.method || "Payment"})</li> <li key={p.id}>{new Date(p.createdAt).toLocaleString()} {formatRand(p.amount)} ({formatPaymentMethod(p.method)})</li>
))} ))}
</ul> </ul>
</div> </div>
@@ -4,6 +4,7 @@ import { useAuth } from "@/hooks/useAuth";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { apiFetch } from "@/lib/api"; import { apiFetch } from "@/lib/api";
import { formatDateTime } from "@/lib/date"; import { formatDateTime } from "@/lib/date";
import { formatPaymentMethod } from "@/lib/paymentMethod";
interface PaymentItem { interface PaymentItem {
id: string; id: string;
@@ -18,21 +19,6 @@ interface PaymentItem {
const formatRand = (n: number) => `R ${Math.abs(n).toFixed(2)}`; 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<string, string> = {
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() { export default function UserPaymentsPage() {
const { user, loading, token } = useAuth(); const { user, loading, token } = useAuth();
const router = useRouter(); const router = useRouter();
@@ -163,7 +149,7 @@ export default function UserPaymentsPage() {
</div> </div>
<div className="text-xs text-gray-500">{formatDateTime(p.createdAt)}</div> <div className="text-xs text-gray-500">{formatDateTime(p.createdAt)}</div>
</div> </div>
<div className="text-xs text-gray-600">Method: {formatMethod(p.method)}</div> <div className="text-xs text-gray-600">Method: {formatPaymentMethod(p.method)}</div>
{eventTitle && <div className="text-xs text-gray-600">Event: {eventTitle}</div>} {eventTitle && <div className="text-xs text-gray-600">Event: {eventTitle}</div>}
</li> </li>
); );
+26
View File
@@ -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<string, string> = {
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)];
}