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)
|
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 {
|
type customGiftReq struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Meaning string `json:"meaning"`
|
Meaning string `json:"meaning"`
|
||||||
|
|||||||
@@ -206,6 +206,29 @@ func (r *RewardRepo) CreateCustom(ctx context.Context, key, name, meaning, image
|
|||||||
return &w, nil
|
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.
|
// ByKey looks up one reward from the catalog.
|
||||||
func (r *RewardRepo) ByKey(ctx context.Context, key string) (*model.SadhnaReward, error) {
|
func (r *RewardRepo) ByKey(ctx context.Context, key string) (*model.SadhnaReward, error) {
|
||||||
var w model.SadhnaReward
|
var w model.SadhnaReward
|
||||||
|
|||||||
@@ -1165,6 +1165,7 @@ func (s *Server) setupRouter() {
|
|||||||
r.Post("/reward-gifts", s.sadhnaH.GiftToEveryone)
|
r.Post("/reward-gifts", s.sadhnaH.GiftToEveryone)
|
||||||
r.Post("/reward-gifts/catalog", s.sadhnaH.CreateCustomGift)
|
r.Post("/reward-gifts/catalog", s.sadhnaH.CreateCustomGift)
|
||||||
r.Post("/reward-gifts/image-presign", s.sadhnaH.PresignGiftImage)
|
r.Post("/reward-gifts/image-presign", s.sadhnaH.PresignGiftImage)
|
||||||
|
r.Delete("/reward-gifts/{key}", s.sadhnaH.DeleteCustomGift)
|
||||||
|
|
||||||
r.Get("/broadcasts", s.broadcastH.List)
|
r.Get("/broadcasts", s.broadcastH.List)
|
||||||
r.Post("/broadcasts", s.broadcastH.Create)
|
r.Post("/broadcasts", s.broadcastH.Create)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -138,3 +139,68 @@ func TestGift_ArrivesWrappedAndOpensOnce(t *testing.T) {
|
|||||||
t.Error("re-gifting re-wrapped an already opened gift")
|
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)
|
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.
|
// GiftableRewards is the catalog an admin picks from.
|
||||||
func (s *SadhnaService) GiftableRewards(ctx context.Context) ([]model.SadhnaReward, error) {
|
func (s *SadhnaService) GiftableRewards(ctx context.Context) ([]model.SadhnaReward, error) {
|
||||||
return s.rewards.ListGiftable(ctx)
|
return s.rewards.ListGiftable(ctx)
|
||||||
|
|||||||
Reference in New Issue
Block a user