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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user