feat: add stickers delete api

This commit is contained in:
Krispire Admin
2026-08-30 10:31:37 +05:30
parent 4427849647
commit a2d22ed829
5 changed files with 111 additions and 0 deletions
+13
View File
@@ -141,6 +141,19 @@ func (h *SadhnaHandler) GiftableRewards(w http.ResponseWriter, r *http.Request)
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"`
+23
View File
@@ -206,6 +206,29 @@ func (r *RewardRepo) CreateCustom(ctx context.Context, key, name, meaning, image
return &w, nil
}
// DeleteCustom removes an admin-made reward from the catalog.
//
// Both sadhna_reward_grants.reward_id and saved_stickers.reward_id are
// ON DELETE CASCADE, so this also takes it out of every devotee's gallery and
// sticker vault. Stickers already SENT in a chat are unaffected: those copy
// the artwork onto the message rather than referencing the catalog, so history
// stays intact.
//
// Restricted to custom keys. A seeded reward would come straight back on the
// next Seed run, and deleting one would rip a streak reward out of the
// galleries of everyone who earned it.
func (r *RewardRepo) DeleteCustom(ctx context.Context, key string) error {
tag, err := r.pg.Pool.Exec(ctx,
`DELETE FROM sadhna_rewards WHERE key = $1 AND key LIKE 'custom\_%'`, key)
if err != nil {
return fmt.Errorf("delete custom reward: %w", err)
}
if tag.RowsAffected() == 0 {
return ErrRewardNotFound
}
return nil
}
// ByKey looks up one reward from the catalog.
func (r *RewardRepo) ByKey(ctx context.Context, key string) (*model.SadhnaReward, error) {
var w model.SadhnaReward
+1
View File
@@ -1165,6 +1165,7 @@ func (s *Server) setupRouter() {
r.Post("/reward-gifts", s.sadhnaH.GiftToEveryone)
r.Post("/reward-gifts/catalog", s.sadhnaH.CreateCustomGift)
r.Post("/reward-gifts/image-presign", s.sadhnaH.PresignGiftImage)
r.Delete("/reward-gifts/{key}", s.sadhnaH.DeleteCustomGift)
r.Get("/broadcasts", s.broadcastH.List)
r.Post("/broadcasts", s.broadcastH.Create)
+66
View File
@@ -2,6 +2,7 @@ package service
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
@@ -138,3 +139,68 @@ func TestGift_ArrivesWrappedAndOpensOnce(t *testing.T) {
t.Error("re-gifting re-wrapped an already opened gift")
}
}
// Deleting a custom gift must clear it from every gallery it reached, and must
// refuse to touch the seeded catalog — a seeded reward would return on the
// next Seed run, after having been ripped out of people's galleries.
func TestGift_DeleteRemovesItFromEveryGallery(t *testing.T) {
rewards, pg := testRewardRepo(t)
ctx := context.Background()
user := giftUser(t, pg)
// A custom reward, as the admin panel creates them.
key := "custom_probe_" + uuid.NewString()[:8]
if _, err := pg.Pool.Exec(ctx, `
INSERT INTO sadhna_rewards (key, name, image_url)
VALUES ($1, 'Probe Gift', 'https://example.invalid/p.png')`, key); err != nil {
t.Fatal(err)
}
reward, err := rewards.ByKey(ctx, key)
if err != nil {
t.Fatal(err)
}
if _, err := rewards.GrantGiftToEveryone(ctx, reward.ID); err != nil {
t.Fatal(err)
}
held := func() int {
grants, err := rewards.GrantsByUser(ctx, user)
if err != nil {
t.Fatal(err)
}
n := 0
for _, g := range grants {
if g.Reward.Key == key {
n++
}
}
return n
}
if held() != 1 {
t.Fatalf("setup: the gift did not reach the gallery")
}
if err := rewards.DeleteCustom(ctx, key); err != nil {
t.Fatal(err)
}
if held() != 0 {
t.Error("deleting the reward left it in a devotee's gallery")
}
if _, err := rewards.ByKey(ctx, key); !errors.Is(err, repository.ErrRewardNotFound) {
t.Errorf("reward still in the catalog: %v", err)
}
// Seeded rewards are off limits.
var seeded string
if err := pg.Pool.QueryRow(ctx,
`SELECT key FROM sadhna_rewards WHERE key NOT LIKE 'custom\_%' LIMIT 1`,
).Scan(&seeded); err != nil {
t.Skip("no seeded rewards to test against")
}
if err := rewards.DeleteCustom(ctx, seeded); !errors.Is(err, repository.ErrRewardNotFound) {
t.Errorf("a seeded reward was deletable: %v", err)
}
if _, err := rewards.ByKey(ctx, seeded); err != nil {
t.Errorf("the seeded reward was removed: %v", err)
}
}
+8
View File
@@ -478,6 +478,14 @@ func (s *SadhnaService) PresignGiftImage(ctx context.Context, contentType string
return presignImage(ctx, s.r2, "reward-gifts", "custom", contentType)
}
// DeleteCustomGift removes a custom sticker everywhere it is held.
func (s *SadhnaService) DeleteCustomGift(ctx context.Context, key string) error {
if !strings.HasPrefix(key, customKeyPrefix) {
return errors.New("only custom gifts can be deleted")
}
return s.rewards.DeleteCustom(ctx, key)
}
// GiftableRewards is the catalog an admin picks from.
func (s *SadhnaService) GiftableRewards(ctx context.Context) ([]model.SadhnaReward, error) {
return s.rewards.ListGiftable(ctx)