Files
spsays-bff/internal/handler/sadhna.go
T

125 lines
4.2 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"time"
"github.com/prabhupadasays/spsays-bff/internal/middleware"
"github.com/prabhupadasays/spsays-bff/internal/model"
"github.com/prabhupadasays/spsays-bff/internal/service"
"github.com/prabhupadasays/spsays-bff/pkg/response"
)
const sadhnaDayFmt = "2006-01-02"
// SadhnaHandler serves the sādhana-streak dashboard and records the day's
// chanting progress. Both endpoints are auth-required; the user is read
// from the request context populated by middleware.RequireAuth.
type SadhnaHandler struct {
svc *service.SadhnaService
}
func NewSadhnaHandler(svc *service.SadhnaService) *SadhnaHandler {
return &SadhnaHandler{svc: svc}
}
// Dashboard — GET /sadhna/dashboard. Returns today's progress, the 7-day
// strip, streak counters, and plant state.
func (h *SadhnaHandler) Dashboard(w http.ResponseWriter, r *http.Request) {
uid := middleware.UserID(r.Context())
dash, err := h.svc.Dashboard(r.Context(), uid)
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not load sadhna dashboard")
return
}
response.JSON(w, http.StatusOK, dash)
}
type saveChantReq struct {
RoundsCompleted int `json:"rounds_completed"`
}
// SaveChant — POST /sadhna/chant. Persists today's round count and returns
// the recomputed dashboard so the client updates the strip + plant in one
// round-trip.
func (h *SadhnaHandler) SaveChant(w http.ResponseWriter, r *http.Request) {
uid := middleware.UserID(r.Context())
var req saveChantReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
response.Error(w, http.StatusBadRequest, "invalid JSON body")
return
}
dash, err := h.svc.SaveChant(r.Context(), uid, req.RoundsCompleted)
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not save chanting progress")
return
}
response.JSON(w, http.StatusOK, dash)
}
// History — GET /sadhna/history?from=YYYY-MM-DD&to=YYYY-MM-DD. Returns the
// per-day sādhana state for the inclusive UTC date range, which the
// calendar uses to render any week or month. The range is bounded to a
// year so a malformed/huge request can't scan the whole table.
func (h *SadhnaHandler) History(w http.ResponseWriter, r *http.Request) {
uid := middleware.UserID(r.Context())
from, err := time.ParseInLocation(sadhnaDayFmt, r.URL.Query().Get("from"), time.UTC)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid 'from' date (want YYYY-MM-DD)")
return
}
to, err := time.ParseInLocation(sadhnaDayFmt, r.URL.Query().Get("to"), time.UTC)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid 'to' date (want YYYY-MM-DD)")
return
}
if to.Before(from) {
response.Error(w, http.StatusBadRequest, "'to' must not be before 'from'")
return
}
if to.Sub(from) > 366*24*time.Hour {
response.Error(w, http.StatusBadRequest, "range too large (max 1 year)")
return
}
days, err := h.svc.History(r.Context(), uid, from, to)
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not load sadhna history")
return
}
response.JSON(w, http.StatusOK, days)
}
type rewardsResp struct {
Collected []model.SadhnaRewardGrant `json:"collected"`
Total int `json:"total"`
}
// Rewards — GET /sadhna/rewards. The user's collected gifts (newest first)
// plus the size of the giftable catalog, for the rewards gallery.
func (h *SadhnaHandler) Rewards(w http.ResponseWriter, r *http.Request) {
uid := middleware.UserID(r.Context())
grants, total, err := h.svc.Rewards(r.Context(), uid)
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not load rewards")
return
}
if grants == nil {
grants = []model.SadhnaRewardGrant{}
}
response.JSON(w, http.StatusOK, rewardsResp{Collected: grants, Total: total})
}
// AckRewards — POST /sadhna/rewards/ack. Clears pending celebrations once the
// client has shown the confetti, so a gift surfaces exactly once.
func (h *SadhnaHandler) AckRewards(w http.ResponseWriter, r *http.Request) {
uid := middleware.UserID(r.Context())
if err := h.svc.AckRewards(r.Context(), uid); err != nil {
response.Error(w, http.StatusInternalServerError, "could not acknowledge rewards")
return
}
response.NoContent(w)
}