From 82497ffc6468efd30972e41a1b27e32b4d80def8 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Fri, 29 Aug 2025 15:20:59 +0300 Subject: [PATCH 1/2] provider enable/disable fix --- .../virtualGame/veli/game_orchestration.go | 26 ++++- internal/services/virtualGame/veli/service.go | 97 +++++++++++++++--- internal/web_server/handlers/bet_handler.go | 2 +- internal/web_server/handlers/veli_games.go | 39 +++++++ static/logos/popok-dark.png | Bin 0 -> 8594 bytes static/logos/popok-light.png | Bin 0 -> 8148 bytes 6 files changed, 143 insertions(+), 21 deletions(-) create mode 100644 static/logos/popok-dark.png create mode 100644 static/logos/popok-light.png diff --git a/internal/services/virtualGame/veli/game_orchestration.go b/internal/services/virtualGame/veli/game_orchestration.go index b2187b0..b2ba665 100644 --- a/internal/services/virtualGame/veli/game_orchestration.go +++ b/internal/services/virtualGame/veli/game_orchestration.go @@ -44,16 +44,36 @@ func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) createParams := dbgen.CreateVirtualGameProviderParams{ ProviderID: p.ProviderID, ProviderName: p.ProviderName, - LogoDark: pgtype.Text{String: p.LogoForDark, Valid: true}, - LogoLight: pgtype.Text{String: p.LogoForLight, Valid: true}, + LogoDark: pgtype.Text{String: p.LogoForDark, Valid: p.LogoForDark != ""}, + LogoLight: pgtype.Text{String: p.LogoForLight, Valid: p.LogoForLight != ""}, Enabled: true, } if _, err := s.repo.CreateVirtualGameProvider(ctx, createParams); err != nil { - // Log error but continue with other providers return nil, fmt.Errorf("failed to add provider %s: %w", p.ProviderID, err) } } + // 4. Always add "popok" provider manually + popokParams := dbgen.CreateVirtualGameProviderParams{ + ProviderID: "popok", + ProviderName: "Popok Gaming", + LogoDark: pgtype.Text{String: "/static/logos/popok-dark.png", Valid: true}, // adjust as needed + LogoLight: pgtype.Text{String: "/static/logos/popok-light.png", Valid: true}, // adjust as needed + Enabled: true, + } + + if _, err := s.repo.CreateVirtualGameProvider(ctx, popokParams); err != nil { + return nil, fmt.Errorf("failed to add popok provider: %w", err) + } + + // Optionally also append it to the response for consistency + // res.Items = append(res.Items, domain.VirtualGameProvider{ + // ProviderID: uuid.New().String(), + // ProviderName: "Popok Gaming", + // LogoForDark: "/static/logos/popok-dark.png", + // LogoForLight: "/static/logos/popok-light.png", + // }) + return &res, nil } diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index c8da008..292f4e9 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -62,39 +62,102 @@ func (s *Service) GetProviders(ctx context.Context, req domain.ProviderRequest) } func (s *Service) GetGames(ctx context.Context, req domain.GameListRequest) ([]domain.GameEntity, error) { - sigParams := map[string]any{ - "brandId": req.BrandID, "providerId": req.ProviderID, + // 1. Check if provider is enabled in DB + provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID) + if err != nil { + return nil, fmt.Errorf("failed to check provider %s: %w", req.ProviderID, err) } + + if !provider.Enabled { + // Provider exists but is disabled → return empty list (or error if you prefer) + return nil, fmt.Errorf("provider %s is disabled", req.ProviderID) + } + + // 2. Prepare signature params + sigParams := map[string]any{ + "brandId": req.BrandID, + "providerId": req.ProviderID, + } + + // 3. Call external API var res struct { Items []domain.GameEntity `json:"items"` } - err := s.client.post(ctx, "/game-lists/public/games", req, sigParams, &res) - return res.Items, err + if err := s.client.post(ctx, "/game-lists/public/games", req, sigParams, &res); err != nil { + return nil, fmt.Errorf("failed to fetch games for provider %s: %w", req.ProviderID, err) + } + + return res.Items, nil } func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (*domain.GameStartResponse, error) { - sigParams := map[string]any{ - "sessionId": req.SessionID, "providerId": req.ProviderID, - "gameId": req.GameID, "language": req.Language, "playerId": req.PlayerID, - "currency": req.Currency, "deviceType": req.DeviceType, "country": "US", - "ip": req.IP, "brandId": req.BrandID, + // 1. Check if provider is enabled in DB + provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID) + if err != nil { + return nil, fmt.Errorf("failed to check provider %s: %w", req.ProviderID, err) } + + if !provider.Enabled { + // Provider exists but is disabled → return error + return nil, fmt.Errorf("provider %s is disabled", req.ProviderID) + } + + // 2. Prepare signature params + sigParams := map[string]any{ + "sessionId": req.SessionID, + "providerId": req.ProviderID, + "gameId": req.GameID, + "language": req.Language, + "playerId": req.PlayerID, + "currency": req.Currency, + "deviceType": req.DeviceType, + "country": "US", + "ip": req.IP, + "brandId": req.BrandID, + } + + // 3. Call external API var res domain.GameStartResponse - err := s.client.post(ctx, "/unified-api/public/start-game", req, sigParams, &res) - return &res, err + if err := s.client.post(ctx, "/unified-api/public/start-game", req, sigParams, &res); err != nil { + return nil, fmt.Errorf("failed to start game with provider %s: %w", req.ProviderID, err) + } + + return &res, nil } + func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) { - sigParams := map[string]any{ - "providerId": req.ProviderID, "gameId": req.GameID, - "language": req.Language, "deviceType": req.DeviceType, - "ip": req.IP, "brandId": req.BrandID, + // 1. Check if provider is enabled in DB + provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID) + if err != nil { + return nil, fmt.Errorf("failed to check provider %s: %w", req.ProviderID, err) } + + if !provider.Enabled { + // Provider exists but is disabled → return error + return nil, fmt.Errorf("provider %s is disabled", req.ProviderID) + } + + // 2. Prepare signature params + sigParams := map[string]any{ + "providerId": req.ProviderID, + "gameId": req.GameID, + "language": req.Language, + "deviceType": req.DeviceType, + "ip": req.IP, + "brandId": req.BrandID, + } + + // 3. Call external API var res domain.GameStartResponse - err := s.client.post(ctx, "/unified-api/public/start-demo-game", req, sigParams, &res) - return &res, err + if err := s.client.post(ctx, "/unified-api/public/start-demo-game", req, sigParams, &res); err != nil { + return nil, fmt.Errorf("failed to start demo game with provider %s: %w", req.ProviderID, err) + } + + return &res, nil } + func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*domain.BalanceResponse, error) { // Retrieve player's real balance from wallet Service playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index a323560..fb59223 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -149,7 +149,7 @@ func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create bet:"+err.Error()) } - wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), bet.UserID) + wallet, _ := h.walletSvc.GetCustomerWallet(c.Context(), bet.UserID) // amount added for fast code owner can be fetched from settings in db settingList, err := h.settingSvc.GetSettingList(c.Context()) diff --git a/internal/web_server/handlers/veli_games.go b/internal/web_server/handlers/veli_games.go index 8a005e6..0f299f5 100644 --- a/internal/web_server/handlers/veli_games.go +++ b/internal/web_server/handlers/veli_games.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log" + "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" @@ -70,6 +71,7 @@ func (h *Handler) GetGamesByProvider(c *fiber.Ctx) error { }) } + // Default brand if not provided if req.BrandID == "" { req.BrandID = h.Cfg.VeliGames.BrandID } @@ -77,6 +79,16 @@ func (h *Handler) GetGamesByProvider(c *fiber.Ctx) error { res, err := h.veliVirtualGameSvc.GetGames(context.Background(), req) if err != nil { log.Println("GetGames error:", err) + + // Handle provider disabled case specifically + if strings.Contains(err.Error(), "is disabled") { + return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ + Message: "Provider is disabled", + Error: err.Error(), + }) + } + + // Fallback for other errors return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Message: "Failed to retrieve games", Error: err.Error(), @@ -91,6 +103,7 @@ func (h *Handler) GetGamesByProvider(c *fiber.Ctx) error { }) } + // StartGame godoc // @Summary Start a real game session // @Description Starts a real VeliGames session with the given player and game info @@ -119,7 +132,10 @@ func (h *Handler) StartGame(c *fiber.Ctx) error { }) } + // Attach user ID to request req.PlayerID = fmt.Sprintf("%d", userId) + + // Default brand if not provided if req.BrandID == "" { req.BrandID = h.Cfg.VeliGames.BrandID } @@ -127,6 +143,16 @@ func (h *Handler) StartGame(c *fiber.Ctx) error { res, err := h.veliVirtualGameSvc.StartGame(context.Background(), req) if err != nil { log.Println("StartGame error:", err) + + // Handle provider disabled case specifically + if strings.Contains(err.Error(), "is disabled") { + return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ + Message: "Provider is disabled", + Error: err.Error(), + }) + } + + // Fallback for other errors return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Message: "Failed to start game", Error: err.Error(), @@ -141,6 +167,7 @@ func (h *Handler) StartGame(c *fiber.Ctx) error { }) } + // StartDemoGame godoc // @Summary Start a demo game session // @Description Starts a demo session of the specified game (must support demo mode) @@ -161,6 +188,7 @@ func (h *Handler) StartDemoGame(c *fiber.Ctx) error { }) } + // Default brand if not provided if req.BrandID == "" { req.BrandID = h.Cfg.VeliGames.BrandID } @@ -168,6 +196,16 @@ func (h *Handler) StartDemoGame(c *fiber.Ctx) error { res, err := h.veliVirtualGameSvc.StartDemoGame(context.Background(), req) if err != nil { log.Println("StartDemoGame error:", err) + + // Handle provider disabled case specifically + if strings.Contains(err.Error(), "is disabled") { + return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ + Message: "Provider is disabled", + Error: err.Error(), + }) + } + + // Fallback for other errors return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Message: "Failed to start demo game", Error: err.Error(), @@ -182,6 +220,7 @@ func (h *Handler) StartDemoGame(c *fiber.Ctx) error { }) } + func (h *Handler) GetBalance(c *fiber.Ctx) error { var req domain.BalanceRequest if err := c.BodyParser(&req); err != nil { diff --git a/static/logos/popok-dark.png b/static/logos/popok-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..71c9e5026c4e46069e8b1d276f2f18444c992442 GIT binary patch literal 8594 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 literal 0 HcmV?d00001 diff --git a/static/logos/popok-light.png b/static/logos/popok-light.png new file mode 100644 index 0000000000000000000000000000000000000000..40f051b75e1acdf6c1616cf1e08ce8897e3ba522 GIT binary patch literal 8148 zcmY*;Wmr_t`}eYRNi4NAEGfODbS_ADHwY3EBHdj}i3mzJ(jXuq4POwXJET-Pq$T%% z{JnUt>)983&6z!W&zw0k_x*`@ZB1oDJZd};2t@c?MNt<7LW=_05jbGr``al84+umI zdafv^?}v71fvZnu)P9^i@JGnm9F#;P#p@Yp%g<{wdzvHWCh#GWk9Rl@-z{rbCtv(F z`9I9y*UT`7Ki?D~k>fATNx%2T$0tC&X({#nNV6!JJmc-Rz|sPE^g2!J3qK}jE_5%A zE^8{w9`4L5-GAJzqt@=`YEB-nyXOW4DIWR3NDyoT8%NS^iwM?(!w(J#1Er@im}9ts zfy*p)b%Z3IkX%>>9IQ%W4cASe8WjX1!`N{6p-x~ft``_oXr(}#QwjMSiiVJdV5r!a zJRhrmM`8{9!j6%J%<0+S00L3P0#Q8|y~h5H76r5+RJ3zYwAy465Bacf!?*;=a3HKF z5DFSAweTI7hbGW|7T&9a2qWQtg63&&Z_d!_09@dJfT-XN=GT-|;j*$}-#CbCnXkkD zUpJNyWvj0iK3E;0HJY&<0tbgAhoRo;7#J9sn3!;eQlTMwa4t{jo$7 zxe{MqjJ}_czH|NTFeZP~4-AfA4C|P174bb<_8v?iFW1bS4$peS2CI{*tf){?RRyCF z(uu$6k0BIwU%PlZ0tFtbfD#-X9bIKuv%lUg6X$fkJ@(P?fYSb5TH5LT`2?RuyZ%gV z<7;64e=x&82s%c*eeLBHti!A`?+5=V9ekT5V8`-t*yP{p$lzQY|B&F}(MY_aQb*Kv zKiBhTItZ#H-WcDH?>Suhys1xOS)68%k-fXo8`;+}fcJ0lT`)HENsi^=NaO~) zZMMn*N&hVr?T9!G|5HJMY(a?KFixQ3Av>ubxd}PU8V4*OAOJoPjzQ|`>9OAS{2N-E zow(Iwvv0FMR!Mk{3f>o?r;ly?Z&y&o^_=}4Rhn}HLHEvu$~nUR*Q>&qnwx83YWkE( zb^;!TOs11SxHG81!F|NmO3REM_kTT^5(pWKiPE9?Dfz)R-v{)CM*~~u zbm6mHBvvFgQJ&#>Jn%_KfOna!f)TP-z|OcBTro65JwE8^>T2dmn&|4TCpin&EjMmF2zH1mW`Jd9-P=g%+h>&a;t zwtb|}t$Cgpq@|^4huoa~UR*SuJy0<0cK8VY=J=hcXPf|Xdb6YFl6%CA^h#^j~)4CS$$Fx5;VhzX9Z247LK!|W$YT} z&j#$@5)x^Is_>(jEYTPU_Z@ieCRKgTMvj_gggQUZ&C|PNBX90DV;N+x2iht5EY*Wp zu|)bl3cFAMALlzp&1!jk0XCSCzCb5~mto?Ptq+B<-E4){^pKL0vM3GMM;z8)VnNiA;yleg zTG$g(xhbSFU&t()vvy$@@wnXQ@(O~x4lk+`!ox#85xSPx81#$$_qC)cpgOM} zv#jYI-x+`JJ_bW!%1|<0Bp(Hds^5#7Zf#1v@qO274BJZb)VK|9Hku*u(ZqY$I<*6Z z4P$T7yH{lvvY3#!X$HWtTc0i*~YXA~EKviXMBGNXc#1?=*0E&87U{E2yEC zCa$g>n)9N~|j#AD@3Fs)dX2 zlH~ZwkM;HZ>R*CyFFpv^A$G=dlj!8t5v|N{CTZy|;8erqTU=e!lIf+EDTFsbYAa!; z8AkC-d0pKtsIR|s$J)yD}fy>NL z3D`!R9dK4F%BD?y(ku7zN71$fE2bg7!z7q`n84$~)!z}XDFKUHqm z?D#d3!F5D3avE88`5Q(9xioR0g7lIOJ>p_Qhplt|HnuqbXcs1!Tfg5HRZgab3OW$v zEKNG;<8Z+H_9^QF@D*O=(@O-L;(kITmbGOBYqL#1 zR%+Z&DFT0g|6Lpem8(M&TzGAPTY`uuq~t7b0-TpCUyB;>yK6d6+**ZlLtE;_sLCy0hkg|S%GkiIqQI2Q<7vw7?(Y5l{qc{E{`7}_A{klsv^%u)ts_^dr%xb;Fvv4kUa^zF`j~N5fc73EfeU#q*97@T| zz_uk#RV73H$P}6XTN(2<2-?SiPc)@8T4zYLSBr2g*T_bks>zeekwN6}{b0ou-HE`&Hzzg7>C08;R3a;`&}BcrT(BCYIGnG^u0}S=ZtcA1 z#pkJ64~b}And)AN!^Lzpw*yN(!)Ezdje$`&gKhHOirwVnczuu5n=4X0D(J?uwa!4% z22B1KukXoq62Ya>5z3Ya}$1m7K zEYP*3NRbUjZ7%W&+YN=u#LN4n!>C35Q>5{MC?6luA)7%ykNZYXc#FH;lLY$D{AKYm z^VAd+KF6!iMmFRGH`%8n;tQbC#c42Q5$9Q)V$2RF0Dh|oJ>`cQz_d)@t6jkzrtRDK z?_X)m9{3s<%whFOTz`Fk{qA&-(#lV@HiLqcG=jv6>-i95Yvb4~~0R?3Eq7*Lm4S5TW*?YJjfv2zdS%J*X}8}0d*>p|xl-gH}Cab*-f zuLkEnsY7Us8Zl{!az|CMvSgn>$IS_NrwOE8H3>F(bkk;MVekFfcnEu+bi}puT&2PP z=4z5Qv@azoxk{aW?{!TZ_{OPYcbE;O@}sn1fYa?~V3zC@}BMUt)9n9(Pm znxVro$=v!_l4)AEe>vR}PSRl&c*9qBEw?DsM5(D)+|G-9d98Z>UjmsklexJ$L2)zU z^XxTHDr{O*p8w5>Wkms0phwQQj+LGL^Te>DVz8mCfRL)1+R1wNi8(n3vT+-=vi`Vj zuBxB&nq99|S3fZ4vZSO$d!A9k=g@<55nW}+^w6a=J;s%WtgYI&{JF)-z?<6z zIX)3(zhVouoN@oQhm%|HQMw1U(b>)D;!xax0O5gtpQwEiZM?pntn_q+4_AlFt2~~8 z?TbIlG_pOIii(Obq`OLLqg|mNJa(*2Q=^Hz;t~l?jiMbV^1L}Yel-Ee-f%*Vuakuf zX3i)s{_zaGiXj6vzh3%D!R!kNJKX&7V2+nD*N`wz&_Jq|{19`MR6^7VCtxI`O6D)V z!#fMrSlG)7qdLor1X2j3x#bmr{v~}57e_`$fP}WQvlDu?jof;-@kt=Iw(W*Px$+&!Gyi-4b*blzwAI#tPns`LbP7nN8-`iQ26Hva9rO2kVSu1g;Q7QFJ;-}&!5Ez(~WpT{{hGz zDcU$)YIH`7YYQ@}-=gmu$y9?@WM0C2DkLm98_?{if~y{hvpk&oa6 zq)?y7_V*9eP9frRwnNmA#-c8}bNq2SE(qt-GWphh&zMczFHjEa%GhA!YvD&HRqm!8%5?C~vW-HZ3#+29RP^hx@)jyrN zEL&YpuJ;|M9+?6530$Q6!kRhov#Xj&=uenye=oj`dqEGZh2*<~;MWj*N?v42O00xI z!Ryk>EMy6>M9|;*0X@e>;*6)Rqb^I0rAKD>;b(W`+`l=HoL!!lLbehN^m=?B^3vD$ zma*1eJY-drk=Ia*PC9mCvCC@Y!q?T_bML9%@d?8davC?2mRmhxhGT_=g;hp%d6NE( zdSz=m0E~~b6K3j+5}DsEK&s(c1RQnm5{T6RpsHBqfNPAVoDeygA77o+RI|`@<(T>Y zy)^3ne9`L5yvkq5<9Y>vAw)ekdM+;BxW)j`XR61Sr{G#00qPZgmyrQ*OidqQIsZfA zs^Cnk&NjoctDG}6eUw1r+qZ86u8(V)>h5B%bo&+MF4b`=2$V6D2k3`51s3m@7W-fc zT;%AGUV1ssL&x42B}I{_*w{#6`@vmsZ*MOEi$FAeiLtWK={LVsSwA41nXs~-@$l3D z#p&I3KWQE9X*8;NSx#(kt+S(g_If>(kE^&@7JI^o2loXJZAA%6;c$L?`GiaFd zY4`4SW?asAb2)1+6$gI@UZeg#Xr-r6er-M2b=L}Z0bsb7P5_=f29GYw9-LP_Dxv_4 zlvU)|OxKbwjMK62kp@(x4!=`dRaLxXlY40xo-^48wM^d41YNM?Ojg4tIiNK`;TYEh z>VULhugWtH&*Zc6k-nrF`nYP>&KJL!7Tv~FB{vuuJubfKUKWN6-DvYYvD|309ZUeK zFIY>g(!f|DdAT9ey_8_6&D{Q1ua~>S@3nwG+M1J1wHtWfOT8A{^)8L1TBeJtEgL~> z^>J}HzMe?gEnC9O$6w#lYpL~fhXw|I?d{@X*co&E=Tyz6hU8ydMH^GO1LU|11~ zgR4eW=eWHqcTY&*(0Uwoziy#)d(_u0?ok_OT)*FNDaRftz>v%z)T2n6X!zI7+JV%? zxWk9%1w_Gg1<;uQ$wi<%;)e&>0bOS8vXp4S@Nj3KX?Yluf8pCa-2cqp1lrz{^UTY- zVszxr_VnA+1^u3Y9lelq5`Fi6(ag^w?WOGk=vVoLnHE%2$-IX;q#|(`kfp?)?--6t zKO2>+lV7|j!K{xIu^qsvCbjPV87CFul`%d}ceytid{V5r*_W>LMez~3y9`xAq=yTmPg2t2FjxtlJLZ5}GE@pZ}$%5<5YE^<|zEUsJ0R z;M&Dq2WfXU;SY9wnN)Qo%%cdnS5B3q%R<-cE*ab*aX}_g{ zc`iLuX%;Ydk}9Uv8#10n@zS{G!`6#HJynzMNyD`2f?0lHt`l-se{EqTWtq;t-Q9cK z7L;pGi#bHm=6*`0OT7I~wRMXJZe)#Y~kqyjat4|_)mu+m-XyveC zaGCw`xU{@?JmyBn=UZH5qvjva`Y#@Y?5-IBvr+FIxL3P9#l@80s zv*iw7J|$9keH9=MK2cK+5b19xSUWvCI|F3Fcg$EyLdvtBvW@-vtrPljVvzkiPfK0_ zX_vh{DN0}22=2ZnS;W8nw4L?$B<|Gxfq<8 znHkWFc_{E{SB%f?bo+i`U40uK?h!K3)s4uIyix_Y@os56{o92)tNh&DBc|eHmS!v{ z$;W_e9dbFc#E8Z%Wl*2LY4PWuI0QT4dM~85)Vw;2@qLG=2c$ z(~Rfs;c7QVbdJ4(vd-5jM%ZvFFLnEVI+*>Wm$hY1dNA+w(=n2Ru*>gSS_#RU9+-Gb z;9G?y{m@SykWTxSO8+8Pn1Jap`Ps+_qF&vs)q|-_A+V=}!%R^EjSZHzxc&S0zYIAn?tDG%_gFGI5O!7M8OF0n=@O+-n; z<&TPOX`PA;htsd&G%EQ!SDDv_`i2Gk>1ne3RR%V@yG{&%(rZm+urOv-Nm6iX{qwdY zx@6zbkE`NkLrjLWCj$%)C$&`LMt5IrP0Pz0pMd)JcRwlbmv&z7>OASr&WyHRsI`y^ zxJV_pL-WUYS!dR9SFdWBAwmkMDWzO%?cQ8UR8J1uympS)yHOn{U0mGUUp;!u*Kc*4 z-=*4&s(@+p8*9zmBIf=YE@x@$=zTa+`Y}LB^J&suorD^Ou>g^DGo#wX)lJ}c5W5{W z)z;S5)J%3I2P!5ZK7QmjfI!*U*=MXOSgK5-!!{Bti!j42>;X9lP{RhKGZ)?Zrc{D- zzAOeb&->&yY8YM`w^Ft#z^2``Cm(bbI@J;=Tn0j$DQIa|3*^w%b9R4tP3(0V#i`N( zN_d(M-|cJ0Qt=2LkwdSCEz0f!Etj?ZzGZqLdy(utzWuARkwu%G8iN7sxVRLUf7PJW zLncplJ9~h7L)Y}W9u_x=*0<(q4tO)3pEy(|`l|mMv@Y{;1@SouXaxQC*aKqhwH8r&LR~hC72>N#WR0x$+h z_{I3)`~!q;tVRIo|*SCxFQs1Z{q`VpzB4DJ6tqoIvsi zEu7LSXexFZ#qA7&0$Ar){-qZZ@DKOJz!vRqNT$)zhll?RL8CxJs{h=(WYCr6AqhhY zR_YX>kp{U6Q91(75=yQBV2+m5#oS9EI35uZgLF`9qeG(*U|#)Y1&?O(QO@zA594T9 zwYWl)X@NKewJ?4l>*-kV#~=YsixL8XRT5Rl0^<_MF|x9jI_d3C+}fMHB!Cv0FofO< zB-8Fc0k7&J3MjyU;c~t+QJ3*+CEmtS__GRXMhD8hJq2((OFnp)^1rj z`5%0Q{ji41OVrPV-kiPg>m%OzXUsk#0f>xna9|7KHWixogEwB|WKC)SzN%=hKDHv_AetJmu~_|MIi8pxlWh=cpNamq`)-7zE-16avt~T0mk!` zp6|D|eq>lEbC3P6{Do+ Date: Sat, 30 Aug 2025 20:26:28 +0300 Subject: [PATCH 2/2] list all and filter virtual games feature --- cmd/main.go | 2 +- db/migrations/000001_fortune.up.sql | 22 ++ .../000004_virtual_game_Session.up.sql | 16 -- db/query/virtual_games.sql | 64 ++++++ gen/db/models.go | 29 +-- gen/db/virtual_games.sql.go | 190 ++++++++++++++++++ internal/domain/virtual_game.go | 18 +- internal/repository/odds.go | 32 --- internal/repository/user.go | 4 +- internal/repository/virtual_game.go | 37 ++-- internal/services/odds/service.go | 34 ++-- .../virtualGame/veli/game_orchestration.go | 162 +++++++++++++++ internal/services/virtualGame/veli/port.go | 3 + internal/services/virtualGame/veli/service.go | 5 +- internal/web_server/cron.go | 103 +++------- internal/web_server/handlers/bet_handler.go | 2 +- internal/web_server/handlers/event_handler.go | 8 +- internal/web_server/handlers/leagues.go | 4 +- .../handlers/virtual_games_hadlers.go | 55 +++++ internal/web_server/routes.go | 7 +- 20 files changed, 606 insertions(+), 191 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index fe39dff..38797ae 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -153,7 +153,7 @@ func main() { virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) veliCLient := veli.NewClient(cfg, walletSvc) - veliVirtualGameService := veli.New(vitualGameRepo, veliCLient, walletSvc, wallet.TransferStore(store), cfg) + veliVirtualGameService := veli.New(virtualGameSvc,vitualGameRepo, veliCLient, walletSvc, wallet.TransferStore(store), cfg) recommendationSvc := recommendation.NewService(recommendationRepo) chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index d05dcbe..710fbd7 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -32,6 +32,28 @@ CREATE TABLE IF NOT EXISTS virtual_game_providers ( created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ ); + +CREATE TABLE IF NOT EXISTS virtual_games ( + id BIGSERIAL PRIMARY KEY, + game_id VARCHAR(150) NOT NULL, + provider_id VARCHAR(100) NOT NULL REFERENCES virtual_game_providers(provider_id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + category VARCHAR(100), + device_type VARCHAR(100), + volatility VARCHAR(50), + rtp NUMERIC(5,2), + has_demo BOOLEAN DEFAULT FALSE, + has_free_bets BOOLEAN DEFAULT FALSE, + bets NUMERIC[] DEFAULT '{}', + thumbnail TEXT, + status INT, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ +); + +CREATE UNIQUE INDEX IF NOT EXISTS ux_virtual_games_provider_game + ON virtual_games (provider_id, game_id); + CREATE TABLE IF NOT EXISTS wallets ( id BIGSERIAL PRIMARY KEY, balance BIGINT NOT NULL DEFAULT 0, diff --git a/db/migrations/000004_virtual_game_Session.up.sql b/db/migrations/000004_virtual_game_Session.up.sql index fe47bac..2dc5ed2 100644 --- a/db/migrations/000004_virtual_game_Session.up.sql +++ b/db/migrations/000004_virtual_game_Session.up.sql @@ -1,19 +1,3 @@ -CREATE TABLE IF NOT EXISTS virtual_games ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, - provider VARCHAR(255) NOT NULL, - category VARCHAR(100), - min_bet NUMERIC(10, 2) NOT NULL, - max_bet NUMERIC(10, 2) NOT NULL, - volatility VARCHAR(50), - is_active BOOLEAN NOT NULL DEFAULT TRUE, - rtp NUMERIC(5, 2) CHECK (rtp >= 0 AND rtp <= 100), - is_featured BOOLEAN NOT NULL DEFAULT FALSE, - popularity_score INT DEFAULT 0, - thumbnail_url TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ -); CREATE TABLE virtual_game_sessions ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id), diff --git a/db/query/virtual_games.sql b/db/query/virtual_games.sql index 2cdc7a5..1e81b98 100644 --- a/db/query/virtual_games.sql +++ b/db/query/virtual_games.sql @@ -122,3 +122,67 @@ SELECT game_id FROM favorite_games WHERE user_id = $1; +-- name: CreateVirtualGame :one +INSERT INTO virtual_games ( + game_id, + provider_id, + name, + category, + device_type, + volatility, + rtp, + has_demo, + has_free_bets, + bets, + thumbnail, + status +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 +) +RETURNING + id, + game_id, + provider_id, + name, + category, + device_type, + volatility, + rtp, + has_demo, + has_free_bets, + bets, + thumbnail, + status, + created_at, + updated_at; + +-- name: GetAllVirtualGames :many +SELECT + vg.id, + vg.game_id, + vg.provider_id, + vp.provider_name, + vg.name, + vg.category, + vg.device_type, + vg.volatility, + vg.rtp, + vg.has_demo, + vg.has_free_bets, + vg.bets, + vg.thumbnail, + vg.status, + vg.created_at, + vg.updated_at +FROM virtual_games vg +JOIN virtual_game_providers vp ON vg.provider_id = vp.provider_id +WHERE + ($1::text IS NULL OR vg.category = $1) -- category filter (optional) + AND ($2::text IS NULL OR vg.name ILIKE '%' || $2 || '%') -- search by name (optional) +ORDER BY vg.created_at DESC +LIMIT $3 OFFSET $4; + +-- name: DeleteAllVirtualGames :exec +DELETE FROM virtual_games; + + diff --git a/gen/db/models.go b/gen/db/models.go index d91961f..68ebcd4 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -809,20 +809,21 @@ type UserGameInteraction struct { } type VirtualGame struct { - ID int64 `json:"id"` - Name string `json:"name"` - Provider string `json:"provider"` - Category pgtype.Text `json:"category"` - MinBet pgtype.Numeric `json:"min_bet"` - MaxBet pgtype.Numeric `json:"max_bet"` - Volatility pgtype.Text `json:"volatility"` - IsActive bool `json:"is_active"` - Rtp pgtype.Numeric `json:"rtp"` - IsFeatured bool `json:"is_featured"` - PopularityScore pgtype.Int4 `json:"popularity_score"` - ThumbnailUrl pgtype.Text `json:"thumbnail_url"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` + ID int64 `json:"id"` + GameID string `json:"game_id"` + ProviderID string `json:"provider_id"` + Name string `json:"name"` + Category pgtype.Text `json:"category"` + DeviceType pgtype.Text `json:"device_type"` + Volatility pgtype.Text `json:"volatility"` + Rtp pgtype.Numeric `json:"rtp"` + HasDemo pgtype.Bool `json:"has_demo"` + HasFreeBets pgtype.Bool `json:"has_free_bets"` + Bets []pgtype.Numeric `json:"bets"` + Thumbnail pgtype.Text `json:"thumbnail"` + Status pgtype.Int4 `json:"status"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` } type VirtualGameHistory struct { diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index 79bce9e..33697d7 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -42,6 +42,92 @@ func (q *Queries) CountVirtualGameProviders(ctx context.Context) (int64, error) return total, err } +const CreateVirtualGame = `-- name: CreateVirtualGame :one +INSERT INTO virtual_games ( + game_id, + provider_id, + name, + category, + device_type, + volatility, + rtp, + has_demo, + has_free_bets, + bets, + thumbnail, + status +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 +) +RETURNING + id, + game_id, + provider_id, + name, + category, + device_type, + volatility, + rtp, + has_demo, + has_free_bets, + bets, + thumbnail, + status, + created_at, + updated_at +` + +type CreateVirtualGameParams struct { + GameID string `json:"game_id"` + ProviderID string `json:"provider_id"` + Name string `json:"name"` + Category pgtype.Text `json:"category"` + DeviceType pgtype.Text `json:"device_type"` + Volatility pgtype.Text `json:"volatility"` + Rtp pgtype.Numeric `json:"rtp"` + HasDemo pgtype.Bool `json:"has_demo"` + HasFreeBets pgtype.Bool `json:"has_free_bets"` + Bets []pgtype.Numeric `json:"bets"` + Thumbnail pgtype.Text `json:"thumbnail"` + Status pgtype.Int4 `json:"status"` +} + +func (q *Queries) CreateVirtualGame(ctx context.Context, arg CreateVirtualGameParams) (VirtualGame, error) { + row := q.db.QueryRow(ctx, CreateVirtualGame, + arg.GameID, + arg.ProviderID, + arg.Name, + arg.Category, + arg.DeviceType, + arg.Volatility, + arg.Rtp, + arg.HasDemo, + arg.HasFreeBets, + arg.Bets, + arg.Thumbnail, + arg.Status, + ) + var i VirtualGame + err := row.Scan( + &i.ID, + &i.GameID, + &i.ProviderID, + &i.Name, + &i.Category, + &i.DeviceType, + &i.Volatility, + &i.Rtp, + &i.HasDemo, + &i.HasFreeBets, + &i.Bets, + &i.Thumbnail, + &i.Status, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const CreateVirtualGameHistory = `-- name: CreateVirtualGameHistory :one INSERT INTO virtual_game_histories ( session_id, @@ -284,6 +370,15 @@ func (q *Queries) DeleteAllVirtualGameProviders(ctx context.Context) error { return err } +const DeleteAllVirtualGames = `-- name: DeleteAllVirtualGames :exec +DELETE FROM virtual_games +` + +func (q *Queries) DeleteAllVirtualGames(ctx context.Context) error { + _, err := q.db.Exec(ctx, DeleteAllVirtualGames) + return err +} + const DeleteVirtualGameProvider = `-- name: DeleteVirtualGameProvider :exec DELETE FROM virtual_game_providers WHERE provider_id = $1 @@ -294,6 +389,101 @@ func (q *Queries) DeleteVirtualGameProvider(ctx context.Context, providerID stri return err } +const GetAllVirtualGames = `-- name: GetAllVirtualGames :many +SELECT + vg.id, + vg.game_id, + vg.provider_id, + vp.provider_name, + vg.name, + vg.category, + vg.device_type, + vg.volatility, + vg.rtp, + vg.has_demo, + vg.has_free_bets, + vg.bets, + vg.thumbnail, + vg.status, + vg.created_at, + vg.updated_at +FROM virtual_games vg +JOIN virtual_game_providers vp ON vg.provider_id = vp.provider_id +WHERE + ($1::text IS NULL OR vg.category = $1) -- category filter (optional) + AND ($2::text IS NULL OR vg.name ILIKE '%' || $2 || '%') -- search by name (optional) +ORDER BY vg.created_at DESC +LIMIT $3 OFFSET $4 +` + +type GetAllVirtualGamesParams struct { + Column1 string `json:"column_1"` + Column2 string `json:"column_2"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +type GetAllVirtualGamesRow struct { + ID int64 `json:"id"` + GameID string `json:"game_id"` + ProviderID string `json:"provider_id"` + ProviderName string `json:"provider_name"` + Name string `json:"name"` + Category pgtype.Text `json:"category"` + DeviceType pgtype.Text `json:"device_type"` + Volatility pgtype.Text `json:"volatility"` + Rtp pgtype.Numeric `json:"rtp"` + HasDemo pgtype.Bool `json:"has_demo"` + HasFreeBets pgtype.Bool `json:"has_free_bets"` + Bets []pgtype.Numeric `json:"bets"` + Thumbnail pgtype.Text `json:"thumbnail"` + Status pgtype.Int4 `json:"status"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +func (q *Queries) GetAllVirtualGames(ctx context.Context, arg GetAllVirtualGamesParams) ([]GetAllVirtualGamesRow, error) { + rows, err := q.db.Query(ctx, GetAllVirtualGames, + arg.Column1, + arg.Column2, + arg.Limit, + arg.Offset, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllVirtualGamesRow + for rows.Next() { + var i GetAllVirtualGamesRow + if err := rows.Scan( + &i.ID, + &i.GameID, + &i.ProviderID, + &i.ProviderName, + &i.Name, + &i.Category, + &i.DeviceType, + &i.Volatility, + &i.Rtp, + &i.HasDemo, + &i.HasFreeBets, + &i.Bets, + &i.Thumbnail, + &i.Status, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetVirtualGameProviderByID = `-- name: GetVirtualGameProviderByID :one SELECT id, provider_id, provider_name, logo_dark, logo_light, enabled, created_at, updated_at FROM virtual_game_providers diff --git a/internal/domain/virtual_game.go b/internal/domain/virtual_game.go index 947fc3d..fe92d4c 100644 --- a/internal/domain/virtual_game.go +++ b/internal/domain/virtual_game.go @@ -298,4 +298,20 @@ type VirtualGameProviderPagination struct { TotalCount int64 `json:"total_count"` Limit int32 `json:"limit"` Offset int32 `json:"offset"` -} \ No newline at end of file +} + +type UnifiedGame struct { + GameID string `json:"gameId"` + ProviderID string `json:"providerId"` + Provider string `json:"provider"` + Name string `json:"name"` + Category string `json:"category,omitempty"` + DeviceType string `json:"deviceType,omitempty"` + Volatility string `json:"volatility,omitempty"` + RTP *float64 `json:"rtp,omitempty"` + HasDemo bool `json:"hasDemo"` + HasFreeBets bool `json:"hasFreeBets"` + Bets []float64 `json:"bets,omitempty"` + Thumbnail string `json:"thumbnail,omitempty"` + Status int `json:"status,omitempty"` +} diff --git a/internal/repository/odds.go b/internal/repository/odds.go index 373de20..cb684ce 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -5,7 +5,6 @@ import ( "encoding/json" "os" - "strconv" "time" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" @@ -186,34 +185,3 @@ func (s *Store) GetOddsWithSettingsByEventID(ctx context.Context, upcomingID str func (s *Store) DeleteOddsForEvent(ctx context.Context, eventID string) error { return s.queries.DeleteOddsForEvent(ctx, eventID) } - -func getString(v interface{}) string { - if s, ok := v.(string); ok { - return s - } - return "" -} - -func getConvertedFloat(v interface{}) float64 { - if s, ok := v.(string); ok { - f, err := strconv.ParseFloat(s, 64) - if err == nil { - return f - } - } - return 0 -} - -func getFloat(v interface{}) float64 { - if n, ok := v.(float64); ok { - return n - } - return 0 -} - -func getMap(v interface{}) map[string]interface{} { - if m, ok := v.(map[string]interface{}); ok { - return m - } - return nil -} diff --git a/internal/repository/user.go b/internal/repository/user.go index 703e745..4198b7d 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -150,7 +150,7 @@ func (s *Store) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]do }, } } - totalCount, err := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{ + totalCount, _ := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{ Role: filter.Role, CompanyID: pgtype.Int8{ Int64: filter.CompanyID.Value, @@ -199,7 +199,7 @@ func (s *Store) GetAllCashiers(ctx context.Context, filter domain.UserFilter) ([ BranchLocation: user.BranchLocation, } } - totalCount, err := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{ + totalCount, _ := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{ Role: string(domain.RoleCashier), }) return userList, totalCount, nil diff --git a/internal/repository/virtual_game.go b/internal/repository/virtual_game.go index 03512a7..ab93c29 100644 --- a/internal/repository/virtual_game.go +++ b/internal/repository/virtual_game.go @@ -33,6 +33,10 @@ type VirtualGameRepository interface { GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error) CreateVirtualGameHistory(ctx context.Context, his *domain.VirtualGameHistory) error + + CreateVirtualGame(ctx context.Context, arg dbgen.CreateVirtualGameParams) (dbgen.VirtualGame, error) + ListAllVirtualGames(ctx context.Context, arg dbgen.GetAllVirtualGamesParams) ([]dbgen.GetAllVirtualGamesRow, error) + RemoveAllVirtualGames(ctx context.Context) error } type VirtualGameRepo struct { @@ -94,8 +98,8 @@ func (r *VirtualGameRepo) CreateVirtualGameProvider(ctx context.Context, arg dbg return r.store.queries.CreateVirtualGameProvider(ctx, arg) } -func (r *VirtualGameRepo) RemoveVirtualGameProvider(ctx context.Context, arg dbgen.CreateVirtualGameProviderParams) (dbgen.VirtualGameProvider, error) { - return r.store.queries.CreateVirtualGameProvider(ctx, arg) +func (r *VirtualGameRepo) RemoveVirtualGameProvider(ctx context.Context, providerID string) error { + return r.store.queries.DeleteVirtualGameProvider(ctx, providerID) } func (r *VirtualGameRepo) DeleteAllVirtualGameProviders(ctx context.Context) error { @@ -300,26 +304,15 @@ func (r *VirtualGameRepo) GetUserGameHistory(ctx context.Context, userID int64) return history, nil } -// func (r *VirtualGameRepo) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error { -// _, tx, err := r.store.BeginTx(ctx) -// if err != nil { -// return err -// } +func (r *VirtualGameRepo) CreateVirtualGame(ctx context.Context, arg dbgen.CreateVirtualGameParams) (dbgen.VirtualGame, error) { + return r.store.queries.CreateVirtualGame(ctx, arg) +} -// txCtx := context.WithValue(ctx, contextTxKey, tx) +func (r *VirtualGameRepo) ListAllVirtualGames(ctx context.Context, arg dbgen.GetAllVirtualGamesParams) ([]dbgen.GetAllVirtualGamesRow, error) { + return r.store.queries.GetAllVirtualGames(ctx, arg) +} -// defer func() { -// if p := recover(); p != nil { -// tx.Rollback(ctx) -// panic(p) -// } -// }() +func (r *VirtualGameRepo) RemoveAllVirtualGames(ctx context.Context) error { + return r.store.queries.DeleteAllVirtualGames(ctx) +} -// err = fn(txCtx) -// if err != nil { -// tx.Rollback(ctx) -// return err -// } - -// return tx.Commit(ctx) -// } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index afa9e11..33e8db8 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -734,12 +734,12 @@ func (s *ServiceImpl) DeleteOddsForEvent(ctx context.Context, eventID string) er return s.store.DeleteOddsForEvent(ctx, eventID) } -func getString(v interface{}) string { - if str, ok := v.(string); ok { - return str - } - return "" -} +// func getString(v interface{}) string { +// if str, ok := v.(string); ok { +// return str +// } +// return "" +// } func getInt(v interface{}) int { if n, ok := v.(float64); ok { @@ -761,17 +761,17 @@ func getMap(v interface{}) map[string]interface{} { return nil } -func getMapArray(v interface{}) []map[string]interface{} { - result := []map[string]interface{}{} - if arr, ok := v.([]interface{}); ok { - for _, item := range arr { - if m, ok := item.(map[string]interface{}); ok { - result = append(result, m) - } - } - } - return result -} +// func getMapArray(v interface{}) []map[string]interface{} { +// result := []map[string]interface{}{} +// if arr, ok := v.([]interface{}); ok { +// for _, item := range arr { +// if m, ok := item.(map[string]interface{}); ok { +// result = append(result, m) +// } +// } +// } +// return result +// } func convertRawMessage(rawMessages []json.RawMessage) ([]map[string]interface{}, error) { var result []map[string]interface{} diff --git a/internal/services/virtualGame/veli/game_orchestration.go b/internal/services/virtualGame/veli/game_orchestration.go index b2ba665..55d312d 100644 --- a/internal/services/virtualGame/veli/game_orchestration.go +++ b/internal/services/virtualGame/veli/game_orchestration.go @@ -77,3 +77,165 @@ func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) return &res, nil } + +func (s *Service) GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error) { + // Build params for repo call + + rows, err := s.repo.ListAllVirtualGames(ctx, params) + if err != nil { + return nil, fmt.Errorf("failed to fetch virtual games: %w", err) + } + + var allGames []domain.UnifiedGame + for _, r := range rows { + // --- Convert nullable Rtp to *float64 --- + var rtpPtr *float64 + if r.Rtp.Valid { + rtpFloat, err := r.Rtp.Float64Value() + if err == nil { + rtpPtr = new(float64) + *rtpPtr = rtpFloat.Float64 + } + } + var betsFloat64 []float64 + for _, bet := range r.Bets { + if bet.Valid { + betFloat, err := bet.Float64Value() + if err == nil { + betsFloat64 = append(betsFloat64, betFloat.Float64) + } + } + } + + allGames = append(allGames, domain.UnifiedGame{ + GameID: r.GameID, + ProviderID: r.ProviderID, + Provider: r.ProviderName, + Name: r.Name, + Category: r.Category.String, + DeviceType: r.DeviceType.String, + Volatility: r.Volatility.String, + RTP: rtpPtr, + HasDemo: r.HasDemo.Bool, + HasFreeBets: r.HasFreeBets.Bool, + Bets: betsFloat64, + Thumbnail: r.Thumbnail.String, + Status: int(r.Status.Int32), // nullable status + }) + } + + return allGames, nil +} + +func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.ProviderRequest, currency string) ([]domain.UnifiedGame, error) { + var allGames []domain.UnifiedGame + + // --- 1. Get providers from external API --- + providersRes, err := s.GetProviders(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to fetch providers: %w", err) + } + + // --- 2. Fetch games for each provider --- + for _, p := range providersRes.Items { + games, err := s.GetGames(ctx, domain.GameListRequest{ + BrandID: s.cfg.VeliGames.BrandID, + ProviderID: p.ProviderID, + Page: req.Page, + Size: req.Size, + }) + if err != nil { + continue // skip failing provider but continue others + } + + for _, g := range games { + unified := domain.UnifiedGame{ + GameID: g.GameID, + ProviderID: g.ProviderID, + Provider: p.ProviderName, + Name: g.Name, + Category: g.Category, + DeviceType: g.DeviceType, + // Volatility: g.Volatility, + // RTP: g.RTP, + HasDemo: g.HasDemoMode, + HasFreeBets: g.HasFreeBets, + } + allGames = append(allGames, unified) + + // --- Save to DB --- + _, _ = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ + GameID: g.GameID, + ProviderID: g.ProviderID, + Name: g.Name, + Category: pgtype.Text{ + String: g.Category, + Valid: g.Category != "", + }, + DeviceType: pgtype.Text{ + String: g.DeviceType, + Valid: g.DeviceType != "", + }, + // Volatility: g.Volatility, + // RTP: g.RTP, + HasDemo: pgtype.Bool{ + Bool: g.HasDemoMode, + Valid: true, + }, + HasFreeBets: pgtype.Bool{ + Bool: g.HasFreeBets, + Valid: true, + }, + // Bets: g.Bets, + // Thumbnail: g.Thumbnail, + // Status: g.Status, + }) + } + } + + // --- 3. Handle PopOK separately --- + popokGames, err := s.virtualGameSvc.ListGames(ctx, currency) + if err != nil { + return nil, fmt.Errorf("failed to fetch PopOK games: %w", err) + } + + for _, g := range popokGames { + unified := domain.UnifiedGame{ + GameID: fmt.Sprintf("popok-%d", g.ID), + ProviderID: "popok", + Provider: "PopOK", + Name: g.GameName, + Category: "Crash", + Bets: g.Bets, + Thumbnail: g.Thumbnail, + Status: g.Status, + } + allGames = append(allGames, unified) + + // --- Convert []float64 to []pgtype.Numeric --- + var betsNumeric []pgtype.Numeric + for _, bet := range g.Bets { + var num pgtype.Numeric + _ = num.Scan(bet) + betsNumeric = append(betsNumeric, num) + } + + // --- Save to DB --- + _, _ = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ + GameID: fmt.Sprintf("popok-%d", g.ID), + ProviderID: "popok", + Name: g.GameName, + Bets: betsNumeric, + Thumbnail: pgtype.Text{ + String: g.Thumbnail, + Valid: g.Thumbnail != "", + }, + Status: pgtype.Int4{ + Int32: int32(g.Status), + Valid: true, + }, + }) + } + + return allGames, nil +} diff --git a/internal/services/virtualGame/veli/port.go b/internal/services/virtualGame/veli/port.go index d1af627..1b56924 100644 --- a/internal/services/virtualGame/veli/port.go +++ b/internal/services/virtualGame/veli/port.go @@ -4,10 +4,13 @@ package veli import ( "context" + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) type VeliVirtualGameService interface { + FetchAndStoreAllVirtualGames(ctx context.Context, req domain.ProviderRequest, currency string) ([]domain.UnifiedGame, error) + GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error) AddProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) GetProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) GetGames(ctx context.Context, req domain.GameListRequest) ([]domain.GameEntity, error) diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 292f4e9..2de2eb4 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -9,6 +9,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" ) @@ -20,6 +21,7 @@ var ( ) type Service struct { + virtualGameSvc virtualgameservice.VirtualGameService repo repository.VirtualGameRepository client *Client walletSvc *wallet.Service @@ -27,8 +29,9 @@ type Service struct { cfg *config.Config } -func New(repo repository.VirtualGameRepository,client *Client, walletSvc *wallet.Service, transferStore wallet.TransferStore, cfg *config.Config) *Service { +func New(virtualGameSvc virtualgameservice.VirtualGameService,repo repository.VirtualGameRepository,client *Client, walletSvc *wallet.Service, transferStore wallet.TransferStore, cfg *config.Config) *Service { return &Service{ + virtualGameSvc: virtualGameSvc, repo: repo, client: client, walletSvc: walletSvc, diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index c18bbe5..9df7279 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -7,8 +7,6 @@ import ( "log" - // "time" - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" betSvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" @@ -157,94 +155,55 @@ func SetupReportandVirtualGameCronJobs( period string }{ { - spec: "*/60 * * * * *", // Every 1 minute for testing + spec: "*/60 * * * * *", // Every 60 seconds for testing period: "test", }, { spec: "0 0 0 * * *", // Daily at midnight period: "daily", }, - { - spec: "0 0 1 * * 0", // Weekly: Sunday at 1 AM - period: "weekly", - }, - { - spec: "0 0 2 1 * *", // Monthly: 1st day of month at 2 AM - period: "monthly", - }, } for _, job := range schedule { period := job.period if _, err := c.AddFunc(job.spec, func() { - now := time.Now() - var from, to time.Time + log.Printf("[%s] Running virtual game fetch & store job...", period) - switch period { - case "daily": - from = time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location()) - to = time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location()) - case "weekly": - weekday := int(now.Weekday()) - daysSinceSunday := (weekday + 7) % 7 - from = time.Date(now.Year(), now.Month(), now.Day()-daysSinceSunday-7, 0, 0, 0, 0, now.Location()) - to = from.AddDate(0, 0, 6).Add(time.Hour*23 + time.Minute*59 + time.Second*59) - case "monthly": - firstOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) - from = firstOfMonth.AddDate(0, -1, 0) - to = firstOfMonth.Add(-time.Second) - default: - log.Printf("Unknown period: %s", period) - // return + brandID := os.Getenv("VELI_BRAND_ID") + if brandID == "" { + log.Println("VELI_BRAND_ID not set, skipping virtual game sync") + return } - // --- Generate Reports (skip for test) --- - if period != "test" { - log.Printf("Running %s report for period %s -> %s", period, from.Format(time.RFC3339), to.Format(time.RFC3339)) + req := domain.ProviderRequest{ + BrandID: brandID, + ExtraData: true, + Size: 1000, + Page: 1, + } + + allGames, err := virtualGameService.FetchAndStoreAllVirtualGames(ctx, req, "ETB") + if err != nil { + log.Printf("[%s] Error fetching/storing virtual games: %v", period, err) + return + } + + log.Printf("[%s] Successfully fetched & stored %d virtual games", period, len(allGames)) + + // --- Generate reports only for daily runs --- + if period == "daily" { + now := time.Now() + from := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location()) + to := time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location()) + + log.Printf("Running daily report for period %s -> %s", from.Format(time.RFC3339), to.Format(time.RFC3339)) if err := reportService.GenerateReport(ctx, from, to); err != nil { - log.Printf("Error generating %s report: %v", period, err) + log.Printf("Error generating daily report: %v", err) } else { - log.Printf("Successfully generated %s report", period) + log.Printf("Successfully generated daily report") } } - - // --- Fetch and Add Virtual Game Providers (daily + test) --- - if period == "daily" || period == "test" { - log.Printf("Fetching and adding virtual game providers (%s)...", period) - - brandID := os.Getenv("VELI_BRAND_ID") - if brandID == "" { - log.Println("VELI_BRAND_ID not set, skipping provider sync") - return - } - - page := 1 - size := 1000 - for { - req := domain.ProviderRequest{ - BrandID: brandID, - ExtraData: true, - Size: int(size), - Page: int(page), - } - - res, err := virtualGameService.AddProviders(ctx, req) - if err != nil { - log.Printf("Error adding virtual game providers on page %d: %v", page, err) - break - } - - log.Printf("[%s] Successfully processed page %d: %d providers", period, page, len(res.Items)) - - if len(res.Items) < size { - // Last page reached - break - } - page++ - } - log.Printf("[%s] Finished fetching and adding virtual game providers", period) - } }); err != nil { log.Fatalf("Failed to schedule %s cron job: %v", period, err) } @@ -254,8 +213,6 @@ func SetupReportandVirtualGameCronJobs( log.Printf("Cron jobs started. Reports will be saved to: %s", outputDir) } - - func ProcessBetCashback(ctx context.Context, betService *betSvc.Service) { c := cron.New(cron.WithSeconds()) diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 31dac1b..2cfd49c 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -161,7 +161,7 @@ func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error { wallet, _ := h.walletSvc.GetCustomerWallet(c.Context(), bet.UserID) // amount added for fast code owner can be fetched from settings in db - settingList, err := h.settingSvc.GetOverrideSettingsList(c.Context(), companyID.Value) + settingList, _ := h.settingSvc.GetOverrideSettingsList(c.Context(), companyID.Value) amount := settingList.AmountForBetReferral _, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, amount, domain.ValidInt64{}, diff --git a/internal/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index 51dd68a..e0914a3 100644 --- a/internal/web_server/handlers/event_handler.go +++ b/internal/web_server/handlers/event_handler.go @@ -23,7 +23,7 @@ import ( // @Param cc query string false "Country Code Filter" // @Param first_start_time query string false "Start Time" // @Param last_start_time query string false "End Time" -// @Success 200 {array} domain.UpcomingEvent +// @Success 200 {array} domain.BaseEvent // @Failure 500 {object} response.APIResponse // @Router /api/v1/events [get] func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { @@ -174,7 +174,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { // @Param cc query string false "Country Code Filter" // @Param first_start_time query string false "Start Time" // @Param last_start_time query string false "End Time" -// @Success 200 {array} domain.UpcomingEvent +// @Success 200 {array} domain.BaseEvent // @Failure 500 {object} response.APIResponse // @Router /api/v1/{tenant_slug}/events [get] func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { @@ -400,7 +400,7 @@ func (h *Handler) GetTopLeagues(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param id path string true "ID" -// @Success 200 {object} domain.UpcomingEvent +// @Success 200 {object} domain.BaseEvent // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/events/{id} [get] @@ -433,7 +433,7 @@ func (h *Handler) GetUpcomingEventByID(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param id path string true "ID" -// @Success 200 {object} domain.UpcomingEvent +// @Success 200 {object} domain.BaseEvent // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/{tenant_slug}events/{id} [get] diff --git a/internal/web_server/handlers/leagues.go b/internal/web_server/handlers/leagues.go index 6755b9a..48274fd 100644 --- a/internal/web_server/handlers/leagues.go +++ b/internal/web_server/handlers/leagues.go @@ -16,7 +16,7 @@ import ( // @Tags leagues // @Accept json // @Produce json -// @Success 200 {array} domain.League +// @Success 200 {array} domain.BaseLeague // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/leagues [get] @@ -102,7 +102,7 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { // @Tags leagues // @Accept json // @Produce json -// @Success 200 {array} domain.League +// @Success 200 {array} domain.BaseLeague // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/{tenant_slug}/leagues [get] diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index 054b3a8..ade2b8b 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -4,8 +4,10 @@ import ( "encoding/json" "errors" "fmt" + "log" "strconv" + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" @@ -22,6 +24,59 @@ type launchVirtualGameRes struct { LaunchURL string `json:"launch_url"` } +// ListVirtualGames godoc +// @Summary List all virtual games +// @Description Returns all virtual games with optional filters (category, search, pagination) +// @Tags VirtualGames - Orchestration +// @Accept json +// @Produce json +// @Param category query string false "Filter by category" +// @Param search query string false "Search by game name" +// @Param limit query int false "Pagination limit" +// @Param offset query int false "Pagination offset" +// @Success 200 {object} domain.Response{data=[]domain.UnifiedGame} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/orchestrator/virtual-games [get] +func (h *Handler) ListVirtualGames(c *fiber.Ctx) error { + // --- Parse query parameters --- + limit := c.QueryInt("limit", 100) + if limit <= 0 { + limit = 100 + } + offset := c.QueryInt("offset", 0) + if offset < 0 { + offset = 0 + } + category := c.Query("category", "") + search := c.Query("search", "") + + params := dbgen.GetAllVirtualGamesParams{ + Column1: category, + Column2: search, + Limit: int32(limit), + Offset: int32(offset), + } + + // --- Call service method --- + games, err := h.veliVirtualGameSvc.GetAllVirtualGames(c.Context(), params) + if err != nil { + log.Println("ListVirtualGames error:", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to fetch virtual games", + Error: err.Error(), + }) + } + + // --- Return response --- + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Virtual games fetched successfully", + Data: games, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + // RemoveProviderHandler // @Summary Remove a virtual game provider // @Description Deletes a provider by provider_id diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index c6c1f7f..789b670 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -71,7 +71,6 @@ func (a *App) initAppRoutes() { groupV1.Post("/direct_deposit", a.authMiddleware, h.InitiateDirectDeposit) groupV1.Post("/direct_deposit/verify", a.authMiddleware, h.VerifyDirectDeposit) groupV1.Get("/direct_deposit/pending", a.authMiddleware, h.GetPendingDirectDeposits) - // Swagger a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) @@ -141,9 +140,6 @@ func (a *App) initAppRoutes() { // groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler) // groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler) - - - // User Routes tenant.Post("/user/resetPassword", h.ResetPassword) tenant.Post("/user/sendResetCode", h.SendResetCode) @@ -358,7 +354,8 @@ func (a *App) initAppRoutes() { groupV1.Delete("/virtual-game/orchestrator/providers/:provideID", a.authMiddleware, h.RemoveProvider) groupV1.Get("/virtual-game/orchestrator/providers/:provideID", a.authMiddleware, h.GetProviderByID) - groupV1.Get("/virtual-game/orchestrator/providers", a.authMiddleware, h.ListProviders) + groupV1.Get("/virtual-game/orchestrator/games", h.ListVirtualGames) + groupV1.Get("/virtual-game/orchestrator/providers", h.ListProviders) groupV1.Patch("/virtual-game/orchestrator/providers/:provideID/status", a.authMiddleware, h.SetProviderEnabled) //Issue Reporting Routes