feat: add stickers delete api
This commit is contained in:
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user