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") } }