Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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
4 changes: 3 additions & 1 deletion 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,9 +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, cfg, logger)
apiHandlers := handlers.NewHandlers(authService, userService, eventInterestService, eventService, emailService, applicationService, teamService, batService, redeemablesService, cfg, logger)

api := api.NewAPI(&logger, apiHandlers, mw)

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 @@ -143,6 +143,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 @@ -204,6 +206,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
}

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

import (
"encoding/json"
"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/config"
"github.com/swamphacks/core/apps/api/internal/services"
)

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) {
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
}
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) {
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
}

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
}

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 201 {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) {
redeemableIdStr := chi.URLParam(r, "redeemableId")
if redeemableIdStr == "" {
res.SendError(w, http.StatusBadRequest, res.NewError("missing_redeemable_id", "The redeemable ID is missing from the URL!"))
return
}
redeemableId, err := uuid.Parse(redeemableIdStr)
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_redeemable_id", "The redeemable ID is not a valid UUID"))
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) {
redeemableIdStr := chi.URLParam(r, "redeemableId")
if redeemableIdStr == "" {
res.SendError(w, http.StatusBadRequest, res.NewError("missing_redeemable_id", "The redeemable ID is missing from the URL!"))
return
}
redeemableId, err := uuid.Parse(redeemableIdStr)
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_redeemable_id", "The redeemable ID is not a valid UUID"))
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
userIdStr := chi.URLParam(r, "userId")
if userIdStr == "" {
res.SendError(w, http.StatusBadRequest, res.NewError("missing_user_id", "The user ID is missing from the URL!"))
return
}
userId, err := uuid.Parse(userIdStr)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, use the web package

if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_user_id", "The user ID is not a valid UUID"))
return
}
redeemableIdStr := chi.URLParam(r, "redeemableId")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use the web.PathParamToUUID for this (and everything above too

if redeemableIdStr == "" {
res.SendError(w, http.StatusBadRequest, res.NewError("missing_redeemable_id", "The redeemable ID is missing from the URL!"))
return
}
redeemableId, err := uuid.Parse(redeemableIdStr)
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_redeemable_id", "The redeemable ID is not a valid UUID"))
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
userIdStr := chi.URLParam(r, "userId")
if userIdStr == "" {
res.SendError(w, http.StatusBadRequest, res.NewError("missing_user_id", "The user ID is missing from the URL!"))
return
}
userId, err := uuid.Parse(userIdStr)
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_user_id", "The user ID is not a valid UUID"))
return
}
redeemableIdStr := chi.URLParam(r, "redeemableId")
if redeemableIdStr == "" {
res.SendError(w, http.StatusBadRequest, res.NewError("missing_redeemable_id", "The redeemable ID is missing from the URL!"))
return
}
redeemableId, err := uuid.Parse(redeemableIdStr)
if err != nil {
res.SendError(w, http.StatusBadRequest, res.NewError("invalid_redeemable_id", "The redeemable ID is not a valid UUID"))
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