slack5-1770603014/services/preferences-api/internal/api/handlers/preferences.go
rdev-worker e5fc44d10e
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /implement-feature user-preferences
2026-02-09 02:37:38 +00:00

131 lines
3.9 KiB
Go

package handlers
import (
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"git.threesix.ai/jordan/slack5-1770603014/pkg/app"
"git.threesix.ai/jordan/slack5-1770603014/pkg/httperror"
"git.threesix.ai/jordan/slack5-1770603014/pkg/httpresponse"
"git.threesix.ai/jordan/slack5-1770603014/pkg/logging"
"git.threesix.ai/jordan/slack5-1770603014/services/preferences-api/internal/domain"
"git.threesix.ai/jordan/slack5-1770603014/services/preferences-api/internal/service"
)
// Preferences handles HTTP requests for user preferences.
type Preferences struct {
svc *service.PreferencesService
logger *logging.Logger
}
// NewPreferences creates a new Preferences handler with injected dependencies.
func NewPreferences(svc *service.PreferencesService, logger *logging.Logger) *Preferences {
return &Preferences{
svc: svc,
logger: logger.WithComponent("PreferencesHandler"),
}
}
// UpdatePreferencesRequest is the request body for updating preferences.
type UpdatePreferencesRequest struct {
Preferences PreferencesInput `json:"preferences" validate:"required"`
}
// PreferencesInput uses pointers to distinguish "not provided" from zero values.
type PreferencesInput struct {
Theme *string `json:"theme,omitempty"`
Language *string `json:"language,omitempty"`
Notifications *NotificationsInput `json:"notifications,omitempty"`
}
// NotificationsInput uses pointers for partial update semantics.
type NotificationsInput struct {
Email *bool `json:"email,omitempty"`
Push *bool `json:"push,omitempty"`
Digest *string `json:"digest,omitempty"`
}
// PreferencesResponse is the response for preferences endpoints.
type PreferencesResponse struct {
UserID string `json:"user_id"`
Preferences domain.Preferences `json:"preferences"`
UpdatedAt string `json:"updated_at"`
}
// toPreferencesResponse converts a domain UserPreferences to an API response.
func toPreferencesResponse(p *domain.UserPreferences) PreferencesResponse {
return PreferencesResponse{
UserID: p.UserID.String(),
Preferences: p.Preferences,
UpdatedAt: p.UpdatedAt.Format("2006-01-02T15:04:05Z"),
}
}
// Get returns preferences for a user.
func (h *Preferences) Get(w http.ResponseWriter, r *http.Request) error {
userID := chi.URLParam(r, "user_id")
if _, err := uuid.Parse(userID); err != nil {
return httperror.BadRequest("invalid user_id format")
}
prefs, err := h.svc.GetPreferences(r.Context(), domain.UserID(userID))
if err != nil {
return err
}
httpresponse.OK(w, r, toPreferencesResponse(prefs))
return nil
}
// Upsert creates or updates preferences for a user.
func (h *Preferences) Upsert(w http.ResponseWriter, r *http.Request) error {
userID := chi.URLParam(r, "user_id")
if _, err := uuid.Parse(userID); err != nil {
return httperror.BadRequest("invalid user_id format")
}
var req UpdatePreferencesRequest
if err := app.BindAndValidate(r, &req); err != nil {
return err
}
input := service.UpdateInput{
Theme: req.Preferences.Theme,
Language: req.Preferences.Language,
}
if req.Preferences.Notifications != nil {
input.Notifications = &service.NotificationsInput{
Email: req.Preferences.Notifications.Email,
Push: req.Preferences.Notifications.Push,
Digest: req.Preferences.Notifications.Digest,
}
}
prefs, err := h.svc.UpdatePreferences(r.Context(), domain.UserID(userID), input)
if err != nil {
return mapPreferencesDomainError(err)
}
httpresponse.OK(w, r, toPreferencesResponse(prefs))
return nil
}
// mapPreferencesDomainError converts domain errors to HTTP errors.
func mapPreferencesDomainError(err error) error {
switch {
case errors.Is(err, domain.ErrInvalidTheme):
return httperror.BadRequest(err.Error())
case errors.Is(err, domain.ErrInvalidLanguage):
return httperror.BadRequest(err.Error())
case errors.Is(err, domain.ErrInvalidDigest):
return httperror.BadRequest(err.Error())
default:
return err
}
}