Switch payment method filter to a static list with an "other" bucket

Per feedback, drop the dynamic /mypayments/methods lookup (wasn't
loading reliably) in favor of a static cash/card/eft/voucher/other
dropdown. Server-side normalization now folds any gateway-reported
method outside those four manual-entry values (apple_pay, google_pay,
yoco, etc.) into "other" instead of "card", both in the returned data
and in the filter query.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 09:24:53 +02:00
co-authored by Claude Sonnet 5
parent 7f074e43a6
commit 325ab87729
7 changed files with 18 additions and 63 deletions
+8 -39
View File
@@ -352,12 +352,12 @@ const getPayments = async (req, res) => {
// Payment.method is free-text — online checkouts get tagged with whatever wallet type the
// gateway reports (apple_pay, google_pay, yoco, ...), not just the manual-entry methods below.
// For the user-facing dashboard we don't want to expose every raw gateway string, so anything
// that isn't one of the methods staff can manually choose is just shown/filtered as "card".
// that isn't one of the methods staff can manually choose is just shown/filtered as "other".
const USER_FACING_METHODS = ['cash', 'card', 'eft', 'voucher'];
function normalizeUserMethod(method) {
const m = String(method || '').toLowerCase();
return USER_FACING_METHODS.includes(m) ? m : 'card';
return USER_FACING_METHODS.includes(m) ? m : 'other';
}
// @desc Get user payments (paginated, excludes donations, supports date range/method/kind filters)
@@ -380,15 +380,14 @@ const getUserPayments = async (req, res) => {
};
if (req.query.method) {
const requested = normalizeUserMethod(req.query.method);
if (requested === 'card') {
// "Card" also covers every gateway-reported method that isn't one of the other
// explicit buckets (apple_pay, google_pay, yoco, etc.) — match anything NOT in those.
const requested = String(req.query.method).toLowerCase();
if (requested === 'other') {
// "Other" covers every method that isn't one of the four explicit buckets —
// e.g. gateway-reported wallet types like apple_pay, google_pay, yoco, etc.
where.NOT = {
OR: USER_FACING_METHODS.filter(m => m !== 'card')
.map(m => ({ method: { equals: m, mode: 'insensitive' } }))
OR: USER_FACING_METHODS.map(m => ({ method: { equals: m, mode: 'insensitive' } }))
};
} else {
} else if (USER_FACING_METHODS.includes(requested)) {
where.method = { equals: requested, mode: 'insensitive' };
}
}
@@ -429,35 +428,6 @@ const getUserPayments = async (req, res) => {
}
};
// @desc Get the payment method options actually present in the user's own payments (normalized
// to the user-facing set — see normalizeUserMethod), so the dashboard filter only ever
// offers methods that exist for this user.
// @route GET /api/payments/mypayments/methods
// @access Private
const getUserPaymentMethods = async (req, res) => {
try {
const rows = await prisma.payment.findMany({
where: {
isDonation: false,
method: { not: null },
OR: [
{ userId: req.user.id },
{ registration: { userId: req.user.id } }
]
},
distinct: ['method'],
select: { method: true }
});
const present = new Set(rows.map(r => normalizeUserMethod(r.method)));
const methods = USER_FACING_METHODS.filter(m => present.has(m));
res.json({ methods });
} catch (error) {
res.status(res.statusCode === 200 ? 400 : res.statusCode).json({ message: safeErrorMessage(error) });
}
};
// @desc Get payment by ID
// @route GET /api/payments/:id
// @access Private
@@ -1277,7 +1247,6 @@ module.exports = {
createPayment,
getPayments,
getUserPayments,
getUserPaymentMethods,
getPaymentById,
getPaymentsByRegistration,
getPaymentsByEvent,
+2 -4
View File
@@ -543,11 +543,9 @@ app.get('/docs', async (req, res) => {
responses:[
{ status:201, desc:'Recorded', body:{ id:'pay-uuid-...', amount:450, method:'cash', status:'succeeded', createdAt:'2025-06-10T09:00:00.000Z' }},
]},
{ method:'GET', path:'/api/payments/mypayments', auth:'user+', desc:'Get own payment history (paginated, excludes donations). Returned method is normalized to cash|card|eft|voucher — any gateway-reported wallet type (apple_pay, google_pay, yoco, ...) is reported as "card"',
queryParams:{ page:'Page (default 1)', limit:'Per page (default 25, max 25)', startDate:'ISO date, filters createdAt >=', endDate:'ISO date, filters createdAt <=', method:'Filter by normalized method: cash|card|eft|voucher', kind:'payment|refund — filters by amount sign' },
{ method:'GET', path:'/api/payments/mypayments', auth:'user+', desc:'Get own payment history (paginated, excludes donations). Returned method is normalized to cash|card|eft|voucher|other — any gateway-reported wallet type (apple_pay, google_pay, yoco, ...) not in that set is reported as "other"',
queryParams:{ page:'Page (default 1)', limit:'Per page (default 25, max 25)', startDate:'ISO date, filters createdAt >=', endDate:'ISO date, filters createdAt <=', method:'Filter by normalized method: cash|card|eft|voucher|other', kind:'payment|refund — filters by amount sign' },
responses:[{ status:200, desc:'Success', body:{ data:[{ id:'pay-uuid-...', amount:450, method:'card', status:'succeeded', createdAt:'2025-06-01T11:00:00.000Z' }], total:1, page:1, limit:25, pages:1 }}]},
{ method:'GET', path:'/api/payments/mypayments/methods', auth:'user+', desc:"Normalized method options (subset of cash|card|eft|voucher) actually present in the user's own payments",
responses:[{ status:200, desc:'Success', body:{ methods:['card', 'cash'] }}]},
{ method:'GET', path:'/api/payments', auth:'supervisor+', desc:'List all payments',
queryParams:{ page:'Page (default 1)', limit:'Per page (default 20)', eventId:'Filter by event', userId:'Filter by user', method:'Filter by method (cash|card|eft|donation)', startDate:'ISO date', endDate:'ISO date' },
responses:[{ status:200, desc:'Success', body:{ data:[{ id:'pay-uuid-...', amount:450, method:'card', user:{ name:'Jane Doe' }, registration:{ event:{ title:'Camp 2025' }}}], total:1 }}]},
-2
View File
@@ -4,7 +4,6 @@ const {
createPayment,
getPayments,
getUserPayments,
getUserPaymentMethods,
getPaymentById,
getPaymentsByRegistration,
getPaymentsByEvent,
@@ -21,7 +20,6 @@ router.post('/', protect, supervisor, createPayment);
router.post('/yoco-checkout', protect, createYocoCheckout);
router.post('/yoco-checkout/send', protect, supervisor, sendPaymentLink);
router.get('/mypayments', protect, getUserPayments);
router.get('/mypayments/methods', protect, getUserPaymentMethods);
router.get('/:id', protect, getPaymentById);
router.get('/registration/:registrationId', protect, getPaymentsByRegistration);