From 083319427efa69a5f27c9b82f5144ca4a703776b Mon Sep 17 00:00:00 2001 From: AlexanderWangY Date: Sat, 13 Dec 2025 19:45:08 -0500 Subject: [PATCH 01/11] feat: Bat service + task logic --- Makefile | 3 + apps/api/cmd/BAT_worker/main.go | 13 ++++- apps/api/cmd/api/main.go | 10 +++- apps/api/internal/api/api.go | 3 +- apps/api/internal/api/handlers/admissions.go | 36 ++++++++++++ apps/api/internal/api/handlers/handlers.go | 3 + apps/api/internal/bat/scores.go | 60 ++++++++++++++++++++ apps/api/internal/services/bat.go | 51 +++++++++++++++++ apps/api/internal/tasks/bat.go | 25 ++++++++ apps/api/internal/workers/bat.go | 30 ++++++++-- 10 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 apps/api/internal/api/handlers/admissions.go create mode 100644 apps/api/internal/bat/scores.go create mode 100644 apps/api/internal/services/bat.go create mode 100644 apps/api/internal/tasks/bat.go diff --git a/Makefile b/Makefile index 31441dbd..7faabad8 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,9 @@ local: api: docker compose up api +bat: + docker compose up api bat_worker asynqmon + storage: docker compose up postgres redis diff --git a/apps/api/cmd/BAT_worker/main.go b/apps/api/cmd/BAT_worker/main.go index fbd1150f..61d4946b 100644 --- a/apps/api/cmd/BAT_worker/main.go +++ b/apps/api/cmd/BAT_worker/main.go @@ -4,8 +4,11 @@ import ( "time" "github.com/hibiken/asynq" + "github.com/swamphacks/core/apps/api/internal/bat" "github.com/swamphacks/core/apps/api/internal/config" "github.com/swamphacks/core/apps/api/internal/logger" + "github.com/swamphacks/core/apps/api/internal/services" + "github.com/swamphacks/core/apps/api/internal/tasks" "github.com/swamphacks/core/apps/api/internal/workers" ) @@ -51,9 +54,17 @@ func main() { }, ) - _ = workers.NewBATWorker(logger) + batEngine, err := bat.NewBatEngine(0.5, 0.5) + if err != nil { + logger.Fatal().Err(err).Msg("Bat engine failed to initialize.") + } + + batService := services.NewBatService(batEngine, nil, logger) + + BATWorker := workers.NewBATWorker(batService, logger) mux := asynq.NewServeMux() + mux.HandleFunc(tasks.TypeCalculateAdmissions, BATWorker.HandleCalculateAdmissionsTask) if err := srv.Run(mux); err != nil { logger.Fatal().Msg("Failed to run BAT worker") diff --git a/apps/api/cmd/api/main.go b/apps/api/cmd/api/main.go index b3b1c3f7..26f084cc 100644 --- a/apps/api/cmd/api/main.go +++ b/apps/api/cmd/api/main.go @@ -9,6 +9,7 @@ import ( "github.com/swamphacks/core/apps/api/internal/api" "github.com/swamphacks/core/apps/api/internal/api/handlers" "github.com/swamphacks/core/apps/api/internal/api/middleware" + "github.com/swamphacks/core/apps/api/internal/bat" "github.com/swamphacks/core/apps/api/internal/config" "github.com/swamphacks/core/apps/api/internal/db" "github.com/swamphacks/core/apps/api/internal/db/repository" @@ -55,6 +56,12 @@ func main() { // Create SES Client for email service sesClient := email.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger) + // BAT Engine, needs to be synced with ENV soon + batEngine, err := bat.NewBatEngine(0.5, 0.5) + if err != nil { + logger.Fatal().Msg("Failed to init Bat Engine") + } + // Create asynq client redisOpt, err := asynq.ParseRedisURI(cfg.RedisURL) if err != nil { @@ -90,9 +97,10 @@ func main() { emailService := services.NewEmailService(taskQueueClient, sesClient, logger) applicationService := services.NewApplicationService(applicationRepo, eventService, emailService, txm, r2Client, &cfg.CoreBuckets, logger) teamService := services.NewTeamService(teamRepo, teamMemberRepo, teamJoinRequestRepo, eventRepo, txm, logger) + batService := services.NewBatService(batEngine, taskQueueClient, logger) // Injections into handlers - apiHandlers := handlers.NewHandlers(authService, userService, eventInterestService, eventService, emailService, applicationService, teamService, cfg, logger) + apiHandlers := handlers.NewHandlers(authService, userService, eventInterestService, eventService, emailService, applicationService, teamService, batService, cfg, logger) api := api.NewAPI(&logger, apiHandlers, mw) diff --git a/apps/api/internal/api/api.go b/apps/api/internal/api/api.go index 08fc0eb4..67612e28 100644 --- a/apps/api/internal/api/api.go +++ b/apps/api/internal/api/api.go @@ -122,8 +122,7 @@ func (api *API) setupRoutes(mw *mw.Middleware) { // --- Event routes --- api.Router.Route("/events", func(r chi.Router) { - // r.Post("/{eventId}/application/reset-reviews", api.Handlers.Application.ResetApplicationReviews) - // r.Post("/{eventId}/application/assign-reviewers", api.Handlers.Application.AssignApplicationReviewers) + r.Post("/{eventId}/calc-admissions", api.Handlers.Admission.HandleCalculateAdmissionsRequest) // Superuser-only r.With(mw.Auth.RequireAuth, ensureSuperuser).Post("/", api.Handlers.Event.CreateEvent) diff --git a/apps/api/internal/api/handlers/admissions.go b/apps/api/internal/api/handlers/admissions.go new file mode 100644 index 00000000..2188782b --- /dev/null +++ b/apps/api/internal/api/handlers/admissions.go @@ -0,0 +1,36 @@ +package handlers + +import ( + "net/http" + + res "github.com/swamphacks/core/apps/api/internal/api/response" + "github.com/swamphacks/core/apps/api/internal/services" + "github.com/swamphacks/core/apps/api/internal/web" +) + +type AdmissionHandler struct { + batService *services.BatService +} + +func NewAdmissionHandler(batService *services.BatService) *AdmissionHandler { + return &AdmissionHandler{ + batService: batService, + } +} + +func (h *AdmissionHandler) HandleCalculateAdmissionsRequest(w http.ResponseWriter, r *http.Request) { + eventId, err := web.PathParamToUUID(r, "eventId") + if err != nil { + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Invalid request body")) + return + } + + _, err = h.batService.QueueCalculateAdmissionsTask(eventId) + if err != nil { + res.Send(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went terribly wrong.")) + return + } + + w.WriteHeader(http.StatusCreated) + +} diff --git a/apps/api/internal/api/handlers/handlers.go b/apps/api/internal/api/handlers/handlers.go index 86f77699..f40a842c 100644 --- a/apps/api/internal/api/handlers/handlers.go +++ b/apps/api/internal/api/handlers/handlers.go @@ -14,6 +14,7 @@ type Handlers struct { Email *EmailHandler Application *ApplicationHandler Teams *TeamHandler + Admission *AdmissionHandler } func NewHandlers( @@ -24,6 +25,7 @@ func NewHandlers( emailService *services.EmailService, appService *services.ApplicationService, teamService *services.TeamService, + batService *services.BatService, cfg *config.Config, logger zerolog.Logger, ) *Handlers { @@ -35,5 +37,6 @@ func NewHandlers( Email: NewEmailHandler(emailService, logger), Application: NewApplicationHandler(appService), Teams: NewTeamHandler(teamService, logger), + Admission: NewAdmissionHandler(batService), } } diff --git a/apps/api/internal/bat/scores.go b/apps/api/internal/bat/scores.go new file mode 100644 index 00000000..5c1fbf16 --- /dev/null +++ b/apps/api/internal/bat/scores.go @@ -0,0 +1,60 @@ +package bat + +import ( + "errors" + "math" +) + +var ( + ErrImproperWeights = errors.New("Passion and experience weights don't add to 1.0") + ErrScoreOutOfBounds = errors.New("The score can only range from 1 to 5") +) + +type BatEngine struct { + passionWeight float64 + experienceWeight float64 + weightedBaseConstant float64 +} + +func NewBatEngine(passionW, experienceW float64) (*BatEngine, error) { + if !equalWithinTolerance(passionW+experienceW, 1.0, 1e-9) { + return nil, ErrImproperWeights + } + + return &BatEngine{ + passionWeight: passionW, + experienceWeight: experienceW, + weightedBaseConstant: 0.1, + }, nil +} + +func (b *BatEngine) CalculateWeightedScore(passionS, expS int32) (float64, error) { + if 5 < passionS || 0 > passionS { + return 0.0, ErrScoreOutOfBounds + } + + if 5 < expS || 0 > expS { + return 0.0, ErrScoreOutOfBounds + } + + return (float64(passionS) * b.passionWeight) + (float64(expS) * b.experienceWeight) + b.weightedBaseConstant, nil +} + +// equalWithinTolerance checks if two float64 values are equal within a given tolerance. +// It handles exact equality, zero values, and relative differences. We recommend you +// set the tolerance to 1e-9. +// +// TL;DR Compares a and b with a tolerance of e. +func equalWithinTolerance(a, b, e float64) bool { + if a == b { + return true + } + + d := math.Abs(a - b) + + if b == 0 { + return d < e + } + + return (d / math.Abs(b)) < e +} diff --git a/apps/api/internal/services/bat.go b/apps/api/internal/services/bat.go new file mode 100644 index 00000000..242bd29c --- /dev/null +++ b/apps/api/internal/services/bat.go @@ -0,0 +1,51 @@ +package services + +import ( + "github.com/google/uuid" + "github.com/hibiken/asynq" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/bat" + "github.com/swamphacks/core/apps/api/internal/tasks" +) + +type BatService struct { + engine *bat.BatEngine + taskQueue *asynq.Client + logger zerolog.Logger +} + +func NewBatService(engine *bat.BatEngine, taskQueue *asynq.Client, logger zerolog.Logger) *BatService { + return &BatService{ + engine: engine, + taskQueue: taskQueue, + logger: logger.With().Str("service", "Bat Service").Str("component", "admissions").Logger(), + } +} + +func (s *BatService) QueueCalculateAdmissionsTask(eventId uuid.UUID) (*asynq.TaskInfo, error) { + task, err := tasks.NewTaskCalculateAdmissions(tasks.CalculateAdmissionsPayload{ + EventID: eventId, + }) + if err != nil { + s.logger.Err(err).Msg("Failed to create CalculateAdmissions task") + return nil, err + } + + info, err := s.taskQueue.Enqueue(task, asynq.Queue("bat")) + if err != nil { + s.logger.Err(err).Msg("Failed to queue CalculateAdmissions task") + return nil, err + } + + return info, nil +} + +func (s *BatService) CalculateAdmissions() { + // First calculate scores for each applicant + score, err := s.engine.CalculateWeightedScore(1, 5) + if err != nil { + s.logger.Err(err).Msg("Failed to calculate weighted score.") + } + + s.logger.Info().Float64("Weighted score", score).Msg("Successed in calculating score.") +} diff --git a/apps/api/internal/tasks/bat.go b/apps/api/internal/tasks/bat.go new file mode 100644 index 00000000..d235ee14 --- /dev/null +++ b/apps/api/internal/tasks/bat.go @@ -0,0 +1,25 @@ +package tasks + +import ( + "encoding/json" + + "github.com/google/uuid" + "github.com/hibiken/asynq" +) + +const ( + TypeCalculateAdmissions = "admissions:calculate" +) + +type CalculateAdmissionsPayload struct { + EventID uuid.UUID +} + +func NewTaskCalculateAdmissions(payload CalculateAdmissionsPayload) (*asynq.Task, error) { + data, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + return asynq.NewTask(TypeCalculateAdmissions, data), nil +} diff --git a/apps/api/internal/workers/bat.go b/apps/api/internal/workers/bat.go index 00ef169d..9a3daea9 100644 --- a/apps/api/internal/workers/bat.go +++ b/apps/api/internal/workers/bat.go @@ -1,6 +1,14 @@ package workers -import "github.com/rs/zerolog" +import ( + "context" + "encoding/json" + + "github.com/hibiken/asynq" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/services" + "github.com/swamphacks/core/apps/api/internal/tasks" +) // BAT Worker // The BAT worker runs the background execution pipeline for our @@ -10,11 +18,25 @@ import "github.com/rs/zerolog" // other decision heuristics. It operates asynchronously to ensure // fair, consistent, and scalable admissions handling. type BATWorker struct { - logger zerolog.Logger + batService *services.BatService + logger zerolog.Logger } -func NewBATWorker(logger zerolog.Logger) *BATWorker { +func NewBATWorker(batService *services.BatService, logger zerolog.Logger) *BATWorker { return &BATWorker{ - logger: logger.With().Str("worker", "BATWorker").Str("component", "BAT").Logger(), + batService: batService, + logger: logger.With().Str("worker", "BATWorker").Str("component", "BAT").Logger(), } } + +func (w *BATWorker) HandleCalculateAdmissionsTask(ctx context.Context, t *asynq.Task) error { + var p tasks.CalculateAdmissionsPayload + if err := json.Unmarshal(t.Payload(), &p); err != nil { + w.logger.Err(err).Msg("Failed to unmarshal payload.") + return err + } + + // Fire off bat service + w.batService.CalculateAdmissions() + return nil +} From ca6f69c1e34118a9b1839fe1a9106196e2cfe396 Mon Sep 17 00:00:00 2001 From: AlexanderWangY Date: Sun, 14 Dec 2025 15:18:12 -0500 Subject: [PATCH 02/11] temp: pushes for phoenix to branch off of, GL! --- apps/api/internal/db/queries/applications.sql | 8 ++- .../api/internal/db/repository/application.go | 14 ++++++ apps/api/internal/db/sqlc/applications.sql.go | 44 +++++++++++++++++ apps/api/internal/services/bat.go | 49 ++++++++++++++++--- 4 files changed, 108 insertions(+), 7 deletions(-) diff --git a/apps/api/internal/db/queries/applications.sql b/apps/api/internal/db/queries/applications.sql index a8237fa1..980520ce 100644 --- a/apps/api/internal/db/queries/applications.sql +++ b/apps/api/internal/db/queries/applications.sql @@ -39,6 +39,12 @@ WHERE event_id = $1 ORDER BY user_id ASC; +-- name: ListApplicationsByEventAndStatus :many +SELECT user_id, application, experience_rating, passion_rating +FROM applications +WHERE event_id = $1 + AND status = $2; + -- name: AssignApplicationsToReviewer :exec UPDATE applications SET assigned_reviewer_id = @reviewer_id::uuid, @@ -60,4 +66,4 @@ SELECT user_id, passion_rating, experience_rating FROM applications WHERE assigned_reviewer_id = $1 AND event_id = $2 AND status IN ('under_review') -ORDER BY user_id ASC; \ No newline at end of file +ORDER BY user_id ASC; diff --git a/apps/api/internal/db/repository/application.go b/apps/api/internal/db/repository/application.go index 4daf1e9c..8bd9f8c0 100644 --- a/apps/api/internal/db/repository/application.go +++ b/apps/api/internal/db/repository/application.go @@ -66,6 +66,20 @@ func (r *ApplicationRepository) GetApplicationByUserAndEventID(ctx context.Conte return &application, nil } +func (r *ApplicationRepository) ListApplicationsByEventAndStatus(ctx context.Context, eventId uuid.UUID, status sqlc.ApplicationStatus) ([]sqlc.ListApplicationsByEventAndStatusRow, error) { + applications, err := r.db.Query.ListApplicationsByEventAndStatus(ctx, sqlc.ListApplicationsByEventAndStatusParams{ + EventID: eventId, + Status: sqlc.NullApplicationStatus{ + Valid: true, + ApplicationStatus: status, + }, + }) + if err != nil { + return []sqlc.ListApplicationsByEventAndStatusRow{}, err + } + return applications, nil +} + func (r *ApplicationRepository) SubmitApplication(ctx context.Context, data any, userId, eventId uuid.UUID) error { jsonBytes, err := json.Marshal(data) diff --git a/apps/api/internal/db/sqlc/applications.sql.go b/apps/api/internal/db/sqlc/applications.sql.go index e9f9051d..9e9fa28e 100644 --- a/apps/api/internal/db/sqlc/applications.sql.go +++ b/apps/api/internal/db/sqlc/applications.sql.go @@ -147,6 +147,50 @@ func (q *Queries) ListApplicationByReviewerAndEvent(ctx context.Context, arg Lis return items, nil } +const listApplicationsByEventAndStatus = `-- name: ListApplicationsByEventAndStatus :many +SELECT user_id, application, experience_rating, passion_rating +FROM applications +WHERE event_id = $1 + AND status = $2 +` + +type ListApplicationsByEventAndStatusParams struct { + EventID uuid.UUID `json:"event_id"` + Status NullApplicationStatus `json:"status"` +} + +type ListApplicationsByEventAndStatusRow struct { + UserID uuid.UUID `json:"user_id"` + Application []byte `json:"application"` + ExperienceRating *int32 `json:"experience_rating"` + PassionRating *int32 `json:"passion_rating"` +} + +func (q *Queries) ListApplicationsByEventAndStatus(ctx context.Context, arg ListApplicationsByEventAndStatusParams) ([]ListApplicationsByEventAndStatusRow, error) { + rows, err := q.db.Query(ctx, listApplicationsByEventAndStatus, arg.EventID, arg.Status) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListApplicationsByEventAndStatusRow{} + for rows.Next() { + var i ListApplicationsByEventAndStatusRow + if err := rows.Scan( + &i.UserID, + &i.Application, + &i.ExperienceRating, + &i.PassionRating, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listAvailableApplicationsForEvent = `-- name: ListAvailableApplicationsForEvent :many SELECT user_id FROM applications diff --git a/apps/api/internal/services/bat.go b/apps/api/internal/services/bat.go index 242bd29c..2abb5899 100644 --- a/apps/api/internal/services/bat.go +++ b/apps/api/internal/services/bat.go @@ -1,20 +1,31 @@ package services import ( + "context" + "errors" + "github.com/google/uuid" "github.com/hibiken/asynq" "github.com/rs/zerolog" "github.com/swamphacks/core/apps/api/internal/bat" + "github.com/swamphacks/core/apps/api/internal/db/repository" + "github.com/swamphacks/core/apps/api/internal/db/sqlc" "github.com/swamphacks/core/apps/api/internal/tasks" ) +var ( + ErrListApplicationsFailure = errors.New("Failed to retrieve applications") +) + type BatService struct { engine *bat.BatEngine + appRepo *repository.ApplicationRepository + eventRepo *repository.EventRepository taskQueue *asynq.Client logger zerolog.Logger } -func NewBatService(engine *bat.BatEngine, taskQueue *asynq.Client, logger zerolog.Logger) *BatService { +func NewBatService(engine *bat.BatEngine, appRepo *repository.ApplicationRepository, eventRepo *repository.EventRepository, taskQueue *asynq.Client, logger zerolog.Logger) *BatService { return &BatService{ engine: engine, taskQueue: taskQueue, @@ -40,12 +51,38 @@ func (s *BatService) QueueCalculateAdmissionsTask(eventId uuid.UUID) (*asynq.Tas return info, nil } -func (s *BatService) CalculateAdmissions() { - // First calculate scores for each applicant - score, err := s.engine.CalculateWeightedScore(1, 5) +type ApplicantAdmissionsData struct { + ID uuid.UUID + TeamID uuid.NullUUID + WeightedScore float32 + IsUFStudent bool // Is a University of Florida student? + IsEarlyCareer bool // Is a freshman to sophomore? +} + +func (s *BatService) CalculateAdmissions(ctx context.Context, eventId uuid.UUID) error { + // First aggregate data necessary + applications, err := s.appRepo.ListApplicationsByEventAndStatus(ctx, eventId, sqlc.ApplicationStatusUnderReview) + if err != nil || len(applications) == 0 { + return ErrListApplicationsFailure + } + + event, err := s.eventRepo.GetEventByID(ctx, eventId) if err != nil { - s.logger.Err(err).Msg("Failed to calculate weighted score.") + return err + } + + maxAttendees := int32(500) + if event.MaxAttendees != nil { + maxAttendees = *event.MaxAttendees + } + + var appAdmissionsData []ApplicantAdmissionsData + for _, app := range applications { + appAdmissionsData = append(appAdmissionsData, ApplicantAdmissionsData{ + ID: app.UserID, + TeamID: , + }) } - s.logger.Info().Float64("Weighted score", score).Msg("Successed in calculating score.") + return nil } From eb543b9630ea63ae0d59899cd04db94dc952f264 Mon Sep 17 00:00:00 2001 From: AlexanderWangY Date: Mon, 15 Dec 2025 01:04:35 -0500 Subject: [PATCH 03/11] feat: HOLY SHIT IT WORKS LMFAO --- apps/api/cmd/BAT_worker/main.go | 10 +- apps/api/cmd/api/main.go | 2 +- apps/api/internal/db/queries/applications.sql | 22 +- .../api/internal/db/repository/application.go | 17 +- apps/api/internal/db/sqlc/applications.sql.go | 90 ++-- apps/api/internal/services/bat.go | 462 +++++++++++++++++- apps/api/internal/workers/bat.go | 7 +- 7 files changed, 537 insertions(+), 73 deletions(-) diff --git a/apps/api/cmd/BAT_worker/main.go b/apps/api/cmd/BAT_worker/main.go index 61d4946b..3bba489b 100644 --- a/apps/api/cmd/BAT_worker/main.go +++ b/apps/api/cmd/BAT_worker/main.go @@ -6,6 +6,8 @@ import ( "github.com/hibiken/asynq" "github.com/swamphacks/core/apps/api/internal/bat" "github.com/swamphacks/core/apps/api/internal/config" + "github.com/swamphacks/core/apps/api/internal/db" + "github.com/swamphacks/core/apps/api/internal/db/repository" "github.com/swamphacks/core/apps/api/internal/logger" "github.com/swamphacks/core/apps/api/internal/services" "github.com/swamphacks/core/apps/api/internal/tasks" @@ -59,7 +61,13 @@ func main() { logger.Fatal().Err(err).Msg("Bat engine failed to initialize.") } - batService := services.NewBatService(batEngine, nil, logger) + database := db.NewDB(cfg.DatabaseURL) + defer database.Close() + + eventRepo := repository.NewEventRespository(database) + applicationRepo := repository.NewApplicationRepository(database) + + batService := services.NewBatService(batEngine, applicationRepo, eventRepo, nil, logger) BATWorker := workers.NewBATWorker(batService, logger) diff --git a/apps/api/cmd/api/main.go b/apps/api/cmd/api/main.go index 26f084cc..66adaeda 100644 --- a/apps/api/cmd/api/main.go +++ b/apps/api/cmd/api/main.go @@ -97,7 +97,7 @@ func main() { emailService := services.NewEmailService(taskQueueClient, sesClient, logger) applicationService := services.NewApplicationService(applicationRepo, eventService, emailService, txm, r2Client, &cfg.CoreBuckets, logger) teamService := services.NewTeamService(teamRepo, teamMemberRepo, teamJoinRequestRepo, eventRepo, txm, logger) - batService := services.NewBatService(batEngine, taskQueueClient, logger) + batService := services.NewBatService(batEngine, applicationRepo, eventRepo, taskQueueClient, logger) // Injections into handlers apiHandlers := handlers.NewHandlers(authService, userService, eventInterestService, eventService, emailService, applicationService, teamService, batService, cfg, logger) diff --git a/apps/api/internal/db/queries/applications.sql b/apps/api/internal/db/queries/applications.sql index 980520ce..cd5a2be1 100644 --- a/apps/api/internal/db/queries/applications.sql +++ b/apps/api/internal/db/queries/applications.sql @@ -39,11 +39,23 @@ WHERE event_id = $1 ORDER BY user_id ASC; --- name: ListApplicationsByEventAndStatus :many -SELECT user_id, application, experience_rating, passion_rating -FROM applications -WHERE event_id = $1 - AND status = $2; +-- name: ListAdmissionCandidatesByEvent :many +SELECT a.user_id, + a.passion_rating, + a.experience_rating, + a.application, + t.id as team_id +FROM applications a +LEFT JOIN team_members tm + ON tm.user_id = a.user_id +LEFT JOIN teams t + ON t.id = tm.team_id + AND t.event_id = a.event_id +WHERE a.event_id = $1 + AND a.status = 'under_review' + AND a.passion_rating IS NOT NULL + AND a.experience_rating IS NOT NULL; + -- name: AssignApplicationsToReviewer :exec UPDATE applications diff --git a/apps/api/internal/db/repository/application.go b/apps/api/internal/db/repository/application.go index 8bd9f8c0..89dca865 100644 --- a/apps/api/internal/db/repository/application.go +++ b/apps/api/internal/db/repository/application.go @@ -66,18 +66,11 @@ func (r *ApplicationRepository) GetApplicationByUserAndEventID(ctx context.Conte return &application, nil } -func (r *ApplicationRepository) ListApplicationsByEventAndStatus(ctx context.Context, eventId uuid.UUID, status sqlc.ApplicationStatus) ([]sqlc.ListApplicationsByEventAndStatusRow, error) { - applications, err := r.db.Query.ListApplicationsByEventAndStatus(ctx, sqlc.ListApplicationsByEventAndStatusParams{ - EventID: eventId, - Status: sqlc.NullApplicationStatus{ - Valid: true, - ApplicationStatus: status, - }, - }) - if err != nil { - return []sqlc.ListApplicationsByEventAndStatusRow{}, err - } - return applications, nil +// List all candidates considered for admission for an eventId. +// This queries for all applications who are 'under_review' and have their rating fields filled out. +// It also LEFT JOINs in their team id (if they have one) for further grouping based on teams. +func (r *ApplicationRepository) ListAdmissionCandidatesByEvent(ctx context.Context, eventId uuid.UUID) ([]sqlc.ListAdmissionCandidatesByEventRow, error) { + return r.db.Query.ListAdmissionCandidatesByEvent(ctx, eventId) } func (r *ApplicationRepository) SubmitApplication(ctx context.Context, data any, userId, eventId uuid.UUID) error { diff --git a/apps/api/internal/db/sqlc/applications.sql.go b/apps/api/internal/db/sqlc/applications.sql.go index 9e9fa28e..7594d632 100644 --- a/apps/api/internal/db/sqlc/applications.sql.go +++ b/apps/api/internal/db/sqlc/applications.sql.go @@ -108,35 +108,48 @@ func (q *Queries) GetApplicationByUserAndEventID(ctx context.Context, arg GetApp return i, err } -const listApplicationByReviewerAndEvent = `-- name: ListApplicationByReviewerAndEvent :many -SELECT user_id, passion_rating, experience_rating FROM applications -WHERE assigned_reviewer_id = $1 - AND event_id = $2 - AND status IN ('under_review') -ORDER BY user_id ASC +const listAdmissionCandidatesByEvent = `-- name: ListAdmissionCandidatesByEvent :many +SELECT a.user_id, + a.passion_rating, + a.experience_rating, + a.application, + t.id as team_id +FROM applications a +LEFT JOIN team_members tm + ON tm.user_id = a.user_id +LEFT JOIN teams t + ON t.id = tm.team_id + AND t.event_id = a.event_id +WHERE a.event_id = $1 + AND a.status = 'under_review' + AND a.passion_rating IS NOT NULL + AND a.experience_rating IS NOT NULL ` -type ListApplicationByReviewerAndEventParams struct { - AssignedReviewerID *uuid.UUID `json:"assigned_reviewer_id"` - EventID uuid.UUID `json:"event_id"` +type ListAdmissionCandidatesByEventRow struct { + UserID uuid.UUID `json:"user_id"` + PassionRating *int32 `json:"passion_rating"` + ExperienceRating *int32 `json:"experience_rating"` + Application []byte `json:"application"` + TeamID *uuid.UUID `json:"team_id"` } -type ListApplicationByReviewerAndEventRow struct { - UserID uuid.UUID `json:"user_id"` - PassionRating *int32 `json:"passion_rating"` - ExperienceRating *int32 `json:"experience_rating"` -} - -func (q *Queries) ListApplicationByReviewerAndEvent(ctx context.Context, arg ListApplicationByReviewerAndEventParams) ([]ListApplicationByReviewerAndEventRow, error) { - rows, err := q.db.Query(ctx, listApplicationByReviewerAndEvent, arg.AssignedReviewerID, arg.EventID) +func (q *Queries) ListAdmissionCandidatesByEvent(ctx context.Context, eventID uuid.UUID) ([]ListAdmissionCandidatesByEventRow, error) { + rows, err := q.db.Query(ctx, listAdmissionCandidatesByEvent, eventID) if err != nil { return nil, err } defer rows.Close() - items := []ListApplicationByReviewerAndEventRow{} + items := []ListAdmissionCandidatesByEventRow{} for rows.Next() { - var i ListApplicationByReviewerAndEventRow - if err := rows.Scan(&i.UserID, &i.PassionRating, &i.ExperienceRating); err != nil { + var i ListAdmissionCandidatesByEventRow + if err := rows.Scan( + &i.UserID, + &i.PassionRating, + &i.ExperienceRating, + &i.Application, + &i.TeamID, + ); err != nil { return nil, err } items = append(items, i) @@ -147,40 +160,35 @@ func (q *Queries) ListApplicationByReviewerAndEvent(ctx context.Context, arg Lis return items, nil } -const listApplicationsByEventAndStatus = `-- name: ListApplicationsByEventAndStatus :many -SELECT user_id, application, experience_rating, passion_rating -FROM applications -WHERE event_id = $1 - AND status = $2 +const listApplicationByReviewerAndEvent = `-- name: ListApplicationByReviewerAndEvent :many +SELECT user_id, passion_rating, experience_rating FROM applications +WHERE assigned_reviewer_id = $1 + AND event_id = $2 + AND status IN ('under_review') +ORDER BY user_id ASC ` -type ListApplicationsByEventAndStatusParams struct { - EventID uuid.UUID `json:"event_id"` - Status NullApplicationStatus `json:"status"` +type ListApplicationByReviewerAndEventParams struct { + AssignedReviewerID *uuid.UUID `json:"assigned_reviewer_id"` + EventID uuid.UUID `json:"event_id"` } -type ListApplicationsByEventAndStatusRow struct { +type ListApplicationByReviewerAndEventRow struct { UserID uuid.UUID `json:"user_id"` - Application []byte `json:"application"` - ExperienceRating *int32 `json:"experience_rating"` PassionRating *int32 `json:"passion_rating"` + ExperienceRating *int32 `json:"experience_rating"` } -func (q *Queries) ListApplicationsByEventAndStatus(ctx context.Context, arg ListApplicationsByEventAndStatusParams) ([]ListApplicationsByEventAndStatusRow, error) { - rows, err := q.db.Query(ctx, listApplicationsByEventAndStatus, arg.EventID, arg.Status) +func (q *Queries) ListApplicationByReviewerAndEvent(ctx context.Context, arg ListApplicationByReviewerAndEventParams) ([]ListApplicationByReviewerAndEventRow, error) { + rows, err := q.db.Query(ctx, listApplicationByReviewerAndEvent, arg.AssignedReviewerID, arg.EventID) if err != nil { return nil, err } defer rows.Close() - items := []ListApplicationsByEventAndStatusRow{} + items := []ListApplicationByReviewerAndEventRow{} for rows.Next() { - var i ListApplicationsByEventAndStatusRow - if err := rows.Scan( - &i.UserID, - &i.Application, - &i.ExperienceRating, - &i.PassionRating, - ); err != nil { + var i ListApplicationByReviewerAndEventRow + if err := rows.Scan(&i.UserID, &i.PassionRating, &i.ExperienceRating); err != nil { return nil, err } items = append(items, i) diff --git a/apps/api/internal/services/bat.go b/apps/api/internal/services/bat.go index 2abb5899..8653a3e5 100644 --- a/apps/api/internal/services/bat.go +++ b/apps/api/internal/services/bat.go @@ -2,19 +2,24 @@ package services import ( "context" + "encoding/json" "errors" + "math" + "math/rand" + "sort" + "time" "github.com/google/uuid" "github.com/hibiken/asynq" "github.com/rs/zerolog" "github.com/swamphacks/core/apps/api/internal/bat" "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" "github.com/swamphacks/core/apps/api/internal/tasks" ) var ( ErrListApplicationsFailure = errors.New("Failed to retrieve applications") + ErrMissingRatings = errors.New("Some applications are missing their review ratings") ) type BatService struct { @@ -29,6 +34,8 @@ func NewBatService(engine *bat.BatEngine, appRepo *repository.ApplicationReposit return &BatService{ engine: engine, taskQueue: taskQueue, + appRepo: appRepo, + eventRepo: eventRepo, logger: logger.With().Str("service", "Bat Service").Str("component", "admissions").Logger(), } } @@ -54,35 +61,466 @@ func (s *BatService) QueueCalculateAdmissionsTask(eventId uuid.UUID) (*asynq.Tas type ApplicantAdmissionsData struct { ID uuid.UUID TeamID uuid.NullUUID - WeightedScore float32 + WeightedScore float64 + SortKey float64 IsUFStudent bool // Is a University of Florida student? IsEarlyCareer bool // Is a freshman to sophomore? } +// Unmarshal into this reduced struct since +// our application fields are fucked +// beyond all fuck man. TODO: FIX OUR APP FIELDS! +type ApplicationSchoolAndYear struct { + School string `json:"school"` + Year string `json:"year"` +} + +type QuotaState struct { + TotalAccepted int32 + TeamSlotsLeft int32 + UFEarlyLeft int32 + UFLateLeft int32 + OtherEarlyLeft int32 + OtherLateLeft int32 +} + +type TeamAdmissionData struct { + TeamID uuid.UUID + MembersAdmissionData []ApplicantAdmissionsData + AverageWeightedScore float64 + SortKey float64 +} + func (s *BatService) CalculateAdmissions(ctx context.Context, eventId uuid.UUID) error { + s.logger.Info().Str("eventId", eventId.String()).Msg("") + // First aggregate data necessary - applications, err := s.appRepo.ListApplicationsByEventAndStatus(ctx, eventId, sqlc.ApplicationStatusUnderReview) + applications, err := s.appRepo.ListAdmissionCandidatesByEvent(ctx, eventId) if err != nil || len(applications) == 0 { return ErrListApplicationsFailure } - event, err := s.eventRepo.GetEventByID(ctx, eventId) - if err != nil { - return err - } + // event, err := s.eventRepo.GetEventByID(ctx, eventId) + // if err != nil { + // return err + // } - maxAttendees := int32(500) - if event.MaxAttendees != nil { - maxAttendees = *event.MaxAttendees - } + // maxAttendees := int32(500) + // if event.MaxAttendees != nil { + // maxAttendees = *event.MaxAttendees + // } var appAdmissionsData []ApplicantAdmissionsData for _, app := range applications { + if app.ExperienceRating == nil || app.PassionRating == nil { + return ErrMissingRatings + } + + var applicationShoolAndYear ApplicationSchoolAndYear + if err := json.Unmarshal(app.Application, &applicationShoolAndYear); err != nil { + s.logger.Debug().Bytes("App", app.Application).Msg("Application data") + return err + } + + var teamId uuid.UUID + if app.TeamID != nil { + teamId = *app.TeamID + } + + wScore, err := s.engine.CalculateWeightedScore(*app.PassionRating, *app.ExperienceRating) + if err != nil { + return err + } appAdmissionsData = append(appAdmissionsData, ApplicantAdmissionsData{ ID: app.UserID, - TeamID: , + TeamID: uuid.NullUUID{ + UUID: teamId, + Valid: app.TeamID != nil, + }, + WeightedScore: wScore, + SortKey: 0.0, + IsUFStudent: applicationShoolAndYear.School == "University of Florida", + IsEarlyCareer: applicationShoolAndYear.Year == "first_year" || applicationShoolAndYear.Year == "second_year", }) } + quota := QuotaState{ + TotalAccepted: 0, + TeamSlotsLeft: 50, + UFEarlyLeft: 210, + UFLateLeft: 140, + OtherEarlyLeft: 90, + OtherLateLeft: 60, + } + + teams, solo := groupAndSortTeams(appAdmissionsData) + applyTeamSortKey(teams) + acceptedTeams, remaining, quota := admitTeams(teams, solo, quota) + accepted, rejected, quota := admitSoloApplicants(remaining, quota) + + s.logger.Info().Int("Accepted", len(accepted)+len(acceptedTeams)).Int("Rejected", len(rejected)).Msg("Finished Algo") + return nil } + +func admitTeams(teams []TeamAdmissionData, solo []ApplicantAdmissionsData, initialQuota QuotaState) ( + []ApplicantAdmissionsData, // Accepted Applicants + []ApplicantAdmissionsData, // remaining applicants (joined with solo) + QuotaState, // Updated Quote +) { + admittedApplicants := make([]ApplicantAdmissionsData, 0, initialQuota.TotalAccepted) + remainingApplicants := make([]ApplicantAdmissionsData, 0) + remainingApplicants = append(remainingApplicants, solo...) + quota := initialQuota + + for _, team := range teams { + // All remaining members get appended to solo/remaining selection + if quota.TeamSlotsLeft <= int32(len(team.MembersAdmissionData)) { + remainingApplicants = append(remainingApplicants, team.MembersAdmissionData...) + continue + } + + required := countTeamSlots(team.MembersAdmissionData) + if canAdmitTeam(required, quota) { + admittedApplicants = append(admittedApplicants, team.MembersAdmissionData...) + + quota.TotalAccepted += int32(len(team.MembersAdmissionData)) + quota.TeamSlotsLeft -= int32(len(team.MembersAdmissionData)) + + quota.UFEarlyLeft -= required.UFEarlyLeft + quota.UFLateLeft -= required.UFLateLeft + quota.OtherEarlyLeft -= required.OtherEarlyLeft + quota.OtherLateLeft -= required.OtherLateLeft + } else { + remainingApplicants = append(remainingApplicants, team.MembersAdmissionData...) + } + } + + return admittedApplicants, remainingApplicants, quota +} + +type BucketConfig struct { + Name string + QuotaPtr *int32 + RolloverPtr *int32 +} + +func admitSoloApplicants(solo []ApplicantAdmissionsData, quota QuotaState) ( + []ApplicantAdmissionsData, // Accepted + []ApplicantAdmissionsData, // Rejected + QuotaState, +) { + var admittedSolo []ApplicantAdmissionsData + + pool := make(map[uuid.UUID]ApplicantAdmissionsData) + for _, app := range solo { + pool[app.ID] = app + } + + buckets := []BucketConfig{ + {"UF_Early", "a.UFEarlyLeft, "a.UFLateLeft}, + {"UF_Late", "a.UFLateLeft, "a.UFEarlyLeft}, + {"Other_Early", "a.OtherEarlyLeft, "a.OtherLateLeft}, + {"Other_Late", "a.OtherLateLeft, "a.OtherEarlyLeft}, + } + + for { + passAdmittedCount := int32(0) + + for _, bucket := range buckets { + originalCount := *bucket.QuotaPtr + if originalCount <= 0 { + continue + } + + bucketPool := filterApplicants(pool, bucket.Name) + applyIndividualSortKey(bucketPool) + sort.Slice(bucketPool, func(i, j int) bool { + return bucketPool[i].SortKey > bucketPool[j].SortKey + }) + + numToSelect := min(originalCount, int32(len(bucketPool))) + for i := range numToSelect { + + app := bucketPool[i] + admittedSolo = append(admittedSolo, app) + + *bucket.QuotaPtr -= 1 + quota.TotalAccepted += 1 + delete(pool, app.ID) + + passAdmittedCount++ + } + + slotsUnfilled := *bucket.QuotaPtr + if slotsUnfilled > 0 && bucket.RolloverPtr != nil { + *bucket.RolloverPtr += slotsUnfilled + + *bucket.QuotaPtr = 0 + + } + } + + if passAdmittedCount == 0 { + break // No admissions in a pass anymore! + } + } + + var rejected []ApplicantAdmissionsData + for _, applicant := range pool { + rejected = append(rejected, applicant) + } + + return admittedSolo, rejected, quota + + // for bucket, targetCount := range buckets { + // count := *targetCount + + // if count <= 0 { + // continue // Skip if bucket is already full/completed + // } + + // bucketPool := filterApplicants(pool, bucket) + // applyIndividualSortKey(bucketPool) + + // // Sort + // sort.Slice(bucketPool, func(i, j int) bool { + // return bucketPool[i].SortKey > bucketPool[j].SortKey + // }) + + // numToSelect := int(min(count, int32(len(bucketPool)))) + + // for i := range numToSelect { + // applicant := bucketPool[i] + // admittedSolo = append(admittedSolo, applicant) + + // *targetCount -= 1 + // quota.TotalAccepted += 1 + // delete(pool, applicant.ID) + // } + // } + + // var rejectedSolo []ApplicantAdmissionsData + // for _, applicant := range pool { + // rejectedSolo = append(rejectedSolo, applicant) + // } + + // return admittedSolo, rejectedSolo, quota +} + +func applyIndividualSortKey(apps []ApplicantAdmissionsData) { + var Epsilon float64 = 0.001 + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + for i := range apps { + app := &apps[i] + randVal := r.Float64() + exp := 1.0 / (app.WeightedScore + Epsilon) + + app.SortKey = math.Pow(randVal, exp) + } +} + +func filterApplicants( + pool map[uuid.UUID]ApplicantAdmissionsData, + bucketName string, +) []ApplicantAdmissionsData { + + var filtered []ApplicantAdmissionsData + + for _, app := range pool { + switch bucketName { + case "UF_Early": + if app.IsUFStudent && app.IsEarlyCareer { + filtered = append(filtered, app) + } + case "UF_Late": + if app.IsUFStudent && !app.IsEarlyCareer { + filtered = append(filtered, app) + } + case "Other_Early": + if !app.IsUFStudent && app.IsEarlyCareer { + filtered = append(filtered, app) + } + case "Other_Late": + if !app.IsUFStudent && !app.IsEarlyCareer { + filtered = append(filtered, app) + } + default: + // Should not happen if buckets map is defined correctly + } + } + return filtered +} +func canAdmitTeam(req QuotaState, curr QuotaState) bool { + return req.UFEarlyLeft <= curr.UFEarlyLeft && + req.UFLateLeft <= curr.UFLateLeft && + req.OtherEarlyLeft <= curr.OtherEarlyLeft && + req.OtherLateLeft <= curr.OtherLateLeft +} + +func countTeamSlots(members []ApplicantAdmissionsData) QuotaState { + reqQuota := QuotaState{ + TotalAccepted: 0, + TeamSlotsLeft: 0, + UFEarlyLeft: 0, + UFLateLeft: 0, + OtherEarlyLeft: 0, + OtherLateLeft: 0, + } + + for _, member := range members { + if member.IsUFStudent { + if member.IsEarlyCareer { + reqQuota.UFEarlyLeft += 1 + } else { + reqQuota.UFLateLeft += 1 + } + } else { + if member.IsEarlyCareer { + reqQuota.OtherEarlyLeft += 1 + } else { + reqQuota.OtherLateLeft += 1 + } + } + + reqQuota.TotalAccepted += 1 + reqQuota.TeamSlotsLeft += 1 + } + + return reqQuota +} + +func applyTeamSortKey(teams []TeamAdmissionData) { + const Epsilon float64 = 0.001 + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + for i := range teams { + // Get reference to apply in place + team := &teams[i] + + randVal := r.Float64() + + exponent := 1.0 / (team.AverageWeightedScore + Epsilon) + team.SortKey = math.Pow(randVal, exponent) + } + + sort.Slice(teams, func(i, j int) bool { + return teams[i].SortKey > teams[j].SortKey + }) +} + +func groupAndSortTeams(data []ApplicantAdmissionsData) ([]TeamAdmissionData, []ApplicantAdmissionsData) { + teamMap := make(map[uuid.UUID][]ApplicantAdmissionsData) + var soloApplicants []ApplicantAdmissionsData + for _, app := range data { + if app.TeamID.Valid { + teamMap[app.TeamID.UUID] = append(teamMap[app.TeamID.UUID], app) + } else { + soloApplicants = append(soloApplicants, app) + } + } + + var teams []TeamAdmissionData + for teamID, members := range teamMap { + var totalScore float64 + for _, member := range members { + totalScore += member.WeightedScore + } + + teams = append(teams, TeamAdmissionData{ + TeamID: teamID, + MembersAdmissionData: members, + AverageWeightedScore: totalScore / float64(len(members)), + }) + } + + return teams, soloApplicants +} + +func (s *BatService) LogAdmissionsStats(data []ApplicantAdmissionsData) { + type scoreStats struct { + count int + sum float64 + min float64 + max float64 + } + + newStats := func() scoreStats { + return scoreStats{ + min: math.Inf(1), + max: math.Inf(-1), + } + } + + update := func(s *scoreStats, score float64) { + s.count++ + s.sum += score + if score < s.min { + s.min = score + } + if score > s.max { + s.max = score + } + } + + finalize := func(s scoreStats) map[string]any { + if s.count == 0 { + return map[string]any{ + "count": 0, + } + } + return map[string]any{ + "count": s.count, + "avg": s.sum / float64(s.count), + "min": s.min, + "max": s.max, + } + } + + var ( + total = newStats() + uf = newStats() + nonUF = newStats() + earlyCareer = newStats() + upperCareer = newStats() + teamApplicants = newStats() + soloApplicants = newStats() + ) + + for _, a := range data { + update(&total, a.WeightedScore) + + if a.IsUFStudent { + update(&uf, a.WeightedScore) + } else { + update(&nonUF, a.WeightedScore) + } + + if a.IsEarlyCareer { + update(&earlyCareer, a.WeightedScore) + } else { + update(&upperCareer, a.WeightedScore) + } + + if a.TeamID.Valid { + update(&teamApplicants, a.WeightedScore) + } else { + update(&soloApplicants, a.WeightedScore) + } + } + + s.logger.Info(). + Int("total_applicants", len(data)). + Fields(map[string]any{ + "overall": finalize(total), + "uf_students": finalize(uf), + "non_uf_students": finalize(nonUF), + "early_career": finalize(earlyCareer), + "upper_career": finalize(upperCareer), + "team_applicants": finalize(teamApplicants), + "solo_applicants": finalize(soloApplicants), + }). + Msg("Admissions statistics snapshot") +} diff --git a/apps/api/internal/workers/bat.go b/apps/api/internal/workers/bat.go index 9a3daea9..a44cdc6e 100644 --- a/apps/api/internal/workers/bat.go +++ b/apps/api/internal/workers/bat.go @@ -37,6 +37,11 @@ func (w *BATWorker) HandleCalculateAdmissionsTask(ctx context.Context, t *asynq. } // Fire off bat service - w.batService.CalculateAdmissions() + err := w.batService.CalculateAdmissions(ctx, p.EventID) + if err != nil { + w.logger.Err(err).Msg("Something went wrong calculating admissions.") + return err + } + return nil } From afb971c936a71a258be32fe26ef6244749be115b Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Mon, 15 Dec 2025 16:30:12 -0500 Subject: [PATCH 04/11] feat: bat result crud --- .../20251215084922_add_bat_results_schema.sql | 11 +++ apps/api/internal/db/queries/bat_results.sql | 17 ++++ .../api/internal/db/repository/bat_results.go | 39 +++++++++ apps/api/internal/db/sqlc/accounts.sql.go | 2 +- apps/api/internal/db/sqlc/applications.sql.go | 2 +- apps/api/internal/db/sqlc/bat_results.sql.go | 79 +++++++++++++++++++ apps/api/internal/db/sqlc/db.go | 2 +- .../db/sqlc/event_interest_submissions.sql.go | 2 +- apps/api/internal/db/sqlc/event_roles.sql.go | 2 +- apps/api/internal/db/sqlc/events.sql.go | 2 +- apps/api/internal/db/sqlc/models.go | 10 ++- apps/api/internal/db/sqlc/sessions.sql.go | 2 +- apps/api/internal/db/sqlc/stats.sql.go | 2 +- .../internal/db/sqlc/team_invitations.sql.go | 2 +- .../db/sqlc/team_join_requests.sql.go | 2 +- apps/api/internal/db/sqlc/team_members.sql.go | 2 +- apps/api/internal/db/sqlc/teams.sql.go | 2 +- apps/api/internal/db/sqlc/users.sql.go | 2 +- apps/api/internal/services/bat.go | 56 +++++++++---- apps/api/internal/services/bat_results.go | 32 ++++++++ 20 files changed, 241 insertions(+), 29 deletions(-) create mode 100644 apps/api/internal/db/migrations/20251215084922_add_bat_results_schema.sql create mode 100644 apps/api/internal/db/queries/bat_results.sql create mode 100644 apps/api/internal/db/repository/bat_results.go create mode 100644 apps/api/internal/db/sqlc/bat_results.sql.go create mode 100644 apps/api/internal/services/bat_results.go diff --git a/apps/api/internal/db/migrations/20251215084922_add_bat_results_schema.sql b/apps/api/internal/db/migrations/20251215084922_add_bat_results_schema.sql new file mode 100644 index 00000000..6b5cdada --- /dev/null +++ b/apps/api/internal/db/migrations/20251215084922_add_bat_results_schema.sql @@ -0,0 +1,11 @@ +-- +goose Up +CREATE TABLE bat_results ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE, + accepted_applicants UUID[] NOT NULL DEFAULT '{}', + rejected_applicants UUID[] NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- +goose Down +DROP TABLE IF EXISTS bat_results; diff --git a/apps/api/internal/db/queries/bat_results.sql b/apps/api/internal/db/queries/bat_results.sql new file mode 100644 index 00000000..5c49c3b1 --- /dev/null +++ b/apps/api/internal/db/queries/bat_results.sql @@ -0,0 +1,17 @@ +-- name: AddResult :one +INSERT INTO bat_results ( + event_id, + accepted_applicants, + rejected_applicants +) VALUES ( + $1, $2, $3 +) RETURNING *; + +-- name: GetResultsByEventId :many +SELECT + accepted_applicants, + rejected_applicants, + created_at +FROM bat_results +WHERE event_id = @event_id::uuid +ORDER BY created_at DESC; diff --git a/apps/api/internal/db/repository/bat_results.go b/apps/api/internal/db/repository/bat_results.go new file mode 100644 index 00000000..aefbb8eb --- /dev/null +++ b/apps/api/internal/db/repository/bat_results.go @@ -0,0 +1,39 @@ +package repository + +import ( + "context" + "errors" + + "github.com/swamphacks/core/apps/api/internal/db" + "github.com/swamphacks/core/apps/api/internal/db/sqlc" +) + +var ( + ErrDuplicateResult = errors.New("result already exists in the database") +) + +type BatResultsRepository struct { + db *db.DB +} + +func NewBatResultsRepository(db *db.DB) *EventInterestRepository { + return &EventInterestRepository{ + db: db, + } +} + +func (r *BatResultsRepository) AddResult(ctx context.Context, params sqlc.AddResultParams) (*sqlc.BatResult, error) { + result, err := r.db.Query.AddResult(ctx, params) + if err != nil { + if db.IsUniqueViolation(err) { + return nil, ErrDuplicateResult + } + return nil, err + } + return &result, nil +} + +func (r *BatResultsRepository) GetResultsByEvent(ctx context.Context, eventId uuid.UUID) (*[]sqlc.BatResult, error) { + results, err := r.db.Query.GetResultsByEventId(ctx, eventId) + return &results, err +} diff --git a/apps/api/internal/db/sqlc/accounts.sql.go b/apps/api/internal/db/sqlc/accounts.sql.go index 628f20b2..b1c94d40 100644 --- a/apps/api/internal/db/sqlc/accounts.sql.go +++ b/apps/api/internal/db/sqlc/accounts.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: accounts.sql package sqlc diff --git a/apps/api/internal/db/sqlc/applications.sql.go b/apps/api/internal/db/sqlc/applications.sql.go index 7594d632..38740b4f 100644 --- a/apps/api/internal/db/sqlc/applications.sql.go +++ b/apps/api/internal/db/sqlc/applications.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: applications.sql package sqlc diff --git a/apps/api/internal/db/sqlc/bat_results.sql.go b/apps/api/internal/db/sqlc/bat_results.sql.go new file mode 100644 index 00000000..72dc1e1e --- /dev/null +++ b/apps/api/internal/db/sqlc/bat_results.sql.go @@ -0,0 +1,79 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: bat_results.sql + +package sqlc + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const addResult = `-- name: AddResult :one +INSERT INTO bat_results ( + event_id, + accepted_applicants, + rejected_applicants +) VALUES ( + $1, $2, $3 +) RETURNING id, event_id, accepted_applicants, rejected_applicants, created_at +` + +type AddResultParams struct { + EventID uuid.UUID `json:"event_id"` + AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` + RejectedApplicants []uuid.UUID `json:"rejected_applicants"` +} + +func (q *Queries) AddResult(ctx context.Context, arg AddResultParams) (BatResult, error) { + row := q.db.QueryRow(ctx, addResult, arg.EventID, arg.AcceptedApplicants, arg.RejectedApplicants) + var i BatResult + err := row.Scan( + &i.ID, + &i.EventID, + &i.AcceptedApplicants, + &i.RejectedApplicants, + &i.CreatedAt, + ) + return i, err +} + +const getResultsByEventId = `-- name: GetResultsByEventId :many +SELECT + accepted_applicants, + rejected_applicants, + created_at +FROM + bat_results +WHERE + event_id = $1::uuid +` + +type GetResultsByEventIdRow struct { + AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` + RejectedApplicants []uuid.UUID `json:"rejected_applicants"` + CreatedAt time.Time `json:"created_at"` +} + +func (q *Queries) GetResultsByEventId(ctx context.Context, eventID uuid.UUID) ([]GetResultsByEventIdRow, error) { + rows, err := q.db.Query(ctx, getResultsByEventId, eventID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetResultsByEventIdRow{} + for rows.Next() { + var i GetResultsByEventIdRow + if err := rows.Scan(&i.AcceptedApplicants, &i.RejectedApplicants, &i.CreatedAt); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/apps/api/internal/db/sqlc/db.go b/apps/api/internal/db/sqlc/db.go index 27251088..7a565074 100644 --- a/apps/api/internal/db/sqlc/db.go +++ b/apps/api/internal/db/sqlc/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package sqlc diff --git a/apps/api/internal/db/sqlc/event_interest_submissions.sql.go b/apps/api/internal/db/sqlc/event_interest_submissions.sql.go index 27239b85..bb73843d 100644 --- a/apps/api/internal/db/sqlc/event_interest_submissions.sql.go +++ b/apps/api/internal/db/sqlc/event_interest_submissions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: event_interest_submissions.sql package sqlc diff --git a/apps/api/internal/db/sqlc/event_roles.sql.go b/apps/api/internal/db/sqlc/event_roles.sql.go index 3a2b5aca..d0743895 100644 --- a/apps/api/internal/db/sqlc/event_roles.sql.go +++ b/apps/api/internal/db/sqlc/event_roles.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: event_roles.sql package sqlc diff --git a/apps/api/internal/db/sqlc/events.sql.go b/apps/api/internal/db/sqlc/events.sql.go index d81597ca..d97dad7c 100644 --- a/apps/api/internal/db/sqlc/events.sql.go +++ b/apps/api/internal/db/sqlc/events.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: events.sql package sqlc diff --git a/apps/api/internal/db/sqlc/models.go b/apps/api/internal/db/sqlc/models.go index 5cd4c35b..9830a5da 100644 --- a/apps/api/internal/db/sqlc/models.go +++ b/apps/api/internal/db/sqlc/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 package sqlc @@ -330,6 +330,14 @@ type AuthUser struct { EmailConsent bool `json:"email_consent"` } +type BatResult struct { + ID uuid.UUID `json:"id"` + EventID uuid.UUID `json:"event_id"` + AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` + RejectedApplicants []uuid.UUID `json:"rejected_applicants"` + CreatedAt time.Time `json:"created_at"` +} + type Event struct { ID uuid.UUID `json:"id"` Name string `json:"name"` diff --git a/apps/api/internal/db/sqlc/sessions.sql.go b/apps/api/internal/db/sqlc/sessions.sql.go index c95d1c82..abfb10aa 100644 --- a/apps/api/internal/db/sqlc/sessions.sql.go +++ b/apps/api/internal/db/sqlc/sessions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: sessions.sql package sqlc diff --git a/apps/api/internal/db/sqlc/stats.sql.go b/apps/api/internal/db/sqlc/stats.sql.go index e0e2a279..816a1afb 100644 --- a/apps/api/internal/db/sqlc/stats.sql.go +++ b/apps/api/internal/db/sqlc/stats.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: stats.sql package sqlc diff --git a/apps/api/internal/db/sqlc/team_invitations.sql.go b/apps/api/internal/db/sqlc/team_invitations.sql.go index 972762b4..faf97881 100644 --- a/apps/api/internal/db/sqlc/team_invitations.sql.go +++ b/apps/api/internal/db/sqlc/team_invitations.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: team_invitations.sql package sqlc diff --git a/apps/api/internal/db/sqlc/team_join_requests.sql.go b/apps/api/internal/db/sqlc/team_join_requests.sql.go index a81c7d8c..ffcf8667 100644 --- a/apps/api/internal/db/sqlc/team_join_requests.sql.go +++ b/apps/api/internal/db/sqlc/team_join_requests.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: team_join_requests.sql package sqlc diff --git a/apps/api/internal/db/sqlc/team_members.sql.go b/apps/api/internal/db/sqlc/team_members.sql.go index 5ecfaf60..79a1fbd4 100644 --- a/apps/api/internal/db/sqlc/team_members.sql.go +++ b/apps/api/internal/db/sqlc/team_members.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: team_members.sql package sqlc diff --git a/apps/api/internal/db/sqlc/teams.sql.go b/apps/api/internal/db/sqlc/teams.sql.go index 1ae5249a..0fa504f1 100644 --- a/apps/api/internal/db/sqlc/teams.sql.go +++ b/apps/api/internal/db/sqlc/teams.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: teams.sql package sqlc diff --git a/apps/api/internal/db/sqlc/users.sql.go b/apps/api/internal/db/sqlc/users.sql.go index bead77ac..f15bf32c 100644 --- a/apps/api/internal/db/sqlc/users.sql.go +++ b/apps/api/internal/db/sqlc/users.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.30.0 // source: users.sql package sqlc diff --git a/apps/api/internal/services/bat.go b/apps/api/internal/services/bat.go index 8653a3e5..3e339b4e 100644 --- a/apps/api/internal/services/bat.go +++ b/apps/api/internal/services/bat.go @@ -14,20 +14,23 @@ import ( "github.com/rs/zerolog" "github.com/swamphacks/core/apps/api/internal/bat" "github.com/swamphacks/core/apps/api/internal/db/repository" + "github.com/swamphacks/core/apps/api/internal/db/sqlc" "github.com/swamphacks/core/apps/api/internal/tasks" ) var ( ErrListApplicationsFailure = errors.New("Failed to retrieve applications") ErrMissingRatings = errors.New("Some applications are missing their review ratings") + ErrAddResultFailure = errors.New("Failed to add generated result") ) type BatService struct { - engine *bat.BatEngine - appRepo *repository.ApplicationRepository - eventRepo *repository.EventRepository - taskQueue *asynq.Client - logger zerolog.Logger + engine *bat.BatEngine + appRepo *repository.ApplicationRepository + eventRepo *repository.EventRepository + batResultsRepo *repository.BatResultsRepository + taskQueue *asynq.Client + logger zerolog.Logger } func NewBatService(engine *bat.BatEngine, appRepo *repository.ApplicationRepository, eventRepo *repository.EventRepository, taskQueue *asynq.Client, logger zerolog.Logger) *BatService { @@ -40,6 +43,25 @@ func NewBatService(engine *bat.BatEngine, appRepo *repository.ApplicationReposit } } +func (s *BatService) AddResult(ctx context.Context, eventID uuid.UUID, acceptedApplicants []uuid.UUID, rejectedApplicants []uuid.UUID) (*sqlc.BatResult, error) { + params := sqlc.AddResultParams{ + EventID: eventID, + AcceptedApplicants: acceptedApplicants, + RejectedApplicants: rejectedApplicants, + } + + result, err := s.batResultRepo.AddResult(ctx, params) + if err != nil && errors.Is(err, repository.ErrDuplicateResult) { + s.logger.Err(err).Msg("Could not insert result as it already exists.") + return nil, ErrResultConflict + } else if err != nil { + s.logger.Err(err).Msg("An unknown error was caught!") + return nil, ErrFailedToCreateResult + } + + return result, nil +} + func (s *BatService) QueueCalculateAdmissionsTask(eventId uuid.UUID) (*asynq.TaskInfo, error) { task, err := tasks.NewTaskCalculateAdmissions(tasks.CalculateAdmissionsPayload{ EventID: eventId, @@ -100,16 +122,6 @@ func (s *BatService) CalculateAdmissions(ctx context.Context, eventId uuid.UUID) return ErrListApplicationsFailure } - // event, err := s.eventRepo.GetEventByID(ctx, eventId) - // if err != nil { - // return err - // } - - // maxAttendees := int32(500) - // if event.MaxAttendees != nil { - // maxAttendees = *event.MaxAttendees - // } - var appAdmissionsData []ApplicantAdmissionsData for _, app := range applications { if app.ExperienceRating == nil || app.PassionRating == nil { @@ -158,6 +170,20 @@ func (s *BatService) CalculateAdmissions(ctx context.Context, eventId uuid.UUID) acceptedTeams, remaining, quota := admitTeams(teams, solo, quota) accepted, rejected, quota := admitSoloApplicants(remaining, quota) + var acceptedUUIDs uuid.UUID[] + var rejectedUUIDs uuid.UUID[] + for _, applicant := range accepted { + acceptedUUIDs = append(acceptedUUIDs, applicant.ID) + } + for _, applicant := range rejected { + rejectedUUIDs = append(acceptedUUIDs, applicant.ID) + } + batResult, err := s.AddResult(eventId, accepted, rejected) + + if err != nil { + return ErrAddResultFailure + } + s.logger.Info().Int("Accepted", len(accepted)+len(acceptedTeams)).Int("Rejected", len(rejected)).Msg("Finished Algo") return nil diff --git a/apps/api/internal/services/bat_results.go b/apps/api/internal/services/bat_results.go new file mode 100644 index 00000000..ccdec295 --- /dev/null +++ b/apps/api/internal/services/bat_results.go @@ -0,0 +1,32 @@ +package services + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/rs/zerolog" + "github.com/swamphacks/core/apps/api/internal/db/repository" + "github.com/swamphacks/core/apps/api/internal/db/sqlc" +) + +var ( + ErrResultConflict = errors.New("result already exists for this event") + ErrFailedToCreateResult = errors.New("failed to result") +) + +type BatResultService struct { + batResultRepo *repository.BatResultsRepository + logger zerolog.Logger +} + +func NewBatResultService(batResultRepo *repository.BatResultsRepository, logger zerolog.Logger) *BatResultService { + return &BatResultService{ + batResultRepo: batResultRepo, + logger: logger.With().Str("service", "BatResultService").Str("component", "bat_result_service").Logger(), + } +} + +func (s *BatResultService) GetResultsByEventId(ctx context.Context, eventId uuid.UUID) (*[]sqlc.BatResult, error) { + return s.batResultRepo.GetResultsByEvent(ctx, eventId) +} From 6e37f73f4d4192b0abe22050f5473efe6d357deb Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Mon, 15 Dec 2025 16:58:58 -0500 Subject: [PATCH 05/11] fix: improved crud --- apps/api/internal/db/queries/bat_results.sql | 7 ++++- .../api/internal/db/repository/bat_results.go | 25 ++++++++++++++-- apps/api/internal/db/sqlc/bat_results.sql.go | 29 +++++++++++++++---- apps/api/internal/services/bat.go | 13 +++++---- apps/api/internal/services/bat_results.go | 24 +++++++++++++-- 5 files changed, 83 insertions(+), 15 deletions(-) diff --git a/apps/api/internal/db/queries/bat_results.sql b/apps/api/internal/db/queries/bat_results.sql index 5c49c3b1..ba649bef 100644 --- a/apps/api/internal/db/queries/bat_results.sql +++ b/apps/api/internal/db/queries/bat_results.sql @@ -9,9 +9,14 @@ INSERT INTO bat_results ( -- name: GetResultsByEventId :many SELECT + id, accepted_applicants, rejected_applicants, created_at FROM bat_results -WHERE event_id = @event_id::uuid +WHERE event_id = $1 ORDER BY created_at DESC; + +-- name: DeleteResultById :execrows +DELETE FROM bat_results +WHERE id = $1; diff --git a/apps/api/internal/db/repository/bat_results.go b/apps/api/internal/db/repository/bat_results.go index aefbb8eb..8fb07b61 100644 --- a/apps/api/internal/db/repository/bat_results.go +++ b/apps/api/internal/db/repository/bat_results.go @@ -4,12 +4,17 @@ import ( "context" "errors" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" "github.com/swamphacks/core/apps/api/internal/db" "github.com/swamphacks/core/apps/api/internal/db/sqlc" ) var ( - ErrDuplicateResult = errors.New("result already exists in the database") + ErrDuplicateResult = errors.New("result already exists in the database") + ErrResultNotFound = errors.New("result not found") + ErrNoResultsDeleted = errors.New("no results deleted") + ErrMultipleResultsDeleted = errors.New("multiple results affected by delete query expecting to delete one") ) type BatResultsRepository struct { @@ -33,7 +38,23 @@ func (r *BatResultsRepository) AddResult(ctx context.Context, params sqlc.AddRes return &result, nil } -func (r *BatResultsRepository) GetResultsByEvent(ctx context.Context, eventId uuid.UUID) (*[]sqlc.BatResult, error) { +func (r *BatResultsRepository) GetResultsByEvent(ctx context.Context, eventId uuid.UUID) (*[]sqlc.GetResultsByEventIdRow, error) { results, err := r.db.Query.GetResultsByEventId(ctx, eventId) return &results, err } + +func (r *BatResultsRepository) DeleteEventById(ctx context.Context, id uuid.UUID) error { + affectedRows, err := r.db.Query.DeleteResultById(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrResultNotFound + } + } + if affectedRows == 0 { + return ErrNoResultsDeleted + } else if affectedRows > 1 { + return ErrMultipleResultsDeleted + } + + return err +} diff --git a/apps/api/internal/db/sqlc/bat_results.sql.go b/apps/api/internal/db/sqlc/bat_results.sql.go index 72dc1e1e..63c3c392 100644 --- a/apps/api/internal/db/sqlc/bat_results.sql.go +++ b/apps/api/internal/db/sqlc/bat_results.sql.go @@ -41,18 +41,32 @@ func (q *Queries) AddResult(ctx context.Context, arg AddResultParams) (BatResult return i, err } +const deleteResultById = `-- name: DeleteResultById :execrows +DELETE FROM bat_results +WHERE id = $1 +` + +func (q *Queries) DeleteResultById(ctx context.Context, id uuid.UUID) (int64, error) { + result, err := q.db.Exec(ctx, deleteResultById, id) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + const getResultsByEventId = `-- name: GetResultsByEventId :many SELECT + id, accepted_applicants, rejected_applicants, created_at -FROM - bat_results -WHERE - event_id = $1::uuid +FROM bat_results +WHERE event_id = $1 +ORDER BY created_at DESC ` type GetResultsByEventIdRow struct { + ID uuid.UUID `json:"id"` AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` RejectedApplicants []uuid.UUID `json:"rejected_applicants"` CreatedAt time.Time `json:"created_at"` @@ -67,7 +81,12 @@ func (q *Queries) GetResultsByEventId(ctx context.Context, eventID uuid.UUID) ([ items := []GetResultsByEventIdRow{} for rows.Next() { var i GetResultsByEventIdRow - if err := rows.Scan(&i.AcceptedApplicants, &i.RejectedApplicants, &i.CreatedAt); err != nil { + if err := rows.Scan( + &i.ID, + &i.AcceptedApplicants, + &i.RejectedApplicants, + &i.CreatedAt, + ); err != nil { return nil, err } items = append(items, i) diff --git a/apps/api/internal/services/bat.go b/apps/api/internal/services/bat.go index 3e339b4e..25668636 100644 --- a/apps/api/internal/services/bat.go +++ b/apps/api/internal/services/bat.go @@ -21,13 +21,14 @@ import ( var ( ErrListApplicationsFailure = errors.New("Failed to retrieve applications") ErrMissingRatings = errors.New("Some applications are missing their review ratings") - ErrAddResultFailure = errors.New("Failed to add generated result") + ErrAddResultFailure = errors.New("Failed to add generated result") ) type BatService struct { engine *bat.BatEngine appRepo *repository.ApplicationRepository eventRepo *repository.EventRepository + batResultRepo *repository.BatResultsRepository batResultsRepo *repository.BatResultsRepository taskQueue *asynq.Client logger zerolog.Logger @@ -170,15 +171,17 @@ func (s *BatService) CalculateAdmissions(ctx context.Context, eventId uuid.UUID) acceptedTeams, remaining, quota := admitTeams(teams, solo, quota) accepted, rejected, quota := admitSoloApplicants(remaining, quota) - var acceptedUUIDs uuid.UUID[] - var rejectedUUIDs uuid.UUID[] + var acceptedUUIDs []uuid.UUID + var rejectedUUIDs []uuid.UUID + for _, applicant := range accepted { acceptedUUIDs = append(acceptedUUIDs, applicant.ID) } + for _, applicant := range rejected { - rejectedUUIDs = append(acceptedUUIDs, applicant.ID) + rejectedUUIDs = append(rejectedUUIDs, applicant.ID) } - batResult, err := s.AddResult(eventId, accepted, rejected) + _, err = s.AddResult(ctx, eventId, acceptedUUIDs, rejectedUUIDs) if err != nil { return ErrAddResultFailure diff --git a/apps/api/internal/services/bat_results.go b/apps/api/internal/services/bat_results.go index ccdec295..42903800 100644 --- a/apps/api/internal/services/bat_results.go +++ b/apps/api/internal/services/bat_results.go @@ -12,7 +12,8 @@ import ( var ( ErrResultConflict = errors.New("result already exists for this event") - ErrFailedToCreateResult = errors.New("failed to result") + ErrFailedToCreateResult = errors.New("failed to add result") + ErrFailedToDeleteResult = errors.New("failed to delete result") ) type BatResultService struct { @@ -27,6 +28,25 @@ func NewBatResultService(batResultRepo *repository.BatResultsRepository, logger } } -func (s *BatResultService) GetResultsByEventId(ctx context.Context, eventId uuid.UUID) (*[]sqlc.BatResult, error) { +func (s *BatResultService) GetResultsByEventId(ctx context.Context, eventId uuid.UUID) (*[]sqlc.GetResultsByEventIdRow, error) { return s.batResultRepo.GetResultsByEvent(ctx, eventId) } + +func (s *BatResultService) DeleteResultById(ctx context.Context, id uuid.UUID) error { + err := s.batResultRepo.DeleteEventById(ctx, id) + if err != nil { + switch err { + case repository.ErrResultNotFound: + s.logger.Err(err).Msg(repository.ErrResultNotFound.Error()) + case repository.ErrNoResultsDeleted: + s.logger.Err(err).Msg(repository.ErrNoResultsDeleted.Error()) + case repository.ErrMultipleResultsDeleted: + s.logger.Err(err).Msg(repository.ErrMultipleResultsDeleted.Error()) + default: + s.logger.Err(err).Msg(repository.ErrUnknown.Error()) + } + return ErrFailedToDeleteResult + } + + return err +} From 048693507c0c791ebe661c23a2a5b60e2f620a76 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Mon, 15 Dec 2025 17:32:04 -0500 Subject: [PATCH 06/11] chore/current stuff --- apps/api/internal/api/handlers/bat_results.go | 0 .../20251215084922_add_bat_results_schema.sql | 16 ++++++++++++---- apps/api/internal/db/queries/bat_results.sql | 8 ++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 apps/api/internal/api/handlers/bat_results.go diff --git a/apps/api/internal/api/handlers/bat_results.go b/apps/api/internal/api/handlers/bat_results.go new file mode 100644 index 00000000..e69de29b diff --git a/apps/api/internal/db/migrations/20251215084922_add_bat_results_schema.sql b/apps/api/internal/db/migrations/20251215084922_add_bat_results_schema.sql index 6b5cdada..fbb61fdf 100644 --- a/apps/api/internal/db/migrations/20251215084922_add_bat_results_schema.sql +++ b/apps/api/internal/db/migrations/20251215084922_add_bat_results_schema.sql @@ -1,11 +1,19 @@ -- +goose Up +-- +goose StatementBegin +CREATE TYPE bat_result_status AS ENUM ('running','completed','failed'); + CREATE TABLE bat_results ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE, - accepted_applicants UUID[] NOT NULL DEFAULT '{}', - rejected_applicants UUID[] NOT NULL DEFAULT '{}', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); + accepted_applicants UUID[] DEFAULT '{}', + rejected_applicants UUID[] DEFAULT '{}', + status bat_result_status DEFAULT 'running', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + completed_at TIMESTAMPTZ; +-- +goose StatementEnd -- +goose Down +-- +goose StatementBegin DROP TABLE IF EXISTS bat_results; +DROP TYPE IF EXISTS bat_result_status; +-- +goose StatementEnd diff --git a/apps/api/internal/db/queries/bat_results.sql b/apps/api/internal/db/queries/bat_results.sql index ba649bef..032bb337 100644 --- a/apps/api/internal/db/queries/bat_results.sql +++ b/apps/api/internal/db/queries/bat_results.sql @@ -2,7 +2,9 @@ INSERT INTO bat_results ( event_id, accepted_applicants, - rejected_applicants + rejected_applicants, + status, + ) VALUES ( $1, $2, $3 ) RETURNING *; @@ -12,7 +14,9 @@ SELECT id, accepted_applicants, rejected_applicants, - created_at + status, + created_at, + completed_at FROM bat_results WHERE event_id = $1 ORDER BY created_at DESC; From 21d3057fdf186ca3058e22c49459fe2e72b48424 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Mon, 15 Dec 2025 18:50:40 -0500 Subject: [PATCH 07/11] chore: rename --- ...=> 20251215225937_add_bat_runs_schema.sql} | 14 +- apps/api/internal/db/queries/bat_results.sql | 26 --- apps/api/internal/db/queries/bat_runs.sql | 44 +++++ .../api/internal/db/repository/bat_results.go | 60 ------ apps/api/internal/db/repository/bat_runs.go | 75 ++++++++ apps/api/internal/db/sqlc/accounts.sql.go | 2 +- apps/api/internal/db/sqlc/applications.sql.go | 2 +- apps/api/internal/db/sqlc/bat_results.sql.go | 98 ---------- apps/api/internal/db/sqlc/bat_runs.sql.go | 172 ++++++++++++++++++ apps/api/internal/db/sqlc/db.go | 2 +- .../db/sqlc/event_interest_submissions.sql.go | 2 +- apps/api/internal/db/sqlc/event_roles.sql.go | 2 +- apps/api/internal/db/sqlc/events.sql.go | 2 +- apps/api/internal/db/sqlc/models.go | 59 +++++- apps/api/internal/db/sqlc/sessions.sql.go | 2 +- apps/api/internal/db/sqlc/stats.sql.go | 2 +- .../internal/db/sqlc/team_invitations.sql.go | 2 +- .../db/sqlc/team_join_requests.sql.go | 2 +- apps/api/internal/db/sqlc/team_members.sql.go | 2 +- apps/api/internal/db/sqlc/teams.sql.go | 2 +- apps/api/internal/db/sqlc/users.sql.go | 2 +- apps/api/internal/services/bat.go | 73 ++++++-- apps/api/internal/services/bat_results.go | 52 ------ 23 files changed, 419 insertions(+), 280 deletions(-) rename apps/api/internal/db/migrations/{20251215084922_add_bat_results_schema.sql => 20251215225937_add_bat_runs_schema.sql} (60%) delete mode 100644 apps/api/internal/db/queries/bat_results.sql create mode 100644 apps/api/internal/db/queries/bat_runs.sql delete mode 100644 apps/api/internal/db/repository/bat_results.go create mode 100644 apps/api/internal/db/repository/bat_runs.go delete mode 100644 apps/api/internal/db/sqlc/bat_results.sql.go create mode 100644 apps/api/internal/db/sqlc/bat_runs.sql.go delete mode 100644 apps/api/internal/services/bat_results.go diff --git a/apps/api/internal/db/migrations/20251215084922_add_bat_results_schema.sql b/apps/api/internal/db/migrations/20251215225937_add_bat_runs_schema.sql similarity index 60% rename from apps/api/internal/db/migrations/20251215084922_add_bat_results_schema.sql rename to apps/api/internal/db/migrations/20251215225937_add_bat_runs_schema.sql index fbb61fdf..da31c9c1 100644 --- a/apps/api/internal/db/migrations/20251215084922_add_bat_results_schema.sql +++ b/apps/api/internal/db/migrations/20251215225937_add_bat_runs_schema.sql @@ -1,19 +1,21 @@ -- +goose Up -- +goose StatementBegin -CREATE TYPE bat_result_status AS ENUM ('running','completed','failed'); +CREATE TYPE bat_run_status AS ENUM ('running','completed','failed'); -CREATE TABLE bat_results ( +CREATE TABLE bat_runs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE, accepted_applicants UUID[] DEFAULT '{}', rejected_applicants UUID[] DEFAULT '{}', - status bat_result_status DEFAULT 'running', + status bat_run_status DEFAULT 'running', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - completed_at TIMESTAMPTZ; + completed_at TIMESTAMPTZ +); -- +goose StatementEnd -- +goose Down -- +goose StatementBegin -DROP TABLE IF EXISTS bat_results; -DROP TYPE IF EXISTS bat_result_status; +DROP TABLE IF EXISTS bat_runs; +DROP TYPE IF EXISTS bat_run_status; -- +goose StatementEnd + diff --git a/apps/api/internal/db/queries/bat_results.sql b/apps/api/internal/db/queries/bat_results.sql deleted file mode 100644 index 032bb337..00000000 --- a/apps/api/internal/db/queries/bat_results.sql +++ /dev/null @@ -1,26 +0,0 @@ --- name: AddResult :one -INSERT INTO bat_results ( - event_id, - accepted_applicants, - rejected_applicants, - status, - -) VALUES ( - $1, $2, $3 -) RETURNING *; - --- name: GetResultsByEventId :many -SELECT - id, - accepted_applicants, - rejected_applicants, - status, - created_at, - completed_at -FROM bat_results -WHERE event_id = $1 -ORDER BY created_at DESC; - --- name: DeleteResultById :execrows -DELETE FROM bat_results -WHERE id = $1; diff --git a/apps/api/internal/db/queries/bat_runs.sql b/apps/api/internal/db/queries/bat_runs.sql new file mode 100644 index 00000000..549f8d56 --- /dev/null +++ b/apps/api/internal/db/queries/bat_runs.sql @@ -0,0 +1,44 @@ +-- name: AddRun :one +INSERT INTO bat_runs ( + event_id +) VALUES ( + $1 +) RETURNING *; + +-- name: GetRunByEventId :one +SELECT + id, + accepted_applicants, + rejected_applicants, + status, + created_at, + completed_at +FROM bat_runs +WHERE event_id = $1; + +-- name: GetRunsByEventId :many +SELECT + id, + accepted_applicants, + rejected_applicants, + status, + created_at, + completed_at +FROM bat_runs +WHERE event_id = $1 +ORDER BY created_at DESC; + +-- name: UpdateRunById :exec +UPDATE bat_runs +SET + accepted_applicants = CASE WHEN @accepted_applicants_do_update::boolean THEN @accepted_applicants ELSE accepted_applicants END, + rejected_applicants = CASE WHEN @rejected_applicants_do_update::boolean THEN @rejected_applicants ELSE rejected_applicants END, + status = CASE WHEN @status_do_update::boolean THEN @status ELSE status END, + created_at = CASE WHEN @created_at_do_update::boolean THEN @created_at ELSE created_at END +WHERE + id = @id::uuid +RETURNING *; + +-- name: DeleteRunById :execrows +DELETE FROM bat_runs +WHERE id = $1; diff --git a/apps/api/internal/db/repository/bat_results.go b/apps/api/internal/db/repository/bat_results.go deleted file mode 100644 index 8fb07b61..00000000 --- a/apps/api/internal/db/repository/bat_results.go +++ /dev/null @@ -1,60 +0,0 @@ -package repository - -import ( - "context" - "errors" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/swamphacks/core/apps/api/internal/db" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" -) - -var ( - ErrDuplicateResult = errors.New("result already exists in the database") - ErrResultNotFound = errors.New("result not found") - ErrNoResultsDeleted = errors.New("no results deleted") - ErrMultipleResultsDeleted = errors.New("multiple results affected by delete query expecting to delete one") -) - -type BatResultsRepository struct { - db *db.DB -} - -func NewBatResultsRepository(db *db.DB) *EventInterestRepository { - return &EventInterestRepository{ - db: db, - } -} - -func (r *BatResultsRepository) AddResult(ctx context.Context, params sqlc.AddResultParams) (*sqlc.BatResult, error) { - result, err := r.db.Query.AddResult(ctx, params) - if err != nil { - if db.IsUniqueViolation(err) { - return nil, ErrDuplicateResult - } - return nil, err - } - return &result, nil -} - -func (r *BatResultsRepository) GetResultsByEvent(ctx context.Context, eventId uuid.UUID) (*[]sqlc.GetResultsByEventIdRow, error) { - results, err := r.db.Query.GetResultsByEventId(ctx, eventId) - return &results, err -} - -func (r *BatResultsRepository) DeleteEventById(ctx context.Context, id uuid.UUID) error { - affectedRows, err := r.db.Query.DeleteResultById(ctx, id) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return ErrResultNotFound - } - } - if affectedRows == 0 { - return ErrNoResultsDeleted - } else if affectedRows > 1 { - return ErrMultipleResultsDeleted - } - - return err -} diff --git a/apps/api/internal/db/repository/bat_runs.go b/apps/api/internal/db/repository/bat_runs.go new file mode 100644 index 00000000..6f1847e8 --- /dev/null +++ b/apps/api/internal/db/repository/bat_runs.go @@ -0,0 +1,75 @@ +package repository + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/swamphacks/core/apps/api/internal/db" + "github.com/swamphacks/core/apps/api/internal/db/sqlc" +) + +var ( + ErrDuplicateRun = errors.New("Run already exists in the database") + ErrRunNotFound = errors.New("Run not found") + ErrNoRunsDeleted = errors.New("No Runs deleted") + ErrMultipleRunsDeleted = errors.New("Multiple Runs affected by delete query expecting to delete one") +) + +type BatRunsRepository struct { + db *db.DB +} + +func NewBatRunsRepository(db *db.DB) *BatRunsRepository { + return &BatRunsRepository{ + db: db, + } +} + +func (r *BatRunsRepository) AddRun(ctx context.Context, eventId uuid.UUID) (*sqlc.BatRun, error) { + run, err := r.db.Query.AddRun(ctx, eventId) + if err != nil { + if db.IsUniqueViolation(err) { + return nil, ErrDuplicateRun + } + return nil, err + } + return &run, nil +} + +func (r *BatRunsRepository) GetRunByEventId(ctx context.Context, eventId uuid.UUID) (*sqlc.GetRunByEventIdRow, error) { + run, err := r.db.Query.GetRunByEventId(ctx, eventId) + return &run, err +} + +func (r *BatRunsRepository) GetRunsByEventId(ctx context.Context, eventId uuid.UUID) (*[]sqlc.GetRunsByEventIdRow, error) { + runs, err := r.db.Query.GetRunsByEventId(ctx, eventId) + return &runs, err +} + +func (r *BatRunsRepository) UpdateRunById(ctx context.Context, params sqlc.UpdateRunByIdParams) error { + err := r.db.Query.UpdateRunById(ctx, params) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrEventNotFound + } + } + return err +} + +func (r *BatRunsRepository) DeletRunById(ctx context.Context, id uuid.UUID) error { + affectedRows, err := r.db.Query.DeleteRunById(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrRunNotFound + } + } + if affectedRows == 0 { + return ErrNoRunsDeleted + } else if affectedRows > 1 { + return ErrMultipleRunsDeleted + } + + return err +} diff --git a/apps/api/internal/db/sqlc/accounts.sql.go b/apps/api/internal/db/sqlc/accounts.sql.go index b1c94d40..628f20b2 100644 --- a/apps/api/internal/db/sqlc/accounts.sql.go +++ b/apps/api/internal/db/sqlc/accounts.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: accounts.sql package sqlc diff --git a/apps/api/internal/db/sqlc/applications.sql.go b/apps/api/internal/db/sqlc/applications.sql.go index 38740b4f..7594d632 100644 --- a/apps/api/internal/db/sqlc/applications.sql.go +++ b/apps/api/internal/db/sqlc/applications.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: applications.sql package sqlc diff --git a/apps/api/internal/db/sqlc/bat_results.sql.go b/apps/api/internal/db/sqlc/bat_results.sql.go deleted file mode 100644 index 63c3c392..00000000 --- a/apps/api/internal/db/sqlc/bat_results.sql.go +++ /dev/null @@ -1,98 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.30.0 -// source: bat_results.sql - -package sqlc - -import ( - "context" - "time" - - "github.com/google/uuid" -) - -const addResult = `-- name: AddResult :one -INSERT INTO bat_results ( - event_id, - accepted_applicants, - rejected_applicants -) VALUES ( - $1, $2, $3 -) RETURNING id, event_id, accepted_applicants, rejected_applicants, created_at -` - -type AddResultParams struct { - EventID uuid.UUID `json:"event_id"` - AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` - RejectedApplicants []uuid.UUID `json:"rejected_applicants"` -} - -func (q *Queries) AddResult(ctx context.Context, arg AddResultParams) (BatResult, error) { - row := q.db.QueryRow(ctx, addResult, arg.EventID, arg.AcceptedApplicants, arg.RejectedApplicants) - var i BatResult - err := row.Scan( - &i.ID, - &i.EventID, - &i.AcceptedApplicants, - &i.RejectedApplicants, - &i.CreatedAt, - ) - return i, err -} - -const deleteResultById = `-- name: DeleteResultById :execrows -DELETE FROM bat_results -WHERE id = $1 -` - -func (q *Queries) DeleteResultById(ctx context.Context, id uuid.UUID) (int64, error) { - result, err := q.db.Exec(ctx, deleteResultById, id) - if err != nil { - return 0, err - } - return result.RowsAffected(), nil -} - -const getResultsByEventId = `-- name: GetResultsByEventId :many -SELECT - id, - accepted_applicants, - rejected_applicants, - created_at -FROM bat_results -WHERE event_id = $1 -ORDER BY created_at DESC -` - -type GetResultsByEventIdRow struct { - ID uuid.UUID `json:"id"` - AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` - RejectedApplicants []uuid.UUID `json:"rejected_applicants"` - CreatedAt time.Time `json:"created_at"` -} - -func (q *Queries) GetResultsByEventId(ctx context.Context, eventID uuid.UUID) ([]GetResultsByEventIdRow, error) { - rows, err := q.db.Query(ctx, getResultsByEventId, eventID) - if err != nil { - return nil, err - } - defer rows.Close() - items := []GetResultsByEventIdRow{} - for rows.Next() { - var i GetResultsByEventIdRow - if err := rows.Scan( - &i.ID, - &i.AcceptedApplicants, - &i.RejectedApplicants, - &i.CreatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} diff --git a/apps/api/internal/db/sqlc/bat_runs.sql.go b/apps/api/internal/db/sqlc/bat_runs.sql.go new file mode 100644 index 00000000..4edd17df --- /dev/null +++ b/apps/api/internal/db/sqlc/bat_runs.sql.go @@ -0,0 +1,172 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: bat_runs.sql + +package sqlc + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const addRun = `-- name: AddRun :one +INSERT INTO bat_runs ( + event_id +) VALUES ( + $1 +) RETURNING id, event_id, accepted_applicants, rejected_applicants, status, created_at, completed_at +` + +func (q *Queries) AddRun(ctx context.Context, eventID uuid.UUID) (BatRun, error) { + row := q.db.QueryRow(ctx, addRun, eventID) + var i BatRun + err := row.Scan( + &i.ID, + &i.EventID, + &i.AcceptedApplicants, + &i.RejectedApplicants, + &i.Status, + &i.CreatedAt, + &i.CompletedAt, + ) + return i, err +} + +const deleteRunById = `-- name: DeleteRunById :execrows +DELETE FROM bat_runs +WHERE id = $1 +` + +func (q *Queries) DeleteRunById(ctx context.Context, id uuid.UUID) (int64, error) { + result, err := q.db.Exec(ctx, deleteRunById, id) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + +const getRunByEventId = `-- name: GetRunByEventId :one +SELECT + id, + accepted_applicants, + rejected_applicants, + status, + created_at, + completed_at +FROM bat_runs +WHERE event_id = $1 +` + +type GetRunByEventIdRow struct { + ID uuid.UUID `json:"id"` + AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` + RejectedApplicants []uuid.UUID `json:"rejected_applicants"` + Status NullBatRunStatus `json:"status"` + CreatedAt time.Time `json:"created_at"` + CompletedAt *time.Time `json:"completed_at"` +} + +func (q *Queries) GetRunByEventId(ctx context.Context, eventID uuid.UUID) (GetRunByEventIdRow, error) { + row := q.db.QueryRow(ctx, getRunByEventId, eventID) + var i GetRunByEventIdRow + err := row.Scan( + &i.ID, + &i.AcceptedApplicants, + &i.RejectedApplicants, + &i.Status, + &i.CreatedAt, + &i.CompletedAt, + ) + return i, err +} + +const getRunsByEventId = `-- name: GetRunsByEventId :many +SELECT + id, + accepted_applicants, + rejected_applicants, + status, + created_at, + completed_at +FROM bat_runs +WHERE event_id = $1 +ORDER BY created_at DESC +` + +type GetRunsByEventIdRow struct { + ID uuid.UUID `json:"id"` + AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` + RejectedApplicants []uuid.UUID `json:"rejected_applicants"` + Status NullBatRunStatus `json:"status"` + CreatedAt time.Time `json:"created_at"` + CompletedAt *time.Time `json:"completed_at"` +} + +func (q *Queries) GetRunsByEventId(ctx context.Context, eventID uuid.UUID) ([]GetRunsByEventIdRow, error) { + rows, err := q.db.Query(ctx, getRunsByEventId, eventID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetRunsByEventIdRow{} + for rows.Next() { + var i GetRunsByEventIdRow + if err := rows.Scan( + &i.ID, + &i.AcceptedApplicants, + &i.RejectedApplicants, + &i.Status, + &i.CreatedAt, + &i.CompletedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateRunById = `-- name: UpdateRunById :exec +UPDATE bat_runs +SET + accepted_applicants = CASE WHEN $1::boolean THEN $2 ELSE accepted_applicants END, + rejected_applicants = CASE WHEN $3::boolean THEN $4 ELSE rejected_applicants END, + status = CASE WHEN $5::boolean THEN $6 ELSE status END, + created_at = CASE WHEN $7::boolean THEN $8 ELSE created_at END +WHERE + id = $9::uuid +RETURNING id, event_id, accepted_applicants, rejected_applicants, status, created_at, completed_at +` + +type UpdateRunByIdParams struct { + AcceptedApplicantsDoUpdate bool `json:"accepted_applicants_do_update"` + AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` + RejectedApplicantsDoUpdate bool `json:"rejected_applicants_do_update"` + RejectedApplicants []uuid.UUID `json:"rejected_applicants"` + StatusDoUpdate bool `json:"status_do_update"` + Status NullBatRunStatus `json:"status"` + CreatedAtDoUpdate bool `json:"created_at_do_update"` + CreatedAt time.Time `json:"created_at"` + ID uuid.UUID `json:"id"` +} + +func (q *Queries) UpdateRunById(ctx context.Context, arg UpdateRunByIdParams) error { + _, err := q.db.Exec(ctx, updateRunById, + arg.AcceptedApplicantsDoUpdate, + arg.AcceptedApplicants, + arg.RejectedApplicantsDoUpdate, + arg.RejectedApplicants, + arg.StatusDoUpdate, + arg.Status, + arg.CreatedAtDoUpdate, + arg.CreatedAt, + arg.ID, + ) + return err +} diff --git a/apps/api/internal/db/sqlc/db.go b/apps/api/internal/db/sqlc/db.go index 7a565074..27251088 100644 --- a/apps/api/internal/db/sqlc/db.go +++ b/apps/api/internal/db/sqlc/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 package sqlc diff --git a/apps/api/internal/db/sqlc/event_interest_submissions.sql.go b/apps/api/internal/db/sqlc/event_interest_submissions.sql.go index bb73843d..27239b85 100644 --- a/apps/api/internal/db/sqlc/event_interest_submissions.sql.go +++ b/apps/api/internal/db/sqlc/event_interest_submissions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: event_interest_submissions.sql package sqlc diff --git a/apps/api/internal/db/sqlc/event_roles.sql.go b/apps/api/internal/db/sqlc/event_roles.sql.go index d0743895..3a2b5aca 100644 --- a/apps/api/internal/db/sqlc/event_roles.sql.go +++ b/apps/api/internal/db/sqlc/event_roles.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: event_roles.sql package sqlc diff --git a/apps/api/internal/db/sqlc/events.sql.go b/apps/api/internal/db/sqlc/events.sql.go index d97dad7c..d81597ca 100644 --- a/apps/api/internal/db/sqlc/events.sql.go +++ b/apps/api/internal/db/sqlc/events.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: events.sql package sqlc diff --git a/apps/api/internal/db/sqlc/models.go b/apps/api/internal/db/sqlc/models.go index 9830a5da..024d4e2e 100644 --- a/apps/api/internal/db/sqlc/models.go +++ b/apps/api/internal/db/sqlc/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 package sqlc @@ -101,6 +101,49 @@ func (ns NullAuthUserRole) Value() (driver.Value, error) { return string(ns.AuthUserRole), nil } +type BatRunStatus string + +const ( + BatRunStatusRunning BatRunStatus = "running" + BatRunStatusCompleted BatRunStatus = "completed" + BatRunStatusFailed BatRunStatus = "failed" +) + +func (e *BatRunStatus) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = BatRunStatus(s) + case string: + *e = BatRunStatus(s) + default: + return fmt.Errorf("unsupported scan type for BatRunStatus: %T", src) + } + return nil +} + +type NullBatRunStatus struct { + BatRunStatus BatRunStatus `json:"bat_run_status"` + Valid bool `json:"valid"` // Valid is true if BatRunStatus is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullBatRunStatus) Scan(value interface{}) error { + if value == nil { + ns.BatRunStatus, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.BatRunStatus.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullBatRunStatus) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.BatRunStatus), nil +} + type EventRoleType string const ( @@ -330,12 +373,14 @@ type AuthUser struct { EmailConsent bool `json:"email_consent"` } -type BatResult struct { - ID uuid.UUID `json:"id"` - EventID uuid.UUID `json:"event_id"` - AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` - RejectedApplicants []uuid.UUID `json:"rejected_applicants"` - CreatedAt time.Time `json:"created_at"` +type BatRun struct { + ID uuid.UUID `json:"id"` + EventID uuid.UUID `json:"event_id"` + AcceptedApplicants []uuid.UUID `json:"accepted_applicants"` + RejectedApplicants []uuid.UUID `json:"rejected_applicants"` + Status NullBatRunStatus `json:"status"` + CreatedAt time.Time `json:"created_at"` + CompletedAt *time.Time `json:"completed_at"` } type Event struct { diff --git a/apps/api/internal/db/sqlc/sessions.sql.go b/apps/api/internal/db/sqlc/sessions.sql.go index abfb10aa..c95d1c82 100644 --- a/apps/api/internal/db/sqlc/sessions.sql.go +++ b/apps/api/internal/db/sqlc/sessions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: sessions.sql package sqlc diff --git a/apps/api/internal/db/sqlc/stats.sql.go b/apps/api/internal/db/sqlc/stats.sql.go index 816a1afb..e0e2a279 100644 --- a/apps/api/internal/db/sqlc/stats.sql.go +++ b/apps/api/internal/db/sqlc/stats.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: stats.sql package sqlc diff --git a/apps/api/internal/db/sqlc/team_invitations.sql.go b/apps/api/internal/db/sqlc/team_invitations.sql.go index faf97881..972762b4 100644 --- a/apps/api/internal/db/sqlc/team_invitations.sql.go +++ b/apps/api/internal/db/sqlc/team_invitations.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: team_invitations.sql package sqlc diff --git a/apps/api/internal/db/sqlc/team_join_requests.sql.go b/apps/api/internal/db/sqlc/team_join_requests.sql.go index ffcf8667..a81c7d8c 100644 --- a/apps/api/internal/db/sqlc/team_join_requests.sql.go +++ b/apps/api/internal/db/sqlc/team_join_requests.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: team_join_requests.sql package sqlc diff --git a/apps/api/internal/db/sqlc/team_members.sql.go b/apps/api/internal/db/sqlc/team_members.sql.go index 79a1fbd4..5ecfaf60 100644 --- a/apps/api/internal/db/sqlc/team_members.sql.go +++ b/apps/api/internal/db/sqlc/team_members.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: team_members.sql package sqlc diff --git a/apps/api/internal/db/sqlc/teams.sql.go b/apps/api/internal/db/sqlc/teams.sql.go index 0fa504f1..1ae5249a 100644 --- a/apps/api/internal/db/sqlc/teams.sql.go +++ b/apps/api/internal/db/sqlc/teams.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: teams.sql package sqlc diff --git a/apps/api/internal/db/sqlc/users.sql.go b/apps/api/internal/db/sqlc/users.sql.go index f15bf32c..bead77ac 100644 --- a/apps/api/internal/db/sqlc/users.sql.go +++ b/apps/api/internal/db/sqlc/users.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.29.0 // source: users.sql package sqlc diff --git a/apps/api/internal/services/bat.go b/apps/api/internal/services/bat.go index 25668636..d7786734 100644 --- a/apps/api/internal/services/bat.go +++ b/apps/api/internal/services/bat.go @@ -22,16 +22,19 @@ var ( ErrListApplicationsFailure = errors.New("Failed to retrieve applications") ErrMissingRatings = errors.New("Some applications are missing their review ratings") ErrAddResultFailure = errors.New("Failed to add generated result") + ErrRunConflict = errors.New("Run already exists for this event") + ErrFailedToCreateRun = errors.New("Failed to add run") + ErrFailedToDeleteRun = errors.New("Failed to delete run") + ErrFailedToUpdateRun = errors.New("Failed to update run") ) type BatService struct { - engine *bat.BatEngine - appRepo *repository.ApplicationRepository - eventRepo *repository.EventRepository - batResultRepo *repository.BatResultsRepository - batResultsRepo *repository.BatResultsRepository - taskQueue *asynq.Client - logger zerolog.Logger + engine *bat.BatEngine + appRepo *repository.ApplicationRepository + eventRepo *repository.EventRepository + batRunsRepo *repository.BatRunsRepository + taskQueue *asynq.Client + logger zerolog.Logger } func NewBatService(engine *bat.BatEngine, appRepo *repository.ApplicationRepository, eventRepo *repository.EventRepository, taskQueue *asynq.Client, logger zerolog.Logger) *BatService { @@ -44,23 +47,57 @@ func NewBatService(engine *bat.BatEngine, appRepo *repository.ApplicationReposit } } -func (s *BatService) AddResult(ctx context.Context, eventID uuid.UUID, acceptedApplicants []uuid.UUID, rejectedApplicants []uuid.UUID) (*sqlc.BatResult, error) { - params := sqlc.AddResultParams{ - EventID: eventID, - AcceptedApplicants: acceptedApplicants, - RejectedApplicants: rejectedApplicants, - } +func (s *BatService) AddRun(ctx context.Context, eventId uuid.UUID) (*sqlc.BatRun, error) { - result, err := s.batResultRepo.AddResult(ctx, params) - if err != nil && errors.Is(err, repository.ErrDuplicateResult) { + run, err := s.batRunsRepo.AddRun(ctx, eventId) + if err != nil && errors.Is(err, repository.ErrDuplicateRun) { s.logger.Err(err).Msg("Could not insert result as it already exists.") - return nil, ErrResultConflict + return nil, ErrRunConflict } else if err != nil { s.logger.Err(err).Msg("An unknown error was caught!") - return nil, ErrFailedToCreateResult + return nil, ErrFailedToCreateRun + } + + return run, nil +} + +func (s *BatService) GetRunsByEventId(ctx context.Context, eventId uuid.UUID) (*[]sqlc.GetRunsByEventIdRow, error) { + return s.batRunsRepo.GetRunsByEventId(ctx, eventId) +} + +func (s *BatService) UpdateRunById(ctx context.Context, params sqlc.UpdateRunByIdParams) (*sqlc.GetRunByEventIdRow, error) { + err := s.batRunsRepo.UpdateRunById(ctx, params) + if err != nil { + if errors.Is(err, repository.ErrRunNotFound) { + s.logger.Err(err).Msg(repository.ErrRunNotFound.Error()) + } else { + s.logger.Err(err).Msg(repository.ErrUnknown.Error()) + } + return nil, ErrFailedToUpdateRun + } + + run, err := s.batRunsRepo.GetRunByEventId(ctx, params.ID) + + return run, err +} + +func (s *BatService) DeleteRunById(ctx context.Context, id uuid.UUID) error { + err := s.batRunsRepo.DeletRunById(ctx, id) + if err != nil { + switch err { + case repository.ErrRunNotFound: + s.logger.Err(err).Msg(repository.ErrRunNotFound.Error()) + case repository.ErrNoRunsDeleted: + s.logger.Err(err).Msg(repository.ErrNoRunsDeleted.Error()) + case repository.ErrMultipleRunsDeleted: + s.logger.Err(err).Msg(repository.ErrMultipleRunsDeleted.Error()) + default: + s.logger.Err(err).Msg(repository.ErrUnknown.Error()) + } + return ErrFailedToDeleteRun } - return result, nil + return err } func (s *BatService) QueueCalculateAdmissionsTask(eventId uuid.UUID) (*asynq.TaskInfo, error) { diff --git a/apps/api/internal/services/bat_results.go b/apps/api/internal/services/bat_results.go deleted file mode 100644 index 42903800..00000000 --- a/apps/api/internal/services/bat_results.go +++ /dev/null @@ -1,52 +0,0 @@ -package services - -import ( - "context" - "errors" - - "github.com/google/uuid" - "github.com/rs/zerolog" - "github.com/swamphacks/core/apps/api/internal/db/repository" - "github.com/swamphacks/core/apps/api/internal/db/sqlc" -) - -var ( - ErrResultConflict = errors.New("result already exists for this event") - ErrFailedToCreateResult = errors.New("failed to add result") - ErrFailedToDeleteResult = errors.New("failed to delete result") -) - -type BatResultService struct { - batResultRepo *repository.BatResultsRepository - logger zerolog.Logger -} - -func NewBatResultService(batResultRepo *repository.BatResultsRepository, logger zerolog.Logger) *BatResultService { - return &BatResultService{ - batResultRepo: batResultRepo, - logger: logger.With().Str("service", "BatResultService").Str("component", "bat_result_service").Logger(), - } -} - -func (s *BatResultService) GetResultsByEventId(ctx context.Context, eventId uuid.UUID) (*[]sqlc.GetResultsByEventIdRow, error) { - return s.batResultRepo.GetResultsByEvent(ctx, eventId) -} - -func (s *BatResultService) DeleteResultById(ctx context.Context, id uuid.UUID) error { - err := s.batResultRepo.DeleteEventById(ctx, id) - if err != nil { - switch err { - case repository.ErrResultNotFound: - s.logger.Err(err).Msg(repository.ErrResultNotFound.Error()) - case repository.ErrNoResultsDeleted: - s.logger.Err(err).Msg(repository.ErrNoResultsDeleted.Error()) - case repository.ErrMultipleResultsDeleted: - s.logger.Err(err).Msg(repository.ErrMultipleResultsDeleted.Error()) - default: - s.logger.Err(err).Msg(repository.ErrUnknown.Error()) - } - return ErrFailedToDeleteResult - } - - return err -} From 6e2039cc264c7333456bb19150367e08f9c9a9ff Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:12:12 -0500 Subject: [PATCH 08/11] feat: handler and route --- apps/api/internal/api/api.go | 1 + apps/api/internal/api/handlers/bat.go | 70 +++++++++++++++++++ apps/api/internal/api/handlers/bat_results.go | 0 apps/api/internal/api/handlers/handlers.go | 2 + 4 files changed, 73 insertions(+) create mode 100644 apps/api/internal/api/handlers/bat.go delete mode 100644 apps/api/internal/api/handlers/bat_results.go diff --git a/apps/api/internal/api/api.go b/apps/api/internal/api/api.go index 67612e28..973fb58e 100644 --- a/apps/api/internal/api/api.go +++ b/apps/api/internal/api/api.go @@ -150,6 +150,7 @@ func (api *API) setupRoutes(mw *mw.Middleware) { r.With(ensureEventAdmin).Post("/roles", api.Handlers.Event.AssignEventRole) r.With(ensureEventAdmin).Delete("/roles/{userId}", api.Handlers.Event.RevokeEventRole) r.With(ensureEventAdmin).Post("/roles/batch", api.Handlers.Event.BatchAssignEventRoles) + r.With(ensureEventAdmin).Get("/bat-runs", api.Handlers.Bat.GetRunsByEventId) // Superuser-only r.With(ensureSuperuser).Delete("/", api.Handlers.Event.DeleteEventById) diff --git a/apps/api/internal/api/handlers/bat.go b/apps/api/internal/api/handlers/bat.go new file mode 100644 index 00000000..458dc3f7 --- /dev/null +++ b/apps/api/internal/api/handlers/bat.go @@ -0,0 +1,70 @@ +package handlers + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/rs/zerolog" + res "github.com/swamphacks/core/apps/api/internal/api/response" + "github.com/swamphacks/core/apps/api/internal/services" +) + +type BatHandler struct { + BatService *services.BatService + logger zerolog.Logger +} + +func NewBatHandler(BatService *services.BatService, logger zerolog.Logger) *BatHandler { + return &BatHandler{ + BatService: BatService, + logger: logger.With().Str("handler", "BatRunHandler").Str("component", "event_interest").Logger(), + } +} + +// Get BatRuns +// +// @Summary Get BatRuns +// @Description Gets BatRuns. +// @Tags Bat +// @Accept json +// @Produce json +// @Success 200 {array} sqlc.GetBatRunsWithUserInfoRow "OK: BatRuns returned" +// @Router /BatRuns [get] +func (h *BatHandler) GetRunsByEventId(w http.ResponseWriter, r *http.Request) { + eventIdStr := chi.URLParam(r, "eventId") + if eventIdStr == "" { + res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) + return + } + eventId, err := uuid.Parse(eventIdStr) + + if err != nil { + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) + return + } + + runs, err := h.BatService.GetRunsByEventId(r.Context(), eventId) + if errors.Is(err, services.ErrMissingFields) { + res.SendError(w, http.StatusBadRequest, res.NewError("missing_fields", "Missing/malformed query. Available parameters: eventId")) + return + } + + if errors.Is(err, services.ErrMissingPerms) { + res.SendError(w, http.StatusForbidden, res.NewError("forbidden", "You are forbidden from this resource.")) + return + } + + if err != nil { + res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong encoding response")) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(runs); err != nil { + res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong encoding response")) + return + } +} diff --git a/apps/api/internal/api/handlers/bat_results.go b/apps/api/internal/api/handlers/bat_results.go deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/api/internal/api/handlers/handlers.go b/apps/api/internal/api/handlers/handlers.go index f40a842c..88c1e4b9 100644 --- a/apps/api/internal/api/handlers/handlers.go +++ b/apps/api/internal/api/handlers/handlers.go @@ -15,6 +15,7 @@ type Handlers struct { Application *ApplicationHandler Teams *TeamHandler Admission *AdmissionHandler + Bat *BatHandler } func NewHandlers( @@ -38,5 +39,6 @@ func NewHandlers( Application: NewApplicationHandler(appService), Teams: NewTeamHandler(teamService, logger), Admission: NewAdmissionHandler(batService), + Bat: NewBatHandler(batService, logger), } } From cc188b084551007175b3c51eba1f4c781abd5fbe Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:27:18 -0500 Subject: [PATCH 09/11] fix: add/update run logic --- apps/api/internal/services/bat.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/apps/api/internal/services/bat.go b/apps/api/internal/services/bat.go index d7786734..81968d96 100644 --- a/apps/api/internal/services/bat.go +++ b/apps/api/internal/services/bat.go @@ -21,9 +21,8 @@ import ( var ( ErrListApplicationsFailure = errors.New("Failed to retrieve applications") ErrMissingRatings = errors.New("Some applications are missing their review ratings") - ErrAddResultFailure = errors.New("Failed to add generated result") ErrRunConflict = errors.New("Run already exists for this event") - ErrFailedToCreateRun = errors.New("Failed to add run") + ErrFailedToAddRun = errors.New("Failed to add run") ErrFailedToDeleteRun = errors.New("Failed to delete run") ErrFailedToUpdateRun = errors.New("Failed to update run") ) @@ -154,7 +153,13 @@ type TeamAdmissionData struct { func (s *BatService) CalculateAdmissions(ctx context.Context, eventId uuid.UUID) error { s.logger.Info().Str("eventId", eventId.String()).Msg("") - // First aggregate data necessary + // Create run, adds state as "running" into db + newRun, err := s.AddRun(ctx, eventId) + if err != nil { + return ErrFailedToAddRun + } + + // Aggregate data necessary applications, err := s.appRepo.ListAdmissionCandidatesByEvent(ctx, eventId) if err != nil || len(applications) == 0 { return ErrListApplicationsFailure @@ -218,10 +223,20 @@ func (s *BatService) CalculateAdmissions(ctx context.Context, eventId uuid.UUID) for _, applicant := range rejected { rejectedUUIDs = append(rejectedUUIDs, applicant.ID) } - _, err = s.AddResult(ctx, eventId, acceptedUUIDs, rejectedUUIDs) + + params := sqlc.UpdateRunByIdParams{ + AcceptedApplicantsDoUpdate: true, + RejectedApplicantsDoUpdate: true, + StatusDoUpdate: true, + AcceptedApplicants: acceptedUUIDs, + RejectedApplicants: rejectedUUIDs, + Status: sqlc.NullBatRunStatus{BatRunStatus: sqlc.BatRunStatusCompleted, Valid: true}, + ID: newRun.ID, + } + _, err = s.UpdateRunById(ctx, params) if err != nil { - return ErrAddResultFailure + return ErrFailedToUpdateRun } s.logger.Info().Int("Accepted", len(accepted)+len(acceptedTeams)).Int("Rejected", len(rejected)).Msg("Finished Algo") From ae23ffad6b0e72ea644dc148cf2ea177340178c2 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:31:04 -0500 Subject: [PATCH 10/11] feat: delete route --- apps/api/internal/api/api.go | 1 + apps/api/internal/api/handlers/bat.go | 38 ++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/apps/api/internal/api/api.go b/apps/api/internal/api/api.go index 973fb58e..e8bf2d3f 100644 --- a/apps/api/internal/api/api.go +++ b/apps/api/internal/api/api.go @@ -151,6 +151,7 @@ func (api *API) setupRoutes(mw *mw.Middleware) { r.With(ensureEventAdmin).Delete("/roles/{userId}", api.Handlers.Event.RevokeEventRole) r.With(ensureEventAdmin).Post("/roles/batch", api.Handlers.Event.BatchAssignEventRoles) r.With(ensureEventAdmin).Get("/bat-runs", api.Handlers.Bat.GetRunsByEventId) + r.With(ensureEventAdmin).Delete("/bat-runs", api.Handlers.Bat.GetRunsByEventId) // Superuser-only r.With(ensureSuperuser).Delete("/", api.Handlers.Event.DeleteEventById) diff --git a/apps/api/internal/api/handlers/bat.go b/apps/api/internal/api/handlers/bat.go index 458dc3f7..9430d72f 100644 --- a/apps/api/internal/api/handlers/bat.go +++ b/apps/api/internal/api/handlers/bat.go @@ -32,7 +32,7 @@ func NewBatHandler(BatService *services.BatService, logger zerolog.Logger) *BatH // @Accept json // @Produce json // @Success 200 {array} sqlc.GetBatRunsWithUserInfoRow "OK: BatRuns returned" -// @Router /BatRuns [get] +// @Router /events/{eventId}/bat-runs [get] func (h *BatHandler) GetRunsByEventId(w http.ResponseWriter, r *http.Request) { eventIdStr := chi.URLParam(r, "eventId") if eventIdStr == "" { @@ -68,3 +68,39 @@ func (h *BatHandler) GetRunsByEventId(w http.ResponseWriter, r *http.Request) { return } } + +// Delete an event +// +// @Summary Delete an event +// @Description Delete an existing event +// @Tags Bat +// @Accept json +// @Produce json +// @Param eventId path string true "Run ID" Format(uuid) +// @Success 204 "OK - Run deleted" +// @Failure 500 {object} response.ErrorResponse "Server Error: Something went terribly wrong on our end." +// @Router /events/{eventId}/bat-runs [delete] +func (h *BatHandler) DeleteRunById(w http.ResponseWriter, r *http.Request) { + eventIdStr := chi.URLParam(r, "eventId") + if eventIdStr == "" { + res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!")) + return + } + eventId, err := uuid.Parse(eventIdStr) + if err != nil { + res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid UUID")) + return + } + err = h.BatService.DeleteRunById(r.Context(), eventId) + + if err != nil { + switch err { + case services.ErrFailedToDeleteRun: + res.SendError(w, http.StatusInternalServerError, res.NewError("delete_error", "Failed to delete event")) + default: + res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong")) + } + } + + w.WriteHeader(http.StatusNoContent) +} From a69e4a2f84fab0e5886ab6e50cdc41b7ecb2b2c0 Mon Sep 17 00:00:00 2001 From: h1divp <71522316+h1divp@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:29:24 -0500 Subject: [PATCH 11/11] chore/sqlc generate --- apps/api/internal/db/sqlc/applications.sql.go | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/apps/api/internal/db/sqlc/applications.sql.go b/apps/api/internal/db/sqlc/applications.sql.go index 19c00e80..e10ba4b7 100644 --- a/apps/api/internal/db/sqlc/applications.sql.go +++ b/apps/api/internal/db/sqlc/applications.sql.go @@ -110,6 +110,23 @@ func (q *Queries) GetApplicationByUserAndEventID(ctx context.Context, arg GetApp return i, err } +const joinWaitlist = `-- name: JoinWaitlist :exec +UPDATE applications +SET waitlist_join_time = COALESCE(waitlist_join_time, NOW()), + status = 'waitlisted' +WHERE user_id = $1 AND event_id = $2 +` + +type JoinWaitlistParams struct { + UserID uuid.UUID `json:"user_id"` + EventID uuid.UUID `json:"event_id"` +} + +func (q *Queries) JoinWaitlist(ctx context.Context, arg JoinWaitlistParams) error { + _, err := q.db.Exec(ctx, joinWaitlist, arg.UserID, arg.EventID) + return err +} + const listAdmissionCandidatesByEvent = `-- name: ListAdmissionCandidatesByEvent :many SELECT a.user_id, a.passion_rating, @@ -160,21 +177,6 @@ func (q *Queries) ListAdmissionCandidatesByEvent(ctx context.Context, eventID uu return nil, err } return items, nil -const joinWaitlist = `-- name: JoinWaitlist :exec -UPDATE applications -SET waitlist_join_time = COALESCE(waitlist_join_time, NOW()), - status = 'waitlisted' -WHERE user_id = $1 AND event_id = $2 -` - -type JoinWaitlistParams struct { - UserID uuid.UUID `json:"user_id"` - EventID uuid.UUID `json:"event_id"` -} - -func (q *Queries) JoinWaitlist(ctx context.Context, arg JoinWaitlistParams) error { - _, err := q.db.Exec(ctx, joinWaitlist, arg.UserID, arg.EventID) - return err } const listApplicationByReviewerAndEvent = `-- name: ListApplicationByReviewerAndEvent :many