Fix payment method handling for non-default types (apple_pay, google_pay)
Yoco webhook payments are tagged with whatever wallet type the gateway reports, not just cash/card/eft/voucher. Cashup/report totals were silently dropping those into the "other" bucket instead of "card", and the new user payment history page couldn't filter by them and showed raw snake_case values. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,11 @@ and this project follows [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
- User dashboard: new "Payment history" page listing the user's own payments (donations excluded), with server-side pagination (25 per page), date range, method, and payment/refund filters.
|
- User dashboard: new "Payment history" page listing the user's own payments (donations excluded), with server-side pagination (25 per page), date range, method, and payment/refund filters.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 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: the method filter and display now recognize Apple Pay / Google Pay instead of only cash/card/EFT/voucher, and unrecognized method values are shown with a readable label instead of a raw snake_case/camelCase string.
|
||||||
|
|
||||||
## [1.0.1] - 2026-07-23
|
## [1.0.1] - 2026-07-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ function emptyByMethod(fill = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Normalizes a free-text Payment.method into one of the fixed cashup buckets.
|
// Normalizes a free-text Payment.method into one of the fixed cashup buckets.
|
||||||
|
// Card-network wallets (Apple Pay, Google Pay, etc.) settle exactly like a card payment —
|
||||||
|
// no separate float to reconcile — so they belong in the 'card' bucket, not 'other'.
|
||||||
function bucketForMethod(method) {
|
function bucketForMethod(method) {
|
||||||
const m = String(method || '').toLowerCase();
|
const m = String(method || '').toLowerCase();
|
||||||
if (m.includes('cash')) return 'cash';
|
if (m.includes('cash')) return 'cash';
|
||||||
if (m.includes('eft')) return 'eft';
|
if (m.includes('eft')) return 'eft';
|
||||||
if (m.includes('card') || m.includes('yoco')) return 'card';
|
if (m.includes('card') || m.includes('yoco') || m.includes('pay')) return 'card';
|
||||||
return 'other';
|
return 'other';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,26 @@ interface PaymentItem {
|
|||||||
|
|
||||||
const formatRand = (n: number) => `R ${Math.abs(n).toFixed(2)}`;
|
const formatRand = (n: number) => `R ${Math.abs(n).toFixed(2)}`;
|
||||||
|
|
||||||
|
// Payment.method is free-text — online payments arrive tagged with whatever wallet type the
|
||||||
|
// gateway reports (e.g. apple_pay, google_pay), not just the manual-entry methods below.
|
||||||
|
const METHOD_LABELS: Record<string, string> = {
|
||||||
|
cash: "Cash",
|
||||||
|
card: "Card",
|
||||||
|
eft: "EFT",
|
||||||
|
voucher: "Voucher",
|
||||||
|
apple_pay: "Apple Pay",
|
||||||
|
google_pay: "Google Pay",
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatMethod = (method: string | null | undefined) => {
|
||||||
|
if (!method) return "Payment";
|
||||||
|
if (METHOD_LABELS[method]) return METHOD_LABELS[method];
|
||||||
|
return method
|
||||||
|
.replace(/[_-]+/g, " ")
|
||||||
|
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||||
|
.replace(/\b\w/g, c => c.toUpperCase());
|
||||||
|
};
|
||||||
|
|
||||||
export default function UserPaymentsPage() {
|
export default function UserPaymentsPage() {
|
||||||
const { user, loading, token } = useAuth();
|
const { user, loading, token } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -37,7 +57,7 @@ export default function UserPaymentsPage() {
|
|||||||
// Filters
|
// Filters
|
||||||
const [startDate, setStartDate] = useState("");
|
const [startDate, setStartDate] = useState("");
|
||||||
const [endDate, setEndDate] = useState("");
|
const [endDate, setEndDate] = useState("");
|
||||||
const [method, setMethod] = useState<"" | "cash" | "card" | "eft" | "voucher">("");
|
const [method, setMethod] = useState<"" | "cash" | "card" | "eft" | "voucher" | "apple_pay" | "google_pay">("");
|
||||||
const [kind, setKind] = useState<"" | "payment" | "refund">("");
|
const [kind, setKind] = useState<"" | "payment" | "refund">("");
|
||||||
|
|
||||||
const buildQuery = useCallback((p: number) => {
|
const buildQuery = useCallback((p: number) => {
|
||||||
@@ -110,6 +130,8 @@ export default function UserPaymentsPage() {
|
|||||||
<option value="card">Card</option>
|
<option value="card">Card</option>
|
||||||
<option value="eft">EFT</option>
|
<option value="eft">EFT</option>
|
||||||
<option value="voucher">Voucher</option>
|
<option value="voucher">Voucher</option>
|
||||||
|
<option value="apple_pay">Apple Pay</option>
|
||||||
|
<option value="google_pay">Google Pay</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -147,7 +169,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: {p.method || "payment"}</div>
|
<div className="text-xs text-gray-600">Method: {formatMethod(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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user