package handler import ( "encoding/json" "errors" "net/http" "strconv" "strings" "time" "github.com/go-chi/chi/v5" "github.com/prabhupadasays/spsays-bff/internal/middleware" "github.com/prabhupadasays/spsays-bff/internal/repository" "github.com/prabhupadasays/spsays-bff/internal/service" "github.com/prabhupadasays/spsays-bff/pkg/response" ) // BroadcastHandler serves both sides of announcements: the admin routes that // compose and publish them, and the two app routes that fetch and dismiss. type BroadcastHandler struct { svc *service.BroadcastService } func NewBroadcastHandler(svc *service.BroadcastService) *BroadcastHandler { return &BroadcastHandler{svc: svc} } func writeBroadcastError(w http.ResponseWriter, err error) { switch { case errors.Is(err, repository.ErrBroadcastGone): response.Error(w, http.StatusNotFound, "broadcast not found") case errors.Is(err, service.ErrBroadcastTitle), errors.Is(err, service.ErrBroadcastBody), errors.Is(err, service.ErrBroadcastCTA), errors.Is(err, service.ErrBroadcastBanner): response.Error(w, http.StatusBadRequest, err.Error()) default: response.Error(w, http.StatusInternalServerError, "broadcast request failed") } } // ── App routes ──────────────────────────────────────────────────────────── // Pending — GET /broadcasts/pending. Returns null when nothing is waiting, so // the client can call it on load and after a nudge without special-casing. func (h *BroadcastHandler) Pending(w http.ResponseWriter, r *http.Request) { b, err := h.svc.Pending(r.Context(), middleware.UserID(r.Context())) if err != nil { writeBroadcastError(w, err) return } response.JSON(w, http.StatusOK, b) } // Seen — POST /broadcasts/{id}/seen. The "at most once" half; idempotent. func (h *BroadcastHandler) Seen(w http.ResponseWriter, r *http.Request) { if err := h.svc.MarkSeen(r.Context(), chi.URLParam(r, "id"), middleware.UserID(r.Context())); err != nil { writeBroadcastError(w, err) return } response.NoContent(w) } // ── Admin routes ────────────────────────────────────────────────────────── type createBroadcastReq struct { Title string `json:"title"` Body string `json:"body"` BannerKey *string `json:"banner_key"` CTALabel *string `json:"cta_label"` CTAURL *string `json:"cta_url"` // ExpiresAt stops the announcement being offered. Without one, someone // signing up months later would still be shown it. ExpiresAt *time.Time `json:"expires_at"` // Publish immediately rather than saving a draft. Publish bool `json:"publish"` } // Create — POST /admin/broadcasts func (h *BroadcastHandler) Create(w http.ResponseWriter, r *http.Request) { var req createBroadcastReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.Error(w, http.StatusBadRequest, "invalid JSON body") return } b, err := h.svc.Create(r.Context(), middleware.UserID(r.Context()), service.CreateParams{ Title: req.Title, Body: req.Body, BannerKey: req.BannerKey, CTALabel: req.CTALabel, CTAURL: req.CTAURL, ExpiresAt: req.ExpiresAt, }) if err != nil { writeBroadcastError(w, err) return } if req.Publish { published, err := h.svc.Publish(r.Context(), b.ID) if err != nil { writeBroadcastError(w, err) return } b = published } response.JSON(w, http.StatusCreated, b) } // Publish — POST /admin/broadcasts/{id}/publish func (h *BroadcastHandler) Publish(w http.ResponseWriter, r *http.Request) { b, err := h.svc.Publish(r.Context(), chi.URLParam(r, "id")) if err != nil { writeBroadcastError(w, err) return } response.JSON(w, http.StatusOK, b) } // List — GET /admin/broadcasts func (h *BroadcastHandler) List(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) items, err := h.svc.List(r.Context(), limit) if err != nil { writeBroadcastError(w, err) return } response.JSON(w, http.StatusOK, items) } // Delete — DELETE /admin/broadcasts/{id} func (h *BroadcastHandler) Delete(w http.ResponseWriter, r *http.Request) { if err := h.svc.Delete(r.Context(), chi.URLParam(r, "id")); err != nil { writeBroadcastError(w, err) return } response.NoContent(w) } // PresignBanner — POST /admin/broadcasts/banner-presign func (h *BroadcastHandler) PresignBanner(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, key, err := h.svc.PresignBanner(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, "key": key}) }