Conversation
- Implemented API routes for submitting and retrieving payment proofs for teams. - Created constants for payment-related configurations including UPI IDs and payment amounts. - Developed utility functions for handling payment proof file validation and storage. - Added tests for payment-related functionalities to ensure correctness. - Established database migrations for creating a storage bucket for payment proofs with appropriate access policies.
There was a problem hiding this comment.
Pull request overview
Adds an end-to-end “accepted team payment” flow: statement-specific UPI QR display, payment proof upload + status tracking, and an admin queue for reviewing/approving/rejecting proofs.
Changes:
- Introduces payment domain utilities/constants (statuses, QR config, proof validation, storage paths).
- Adds user payment-proof upload + download APIs, dashboard UI, and rate limits.
- Adds admin payment-proof listing/review APIs + admin UI section, plus storage bucket + RLS policies.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| supabase/migrations/202603070001_insert_dashboard_test_registration.sql | Adds a migration that seeds a demo accepted registration (for dashboard testing). |
| supabase/migrations/202603070002_create_payment_proof_bucket.sql | Creates a private Storage bucket and RLS policies for payment proof objects. |
| src/server/security/rate-limit.ts | Adds rate-limit policies for payment proof upload/view (IP + user). |
| src/server/registration/service.ts | Preserves payment metadata during patch flows and removes payment proof objects on team deletion. |
| src/server/registration/payment-proofs.ts | Implements submit + signed-download URL services for team payment proofs. |
| src/server/registration/mappers.ts | Maps payment metadata fields from registration details into TeamRecord. |
| src/server/registration/mappers.test.ts | Adds tests for mapping and dropping invalid payment metadata. |
| src/server/mass-mail/team-decision.ts | Updates decision emails to link directly to team dashboard and includes payment instructions; adds required teamId. |
| src/server/mass-mail/team-decision.test.ts | Updates tests for new decision email content/linking. |
| src/server/admin/review-submissions.ts | Passes teamId into decision mail sender. |
| src/server/admin/review-submissions.test.ts | Updates expectations for decision mail payload including teamId. |
| src/server/admin/payment-proofs.ts | Adds admin services to list payment proofs, update payment decisions, and generate signed URLs. |
| src/server/admin/payment-proofs.test.ts | Adds tests for listing and decision updates (approve/reject reason required). |
| src/lib/register-schema.ts | Extends team record schema/types to include payment metadata + payment status enum. |
| src/lib/register-schema.test.ts | Adds schema tests for valid/invalid payment metadata in team records. |
| src/lib/payments.ts | Adds payment statuses, QR config builder, and payment proof validation/path helpers. |
| src/lib/payments.test.ts | Tests QR config coverage and status values. |
| src/lib/payment-constants.ts | Adds constants for INR amount and per-statement UPI IDs (currently defaulted). |
| src/app/dashboard/[teamId]/page.tsx | Adds dashboard payment section (QR generation, UTR + proof upload, ticket unlock gating). |
| src/app/api/register/[teamId]/payment-proof/route.ts | Adds authenticated GET (redirect to signed URL) and POST (multipart upload) endpoints. |
| src/app/api/register/[teamId]/payment-proof/route.test.ts | Adds route tests for CSRF, validation, upload success, and GET redirect/error paths. |
| src/app/api/admin/payment-proofs/route.ts | Adds admin GET endpoint for payment-proof queue with statement filtering. |
| src/app/api/admin/payment-proofs/route.test.ts | Adds tests for auth/admin enforcement and statement filter validation. |
| src/app/api/admin/payment-proofs/[teamId]/route.ts | Adds admin PATCH endpoint to approve/reject payment proofs (CSRF + JSON validation). |
| src/app/api/admin/payment-proofs/[teamId]/route.test.ts | Adds tests for CSRF failure, payload validation, and success path. |
| src/app/api/admin/payment-proofs/[teamId]/file/route.ts | Adds admin GET endpoint to redirect to signed proof URL (auth + admin + rate limits). |
| src/app/api/admin/payment-proofs/[teamId]/file/route.test.ts | Adds tests for redirect and unauthenticated forwarding. |
| src/app/admin/admin-review-client.tsx | Adds admin “Payment Queue” section and navigation, plus approve/reject interactions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| -- Seeds a fake accepted registration for the maintainer account so the | ||
| -- dashboard can be exercised end-to-end in local/dev environments. | ||
| -- | ||
| -- Assumption: the target auth account uses opdhaker2007@gmail.com. | ||
| -- If your local Supabase auth user uses a different email, update the value | ||
| -- below before applying this migration. | ||
| do $$ | ||
| declare | ||
| v_target_email constant text := 'opdhaker2007@gmail.com'; | ||
| v_target_user_id uuid; | ||
| begin | ||
| select users.id | ||
| into v_target_user_id | ||
| from auth.users as users | ||
| where lower(users.email) = lower(v_target_email) | ||
| order by users.created_at desc | ||
| limit 1; | ||
|
|
||
| if v_target_user_id is null then | ||
| raise notice | ||
| 'Skipping dashboard test registration seed because no auth.users row exists for %.', | ||
| v_target_email; | ||
| else | ||
| delete from public.eventsregistrations | ||
| where event_id = '325b1472-4ce9-412f-8a5e-e4b7153064fa'::uuid | ||
| and application_id = v_target_user_id | ||
| and coalesce(details ->> 'isTestEntry', 'false') = 'true'; | ||
|
|
||
| insert into public.eventsregistrations ( | ||
| event_id, | ||
| event_title, | ||
| application_id, | ||
| details, | ||
| registration_email, | ||
| is_team_entry, | ||
| is_approved | ||
| ) | ||
| values ( | ||
| '325b1472-4ce9-412f-8a5e-e4b7153064fa'::uuid, | ||
| 'Foundathon 3.0', | ||
| v_target_user_id, | ||
| jsonb_build_object( | ||
| 'teamType', 'non_srm', | ||
| 'teamName', 'Dashboard Demo Team', | ||
| 'collegeName', 'Foundathon Test University', | ||
| 'isClub', true, | ||
| 'clubName', 'Founders Club QA', | ||
| 'lead', jsonb_build_object( | ||
| 'name', 'Uttam Demo', | ||
| 'collegeId', 'FTU-LEAD-001', | ||
| 'collegeEmail', v_target_email, | ||
| 'contact', 9876543210 | ||
| ), | ||
| 'members', jsonb_build_array( | ||
| jsonb_build_object( | ||
| 'name', 'Demo Member One', | ||
| 'collegeId', 'FTU-MEM-002', | ||
| 'collegeEmail', 'member.one@example.com', | ||
| 'contact', 9876543211 | ||
| ), | ||
| jsonb_build_object( | ||
| 'name', 'Demo Member Two', | ||
| 'collegeId', 'FTU-MEM-003', | ||
| 'collegeEmail', 'member.two@example.com', | ||
| 'contact', 9876543212 | ||
| ) | ||
| ), | ||
| 'problemStatementCap', 15, | ||
| 'problemStatementId', 'ps-03', | ||
| 'problemStatementLockedAt', '2026-03-07T00:00:00.000Z', | ||
| 'problemStatementTitle', 'Localized AI Skills Training Platform', | ||
| 'presentationFileName', 'dashboard-demo-submission.pptx', | ||
| 'presentationFileSizeBytes', 1048576, | ||
| 'presentationMimeType', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', | ||
| 'presentationPublicUrl', 'https://example.com/foundathon/dashboard-demo-submission.pptx', | ||
| 'presentationStoragePath', 'registrations/dashboard-demo/submission.pptx', | ||
| 'presentationUploadedAt', '2026-03-07T00:00:00.000Z', | ||
| 'isTestEntry', true, | ||
| 'testEntryLabel', 'dashboard-demo' | ||
| ), | ||
| v_target_email, | ||
| true, | ||
| 'ACCEPTED' | ||
| ); | ||
| end if; |
There was a problem hiding this comment.
This migration seeds a fake accepted registration into public.eventsregistrations. Supabase migrations are typically applied to every environment (including production), so this will permanently insert test/PII data (hard-coded personal email, dummy phone numbers, etc.) into real databases. Please move this to a dev-only seed mechanism (e.g., a separate seed.sql/manual script) or gate it behind an explicit, environment-specific process rather than a numbered migration.
| -- Seeds a fake accepted registration for the maintainer account so the | |
| -- dashboard can be exercised end-to-end in local/dev environments. | |
| -- | |
| -- Assumption: the target auth account uses opdhaker2007@gmail.com. | |
| -- If your local Supabase auth user uses a different email, update the value | |
| -- below before applying this migration. | |
| do $$ | |
| declare | |
| v_target_email constant text := 'opdhaker2007@gmail.com'; | |
| v_target_user_id uuid; | |
| begin | |
| select users.id | |
| into v_target_user_id | |
| from auth.users as users | |
| where lower(users.email) = lower(v_target_email) | |
| order by users.created_at desc | |
| limit 1; | |
| if v_target_user_id is null then | |
| raise notice | |
| 'Skipping dashboard test registration seed because no auth.users row exists for %.', | |
| v_target_email; | |
| else | |
| delete from public.eventsregistrations | |
| where event_id = '325b1472-4ce9-412f-8a5e-e4b7153064fa'::uuid | |
| and application_id = v_target_user_id | |
| and coalesce(details ->> 'isTestEntry', 'false') = 'true'; | |
| insert into public.eventsregistrations ( | |
| event_id, | |
| event_title, | |
| application_id, | |
| details, | |
| registration_email, | |
| is_team_entry, | |
| is_approved | |
| ) | |
| values ( | |
| '325b1472-4ce9-412f-8a5e-e4b7153064fa'::uuid, | |
| 'Foundathon 3.0', | |
| v_target_user_id, | |
| jsonb_build_object( | |
| 'teamType', 'non_srm', | |
| 'teamName', 'Dashboard Demo Team', | |
| 'collegeName', 'Foundathon Test University', | |
| 'isClub', true, | |
| 'clubName', 'Founders Club QA', | |
| 'lead', jsonb_build_object( | |
| 'name', 'Uttam Demo', | |
| 'collegeId', 'FTU-LEAD-001', | |
| 'collegeEmail', v_target_email, | |
| 'contact', 9876543210 | |
| ), | |
| 'members', jsonb_build_array( | |
| jsonb_build_object( | |
| 'name', 'Demo Member One', | |
| 'collegeId', 'FTU-MEM-002', | |
| 'collegeEmail', 'member.one@example.com', | |
| 'contact', 9876543211 | |
| ), | |
| jsonb_build_object( | |
| 'name', 'Demo Member Two', | |
| 'collegeId', 'FTU-MEM-003', | |
| 'collegeEmail', 'member.two@example.com', | |
| 'contact', 9876543212 | |
| ) | |
| ), | |
| 'problemStatementCap', 15, | |
| 'problemStatementId', 'ps-03', | |
| 'problemStatementLockedAt', '2026-03-07T00:00:00.000Z', | |
| 'problemStatementTitle', 'Localized AI Skills Training Platform', | |
| 'presentationFileName', 'dashboard-demo-submission.pptx', | |
| 'presentationFileSizeBytes', 1048576, | |
| 'presentationMimeType', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', | |
| 'presentationPublicUrl', 'https://example.com/foundathon/dashboard-demo-submission.pptx', | |
| 'presentationStoragePath', 'registrations/dashboard-demo/submission.pptx', | |
| 'presentationUploadedAt', '2026-03-07T00:00:00.000Z', | |
| 'isTestEntry', true, | |
| 'testEntryLabel', 'dashboard-demo' | |
| ), | |
| v_target_email, | |
| true, | |
| 'ACCEPTED' | |
| ); | |
| end if; | |
| -- NOTE: This migration was originally used to seed a fake accepted | |
| -- registration for dashboard testing in local/dev environments. | |
| -- | |
| -- Seeding test/demo data via numbered Supabase migrations is unsafe because | |
| -- those migrations are typically applied to every environment, including | |
| -- production. That would permanently insert test/PII-like data into real | |
| -- databases. | |
| -- | |
| -- To create dashboard demo data, copy the appropriate INSERT statement(s) | |
| -- into a separate, dev-only seed script (for example, a local `seed.sql` | |
| -- file or a manual psql script) and run it explicitly only in non-prod | |
| -- environments. | |
| -- | |
| -- This migration is intentionally a no-op to avoid seeding data anywhere. | |
| do $$ | |
| begin | |
| raise notice | |
| 'Migration 202603070001_insert_dashboard_test_registration: no-op by design. ' | |
| 'Use a separate dev-only seed script to create dashboard test registrations.'; |
| -- Assumption: the target auth account uses opdhaker2007@gmail.com. | ||
| -- If your local Supabase auth user uses a different email, update the value | ||
| -- below before applying this migration. | ||
| do $$ | ||
| declare | ||
| v_target_email constant text := 'opdhaker2007@gmail.com'; |
There was a problem hiding this comment.
The hard-coded target email (opdhaker2007@gmail.com) looks like real PII and will be written into whichever database applies this migration. If you keep any kind of seeding here, avoid real emails and prefer an obviously fake/example domain (or derive the user by a non-PII identifier).
| -- Assumption: the target auth account uses opdhaker2007@gmail.com. | |
| -- If your local Supabase auth user uses a different email, update the value | |
| -- below before applying this migration. | |
| do $$ | |
| declare | |
| v_target_email constant text := 'opdhaker2007@gmail.com'; | |
| -- Assumption: the target auth account uses dashboard.maintainer@example.com. | |
| -- If your local Supabase auth user uses a different email, update the value | |
| -- below before applying this migration. | |
| do $$ | |
| declare | |
| v_target_email constant text := 'dashboard.maintainer@example.com'; |
| export const PAYMENT_AMOUNT_INR = 300; | ||
| export const DEFAULT_PAYMENT_UPI_ID = "9301161940@fam"; | ||
|
|
||
| export const PAYMENT_UPI_ID_BY_STATEMENT = Object.fromEntries( | ||
| PROBLEM_STATEMENTS.map((statement) => [statement.id, DEFAULT_PAYMENT_UPI_ID]), | ||
| ) as Record<(typeof PROBLEM_STATEMENTS)[number]["id"], string>; |
There was a problem hiding this comment.
DEFAULT_PAYMENT_UPI_ID is hard-coded to a specific UPI/VPA. If this repo is shared beyond a private ops context, this can unintentionally expose real payment routing info and makes rotations/environment differences harder. Consider sourcing this from environment/config (or Supabase event settings) and keeping only non-sensitive defaults in code.
| } | ||
|
|
||
| if (!isPaymentProofExtensionAllowed(fileName)) { | ||
| return fail("Only PNG, JPG, WEBP, or PDF payment proofs are allowed.", 400); |
There was a problem hiding this comment.
The extension allowlist includes .jpeg, but this error message omits JPEG (it says PNG/JPG/WEBP/PDF only). To avoid confusing users, update the message to include JPEG as an allowed type (or drop .jpeg from the allowlist if it’s not intended).
| return fail("Only PNG, JPG, WEBP, or PDF payment proofs are allowed.", 400); | |
| return fail( | |
| "Only PNG, JPG/JPEG, WEBP, or PDF payment proofs are allowed.", | |
| 400, | |
| ); |
| using ( | ||
| bucket_id = 'foundathon-payment-proofs' | ||
| and (storage.foldername(name))[1] = 'payment-proofs' | ||
| and (storage.foldername(name))[2] = auth.uid()::text | ||
| ); | ||
|
|
||
| create policy "payment proof owners can upload" | ||
| on storage.objects | ||
| for insert | ||
| to authenticated | ||
| with check ( | ||
| bucket_id = 'foundathon-payment-proofs' | ||
| and (storage.foldername(name))[1] = 'payment-proofs' | ||
| and (storage.foldername(name))[2] = auth.uid()::text | ||
| ); |
There was a problem hiding this comment.
The storage RLS predicates only constrain the first two path segments (payment-proofs/<uid>/...), which still allows an authenticated user to upload arbitrary objects under their UID folder (unlimited filenames/subfolders) and use this bucket as general-purpose storage. If this bucket is meant strictly for one proof per team, consider tightening the policy to an exact key pattern (e.g., regex/LIKE enforcing payment-proofs/<uid>/<teamId>/proof.<ext> and allowed extensions) to reduce storage abuse risk.
No description provided.