Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions apps/api/cmd/BAT_worker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ func main() {
},
)

schedulerLocation, err := time.LoadLocation("America/New_York")
if err != nil {
panic(err)
}
scheduler := asynq.NewScheduler(
redisOpt,
&asynq.SchedulerOpts{
Location: schedulerLocation,
},
)

taskQueueClient := asynq.NewClient(redisOpt)

database := db.NewDB(cfg.DatabaseURL)
defer database.Close()

Expand All @@ -64,16 +77,20 @@ func main() {
applicationRepo := repository.NewApplicationRepository(database)
eventRepo := repository.NewEventRespository(database)
userRepo := repository.NewUserRepository(database)
eventService := services.NewEventService(eventRepo, userRepo, nil, nil, logger)
batRunsRepo := repository.NewBatRunsRepository(database)

sesClient := email.NewSESClient(cfg.AWS.AccessKey, cfg.AWS.AccessKeySecret, cfg.AWS.Region, logger)
emailService := services.NewEmailService(nil, sesClient, logger)
emailService := services.NewEmailService(taskQueueClient, sesClient, logger)
batService := services.NewBatService(applicationRepo, eventRepo, userRepo, batRunsRepo, emailService, txm, nil, logger)
applicationService := services.NewApplicationService(applicationRepo, userRepo, eventService, emailService, txm, nil, nil, scheduler, logger)

BATWorker := workers.NewBATWorker(batService, logger)
BATWorker := workers.NewBATWorker(batService, applicationService, scheduler, logger)

mux := asynq.NewServeMux()
mux.HandleFunc(tasks.TypeCalculateAdmissions, BATWorker.HandleCalculateAdmissionsTask)
mux.HandleFunc(tasks.TypeTransitionWaitlist, BATWorker.HandleTransitionWaitlistTask)
mux.HandleFunc(tasks.TypeScheduleTransitionWaitlist, BATWorker.HandleScheduleTransitionWaitlistTask)

if err := srv.Run(mux); err != nil {
logger.Fatal().Msg("Failed to run BAT worker")
Expand Down
4 changes: 2 additions & 2 deletions apps/api/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ func main() {
eventInterestService := services.NewEventInterestService(eventInterestRepo, logger)
eventService := services.NewEventService(eventRepo, userRepo, r2Client, &cfg.CoreBuckets, logger)
emailService := services.NewEmailService(taskQueueClient, sesClient, logger)
applicationService := services.NewApplicationService(applicationRepo, eventService, emailService, txm, r2Client, &cfg.CoreBuckets, logger)
applicationService := services.NewApplicationService(applicationRepo, userRepo, eventService, emailService, txm, r2Client, &cfg.CoreBuckets, nil, logger)
teamService := services.NewTeamService(teamRepo, teamMemberRepo, teamJoinRequestRepo, eventRepo, txm, logger)
batService := services.NewBatService(applicationRepo, eventRepo, userRepo, batRunsRepo, emailService, txm, taskQueueClient, logger)
batService := services.NewBatService(applicationRepo, eventRepo, userRepo, batRunsRepo, emailService, txm, taskQueueClient, nil, logger)

// Injections into handlers
apiHandlers := handlers.NewHandlers(authService, userService, eventInterestService, eventService, emailService, applicationService, teamService, batService, cfg, logger)
Expand Down
1 change: 0 additions & 1 deletion apps/api/cmd/email_worker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ func main() {

mux := asynq.NewServeMux()

mux.HandleFunc(tasks.TypeSendConfirmationEmail, emailWorker.HandleSendConfirmationEmailTask)
mux.HandleFunc(tasks.TypeSendHtmlEmail, emailWorker.HandleSendHtmlEmailTask)

wd, err := os.Getwd()
Expand Down
8 changes: 5 additions & 3 deletions apps/api/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,6 @@ func (api *API) setupRoutes(mw *mw.Middleware) {

// --- Event routes ---
api.Router.Route("/events", func(r chi.Router) {
r.Post("/{eventId}/calc-admissions", api.Handlers.Admission.HandleCalculateAdmissionsRequest)
r.Post("/{eventId}/reviews/bat-runs/{runId}/release", api.Handlers.Admission.ReleaseDecisions)

// Superuser-only
r.With(mw.Auth.RequireAuth, ensureSuperuser).Post("/", api.Handlers.Event.CreateEvent)

Expand All @@ -143,6 +140,11 @@ func (api *API) setupRoutes(mw *mw.Middleware) {
r.With(ensureEventStaff).Get("/overview", api.Handlers.Event.GetEventOverview)

// Admin-only
r.With(ensureEventAdmin).Post("/queue-confirmation-email", api.Handlers.Email.QueueConfirmationEmail)
r.With(ensureEventAdmin).Post("/calc-admissions", api.Handlers.Admission.HandleCalculateAdmissionsRequest)
r.With(ensureEventAdmin).Patch("/transition-waitlisted-applications", api.Handlers.Application.TransitionWaitlistedApplications)
r.With(ensureEventAdmin).Post("/queue-transition-waitlist-task", api.Handlers.Bat.QueueScheduleWaitlistTransitionTask)
r.With(ensureEventAdmin).Post("/reviews/bat-runs/{runId}/release", api.Handlers.Admission.ReleaseDecisions)
r.With(ensureEventAdmin).Patch("/", api.Handlers.Event.UpdateEventById)
r.With(ensureEventAdmin).Post("/banner", api.Handlers.Event.UploadEventBanner)
r.With(ensureEventAdmin).Delete("/banner", api.Handlers.Event.DeleteBanner)
Expand Down
31 changes: 30 additions & 1 deletion apps/api/internal/api/handlers/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ func (h *ApplicationHandler) WithdrawAcceptance(w http.ResponseWriter, r *http.R
// @Tags Application Event
//
// @Param eventId path string true "ID of the event to join the waitlist for"
// @Success 200 "Acceptance withdrawn joined successfully"
// @Success 200 "Acceptance successful"
// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID"
// @Failure 500 {object} res.ErrorResponse "Server error: failed to accept"
// @Router /events/{eventId}/application/accept-acceptance [patch]
Expand All @@ -672,3 +672,32 @@ func (h *ApplicationHandler) AcceptApplicationAcceptance(w http.ResponseWriter,

w.WriteHeader(http.StatusOK)
}

// Transition waitlisted applications
//
// @Summary Sets application status from accepted to rejected
// @Description Transitions all accepted users to waitlist, and accepts 50 from the waitlist.
// @Tags Application Event
//
// @Param eventId path string true "ID of the event to join the waitlist for"
// @Success 200 "Transitioned application statuses successfully"
// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID"
// @Failure 500 {object} res.ErrorResponse "Server error: failed to transition application statuses"
// @Router /events/{eventId}/application/transition-waitlisted-applications [patch]
func (h *ApplicationHandler) TransitionWaitlistedApplications(w http.ResponseWriter, r *http.Request) {
eventId, err := web.PathParamToUUID(r, "eventId")
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not a valid."))
return
}

var acceptanceCount uint32 = 50
var acceptanceQuota uint32 = 500
err = h.appService.TransitionWaitlistedApplications(r.Context(), eventId, acceptanceCount, acceptanceQuota)
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("transition-waitlisted-applications-error", "Something went wrong while transitioning waitlisted applications."))
return
}

w.WriteHeader(http.StatusOK)
}
33 changes: 30 additions & 3 deletions apps/api/internal/api/handlers/bat.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/rs/zerolog"
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 BatHandler struct {
Expand Down Expand Up @@ -116,10 +117,10 @@ func (h *BatHandler) CheckApplicationReviewsComplete(w http.ResponseWriter, r *h
}
}

// Delete an event
// Delete a run
//
// @Summary Delete an event
// @Description Delete an existing event
// @Summary Delete a run
// @Description Delete an existing BAT run
// @Tags Bat
// @Accept json
// @Produce json
Expand Down Expand Up @@ -151,3 +152,29 @@ func (h *BatHandler) DeleteRunById(w http.ResponseWriter, r *http.Request) {

w.WriteHeader(http.StatusNoContent)
}

// Queue transition waitlist task
//
// @Summary Queues a waitlist transition task
// @Description Queues an asynq task that transitions waitlisted applications, running every 3 days.
// @Tags
//
// @Param eventId path string true "ID of the event to join the waitlist for"
// @Success 200 "Transitioned application statuses successfully"
// @Failure 400 {object} res.ErrorResponse "Bad request: invalid event ID"
// @Failure 500 {object} res.ErrorResponse "Server error: failed to transition application statuses"
// @Router /events/{eventId}/queue-transition-waitlist-task [post]
func (h *BatHandler) QueueScheduleWaitlistTransitionTask(w http.ResponseWriter, r *http.Request) {
eventId, err := web.PathParamToUUID(r, "eventId")
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_event_id", "The event ID is not valid."))
return
}

err = h.BatService.QueueScheduleWaitlistTransitionTask(r.Context(), eventId)
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Failed to create ScheduleWaitlistTransition task."))
}

res.Send(w, http.StatusCreated, nil)
}
32 changes: 28 additions & 4 deletions apps/api/internal/api/handlers/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"

"github.com/go-playground/validator/v10"
"github.com/rs/zerolog"
res "github.com/swamphacks/core/apps/api/internal/api/response"
"github.com/swamphacks/core/apps/api/internal/email"
Expand All @@ -27,10 +28,6 @@ type QueueTextEmailRequest struct {
Subject string `json:"subject"`
Body string `json:"body"`
}
type QueueConfirmationEmailRequest struct {
To string `json:"to"`
Name string `json:"name"`
}

// Queue an Email Request
//
Expand Down Expand Up @@ -79,3 +76,30 @@ func (h *EmailHandler) QueueTextEmail(w http.ResponseWriter, r *http.Request) {

w.WriteHeader(http.StatusCreated)
}

type QueueConfirmationEmailFields struct {
Email string `json:"email" validate:"required"`
FirstName string `json:"firstName" validate:"required"`
}

func (h *EmailHandler) QueueConfirmationEmail(w http.ResponseWriter, r *http.Request) {
var req QueueConfirmationEmailFields
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
err := decoder.Decode(&req)
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", "Could not parse request body"))
return
}
validate := validator.New()
if err := validate.Struct(req); err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request", err.Error()))
}

err = h.emailService.QueueConfirmationEmail(req.Email, req.FirstName)
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Confirmation email could not be queued."))
}

res.Send(w, http.StatusOK, nil)
}
15 changes: 9 additions & 6 deletions apps/api/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type CloudflareConfig struct {
AccessKeySecret string `env:"ACCESS_KEY_SECRET"`
}

// TODO: deprecate.
type SmtpConfig struct {
Username string `env:"USERNAME"`
Password string `env:"PASSWORD"`
Expand All @@ -54,12 +55,14 @@ type CoreBuckets struct {
}

type Config struct {
DatabaseURL string `env:"DATABASE_URL"`
RedisURL string `env:"REDIS_URL"`
Port string `env:"PORT" envDefault:"8080"`
AllowedOriginsString string `env:"ALLOWED_ORIGINS"`
EmailTemplateDirectory string `env:"EMAIL_TEMPLATE_DIRECTORY"`
AllowedOrigins []string ``
DatabaseURL string `env:"DATABASE_URL"`
RedisURL string `env:"REDIS_URL"`
Port string `env:"PORT" envDefault:"8080"`
AllowedOriginsString string `env:"ALLOWED_ORIGINS"`
EmailTemplateDirectory string `env:"EMAIL_TEMPLATE_DIRECTORY"`
AllowedOrigins []string ``
ApiOrigin string `env:"API_ORIGIN"`
WaitlistWorkerSessionIdCookie string `env:WAITLIST_WORKER_SESSION_ID_COOKIE`

Auth AuthConfig `envPrefix:"AUTH_"`
Cookie CookieConfig `envPrefix:"COOKIE_"`
Expand Down
25 changes: 25 additions & 0 deletions apps/api/internal/db/queries/applications.sql
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,28 @@ UPDATE applications
SET status = @status::application_status
WHERE event_id = @event_id::uuid
AND user_id = ANY(@user_ids::uuid[]);

-- name: TransitionAcceptedApplicationsToWaitlistByEventID :exec
UPDATE applications
SET waitlist_join_time = COALESCE(waitlist_join_time, NOW()),
status = 'waitlisted'
WHERE event_id = @event_id::uuid
AND status = 'accepted';

-- name: TransitionWaitlistedApplicationsToAcceptedByEventID :many
UPDATE applications
SET waitlist_join_time = NULL,
status = 'accepted'
WHERE user_id IN (
SELECT user_id FROM applications
WHERE event_id = @event_id::uuid
AND status = 'waitlisted'
ORDER BY waitlist_join_time ASC
LIMIT @acceptanceCount::int
)
RETURNING user_id;

-- name: GetTotalAcceptedApplicationsByEventId :one
SELECT COUNT(*) FROM applications
WHERE event_id = @event_id::uuid
AND status = 'accepted';
16 changes: 16 additions & 0 deletions apps/api/internal/db/repository/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,19 @@ func (r *ApplicationRepository) JoinWaitlist(ctx context.Context, userId, eventI
EventID: eventId,
})
}

func (r *ApplicationRepository) TransitionAcceptedApplicationsToWaitlistByEventID(ctx context.Context, eventId uuid.UUID) error {
return r.db.Query.TransitionAcceptedApplicationsToWaitlistByEventID(ctx, eventId)
}

func (r *ApplicationRepository) TransitionWaitlistedApplicationsToAcceptedByEventID(ctx context.Context, eventId uuid.UUID, acceptanceCount uint32) ([]uuid.UUID, error) {
return r.db.Query.TransitionWaitlistedApplicationsToAcceptedByEventID(ctx, sqlc.TransitionWaitlistedApplicationsToAcceptedByEventIDParams{
EventID: eventId,
Acceptancecount: int32(acceptanceCount),
})
}

func (r *ApplicationRepository) GetTotalAcceptedApplicationsByEventId(ctx context.Context, eventId uuid.UUID) (uint32, error) {
amount, err := r.db.Query.GetTotalAcceptedApplicationsByEventId(ctx, eventId)
return uint32(amount), err
}
65 changes: 65 additions & 0 deletions apps/api/internal/db/sqlc/applications.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading