Files
back-hokm/internal/economy/handler.go
T
2026-07-07 21:10:01 +03:30

419 lines
13 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")
}
}
// 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")
}
}