Populate payment method filter from real data instead of a guessed list

The previous fix hardcoded apple_pay/google_pay as extra filter options,
but Payment.method is free-text set by whatever the gateway reports, so
guessing at literal values was fragile and still didn't surface them for
this user. Add GET /api/payments/mypayments/methods returning the
distinct method values actually present in the user's payments, and have
the dashboard filter build its options from that instead. Also switch
the method filter from a startsWith match to an exact match, since the
values now come straight from the same column being filtered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 08:55:58 +02:00
co-authored by Claude Sonnet 5
parent 90bea25338
commit 59194349d2
7 changed files with 53 additions and 11 deletions
+1
View File
@@ -240,6 +240,7 @@ For unauthenticated (guest) registration on events with `requiresAuth: false`:
| POST | `/api/payments` | supervisor+ | Record manual payment (cash/EFT) |
| POST | `/api/payments/yoco-checkout` | user+ | Initiate Yoco checkout; re-evaluates early-bird pricing first — returns `{ priceUpdated: true, newTotal }` if prices changed |
| GET | `/api/payments/mypayments` | user+ | Own payments |
| GET | `/api/payments/mypayments/methods` | user+ | Distinct `method` values present in the user's own payments |
| GET | `/api/payments/:id` | user+ | Payment by ID |
| GET | `/api/payments/registration/:registrationId` | user+ | Payments for a registration |
| GET | `/api/payments` | supervisor+ | All payments |
+1
View File
@@ -260,6 +260,7 @@ Full interactive API docs available at `GET /docs` (requires admin JWT — pass
| POST | `/` | supervisor+ | Record manual payment |
| POST | `/yoco-checkout` | user+ | Initiate Yoco checkout |
| GET | `/mypayments` | user+ | Own payments |
| GET | `/mypayments/methods` | user+ | Distinct `method` values present in the user's own payments |
| GET | `/:id` | user+ | Payment by ID |
| GET | `/registration/:registrationId` | user+ | Payments for a registration |
| GET | `/` | supervisor+ | All payments |
+29 -1
View File
@@ -369,7 +369,7 @@ const getUserPayments = async (req, res) => {
};
if (req.query.method) {
where.method = { startsWith: String(req.query.method), mode: 'insensitive' };
where.method = { equals: String(req.query.method), mode: 'insensitive' };
}
if (req.query.kind === 'refund') {
@@ -405,6 +405,33 @@ const getUserPayments = async (req, res) => {
}
};
// @desc Get the distinct payment methods actually present in the user's own payments,
// so the dashboard filter never has to guess at gateway-reported values
// (e.g. apple_pay, google_pay) — it only ever offers what's really there.
// @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 },
orderBy: { method: 'asc' }
});
res.json({ methods: rows.map(r => r.method).filter(Boolean) });
} 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
@@ -1224,6 +1251,7 @@ module.exports = {
createPayment,
getPayments,
getUserPayments,
getUserPaymentMethods,
getPaymentById,
getPaymentsByRegistration,
getPaymentsByEvent,
+3 -1
View File
@@ -544,8 +544,10 @@ app.get('/docs', async (req, res) => {
{ 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)',
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 method prefix (cash|card|eft|voucher)', kind:'payment|refund — filters by amount sign' },
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 exact method value (see /mypayments/methods for the actual set)', 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:"Distinct Payment.method values present in the user's own payments (method is free-text — gateways can report values like apple_pay/google_pay beyond the manual-entry set)",
responses:[{ status:200, desc:'Success', body:{ methods:['card', 'cash', 'apple_pay'] }}]},
{ 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,6 +4,7 @@ const {
createPayment,
getPayments,
getUserPayments,
getUserPaymentMethods,
getPaymentById,
getPaymentsByRegistration,
getPaymentsByEvent,
@@ -20,6 +21,7 @@ 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);