Files
2026-07-10 00:56:03 +03:30

555 lines
17 KiB
Go

package economy
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"hakemsho/internal/auth"
"hakemsho/internal/httpx"
)
// Handler endpointهای فروشگاه و کیف‌پول (همه پشت JWT).
type Handler struct {
svc *Service
}
func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} }
func uid(r *http.Request) (int64, bool) { return auth.UserID(r.Context()) }
// Wallet — GET /api/wallet
func (h *Handler) Wallet(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
p, err := h.svc.GetProfile(r.Context(), id)
if err != nil {
httpx.Error(w, http.StatusNotFound, "user not found")
return
}
httpx.JSON(w, http.StatusOK, p)
}
// Shop — GET /api/shop
func (h *Handler) Shop(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
owned, err := h.svc.OwnedCards(r.Context(), id)
if err != nil {
httpx.Error(w, http.StatusInternalServerError, "server error")
return
}
p, _ := h.svc.GetProfile(r.Context(), id)
selected := "simple"
if p != nil {
selected = p.SelectedCard
}
httpx.JSON(w, http.StatusOK, map[string]any{
"catalog": h.svc.Catalog(),
"owned_cards": owned,
"selected_card": selected,
})
}
type cardReq struct {
CardID string `json:"card_id"`
}
// BuyCard — POST /api/shop/buy-card
func (h *Handler) BuyCard(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
var req cardReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpx.Error(w, http.StatusBadRequest, "invalid body")
return
}
switch err := h.svc.BuyCard(r.Context(), id, req.CardID); {
case err == nil:
httpx.JSON(w, http.StatusOK, map[string]string{"message": "ok"})
case errors.Is(err, ErrNotFound):
httpx.Error(w, http.StatusNotFound, "card not found")
case errors.Is(err, ErrAlreadyOwned):
httpx.Error(w, http.StatusConflict, "already owned")
case errors.Is(err, ErrInsufficient):
httpx.Error(w, http.StatusPaymentRequired, "insufficient coins")
default:
httpx.Error(w, http.StatusInternalServerError, "server error")
}
}
// Carpets — GET /api/carpets (فرش‌ها + مالکیت + فرشِ انتخابی)
func (h *Handler) Carpets(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
carpets, err := h.svc.Carpets(r.Context(), id)
if err != nil {
httpx.Error(w, http.StatusInternalServerError, "server error")
return
}
selected := "classic"
if p, _ := h.svc.GetProfile(r.Context(), id); p != nil && p.SelectedCarpet != "" {
selected = p.SelectedCarpet
}
httpx.JSON(w, http.StatusOK, map[string]any{
"carpets": carpets,
"selected": selected,
})
}
// BuyCarpet — POST /api/shop/buy-carpet
func (h *Handler) BuyCarpet(w http.ResponseWriter, r *http.Request) {
h.carpetAction(w, r, false)
}
// SelectCarpet — POST /api/shop/select-carpet
func (h *Handler) SelectCarpet(w http.ResponseWriter, r *http.Request) {
h.carpetAction(w, r, true)
}
func (h *Handler) carpetAction(w http.ResponseWriter, r *http.Request, sel bool) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
var req struct {
CarpetID string `json:"carpet_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpx.Error(w, http.StatusBadRequest, "invalid body")
return
}
var err error
if sel {
err = h.svc.SelectCarpet(r.Context(), id, req.CarpetID)
} else {
err = h.svc.BuyCarpet(r.Context(), id, req.CarpetID)
}
switch {
case err == nil:
httpx.JSON(w, http.StatusOK, map[string]string{"message": "ok"})
case errors.Is(err, ErrNotFound):
httpx.Error(w, http.StatusNotFound, "carpet not found")
case errors.Is(err, ErrAlreadyOwned):
httpx.Error(w, http.StatusConflict, "already owned")
case errors.Is(err, ErrNotOwned):
httpx.Error(w, http.StatusForbidden, "carpet not owned")
case errors.Is(err, ErrInsufficient):
httpx.Error(w, http.StatusPaymentRequired, "insufficient coins")
default:
httpx.Error(w, http.StatusInternalServerError, "server error")
}
}
// Frames — GET /api/frames (قاب‌های آواتار + مالکیت + انتخابِ کاربر)
func (h *Handler) Frames(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
frames, err := h.svc.Frames(r.Context(), id)
if err != nil {
httpx.Error(w, http.StatusInternalServerError, "server error")
return
}
selected := ""
if p, _ := h.svc.GetProfile(r.Context(), id); p != nil {
selected = p.SelectedFrame
}
tiers, _ := h.svc.RankTiers(r.Context())
httpx.JSON(w, http.StatusOK, map[string]any{
"frames": frames,
"selected": selected,
"rank_tiers": tiers,
})
}
// BuyFrame — POST /api/shop/buy-frame
// Avatars — GET /api/avatars: کاتالوگِ شخصیت‌ها + انتخابیِ کاربر.
func (h *Handler) Avatars(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
avatars, err := h.svc.Avatars(r.Context(), id)
if err != nil {
httpx.Error(w, http.StatusInternalServerError, "server error")
return
}
selected := ""
if p, _ := h.svc.GetProfile(r.Context(), id); p != nil {
selected = p.SelectedAvatar
}
httpx.JSON(w, http.StatusOK, map[string]any{
"avatars": avatars,
"selected": selected,
})
}
// BuyAvatar — POST /api/shop/buy-avatar
func (h *Handler) BuyAvatar(w http.ResponseWriter, r *http.Request) {
h.avatarAction(w, r, false)
}
// SelectAvatar — POST /api/shop/select-avatar (avatar_id خالی ⇒ آواتارِ پیش‌فرض)
func (h *Handler) SelectAvatar(w http.ResponseWriter, r *http.Request) {
h.avatarAction(w, r, true)
}
func (h *Handler) avatarAction(w http.ResponseWriter, r *http.Request, sel bool) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
var req struct {
AvatarID string `json:"avatar_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpx.Error(w, http.StatusBadRequest, "invalid body")
return
}
var err error
if sel {
err = h.svc.SelectAvatar(r.Context(), id, req.AvatarID)
} else {
err = h.svc.BuyAvatar(r.Context(), id, req.AvatarID)
}
switch {
case err == nil:
httpx.JSON(w, http.StatusOK, map[string]string{"message": "ok"})
case errors.Is(err, ErrNotFound):
httpx.Error(w, http.StatusNotFound, "avatar not found")
case errors.Is(err, ErrAlreadyOwned):
httpx.Error(w, http.StatusConflict, "already owned")
case errors.Is(err, ErrNotOwned):
httpx.Error(w, http.StatusForbidden, "avatar not owned")
case errors.Is(err, ErrInsufficient):
httpx.Error(w, http.StatusPaymentRequired, "insufficient coins")
default:
httpx.Error(w, http.StatusInternalServerError, "server error")
}
}
func (h *Handler) BuyFrame(w http.ResponseWriter, r *http.Request) {
h.frameAction(w, r, false)
}
// SelectFrame — POST /api/shop/select-frame
func (h *Handler) SelectFrame(w http.ResponseWriter, r *http.Request) {
h.frameAction(w, r, true)
}
func (h *Handler) frameAction(w http.ResponseWriter, r *http.Request, sel bool) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
var req struct {
FrameID string `json:"frame_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpx.Error(w, http.StatusBadRequest, "invalid body")
return
}
var err error
if sel {
err = h.svc.SelectFrame(r.Context(), id, req.FrameID)
} else {
err = h.svc.BuyFrame(r.Context(), id, req.FrameID)
}
switch {
case err == nil:
httpx.JSON(w, http.StatusOK, map[string]string{"message": "ok"})
case errors.Is(err, ErrNotFound):
httpx.Error(w, http.StatusNotFound, "frame not found")
case errors.Is(err, ErrAlreadyOwned):
httpx.Error(w, http.StatusConflict, "already owned")
case errors.Is(err, ErrNotOwned):
httpx.Error(w, http.StatusForbidden, "frame not owned")
case errors.Is(err, ErrInsufficient):
httpx.Error(w, http.StatusPaymentRequired, "insufficient coins")
default:
httpx.Error(w, http.StatusInternalServerError, "server error")
}
}
// ChatPacks — GET /api/chat-packs (بسته‌های پیام/شکلک + مالکیتِ کاربر)
func (h *Handler) ChatPacks(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
packs, err := h.svc.ChatPacks(r.Context(), id)
if err != nil {
httpx.Error(w, http.StatusInternalServerError, "server error")
return
}
httpx.JSON(w, http.StatusOK, map[string]any{"packs": packs})
}
// BuyChatPack — POST /api/shop/buy-chat-pack
func (h *Handler) BuyChatPack(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
var req struct {
PackID string `json:"pack_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpx.Error(w, http.StatusBadRequest, "invalid body")
return
}
switch err := h.svc.BuyChatPack(r.Context(), id, req.PackID); {
case err == nil:
httpx.JSON(w, http.StatusOK, map[string]string{"message": "ok"})
case errors.Is(err, ErrNotFound):
httpx.Error(w, http.StatusNotFound, "pack not found")
case errors.Is(err, ErrAlreadyOwned):
httpx.Error(w, http.StatusConflict, "already owned")
case errors.Is(err, ErrInsufficient):
httpx.Error(w, http.StatusPaymentRequired, "insufficient coins")
default:
httpx.Error(w, http.StatusInternalServerError, "server error")
}
}
// Tournaments — GET /api/tournaments (فهرست با وضعیتِ عضویتِ کاربر)
func (h *Handler) Tournaments(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
list, err := h.svc.ListTournaments(r.Context(), id)
if err != nil {
httpx.Error(w, http.StatusInternalServerError, "server error")
return
}
httpx.JSON(w, http.StatusOK, map[string]any{"tournaments": list})
}
// TournamentStandings — GET /api/tournaments/standings?id=X (رده‌بندی)
func (h *Handler) TournamentStandings(w http.ResponseWriter, r *http.Request) {
if _, ok := uid(r); !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
tid, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)
if tid == 0 {
httpx.Error(w, http.StatusBadRequest, "invalid id")
return
}
rows, err := h.svc.TournamentStandings(r.Context(), tid, 50)
if err != nil {
httpx.Error(w, http.StatusInternalServerError, "server error")
return
}
httpx.JSON(w, http.StatusOK, map[string]any{"standings": rows})
}
// JoinTournament — POST /api/tournaments/join {id}
func (h *Handler) JoinTournament(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
var req struct {
ID int64 `json:"id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpx.Error(w, http.StatusBadRequest, "invalid body")
return
}
switch err := h.svc.JoinTournament(r.Context(), id, req.ID); {
case err == nil:
httpx.JSON(w, http.StatusOK, map[string]string{"message": "ثبت‌نام انجام شد"})
case errors.Is(err, ErrTournamentNotFound):
httpx.Error(w, http.StatusNotFound, "تورنومنت یافت نشد")
case errors.Is(err, ErrTournamentClosed):
httpx.Error(w, http.StatusConflict, "مهلتِ ثبت‌نام تمام شده")
case errors.Is(err, ErrAlreadyJoined):
httpx.Error(w, http.StatusConflict, "قبلاً ثبت‌نام کرده‌اید")
case errors.Is(err, ErrInsufficient):
httpx.Error(w, http.StatusPaymentRequired, "insufficient coins")
default:
httpx.Error(w, http.StatusInternalServerError, "server error")
}
}
// SelectCard — POST /api/shop/select-card
func (h *Handler) SelectCard(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
var req cardReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpx.Error(w, http.StatusBadRequest, "invalid body")
return
}
switch err := h.svc.SelectCard(r.Context(), id, req.CardID); {
case err == nil:
httpx.JSON(w, http.StatusOK, map[string]string{"message": "ok"})
case errors.Is(err, ErrNotFound):
httpx.Error(w, http.StatusNotFound, "card not found")
case errors.Is(err, ErrNotOwned):
httpx.Error(w, http.StatusForbidden, "card not owned")
default:
httpx.Error(w, http.StatusInternalServerError, "server error")
}
}
// Purchase — POST /api/shop/purchase (تأیید خرید IAP)
func (h *Handler) Purchase(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
var req PurchaseRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpx.Error(w, http.StatusBadRequest, "invalid body")
return
}
switch err := h.svc.VerifyPurchase(r.Context(), id, req); {
case err == nil:
p, _ := h.svc.GetProfile(r.Context(), id)
httpx.JSON(w, http.StatusOK, map[string]any{"message": "ok", "wallet": p})
case errors.Is(err, ErrAlreadyOwned):
httpx.Error(w, http.StatusConflict, "already processed")
case errors.Is(err, ErrIAPInvalid):
httpx.Error(w, http.StatusBadRequest, "purchase not valid")
case errors.Is(err, ErrNotFound):
httpx.Error(w, http.StatusNotFound, "package not found")
default:
httpx.Error(w, http.StatusInternalServerError, "server error")
}
}
// Stats — GET /api/stats (آمار پروفایل؛ نمایشِ کامل ویژه‌ی کاربران VIP)
func (h *Handler) Stats(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
p, err := h.svc.GetProfile(r.Context(), id)
if err != nil {
httpx.Error(w, http.StatusNotFound, "user not found")
return
}
resp := map[string]any{"vip": p.VIP}
if p.VIP {
st, err := h.svc.GetStats(r.Context(), id)
if err != nil {
httpx.Error(w, http.StatusInternalServerError, "server error")
return
}
resp["stats"] = st
}
httpx.JSON(w, http.StatusOK, resp)
}
// TablesInfo — GET /api/tables/info (باقیمانده‌ی میزهای خصوصیِ رایگان)
func (h *Handler) TablesInfo(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
remaining, unlimited := h.svc.PrivateTableInfo(r.Context(), id)
httpx.JSON(w, http.StatusOK, map[string]any{
"remaining": remaining,
"unlimited": unlimited,
})
}
// Leaderboard — GET /api/leaderboard (جدولِ رتبه‌بندیِ فصل)
func (h *Handler) Leaderboard(w http.ResponseWriter, r *http.Request) {
if _, ok := uid(r); !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
list, err := h.svc.Leaderboard(r.Context(), 50)
if err != nil {
httpx.Error(w, http.StatusInternalServerError, "server error")
return
}
httpx.JSON(w, http.StatusOK, map[string]any{
"season": h.svc.Season(r.Context()),
"entries": list,
})
}
// Daily — POST /api/rewards/daily
func (h *Handler) Daily(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
switch amount, streak, best, err := h.svc.ClaimDaily(r.Context(), id); {
case err == nil:
httpx.JSON(w, http.StatusOK, map[string]any{
"message": "ok", "amount": amount, "streak": streak, "best": best,
})
case errors.Is(err, ErrTooSoon):
httpx.Error(w, http.StatusTooManyRequests, "already claimed today")
default:
httpx.Error(w, http.StatusInternalServerError, "server error")
}
}
type adReq struct {
Token string `json:"token"`
}
// AdReward — POST /api/rewards/ad (سکه رایگان پس از دیدن کامل تبلیغ)
func (h *Handler) AdReward(w http.ResponseWriter, r *http.Request) {
id, ok := uid(r)
if !ok {
httpx.Error(w, http.StatusUnauthorized, "unauthorized")
return
}
var req adReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpx.Error(w, http.StatusBadRequest, "invalid body")
return
}
switch amount, err := h.svc.ClaimAdReward(r.Context(), id, req.Token); {
case err == nil:
httpx.JSON(w, http.StatusOK, map[string]any{"message": "ok", "amount": amount})
case errors.Is(err, ErrAdNotVerified):
httpx.Error(w, http.StatusBadRequest, "ad not verified")
case errors.Is(err, ErrAdDuplicate):
httpx.Error(w, http.StatusConflict, "already claimed")
case errors.Is(err, ErrAdCap):
httpx.Error(w, http.StatusTooManyRequests, "daily ad cap reached")
default:
httpx.Error(w, http.StatusInternalServerError, "server error")
}
}