Files
spsays-bff/internal/handler/broadcast.go
T
2026-08-29 18:34:07 +05:30

152 lines
4.9 KiB
Go

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