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