From 40d86e28dbc10d3d48bba1efca758fbd432735e9 Mon Sep 17 00:00:00 2001 From: Hugo Liu <98724522+hugoliu-code@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:11:24 -0500 Subject: [PATCH 1/5] Add Withdraw Attendance + Update Withdraw Acceptance + Fix some documentation --- apps/api/internal/api/api.go | 3 + apps/api/internal/api/handlers/application.go | 58 ++++++++++++++----- apps/api/internal/services/application.go | 29 +++++++++- 3 files changed, 74 insertions(+), 16 deletions(-) diff --git a/apps/api/internal/api/api.go b/apps/api/internal/api/api.go index 9001c779..3fa89676 100644 --- a/apps/api/internal/api/api.go +++ b/apps/api/internal/api/api.go @@ -192,6 +192,9 @@ func (api *API) setupRoutes(mw *mw.Middleware) { //Accept acceptance r.Patch("/accept-acceptance", api.Handlers.Application.AcceptApplicationAcceptance) + //Withdraw attendance + r.Patch("/withdraw-attendance", api.Handlers.Application.WithdrawAttendance) + //Waitlist application r.Patch("/join-waitlist", api.Handlers.Application.JoinWaitlist) }) diff --git a/apps/api/internal/api/handlers/application.go b/apps/api/internal/api/handlers/application.go index 7fcfc706..c7175874 100644 --- a/apps/api/internal/api/handlers/application.go +++ b/apps/api/internal/api/handlers/application.go @@ -375,14 +375,14 @@ func (h *ApplicationHandler) GetApplicationStatistics(w http.ResponseWriter, r * func (h *ApplicationHandler) GetApplication(w http.ResponseWriter, r *http.Request) { eventId, err := web.PathParamToUUID(r, "eventId") if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid.")) + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) return } // So funny story, there is no ID in the application table, this is just an abstracted user_id. applicationId, err := web.PathParamToUUID(r, "applicationId") if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_application_id", "The application ID is not a valid.")) + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_application_id", "The application ID is not valid.")) return } @@ -417,13 +417,13 @@ type ReviewRatings struct { func (h *ApplicationHandler) SubmitApplicationReview(w http.ResponseWriter, r *http.Request) { eventId, err := web.PathParamToUUID(r, "eventId") if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid.")) + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) return } applicationId, err := web.PathParamToUUID(r, "applicationId") if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_application_id", "The application ID is not a valid.")) + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_application_id", "The application ID is not valid.")) return } @@ -468,7 +468,7 @@ func (h *ApplicationHandler) SubmitApplicationReview(w http.ResponseWriter, r *h func (h *ApplicationHandler) GetAssignedApplications(w http.ResponseWriter, r *http.Request) { eventId, err := web.PathParamToUUID(r, "eventId") if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid.")) + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) return } @@ -502,7 +502,7 @@ func (h *ApplicationHandler) GetAssignedApplications(w http.ResponseWriter, r *h func (h *ApplicationHandler) AssignApplicationReviewers(w http.ResponseWriter, r *http.Request) { eventId, err := web.PathParamToUUID(r, "eventId") if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid.")) + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) return } @@ -536,7 +536,7 @@ func (h *ApplicationHandler) AssignApplicationReviewers(w http.ResponseWriter, r func (h *ApplicationHandler) ResetApplicationReviews(w http.ResponseWriter, r *http.Request) { eventId, err := web.PathParamToUUID(r, "eventId") if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid.")) + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) return } @@ -603,7 +603,7 @@ func (h *ApplicationHandler) GetResumePresignedUrl(w http.ResponseWriter, r *htt func (h *ApplicationHandler) JoinWaitlist(w http.ResponseWriter, r *http.Request) { eventId, err := web.PathParamToUUID(r, "eventId") if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid.")) + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) return } userId := ctxutils.GetUserIdFromCtx(r.Context()) @@ -623,15 +623,15 @@ func (h *ApplicationHandler) JoinWaitlist(w http.ResponseWriter, r *http.Request // @Description Sets application status from accepted to rejected // @Tags Application // -// @Param eventId path string true "ID of the event to join the waitlist for" -// @Success 200 "Acceptance withdrawn joined successfully" +// @Param eventId path string true "ID of the event to withdraw acceptance from" +// @Success 200 "Acceptance withdrawn successfully" // @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID" -// @Failure 500 {object} res.ErrorResponse "Server error: failed to withdraw" +// @Failure 500 {object} res.ErrorResponse "Server error: failed to withdraw acceptance" // @Router /events/{eventId}/application/withdraw-acceptance [patch] func (h *ApplicationHandler) WithdrawAcceptance(w http.ResponseWriter, r *http.Request) { eventId, err := web.PathParamToUUID(r, "eventId") if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid.")) + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) return } userId := ctxutils.GetUserIdFromCtx(r.Context()) @@ -645,10 +645,38 @@ func (h *ApplicationHandler) WithdrawAcceptance(w http.ResponseWriter, r *http.R w.WriteHeader(http.StatusOK) } +// Withdraw attendance to an event +// +// @Summary Withdraw attendance after accepting to go to an event. +// @Description Sets application status from accepted to withdrawn. Sets event role from attendee, back to applicant. +// @Tags Application +// +// @Param eventId path string true "ID of the event to withdraw attendance from" +// @Success 200 "Attendance withdrawn successfully" +// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID" +// @Failure 500 {object} res.ErrorResponse "Server error: failed to withdraw attendance" +// @Router /events/{eventId}/application/withdraw-attendance [patch] +func (h *ApplicationHandler) WithdrawAttendance(w http.ResponseWriter, r *http.Request) { + eventId, err := web.PathParamToUUID(r, "eventId") + if err != nil { + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) + return + } + userId := ctxutils.GetUserIdFromCtx(r.Context()) + + err = h.appService.WithdrawAttendance(r.Context(), *userId, eventId) + if err != nil { + res.SendError(w, http.StatusInternalServerError, res.NewError("withdraw_attendance_error", "Something went wrong while withdrawing attendance")) + return + } + w.WriteHeader(http.StatusOK) + +} + // Accept an Acceptance for an Event/Application // // @Summary Accept an acceptance after being accepted to an event. -// @Description Sets application status from accepted to rejected +// @Description Sets event role to attendee, from applicant // @Tags Application Event // // @Param eventId path string true "ID of the event to join the waitlist for" @@ -659,7 +687,7 @@ func (h *ApplicationHandler) WithdrawAcceptance(w http.ResponseWriter, r *http.R func (h *ApplicationHandler) AcceptApplicationAcceptance(w http.ResponseWriter, r *http.Request) { eventId, err := web.PathParamToUUID(r, "eventId") if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid.")) + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) return } userId := ctxutils.GetUserIdFromCtx(r.Context()) @@ -687,7 +715,7 @@ func (h *ApplicationHandler) AcceptApplicationAcceptance(w http.ResponseWriter, func (h *ApplicationHandler) TransitionWaitlistedApplications(w http.ResponseWriter, r *http.Request) { eventId, err := web.PathParamToUUID(r, "eventId") if err != nil { - res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid.")) + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid.")) return } diff --git a/apps/api/internal/services/application.go b/apps/api/internal/services/application.go index 6ceba220..496f8f58 100644 --- a/apps/api/internal/services/application.go +++ b/apps/api/internal/services/application.go @@ -542,7 +542,34 @@ func (s *ApplicationService) WithdrawAcceptance(ctx context.Context, userId uuid EventID: eventId, //TODO: Make it so I don't have to set this! StatusDoUpdate: true, - Status: sqlc.ApplicationStatusRejected, + Status: sqlc.ApplicationStatusWithdrawn, + }) + if err != nil { + s.logger.Err(err).Msg(err.Error()) + return err + } + return nil +} + +func (s *ApplicationService) WithdrawAttendance(ctx context.Context, userId uuid.UUID, eventId uuid.UUID) error { + // Make atomic + err := s.txm.WithTx(ctx, func(tx pgx.Tx) error { + txAppRepo := s.appRepo.NewTx(tx) + txEventRepo := s.eventsService.eventRepo.NewTx(tx) + if err := txAppRepo.UpdateApplication(ctx, sqlc.UpdateApplicationParams{ + UserID: userId, + EventID: eventId, + StatusDoUpdate: true, + Status: sqlc.ApplicationStatusWithdrawn, + }); err != nil { + return err + } + + return txEventRepo.UpdateRole(ctx, + userId, + eventId, + sqlc.EventRoleTypeApplicant, + ) }) if err != nil { s.logger.Err(err).Msg(err.Error()) From b5edbc1e06da602587b9d699a205e2a8aa0c0887 Mon Sep 17 00:00:00 2001 From: Hugo Liu <98724522+hugoliu-code@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:26:12 -0500 Subject: [PATCH 2/5] Frontend --- .../src/features/Event/applicationStatus.ts | 12 ++- ...tsx => EventAcceptanceWithdrawalModal.tsx} | 14 +-- .../EventAttendanceWithdrawalModal.tsx | 86 +++++++++++++++++++ .../features/Event/components/EventButton.tsx | 12 ++- apps/web/src/features/Event/utils/mapper.ts | 2 +- .../components/AttendeeOverview.tsx | 27 +++++- .../events/$eventId/dashboard/index.tsx | 2 +- 7 files changed, 141 insertions(+), 14 deletions(-) rename apps/web/src/features/Event/components/{EventWithdrawalModal.tsx => EventAcceptanceWithdrawalModal.tsx} (85%) create mode 100644 apps/web/src/features/Event/components/EventAttendanceWithdrawalModal.tsx diff --git a/apps/web/src/features/Event/applicationStatus.ts b/apps/web/src/features/Event/applicationStatus.ts index 5d44fc60..d6d875b3 100644 --- a/apps/web/src/features/Event/applicationStatus.ts +++ b/apps/web/src/features/Event/applicationStatus.ts @@ -114,7 +114,7 @@ const applicationStatus = defineStatus({ button: { className: "bg-event-button-bg-not-going text-event-button-text-not-going hover:bg-event-button-bg-not-going-hover", - text: "Help Us Improve", + text: "Withdraw", }, }, completed: { @@ -137,6 +137,16 @@ const applicationStatus = defineStatus({ text: "Coming Soon", }, }, + withdrawn: { + className: "bg-badge-bg-not-applied text-badge-text-withdrawn", + text: "Withdrawn", + icon: TablerBan, + button: { + className: + "bg-event-button-bg-completed text-event-button-text-completed hover:bg-event-button-bg-completed-hover", + text: "Help Us Improve", + }, + }, }); export default applicationStatus; diff --git a/apps/web/src/features/Event/components/EventWithdrawalModal.tsx b/apps/web/src/features/Event/components/EventAcceptanceWithdrawalModal.tsx similarity index 85% rename from apps/web/src/features/Event/components/EventWithdrawalModal.tsx rename to apps/web/src/features/Event/components/EventAcceptanceWithdrawalModal.tsx index 228745cd..6b8b540e 100644 --- a/apps/web/src/features/Event/components/EventWithdrawalModal.tsx +++ b/apps/web/src/features/Event/components/EventAcceptanceWithdrawalModal.tsx @@ -7,11 +7,13 @@ import { useQueryClient } from "@tanstack/react-query"; import { eventsQueryKey } from "../hooks/useEventsWithUserInfo"; import { myApplicationBaseKey } from "@/features/Application/hooks/useMyApplication"; -interface EventWithdrawalModalProps { +interface EventAcceptanceWithdrawalModalProps { eventId: string; } -function EventWithdrawalModal({ eventId }: EventWithdrawalModalProps) { +function EventAcceptanceWithdrawalModal({ + eventId, +}: EventAcceptanceWithdrawalModalProps) { const queryClient = useQueryClient(); const { data: userData } = auth.useUser(); @@ -38,7 +40,7 @@ function EventWithdrawalModal({ eventId }: EventWithdrawalModalProps) { queryKey: [...myApplicationBaseKey, eventId], }); } catch (error) { - console.error("Failed to join waitlist", error); + console.error("Failed to withdraw application", error); showToast({ title: "Failed to Withdraw", message: "Failed to Withdraw Acceptance. Please try again.", @@ -59,9 +61,9 @@ function EventWithdrawalModal({ eventId }: EventWithdrawalModalProps) {

Are you sure? This action cannot be undone.

-

+ {/*

You can still join the waitlist after withdrawing. -

+

*/}
; + } + + const { user } = userData; + + const handleWithdrawAttendance = async (userId: string, eventId: string) => { + try { + await api.patch( + `events/${eventId}/application/withdraw-attendance?userId=${userId}`, + ); + showToast({ + title: "Attendance Withdrawn", + message: "Successfully Withdrawn Attendance.", + type: "success", + }); + await queryClient.invalidateQueries({ + queryKey: eventsQueryKey, + }); + await queryClient.invalidateQueries({ + queryKey: [...myApplicationBaseKey, eventId], + }); + navigate({ + to: "/portal", + }); + } catch (error) { + console.error("Failed to withdraw attendance", error); + showToast({ + title: "Failed to Withdraw", + message: "Failed to Withdraw Attendance. Please try again.", + type: "error", + }); + } + }; + + return ( + +
+

+ Withdraw Attendance? +

+

+ Are you sure? This action cannot be undone. +

+ {/*

+ You can still join the waitlist after withdrawing. +

*/} +
+
+ +
+
+ ); +} + +export { EventAttendanceWithdrawalModal as EventAttendanceWithdrawalModal }; diff --git a/apps/web/src/features/Event/components/EventButton.tsx b/apps/web/src/features/Event/components/EventButton.tsx index 659c3264..151a886e 100644 --- a/apps/web/src/features/Event/components/EventButton.tsx +++ b/apps/web/src/features/Event/components/EventButton.tsx @@ -6,7 +6,7 @@ import { cn } from "@/utils/cn"; import { useRouter } from "@tanstack/react-router"; import { toast } from "react-toastify"; import { DialogTrigger } from "react-aria-components"; -import { EventWithdrawalModal } from "./EventWithdrawalModal"; +import { EventAcceptanceWithdrawalModal } from "./EventAcceptanceWithdrawalModal"; import { EventWaitlistModal } from "./EventWaitlistModal"; import { api } from "@/lib/ky"; import { auth } from "@/lib/authClient"; @@ -76,7 +76,7 @@ const EventButton = ({ to: `/events/${eventId}/dashboard`, }); } catch (error) { - console.error("Failed to join waitlist", error); + console.error("Failed to Accept", error); showToast({ title: "Acceptance Failed", message: "Failed to Accept. Please try again.", @@ -127,6 +127,10 @@ const EventButton = ({ position: "bottom-right", }); break; + case "withdrawn": + window.location.href = + "https://swamphack.notion.site/2e73b41de22f806da958fa548a176725?pvs=105"; + break; } }; @@ -139,7 +143,9 @@ const EventButton = ({ > {text || applicationStatus[statusProp].button.text} - + ); } else if (statusProp === "rejected") { diff --git a/apps/web/src/features/Event/utils/mapper.ts b/apps/web/src/features/Event/utils/mapper.ts index 71b15f94..6b6ca8c1 100644 --- a/apps/web/src/features/Event/utils/mapper.ts +++ b/apps/web/src/features/Event/utils/mapper.ts @@ -26,7 +26,7 @@ const statusMap: Record = { submitted: "underReview", under_review: "underReview", started: "notApplied", - withdrawn: "notGoing", + withdrawn: "withdrawn", staff: "staff", admin: "admin", attendee: "attending", diff --git a/apps/web/src/features/EventOverview/components/AttendeeOverview.tsx b/apps/web/src/features/EventOverview/components/AttendeeOverview.tsx index c18f584b..c7092b50 100644 --- a/apps/web/src/features/EventOverview/components/AttendeeOverview.tsx +++ b/apps/web/src/features/EventOverview/components/AttendeeOverview.tsx @@ -1,3 +1,26 @@ -export default function AttendeeOverview() { - return
More here coming soon!
; +import { DialogTrigger } from "react-aria-components"; +import { EventAttendanceWithdrawalModal } from "@/features/Event/components/EventAttendanceWithdrawalModal"; +import { Button } from "@/components/ui/Button"; + +interface ApplicationOverviewProps { + eventId: string; +} + +export default function AttendeeOverview({ + eventId, +}: ApplicationOverviewProps) { + return ( +
+
More here coming soon!
+
+

Can't make it to the event?

+ + + + +
+
+ ); } diff --git a/apps/web/src/routes/_protected/events/$eventId/dashboard/index.tsx b/apps/web/src/routes/_protected/events/$eventId/dashboard/index.tsx index 15602176..88fec509 100644 --- a/apps/web/src/routes/_protected/events/$eventId/dashboard/index.tsx +++ b/apps/web/src/routes/_protected/events/$eventId/dashboard/index.tsx @@ -28,7 +28,7 @@ function RouteComponent() { } if (eventRole === "attendee") { - return ; + return ; } // Should never reach here due to the redirect in beforeLoad From de4c1ffd9d620cbd159e385f76d844414a8841a7 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:45:06 -0500 Subject: [PATCH 3/5] feat:add guard clause for when the event has begun --- apps/api/internal/services/application.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/api/internal/services/application.go b/apps/api/internal/services/application.go index 496f8f58..af078725 100644 --- a/apps/api/internal/services/application.go +++ b/apps/api/internal/services/application.go @@ -3,6 +3,7 @@ package services import ( "context" "errors" + "time" "github.com/google/uuid" "github.com/hibiken/asynq" @@ -21,6 +22,7 @@ var ( ErrGetApplicationStatistics = errors.New("failed to aggregate application stats") ErrMismatchedReviewerCounts = errors.New("the total number of applications does not match the total number of assigned reviews") ErrWrongReviewerAssignment = errors.New("an application has been assigned to a reviewer who is not authorized to review it") + ErrEventAlreadyStarted = errors.New("the event has already started") ) // TODO: figure out a way to create the submission fields dynamically using the json form files with proper validation. @@ -94,7 +96,7 @@ func NewApplicationService(appRepo *repository.ApplicationRepository, userRepo * buckets: buckets, txm: txm, scheduler: scheduler, - logger: logger, + logger: logger.With().Str("component", "applicationService").Logger(), } } @@ -598,7 +600,19 @@ func (s *ApplicationService) TransitionWaitlistedApplications(ctx context.Contex err := s.txm.WithTx(ctx, func(tx pgx.Tx) error { txAppRepo := s.appRepo.NewTx(tx) - err := txAppRepo.TransitionAcceptedApplicationsToWaitlistByEventID(ctx, eventId) + // Get event start date, error if past the start of the event. + event, err := s.eventsService.GetEventByID(ctx, eventId) + if err != nil { + s.logger.Err(err).Msg(err.Error()) + return err + } + currentTime := time.Now() + if event.StartTime.After(currentTime) { + s.logger.Err(ErrEventAlreadyStarted).Msg("Could not transition waitlisted applications: the event has already started.") + return ErrEventAlreadyStarted + } + + err = txAppRepo.TransitionAcceptedApplicationsToWaitlistByEventID(ctx, eventId) if err != nil { s.logger.Err(err).Msg(err.Error()) return err From 5a45f7e1145c41b32be37c20bdc2faf2aea6b21d Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:55:10 -0500 Subject: [PATCH 4/5] fix: close() taskQueue client, move guard clause to HandleScheduleTransitionWaitlistTask --- apps/api/cmd/BAT_worker/main.go | 10 ++-------- apps/api/internal/services/application.go | 16 +--------------- apps/api/internal/workers/bat.go | 22 +++++++++++++++++++++- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/api/cmd/BAT_worker/main.go b/apps/api/cmd/BAT_worker/main.go index 79cff8a0..3ebc57ef 100644 --- a/apps/api/cmd/BAT_worker/main.go +++ b/apps/api/cmd/BAT_worker/main.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "time" "github.com/hibiken/asynq" @@ -57,9 +56,6 @@ func main() { }, ) - logger.Info().Msg("Debug test") - fmt.Print("Debug test") - schedulerLocation, err := time.LoadLocation("America/New_York") if err != nil { panic(err) @@ -71,10 +67,8 @@ func main() { }, ) - logger.Info().Msg("Debug test") - fmt.Print("Debug test") - taskQueueClient := asynq.NewClient(redisOpt) + defer taskQueueClient.Close() database := db.NewDB(cfg.DatabaseURL) defer database.Close() @@ -92,7 +86,7 @@ func main() { batService := services.NewBatService(applicationRepo, eventRepo, userRepo, batRunsRepo, emailService, txm, nil, scheduler, logger) applicationService := services.NewApplicationService(applicationRepo, userRepo, eventService, emailService, txm, nil, nil, scheduler, logger) - BATWorker := workers.NewBATWorker(batService, applicationService, scheduler, taskQueueClient, logger) + BATWorker := workers.NewBATWorker(batService, applicationService, eventService, scheduler, taskQueueClient, logger) mux := asynq.NewServeMux() mux.HandleFunc(tasks.TypeCalculateAdmissions, BATWorker.HandleCalculateAdmissionsTask) diff --git a/apps/api/internal/services/application.go b/apps/api/internal/services/application.go index af078725..08e29b3c 100644 --- a/apps/api/internal/services/application.go +++ b/apps/api/internal/services/application.go @@ -3,7 +3,6 @@ package services import ( "context" "errors" - "time" "github.com/google/uuid" "github.com/hibiken/asynq" @@ -22,7 +21,6 @@ var ( ErrGetApplicationStatistics = errors.New("failed to aggregate application stats") ErrMismatchedReviewerCounts = errors.New("the total number of applications does not match the total number of assigned reviews") ErrWrongReviewerAssignment = errors.New("an application has been assigned to a reviewer who is not authorized to review it") - ErrEventAlreadyStarted = errors.New("the event has already started") ) // TODO: figure out a way to create the submission fields dynamically using the json form files with proper validation. @@ -600,19 +598,7 @@ func (s *ApplicationService) TransitionWaitlistedApplications(ctx context.Contex err := s.txm.WithTx(ctx, func(tx pgx.Tx) error { txAppRepo := s.appRepo.NewTx(tx) - // Get event start date, error if past the start of the event. - event, err := s.eventsService.GetEventByID(ctx, eventId) - if err != nil { - s.logger.Err(err).Msg(err.Error()) - return err - } - currentTime := time.Now() - if event.StartTime.After(currentTime) { - s.logger.Err(ErrEventAlreadyStarted).Msg("Could not transition waitlisted applications: the event has already started.") - return ErrEventAlreadyStarted - } - - err = txAppRepo.TransitionAcceptedApplicationsToWaitlistByEventID(ctx, eventId) + err := txAppRepo.TransitionAcceptedApplicationsToWaitlistByEventID(ctx, eventId) if err != nil { s.logger.Err(err).Msg(err.Error()) return err diff --git a/apps/api/internal/workers/bat.go b/apps/api/internal/workers/bat.go index 90cdcf76..1c4108f5 100644 --- a/apps/api/internal/workers/bat.go +++ b/apps/api/internal/workers/bat.go @@ -3,6 +3,8 @@ package workers import ( "context" "encoding/json" + "errors" + "time" "github.com/hibiken/asynq" "github.com/rs/zerolog" @@ -12,6 +14,10 @@ import ( "github.com/swamphacks/core/apps/api/internal/tasks" ) +var ( + ErrEventAlreadyStarted = errors.New("the event has already started") +) + // BAT Worker // The BAT worker runs the background execution pipeline for our // Balanced Admissions Thresher (BAT). This worker processes @@ -22,15 +28,17 @@ import ( type BATWorker struct { batService *services.BatService applicationService *services.ApplicationService + eventService *services.EventService scheduler *asynq.Scheduler taskQueue *asynq.Client logger zerolog.Logger } -func NewBATWorker(batService *services.BatService, applicationService *services.ApplicationService, scheduler *asynq.Scheduler, taskQueue *asynq.Client, logger zerolog.Logger) *BATWorker { +func NewBATWorker(batService *services.BatService, applicationService *services.ApplicationService, eventService *services.EventService, scheduler *asynq.Scheduler, taskQueue *asynq.Client, logger zerolog.Logger) *BATWorker { return &BATWorker{ batService: batService, applicationService: applicationService, + eventService: eventService, logger: logger.With().Str("worker", "BATWorker").Str("component", "BAT").Logger(), scheduler: scheduler, taskQueue: taskQueue, @@ -72,6 +80,18 @@ func (w *BATWorker) HandleScheduleTransitionWaitlistTask(ctx context.Context, t return err } + // Get event start date, error if past the start of the event. + event, err := w.eventService.GetEventByID(ctx, payload.EventID) + if err != nil { + w.logger.Err(err).Msg(err.Error()) + return err + } + currentTime := time.Now() + if currentTime.After(event.StartTime) { + w.logger.Err(ErrEventAlreadyStarted).Msg("Could not transition waitlisted applications: the event has already started.") + return ErrEventAlreadyStarted + } + cfg := config.Load() task, err := tasks.NewTaskTransitionWaitlist(tasks.TransitionWaitlistPayload{ EventID: payload.EventID, From 8e56625196acd938f08c7f43306879901cef7a37 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:04:59 -0500 Subject: [PATCH 5/5] fix: readded guard clause to TransitionWaitlistedApplications --- apps/api/internal/services/application.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/api/internal/services/application.go b/apps/api/internal/services/application.go index 08e29b3c..159d0b59 100644 --- a/apps/api/internal/services/application.go +++ b/apps/api/internal/services/application.go @@ -3,6 +3,7 @@ package services import ( "context" "errors" + "time" "github.com/google/uuid" "github.com/hibiken/asynq" @@ -21,6 +22,7 @@ var ( ErrGetApplicationStatistics = errors.New("failed to aggregate application stats") ErrMismatchedReviewerCounts = errors.New("the total number of applications does not match the total number of assigned reviews") ErrWrongReviewerAssignment = errors.New("an application has been assigned to a reviewer who is not authorized to review it") + ErrEventAlreadyStarted = errors.New("the event has already started") ) // TODO: figure out a way to create the submission fields dynamically using the json form files with proper validation. @@ -595,7 +597,19 @@ func (s *ApplicationService) AcceptApplicationAcceptance(ctx context.Context, us func (s *ApplicationService) TransitionWaitlistedApplications(ctx context.Context, eventId uuid.UUID, acceptanceCount uint32, acceptanceQuota uint32) error { var acceptedUserIds []uuid.UUID - err := s.txm.WithTx(ctx, func(tx pgx.Tx) error { + + event, err := s.eventsService.GetEventByID(ctx, eventId) + if err != nil { + s.logger.Err(err).Msg(err.Error()) + return err + } + currentTime := time.Now() + if currentTime.After(event.StartTime) { + s.logger.Err(ErrEventAlreadyStarted).Msg("Could not transition waitlisted applications: the event has already started.") + return ErrEventAlreadyStarted + } + + err = s.txm.WithTx(ctx, func(tx pgx.Tx) error { txAppRepo := s.appRepo.NewTx(tx) err := txAppRepo.TransitionAcceptedApplicationsToWaitlistByEventID(ctx, eventId)