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
5 changes: 5 additions & 0 deletions apps/api/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func main() {
teamMemberRepo := repository.NewTeamMemberRespository(database)
teamJoinRequestRepo := repository.NewTeamJoinRequestRepository(database)
batRunsRepo := repository.NewBatRunsRepository(database)
redeemablesRepo := repository.NewRedeemablesRepository(database)

// Injections into services
authService := services.NewAuthService(userRepo, accountRepo, sessionRepo, txm, client, logger, &cfg.Auth)
Expand All @@ -92,6 +93,10 @@ func main() {
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, nil, logger)
redeemablesService := services.NewRedeemablesService(redeemablesRepo, logger)

// Injections into handlers
apiHandlers := handlers.NewHandlers(authService, userService, eventInterestService, eventService, emailService, applicationService, teamService, batService, redeemablesService, cfg, logger)
discordService := services.NewDiscordService(eventRepo, logger)

// Injections into handlers
Expand Down
21 changes: 21 additions & 0 deletions apps/api/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ func (api *API) setupRoutes(mw *mw.Middleware) {
r.With(ensureEventStaff).Post("/checkin", api.Handlers.Admission.HandleEventCheckIn)
// Used to fetch user info for checking in
r.With(ensureEventStaff).Get("/users/{userId}", api.Handlers.Event.GetUserForEvent)
// Get user ID by RFID
r.With(ensureEventStaff).Get("/users/by-rfid/{rfid}", api.Handlers.Event.GetUserByRFID)

// Admin-only
r.With(ensureEventAdmin).Post("/queue-confirmation-email", api.Handlers.Email.QueueConfirmationEmail)
Expand Down Expand Up @@ -211,6 +213,25 @@ func (api *API) setupRoutes(mw *mw.Middleware) {
r.Patch("/join-waitlist", api.Handlers.Application.JoinWaitlist)
})

r.Route("/redeemables", func(r chi.Router) {
r.Use(ensureEventStaff)
// Get all redeemables and create new redeemable
// eventId is available from parent route context
r.Get("/", api.Handlers.Redeemables.GetRedeemables)
r.Post("/", api.Handlers.Redeemables.CreateRedeemable)

// Update and delete specific redeemable
r.Route("/{redeemableId}", func(r chi.Router) {
r.Patch("/", api.Handlers.Redeemables.UpdateRedeemable)
r.Delete("/", api.Handlers.Redeemables.DeleteRedeemable)

r.Route("/users/{userId}", func(r chi.Router) {
r.Post("/", api.Handlers.Redeemables.RedeemRedeemable)
r.Patch("/", api.Handlers.Redeemables.UpdateRedemption)
})
})
})

// Team routes
r.Route("/teams", func(r chi.Router) {
r.Post("/", api.Handlers.Teams.CreateTeam)
Expand Down
40 changes: 40 additions & 0 deletions apps/api/internal/api/handlers/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -826,3 +826,43 @@ func (h *EventHandler) GetUserForEvent(w http.ResponseWriter, r *http.Request) {

res.Send(w, http.StatusOK, info)
}

// Get User by RFID
//
// @Summary Retrieves a user's ID by their RFID
// @Description Looks up a user's ID by their RFID code for a specific event. Returns the user ID which can be used for other operations.
// @Tags Event
// @Produce json
// @Param eventId path string true "Event ID" Format(uuid)
// @Param rfid path string true "RFID code (10 digits)"
// @Success 200 {object} map[string]string "OK - Returns user ID"
// @Failure 400 {object} response.ErrorResponse "Bad request/Malformed request."
// @Failure 404 {object} response.ErrorResponse "User not found with the provided RFID"
// @Failure 500 {object} response.ErrorResponse "Server Error: error getting user by RFID"
// @Router /events/{eventId}/users/by-rfid/{rfid} [get]
func (h *EventHandler) GetUserByRFID(w http.ResponseWriter, r *http.Request) {
eventId, err := web.PathParamToUUID(r, "eventId")
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("missing_event_id", "The event ID is missing from the URL!"))
return
}

rfid := chi.URLParam(r, "rfid")
if rfid == "" {
res.SendError(w, http.StatusBadRequest, res.NewError("missing_rfid", "The RFID is missing from the URL!"))
return
}

user, err := h.eventService.GetUserByRFID(r.Context(), eventId, rfid)
if err != nil {
if errors.Is(err, repository.ErrUserNotFound) || errors.Is(err, repository.ErrEventRoleNotFound) {
res.SendError(w, http.StatusNotFound, res.NewError("user_not_found", "No user found with the provided RFID"))
} else {
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_err", "Something went wrong while querying user by RFID"))
}
return
}

// Return just the user ID as a simple JSON object
res.Send(w, http.StatusOK, map[string]string{"user_id": user.ID.String()})
}
3 changes: 3 additions & 0 deletions apps/api/internal/api/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Handlers struct {
Teams *TeamHandler
Admission *AdmissionHandler
Bat *BatHandler
Redeemables *RedeemablesHandler
Discord *DiscordHandler
}

Expand All @@ -28,6 +29,7 @@ func NewHandlers(
appService *services.ApplicationService,
teamService *services.TeamService,
batService *services.BatService,
redeemablesService *services.RedeemablesService,
discordService *services.DiscordService,
cfg *config.Config,
logger zerolog.Logger,
Expand All @@ -42,6 +44,7 @@ func NewHandlers(
Teams: NewTeamHandler(teamService, logger),
Admission: NewAdmissionHandler(batService, logger),
Bat: NewBatHandler(batService, logger),
Redeemables: NewRedeemablesHandler(redeemablesService, cfg, logger),
Discord: NewDiscordHandler(discordService, logger),
}
}
257 changes: 257 additions & 0 deletions apps/api/internal/api/handlers/redeemables.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
package handlers

import (
"encoding/json"
"net/http"

"github.com/rs/zerolog"
res "github.com/swamphacks/core/apps/api/internal/api/response"
"github.com/swamphacks/core/apps/api/internal/config"
"github.com/swamphacks/core/apps/api/internal/services"
"github.com/swamphacks/core/apps/api/internal/web"
)

type RedeemablesHandler struct {
redeemablesService *services.RedeemablesService
cfg *config.Config
logger zerolog.Logger
}

func NewRedeemablesHandler(
redeemablesService *services.RedeemablesService,
cfg *config.Config,
logger zerolog.Logger,
) *RedeemablesHandler {
return &RedeemablesHandler{
redeemablesService: redeemablesService,
cfg: cfg,
logger: logger,
}
}

// GetRedeemables
//
// @Summary Get all redeemables for an event
// @Description Retrieve a list of all redeemable items associated with a specific event ID.
// @Tags Redeemables
// @Accept json
// @Produce json
// @Param eventId path string true "Event ID (UUID)"
// @Success 200 {array} sqlc.Redeemable
// @Failure 400 {object} response.ErrorResponse "Missing or invalid Event ID"
// @Failure 500 {object} response.ErrorResponse "Internal Server Error"
// @Router /events/{eventId}/redeemables [get]
func (h *RedeemablesHandler) GetRedeemables(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 or missing event_id."),
)
return
}

redeemables, err := h.redeemablesService.GetRedeemablesByEventID(r.Context(), eventId)
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_server_error", "internal service error, failed to get redeemables by ID"))
return
}

res.Send(w, http.StatusOK, redeemables)
}

type CreateRedeemableRequest struct {
Name string `json:"name"`
Amount int `json:"amount"`
MaxUserAmount int `json:"max_user_amount"`
}

// CreateRedeemable
//
// @Summary Create a new redeemable
// @Description Create a new redeemable item for a specific event.
// @Tags Redeemables
// @Accept json
// @Produce json
// @Param eventId path string true "Event ID (UUID)"
// @Param request body CreateRedeemableRequest true "Redeemable creation data"
// @Success 201 {object} sqlc.Redeemable
// @Failure 400 {object} response.ErrorResponse "Invalid request body or ID"
// @Failure 500 {object} response.ErrorResponse "Internal Server Error"
// @Router /events/{eventId}/redeemables [post]
func (h *RedeemablesHandler) CreateRedeemable(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 or missing event_id."),
)
return
}

var req CreateRedeemableRequest
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // Prevents requests with extraneous fields
if err := decoder.Decode(&req); err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request_body", "The request body is invalid: "+err.Error()))
return
}

if req.Name == "" || req.Amount <= 0 {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_params", "name and amount are required"))
return
}

redeemable, err := h.redeemablesService.CreateRedeemable(r.Context(), eventId, req.Name, req.Amount, req.MaxUserAmount)
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_server_error", "internal service error, failed to create redeemable"))
return
}

res.Send(w, http.StatusCreated, redeemable)
}

type UpdateRedeemableRequest struct {
Name *string `json:"name,omitempty"`
Amount *int `json:"total_stock,omitempty"`
MaxUserAmount *int `json:"max_user_amount,omitempty"`
}

// UpdateRedeemable
//
// @Summary Update an existing redeemable
// @Description Update specific fields (name, stock, max per user) of a redeemable.
// @Tags Redeemables
// @Accept json
// @Produce json
// @Param redeemableId path string true "Redeemable ID (UUID)"
// @Param request body UpdateRedeemableRequest true "Redeemable update data (partial fields allowed)"
// @Success 200 {object} sqlc.Redeemable
// @Failure 400 {object} response.ErrorResponse "Invalid ID or request body"
// @Failure 500 {object} response.ErrorResponse "Internal Server Error"
// @Router /redeemables/{redeemableId} [patch]
func (h *RedeemablesHandler) UpdateRedeemable(w http.ResponseWriter, r *http.Request) {
redeemableId, err := web.PathParamToUUID(r, "redeemableId")
if err != nil {
res.SendError(w, http.StatusBadRequest,
res.NewError("invalid_request", "Invalid or missing redeemable_id."),
)
return
}

var req UpdateRedeemableRequest
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // Prevents requests with extraneous fields
if err := decoder.Decode(&req); err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request_body", "The request body is invalid: "+err.Error()))
return
}

redeemable, err := h.redeemablesService.UpdateRedeemable(r.Context(), redeemableId, req.Name, req.Amount, req.MaxUserAmount)
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_server_error", "internal service error, failed to update redeemable"))
return
}

res.Send(w, http.StatusCreated, redeemable)
return
}

// DeleteRedeemable
//
// @Summary Delete a redeemable
// @Description Permanently delete a redeemable item by ID.
// @Tags Redeemables
// @Param redeemableId path string true "Redeemable ID (UUID)"
// @Success 204 "No Content"
// @Failure 400 {object} response.ErrorResponse "Invalid Redeemable ID"
// @Failure 500 {object} response.ErrorResponse "Internal Server Error"
// @Router /redeemables/{redeemableId} [delete]
func (h *RedeemablesHandler) DeleteRedeemable(w http.ResponseWriter, r *http.Request) {
redeemableId, err := web.PathParamToUUID(r, "redeemableId")
if err != nil {
res.SendError(w, http.StatusBadRequest,
res.NewError("invalid_request", "Invalid or missing redeemable_id."),
)
return
}

err = h.redeemablesService.DeleteRedeemable(r.Context(), redeemableId)
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_server_error", "internal service error, failed to delete redeemable"))
return
}
res.Send(w, http.StatusNoContent, nil)
}

// RedeemRedeemable
//
// @Summary Redeem an item for a user
// @Description Create a redemption record linking a specific user to a redeemable item.
// @Tags Redeemables
// @Param redeemableId path string true "Redeemable ID (UUID)"
// @Param userId path string true "User ID (UUID)"
// @Success 204 "No Content"
// @Failure 400 {object} response.ErrorResponse "Invalid IDs"
// @Failure 500 {object} response.ErrorResponse "Internal Server Error"
// @Router /redeemables/{redeemableId}/users/{userId} [post]
func (h *RedeemablesHandler) RedeemRedeemable(w http.ResponseWriter, r *http.Request) {
// user id, redeemable id
userId, err := web.PathParamToUUID(r, "userId")
if err != nil {
res.SendError(w, http.StatusBadRequest,
res.NewError("invalid_request", "Invalid or missing user_id."),
)
return
}

redeemableId, err := web.PathParamToUUID(r, "redeemableId")
if err != nil {
res.SendError(w, http.StatusBadRequest,
res.NewError("invalid_request", "Invalid or missing redeemable_id."),
)
return
}

err = h.redeemablesService.RedeemRedeemable(r.Context(), redeemableId, userId)
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_server_error", "internal service error, failed to redeem redeemable"))
return
}
res.Send(w, http.StatusNoContent, nil)
}

type UpdateRedemptionRequest struct {
Amount int `json:"new_amount,omitempty"`
}

func (h *RedeemablesHandler) UpdateRedemption(w http.ResponseWriter, r *http.Request) {
// user id, redeemable id
userId, err := web.PathParamToUUID(r, "userId")
if err != nil {
res.SendError(w, http.StatusBadRequest,
res.NewError("invalid_request", "Invalid or missing user_id."),
)
return
}

redeemableId, err := web.PathParamToUUID(r, "redeemableId")
if err != nil {
res.SendError(w, http.StatusBadRequest,
res.NewError("invalid_request", "Invalid or missing redeemable_id."),
)
return
}
var req UpdateRedemptionRequest
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&req); err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_request_body", "The request body is invalid: "+err.Error()))
return
}

err = h.redeemablesService.UpdateRedemption(r.Context(), redeemableId, userId, req.Amount)
if err != nil {
res.SendError(w, http.StatusInternalServerError, res.NewError("internal_server_error", "internal service error, failed to update redemption"))
return
}
res.Send(w, http.StatusNoContent, nil)
}
Loading