diff --git a/CHANGELOG.md b/CHANGELOG.md index cfcf228..d6f34c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project follows [Semantic Versioning](https://semver.org/). ### 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: `GET /api/payments/mypayments` and its new `/methods` endpoint now normalize `method` to cash/card/eft/voucher — any other gateway-reported value (apple_pay, google_pay, yoco, etc.) is reported and filterable as "card" — 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 — any gateway-reported value outside that set (apple_pay, google_pay, yoco, etc.) is reported and filterable as "other" — instead of exposing raw, inconsistent gateway strings the filter dropdown didn't know about. ## [1.0.1] - 2026-07-23 diff --git a/backend/API_DOCUMENTATION.md b/backend/API_DOCUMENTATION.md index 0979db2..89f23af 100644 --- a/backend/API_DOCUMENTATION.md +++ b/backend/API_DOCUMENTATION.md @@ -240,7 +240,6 @@ 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+ | Normalized method options (cash/card/eft/voucher) 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 | diff --git a/backend/README.md b/backend/README.md index 45dd133..ab865aa 100644 --- a/backend/README.md +++ b/backend/README.md @@ -260,7 +260,6 @@ 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+ | Normalized method options (cash/card/eft/voucher) 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 | diff --git a/backend/src/controllers/paymentController.js b/backend/src/controllers/paymentController.js index b7463a9..cd75a53 100644 --- a/backend/src/controllers/paymentController.js +++ b/backend/src/controllers/paymentController.js @@ -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, diff --git a/backend/src/index.js b/backend/src/index.js index 0605096..b84aa9e 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -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 }}]}, diff --git a/backend/src/routes/paymentRoutes.js b/backend/src/routes/paymentRoutes.js index 1677f6e..1774b47 100644 --- a/backend/src/routes/paymentRoutes.js +++ b/backend/src/routes/paymentRoutes.js @@ -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); diff --git a/frontend/src/app/dashboard/user/payments/page.tsx b/frontend/src/app/dashboard/user/payments/page.tsx index ed853bf..4208c3d 100644 --- a/frontend/src/app/dashboard/user/payments/page.tsx +++ b/frontend/src/app/dashboard/user/payments/page.tsx @@ -19,12 +19,13 @@ interface PaymentItem { 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 "card". +// gateway-reported wallet type (apple_pay, google_pay, yoco, ...) is folded into "other". const METHOD_LABELS: Record = { cash: "Cash", card: "Card", eft: "EFT", voucher: "Voucher", + other: "Other", }; const formatMethod = (method: string | null | undefined) => { @@ -54,17 +55,6 @@ export default function UserPaymentsPage() { const [method, setMethod] = useState(""); const [kind, setKind] = useState<"" | "payment" | "refund">(""); - // Method is free-text (gateways can report values like apple_pay/google_pay beyond the - // manual-entry set), so the filter options come from what's actually in the user's payments - // rather than a hardcoded guess. - const [availableMethods, setAvailableMethods] = useState([]); - useEffect(() => { - if (!token) return; - apiFetch("/api/payments/mypayments/methods", { authToken: token }) - .then(res => setAvailableMethods(Array.isArray(res?.methods) ? res.methods : [])) - .catch(() => {}); - }, [token]); - const buildQuery = useCallback((p: number) => { const qs = new URLSearchParams({ page: String(p), limit: "25" }); if (startDate) qs.set("startDate", startDate); @@ -131,9 +121,11 @@ export default function UserPaymentsPage() {