Merge: resolved conflicts

This commit is contained in:
Yared Yemane 2025-08-01 20:10:47 +03:00
commit 354989e6e1
22 changed files with 1659 additions and 426 deletions

View File

@ -110,8 +110,8 @@ func main() {
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
userSvc := user.NewService(store, store, messengerSvc, cfg)
eventSvc := event.New(cfg.Bet365Token, store)
oddsSvc := odds.New(store, cfg, logger)
eventSvc := event.New(cfg.Bet365Token, store, domain.MongoDBLogger)
oddsSvc := odds.New(store, cfg, logger, domain.MongoDBLogger)
notificationRepo := repository.NewNotificationRepository(store)
virtuaGamesRepo := repository.NewVirtualGameRepository(store)
notificationSvc := notificationservice.New(notificationRepo, domain.MongoDBLogger, logger, cfg, messengerSvc, userSvc)
@ -141,7 +141,7 @@ func main() {
leagueSvc := league.New(store)
ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc)
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, *companySvc, *settingSvc, *userSvc, notificationSvc, logger, domain.MongoDBLogger)
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc)
resultSvc := result.NewService(store, cfg, logger, domain.MongoDBLogger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc, *userSvc)
bonusSvc := bonus.NewService(store)
referalRepo := repository.NewReferralRepository(store)
vitualGameRepo := repository.NewVirtualGameRepository(store)
@ -221,8 +221,8 @@ func main() {
defer exchangeWorker.Stop()
go walletMonitorSvc.Start()
httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc)
httpserver.StartTicketCrons(*ticketSvc)
httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc, domain.MongoDBLogger)
httpserver.StartTicketCrons(*ticketSvc, domain.MongoDBLogger)
issueReportingRepo := repository.NewReportedIssueRepository(store)

View File

@ -220,6 +220,7 @@ INSERT INTO wallets (
is_transferable,
user_id,
is_active,
type
created_at,
updated_at
)
@ -231,6 +232,7 @@ VALUES (
TRUE,
2,
TRUE,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
@ -242,6 +244,7 @@ INSERT INTO branches (
branch_manager_id,
company_id,
is_self_owned,
profit_percent,
created_at,
updated_at
)
@ -253,6 +256,7 @@ values (
2,
1,
TRUE,
0.1,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);

View File

@ -5,12 +5,20 @@ CREATE TABLE IF NOT EXISTS notifications (
type IN (
'cash_out_success',
'deposit_success',
'withdraw_success',
'bet_placed',
'daily_report',
'high_loss_on_bet',
'bet_overload',
'signup_welcome',
'otp_sent'
'otp_sent',
'wallet_threshold',
'transfer_failed',
'transfer_success',
'admin_alert',
'bet_result',
'transfer_rejected',
'approval_required'
)
),
level TEXT NOT NULL CHECK (level IN ('info', 'error', 'warning', 'success')),
@ -29,19 +37,13 @@ CREATE TABLE IF NOT EXISTS notifications (
timestamp TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
metadata JSONB
);
CREATE TABLE IF NOT EXISTS wallet_threshold_notifications (
company_id BIGINT NOT NULL,
threshold FLOAT NOT NULL,
notified_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (company_id, threshold)
);
CREATE INDEX idx_wallet_threshold_notifications_company ON wallet_threshold_notifications(company_id);
CREATE INDEX idx_notifications_recipient_id ON notifications (recipient_id);
CREATE INDEX idx_notifications_timestamp ON notifications (timestamp);
CREATE INDEX idx_notifications_type ON notifications (type);

View File

@ -1,11 +1,20 @@
-- Settings Initial Data
INSERT INTO settings (key, value)
VALUES ('sms_provider', '30'),
<<<<<<< HEAD
('max_number_of_outcomes', '30'),
('bet_amount_limit', '100000'),
=======
('max_number_of_outcomes', '30'),
('bet_amount_limit', '10000000'),
>>>>>>> 7d8d824a94381bd82c40398654c3bd78218c5950
('daily_ticket_limit', '50'),
('total_winnings_limit', '1000000'),
('amount_for_bet_referral', '1000000'),
('cashback_amount_cap', '1000') ON CONFLICT (key) DO
UPDATE
<<<<<<< HEAD
SET value = EXCLUDED.value;
=======
SET value = EXCLUDED.value;
>>>>>>> 7d8d824a94381bd82c40398654c3bd78218c5950

19
db/query/events_stat.sql Normal file
View File

@ -0,0 +1,19 @@
-- name: GetTotalMontlyEventStat :many
SELECT DATE_TRUNC('month', start_time) AS month,
COUNT(*) AS event_count
FROM events
JOIN leagues ON leagues.id = events.league_id
WHERE (
events.is_featured = sqlc.narg('is_event_featured')
OR sqlc.narg('is_event_featured') IS NULL
)
AND (
leagues.is_featured = sqlc.narg('is_league_featured')
OR sqlc.narg('is_league_featured') IS NULL
)
AND (
events.league_id = sqlc.narg('league_id')
OR sqlc.narg('league_id') IS NULL
)
GROUP BY month
ORDER BY month;

64
gen/db/events_stat.sql.go Normal file
View File

@ -0,0 +1,64 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: events_stat.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const GetTotalMontlyEventStat = `-- name: GetTotalMontlyEventStat :many
SELECT DATE_TRUNC('month', start_time) AS month,
COUNT(*) AS event_count
FROM events
JOIN leagues ON leagues.id = events.league_id
WHERE (
events.is_featured = $1
OR $1 IS NULL
)
AND (
leagues.is_featured = $2
OR $2 IS NULL
)
AND (
events.league_id = $3
OR $3 IS NULL
)
GROUP BY month
ORDER BY month
`
type GetTotalMontlyEventStatParams struct {
IsEventFeatured pgtype.Bool `json:"is_event_featured"`
IsLeagueFeatured pgtype.Bool `json:"is_league_featured"`
LeagueID pgtype.Int4 `json:"league_id"`
}
type GetTotalMontlyEventStatRow struct {
Month pgtype.Interval `json:"month"`
EventCount int64 `json:"event_count"`
}
func (q *Queries) GetTotalMontlyEventStat(ctx context.Context, arg GetTotalMontlyEventStatParams) ([]GetTotalMontlyEventStatRow, error) {
rows, err := q.db.Query(ctx, GetTotalMontlyEventStat, arg.IsEventFeatured, arg.IsLeagueFeatured, arg.LeagueID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTotalMontlyEventStatRow
for rows.Next() {
var i GetTotalMontlyEventStatRow
if err := rows.Scan(&i.Month, &i.EventCount); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -1,7 +1,9 @@
package domain
import (
"encoding/json"
"fmt"
"strconv"
"time"
"go.uber.org/zap"
@ -93,3 +95,73 @@ type Pagination struct {
func PtrFloat64(v float64) *float64 { return &v }
func PtrInt64(v int64) *int64 { return &v }
type Int64JSON int64
// func (i *Int64JSON) Parse() (int64, error) {
// var asString string
// if err := json.Unmarshal(i, &asString); err == nil {
// // Try to parse the string to int64
// return strconv.ParseInt(strings.TrimSpace(asString), 10, 64)
// }
// var asInt int64
// if err := json.Unmarshal(i, &asInt); err == nil {
// return asInt, nil
// }
// // Neither string nor int worked
// return 0, fmt.Errorf("invalid int64 value: %s", string(i))
// }
func (i *Int64JSON) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err == nil {
// it was a string, parse it
v, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return err
}
*i = Int64JSON(v)
return nil
}
var v int64
if err := json.Unmarshal(data, &v); err == nil {
*i = Int64JSON(v)
return nil
}
return fmt.Errorf("invalid int64 value: %s", string(data))
}
type NullableInt64JSON struct {
Int64 int64
Valid bool
}
func (n *NullableInt64JSON) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err == nil {
if s == "" {
n.Valid = false
return nil
}
v, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return err
}
n.Int64, n.Valid = v, true
return nil
}
var v int64
if err := json.Unmarshal(data, &v); err == nil {
n.Int64, n.Valid = v, true
return nil
}
return fmt.Errorf("invalid int64 value: %s", string(data))
}

View File

@ -84,6 +84,19 @@ type Notification struct {
Timestamp time.Time `json:"timestamp"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
type CreateNotification struct {
RecipientID int64 `json:"recipient_id"`
Type NotificationType `json:"type"`
Level NotificationLevel `json:"level"`
ErrorSeverity *NotificationErrorSeverity `json:"error_severity"`
Reciever NotificationRecieverSide `json:"reciever"`
IsRead bool `json:"is_read"`
DeliveryStatus NotificationDeliveryStatus `json:"delivery_status,omitempty"`
DeliveryChannel DeliveryChannel `json:"delivery_channel,omitempty"`
Payload NotificationPayload `json:"payload"`
Priority int `json:"priority,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
func (n *Notification) ToJSON() ([]byte, error) {
return json.Marshal(n)

View File

@ -27,7 +27,7 @@ type RawOdd struct {
// The Market ID for the json data can be either string / int which is causing problems when UnMarshalling
type OddsMarket struct {
ID json.RawMessage `json:"id"`
ID NullableInt64JSON `json:"id"`
Name string `json:"name"`
Odds []json.RawMessage `json:"odds"`
Header string `json:"header,omitempty"`

View File

@ -82,3 +82,23 @@ const (
TIME_STATUS_DECIDED_BY_FA TimeStatus = 11
TIME_STATUS_REMOVED TimeStatus = 99
)
type ResultStatusCounts struct {
IsNotFinished int `json:"is_not_finished"`
IsNotFinishedBets int `json:"is_not_finished_bets"`
IsToBeFixed int `json:"is_to_be_fixed"`
IsToBeFixedBets int `json:"is_to_be_fixed_bets"`
IsPostponed int `json:"is_postponed"`
IsPostponedBets int `json:"is_postponed_bets"`
IsEnded int `json:"is_ended"`
IsEndedBets int `json:"is_ended_bets"`
IsRemoved int `json:"is_removed"`
IsRemovedBets int `json:"is_removed_bets"`
}
type ResultStatusBets struct {
IsNotFinished []int64 `json:"is_not_finished"`
IsToBeFixed []int64 `json:"is_to_be_fixed"`
IsPostponed []int64 `json:"is_postponed"`
IsEnded []int64 `json:"is_ended"`
IsRemoved []int64 `json:"is_removed"`
}

View File

@ -18,7 +18,7 @@ type LeagueRes struct {
type Team struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
ImageID NullableInt64JSON `json:"image_id"`
CC string `json:"cc"`
}
@ -31,7 +31,7 @@ type CommonResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
TimeStatus NullableInt64JSON `json:"time_status"`
League LeagueRes `json:"league"`
Home Team `json:"home"`
Away Team `json:"away"`

View File

@ -32,3 +32,81 @@ const (
MMA = 162
SURFING = 148
)
func (s Sport) String() string {
switch s {
case FOOTBALL:
return "FOOTBALL"
case BASKETBALL:
return "BASKETBALL"
case VOLLEYBALL:
return "VOLLEYBALL"
case HANDBALL:
return "HANDBALL"
case BASEBALL:
return "BASEBALL"
case HORSE_RACING:
return "HORSE_RACING"
case GREYHOUNDS:
return "GREYHOUNDS"
case ICE_HOCKEY:
return "ICE_HOCKEY"
case SNOOKER:
return "SNOOKER"
case AMERICAN_FOOTBALL:
return "AMERICAN_FOOTBALL"
case CRICKET:
return "CRICKET"
case FUTSAL:
return "FUTSAL"
case DARTS:
return "DARTS"
case TABLE_TENNIS:
return "TABLE_TENNIS"
case BADMINTON:
return "BADMINTON"
case RUGBY_UNION:
return "RUGBY_UNION"
case RUGBY_LEAGUE:
return "RUGBY_LEAGUE"
case AUSTRALIAN_RULES:
return "AUSTRALIAN_RULES"
case BOWLS:
return "BOWLS"
case BOXING:
return "BOXING"
case GAELIC_SPORTS:
return "GAELIC_SPORTS"
case FLOORBALL:
return "FLOORBALL"
case BEACH_VOLLEYBALL:
return "BEACH_VOLLEYBALL"
case WATER_POLO:
return "WATER_POLO"
case SQUASH:
return "SQUASH"
case E_SPORTS:
return "E_SPORTS"
case MMA:
return "MMA"
case SURFING:
return "SURFING"
default:
return "Unknown"
}
}
func IsValidSport(s Sport) bool {
switch s {
case
FOOTBALL, BASKETBALL, VOLLEYBALL, HANDBALL, BASEBALL,
HORSE_RACING, GREYHOUNDS, ICE_HOCKEY, SNOOKER, AMERICAN_FOOTBALL,
CRICKET, FUTSAL, DARTS, TABLE_TENNIS, BADMINTON,
RUGBY_UNION, RUGBY_LEAGUE, AUSTRALIAN_RULES, BOWLS, BOXING,
GAELIC_SPORTS, FLOORBALL, BEACH_VOLLEYBALL, WATER_POLO,
SQUASH, E_SPORTS, MMA, SURFING:
return true
default:
return false
}
}

View File

@ -88,7 +88,6 @@ func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error)
}, nil
}
func (s *Store) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) {
fmt.Printf("\n\nuser_filter %v \n\n", filter)
users, err := s.queries.GetAllUsers(ctx, dbgen.GetAllUsersParams{
Role: filter.Role,
CompanyID: pgtype.Int8{

View File

@ -874,6 +874,10 @@ func (s *Service) GetBetOutcomeByBetID(ctx context.Context, UserID int64) ([]dom
return s.betStore.GetBetOutcomeByBetID(ctx, UserID)
}
func (s *Service) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) {
return s.betStore.GetBetOutcomeByEventID(ctx, eventID, is_filtered)
}
func (s *Service) GetBetByFastCode(ctx context.Context, fastcode string) (domain.GetBet, error) {
return s.betStore.GetBetByFastCode(ctx, fastcode)
}
@ -900,22 +904,19 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
return err
}
switch {
case bet.IsShopBet:
return s.betStore.UpdateStatus(ctx, id, status)
case status == domain.OUTCOME_STATUS_ERROR, status == domain.OUTCOME_STATUS_PENDING:
s.SendErrorStatusNotification(ctx, status, bet.UserID, "")
if status == domain.OUTCOME_STATUS_ERROR || status == domain.OUTCOME_STATUS_PENDING {
s.SendAdminErrorAlertNotification(ctx, status, "")
s.SendErrorStatusNotification(ctx, status, bet.UserID, "")
s.mongoLogger.Error("Bet Status is error",
zap.Int64("bet_id", id),
zap.Error(err),
)
return s.betStore.UpdateStatus(ctx, id, status)
case status == domain.OUTCOME_STATUS_LOSS:
s.SendLosingStatusNotification(ctx, status, bet.UserID, "")
return s.betStore.UpdateStatus(ctx, id, status)
}
if bet.IsShopBet {
return s.betStore.UpdateStatus(ctx, id, status)
}
customerWallet, err := s.walletSvc.GetCustomerWallet(ctx, id)
if err != nil {
s.mongoLogger.Error("failed to get customer wallet",
@ -927,6 +928,9 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
var amount domain.Currency
switch status {
case domain.OUTCOME_STATUS_LOSS:
s.SendLosingStatusNotification(ctx, status, bet.UserID, "")
return s.betStore.UpdateStatus(ctx, id, status)
case domain.OUTCOME_STATUS_WIN:
amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds)
s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "")
@ -1120,7 +1124,19 @@ func (s *Service) SendAdminErrorAlertNotification(ctx context.Context, status do
}`, status, extra),
}
users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleSuperAdmin),
})
if err != nil {
s.mongoLogger.Error("failed to get super_admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleAdmin),
})
@ -1132,6 +1148,8 @@ func (s *Service) SendAdminErrorAlertNotification(ctx context.Context, status do
return err
}
users := append(super_admin_users, admin_users...)
for _, user := range users {
betNotification.RecipientID = user.ID
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {

View File

@ -14,18 +14,21 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"go.uber.org/zap"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
)
type service struct {
token string
store *repository.Store
mongoLogger *zap.Logger
}
func New(token string, store *repository.Store) Service {
func New(token string, store *repository.Store, mongoLogger *zap.Logger) Service {
return &service{
token: token,
store: store,
mongoLogger: mongoLogger,
}
}
@ -202,7 +205,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
return nil
}
func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, source string) {
func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_url, source string) {
sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91}
// sportIDs := []int{1}
// TODO: Add the league skipping again when we have dynamic leagues
@ -217,14 +220,24 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
var page int = 0
var limit int = 200
var count int = 0
var skippedLeague []string
var totalEvents = 0
for page <= totalPages {
page = page + 1
url := fmt.Sprintf(url, sportID, s.token, page)
url := fmt.Sprintf(source_url, sportID, s.token, page)
log.Printf("📡 Fetching data from %s - sport %d (%d/%d), for event data page (%d/%d)",
source, sportID, sportIndex+1, len(sportIDs), page, totalPages)
resp, err := http.Get(url)
if err != nil {
log.Printf("❌ Failed to fetch event data for page %d: %v", page, err)
s.mongoLogger.Error(
"Failed to fetch event data for page",
zap.String("source", source),
zap.Int("sport_id", sportID),
zap.Int("page", page),
zap.Int("total_pages", totalPages),
zap.Error(err),
)
continue
}
defer resp.Body.Close()
@ -233,21 +246,42 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
var data domain.BetResult
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
log.Printf("❌ Failed to parse json data")
s.mongoLogger.Error(
"Failed to parse event json data",
zap.String("source", source),
zap.Int("sport_id", sportID),
zap.Int("page", page),
zap.Int("total_pages", totalPages),
zap.Error(err),
)
continue
}
var skippedLeague []string
for _, ev := range data.Results {
startUnix, _ := strconv.ParseInt(ev.Time, 10, 64)
// eventID, err := strconv.ParseInt(ev.ID, 10, 64)
// if err != nil {
// log.Panicf("❌ Invalid event id, eventID %v", ev.ID)
// continue
// }
for _, ev := range data.Results {
startUnix, err := strconv.ParseInt(ev.Time, 10, 64)
if err != nil {
s.mongoLogger.Error(
"Invalid time",
zap.String("time", ev.Time),
zap.String("source", source),
zap.Int("sport_id", sportID),
zap.Int("page", page),
zap.Int("total_pages", totalPages),
zap.Error(err),
)
continue
}
leagueID, err := strconv.ParseInt(ev.League.ID, 10, 64)
if err != nil {
log.Printf("❌ Invalid league id, leagueID %v", ev.League.ID)
s.mongoLogger.Error(
"Invalid league id",
zap.String("leagueID", ev.League.ID),
zap.String("source", source),
zap.Int("sport_id", sportID),
zap.Int("page", page),
zap.Int("total_pages", totalPages),
zap.Error(err),
)
continue
}
@ -264,13 +298,26 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
})
if err != nil {
log.Printf("❌ Error Saving League on %v", ev.League.Name)
log.Printf("err:%v", err)
s.mongoLogger.Error(
"error while saving league",
zap.String("leagueID", ev.League.ID),
zap.String("leagueName", ev.League.Name),
zap.String("source", source),
zap.Int("sport_id", sportID),
zap.Int("page", page),
zap.Int("total_pages", totalPages),
zap.Error(err),
)
continue
}
if supported, err := s.store.CheckLeagueSupport(ctx, leagueID); !supported || err != nil {
log.Printf("Skipping league %v", ev.League.Name)
s.mongoLogger.Warn(
"Skipping league",
zap.String("league", ev.League.Name),
zap.Bool("is_supported", supported),
zap.Error(err),
)
skippedLeague = append(skippedLeague, ev.League.Name)
continue
}
@ -300,12 +347,23 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
err = s.store.SaveUpcomingEvent(ctx, event)
if err != nil {
log.Printf("❌ Failed to save upcoming event %s", event.ID)
s.mongoLogger.Error(
"failed to save upcoming event",
zap.String("leagueID", ev.League.ID),
zap.String("leagueName", ev.League.Name),
zap.String("source", source),
zap.Int("sport_id", sportID),
zap.Int("page", page),
zap.Int("total_pages", totalPages),
zap.Error(err),
)
}
totalEvents += 1
}
log.Printf("⚠️ Skipped leagues %v", len(skippedLeague))
log.Printf("⚠️ Total pages %v", data.Pager.Total/data.Pager.PerPage)
// log.Printf("⚠️ Skipped leagues %v", len(skippedLeague))
// log.Printf("⚠️ Total pages %v", data.Pager.Total/data.Pager.PerPage)
totalPages = data.Pager.Total / data.Pager.PerPage
if count >= limit {
@ -316,6 +374,17 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
}
count++
}
s.mongoLogger.Info(
"Successfully fetched upcoming events",
zap.String("source", source),
zap.Int("totalEvents", totalEvents),
zap.Int("sport_id", sportID),
zap.String("sport_name", domain.Sport(sportID).String()),
zap.Int("page", page),
zap.Int("total_pages", totalPages),
zap.Int("Skipped leagues", len(skippedLeague)),
)
}
}

View File

@ -93,6 +93,7 @@ func (s *Service) addConnection(recipientID int64, c *websocket.Conn) error {
}
func (s *Service) SendNotification(ctx context.Context, notification *domain.Notification) error {
notification.ID = helpers.GenerateID()
notification.Timestamp = time.Now()
notification.DeliveryStatus = domain.DeliveryStatusPending
@ -642,5 +643,3 @@ func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error)
// // Broadcast over WebSocket
// s.Hub.Broadcast <- event
// }

View File

@ -16,20 +16,23 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"go.uber.org/zap"
)
type ServiceImpl struct {
store *repository.Store
config *config.Config
logger *slog.Logger
mongoLogger *zap.Logger
client *http.Client
}
func New(store *repository.Store, cfg *config.Config, logger *slog.Logger) *ServiceImpl {
func New(store *repository.Store, cfg *config.Config, logger *slog.Logger, mongoLogger *zap.Logger) *ServiceImpl {
return &ServiceImpl{
store: store,
config: cfg,
logger: logger,
mongoLogger: mongoLogger,
client: &http.Client{Timeout: 10 * time.Second},
}
}
@ -75,7 +78,10 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error {
eventIDs, err := s.store.GetAllUpcomingEvents(ctx)
if err != nil {
log.Printf("❌ Failed to fetch upcoming event IDs: %v", err)
s.mongoLogger.Error(
"Failed to fetch upcoming event IDs",
zap.Error(err),
)
return err
}
@ -86,34 +92,59 @@ func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error {
oddsData, err := s.FetchNonLiveOddsByEventID(ctx, event.ID)
if err != nil || oddsData.Success != 1 {
s.logger.Error("Failed to fetch prematch odds", "eventID", event.ID, "error", err)
s.mongoLogger.Error(
"Failed to fetch prematch odds",
zap.String("eventID", event.ID),
zap.Error(err),
)
errs = append(errs, fmt.Errorf("failed to fetch prematch odds for event %v: %w", event.ID, err))
continue
}
parsedOddSections, err := s.ParseOddSections(ctx, oddsData.Results[0], event.SportID)
if err != nil {
s.logger.Error("Failed to parse odd section", "error", err)
s.mongoLogger.Error(
"Failed to parse odd section",
zap.String("eventID", event.ID),
zap.Int32("sportID", event.SportID),
zap.Error(err),
)
errs = append(errs, fmt.Errorf("failed to parse odd section for event %v: %w", event.ID, err))
continue
}
if parsedOddSections.EventFI == "" {
s.logger.Error("Skipping result with no valid Event FI field", "fi", parsedOddSections.EventFI)
s.mongoLogger.Error(
"Skipping result with no valid Event FI field",
zap.String("FI", parsedOddSections.EventFI),
zap.String("eventID", event.ID),
zap.Int32("sportID", event.SportID),
zap.Error(err),
)
errs = append(errs, errors.New("event FI is empty"))
continue
}
for oddCategory, section := range parsedOddSections.Sections {
if err := s.storeSection(ctx, event.ID, parsedOddSections.EventFI, oddCategory, section); err != nil {
s.logger.Error("Error storing odd section", "eventID", event.ID, "odd", oddCategory)
log.Printf("⚠️ Error when storing %v", err)
s.mongoLogger.Error(
"Error storing odd section",
zap.String("eventID", event.ID),
zap.String("odd", oddCategory),
zap.Int32("sportID", event.SportID),
zap.Error(err),
)
errs = append(errs, err)
}
}
for _, section := range parsedOddSections.OtherRes {
if err := s.storeSection(ctx, event.ID, parsedOddSections.EventFI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
s.mongoLogger.Error(
"Error storing odd other section",
zap.String("eventID", event.ID),
zap.Int32("sportID", event.SportID),
zap.Error(err),
)
errs = append(errs, err)
continue
}
@ -138,20 +169,32 @@ func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error {
url := fmt.Sprintf("https://api.b365api.com/v1/bwin/prematch?sport_id=%d&token=%s", sportId, s.config.Bet365Token)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
log.Printf("❌ Failed to create request for sportId %d: %v", sportId, err)
s.mongoLogger.Error(
"Failed to create request for sportId",
zap.Int("sportID", sportId),
zap.Error(err),
)
continue
}
resp, err := s.client.Do(req)
if err != nil {
log.Printf("❌ Failed to fetch request for sportId %d: %v", sportId, err)
s.mongoLogger.Error(
"Failed to fetch request for sportId",
zap.Int("sportID", sportId),
zap.Error(err),
)
continue
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("❌ Failed to read response body for sportId %d: %v", sportId, err)
s.mongoLogger.Error(
"Failed to read response body for sportId",
zap.Int("sportID", sportId),
zap.Error(err),
)
continue
}
@ -162,6 +205,11 @@ func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error {
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
fmt.Printf("Decode failed for sport_id=%d\nRaw: %s\n", sportId, string(body))
s.mongoLogger.Error(
"Failed to decode BWin response body",
zap.Int("sportID", sportId),
zap.Error(err),
)
continue
}
@ -188,6 +236,12 @@ func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error {
if err := s.store.SaveEvent(ctx, event); err != nil {
fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err)
s.mongoLogger.Error(
"Could not store live event",
zap.Int("sportID", sportId),
zap.String("eventID", event.ID),
zap.Error(err),
)
continue
}
@ -222,6 +276,11 @@ func (s *ServiceImpl) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr
eventID, err := strconv.ParseInt(eventIDStr, 10, 64)
if err != nil {
s.logger.Error("Failed to parse event id")
s.mongoLogger.Error(
"Failed to parse event id",
zap.String("eventID", eventIDStr),
zap.Error(err),
)
return domain.BaseNonLiveOddResponse{}, err
}
@ -229,26 +288,42 @@ func (s *ServiceImpl) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
log.Printf("❌ Failed to create request for event %d: %v", eventID, err)
s.mongoLogger.Error(
"Failed to create request for event",
zap.String("eventID", eventIDStr),
zap.Error(err),
)
}
resp, err := s.client.Do(req)
if err != nil {
log.Printf("❌ Failed to fetch prematch odds for event %d: %v", eventID, err)
s.mongoLogger.Error(
"Failed to fetch prematch odds for event",
zap.String("eventID", eventIDStr),
zap.Error(err),
)
return domain.BaseNonLiveOddResponse{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("❌ Failed to read response body for event %d: %v", eventID, err)
s.mongoLogger.Error(
"Failed to read response body for event",
zap.String("eventID", eventIDStr),
zap.Error(err),
)
return domain.BaseNonLiveOddResponse{}, err
}
var oddsData domain.BaseNonLiveOddResponse
if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 {
log.Printf("❌ Invalid prematch data for event %d", eventID)
s.mongoLogger.Error(
"Invalid prematch data for event",
zap.String("eventID", eventIDStr),
zap.Error(err),
)
return domain.BaseNonLiveOddResponse{}, err
}
@ -263,7 +338,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage,
case domain.FOOTBALL:
var footballRes domain.FootballOddsResponse
if err := json.Unmarshal(res, &footballRes); err != nil {
s.logger.Error("Failed to unmarshal football result", "error", err)
s.mongoLogger.Error(
"Failed to unmarshal football result",
zap.Error(err),
)
return domain.ParseOddSectionsRes{}, err
}
eventFI = footballRes.FI
@ -276,7 +354,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage,
case domain.BASKETBALL:
var basketballRes domain.BasketballOddsResponse
if err := json.Unmarshal(res, &basketballRes); err != nil {
s.logger.Error("Failed to unmarshal basketball result", "error", err)
s.mongoLogger.Error(
"Failed to unmarshal basketball result",
zap.Error(err),
)
return domain.ParseOddSectionsRes{}, err
}
eventFI = basketballRes.FI
@ -291,7 +372,11 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage,
case domain.ICE_HOCKEY:
var iceHockeyRes domain.IceHockeyOddsResponse
if err := json.Unmarshal(res, &iceHockeyRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
s.mongoLogger.Error(
"Failed to unmarshal ice hockey result",
zap.Error(err),
)
return domain.ParseOddSectionsRes{}, err
}
eventFI = iceHockeyRes.FI
@ -304,7 +389,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage,
case domain.CRICKET:
var cricketRes domain.CricketOddsResponse
if err := json.Unmarshal(res, &cricketRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
s.mongoLogger.Error(
"Failed to unmarshal cricket result",
zap.Error(err),
)
return domain.ParseOddSectionsRes{}, err
}
eventFI = cricketRes.FI
@ -320,7 +408,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage,
case domain.VOLLEYBALL:
var volleyballRes domain.VolleyballOddsResponse
if err := json.Unmarshal(res, &volleyballRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
s.mongoLogger.Error(
"Failed to unmarshal volleyball result",
zap.Error(err),
)
return domain.ParseOddSectionsRes{}, err
}
eventFI = volleyballRes.FI
@ -331,7 +422,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage,
case domain.DARTS:
var dartsRes domain.DartsOddsResponse
if err := json.Unmarshal(res, &dartsRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
s.mongoLogger.Error(
"Failed to unmarshal darts result",
zap.Error(err),
)
return domain.ParseOddSectionsRes{}, err
}
eventFI = dartsRes.FI
@ -345,7 +439,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage,
case domain.FUTSAL:
var futsalRes domain.FutsalOddsResponse
if err := json.Unmarshal(res, &futsalRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
s.mongoLogger.Error(
"Failed to unmarshal futsal result",
zap.Error(err),
)
return domain.ParseOddSectionsRes{}, err
}
eventFI = futsalRes.FI
@ -357,7 +454,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage,
case domain.AMERICAN_FOOTBALL:
var americanFootballRes domain.AmericanFootballOddsResponse
if err := json.Unmarshal(res, &americanFootballRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
s.mongoLogger.Error(
"Failed to unmarshal american football result",
zap.Error(err),
)
return domain.ParseOddSectionsRes{}, err
}
eventFI = americanFootballRes.FI
@ -370,7 +470,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage,
case domain.RUGBY_LEAGUE:
var rugbyLeagueRes domain.RugbyLeagueOddsResponse
if err := json.Unmarshal(res, &rugbyLeagueRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
s.mongoLogger.Error(
"Failed to unmarshal rugby league result",
zap.Error(err),
)
return domain.ParseOddSectionsRes{}, err
}
eventFI = rugbyLeagueRes.FI
@ -386,7 +489,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage,
case domain.RUGBY_UNION:
var rugbyUnionRes domain.RugbyUnionOddsResponse
if err := json.Unmarshal(res, &rugbyUnionRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
s.mongoLogger.Error(
"Failed to unmarshal rugby union result",
zap.Error(err),
)
return domain.ParseOddSectionsRes{}, err
}
eventFI = rugbyUnionRes.FI
@ -401,7 +507,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage,
case domain.BASEBALL:
var baseballRes domain.BaseballOddsResponse
if err := json.Unmarshal(res, &baseballRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
s.mongoLogger.Error(
"Failed to unmarshal baseball result",
zap.Error(err),
)
return domain.ParseOddSectionsRes{}, err
}
eventFI = baseballRes.FI
@ -433,23 +542,19 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName
}
// Check if the market id is a string
var marketIDstr string
err := json.Unmarshal(market.ID, &marketIDstr)
var marketIDint int64
if err != nil {
// check if its int
err := json.Unmarshal(market.ID, &marketIDint)
if err != nil {
s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name)
continue
}
} else {
marketIDint, err = strconv.ParseInt(marketIDstr, 10, 64)
if err != nil {
s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name)
continue
}
}
marketIDint := market.ID.Int64
// if err != nil {
// s.mongoLogger.Error(
// "Invalid market id",
// zap.Int64("market_id", marketIDint),
// zap.String("market_name", market.Name),
// zap.String("eventID", eventID),
// zap.Error(err),
// )
// continue
// }
marketIDstr := strconv.FormatInt(marketIDint, 10)
isSupported, ok := domain.SupportedMarkets[marketIDint]
@ -460,7 +565,13 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName
marketOdds, err := convertRawMessage(market.Odds)
if err != nil {
s.logger.Error("failed to conver json.RawMessage to []map[string]interface{} for market_id: ", market.ID)
s.mongoLogger.Error(
"failed to convert market.Odds to json.RawMessage to []map[string]interface{}",
zap.String("market_id", marketIDstr),
zap.String("market_name", market.Name),
zap.String("eventID", eventID),
zap.Error(err),
)
errs = append(errs, err)
continue
}
@ -480,7 +591,13 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName
err = s.store.SaveNonLiveMarket(ctx, marketRecord)
if err != nil {
s.logger.Error("failed to save market", "market_id", market.ID, "error", err)
s.mongoLogger.Error(
"failed to save market",
zap.String("market_id", marketIDstr),
zap.String("market_name", market.Name),
zap.String("eventID", eventID),
zap.Error(err),
)
errs = append(errs, fmt.Errorf("market %s: %w", market.ID, err))
continue
}

View File

@ -190,7 +190,7 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS)
if err != nil {
fmt.Printf("multi outcome check error")
return domain.OUTCOME_STATUS_PENDING, err
return domain.OUTCOME_STATUS_ERROR, err
}
}
} else if adjustedHomeScore < adjustedAwayScore {
@ -203,7 +203,7 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS)
if err != nil {
fmt.Printf("multi outcome check error")
return domain.OUTCOME_STATUS_PENDING, err
return domain.OUTCOME_STATUS_ERROR, err
}
}
}
@ -211,7 +211,7 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID)
if err != nil {
fmt.Printf("multi outcome check error")
return domain.OUTCOME_STATUS_PENDING, err
return domain.OUTCOME_STATUS_ERROR, err
}
}
}
@ -510,7 +510,7 @@ func evaluateTotalOverUnder(outcome domain.BetOutcome, score struct{ Home, Away
func evaluateTotalLegs(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
total, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid : %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid : %s", outcome.OddName)
}
totalLegs := float64(score.Home + score.Away)
@ -528,7 +528,7 @@ func evaluateTotalLegs(outcome domain.BetOutcome, score struct{ Home, Away int }
return domain.OUTCOME_STATUS_LOSS, nil
}
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
// Result and Total betting is a type of bet where the bettor predicts
@ -736,16 +736,16 @@ func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away i
func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home, Away int }, fullTimeScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
halfWins := strings.Split(outcome.OddName, "-")
if len(halfWins) != 2 {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
firstHalfWinner := strings.TrimSpace(halfWins[0])
fullTimeWinner := strings.TrimSpace(halfWins[1])
if firstHalfWinner != outcome.HomeTeamName && firstHalfWinner != outcome.AwayTeamName && firstHalfWinner != "Tie" {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", firstHalfWinner)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner)
}
if fullTimeWinner != outcome.HomeTeamName && fullTimeWinner != outcome.AwayTeamName && fullTimeWinner != "Tie" {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", firstHalfWinner)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner)
}
switch {
@ -908,7 +908,7 @@ func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Awa
func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
if len(outcome.OddName) < 1 {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
}
isGtr := false
@ -920,7 +920,7 @@ func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away i
margin, err := strconv.ParseInt(outcome.OddName[:idx], 10, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
}
switch outcome.OddHeader {
@ -992,7 +992,7 @@ func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Hom
return domain.OUTCOME_STATUS_LOSS, nil
}
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
}
func evaluateVolleyballGamelines(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
@ -1000,7 +1000,7 @@ func evaluateVolleyballGamelines(outcome domain.BetOutcome, score struct{ Home,
case "Total":
return evaluateTotalOverUnder(outcome, score)
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -304,8 +304,12 @@ func (s *Service) GetAdminNotificationRecipients(ctx context.Context, walletID i
}
func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWallet domain.Wallet) error {
errorSeverity := domain.NotificationErrorSeverityLow
// Send notification to admin team
adminNotification := &domain.Notification{
ErrorSeverity: &errorSeverity,
IsRead: false,
DeliveryStatus: domain.DeliveryStatusPending,
RecipientID: adminWallet.UserID,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelWarning,
@ -363,8 +367,12 @@ func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWalle
func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, adminWallet domain.Wallet, amount domain.Currency) error {
errorSeverity := domain.NotificationErrorSeverityLow
// Send notification to admin team
adminNotification := &domain.Notification{
ErrorSeverity: &errorSeverity,
IsRead: false,
DeliveryStatus: domain.DeliveryStatusPending,
RecipientID: adminWallet.UserID,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelError,
@ -423,8 +431,12 @@ func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, a
}
func (s *Service) SendCustomerWalletInsufficientNotification(ctx context.Context, customerWallet domain.Wallet, amount domain.Currency) error {
errorSeverity := domain.NotificationErrorSeverityLow
// Send notification to admin team
customerNotification := &domain.Notification{
ErrorSeverity: &errorSeverity,
IsRead: false,
DeliveryStatus: domain.DeliveryStatusPending,
RecipientID: customerWallet.UserID,
Type: domain.NOTIFICATION_TYPE_WALLET,
Level: domain.NotificationLevelError,

View File

@ -15,69 +15,85 @@ import (
resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/robfig/cron/v3"
"go.uber.org/zap"
)
func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.ServiceImpl, resultService *resultsvc.Service) {
func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.ServiceImpl, resultService *resultsvc.Service, mongoLogger *zap.Logger) {
c := cron.New(cron.WithSeconds())
schedule := []struct {
spec string
task func()
}{
{
spec: "0 0 * * * *", // Every 1 hour
task: func() {
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
log.Printf("FetchUpcomingEvents error: %v", err)
}
},
},
{
spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
task: func() {
if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
log.Printf("FetchNonLiveOdds error: %v", err)
}
},
},
{
spec: "0 */5 * * * *", // Every 5 Minutes
task: func() {
log.Println("Updating expired events status...")
if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil {
log.Printf("Failed to update events: %v", err)
} else {
log.Printf("Successfully updated expired events")
}
},
},
// {
// spec: "0 0 * * * *", // Every 1 hour
// task: func() {
// mongoLogger.Info("Began fetching upcoming events")
// if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
// mongoLogger.Error("Failed to fetch upcoming events",
// zap.Error(err),
// )
// } else {
// mongoLogger.Info("Successfully fetched upcoming events")
// }
// },
// },
// {
// spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
// task: func() {
// mongoLogger.Info("Began fetching non live odds")
// if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
// mongoLogger.Error("Failed to fetch non live odds",
// zap.Error(err),
// )
// } else {
// mongoLogger.Info("Successfully fetched non live odds")
// }
// },
// },
// {
// spec: "0 */5 * * * *", // Every 5 Minutes
// task: func() {
// mongoLogger.Info("Began updating all expired events status")
// if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil {
// mongoLogger.Error("Failed to update expired events status",
// zap.Error(err),
// )
// } else {
// mongoLogger.Info("Successfully updated expired events")
// }
// },
// },
{
spec: "0 */15 * * * *", // Every 15 Minutes
task: func() {
log.Println("Fetching results for upcoming events...")
mongoLogger.Info("Fetching results for upcoming events")
if err := resultService.FetchAndProcessResults(context.Background()); err != nil {
log.Printf("Failed to process result: %v", err)
mongoLogger.Error("Failed to process result",
zap.Error(err),
)
} else {
log.Printf("Successfully processed all outcomes")
mongoLogger.Info("Successfully processed all event result outcomes")
}
},
},
}
for _, job := range schedule {
// job.task()
job.task()
if _, err := c.AddFunc(job.spec, job.task); err != nil {
log.Fatalf("Failed to schedule cron job: %v", err)
mongoLogger.Error("Failed to schedule data fetching cron job",
zap.Error(err),
)
}
}
c.Start()
log.Println("Cron jobs started for event and odds services")
mongoLogger.Info("Cron jobs started for event and odds services")
}
func StartTicketCrons(ticketService ticket.Service) {
func StartTicketCrons(ticketService ticket.Service, mongoLogger *zap.Logger) {
c := cron.New(cron.WithSeconds())
schedule := []struct {
@ -87,11 +103,13 @@ func StartTicketCrons(ticketService ticket.Service) {
{
spec: "0 0 * * * *", // Every hour
task: func() {
log.Println("Deleting old tickets...")
mongoLogger.Info("Deleting old tickets")
if err := ticketService.DeleteOldTickets(context.Background()); err != nil {
log.Printf("Failed to remove old ticket: %v", err)
mongoLogger.Error("Failed to remove old ticket",
zap.Error(err),
)
} else {
log.Printf("Successfully deleted old tickets")
mongoLogger.Info("Successfully deleted old tickets")
}
},
},
@ -99,12 +117,14 @@ func StartTicketCrons(ticketService ticket.Service) {
for _, job := range schedule {
if _, err := c.AddFunc(job.spec, job.task); err != nil {
log.Fatalf("Failed to schedule cron job: %v", err)
mongoLogger.Error("Failed to schedule ticket cron job",
zap.Error(err),
)
}
}
c.Start()
log.Println("Cron jobs started for ticket service")
mongoLogger.Info("Cron jobs started for ticket service")
}
func SetupReportCronJobs(ctx context.Context, reportService *report.Service) {

View File

@ -70,7 +70,8 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error {
rawOdds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketID, upcomingID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get raw odds by market ID",
// Lets turn this into a warn because this is constantly going off
h.mongoLoggerSvc.Warn("Failed to get raw odds by market ID",
zap.String("marketID", marketID),
zap.String("upcomingID", upcomingID),
zap.Int("status_code", fiber.StatusInternalServerError),