233 lines
7.7 KiB
Go
233 lines
7.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/prabhupadasays/spsays-bff/internal/middleware"
|
|
"github.com/prabhupadasays/spsays-bff/internal/model"
|
|
"github.com/prabhupadasays/spsays-bff/internal/repository"
|
|
"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.
|
|
// OpenGift — POST /sadhna/rewards/gifts/{key}/open
|
|
//
|
|
// Called after the celebration is dismissed. Idempotent: opening an already
|
|
// opened gift changes nothing.
|
|
func (h *SadhnaHandler) OpenGift(w http.ResponseWriter, r *http.Request) {
|
|
if err := h.svc.OpenGift(r.Context(),
|
|
middleware.UserID(r.Context()), chi.URLParam(r, "key")); err != nil {
|
|
response.Error(w, http.StatusInternalServerError, "could not open that gift")
|
|
return
|
|
}
|
|
response.NoContent(w)
|
|
}
|
|
|
|
// GiftableRewards — GET /admin/reward-gifts/catalog
|
|
func (h *SadhnaHandler) GiftableRewards(w http.ResponseWriter, r *http.Request) {
|
|
list, err := h.svc.GiftableRewards(r.Context())
|
|
if err != nil {
|
|
response.Error(w, http.StatusInternalServerError, "could not load the catalog")
|
|
return
|
|
}
|
|
response.JSON(w, http.StatusOK, list)
|
|
}
|
|
|
|
// DeleteCustomGift — DELETE /admin/reward-gifts/{key}
|
|
func (h *SadhnaHandler) DeleteCustomGift(w http.ResponseWriter, r *http.Request) {
|
|
if err := h.svc.DeleteCustomGift(r.Context(), chi.URLParam(r, "key")); err != nil {
|
|
if errors.Is(err, repository.ErrRewardNotFound) {
|
|
response.Error(w, http.StatusNotFound, "no such custom gift")
|
|
return
|
|
}
|
|
response.Error(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
response.NoContent(w)
|
|
}
|
|
|
|
type customGiftReq struct {
|
|
Name string `json:"name"`
|
|
Meaning string `json:"meaning"`
|
|
ImageURL string `json:"image_url"`
|
|
}
|
|
|
|
// CreateCustomGift — POST /admin/reward-gifts/catalog
|
|
//
|
|
// Adds a sticker to the catalog. It reaches nobody until it is sent.
|
|
func (h *SadhnaHandler) CreateCustomGift(w http.ResponseWriter, r *http.Request) {
|
|
var req customGiftReq
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
response.Error(w, http.StatusBadRequest, "invalid JSON body")
|
|
return
|
|
}
|
|
reward, err := h.svc.CreateCustomGift(r.Context(), req.Name, req.Meaning, req.ImageURL)
|
|
if err != nil {
|
|
response.Error(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
response.JSON(w, http.StatusCreated, reward)
|
|
}
|
|
|
|
// PresignGiftImage — POST /admin/reward-gifts/image-presign
|
|
func (h *SadhnaHandler) PresignGiftImage(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
ContentType string `json:"content_type"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
response.Error(w, http.StatusBadRequest, "invalid JSON body")
|
|
return
|
|
}
|
|
upload, public, err := h.svc.PresignGiftImage(r.Context(), strings.TrimSpace(req.ContentType))
|
|
if err != nil {
|
|
response.Error(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
response.JSON(w, http.StatusOK, map[string]string{
|
|
"upload_url": upload, "image_url": public,
|
|
})
|
|
}
|
|
|
|
type giftReq struct {
|
|
RewardKey string `json:"reward_key"`
|
|
}
|
|
|
|
// GiftToEveryone — POST /admin/reward-gifts
|
|
//
|
|
// Hands one reward to every account, unopened. Idempotent, so a repeated
|
|
// click tops up only the people who don't have it rather than duplicating.
|
|
func (h *SadhnaHandler) GiftToEveryone(w http.ResponseWriter, r *http.Request) {
|
|
var req giftReq
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.RewardKey == "" {
|
|
response.Error(w, http.StatusBadRequest, "reward_key is required")
|
|
return
|
|
}
|
|
n, err := h.svc.GiftToEveryone(r.Context(), req.RewardKey)
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrRewardNotFound) {
|
|
response.Error(w, http.StatusNotFound, "no such reward")
|
|
return
|
|
}
|
|
response.Error(w, http.StatusInternalServerError, "could not send the gift")
|
|
return
|
|
}
|
|
response.JSON(w, http.StatusOK, map[string]int64{"granted": n})
|
|
}
|
|
|
|
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)
|
|
}
|