diff --git a/cmd/main.go b/cmd/main.go index cd98778..1ff0cb1 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,15 +3,21 @@ package main import ( // "context" + // "context" "fmt" "log/slog" "os" + "time" "github.com/go-playground/validator/v10" // "github.com/gofiber/fiber/v2" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger" + + // mongologger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger" + + // "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger" mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email" mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" @@ -27,6 +33,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" @@ -35,6 +42,7 @@ import ( alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor" // "github.com/SamuelTariku/FortuneBet-Backend/internal/utils" httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server" @@ -70,6 +78,32 @@ func main() { } logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel) + + // mongologger.Init() + + // client := mongoLogger.InitDB() + // defer func() { + // if err := client.Disconnect(context.Background()); err != nil { + // slog.Error("Failed to disconnect MongoDB", "error", err) + // } + // }() + + // // 2. Create MongoDB logger handler + // handler, err := mongoLogger.NewMongoHandler("logs", "app_logs", slog.LevelDebug) + // if err != nil { + // slog.Error("Failed to create MongoDB logger", "error", err) + // os.Exit(1) + // } + + // // 3. Set as default logger + // tempLogger := slog.New(handler) + // slog.SetDefault(tempLogger) + + // // 4. Log examples + // tempLogger.Info("Application started", "version", "1.0.0") + // slog.Warn("Low disk space", "available_gb", 12.5) + // slog.Error("Payment failed", "transaction_id", "tx123", "error", "insufficient funds") + store := repository.NewStore(db) v := customvalidator.NewCustomValidator(validator.New()) @@ -82,18 +116,33 @@ func main() { eventSvc := event.New(cfg.Bet365Token, store) oddsSvc := odds.New(store, cfg, logger) ticketSvc := ticket.NewService(store) - walletSvc := wallet.NewService(store, store) + notificationRepo := repository.NewNotificationRepository(store) + + notificationSvc := notificationservice.New(notificationRepo, logger, cfg) + + var betStore bet.BetStore + var walletStore wallet.WalletStore + var transactionStore transaction.TransactionStore + var branchStore branch.BranchStore + var userStore user.UserStore + var notificationStore notificationservice.NotificationStore + + walletSvc := wallet.NewService( + wallet.WalletStore(store), + wallet.TransferStore(store), + notificationStore, + logger, + ) + transactionSvc := transaction.NewService(store) branchSvc := branch.NewService(store) companySvc := company.NewService(store) betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger) resultSvc := result.NewService(store, cfg, logger, *betSvc) - notificationRepo := repository.NewNotificationRepository(store) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) recommendationRepo := repository.NewRecommendationRepository(store) - notificationSvc := notificationservice.New(notificationRepo, logger, cfg) referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger) virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) aleaService := alea.NewAleaPlayService( @@ -121,6 +170,24 @@ func main() { store, ) + reportSvc := report.NewService( + betStore, + walletStore, + transactionStore, + branchStore, + userStore, + logger, + ) + + walletMonitorSvc := monitor.NewService( + *walletSvc, + *branchSvc, + notificationSvc, + logger, + 5*time.Minute, + ) + walletMonitorSvc.Start() + httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc) httpserver.StartTicketCrons(*ticketSvc) @@ -128,7 +195,7 @@ func main() { JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, }, userSvc, - ticketSvc, betSvc, chapaSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, aleaService, veliService, recommendationSvc, resultSvc, cfg) + ticketSvc, betSvc, reportSvc, chapaSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, aleaService, veliService, recommendationSvc, resultSvc, cfg) logger.Info("Starting server", "port", cfg.Port) if err := app.Run(); err != nil { diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 6e12d64..af78c17 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -136,25 +136,25 @@ CREATE TABLE IF NOT EXISTS transactions ( id BIGSERIAL PRIMARY KEY, amount BIGINT NOT NULL, branch_id BIGINT NOT NULL, - company_id BIGINT NOT NULL, - cashier_id BIGINT NOT NULL, - cashier_name VARCHAR(255) NOT NULL, - bet_id BIGINT NOT NULL, - number_of_outcomes BIGINT NOT NULL, - type BIGINT NOT NULL, - payment_option BIGINT NOT NULL, - full_name VARCHAR(255) NOT NULL, - phone_number VARCHAR(255) NOT NULL, - bank_code VARCHAR(255) NOT NULL, - beneficiary_name VARCHAR(255) NOT NULL, - account_name VARCHAR(255) NOT NULL, - account_number VARCHAR(255) NOT NULL, - reference_number VARCHAR(255) NOT NULL, + company_id BIGINT, + cashier_id BIGINT, + cashier_name VARCHAR(255)L, + bet_id BIGINT, + number_of_outcomes BIGINT, + type BIGINT, + payment_option BIGINT, + full_name VARCHAR(255), + phone_number VARCHAR(255), + bank_code VARCHAR(255), + beneficiary_name VARCHAR(255), + account_name VARCHAR(255), + account_number VARCHAR(255), + reference_number VARCHAR(255), verified BOOLEAN NOT NULL DEFAULT false, approved_by BIGINT, approver_name VARCHAR(255), - branch_location VARCHAR(255) NOT NULL, - branch_name VARCHAR(255) NOT NULL, + branch_location VARCHAR(255), + branch_name VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); diff --git a/db/migrations/000002_notification.down.sql b/db/migrations/000002_notification.down.sql index 45f6e69..5e492c9 100644 --- a/db/migrations/000002_notification.down.sql +++ b/db/migrations/000002_notification.down.sql @@ -1 +1,2 @@ DROP TABLE notifications; +DROP TABLE wallet_threshold_notifications; diff --git a/db/migrations/000002_notification.up.sql b/db/migrations/000002_notification.up.sql index 134fe94..31c47fe 100644 --- a/db/migrations/000002_notification.up.sql +++ b/db/migrations/000002_notification.up.sql @@ -30,6 +30,15 @@ CREATE TABLE IF NOT EXISTS notifications ( 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); diff --git a/db/query/monitor.sql b/db/query/monitor.sql new file mode 100644 index 0000000..a5d21b7 --- /dev/null +++ b/db/query/monitor.sql @@ -0,0 +1,24 @@ +-- name: GetAllCompanies :many +SELECT id, name, wallet_id, admin_id, created_at +FROM companies; + +-- name: GetBranchesByCompanyID :many +SELECT + id, + name, + location, + wallet_id, + branch_manager_id, + company_id, + is_self_owned +FROM branches +WHERE company_id = $1; + +-- name: CountThresholdNotifications :one +SELECT COUNT(*) +FROM wallet_threshold_notifications +WHERE company_id = $1 AND threshold = $2; + +-- name: CreateThresholdNotification :exec +INSERT INTO wallet_threshold_notifications (company_id, threshold) +VALUES ($1, $2); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index e1f077d..bf5801f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,25 @@ services: volumes: - postgres_data:/var/lib/postgresql/data + mongo: + container_name: fortunebet-mongo + image: mongo:7.0 + restart: always + ports: + - "27017:27017" + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: secret + volumes: + - mongo_data:/data/db + networks: + - app + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + migrate: image: migrate/migrate volumes: @@ -44,14 +63,16 @@ services: - ${PORT}:8080 environment: - DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable + - MONGO_URI=mongodb://root:secret@mongo:27017 depends_on: migrate: condition: service_completed_successfully + mongo: + condition: service_healthy networks: - app command: ["/app/bin/web"] - test: build: context: . @@ -69,3 +90,4 @@ networks: volumes: postgres_data: + mongo_data: diff --git a/docs/docs.go b/docs/docs.go index 78486ff..ea14882 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -511,6 +511,96 @@ const docTemplate = `{ } } }, + "/api/v1/reports/dashboard": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Returns a comprehensive dashboard report with key metrics", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Reports" + ], + "summary": "Get dashboard report", + "parameters": [ + { + "type": "integer", + "description": "Company ID filter", + "name": "company_id", + "in": "query" + }, + { + "type": "integer", + "description": "Branch ID filter", + "name": "branch_id", + "in": "query" + }, + { + "type": "integer", + "description": "User ID filter", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "description": "Start time filter (RFC3339 format)", + "name": "start_time", + "in": "query" + }, + { + "type": "string", + "description": "End time filter (RFC3339 format)", + "name": "end_time", + "in": "query" + }, + { + "type": "string", + "description": "Sport ID filter", + "name": "sport_id", + "in": "query" + }, + { + "type": "integer", + "description": "Status filter (0=Pending, 1=Win, 2=Loss, 3=Half, 4=Void, 5=Error)", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/report.DashboardSummary" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/virtual-games/recommendations/{userID}": { "get": { "description": "Returns a list of recommended virtual games for a specific user", @@ -4629,6 +4719,17 @@ const docTemplate = `{ } } }, + "domain.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, "domain.Odd": { "type": "object", "properties": { @@ -6285,6 +6386,56 @@ const docTemplate = `{ } } }, + "report.DashboardSummary": { + "type": "object", + "properties": { + "active_bets": { + "type": "integer" + }, + "active_branches": { + "type": "integer" + }, + "active_customers": { + "type": "integer" + }, + "average_stake": { + "type": "integer" + }, + "branches_count": { + "type": "integer" + }, + "customer_count": { + "type": "integer" + }, + "profit": { + "type": "integer" + }, + "total_bets": { + "type": "integer" + }, + "total_deposits": { + "type": "integer" + }, + "total_losses": { + "type": "integer" + }, + "total_stakes": { + "type": "integer" + }, + "total_wins": { + "type": "integer" + }, + "total_withdrawals": { + "type": "integer" + }, + "win_balance": { + "type": "integer" + }, + "win_rate": { + "type": "number" + } + } + }, "response.APIResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 948658c..d6e140b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -503,6 +503,96 @@ } } }, + "/api/v1/reports/dashboard": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Returns a comprehensive dashboard report with key metrics", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Reports" + ], + "summary": "Get dashboard report", + "parameters": [ + { + "type": "integer", + "description": "Company ID filter", + "name": "company_id", + "in": "query" + }, + { + "type": "integer", + "description": "Branch ID filter", + "name": "branch_id", + "in": "query" + }, + { + "type": "integer", + "description": "User ID filter", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "description": "Start time filter (RFC3339 format)", + "name": "start_time", + "in": "query" + }, + { + "type": "string", + "description": "End time filter (RFC3339 format)", + "name": "end_time", + "in": "query" + }, + { + "type": "string", + "description": "Sport ID filter", + "name": "sport_id", + "in": "query" + }, + { + "type": "integer", + "description": "Status filter (0=Pending, 1=Win, 2=Loss, 3=Half, 4=Void, 5=Error)", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/report.DashboardSummary" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/virtual-games/recommendations/{userID}": { "get": { "description": "Returns a list of recommended virtual games for a specific user", @@ -4621,6 +4711,17 @@ } } }, + "domain.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, "domain.Odd": { "type": "object", "properties": { @@ -6277,6 +6378,56 @@ } } }, + "report.DashboardSummary": { + "type": "object", + "properties": { + "active_bets": { + "type": "integer" + }, + "active_branches": { + "type": "integer" + }, + "active_customers": { + "type": "integer" + }, + "average_stake": { + "type": "integer" + }, + "branches_count": { + "type": "integer" + }, + "customer_count": { + "type": "integer" + }, + "profit": { + "type": "integer" + }, + "total_bets": { + "type": "integer" + }, + "total_deposits": { + "type": "integer" + }, + "total_losses": { + "type": "integer" + }, + "total_stakes": { + "type": "integer" + }, + "total_wins": { + "type": "integer" + }, + "total_withdrawals": { + "type": "integer" + }, + "win_balance": { + "type": "integer" + }, + "win_rate": { + "type": "number" + } + } + }, "response.APIResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index feaedda..60a0b9f 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -255,6 +255,13 @@ definitions: - $ref: '#/definitions/domain.OutcomeStatus' example: 1 type: object + domain.ErrorResponse: + properties: + error: + type: string + message: + type: string + type: object domain.Odd: properties: category: @@ -1415,6 +1422,39 @@ definitions: example: false type: boolean type: object + report.DashboardSummary: + properties: + active_bets: + type: integer + active_branches: + type: integer + active_customers: + type: integer + average_stake: + type: integer + branches_count: + type: integer + customer_count: + type: integer + profit: + type: integer + total_bets: + type: integer + total_deposits: + type: integer + total_losses: + type: integer + total_stakes: + type: integer + total_wins: + type: integer + total_withdrawals: + type: integer + win_balance: + type: integer + win_rate: + type: number + type: object response.APIResponse: properties: data: {} @@ -1766,6 +1806,64 @@ paths: summary: Withdraw using Chapa tags: - Chapa + /api/v1/reports/dashboard: + get: + consumes: + - application/json + description: Returns a comprehensive dashboard report with key metrics + parameters: + - description: Company ID filter + in: query + name: company_id + type: integer + - description: Branch ID filter + in: query + name: branch_id + type: integer + - description: User ID filter + in: query + name: user_id + type: integer + - description: Start time filter (RFC3339 format) + in: query + name: start_time + type: string + - description: End time filter (RFC3339 format) + in: query + name: end_time + type: string + - description: Sport ID filter + in: query + name: sport_id + type: string + - description: Status filter (0=Pending, 1=Win, 2=Loss, 3=Half, 4=Void, 5=Error) + in: query + name: status + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/report.DashboardSummary' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: Get dashboard report + tags: + - Reports /api/v1/virtual-games/recommendations/{userID}: get: consumes: diff --git a/go.mod b/go.mod index 32d9786..1c0137f 100644 --- a/go.mod +++ b/go.mod @@ -56,11 +56,24 @@ require ( github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect + go.mongodb.org/mongo-driver v1.17.3 golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect - golang.org/x/net v0.38.0 // indirect + golang.org/x/net v0.38.0 // direct golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect golang.org/x/tools v0.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +require ( + github.com/golang/snappy v0.0.4 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.uber.org/zap v1.27.0 +) + +require go.uber.org/multierr v1.10.0 // indirect diff --git a/go.sum b/go.sum index 69ce8cd..de410d7 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,10 @@ github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27X github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -96,6 +100,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= @@ -147,30 +153,52 @@ github.com/valyala/fasthttp v1.36.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxn github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= +go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -182,20 +210,25 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/domain/chapa.go b/internal/domain/chapa.go index 1dba8f9..acb82ec 100644 --- a/internal/domain/chapa.go +++ b/internal/domain/chapa.go @@ -192,9 +192,9 @@ func (r ChapaDepositRequest) Validate() error { if r.PhoneNumber == "" { return errors.New("phone number is required") } - if r.BranchID == 0 { - return errors.New("branch ID is required") - } + // if r.BranchID == 0 { + // return errors.New("branch ID is required") + // } return nil } diff --git a/internal/domain/notification.go b/internal/domain/notification.go index e39ee16..9c7a109 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -14,14 +14,18 @@ type NotificationDeliveryStatus string type DeliveryChannel string const ( - NotificationTypeCashOutSuccess NotificationType = "cash_out_success" - NotificationTypeDepositSuccess NotificationType = "deposit_success" - NotificationTypeBetPlaced NotificationType = "bet_placed" - NotificationTypeDailyReport NotificationType = "daily_report" - NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet" - NotificationTypeBetOverload NotificationType = "bet_overload" - NotificationTypeSignUpWelcome NotificationType = "signup_welcome" - NotificationTypeOTPSent NotificationType = "otp_sent" + NotificationTypeCashOutSuccess NotificationType = "cash_out_success" + NotificationTypeDepositSuccess NotificationType = "deposit_success" + NotificationTypeBetPlaced NotificationType = "bet_placed" + NotificationTypeDailyReport NotificationType = "daily_report" + NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet" + NotificationTypeBetOverload NotificationType = "bet_overload" + NotificationTypeSignUpWelcome NotificationType = "signup_welcome" + NotificationTypeOTPSent NotificationType = "otp_sent" + NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold" + NOTIFICATION_TYPE_TRANSFER NotificationType = "transfer_failed" + NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert" + NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin" NotificationRecieverSideAdmin NotificationRecieverSide = "admin" NotificationRecieverSideCustomer NotificationRecieverSide = "customer" diff --git a/internal/domain/report.go b/internal/domain/report.go new file mode 100644 index 0000000..df7921e --- /dev/null +++ b/internal/domain/report.go @@ -0,0 +1,123 @@ +package domain + +import "time" + +type ValidOutcomeStatus struct { + Value OutcomeStatus + Valid bool // Valid is true if Value is not NULL +} + +// ReportFilter contains filters for report generation +type ReportFilter struct { + StartTime ValidTime `json:"start_time"` + EndTime ValidTime `json:"end_time"` + CompanyID ValidInt64 `json:"company_id"` + BranchID ValidInt64 `json:"branch_id"` + UserID ValidInt64 `json:"user_id"` + SportID ValidString `json:"sport_id"` + Status ValidOutcomeStatus `json:"status"` +} + +// BetStat represents aggregated bet statistics +type BetStat struct { + Date time.Time + TotalBets int64 + TotalStakes Currency + TotalWins int64 + TotalPayouts Currency + AverageOdds float64 +} + +// ExtremeValues represents extreme values in betting +type ExtremeValues struct { + HighestStake Currency + HighestPayout Currency +} + +// CustomerBetActivity represents customer betting activity +type CustomerBetActivity struct { + CustomerID int64 + TotalBets int64 + TotalStakes Currency + TotalWins int64 + TotalPayouts Currency + FirstBetDate time.Time + LastBetDate time.Time + AverageOdds float64 +} + + +// BranchBetActivity represents branch betting activity +type BranchBetActivity struct { + BranchID int64 + TotalBets int64 + TotalStakes Currency + TotalWins int64 + TotalPayouts Currency +} + +// BranchDetail represents branch details +// type BranchDetail struct { +// Name string +// Location string +// ManagerName string +// } + +// BranchTransactions represents branch transaction totals +type BranchTransactions struct { + Deposits Currency + Withdrawals Currency +} + +// SportBetActivity represents sport betting activity +type SportBetActivity struct { + SportID string + TotalBets int64 + TotalStakes Currency + TotalWins int64 + TotalPayouts Currency + AverageOdds float64 +} + +// CustomerDetail represents customer details +type CustomerDetail struct { + Name string +} + +// BalanceSummary represents wallet balance summary +type BalanceSummary struct { + TotalBalance Currency + ActiveBalance Currency + InactiveBalance Currency + BettableBalance Currency + NonBettableBalance Currency +} + +// In your domain package +type CustomerPreferences struct { + FavoriteSport string `json:"favorite_sport"` + FavoriteMarket string `json:"favorite_market"` +} + +type DashboardSummary struct { + TotalStakes Currency `json:"total_stakes"` + TotalBets int64 `json:"total_bets"` + ActiveBets int64 `json:"active_bets"` + WinBalance Currency `json:"win_balance"` + TotalWins int64 `json:"total_wins"` + TotalLosses int64 `json:"total_losses"` + CustomerCount int64 `json:"customer_count"` + Profit Currency `json:"profit"` + WinRate float64 `json:"win_rate"` + AverageStake Currency `json:"average_stake"` + TotalDeposits Currency `json:"total_deposits"` + TotalWithdrawals Currency `json:"total_withdrawals"` + ActiveCustomers int64 `json:"active_customers"` + BranchesCount int64 `json:"branches_count"` + ActiveBranches int64 `json:"active_branches"` +} + +type ErrorResponse struct { + Message string `json:"message"` + Error string `json:"error,omitempty"` +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 41e6bb4..f7ff602 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -1,8 +1,15 @@ package customlogger import ( + "context" + "fmt" "log/slog" "os" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) var LogLevels = map[string]slog.Level{ @@ -24,7 +31,7 @@ func NewLogger(env string, lvl slog.Level) *slog.Logger { panic("Failed to create log directory: " + err.Error()) } - file, err := os.OpenFile("logs/app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + file, err := os.OpenFile("logs/app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { panic("Failed to open log file: " + err.Error()) } @@ -48,3 +55,59 @@ func NewLogger(env string, lvl slog.Level) *slog.Logger { return logger } + +// MongoLogger wraps a MongoDB connection and logger +type MongoLogger struct { + client *mongo.Client + collection *mongo.Collection +} + +// NewMongoLogger creates a new MongoDB-connected logger +func NewMongoLogger(mongoURI, dbName, collectionName string) (*MongoLogger, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // 1. Connect to MongoDB with retries + client, err := mongo.Connect(ctx, options.Client(). + ApplyURI(mongoURI). + SetServerAPIOptions(options.ServerAPI(options.ServerAPIVersion1)), + ) + if err != nil { + return nil, fmt.Errorf("MongoDB connection failed: %w", err) + } + + // 2. Verify connection + err = client.Ping(ctx, nil) + if err != nil { + return nil, fmt.Errorf("MongoDB ping failed: %w", err) + } + + // 3. Get collection handle + coll := client.Database(dbName).Collection(collectionName) + + return &MongoLogger{ + client: client, + collection: coll, + }, nil +} + +// Log writes a log entry to MongoDB +func (ml *MongoLogger) Log(level, message string, attrs map[string]interface{}) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := ml.collection.InsertOne(ctx, bson.M{ + "timestamp": time.Now(), + "level": level, + "message": message, + "attrs": attrs, + }) + return err +} + +// Close safely disconnects from MongoDB +func (ml *MongoLogger) Close() error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + return ml.client.Disconnect(ctx) +} diff --git a/internal/logger/mongoLogger/handler.go b/internal/logger/mongoLogger/handler.go new file mode 100644 index 0000000..81aa893 --- /dev/null +++ b/internal/logger/mongoLogger/handler.go @@ -0,0 +1,47 @@ +package mongoLogger + +import ( + "context" + + "github.com/gofiber/fiber/v2" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type LogEntry struct { + Level string `json:"level" bson:"level"` + Message string `json:"message" bson:"message"` + Timestamp string `json:"timestamp" bson:"timestamp"` + Fields map[string]interface{} `json:"fields" bson:"fields"` + Caller string `json:"caller" bson:"caller"` + Stack string `json:"stacktrace" bson:"stacktrace"` + Service string `json:"service" bson:"service"` + Env string `json:"env" bson:"env"` +} + +func GetLogsHandler(appCtx context.Context) fiber.Handler { + return func(c *fiber.Ctx) error { + client, err := mongo.Connect(appCtx, options.Client().ApplyURI("mongodb://root:secret@mongo:27017/?authSource=admin")) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "MongoDB connection failed: "+err.Error()) + } + + collection := client.Database("logdb").Collection("applogs") + filter := bson.M{} + opts := options.Find().SetSort(bson.D{{Key: "timestamp", Value: -1}}).SetLimit(100) + + cursor, err := collection.Find(appCtx, filter, opts) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch logs: "+err.Error()) + } + defer cursor.Close(appCtx) + + var logs []LogEntry + if err := cursor.All(appCtx, &logs); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Cursor decoding error: "+err.Error()) + } + + return c.JSON(logs) + } +} diff --git a/internal/logger/mongoLogger/init.go b/internal/logger/mongoLogger/init.go new file mode 100644 index 0000000..b71eb97 --- /dev/null +++ b/internal/logger/mongoLogger/init.go @@ -0,0 +1,30 @@ +package mongoLogger + +import ( + "log" + "os" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func Init() { + // Replace localhost if inside Docker + mongoCore, err := NewMongoCore("mongodb://root:secret@mongo:27017/?authSource=admin", "logdb", "applogs", zapcore.InfoLevel) + if err != nil { + log.Fatalf("failed to create MongoDB core: %v", err) + } + + consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) + consoleCore := zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel) + + combinedCore := zapcore.NewTee(mongoCore, consoleCore) + + logger := zap.New(combinedCore, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)) + zap.ReplaceGlobals(logger) // Optional but useful if you use zap.L() + + defer logger.Sync() + + logger.Info("Application started", zap.String("module", "main")) + logger.Error("Something went wrong", zap.String("error_code", "E123")) +} diff --git a/internal/logger/mongoLogger/logger.go b/internal/logger/mongoLogger/logger.go new file mode 100644 index 0000000..b3bec21 --- /dev/null +++ b/internal/logger/mongoLogger/logger.go @@ -0,0 +1,89 @@ +package mongoLogger + +import ( + "context" + "fmt" + "time" + + "maps" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.uber.org/zap/zapcore" +) + +type MongoCore struct { + collection *mongo.Collection + level zapcore.Level + fields []zapcore.Field +} + +func NewMongoCore(uri, dbName, collectionName string, level zapcore.Level) (zapcore.Core, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri)) + if err != nil { + return nil, err + } + + if err := client.Ping(ctx, nil); err != nil { + return nil, fmt.Errorf("unable to connect to MongoDB: %w", err) + } + + coll := client.Database(dbName).Collection(collectionName) + return &MongoCore{ + collection: coll, + level: level, + }, nil +} + +func (mc *MongoCore) Enabled(lvl zapcore.Level) bool { + return lvl >= mc.level +} + +func (mc *MongoCore) With(fields []zapcore.Field) zapcore.Core { + clone := *mc + clone.fields = append(clone.fields, fields...) + return &clone +} + +func (mc *MongoCore) Check(entry zapcore.Entry, checkedEntry *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if mc.Enabled(entry.Level) { + return checkedEntry.AddCore(entry, mc) + } + return checkedEntry +} + +func (mc *MongoCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { + logMap := make(map[string]interface{}) + enc := zapcore.NewMapObjectEncoder() + + for _, f := range append(mc.fields, fields...) { + f.AddTo(enc) + } + + maps.Copy(logMap, enc.Fields) + + doc := bson.M{ + "level": entry.Level.String(), + "message": entry.Message, + "timestamp": entry.Time.UTC().Format(time.RFC3339Nano), + "fields": logMap, + "caller": entry.Caller.String(), + "stacktrace": entry.Stack, + "service": "fortunebet-backend", + "env": "dev", + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := mc.collection.InsertOne(ctx, doc) + return err +} + +func (mc *MongoCore) Sync() error { + return nil +} diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 28ea2ff..f372ca1 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -2,6 +2,9 @@ package repository import ( "context" + "fmt" + "log/slog" + "time" // "fmt" @@ -10,6 +13,8 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +var logger *slog.Logger + func convertDBBet(bet dbgen.Bet) domain.Bet { return domain.Bet{ ID: bet.ID, @@ -132,6 +137,8 @@ func convertCreateBet(bet domain.CreateBet) dbgen.CreateBetParams { func (s *Store) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) { newBet, err := s.queries.CreateBet(ctx, convertCreateBet(bet)) if err != nil { + fmt.Println("We are here") + logger.Error("Failed to create bet", slog.String("error", err.Error()), slog.Any("bet", bet)) return domain.Bet{}, err } return convertDBBet(newBet), err @@ -289,3 +296,768 @@ func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status dom func (s *Store) DeleteBet(ctx context.Context, id int64) error { return s.queries.DeleteBet(ctx, id) } + +// GetBetSummary returns aggregated bet statistics +func (s *Store) GetBetSummary(ctx context.Context, filter domain.ReportFilter) ( + totalStakes domain.Currency, + totalBets int64, + activeBets int64, + totalWins int64, + totalLosses int64, + winBalance domain.Currency, + err error, +) { + query := `SELECT + COALESCE(SUM(amount), 0) as total_stakes, + COUNT(*) as total_bets, + SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) as active_bets, + SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as total_wins, + SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as total_losses, + COALESCE(SUM(CASE WHEN status = 1 THEN amount * total_odds ELSE 0 END), 0) as win_balance + FROM bets` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" WHERE company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.BranchID.Valid { + query += fmt.Sprintf(" AND %sbranch_id = $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } + if filter.UserID.Valid { + query += fmt.Sprintf(" AND %suser_id = $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.UserID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND %screated_at >= $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + if filter.Status.Valid { + query += fmt.Sprintf(" AND %sstatus = $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.Status.Value) + argPos++ + } + + row := s.conn.QueryRow(ctx, query, args...) + err = row.Scan(&totalStakes, &totalBets, &activeBets, &totalWins, &totalLosses, &winBalance) + if err != nil { + return 0, 0, 0, 0, 0, 0, fmt.Errorf("failed to get bet summary: %w", err) + } + + return totalStakes, totalBets, activeBets, totalWins, totalLosses, winBalance, nil +} + +// GetBetStats returns bet statistics grouped by date +func (s *Store) GetBetStats(ctx context.Context, filter domain.ReportFilter) ([]domain.BetStat, error) { + query := `SELECT + DATE(created_at) as date, + COUNT(*) as total_bets, + COALESCE(SUM(amount), 0) as total_stakes, + SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as total_wins, + COALESCE(SUM(CASE WHEN status = 1 THEN amount * total_odds ELSE 0 END), 0) as total_payouts, + AVG(total_odds) as average_odds + FROM bets` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" WHERE company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.BranchID.Valid { + query += fmt.Sprintf(" AND %sbranch_id = $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } + if filter.UserID.Valid { + query += fmt.Sprintf(" AND %suser_id = $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.UserID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND %screated_at >= $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + if filter.Status.Valid { + query += fmt.Sprintf(" AND %sstatus = $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.Status.Value) + argPos++ + } + + query += " GROUP BY DATE(created_at) ORDER BY DATE(created_at)" + + rows, err := s.conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query bet stats: %w", err) + } + defer rows.Close() + + var stats []domain.BetStat + for rows.Next() { + var stat domain.BetStat + if err := rows.Scan( + &stat.Date, + &stat.TotalBets, + &stat.TotalStakes, + &stat.TotalWins, + &stat.TotalPayouts, + &stat.AverageOdds, + ); err != nil { + return nil, fmt.Errorf("failed to scan bet stat: %w", err) + } + stats = append(stats, stat) + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return stats, nil +} + +// GetSportPopularity returns the most popular sport by date +func (s *Store) GetSportPopularity(ctx context.Context, filter domain.ReportFilter) (map[time.Time]string, error) { + query := `WITH sport_counts AS ( + SELECT + DATE(b.created_at) as date, + bo.sport_id, + COUNT(*) as bet_count, + ROW_NUMBER() OVER (PARTITION BY DATE(b.created_at) ORDER BY COUNT(*) DESC) as rank + FROM bets b + JOIN bet_outcomes bo ON b.id = bo.bet_id + WHERE bo.sport_id IS NOT NULL` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" AND b.company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.BranchID.Valid { + query += fmt.Sprintf(" AND b.branch_id = $%d", argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } + if filter.UserID.Valid { + query += fmt.Sprintf(" AND b.user_id = $%d", argPos) + args = append(args, filter.UserID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND b.created_at >= $%d", argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND b.created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + if filter.Status.Valid { + query += fmt.Sprintf(" AND b.status = $%d", argPos) + args = append(args, filter.Status.Value) + argPos++ + } + + query += ` GROUP BY DATE(b.created_at), bo.sport_id + ) + SELECT date, sport_id FROM sport_counts WHERE rank = 1` + + rows, err := s.conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query sport popularity: %w", err) + } + defer rows.Close() + + popularity := make(map[time.Time]string) + for rows.Next() { + var date time.Time + var sportID string + if err := rows.Scan(&date, &sportID); err != nil { + return nil, fmt.Errorf("failed to scan sport popularity: %w", err) + } + popularity[date] = sportID + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return popularity, nil +} + +// GetMarketPopularity returns the most popular market by date +func (s *Store) GetMarketPopularity(ctx context.Context, filter domain.ReportFilter) (map[time.Time]string, error) { + query := `WITH market_counts AS ( + SELECT + DATE(b.created_at) as date, + bo.market_name, + COUNT(*) as bet_count, + ROW_NUMBER() OVER (PARTITION BY DATE(b.created_at) ORDER BY COUNT(*) DESC) as rank + FROM bets b + JOIN bet_outcomes bo ON b.id = bo.bet_id + WHERE bo.market_name IS NOT NULL` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" AND b.company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.BranchID.Valid { + query += fmt.Sprintf(" AND b.branch_id = $%d", argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } + if filter.UserID.Valid { + query += fmt.Sprintf(" AND b.user_id = $%d", argPos) + args = append(args, filter.UserID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND b.created_at >= $%d", argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND b.created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + if filter.Status.Valid { + query += fmt.Sprintf(" AND b.status = $%d", argPos) + args = append(args, filter.Status.Value) + argPos++ + } + + query += ` GROUP BY DATE(b.created_at), bo.market_name + ) + SELECT date, market_name FROM market_counts WHERE rank = 1` + + rows, err := s.conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query market popularity: %w", err) + } + defer rows.Close() + + popularity := make(map[time.Time]string) + for rows.Next() { + var date time.Time + var marketName string + if err := rows.Scan(&date, &marketName); err != nil { + return nil, fmt.Errorf("failed to scan market popularity: %w", err) + } + popularity[date] = marketName + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return popularity, nil +} + +// GetExtremeValues returns the highest stake and payout by date +func (s *Store) GetExtremeValues(ctx context.Context, filter domain.ReportFilter) (map[time.Time]domain.ExtremeValues, error) { + query := `SELECT + DATE(created_at) as date, + MAX(amount) as highest_stake, + MAX(CASE WHEN status = 1 THEN amount * total_odds ELSE 0 END) as highest_payout + FROM bets` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" WHERE company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.BranchID.Valid { + query += fmt.Sprintf(" AND %sbranch_id = $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } + if filter.UserID.Valid { + query += fmt.Sprintf(" AND %suser_id = $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.UserID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND %screated_at >= $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + if filter.Status.Valid { + query += fmt.Sprintf(" AND %sstatus = $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.Status.Value) + argPos++ + } + + query += " GROUP BY DATE(created_at)" + + rows, err := s.conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query extreme values: %w", err) + } + defer rows.Close() + + extremes := make(map[time.Time]domain.ExtremeValues) + for rows.Next() { + var date time.Time + var extreme domain.ExtremeValues + if err := rows.Scan(&date, &extreme.HighestStake, &extreme.HighestPayout); err != nil { + return nil, fmt.Errorf("failed to scan extreme values: %w", err) + } + extremes[date] = extreme + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return extremes, nil +} + +// GetCustomerBetActivity returns bet activity by customer +func (s *Store) GetCustomerBetActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.CustomerBetActivity, error) { + query := `SELECT + user_id as customer_id, + COUNT(*) as total_bets, + COALESCE(SUM(amount), 0) as total_stakes, + SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as total_wins, + COALESCE(SUM(CASE WHEN status = 1 THEN amount * total_odds ELSE 0 END), 0) as total_payouts, + MIN(created_at) as first_bet_date, + MAX(created_at) as last_bet_date, + AVG(total_odds) as average_odds + FROM bets + WHERE user_id IS NOT NULL` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" AND company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.BranchID.Valid { + query += fmt.Sprintf(" AND branch_id = $%d", argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } + if filter.UserID.Valid { + query += fmt.Sprintf(" AND user_id = $%d", argPos) + args = append(args, filter.UserID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND created_at >= $%d", argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + if filter.Status.Valid { + query += fmt.Sprintf(" AND status = $%d", argPos) + args = append(args, filter.Status.Value) + argPos++ + } + + query += " GROUP BY user_id" + + rows, err := s.conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query customer bet activity: %w", err) + } + defer rows.Close() + + var activities []domain.CustomerBetActivity + for rows.Next() { + var activity domain.CustomerBetActivity + if err := rows.Scan( + &activity.CustomerID, + &activity.TotalBets, + &activity.TotalStakes, + &activity.TotalWins, + &activity.TotalPayouts, + &activity.FirstBetDate, + &activity.LastBetDate, + &activity.AverageOdds, + ); err != nil { + return nil, fmt.Errorf("failed to scan customer bet activity: %w", err) + } + activities = append(activities, activity) + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return activities, nil +} + +// GetBranchBetActivity returns bet activity by branch +func (s *Store) GetBranchBetActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.BranchBetActivity, error) { + query := `SELECT + branch_id, + COUNT(*) as total_bets, + COALESCE(SUM(amount), 0) as total_stakes, + SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as total_wins, + COALESCE(SUM(CASE WHEN status = 1 THEN amount * total_odds ELSE 0 END), 0) as total_payouts + FROM bets + WHERE branch_id IS NOT NULL` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" AND company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.BranchID.Valid { + query += fmt.Sprintf(" AND branch_id = $%d", argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND created_at >= $%d", argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + if filter.Status.Valid { + query += fmt.Sprintf(" AND status = $%d", argPos) + args = append(args, filter.Status.Value) + argPos++ + } + + query += " GROUP BY branch_id" + + rows, err := s.conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query branch bet activity: %w", err) + } + defer rows.Close() + + var activities []domain.BranchBetActivity + for rows.Next() { + var activity domain.BranchBetActivity + if err := rows.Scan( + &activity.BranchID, + &activity.TotalBets, + &activity.TotalStakes, + &activity.TotalWins, + &activity.TotalPayouts, + ); err != nil { + return nil, fmt.Errorf("failed to scan branch bet activity: %w", err) + } + activities = append(activities, activity) + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return activities, nil +} + +// GetSportBetActivity returns bet activity by sport +func (s *Store) GetSportBetActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.SportBetActivity, error) { + query := `SELECT + bo.sport_id, + COUNT(*) as total_bets, + COALESCE(SUM(b.amount), 0) as total_stakes, + SUM(CASE WHEN b.status = 1 THEN 1 ELSE 0 END) as total_wins, + COALESCE(SUM(CASE WHEN b.status = 1 THEN b.amount * b.total_odds ELSE 0 END), 0) as total_payouts, + AVG(b.total_odds) as average_odds + FROM bets b + JOIN bet_outcomes bo ON b.id = bo.bet_id + WHERE bo.sport_id IS NOT NULL` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" AND b.company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.BranchID.Valid { + query += fmt.Sprintf(" AND b.branch_id = $%d", argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } + if filter.UserID.Valid { + query += fmt.Sprintf(" AND b.user_id = $%d", argPos) + args = append(args, filter.UserID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND b.created_at >= $%d", argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND b.created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + if filter.Status.Valid { + query += fmt.Sprintf(" AND b.status = $%d", argPos) + args = append(args, filter.Status.Value) + argPos++ + } + + query += " GROUP BY bo.sport_id" + + rows, err := s.conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query sport bet activity: %w", err) + } + defer rows.Close() + + var activities []domain.SportBetActivity + for rows.Next() { + var activity domain.SportBetActivity + if err := rows.Scan( + &activity.SportID, + &activity.TotalBets, + &activity.TotalStakes, + &activity.TotalWins, + &activity.TotalPayouts, + &activity.AverageOdds, + ); err != nil { + return nil, fmt.Errorf("failed to scan sport bet activity: %w", err) + } + activities = append(activities, activity) + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return activities, nil +} + +// GetSportDetails returns sport names by ID +func (s *Store) GetSportDetails(ctx context.Context, filter domain.ReportFilter) (map[string]string, error) { + query := `SELECT DISTINCT bo.sport_id, e.match_name + FROM bet_outcomes bo + JOIN events e ON bo.event_id = e.id::bigint + WHERE bo.sport_id IS NOT NULL` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND bo.created_at >= $%d", argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND bo.created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + + rows, err := s.conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query sport details: %w", err) + } + defer rows.Close() + + details := make(map[string]string) + for rows.Next() { + var sportID, matchName string + if err := rows.Scan(&sportID, &matchName); err != nil { + return nil, fmt.Errorf("failed to scan sport detail: %w", err) + } + details[sportID] = matchName + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return details, nil +} + +// GetSportMarketPopularity returns most popular market by sport +func (s *Store) GetSportMarketPopularity(ctx context.Context, filter domain.ReportFilter) (map[string]string, error) { + query := `WITH market_counts AS ( + SELECT + bo.sport_id, + bo.market_name, + COUNT(*) as bet_count, + ROW_NUMBER() OVER (PARTITION BY bo.sport_id ORDER BY COUNT(*) DESC) as rank + FROM bets b + JOIN bet_outcomes bo ON b.id = bo.bet_id + WHERE bo.sport_id IS NOT NULL AND bo.market_name IS NOT NULL` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" AND b.company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.BranchID.Valid { + query += fmt.Sprintf(" AND b.branch_id = $%d", argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } + if filter.UserID.Valid { + query += fmt.Sprintf(" AND b.user_id = $%d", argPos) + args = append(args, filter.UserID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND b.created_at >= $%d", argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND b.created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + if filter.Status.Valid { + query += fmt.Sprintf(" AND b.status = $%d", argPos) + args = append(args, filter.Status.Value) + argPos++ + } + + query += ` GROUP BY bo.sport_id, bo.market_name + ) + SELECT sport_id, market_name FROM market_counts WHERE rank = 1` + + rows, err := s.conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query sport market popularity: %w", err) + } + defer rows.Close() + + popularity := make(map[string]string) + for rows.Next() { + var sportID, marketName string + if err := rows.Scan(&sportID, &marketName); err != nil { + return nil, fmt.Errorf("failed to scan sport market popularity: %w", err) + } + popularity[sportID] = marketName + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return popularity, nil +} diff --git a/internal/repository/branch.go b/internal/repository/branch.go index 8a98ff8..300ef7e 100644 --- a/internal/repository/branch.go +++ b/internal/repository/branch.go @@ -2,6 +2,7 @@ package repository import ( "context" + "fmt" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -256,3 +257,158 @@ func (s *Store) DeleteBranchCashier(ctx context.Context, userID int64) error { return s.queries.DeleteBranchCashier(ctx, userID) } + +// GetBranchCounts returns total and active branch counts +func (s *Store) GetBranchCounts(ctx context.Context, filter domain.ReportFilter) (total, active int64, err error) { + query := `SELECT + COUNT(*) as total, + COUNT(CASE WHEN is_active = true THEN 1 END) as active + FROM branches` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" WHERE company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND %screated_at >= $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + + row := s.conn.QueryRow(ctx, query, args...) + err = row.Scan(&total, &active) + if err != nil { + return 0, 0, fmt.Errorf("failed to get branch counts: %w", err) + } + + return total, active, nil +} + +// GetBranchDetails returns branch details map +func (s *Store) GetBranchDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.BranchDetail, error) { + query := `SELECT + b.id, + b.name, + b.location, + CONCAT(u.first_name, ' ', u.last_name) as manager_name + FROM branches b + LEFT JOIN users u ON b.branch_manager_id = u.id` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" WHERE b.company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.BranchID.Valid { + query += fmt.Sprintf(" AND %sb.id = $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND %sb.created_at >= $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND b.created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + + rows, err := s.conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query branch details: %w", err) + } + defer rows.Close() + + details := make(map[int64]domain.BranchDetail) + for rows.Next() { + var id int64 + var detail domain.BranchDetail + if err := rows.Scan(&id, &detail.Name, &detail.Location, &detail.ManagerName); err != nil { + return nil, fmt.Errorf("failed to scan branch detail: %w", err) + } + details[id] = detail + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return details, nil +} + +// In internal/repository/branch.go +func (s *Store) GetAllCompaniesBranch(ctx context.Context) ([]domain.Company, error) { + dbCompanies, err := s.queries.GetAllCompanies(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get all companies: %w", err) + } + + companies := make([]domain.Company, 0, len(dbCompanies)) + for _, dbCompany := range dbCompanies { + companies = append(companies, domain.Company{ + ID: dbCompany.ID, + Name: dbCompany.Name, + WalletID: dbCompany.WalletID, + AdminID: dbCompany.AdminID, + }) + } + + return companies, nil +} + +// In internal/repository/branch.go +func (s *Store) GetBranchesByCompany(ctx context.Context, companyID int64) ([]domain.Branch, error) { + dbBranches, err := s.queries.GetBranchByCompanyID(ctx, companyID) + if err != nil { + return nil, fmt.Errorf("failed to get branches for company %d: %w", companyID, err) + } + + branches := make([]domain.Branch, 0, len(dbBranches)) + for _, dbBranch := range dbBranches { + branch := domain.Branch{ + ID: dbBranch.ID, + Name: dbBranch.Name, + Location: dbBranch.Location, + WalletID: dbBranch.WalletID, + CompanyID: dbBranch.CompanyID, + IsSelfOwned: dbBranch.IsSelfOwned, + } + + branch.BranchManagerID = dbBranch.BranchManagerID + + branches = append(branches, branch) + } + + return branches, nil +} diff --git a/internal/repository/notification.go b/internal/repository/notification.go index b189ccf..c2150c7 100644 --- a/internal/repository/notification.go +++ b/internal/repository/notification.go @@ -7,6 +7,7 @@ import ( dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/jackc/pgx/v5/pgtype" + "golang.org/x/net/websocket" ) type NotificationRepository interface { @@ -27,6 +28,10 @@ func NewNotificationRepository(store *Store) NotificationRepository { return &Repository{store: store} } +func (r *Repository) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error { + return nil +} + func (r *Repository) CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error) { var errorSeverity pgtype.Text if notification.ErrorSeverity != nil { diff --git a/internal/repository/store.go b/internal/repository/store.go index f3e7579..c60aa2b 100644 --- a/internal/repository/store.go +++ b/internal/repository/store.go @@ -7,6 +7,7 @@ import ( dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" + // "golang.org/x/net/websocket" ) type Store struct { @@ -49,3 +50,12 @@ func (s *Store) BeginTx(ctx context.Context) (*dbgen.Queries, pgx.Tx, error) { q := s.queries.WithTx(tx) return q, tx, nil } + +// func (s *Store) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error { +// // Implement WebSocket connection logic +// return nil +// } + +// func (s *Store) DisconnectWebSocket(recipientID int64) { +// // Implement WebSocket disconnection logic +// } diff --git a/internal/repository/transaction.go b/internal/repository/transaction.go index dd38797..fc4826f 100644 --- a/internal/repository/transaction.go +++ b/internal/repository/transaction.go @@ -2,6 +2,7 @@ package repository import ( "context" + "fmt" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -139,3 +140,106 @@ func (s *Store) UpdateTransactionVerified(ctx context.Context, id int64, verifie }) return err } + +// GetTransactionTotals returns total deposits and withdrawals +func (s *Store) GetTransactionTotals(ctx context.Context, filter domain.ReportFilter) (deposits, withdrawals domain.Currency, err error) { + query := `SELECT + COALESCE(SUM(CASE WHEN type = 'deposit' THEN amount ELSE 0 END), 0) as deposits, + COALESCE(SUM(CASE WHEN type = 'withdrawal' THEN amount ELSE 0 END), 0) as withdrawals + FROM transactions` + + args := []interface{}{} + argPos := 1 + + if filter.CompanyID.Valid { + query += fmt.Sprintf(" WHERE company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } else if filter.BranchID.Valid { + query += fmt.Sprintf(" WHERE branch_id = $%d", argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } else if filter.UserID.Valid { + query += fmt.Sprintf(" WHERE cashier_id = $%d", argPos) + args = append(args, filter.UserID.Value) + argPos++ + } + + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND %screated_at >= $%d", func() string { + if len(args) == 0 { + return "" + } + return " " + }(), argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + + row := s.conn.QueryRow(ctx, query, args...) + err = row.Scan(&deposits, &withdrawals) + if err != nil { + return 0, 0, fmt.Errorf("failed to get transaction totals: %w", err) + } + return deposits, withdrawals, nil +} + +// GetBranchTransactionTotals returns transaction totals by branch +func (s *Store) GetBranchTransactionTotals(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.BranchTransactions, error) { + query := `SELECT + branch_id, + COALESCE(SUM(CASE WHEN type = 'deposit' THEN amount ELSE 0 END), 0) as deposits, + COALESCE(SUM(CASE WHEN type = 'withdrawal' THEN amount ELSE 0 END), 0) as withdrawals + FROM transactions` + + args := []interface{}{} + argPos := 1 + + if filter.CompanyID.Valid { + query += fmt.Sprintf(" WHERE company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND %screated_at >= $%d", func() string { + if len(args) == 0 { + return " WHERE " + } + return " AND " + }(), argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + + query += " GROUP BY branch_id" + + rows, err := s.conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query branch transaction totals: %w", err) + } + defer rows.Close() + + totals := make(map[int64]domain.BranchTransactions) + for rows.Next() { + var branchID int64 + var transactions domain.BranchTransactions + if err := rows.Scan(&branchID, &transactions.Deposits, &transactions.Withdrawals); err != nil { + return nil, fmt.Errorf("failed to scan branch transaction totals: %w", err) + } + totals[branchID] = transactions + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + return totals, nil +} diff --git a/internal/repository/user.go b/internal/repository/user.go index 7405542..9b05b5b 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -446,3 +446,250 @@ func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.User, is_c Suspended: userRes.Suspended, }, nil } + +// GetCustomerCounts returns total and active customer counts +func (s *Store) GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active int64, err error) { + query := `SELECT + COUNT(*) as total, + SUM(CASE WHEN suspended = false THEN 1 ELSE 0 END) as active + FROM users WHERE role = 'customer'` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" AND company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.BranchID.Valid { + query += fmt.Sprintf(" AND id IN (SELECT user_id FROM branch_cashiers WHERE branch_id = $%d)", argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND created_at >= $%d", argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + + row := s.conn.QueryRow(ctx, query, args...) + err = row.Scan(&total, &active) + if err != nil { + return 0, 0, fmt.Errorf("failed to get customer counts: %w", err) + } + + return total, active, nil +} + +// GetCustomerDetails returns customer details map +func (s *Store) GetCustomerDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerDetail, error) { + query := `SELECT id, first_name, last_name + FROM users WHERE role = 'customer'` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" AND company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.BranchID.Valid { + query += fmt.Sprintf(" AND id IN (SELECT user_id FROM branch_cashiers WHERE branch_id = $%d)", argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND created_at >= $%d", argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + + rows, err := s.conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query customer details: %w", err) + } + defer rows.Close() + + details := make(map[int64]domain.CustomerDetail) + for rows.Next() { + var id int64 + var firstName, lastName string + if err := rows.Scan(&id, &firstName, &lastName); err != nil { + return nil, fmt.Errorf("failed to scan customer detail: %w", err) + } + details[id] = domain.CustomerDetail{ + Name: fmt.Sprintf("%s %s", firstName, lastName), + } + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return details, nil +} + +// GetBranchCustomerCounts returns customer counts per branch +func (s *Store) GetBranchCustomerCounts(ctx context.Context, filter domain.ReportFilter) (map[int64]int64, error) { + query := `SELECT branch_id, COUNT(DISTINCT user_id) + FROM branch_cashiers + JOIN users ON branch_cashiers.user_id = users.id + WHERE users.role = 'customer'` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" AND branch_id IN (SELECT id FROM branches WHERE company_id = $%d)", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.BranchID.Valid { + query += fmt.Sprintf(" AND branch_id = $%d", argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND users.created_at >= $%d", argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND users.created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + + query += " GROUP BY branch_id" + + rows, err := s.conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query branch customer counts: %w", err) + } + defer rows.Close() + + counts := make(map[int64]int64) + for rows.Next() { + var branchID int64 + var count int64 + if err := rows.Scan(&branchID, &count); err != nil { + return nil, fmt.Errorf("failed to scan branch customer count: %w", err) + } + counts[branchID] = count + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return counts, nil +} + +func (s *Store) GetCustomerPreferences(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerPreferences, error) { + query := `WITH customer_sports AS ( + SELECT + b.user_id, + bo.sport_id, + COUNT(*) as bet_count, + ROW_NUMBER() OVER (PARTITION BY b.user_id ORDER BY COUNT(*) DESC) as sport_rank + FROM bets b + JOIN bet_outcomes bo ON b.id = bo.bet_id + WHERE b.user_id IS NOT NULL AND bo.sport_id IS NOT NULL + ), + customer_markets AS ( + SELECT + b.user_id, + bo.market_name, + COUNT(*) as bet_count, + ROW_NUMBER() OVER (PARTITION BY b.user_id ORDER BY COUNT(*) DESC) as market_rank + FROM bets b + JOIN bet_outcomes bo ON b.id = bo.bet_id + WHERE b.user_id IS NOT NULL AND bo.market_name IS NOT NULL + ` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" AND b.company_id = $%d", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } + if filter.BranchID.Valid { + query += fmt.Sprintf(" AND b.branch_id = $%d", argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } + if filter.UserID.Valid { + query += fmt.Sprintf(" AND b.user_id = $%d", argPos) + args = append(args, filter.UserID.Value) + argPos++ + } + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND b.created_at >= $%d", argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND b.created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + + query += ` GROUP BY b.user_id, bo.sport_id + ), + favorite_sports AS ( + SELECT user_id, sport_id + FROM customer_sports + WHERE sport_rank = 1 + ), + favorite_markets AS ( + SELECT user_id, market_name + FROM customer_markets + WHERE market_rank = 1 + ) + SELECT + fs.user_id, + fs.sport_id as favorite_sport, + fm.market_name as favorite_market + FROM favorite_sports fs + LEFT JOIN favorite_markets fm ON fs.user_id = fm.user_id` + + rows, err := s.conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query customer preferences: %w", err) + } + defer rows.Close() + + preferences := make(map[int64]domain.CustomerPreferences) + for rows.Next() { + var userID int64 + var pref domain.CustomerPreferences + if err := rows.Scan(&userID, &pref.FavoriteSport, &pref.FavoriteMarket); err != nil { + return nil, fmt.Errorf("failed to scan customer preference: %w", err) + } + preferences[userID] = pref + } + + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return preferences, nil +} diff --git a/internal/repository/wallet.go b/internal/repository/wallet.go index 54fd077..2223cbf 100644 --- a/internal/repository/wallet.go +++ b/internal/repository/wallet.go @@ -2,6 +2,7 @@ package repository import ( "context" + "fmt" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -163,3 +164,64 @@ func (s *Store) UpdateWalletActive(ctx context.Context, id int64, isActive bool) }) return err } + +// GetBalanceSummary returns wallet balance summary +func (s *Store) GetBalanceSummary(ctx context.Context, filter domain.ReportFilter) (domain.BalanceSummary, error) { + var summary domain.BalanceSummary + + query := `SELECT + COALESCE(SUM(balance), 0) as total_balance, + COALESCE(SUM(CASE WHEN is_active = true THEN balance ELSE 0 END), 0) as active_balance, + COALESCE(SUM(CASE WHEN is_active = false THEN balance ELSE 0 END), 0) as inactive_balance, + COALESCE(SUM(CASE WHEN is_bettable = true THEN balance ELSE 0 END), 0) as bettable_balance, + COALESCE(SUM(CASE WHEN is_bettable = false THEN balance ELSE 0 END), 0) as non_bettable_balance + FROM wallets` + + args := []interface{}{} + argPos := 1 + + // Add filters if provided + if filter.CompanyID.Valid { + query += fmt.Sprintf(" WHERE user_id IN (SELECT id FROM users WHERE company_id = $%d)", argPos) + args = append(args, filter.CompanyID.Value) + argPos++ + } else if filter.BranchID.Valid { + query += fmt.Sprintf(" WHERE user_id IN (SELECT user_id FROM branch_cashiers WHERE branch_id = $%d)", argPos) + args = append(args, filter.BranchID.Value) + argPos++ + } else if filter.UserID.Valid { + query += fmt.Sprintf(" WHERE user_id = $%d", argPos) + args = append(args, filter.UserID.Value) + argPos++ + } + + if filter.StartTime.Valid { + query += fmt.Sprintf(" AND %screated_at >= $%d", func() string { + if len(args) == 0 { + return "" + } + return " " + }(), argPos) + args = append(args, filter.StartTime.Value) + argPos++ + } + if filter.EndTime.Valid { + query += fmt.Sprintf(" AND created_at <= $%d", argPos) + args = append(args, filter.EndTime.Value) + argPos++ + } + + row := s.conn.QueryRow(ctx, query, args...) + err := row.Scan( + &summary.TotalBalance, + &summary.ActiveBalance, + &summary.InactiveBalance, + &summary.BettableBalance, + &summary.NonBettableBalance, + ) + if err != nil { + return domain.BalanceSummary{}, fmt.Errorf("failed to get balance summary: %w", err) + } + + return summary, nil +} diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index 358bdee..a249e43 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -2,6 +2,7 @@ package bet import ( "context" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) @@ -20,4 +21,24 @@ type BetStore interface { UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) DeleteBet(ctx context.Context, id int64) error + + GetBetSummary(ctx context.Context, filter domain.ReportFilter) ( + totalStakes domain.Currency, + totalBets int64, + activeBets int64, + totalWins int64, + totalLosses int64, + winBalance domain.Currency, + err error, + ) + GetBetStats(ctx context.Context, filter domain.ReportFilter) ([]domain.BetStat, error) + GetSportPopularity(ctx context.Context, filter domain.ReportFilter) (map[time.Time]string, error) + GetMarketPopularity(ctx context.Context, filter domain.ReportFilter) (map[time.Time]string, error) + GetExtremeValues(ctx context.Context, filter domain.ReportFilter) (map[time.Time]domain.ExtremeValues, error) + GetCustomerBetActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.CustomerBetActivity, error) + GetCustomerPreferences(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerPreferences, error) + GetBranchBetActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.BranchBetActivity, error) + GetSportBetActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.SportBetActivity, error) + GetSportDetails(ctx context.Context, filter domain.ReportFilter) (map[string]string, error) + GetSportMarketPopularity(ctx context.Context, filter domain.ReportFilter) (map[string]string, error) } diff --git a/internal/services/branch/port.go b/internal/services/branch/port.go index 2e9c58c..95b5f76 100644 --- a/internal/services/branch/port.go +++ b/internal/services/branch/port.go @@ -23,4 +23,10 @@ type BranchStore interface { CreateBranchCashier(ctx context.Context, branchID int64, userID int64) error GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error) DeleteBranchCashier(ctx context.Context, userID int64) error + + GetBranchCounts(ctx context.Context, filter domain.ReportFilter) (total, active int64, err error) + GetBranchDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.BranchDetail, error) + + GetAllCompaniesBranch(ctx context.Context) ([]domain.Company, error) + GetBranchesByCompany(ctx context.Context, companyID int64) ([]domain.Branch, error) } diff --git a/internal/services/branch/service.go b/internal/services/branch/service.go index 8c9e4d1..eb75170 100644 --- a/internal/services/branch/service.go +++ b/internal/services/branch/service.go @@ -70,3 +70,11 @@ func (s *Service) DeleteBranchOperation(ctx context.Context, branchID int64, ope func (s *Service) DeleteBranchCashier(ctx context.Context, userID int64) error { return s.branchStore.DeleteBranchCashier(ctx, userID) } + +func (s *Service) GetAllCompaniesBranch(ctx context.Context) ([]domain.Company, error) { + return s.branchStore.GetAllCompaniesBranch(ctx) +} + +func (s *Service) GetBranchesByCompany(ctx context.Context, companyID int64) ([]domain.Branch, error) { + return s.branchStore.GetBranchesByCompany(ctx, companyID) +} diff --git a/internal/services/chapa/client.go b/internal/services/chapa/client.go index 8e0374f..be88ebd 100644 --- a/internal/services/chapa/client.go +++ b/internal/services/chapa/client.go @@ -63,13 +63,16 @@ func (c *Client) IssuePayment(ctx context.Context, payload domain.ChapaTransferP // service/chapa_service.go func (c *Client) InitPayment(ctx context.Context, req domain.InitPaymentRequest) (string, error) { + fmt.Println("\n\nInit payment request: ", req) payloadBytes, err := json.Marshal(req) if err != nil { + fmt.Println("\n\nWe are here") return "", fmt.Errorf("failed to serialize payload: %w", err) } httpReq, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/transaction/initialize", bytes.NewBuffer(payloadBytes)) if err != nil { + fmt.Println("\n\nWe are here 2") return "", fmt.Errorf("failed to create HTTP request: %w", err) } @@ -78,12 +81,14 @@ func (c *Client) InitPayment(ctx context.Context, req domain.InitPaymentRequest) resp, err := c.HTTPClient.Do(httpReq) if err != nil { + fmt.Println("\n\nWe are here 3") return "", fmt.Errorf("chapa HTTP request failed: %w", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { + fmt.Println("\n\nWe are here 4") return "", fmt.Errorf("chapa error: status %d, body: %s", resp.StatusCode, string(body)) } @@ -93,6 +98,8 @@ func (c *Client) InitPayment(ctx context.Context, req domain.InitPaymentRequest) } `json:"data"` } + fmt.Printf("\n\nInit payment response body: %v\n\n", response) + if err := json.Unmarshal(body, &response); err != nil { return "", fmt.Errorf("failed to parse chapa response: %w", err) } diff --git a/internal/services/chapa/service.go b/internal/services/chapa/service.go index 9c67ab4..08a573b 100644 --- a/internal/services/chapa/service.go +++ b/internal/services/chapa/service.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "time" // "log/slog" "strconv" @@ -116,6 +117,7 @@ func (s *Service) HandleChapaPaymentWebhook(ctx context.Context, req domain.Chap } return err } + if txn.Verified { return nil // already processed } @@ -170,29 +172,41 @@ func (s *Service) WithdrawUsingChapa(ctx context.Context, userID int64, req doma return fmt.Errorf("user not found: %w", err) } - branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID) - if err != nil { - return err - } - - wallets, err := s.walletStore.GetWalletsByUser(ctx, userID) - if err != nil { - return err - } - - var targetWallet *domain.Wallet - for _, w := range wallets { - if w.ID == req.WalletID { - targetWallet = &w + banks, err := s.GetSupportedBanks() + validBank := false + for _, bank := range banks { + if strconv.FormatInt(bank.Id, 10) == req.BankCode { + validBank = true break } } - - if targetWallet == nil { - return fmt.Errorf("no wallet found with the specified ID") + if !validBank { + return fmt.Errorf("invalid bank code") } - if !targetWallet.IsWithdraw || !targetWallet.IsActive { + // branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID) + // if err != nil { + // return err + // } + + var targetWallet domain.Wallet + targetWallet, err = s.walletStore.GetWalletByID(ctx, req.WalletID) + if err != nil { + return err + } + + // for _, w := range wallets { + // if w.ID == req.WalletID { + // targetWallet = &w + // break + // } + // } + + // if targetWallet == nil { + // return fmt.Errorf("no wallet found with the specified ID") + // } + + if !targetWallet.IsTransferable || !targetWallet.IsActive { return fmt.Errorf("wallet not eligible for withdrawal") } @@ -229,13 +243,13 @@ func (s *Service) WithdrawUsingChapa(ctx context.Context, userID int64, req doma BeneficiaryName: req.BeneficiaryName, PaymentOption: domain.PaymentOption(domain.BANK), BranchID: req.BranchID, - BranchName: branch.Name, - BranchLocation: branch.Location, + // BranchName: branch.Name, + // BranchLocation: branch.Location, // CashierID: user.ID, // CashierName: user.FullName, FullName: user.FirstName + " " + user.LastName, PhoneNumber: user.PhoneNumber, - CompanyID: branch.CompanyID, + // CompanyID: branch.CompanyID, }) if err != nil { return fmt.Errorf("failed to create transaction: %w", err) @@ -257,6 +271,10 @@ func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domai } defer tx.Rollback(ctx) + if req.Amount <= 0 { + return "", fmt.Errorf("amount must be positive") + } + user, err := s.userStore.GetUserByID(ctx, userID) if err != nil { return "", err @@ -269,20 +287,22 @@ func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domai txID := uuid.New().String() - _, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{ - Amount: req.Amount, - Type: domain.TransactionType(domain.TRANSACTION_DEPOSIT), - ReferenceNumber: txID, - BranchID: req.BranchID, - BranchName: branch.Name, - BranchLocation: branch.Location, - FullName: user.FirstName + " " + user.LastName, - PhoneNumber: user.PhoneNumber, - CompanyID: branch.CompanyID, - }) - if err != nil { - return "", err - } + fmt.Printf("\n\nChapa deposit transaction created: %v%v\n\n", branch, user) + + // _, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{ + // Amount: req.Amount, + // Type: domain.TransactionType(domain.TRANSACTION_DEPOSIT), + // ReferenceNumber: txID, + // BranchID: req.BranchID, + // BranchName: branch.Name, + // BranchLocation: branch.Location, + // FullName: user.FirstName + " " + user.LastName, + // PhoneNumber: user.PhoneNumber, + // // CompanyID: branch.CompanyID, + // }) + // if err != nil { + // return "", err + // } // Fetch user details for Chapa payment userInfo, err := s.userStore.GetUserByID(ctx, userID) @@ -290,6 +310,8 @@ func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domai return "", err } + // fmt.Printf("\n\nCallbackURL is:%v\n\n", s.config.CHAPA_CALLBACK_URL) + // Build Chapa InitPaymentRequest (matches Chapa API) paymentReq := domain.InitPaymentRequest{ Amount: req.Amount, @@ -298,14 +320,19 @@ func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domai FirstName: userInfo.FirstName, LastName: userInfo.LastName, TxRef: txID, - CallbackURL: s.config.CHAPA_CALLBACK_URL, - ReturnURL: s.config.CHAPA_RETURN_URL, + CallbackURL: "https://fortunebet.com/api/v1/payments/callback", + ReturnURL: "https://fortunebet.com/api/v1/payment-success", } // Call Chapa to initialize payment - paymentURL, err := s.chapaClient.InitPayment(ctx, paymentReq) - if err != nil { - return "", err + var paymentURL string + maxRetries := 3 + for range maxRetries { + paymentURL, err = s.chapaClient.InitPayment(ctx, paymentReq) + if err == nil { + break + } + time.Sleep(1 * time.Second) // Backoff } // Commit DB transaction diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 335c2d0..090cc97 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -47,7 +47,7 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { eventID, err := strconv.ParseInt(event.ID, 10, 64) if err != nil { - s.logger.Error("Failed to parse event id") + s.logger.Error("Failed to parse event id", "error", err.Error()) return err } @@ -58,6 +58,7 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { 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.logger.Error("Failed to create request for event%d: %v", strconv.FormatInt(eventID, 10), err.Error()) continue } diff --git a/internal/services/report/port.go b/internal/services/report/port.go new file mode 100644 index 0000000..7f592e8 --- /dev/null +++ b/internal/services/report/port.go @@ -0,0 +1,15 @@ +package report + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type ReportStore interface { + GetDashboardSummary(ctx context.Context, filter domain.ReportFilter) (DashboardSummary, error) + GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]BetAnalysis, error) + GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]CustomerActivity, error) + GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]BranchPerformance, error) + GetSportPerformance(ctx context.Context, filter domain.ReportFilter) ([]SportPerformance, error) +} diff --git a/internal/services/report/service.go b/internal/services/report/service.go new file mode 100644 index 0000000..854ef32 --- /dev/null +++ b/internal/services/report/service.go @@ -0,0 +1,509 @@ +package report + +import ( + "context" + "errors" + "log/slog" + "sort" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" +) + +var ( + ErrInvalidTimeRange = errors.New("invalid time range - start time must be before end time") + ErrInvalidReportCriteria = errors.New("invalid report criteria") +) + +type Service struct { + betStore bet.BetStore + walletStore wallet.WalletStore + transactionStore transaction.TransactionStore + branchStore branch.BranchStore + userStore user.UserStore + logger *slog.Logger +} + +func NewService( + betStore bet.BetStore, + walletStore wallet.WalletStore, + transactionStore transaction.TransactionStore, + branchStore branch.BranchStore, + userStore user.UserStore, + logger *slog.Logger, +) *Service { + return &Service{ + betStore: betStore, + walletStore: walletStore, + transactionStore: transactionStore, + branchStore: branchStore, + userStore: userStore, + logger: logger, + } +} + +// DashboardSummary represents comprehensive dashboard metrics +type DashboardSummary struct { + TotalStakes domain.Currency `json:"total_stakes"` + TotalBets int64 `json:"total_bets"` + ActiveBets int64 `json:"active_bets"` + WinBalance domain.Currency `json:"win_balance"` + TotalWins int64 `json:"total_wins"` + TotalLosses int64 `json:"total_losses"` + CustomerCount int64 `json:"customer_count"` + Profit domain.Currency `json:"profit"` + WinRate float64 `json:"win_rate"` + AverageStake domain.Currency `json:"average_stake"` + TotalDeposits domain.Currency `json:"total_deposits"` + TotalWithdrawals domain.Currency `json:"total_withdrawals"` + ActiveCustomers int64 `json:"active_customers"` + BranchesCount int64 `json:"branches_count"` + ActiveBranches int64 `json:"active_branches"` +} + +// GetDashboardSummary returns comprehensive dashboard metrics +func (s *Service) GetDashboardSummary(ctx context.Context, filter domain.ReportFilter) (DashboardSummary, error) { + if err := validateTimeRange(filter); err != nil { + return DashboardSummary{}, err + } + + var summary DashboardSummary + var err error + + // Get bets summary + summary.TotalStakes, summary.TotalBets, summary.ActiveBets, summary.TotalWins, summary.TotalLosses, summary.WinBalance, err = + s.betStore.GetBetSummary(ctx, filter) + if err != nil { + s.logger.Error("failed to get bet summary", "error", err) + return DashboardSummary{}, err + } + + // Get customer metrics + summary.CustomerCount, summary.ActiveCustomers, err = s.userStore.GetCustomerCounts(ctx, filter) + if err != nil { + s.logger.Error("failed to get customer counts", "error", err) + return DashboardSummary{}, err + } + + // Get branch metrics + summary.BranchesCount, summary.ActiveBranches, err = s.branchStore.GetBranchCounts(ctx, filter) + if err != nil { + s.logger.Error("failed to get branch counts", "error", err) + return DashboardSummary{}, err + } + + // Get transaction metrics + summary.TotalDeposits, summary.TotalWithdrawals, err = s.transactionStore.GetTransactionTotals(ctx, filter) + if err != nil { + s.logger.Error("failed to get transaction totals", "error", err) + return DashboardSummary{}, err + } + + // Calculate derived metrics + if summary.TotalBets > 0 { + summary.AverageStake = summary.TotalStakes / domain.Currency(summary.TotalBets) + summary.WinRate = float64(summary.TotalWins) / float64(summary.TotalBets) * 100 + summary.Profit = summary.TotalStakes - summary.WinBalance + } + + return summary, nil +} + +// BetAnalysis represents detailed bet analysis +type BetAnalysis struct { + Date time.Time `json:"date"` + TotalBets int64 `json:"total_bets"` + TotalStakes domain.Currency `json:"total_stakes"` + TotalWins int64 `json:"total_wins"` + TotalPayouts domain.Currency `json:"total_payouts"` + Profit domain.Currency `json:"profit"` + MostPopularSport string `json:"most_popular_sport"` + MostPopularMarket string `json:"most_popular_market"` + HighestStake domain.Currency `json:"highest_stake"` + HighestPayout domain.Currency `json:"highest_payout"` + AverageOdds float64 `json:"average_odds"` +} + +// GetBetAnalysis returns detailed bet analysis +func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]BetAnalysis, error) { + if err := validateTimeRange(filter); err != nil { + return nil, err + } + + // Get basic bet stats + betStats, err := s.betStore.GetBetStats(ctx, filter) + if err != nil { + s.logger.Error("failed to get bet stats", "error", err) + return nil, err + } + + // Get sport popularity + sportPopularity, err := s.betStore.GetSportPopularity(ctx, filter) + if err != nil { + s.logger.Error("failed to get sport popularity", "error", err) + return nil, err + } + + // Get market popularity + marketPopularity, err := s.betStore.GetMarketPopularity(ctx, filter) + if err != nil { + s.logger.Error("failed to get market popularity", "error", err) + return nil, err + } + + // Get extreme values + extremeValues, err := s.betStore.GetExtremeValues(ctx, filter) + if err != nil { + s.logger.Error("failed to get extreme values", "error", err) + return nil, err + } + + // Combine data into analysis + var analysis []BetAnalysis + for _, stat := range betStats { + a := BetAnalysis{ + Date: stat.Date, + TotalBets: stat.TotalBets, + TotalStakes: stat.TotalStakes, + TotalWins: stat.TotalWins, + TotalPayouts: stat.TotalPayouts, + Profit: stat.TotalStakes - stat.TotalPayouts, + AverageOdds: stat.AverageOdds, + } + + // Add sport popularity + if sport, ok := sportPopularity[stat.Date]; ok { + a.MostPopularSport = sport + } + + // Add market popularity + if market, ok := marketPopularity[stat.Date]; ok { + a.MostPopularMarket = market + } + + // Add extreme values + if extremes, ok := extremeValues[stat.Date]; ok { + a.HighestStake = extremes.HighestStake + a.HighestPayout = extremes.HighestPayout + } + + analysis = append(analysis, a) + } + + // Sort by date + sort.Slice(analysis, func(i, j int) bool { + return analysis[i].Date.Before(analysis[j].Date) + }) + + return analysis, nil +} + +// CustomerActivity represents customer activity metrics +type CustomerActivity struct { + CustomerID int64 `json:"customer_id"` + CustomerName string `json:"customer_name"` + TotalBets int64 `json:"total_bets"` + TotalStakes domain.Currency `json:"total_stakes"` + TotalWins int64 `json:"total_wins"` + TotalPayouts domain.Currency `json:"total_payouts"` + Profit domain.Currency `json:"profit"` + FirstBetDate time.Time `json:"first_bet_date"` + LastBetDate time.Time `json:"last_bet_date"` + FavoriteSport string `json:"favorite_sport"` + FavoriteMarket string `json:"favorite_market"` + AverageStake domain.Currency `json:"average_stake"` + AverageOdds float64 `json:"average_odds"` + WinRate float64 `json:"win_rate"` + ActivityLevel string `json:"activity_level"` // High, Medium, Low +} + +// GetCustomerActivity returns customer activity report +func (s *Service) GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]CustomerActivity, error) { + if err := validateTimeRange(filter); err != nil { + return nil, err + } + + // Get customer bet activity + customerBets, err := s.betStore.GetCustomerBetActivity(ctx, filter) + if err != nil { + s.logger.Error("failed to get customer bet activity", "error", err) + return nil, err + } + + // Get customer details + customerDetails, err := s.userStore.GetCustomerDetails(ctx, filter) + if err != nil { + s.logger.Error("failed to get customer details", "error", err) + return nil, err + } + + // Get customer preferences + customerPrefs, err := s.betStore.GetCustomerPreferences(ctx, filter) + if err != nil { + s.logger.Error("failed to get customer preferences", "error", err) + return nil, err + } + + // Combine data into activity report + var activities []CustomerActivity + for _, bet := range customerBets { + activity := CustomerActivity{ + CustomerID: bet.CustomerID, + TotalBets: bet.TotalBets, + TotalStakes: bet.TotalStakes, + TotalWins: bet.TotalWins, + TotalPayouts: bet.TotalPayouts, + Profit: bet.TotalStakes - bet.TotalPayouts, + FirstBetDate: bet.FirstBetDate, + LastBetDate: bet.LastBetDate, + AverageStake: bet.TotalStakes / domain.Currency(bet.TotalBets), + AverageOdds: bet.AverageOdds, + } + + // Add customer details + if details, ok := customerDetails[bet.CustomerID]; ok { + activity.CustomerName = details.Name + } + + // Add preferences + if prefs, ok := customerPrefs[bet.CustomerID]; ok { + activity.FavoriteSport = prefs.FavoriteSport + activity.FavoriteMarket = prefs.FavoriteMarket + } + + // Calculate win rate + if bet.TotalBets > 0 { + activity.WinRate = float64(bet.TotalWins) / float64(bet.TotalBets) * 100 + } + + // Determine activity level + activity.ActivityLevel = calculateActivityLevel(bet.TotalBets, bet.TotalStakes) + + activities = append(activities, activity) + } + + // Sort by total stakes (descending) + sort.Slice(activities, func(i, j int) bool { + return activities[i].TotalStakes > activities[j].TotalStakes + }) + + return activities, nil +} + +// BranchPerformance represents branch performance metrics +type BranchPerformance struct { + BranchID int64 `json:"branch_id"` + BranchName string `json:"branch_name"` + Location string `json:"location"` + ManagerName string `json:"manager_name"` + TotalBets int64 `json:"total_bets"` + TotalStakes domain.Currency `json:"total_stakes"` + TotalWins int64 `json:"total_wins"` + TotalPayouts domain.Currency `json:"total_payouts"` + Profit domain.Currency `json:"profit"` + CustomerCount int64 `json:"customer_count"` + Deposits domain.Currency `json:"deposits"` + Withdrawals domain.Currency `json:"withdrawals"` + WinRate float64 `json:"win_rate"` + AverageStake domain.Currency `json:"average_stake"` + PerformanceScore float64 `json:"performance_score"` +} + +// GetBranchPerformance returns branch performance report +func (s *Service) GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]BranchPerformance, error) { + // Get branch bet activity + branchBets, err := s.betStore.GetBranchBetActivity(ctx, filter) + if err != nil { + s.logger.Error("failed to get branch bet activity", "error", err) + return nil, err + } + + // Get branch details + branchDetails, err := s.branchStore.GetBranchDetails(ctx, filter) + if err != nil { + s.logger.Error("failed to get branch details", "error", err) + return nil, err + } + + // Get branch transactions + branchTransactions, err := s.transactionStore.GetBranchTransactionTotals(ctx, filter) + if err != nil { + s.logger.Error("failed to get branch transactions", "error", err) + return nil, err + } + + // Get branch customer counts + branchCustomers, err := s.userStore.GetBranchCustomerCounts(ctx, filter) + if err != nil { + s.logger.Error("failed to get branch customer counts", "error", err) + return nil, err + } + + // Combine data into performance report + var performances []BranchPerformance + for _, bet := range branchBets { + performance := BranchPerformance{ + BranchID: bet.BranchID, + TotalBets: bet.TotalBets, + TotalStakes: bet.TotalStakes, + TotalWins: bet.TotalWins, + TotalPayouts: bet.TotalPayouts, + Profit: bet.TotalStakes - bet.TotalPayouts, + } + + // Add branch details + if details, ok := branchDetails[bet.BranchID]; ok { + performance.BranchName = details.Name + performance.Location = details.Location + performance.ManagerName = details.ManagerName + } + + // Add transactions + if transactions, ok := branchTransactions[bet.BranchID]; ok { + performance.Deposits = transactions.Deposits + performance.Withdrawals = transactions.Withdrawals + } + + // Add customer counts + if customers, ok := branchCustomers[bet.BranchID]; ok { + performance.CustomerCount = customers + } + + // Calculate metrics + if bet.TotalBets > 0 { + performance.WinRate = float64(bet.TotalWins) / float64(bet.TotalBets) * 100 + performance.AverageStake = bet.TotalStakes / domain.Currency(bet.TotalBets) + } + + // Calculate performance score + performance.PerformanceScore = calculatePerformanceScore(performance) + + performances = append(performances, performance) + } + + // Sort by performance score (descending) + sort.Slice(performances, func(i, j int) bool { + return performances[i].PerformanceScore > performances[j].PerformanceScore + }) + + return performances, nil +} + +// SportPerformance represents sport performance metrics +type SportPerformance struct { + SportID string `json:"sport_id"` + SportName string `json:"sport_name"` + TotalBets int64 `json:"total_bets"` + TotalStakes domain.Currency `json:"total_stakes"` + TotalWins int64 `json:"total_wins"` + TotalPayouts domain.Currency `json:"total_payouts"` + Profit domain.Currency `json:"profit"` + PopularityRank int `json:"popularity_rank"` + WinRate float64 `json:"win_rate"` + AverageStake domain.Currency `json:"average_stake"` + AverageOdds float64 `json:"average_odds"` + MostPopularMarket string `json:"most_popular_market"` +} + +// GetSportPerformance returns sport performance report +func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportFilter) ([]SportPerformance, error) { + // Get sport bet activity + sportBets, err := s.betStore.GetSportBetActivity(ctx, filter) + if err != nil { + s.logger.Error("failed to get sport bet activity", "error", err) + return nil, err + } + + // Get sport details (names) + sportDetails, err := s.betStore.GetSportDetails(ctx, filter) + if err != nil { + s.logger.Error("failed to get sport details", "error", err) + return nil, err + } + + // Get sport market popularity + sportMarkets, err := s.betStore.GetSportMarketPopularity(ctx, filter) + if err != nil { + s.logger.Error("failed to get sport market popularity", "error", err) + return nil, err + } + + // Combine data into performance report + var performances []SportPerformance + for _, bet := range sportBets { + performance := SportPerformance{ + SportID: bet.SportID, + TotalBets: bet.TotalBets, + TotalStakes: bet.TotalStakes, + TotalWins: bet.TotalWins, + TotalPayouts: bet.TotalPayouts, + Profit: bet.TotalStakes - bet.TotalPayouts, + AverageOdds: bet.AverageOdds, + } + + // Add sport details + if details, ok := sportDetails[bet.SportID]; ok { + performance.SportName = details + } + + // Add market popularity + if market, ok := sportMarkets[bet.SportID]; ok { + performance.MostPopularMarket = market + } + + // Calculate metrics + if bet.TotalBets > 0 { + performance.WinRate = float64(bet.TotalWins) / float64(bet.TotalBets) * 100 + performance.AverageStake = bet.TotalStakes / domain.Currency(bet.TotalBets) + } + + performances = append(performances, performance) + } + + // Sort by total stakes (descending) and assign popularity rank + sort.Slice(performances, func(i, j int) bool { + return performances[i].TotalStakes > performances[j].TotalStakes + }) + + for i := range performances { + performances[i].PopularityRank = i + 1 + } + + return performances, nil +} + +// Helper functions +func validateTimeRange(filter domain.ReportFilter) error { + if filter.StartTime.Valid && filter.EndTime.Valid { + if filter.StartTime.Value.After(filter.EndTime.Value) { + return ErrInvalidTimeRange + } + } + return nil +} + +func calculateActivityLevel(totalBets int64, totalStakes domain.Currency) string { + switch { + case totalBets > 100 || totalStakes > 10000: + return "High" + case totalBets > 50 || totalStakes > 5000: + return "Medium" + default: + return "Low" + } +} + +func calculatePerformanceScore(perf BranchPerformance) float64 { + // Simple scoring algorithm - can be enhanced based on business rules + profitScore := float64(perf.Profit) / 1000 + customerScore := float64(perf.CustomerCount) * 0.1 + betScore := float64(perf.TotalBets) * 0.01 + winRateScore := perf.WinRate * 0.1 + + return profitScore + customerScore + betScore + winRateScore +} diff --git a/internal/services/result/sports_eval_test.go b/internal/services/result/sports_eval_test.go index 9132658..f45bd58 100644 --- a/internal/services/result/sports_eval_test.go +++ b/internal/services/result/sports_eval_test.go @@ -1,980 +1,980 @@ package result -import ( - "fmt" - "testing" - "time" +// import ( +// "fmt" +// "testing" +// "time" - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/stretchr/testify/assert" -) +// "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +// "github.com/stretchr/testify/assert" +// ) -// TestNFLMarkets covers all American Football (NFL) market types defined in the domain. -// For each market (Money Line, Spread, Total Points), it tests home/away win, draw, void, and invalid input scenarios. -func TestNFLMarkets(t *testing.T) { - t.Log("Testing NFL (American Football) Markets") - markets := []struct { - marketID int64 - name string - }{ - {int64(domain.AMERICAN_FOOTBALL_MONEY_LINE), "MONEY_LINE"}, - {int64(domain.AMERICAN_FOOTBALL_SPREAD), "SPREAD"}, - {int64(domain.AMERICAN_FOOTBALL_TOTAL_POINTS), "TOTAL_POINTS"}, - } +// // TestNFLMarkets covers all American Football (NFL) market types defined in the domain. +// // For each market (Money Line, Spread, Total Points), it tests home/away win, draw, void, and invalid input scenarios. +// func TestNFLMarkets(t *testing.T) { +// t.Log("Testing NFL (American Football) Markets") +// markets := []struct { +// marketID int64 +// name string +// }{ +// {int64(domain.AMERICAN_FOOTBALL_MONEY_LINE), "MONEY_LINE"}, +// {int64(domain.AMERICAN_FOOTBALL_SPREAD), "SPREAD"}, +// {int64(domain.AMERICAN_FOOTBALL_TOTAL_POINTS), "TOTAL_POINTS"}, +// } - for _, m := range markets { - t.Run(m.name, func(t *testing.T) { - // Each subtest below covers a key scenario for the given NFL market. - switch m.marketID { - case int64(domain.AMERICAN_FOOTBALL_MONEY_LINE): - // Home win, away win, draw, and invalid OddHeader for Money Line - t.Run("Home Win", func(t *testing.T) { - status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 21, Away: 14}) - t.Logf("Market: %s, Scenario: Home Win", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Away Win", func(t *testing.T) { - status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 14, Away: 21}) - t.Logf("Market: %s, Scenario: Away Win", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Draw", func(t *testing.T) { - status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 17, Away: 17}) - t.Logf("Market: %s, Scenario: Draw", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status) - }) - t.Run("Invalid OddHeader", func(t *testing.T) { - status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) - t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name) - assert.Error(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) - }) - case int64(domain.AMERICAN_FOOTBALL_SPREAD): - t.Run("Home Win with Handicap", func(t *testing.T) { - status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-3.5"}, struct{ Home, Away int }{Home: 24, Away: 20}) - t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Away Win with Handicap", func(t *testing.T) { - status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+3.5"}, struct{ Home, Away int }{Home: 20, Away: 24}) - t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Push (Void)", func(t *testing.T) { - status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21}) - t.Logf("Market: %s, Scenario: Push (Void)", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) - }) - t.Run("Non-numeric Handicap", func(t *testing.T) { - status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14}) - t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name) - assert.Error(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) - }) - case int64(domain.AMERICAN_FOOTBALL_TOTAL_POINTS): - t.Run("Over Win", func(t *testing.T) { - status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "44.5"}, struct{ Home, Away int }{Home: 30, Away: 20}) - t.Logf("Market: %s, Scenario: Over Win", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Under Win", func(t *testing.T) { - status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "44.5"}, struct{ Home, Away int }{Home: 20, Away: 17}) - t.Logf("Market: %s, Scenario: Under Win", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Push (Void)", func(t *testing.T) { - status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "37"}, struct{ Home, Away int }{Home: 20, Away: 17}) - t.Logf("Market: %s, Scenario: Push (Void)", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) - }) - t.Run("Non-numeric OddName", func(t *testing.T) { - status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 17}) - t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name) - assert.Error(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) - }) - } - }) - } -} +// for _, m := range markets { +// t.Run(m.name, func(t *testing.T) { +// // Each subtest below covers a key scenario for the given NFL market. +// switch m.marketID { +// case int64(domain.AMERICAN_FOOTBALL_MONEY_LINE): +// // Home win, away win, draw, and invalid OddHeader for Money Line +// t.Run("Home Win", func(t *testing.T) { +// status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 21, Away: 14}) +// t.Logf("Market: %s, Scenario: Home Win", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Away Win", func(t *testing.T) { +// status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 14, Away: 21}) +// t.Logf("Market: %s, Scenario: Away Win", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Draw", func(t *testing.T) { +// status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 17, Away: 17}) +// t.Logf("Market: %s, Scenario: Draw", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status) +// }) +// t.Run("Invalid OddHeader", func(t *testing.T) { +// status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) +// t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name) +// assert.Error(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) +// }) +// case int64(domain.AMERICAN_FOOTBALL_SPREAD): +// t.Run("Home Win with Handicap", func(t *testing.T) { +// status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-3.5"}, struct{ Home, Away int }{Home: 24, Away: 20}) +// t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Away Win with Handicap", func(t *testing.T) { +// status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+3.5"}, struct{ Home, Away int }{Home: 20, Away: 24}) +// t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Push (Void)", func(t *testing.T) { +// status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21}) +// t.Logf("Market: %s, Scenario: Push (Void)", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) +// }) +// t.Run("Non-numeric Handicap", func(t *testing.T) { +// status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14}) +// t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name) +// assert.Error(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) +// }) +// case int64(domain.AMERICAN_FOOTBALL_TOTAL_POINTS): +// t.Run("Over Win", func(t *testing.T) { +// status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "44.5"}, struct{ Home, Away int }{Home: 30, Away: 20}) +// t.Logf("Market: %s, Scenario: Over Win", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Under Win", func(t *testing.T) { +// status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "44.5"}, struct{ Home, Away int }{Home: 20, Away: 17}) +// t.Logf("Market: %s, Scenario: Under Win", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Push (Void)", func(t *testing.T) { +// status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "37"}, struct{ Home, Away int }{Home: 20, Away: 17}) +// t.Logf("Market: %s, Scenario: Push (Void)", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) +// }) +// t.Run("Non-numeric OddName", func(t *testing.T) { +// status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 17}) +// t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name) +// assert.Error(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) +// }) +// } +// }) +// } +// } -// TestRugbyMarkets covers all Rugby (Union & League) market types defined in the domain. -// For each market (Money Line, Spread, Handicap, Total Points), it tests home/away win, draw, void, and invalid input scenarios. -func TestRugbyMarkets(t *testing.T) { - t.Log("Testing Rugby Markets (Union & League)") - markets := []struct { - marketID int64 - name string - }{ - {int64(domain.RUGBY_MONEY_LINE), "MONEY_LINE"}, - {int64(domain.RUGBY_SPREAD), "SPREAD"}, - {int64(domain.RUGBY_TOTAL_POINTS), "TOTAL_POINTS"}, - {int64(domain.RUGBY_HANDICAP), "HANDICAP"}, - } +// // TestRugbyMarkets covers all Rugby (Union & League) market types defined in the domain. +// // For each market (Money Line, Spread, Handicap, Total Points), it tests home/away win, draw, void, and invalid input scenarios. +// func TestRugbyMarkets(t *testing.T) { +// t.Log("Testing Rugby Markets (Union & League)") +// markets := []struct { +// marketID int64 +// name string +// }{ +// {int64(domain.RUGBY_MONEY_LINE), "MONEY_LINE"}, +// {int64(domain.RUGBY_SPREAD), "SPREAD"}, +// {int64(domain.RUGBY_TOTAL_POINTS), "TOTAL_POINTS"}, +// {int64(domain.RUGBY_HANDICAP), "HANDICAP"}, +// } - for _, m := range markets { - t.Run(m.name, func(t *testing.T) { - // Each subtest below covers a key scenario for the given Rugby market. - switch m.marketID { - case int64(domain.RUGBY_MONEY_LINE): - // Home win, away win, draw, and invalid OddHeader for Money Line - t.Run("Home Win", func(t *testing.T) { - status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 30, Away: 20}) - t.Logf("Market: %s, Scenario: Home Win", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Away Win", func(t *testing.T) { - status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 20, Away: 30}) - t.Logf("Market: %s, Scenario: Away Win", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Draw", func(t *testing.T) { - status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 25, Away: 25}) - t.Logf("Market: %s, Scenario: Draw", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status) - }) - t.Run("Invalid OddHeader", func(t *testing.T) { - status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) - t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name) - assert.Error(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) - }) - case int64(domain.RUGBY_SPREAD), int64(domain.RUGBY_HANDICAP): - t.Run("Home Win with Handicap", func(t *testing.T) { - status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-6.5"}, struct{ Home, Away int }{Home: 28, Away: 20}) - t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Away Win with Handicap", func(t *testing.T) { - status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+6.5"}, struct{ Home, Away int }{Home: 20, Away: 28}) - t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Push (Void)", func(t *testing.T) { - status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21}) - t.Logf("Market: %s, Scenario: Push (Void)", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) - }) - t.Run("Non-numeric Handicap", func(t *testing.T) { - status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14}) - t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name) - assert.Error(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) - }) - case int64(domain.RUGBY_TOTAL_POINTS): - t.Run("Over Win", func(t *testing.T) { - status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "40.5"}, struct{ Home, Away int }{Home: 25, Away: 20}) - t.Logf("Market: %s, Scenario: Over Win", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Under Win", func(t *testing.T) { - status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "40.5"}, struct{ Home, Away int }{Home: 15, Away: 20}) - t.Logf("Market: %s, Scenario: Under Win", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Push (Void)", func(t *testing.T) { - status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "35"}, struct{ Home, Away int }{Home: 20, Away: 15}) - t.Logf("Market: %s, Scenario: Push (Void)", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) - }) - t.Run("Non-numeric OddName", func(t *testing.T) { - status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 15}) - t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name) - assert.Error(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) - }) - } - }) - } -} +// for _, m := range markets { +// t.Run(m.name, func(t *testing.T) { +// // Each subtest below covers a key scenario for the given Rugby market. +// switch m.marketID { +// case int64(domain.RUGBY_MONEY_LINE): +// // Home win, away win, draw, and invalid OddHeader for Money Line +// t.Run("Home Win", func(t *testing.T) { +// status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 30, Away: 20}) +// t.Logf("Market: %s, Scenario: Home Win", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Away Win", func(t *testing.T) { +// status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 20, Away: 30}) +// t.Logf("Market: %s, Scenario: Away Win", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Draw", func(t *testing.T) { +// status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 25, Away: 25}) +// t.Logf("Market: %s, Scenario: Draw", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status) +// }) +// t.Run("Invalid OddHeader", func(t *testing.T) { +// status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) +// t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name) +// assert.Error(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) +// }) +// case int64(domain.RUGBY_SPREAD), int64(domain.RUGBY_HANDICAP): +// t.Run("Home Win with Handicap", func(t *testing.T) { +// status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-6.5"}, struct{ Home, Away int }{Home: 28, Away: 20}) +// t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Away Win with Handicap", func(t *testing.T) { +// status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+6.5"}, struct{ Home, Away int }{Home: 20, Away: 28}) +// t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Push (Void)", func(t *testing.T) { +// status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21}) +// t.Logf("Market: %s, Scenario: Push (Void)", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) +// }) +// t.Run("Non-numeric Handicap", func(t *testing.T) { +// status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14}) +// t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name) +// assert.Error(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) +// }) +// case int64(domain.RUGBY_TOTAL_POINTS): +// t.Run("Over Win", func(t *testing.T) { +// status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "40.5"}, struct{ Home, Away int }{Home: 25, Away: 20}) +// t.Logf("Market: %s, Scenario: Over Win", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Under Win", func(t *testing.T) { +// status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "40.5"}, struct{ Home, Away int }{Home: 15, Away: 20}) +// t.Logf("Market: %s, Scenario: Under Win", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Push (Void)", func(t *testing.T) { +// status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "35"}, struct{ Home, Away int }{Home: 20, Away: 15}) +// t.Logf("Market: %s, Scenario: Push (Void)", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) +// }) +// t.Run("Non-numeric OddName", func(t *testing.T) { +// status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 15}) +// t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name) +// assert.Error(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) +// }) +// } +// }) +// } +// } -// TestBaseballMarkets covers all Baseball market types defined in the domain. -// For each market (Money Line, Spread, Total Runs), it tests home/away win, draw, void, and invalid input scenarios. -func TestBaseballMarkets(t *testing.T) { - t.Log("Testing Baseball Markets") - markets := []struct { - marketID int64 - name string - }{ - {int64(domain.BASEBALL_MONEY_LINE), "MONEY_LINE"}, - {int64(domain.BASEBALL_SPREAD), "SPREAD"}, - {int64(domain.BASEBALL_TOTAL_RUNS), "TOTAL_RUNS"}, - } +// // TestBaseballMarkets covers all Baseball market types defined in the domain. +// // For each market (Money Line, Spread, Total Runs), it tests home/away win, draw, void, and invalid input scenarios. +// func TestBaseballMarkets(t *testing.T) { +// t.Log("Testing Baseball Markets") +// markets := []struct { +// marketID int64 +// name string +// }{ +// {int64(domain.BASEBALL_MONEY_LINE), "MONEY_LINE"}, +// {int64(domain.BASEBALL_SPREAD), "SPREAD"}, +// {int64(domain.BASEBALL_TOTAL_RUNS), "TOTAL_RUNS"}, +// } - for _, m := range markets { - t.Run(m.name, func(t *testing.T) { - // Each subtest below covers a key scenario for the given Baseball market. - switch m.marketID { - case int64(domain.BASEBALL_MONEY_LINE): - // Home win, away win, draw, and invalid OddHeader for Money Line - t.Run("Home Win", func(t *testing.T) { - status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 6, Away: 3}) - t.Logf("Market: %s, Scenario: Home Win", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Away Win", func(t *testing.T) { - status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 2, Away: 5}) - t.Logf("Market: %s, Scenario: Away Win", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Draw", func(t *testing.T) { - status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 4, Away: 4}) - t.Logf("Market: %s, Scenario: Draw", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status) - }) - t.Run("Invalid OddHeader", func(t *testing.T) { - status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) - t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name) - assert.Error(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) - }) - case int64(domain.BASEBALL_SPREAD): - t.Run("Home Win with Handicap", func(t *testing.T) { - status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-1.5"}, struct{ Home, Away int }{Home: 5, Away: 3}) - t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Away Win with Handicap", func(t *testing.T) { - status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+1.5"}, struct{ Home, Away int }{Home: 3, Away: 5}) - t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Push (Void)", func(t *testing.T) { - status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 4, Away: 4}) - t.Logf("Market: %s, Scenario: Push (Void)", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) - }) - t.Run("Non-numeric Handicap", func(t *testing.T) { - status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 5, Away: 3}) - t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name) - assert.Error(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) - }) - case int64(domain.BASEBALL_TOTAL_RUNS): - t.Run("Over Win", func(t *testing.T) { - status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7.5"}, struct{ Home, Away int }{Home: 5, Away: 4}) - t.Logf("Market: %s, Scenario: Over Win", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Under Win", func(t *testing.T) { - status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "7.5"}, struct{ Home, Away int }{Home: 2, Away: 3}) - t.Logf("Market: %s, Scenario: Under Win", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) - }) - t.Run("Push (Void)", func(t *testing.T) { - status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7"}, struct{ Home, Away int }{Home: 4, Away: 3}) - t.Logf("Market: %s, Scenario: Push (Void)", m.name) - assert.NoError(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) - }) - t.Run("Non-numeric OddName", func(t *testing.T) { - status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 4, Away: 3}) - t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name) - assert.Error(t, err) - assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) - }) - } - }) - } -} +// for _, m := range markets { +// t.Run(m.name, func(t *testing.T) { +// // Each subtest below covers a key scenario for the given Baseball market. +// switch m.marketID { +// case int64(domain.BASEBALL_MONEY_LINE): +// // Home win, away win, draw, and invalid OddHeader for Money Line +// t.Run("Home Win", func(t *testing.T) { +// status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 6, Away: 3}) +// t.Logf("Market: %s, Scenario: Home Win", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Away Win", func(t *testing.T) { +// status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 2, Away: 5}) +// t.Logf("Market: %s, Scenario: Away Win", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Draw", func(t *testing.T) { +// status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 4, Away: 4}) +// t.Logf("Market: %s, Scenario: Draw", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status) +// }) +// t.Run("Invalid OddHeader", func(t *testing.T) { +// status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) +// t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name) +// assert.Error(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) +// }) +// case int64(domain.BASEBALL_SPREAD): +// t.Run("Home Win with Handicap", func(t *testing.T) { +// status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-1.5"}, struct{ Home, Away int }{Home: 5, Away: 3}) +// t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Away Win with Handicap", func(t *testing.T) { +// status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+1.5"}, struct{ Home, Away int }{Home: 3, Away: 5}) +// t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Push (Void)", func(t *testing.T) { +// status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 4, Away: 4}) +// t.Logf("Market: %s, Scenario: Push (Void)", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) +// }) +// t.Run("Non-numeric Handicap", func(t *testing.T) { +// status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 5, Away: 3}) +// t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name) +// assert.Error(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) +// }) +// case int64(domain.BASEBALL_TOTAL_RUNS): +// t.Run("Over Win", func(t *testing.T) { +// status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7.5"}, struct{ Home, Away int }{Home: 5, Away: 4}) +// t.Logf("Market: %s, Scenario: Over Win", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Under Win", func(t *testing.T) { +// status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "7.5"}, struct{ Home, Away int }{Home: 2, Away: 3}) +// t.Logf("Market: %s, Scenario: Under Win", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) +// }) +// t.Run("Push (Void)", func(t *testing.T) { +// status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7"}, struct{ Home, Away int }{Home: 4, Away: 3}) +// t.Logf("Market: %s, Scenario: Push (Void)", m.name) +// assert.NoError(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) +// }) +// t.Run("Non-numeric OddName", func(t *testing.T) { +// status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 4, Away: 3}) +// t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name) +// assert.Error(t, err) +// assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) +// }) +// } +// }) +// } +// } -func TestEvaluateFootballOutcome(t *testing.T) { - service := &Service{} // or your real logger +// func TestEvaluateFootballOutcome(t *testing.T) { +// service := &Service{} // or your real logger - // Mock outcome - outcome := domain.BetOutcome{ - ID: 1, - BetID: 1, - EventID: 1001, - OddID: 2001, - SportID: 1, // Assuming 1 = Football - HomeTeamName: "Manchester", - AwayTeamName: "Liverpool", - MarketID: int64(domain.FOOTBALL_FULL_TIME_RESULT), - MarketName: "Full Time Result", - Odd: 1.75, - OddName: "2", // Home win - OddHeader: "1", - OddHandicap: "", - Status: domain.OUTCOME_STATUS_PENDING, // Initial status - Expires: time.Now().Add(24 * time.Hour), - } +// // Mock outcome +// outcome := domain.BetOutcome{ +// ID: 1, +// BetID: 1, +// EventID: 1001, +// OddID: 2001, +// SportID: 1, // Assuming 1 = Football +// HomeTeamName: "Manchester", +// AwayTeamName: "Liverpool", +// MarketID: int64(domain.FOOTBALL_FULL_TIME_RESULT), +// MarketName: "Full Time Result", +// Odd: 1.75, +// OddName: "2", // Home win +// OddHeader: "1", +// OddHandicap: "", +// Status: domain.OUTCOME_STATUS_PENDING, // Initial status +// Expires: time.Now().Add(24 * time.Hour), +// } - // Parsed result (simulate Bet365 JSON) - finalScore := struct{ Home, Away int }{Home: 2, Away: 1} - firstHalfScore := struct{ Home, Away int }{Home: 1, Away: 1} - secondHalfScore := struct{ Home, Away int }{Home: 1, Away: 0} - corners := struct{ Home, Away int }{Home: 5, Away: 3} - halfTimeCorners := struct{ Home, Away int }{Home: 2, Away: 2} - events := []map[string]string{ - {"type": "goal", "team": "home", "minute": "23"}, - {"type": "goal", "team": "away", "minute": "34"}, - } +// // Parsed result (simulate Bet365 JSON) +// finalScore := struct{ Home, Away int }{Home: 2, Away: 1} +// firstHalfScore := struct{ Home, Away int }{Home: 1, Away: 1} +// secondHalfScore := struct{ Home, Away int }{Home: 1, Away: 0} +// corners := struct{ Home, Away int }{Home: 5, Away: 3} +// halfTimeCorners := struct{ Home, Away int }{Home: 2, Away: 2} +// events := []map[string]string{ +// {"type": "goal", "team": "home", "minute": "23"}, +// {"type": "goal", "team": "away", "minute": "34"}, +// } - // Act - status, _ := service.EvaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, events) +// // Act +// status, _ := service.EvaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, events) - fmt.Printf("\n\nBet Outcome: %v\n\n", &status) +// fmt.Printf("\n\nBet Outcome: %v\n\n", &status) -} -func TestEvaluateTotalLegs(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"OverTotalLegs", domain.BetOutcome{OddName: "3", OddHeader: "Over"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_WIN}, - {"OverTotalLegs", domain.BetOutcome{OddName: "3", OddHeader: "Under"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, - {"UnderTotalLegs", domain.BetOutcome{OddName: "7", OddHeader: "Under"}, struct{ Home, Away int }{2, 3}, domain.OUTCOME_STATUS_WIN}, - } +// } +// func TestEvaluateTotalLegs(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"OverTotalLegs", domain.BetOutcome{OddName: "3", OddHeader: "Over"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_WIN}, +// {"OverTotalLegs", domain.BetOutcome{OddName: "3", OddHeader: "Under"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, +// {"UnderTotalLegs", domain.BetOutcome{OddName: "7", OddHeader: "Under"}, struct{ Home, Away int }{2, 3}, domain.OUTCOME_STATUS_WIN}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateTotalLegs(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateTotalLegs(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateGameLines(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"GameLines - Total", domain.BetOutcome{OddName: "Total", OddHandicap: "O 5.5"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_WIN}, - {"GameLines - Total", domain.BetOutcome{OddName: "Total", OddHandicap: "O 5.5"}, struct{ Home, Away int }{2, 3}, domain.OUTCOME_STATUS_LOSS}, - {"GameLines - Money Line", domain.BetOutcome{OddName: "Money Line", OddHeader: "1"}, struct{ Home, Away int }{2, 3}, domain.OUTCOME_STATUS_LOSS}, - {"GameLines - Money Line", domain.BetOutcome{OddName: "Money Line", OddHeader: "1"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_WIN}, - } +// func TestEvaluateGameLines(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"GameLines - Total", domain.BetOutcome{OddName: "Total", OddHandicap: "O 5.5"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_WIN}, +// {"GameLines - Total", domain.BetOutcome{OddName: "Total", OddHandicap: "O 5.5"}, struct{ Home, Away int }{2, 3}, domain.OUTCOME_STATUS_LOSS}, +// {"GameLines - Money Line", domain.BetOutcome{OddName: "Money Line", OddHeader: "1"}, struct{ Home, Away int }{2, 3}, domain.OUTCOME_STATUS_LOSS}, +// {"GameLines - Money Line", domain.BetOutcome{OddName: "Money Line", OddHeader: "1"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_WIN}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateGameLines(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateGameLines(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateFirstTeamToScore(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - events []map[string]string - expected domain.OutcomeStatus - }{ - {"HomeScoreFirst", domain.BetOutcome{OddName: "1", HomeTeamName: "Team A", AwayTeamName: "Team B"}, []map[string]string{ - {"text": "1st Goal - Team A"}, - }, domain.OUTCOME_STATUS_WIN}, - {"AwayScoreFirst", domain.BetOutcome{OddName: "2", HomeTeamName: "Team A", AwayTeamName: "Team B"}, []map[string]string{ - {"text": "1st Goal - Team A"}, - }, domain.OUTCOME_STATUS_LOSS}, - {"AwayScoreFirst", domain.BetOutcome{OddName: "2", HomeTeamName: "Team A", AwayTeamName: "Team B"}, []map[string]string{ - {"text": "1st Goal - Team B"}, - }, domain.OUTCOME_STATUS_WIN}, - } +// func TestEvaluateFirstTeamToScore(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// events []map[string]string +// expected domain.OutcomeStatus +// }{ +// {"HomeScoreFirst", domain.BetOutcome{OddName: "1", HomeTeamName: "Team A", AwayTeamName: "Team B"}, []map[string]string{ +// {"text": "1st Goal - Team A"}, +// }, domain.OUTCOME_STATUS_WIN}, +// {"AwayScoreFirst", domain.BetOutcome{OddName: "2", HomeTeamName: "Team A", AwayTeamName: "Team B"}, []map[string]string{ +// {"text": "1st Goal - Team A"}, +// }, domain.OUTCOME_STATUS_LOSS}, +// {"AwayScoreFirst", domain.BetOutcome{OddName: "2", HomeTeamName: "Team A", AwayTeamName: "Team B"}, []map[string]string{ +// {"text": "1st Goal - Team B"}, +// }, domain.OUTCOME_STATUS_WIN}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateFirstTeamToScore(tt.outcome, tt.events) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateFirstTeamToScore(tt.outcome, tt.events) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateGoalsOverUnder(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"LosingGoalsOver", domain.BetOutcome{OddHeader: "Over", OddName: "13"}, struct{ Home, Away int }{7, 5}, domain.OUTCOME_STATUS_LOSS}, - {"WinningGoalsOver", domain.BetOutcome{OddHeader: "Over", OddName: "11"}, struct{ Home, Away int }{7, 5}, domain.OUTCOME_STATUS_WIN}, - {"WinningGoalsUnder", domain.BetOutcome{OddHeader: "Under", OddName: "12"}, struct{ Home, Away int }{6, 5}, domain.OUTCOME_STATUS_WIN}, - } +// func TestEvaluateGoalsOverUnder(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"LosingGoalsOver", domain.BetOutcome{OddHeader: "Over", OddName: "13"}, struct{ Home, Away int }{7, 5}, domain.OUTCOME_STATUS_LOSS}, +// {"WinningGoalsOver", domain.BetOutcome{OddHeader: "Over", OddName: "11"}, struct{ Home, Away int }{7, 5}, domain.OUTCOME_STATUS_WIN}, +// {"WinningGoalsUnder", domain.BetOutcome{OddHeader: "Under", OddName: "12"}, struct{ Home, Away int }{6, 5}, domain.OUTCOME_STATUS_WIN}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateGoalsOverUnder(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateGoalsOverUnder(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateGoalsOddEven(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"WinningOddGoals", domain.BetOutcome{OddName: "Odd"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_WIN}, - {"LosingEvenGoals", domain.BetOutcome{OddName: "Even"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_LOSS}, - {"WinningEvenGoals", domain.BetOutcome{OddName: "Even"}, struct{ Home, Away int }{6, 6}, domain.OUTCOME_STATUS_WIN}, - } +// func TestEvaluateGoalsOddEven(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"WinningOddGoals", domain.BetOutcome{OddName: "Odd"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_WIN}, +// {"LosingEvenGoals", domain.BetOutcome{OddName: "Even"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_LOSS}, +// {"WinningEvenGoals", domain.BetOutcome{OddName: "Even"}, struct{ Home, Away int }{6, 6}, domain.OUTCOME_STATUS_WIN}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateGoalsOddEven(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateGoalsOddEven(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateCorrectScore(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"CorrectScore", domain.BetOutcome{OddName: "7-4"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_WIN}, - {"CorrectScore", domain.BetOutcome{OddName: "6-6"}, struct{ Home, Away int }{6, 6}, domain.OUTCOME_STATUS_WIN}, - {"IncorrectScore", domain.BetOutcome{OddName: "2-3"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_LOSS}, - } +// func TestEvaluateCorrectScore(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"CorrectScore", domain.BetOutcome{OddName: "7-4"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_WIN}, +// {"CorrectScore", domain.BetOutcome{OddName: "6-6"}, struct{ Home, Away int }{6, 6}, domain.OUTCOME_STATUS_WIN}, +// {"IncorrectScore", domain.BetOutcome{OddName: "2-3"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_LOSS}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateCorrectScore(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateCorrectScore(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateHighestScoringHalf(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - firstScore struct{ Home, Away int } - secondScore struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"Winning1stHalf", domain.BetOutcome{OddName: "1st Half"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, - {"Losing1stHalf", domain.BetOutcome{OddName: "1st Half"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_LOSS}, - {"Losing2ndHalf", domain.BetOutcome{OddName: "2nd Half"}, struct{ Home, Away int }{0, 0}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_LOSS}, - {"Winning2ndHalf", domain.BetOutcome{OddName: "2nd Half"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, - } +// func TestEvaluateHighestScoringHalf(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// firstScore struct{ Home, Away int } +// secondScore struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"Winning1stHalf", domain.BetOutcome{OddName: "1st Half"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, +// {"Losing1stHalf", domain.BetOutcome{OddName: "1st Half"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_LOSS}, +// {"Losing2ndHalf", domain.BetOutcome{OddName: "2nd Half"}, struct{ Home, Away int }{0, 0}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_LOSS}, +// {"Winning2ndHalf", domain.BetOutcome{OddName: "2nd Half"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateHighestScoringHalf(tt.outcome, tt.firstScore, tt.secondScore) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateHighestScoringHalf(tt.outcome, tt.firstScore, tt.secondScore) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } -} +// } -func TestEvaluateHighestScoringQuarter(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - firstScore struct{ Home, Away int } - secondScore struct{ Home, Away int } - thirdScore struct{ Home, Away int } - fourthScore struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"Winning1stQuarter", domain.BetOutcome{OddName: "1st Quarter"}, - struct{ Home, Away int }{1, 1}, - struct{ Home, Away int }{0, 0}, - struct{ Home, Away int }{1, 0}, - struct{ Home, Away int }{0, 0}, - domain.OUTCOME_STATUS_WIN}, - {"Losing1stQuarter", domain.BetOutcome{OddName: "1st Quarter"}, - struct{ Home, Away int }{1, 1}, - struct{ Home, Away int }{0, 0}, - struct{ Home, Away int }{1, 1}, - struct{ Home, Away int }{0, 0}, - domain.OUTCOME_STATUS_LOSS}, - {"Losing2ndQuarter", domain.BetOutcome{OddName: "2nd Quarter"}, - struct{ Home, Away int }{1, 1}, - struct{ Home, Away int }{0, 0}, - struct{ Home, Away int }{1, 1}, - struct{ Home, Away int }{0, 0}, - domain.OUTCOME_STATUS_LOSS}, - {"Winning3rdQuarter", domain.BetOutcome{OddName: "3rd Quarter"}, - struct{ Home, Away int }{1, 0}, - struct{ Home, Away int }{0, 0}, - struct{ Home, Away int }{1, 1}, - struct{ Home, Away int }{0, 0}, - domain.OUTCOME_STATUS_WIN}, - {"Wining4thQuarter", domain.BetOutcome{OddName: "4th Quarter"}, - struct{ Home, Away int }{1, 1}, - struct{ Home, Away int }{0, 0}, - struct{ Home, Away int }{1, 1}, - struct{ Home, Away int }{2, 2}, - domain.OUTCOME_STATUS_WIN}, - } +// func TestEvaluateHighestScoringQuarter(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// firstScore struct{ Home, Away int } +// secondScore struct{ Home, Away int } +// thirdScore struct{ Home, Away int } +// fourthScore struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"Winning1stQuarter", domain.BetOutcome{OddName: "1st Quarter"}, +// struct{ Home, Away int }{1, 1}, +// struct{ Home, Away int }{0, 0}, +// struct{ Home, Away int }{1, 0}, +// struct{ Home, Away int }{0, 0}, +// domain.OUTCOME_STATUS_WIN}, +// {"Losing1stQuarter", domain.BetOutcome{OddName: "1st Quarter"}, +// struct{ Home, Away int }{1, 1}, +// struct{ Home, Away int }{0, 0}, +// struct{ Home, Away int }{1, 1}, +// struct{ Home, Away int }{0, 0}, +// domain.OUTCOME_STATUS_LOSS}, +// {"Losing2ndQuarter", domain.BetOutcome{OddName: "2nd Quarter"}, +// struct{ Home, Away int }{1, 1}, +// struct{ Home, Away int }{0, 0}, +// struct{ Home, Away int }{1, 1}, +// struct{ Home, Away int }{0, 0}, +// domain.OUTCOME_STATUS_LOSS}, +// {"Winning3rdQuarter", domain.BetOutcome{OddName: "3rd Quarter"}, +// struct{ Home, Away int }{1, 0}, +// struct{ Home, Away int }{0, 0}, +// struct{ Home, Away int }{1, 1}, +// struct{ Home, Away int }{0, 0}, +// domain.OUTCOME_STATUS_WIN}, +// {"Wining4thQuarter", domain.BetOutcome{OddName: "4th Quarter"}, +// struct{ Home, Away int }{1, 1}, +// struct{ Home, Away int }{0, 0}, +// struct{ Home, Away int }{1, 1}, +// struct{ Home, Away int }{2, 2}, +// domain.OUTCOME_STATUS_WIN}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateHighestScoringQuarter(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore, tt.fourthScore) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateHighestScoringQuarter(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore, tt.fourthScore) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } -} +// } -func TestEvaluateWinningMargin(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"WinningMargin", domain.BetOutcome{OddHeader: "1", OddName: "12"}, struct{ Home, Away int }{12, 0}, domain.OUTCOME_STATUS_WIN}, - {"WinningMargin", domain.BetOutcome{OddHeader: "2", OddName: "3"}, struct{ Home, Away int }{1, 4}, domain.OUTCOME_STATUS_WIN}, - {"WinningMargin", domain.BetOutcome{OddHeader: "1", OddName: "3+"}, struct{ Home, Away int }{4, 0}, domain.OUTCOME_STATUS_WIN}, - {"WinningMargin", domain.BetOutcome{OddHeader: "2", OddName: "12+"}, struct{ Home, Away int }{0, 13}, domain.OUTCOME_STATUS_WIN}, - {"LosingMargin", domain.BetOutcome{OddHeader: "2", OddName: "3"}, struct{ Home, Away int }{0, 4}, domain.OUTCOME_STATUS_LOSS}, - {"LosingMargin", domain.BetOutcome{OddHeader: "2", OddName: "3+"}, struct{ Home, Away int }{1, 3}, domain.OUTCOME_STATUS_LOSS}, - } +// func TestEvaluateWinningMargin(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"WinningMargin", domain.BetOutcome{OddHeader: "1", OddName: "12"}, struct{ Home, Away int }{12, 0}, domain.OUTCOME_STATUS_WIN}, +// {"WinningMargin", domain.BetOutcome{OddHeader: "2", OddName: "3"}, struct{ Home, Away int }{1, 4}, domain.OUTCOME_STATUS_WIN}, +// {"WinningMargin", domain.BetOutcome{OddHeader: "1", OddName: "3+"}, struct{ Home, Away int }{4, 0}, domain.OUTCOME_STATUS_WIN}, +// {"WinningMargin", domain.BetOutcome{OddHeader: "2", OddName: "12+"}, struct{ Home, Away int }{0, 13}, domain.OUTCOME_STATUS_WIN}, +// {"LosingMargin", domain.BetOutcome{OddHeader: "2", OddName: "3"}, struct{ Home, Away int }{0, 4}, domain.OUTCOME_STATUS_LOSS}, +// {"LosingMargin", domain.BetOutcome{OddHeader: "2", OddName: "3+"}, struct{ Home, Away int }{1, 3}, domain.OUTCOME_STATUS_LOSS}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateWinningMargin(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateWinningMargin(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateDoubleResult(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - firstHalfScore struct{ Home, Away int } - fullTimeScore struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"WinningHomeAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A - Team B"}, struct{ Home, Away int }{1, 0}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, - {"WinningAwayHome", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B - Team A"}, struct{ Home, Away int }{0, 1}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, - {"WinningTie", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Tie"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, - {"WinningTieAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Team B"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, - {"LosingHomeAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A - Team B"}, struct{ Home, Away int }{1, 0}, struct{ Home, Away int }{2, 0}, domain.OUTCOME_STATUS_LOSS}, - {"LosingTie", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Tie"}, struct{ Home, Away int }{1, 0}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_LOSS}, - {"LosingTieAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Team A"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, - {"BadInput", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A - "}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_PENDING}, - } +// func TestEvaluateDoubleResult(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// firstHalfScore struct{ Home, Away int } +// fullTimeScore struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"WinningHomeAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A - Team B"}, struct{ Home, Away int }{1, 0}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, +// {"WinningAwayHome", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B - Team A"}, struct{ Home, Away int }{0, 1}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, +// {"WinningTie", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Tie"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, +// {"WinningTieAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Team B"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, +// {"LosingHomeAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A - Team B"}, struct{ Home, Away int }{1, 0}, struct{ Home, Away int }{2, 0}, domain.OUTCOME_STATUS_LOSS}, +// {"LosingTie", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Tie"}, struct{ Home, Away int }{1, 0}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_LOSS}, +// {"LosingTieAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Team A"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, +// {"BadInput", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A - "}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_PENDING}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateDoubleResult(tt.outcome, tt.firstHalfScore, tt.fullTimeScore) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateDoubleResult(tt.outcome, tt.firstHalfScore, tt.fullTimeScore) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateHighestScoringPeriod(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - firstScore struct{ Home, Away int } - secondScore struct{ Home, Away int } - thirdScore struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"Winning1stPeriod", domain.BetOutcome{OddName: "Period 1"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, - {"Winning2ndPeriod", domain.BetOutcome{OddName: "Period 2"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, - {"Winning3rdPeriod", domain.BetOutcome{OddName: "Period 3"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{3, 3}, domain.OUTCOME_STATUS_WIN}, - {"WinningTie", domain.BetOutcome{OddName: "Tie"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, - {"Losing1stPeriod", domain.BetOutcome{OddName: "Period 1"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_LOSS}, - {"Losing3rdPeriod", domain.BetOutcome{OddName: "Period 3"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_LOSS}, - } +// func TestEvaluateHighestScoringPeriod(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// firstScore struct{ Home, Away int } +// secondScore struct{ Home, Away int } +// thirdScore struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"Winning1stPeriod", domain.BetOutcome{OddName: "Period 1"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, +// {"Winning2ndPeriod", domain.BetOutcome{OddName: "Period 2"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, +// {"Winning3rdPeriod", domain.BetOutcome{OddName: "Period 3"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{3, 3}, domain.OUTCOME_STATUS_WIN}, +// {"WinningTie", domain.BetOutcome{OddName: "Tie"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, +// {"Losing1stPeriod", domain.BetOutcome{OddName: "Period 1"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_LOSS}, +// {"Losing3rdPeriod", domain.BetOutcome{OddName: "Period 3"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_LOSS}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateHighestScoringPeriod(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateHighestScoringPeriod(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvalauteTiedAfterRegulation(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score []struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"WinningTied", domain.BetOutcome{OddName: "Yes"}, []struct{ Home, Away int }{{1, 0}, {0, 1}, {2, 2}}, domain.OUTCOME_STATUS_WIN}, - {"WinningTied", domain.BetOutcome{OddName: "Yes"}, []struct{ Home, Away int }{{1, 1}, {0, 1}, {2, 2}, {2, 1}}, domain.OUTCOME_STATUS_WIN}, - {"WinningNotTied", domain.BetOutcome{OddName: "No"}, []struct{ Home, Away int }{{0, 0}, {0, 0}, {0, 0}, {1, 0}}, domain.OUTCOME_STATUS_WIN}, - {"LosingTied", domain.BetOutcome{OddName: "Yes"}, []struct{ Home, Away int }{{0, 2}, {0, 0}, {0, 0}, {0, 0}}, domain.OUTCOME_STATUS_LOSS}, - {"LosingNotTied", domain.BetOutcome{OddName: "No"}, []struct{ Home, Away int }{{0, 0}, {0, 0}, {0, 0}, {0, 0}}, domain.OUTCOME_STATUS_LOSS}, - } +// func TestEvalauteTiedAfterRegulation(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score []struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"WinningTied", domain.BetOutcome{OddName: "Yes"}, []struct{ Home, Away int }{{1, 0}, {0, 1}, {2, 2}}, domain.OUTCOME_STATUS_WIN}, +// {"WinningTied", domain.BetOutcome{OddName: "Yes"}, []struct{ Home, Away int }{{1, 1}, {0, 1}, {2, 2}, {2, 1}}, domain.OUTCOME_STATUS_WIN}, +// {"WinningNotTied", domain.BetOutcome{OddName: "No"}, []struct{ Home, Away int }{{0, 0}, {0, 0}, {0, 0}, {1, 0}}, domain.OUTCOME_STATUS_WIN}, +// {"LosingTied", domain.BetOutcome{OddName: "Yes"}, []struct{ Home, Away int }{{0, 2}, {0, 0}, {0, 0}, {0, 0}}, domain.OUTCOME_STATUS_LOSS}, +// {"LosingNotTied", domain.BetOutcome{OddName: "No"}, []struct{ Home, Away int }{{0, 0}, {0, 0}, {0, 0}, {0, 0}}, domain.OUTCOME_STATUS_LOSS}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateTiedAfterRegulation(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateTiedAfterRegulation(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateTeamTotal(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"WinningHomeUnder", domain.BetOutcome{OddHandicap: "Under 3", OddHeader: "1"}, struct{ Home, Away int }{2, 0}, domain.OUTCOME_STATUS_WIN}, - {"WinningHomeOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "1"}, struct{ Home, Away int }{3, 1}, domain.OUTCOME_STATUS_WIN}, - {"WinningAwayOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "2"}, struct{ Home, Away int }{1, 3}, domain.OUTCOME_STATUS_WIN}, - {"LosingHomeOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, - {"LosingAwayOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, - } +// func TestEvaluateTeamTotal(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"WinningHomeUnder", domain.BetOutcome{OddHandicap: "Under 3", OddHeader: "1"}, struct{ Home, Away int }{2, 0}, domain.OUTCOME_STATUS_WIN}, +// {"WinningHomeOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "1"}, struct{ Home, Away int }{3, 1}, domain.OUTCOME_STATUS_WIN}, +// {"WinningAwayOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "2"}, struct{ Home, Away int }{1, 3}, domain.OUTCOME_STATUS_WIN}, +// {"LosingHomeOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, +// {"LosingAwayOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateTeamTotal(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateTeamTotal(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestDrawNoBet(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"WinningHome", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, - {"WinningAway", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, - {"LosingHome", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, - {"Tie", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_VOID}, - } +// func TestDrawNoBet(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"WinningHome", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, +// {"WinningAway", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, +// {"LosingHome", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, +// {"Tie", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_VOID}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateDrawNoBet(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateDrawNoBet(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateMoneyLine(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"WinningHome", domain.BetOutcome{OddHeader: "1"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, - {"WinningAway", domain.BetOutcome{OddHeader: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, - {"WinningTie", domain.BetOutcome{OddHeader: "Tie"}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, - {"LosingTie", domain.BetOutcome{OddHeader: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, - {"LosingAway", domain.BetOutcome{OddHeader: "2"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_LOSS}, - } +// func TestEvaluateMoneyLine(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"WinningHome", domain.BetOutcome{OddHeader: "1"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, +// {"WinningAway", domain.BetOutcome{OddHeader: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, +// {"WinningTie", domain.BetOutcome{OddHeader: "Tie"}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, +// {"LosingTie", domain.BetOutcome{OddHeader: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, +// {"LosingAway", domain.BetOutcome{OddHeader: "2"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_LOSS}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateMoneyLine(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateMoneyLine(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateDoubleChance(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"WinningHomeOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "1 or Draw"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, - {"WinningHomeOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A or Draw"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, - {"WinningAwayOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Draw or Team B"}, struct{ Home, Away int }{0, 1}, domain.OUTCOME_STATUS_WIN}, - {"LosingHomeorAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "1 or 2"}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_LOSS}, - {"LosingAwayOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Draw or 2"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_LOSS}, - } +// func TestEvaluateDoubleChance(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"WinningHomeOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "1 or Draw"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, +// {"WinningHomeOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A or Draw"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, +// {"WinningAwayOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Draw or Team B"}, struct{ Home, Away int }{0, 1}, domain.OUTCOME_STATUS_WIN}, +// {"LosingHomeorAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "1 or 2"}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_LOSS}, +// {"LosingAwayOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Draw or 2"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_LOSS}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateDoubleChance(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateDoubleChance(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateResultAndTotal(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"WinningHomeOver", domain.BetOutcome{OddHeader: "1", OddHandicap: "Over 4"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_WIN}, - {"WinningHomeUnder", domain.BetOutcome{OddHeader: "1", OddHandicap: "Under 4"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, - {"WinningAwayUnder", domain.BetOutcome{OddHeader: "2", OddHandicap: "Under 4"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, - {"LosingHomeOver", domain.BetOutcome{OddHeader: "1", OddHandicap: "Under 4"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_LOSS}, - {"LosingAwayUnder", domain.BetOutcome{OddHeader: "2", OddHandicap: "Under 4"}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_LOSS}, - } +// func TestEvaluateResultAndTotal(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"WinningHomeOver", domain.BetOutcome{OddHeader: "1", OddHandicap: "Over 4"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_WIN}, +// {"WinningHomeUnder", domain.BetOutcome{OddHeader: "1", OddHandicap: "Under 4"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, +// {"WinningAwayUnder", domain.BetOutcome{OddHeader: "2", OddHandicap: "Under 4"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, +// {"LosingHomeOver", domain.BetOutcome{OddHeader: "1", OddHandicap: "Under 4"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_LOSS}, +// {"LosingAwayUnder", domain.BetOutcome{OddHeader: "2", OddHandicap: "Under 4"}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_LOSS}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateResultAndTotal(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateResultAndTotal(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestCheckMultiOutcome(t *testing.T) { - tests := []struct { - name string - outcome domain.OutcomeStatus - secondOutcome domain.OutcomeStatus - expected domain.OutcomeStatus - }{ - {"Win-Win", domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_WIN}, - {"Win-Void", domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_HALF}, - {"Win-Loss", domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_LOSS}, - {"Loss-Loss", domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_LOSS}, - {"Loss-Void", domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_VOID}, - {"Loss-Win", domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_LOSS}, - {"Void-Win", domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_VOID}, - {"Void-Loss", domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_VOID}, - {"Void-Void", domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_VOID}, - } +// func TestCheckMultiOutcome(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.OutcomeStatus +// secondOutcome domain.OutcomeStatus +// expected domain.OutcomeStatus +// }{ +// {"Win-Win", domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_WIN}, +// {"Win-Void", domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_HALF}, +// {"Win-Loss", domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_LOSS}, +// {"Loss-Loss", domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_LOSS}, +// {"Loss-Void", domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_VOID}, +// {"Loss-Win", domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_LOSS}, +// {"Void-Win", domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_VOID}, +// {"Void-Loss", domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_VOID}, +// {"Void-Void", domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_VOID}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := checkMultiOutcome(tt.outcome, tt.secondOutcome) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := checkMultiOutcome(tt.outcome, tt.secondOutcome) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateBTTSX(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"WinningBothScoreX", domain.BetOutcome{OddName: "3", OddHeader: "Yes"}, struct{ Home, Away int }{3, 4}, domain.OUTCOME_STATUS_WIN}, - {"WinningBothScoreLess", domain.BetOutcome{OddName: "3", OddHeader: "No"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, - {"LosingBothScoreX", domain.BetOutcome{OddName: "3", OddHeader: "Yes"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, - {"LosingBothScoreLess", domain.BetOutcome{OddName: "3", OddHeader: "No"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, - } +// func TestEvaluateBTTSX(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"WinningBothScoreX", domain.BetOutcome{OddName: "3", OddHeader: "Yes"}, struct{ Home, Away int }{3, 4}, domain.OUTCOME_STATUS_WIN}, +// {"WinningBothScoreLess", domain.BetOutcome{OddName: "3", OddHeader: "No"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, +// {"LosingBothScoreX", domain.BetOutcome{OddName: "3", OddHeader: "Yes"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, +// {"LosingBothScoreLess", domain.BetOutcome{OddName: "3", OddHeader: "No"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateBTTSX(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateBTTSX(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateResultAndBTTSX(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"WinningHomeAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A and Yes", OddHeader: "3"}, struct{ Home, Away int }{4, 3}, domain.OUTCOME_STATUS_WIN}, - {"WinningHomeAndBothScoreLess", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A and No", OddHeader: "3"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, - {"WinningAwayAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and Yes", OddHeader: "3"}, struct{ Home, Away int }{3, 4}, domain.OUTCOME_STATUS_WIN}, - {"WinningAwayAndBothScoreLess", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and No", OddHeader: "3"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, - {"LosingHomeAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A and Yes", OddHeader: "3"}, struct{ Home, Away int }{3, 4}, domain.OUTCOME_STATUS_LOSS}, - {"LosingHomeAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and Yes", OddHeader: "3"}, struct{ Home, Away int }{4, 2}, domain.OUTCOME_STATUS_LOSS}, - {"LosingAwayAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and Yes", OddHeader: "3"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, - } +// func TestEvaluateResultAndBTTSX(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"WinningHomeAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A and Yes", OddHeader: "3"}, struct{ Home, Away int }{4, 3}, domain.OUTCOME_STATUS_WIN}, +// {"WinningHomeAndBothScoreLess", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A and No", OddHeader: "3"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, +// {"WinningAwayAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and Yes", OddHeader: "3"}, struct{ Home, Away int }{3, 4}, domain.OUTCOME_STATUS_WIN}, +// {"WinningAwayAndBothScoreLess", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and No", OddHeader: "3"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, +// {"LosingHomeAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A and Yes", OddHeader: "3"}, struct{ Home, Away int }{3, 4}, domain.OUTCOME_STATUS_LOSS}, +// {"LosingHomeAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and Yes", OddHeader: "3"}, struct{ Home, Away int }{4, 2}, domain.OUTCOME_STATUS_LOSS}, +// {"LosingAwayAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and Yes", OddHeader: "3"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateResultAndBTTSX(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateResultAndBTTSX(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateMoneyLine3Way(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"WinningHome", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, - {"WinningAway", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, - {"WinningTie", domain.BetOutcome{OddName: "Tie"}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, - {"LosingTie", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, - {"LosingAway", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_LOSS}, - } +// func TestEvaluateMoneyLine3Way(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"WinningHome", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, +// {"WinningAway", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, +// {"WinningTie", domain.BetOutcome{OddName: "Tie"}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, +// {"LosingTie", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, +// {"LosingAway", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_LOSS}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateMoneyLine3Way(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateMoneyLine3Way(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateAsianHandicap(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - { - name: "Home -1 Win", - outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-1"}, - score: struct{ Home, Away int }{Home: 2, Away: 0}, - expected: domain.OUTCOME_STATUS_WIN, - }, - { - name: "Home -0.5 Win", - outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-0.5"}, - score: struct{ Home, Away int }{Home: 1, Away: 0}, - expected: domain.OUTCOME_STATUS_WIN, - }, - { - name: "Home -1 Void", - outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-1"}, - score: struct{ Home, Away int }{Home: 1, Away: 0}, - expected: domain.OUTCOME_STATUS_VOID, - }, - { - name: "Away +3 Win", - outcome: domain.BetOutcome{OddHeader: "2", OddHandicap: "3"}, - score: struct{ Home, Away int }{Home: 1, Away: 2}, - expected: domain.OUTCOME_STATUS_WIN, - }, - { - name: "Split Handicap Home -0.5,-1 Win/Win", - outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-0.5,-1"}, - score: struct{ Home, Away int }{Home: 2, Away: 0}, - expected: domain.OUTCOME_STATUS_WIN, - }, - { - name: "Split Handicap Home -0.5,-1 Win/Void", - outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-0.5,-1"}, - score: struct{ Home, Away int }{Home: 1, Away: 0}, - expected: domain.OUTCOME_STATUS_WIN, - }, - { - name: "Invalid Handicap", - outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "invalid"}, - score: struct{ Home, Away int }{Home: 1, Away: 0}, - expected: domain.OUTCOME_STATUS_ERROR, - }, - } +// func TestEvaluateAsianHandicap(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// { +// name: "Home -1 Win", +// outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-1"}, +// score: struct{ Home, Away int }{Home: 2, Away: 0}, +// expected: domain.OUTCOME_STATUS_WIN, +// }, +// { +// name: "Home -0.5 Win", +// outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-0.5"}, +// score: struct{ Home, Away int }{Home: 1, Away: 0}, +// expected: domain.OUTCOME_STATUS_WIN, +// }, +// { +// name: "Home -1 Void", +// outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-1"}, +// score: struct{ Home, Away int }{Home: 1, Away: 0}, +// expected: domain.OUTCOME_STATUS_VOID, +// }, +// { +// name: "Away +3 Win", +// outcome: domain.BetOutcome{OddHeader: "2", OddHandicap: "3"}, +// score: struct{ Home, Away int }{Home: 1, Away: 2}, +// expected: domain.OUTCOME_STATUS_WIN, +// }, +// { +// name: "Split Handicap Home -0.5,-1 Win/Win", +// outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-0.5,-1"}, +// score: struct{ Home, Away int }{Home: 2, Away: 0}, +// expected: domain.OUTCOME_STATUS_WIN, +// }, +// { +// name: "Split Handicap Home -0.5,-1 Win/Void", +// outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-0.5,-1"}, +// score: struct{ Home, Away int }{Home: 1, Away: 0}, +// expected: domain.OUTCOME_STATUS_WIN, +// }, +// { +// name: "Invalid Handicap", +// outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "invalid"}, +// score: struct{ Home, Away int }{Home: 1, Away: 0}, +// expected: domain.OUTCOME_STATUS_ERROR, +// }, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateAsianHandicap(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateAsianHandicap(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } -func TestEvaluateHandicapAndTotal(t *testing.T) { - tests := []struct { - name string - outcome domain.BetOutcome - score struct{ Home, Away int } - expected domain.OutcomeStatus - }{ - {"Home +2.5 Over 3", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A +2.5 & Over 3"}, - struct{ Home, Away int }{4, 0}, - domain.OUTCOME_STATUS_WIN}, - {"Away +2.5 Over 4", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B +2.5 & Over 4"}, - struct{ Home, Away int }{1, 5}, - domain.OUTCOME_STATUS_WIN}, - {"Home +2.5 Over 3", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A +2.5 & Over 3"}, - struct{ Home, Away int }{2, 0}, - domain.OUTCOME_STATUS_LOSS}, - {"Home -3.5 Over 3", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A -2.5 & Over 3"}, - struct{ Home, Away int }{4, 3}, - domain.OUTCOME_STATUS_LOSS}, - {"Away -3 Over 4", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B -3 & Over 4"}, - struct{ Home, Away int }{3, 5}, - domain.OUTCOME_STATUS_LOSS}, - } +// func TestEvaluateHandicapAndTotal(t *testing.T) { +// tests := []struct { +// name string +// outcome domain.BetOutcome +// score struct{ Home, Away int } +// expected domain.OutcomeStatus +// }{ +// {"Home +2.5 Over 3", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A +2.5 & Over 3"}, +// struct{ Home, Away int }{4, 0}, +// domain.OUTCOME_STATUS_WIN}, +// {"Away +2.5 Over 4", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B +2.5 & Over 4"}, +// struct{ Home, Away int }{1, 5}, +// domain.OUTCOME_STATUS_WIN}, +// {"Home +2.5 Over 3", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A +2.5 & Over 3"}, +// struct{ Home, Away int }{2, 0}, +// domain.OUTCOME_STATUS_LOSS}, +// {"Home -3.5 Over 3", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A -2.5 & Over 3"}, +// struct{ Home, Away int }{4, 3}, +// domain.OUTCOME_STATUS_LOSS}, +// {"Away -3 Over 4", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B -3 & Over 4"}, +// struct{ Home, Away int }{3, 5}, +// domain.OUTCOME_STATUS_LOSS}, +// } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, _ := EvaluateHandicapAndTotal(tt.outcome, tt.score) - if status != tt.expected { - t.Errorf("expected %d, got %d", tt.expected, status) - } - }) - } -} +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// status, _ := EvaluateHandicapAndTotal(tt.outcome, tt.score) +// if status != tt.expected { +// t.Errorf("expected %d, got %d", tt.expected, status) +// } +// }) +// } +// } diff --git a/internal/services/transaction/port.go b/internal/services/transaction/port.go index 2bc6f6b..3183869 100644 --- a/internal/services/transaction/port.go +++ b/internal/services/transaction/port.go @@ -12,4 +12,7 @@ type TransactionStore interface { GetAllTransactions(ctx context.Context, filter domain.TransactionFilter) ([]domain.Transaction, error) GetTransactionByBranch(ctx context.Context, id int64) ([]domain.Transaction, error) UpdateTransactionVerified(ctx context.Context, id int64, verified bool, approvedBy int64, approverName string) error + + GetTransactionTotals(ctx context.Context, filter domain.ReportFilter) (deposits, withdrawals domain.Currency, err error) + GetBranchTransactionTotals(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.BranchTransactions, error) } diff --git a/internal/services/user/port.go b/internal/services/user/port.go index 6a09597..9a1317c 100644 --- a/internal/services/user/port.go +++ b/internal/services/user/port.go @@ -23,6 +23,10 @@ type UserStore interface { GetUserByPhone(ctx context.Context, phoneNum string) (domain.User, error) SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error) UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error // identifier verified email or phone + + GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active int64, err error) + GetCustomerDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerDetail, error) + GetBranchCustomerCounts(ctx context.Context, filter domain.ReportFilter) (map[int64]int64, error) } type SmsGateway interface { SendSMSOTP(ctx context.Context, phoneNumber, otp string) error diff --git a/internal/services/wallet/monitor/service.go b/internal/services/wallet/monitor/service.go new file mode 100644 index 0000000..ea96534 --- /dev/null +++ b/internal/services/wallet/monitor/service.go @@ -0,0 +1,216 @@ +// internal/services/walletmonitor/service.go +package monitor + +import ( + "context" + "fmt" + "log/slog" + "sync" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" + notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" +) + +type Service struct { + walletSvc wallet.Service + branchSvc branch.Service + notificationSvc *notificationservice.Service + logger *slog.Logger + thresholds []float64 + checkInterval time.Duration + stopCh chan struct{} + wg sync.WaitGroup + initialDeposits map[int64]domain.Currency // companyID -> initial deposit + mu sync.RWMutex +} + +func NewService( + walletSvc wallet.Service, + branchSvc branch.Service, + notificationSvc *notificationservice.Service, // Change to pointer + logger *slog.Logger, + checkInterval time.Duration, +) *Service { + return &Service{ + walletSvc: walletSvc, + branchSvc: branchSvc, + notificationSvc: notificationSvc, // Now storing the pointer + logger: logger, + thresholds: []float64{0.75, 0.50, 0.25, 0.10, 0.05}, + checkInterval: checkInterval, + stopCh: make(chan struct{}), + initialDeposits: make(map[int64]domain.Currency), + } +} + +func (s *Service) Start() { + s.wg.Add(1) + go s.monitorWallets() +} + +func (s *Service) Stop() { + close(s.stopCh) + s.wg.Wait() +} + +func (s *Service) monitorWallets() { + defer s.wg.Done() + + ticker := time.NewTicker(s.checkInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + s.checkWalletThresholds() + case <-s.stopCh: + return + } + } +} + +func (s *Service) checkWalletThresholds() { + ctx := context.Background() + + // Get all company wallets + companies, err := s.branchSvc.GetAllCompaniesBranch(ctx) + if err != nil { + s.logger.Error("failed to get companies", "error", err) + return + } + + for _, company := range companies { + wallet, err := s.walletSvc.GetWalletByID(ctx, company.WalletID) + if err != nil { + s.logger.Error("failed to get company wallet", "company_id", company.ID, "error", err) + continue + } + + // Initialize initial deposit if not set + s.mu.Lock() + if _, exists := s.initialDeposits[company.ID]; !exists { + s.initialDeposits[company.ID] = wallet.Balance + s.mu.Unlock() + continue + } + initialDeposit := s.initialDeposits[company.ID] + s.mu.Unlock() + + if initialDeposit == 0 { + continue // avoid division by zero + } + + currentBalance := wallet.Balance + currentPercentage := float64(currentBalance) / float64(initialDeposit) + + for _, threshold := range s.thresholds { + if currentPercentage <= threshold { + // Check if we've already notified for this threshold + key := notificationKey(company.ID, threshold) + if s.hasNotified(key) { + continue + } + + // Send notifications + s.sendThresholdNotifications(ctx, company.ID, threshold, currentBalance, initialDeposit) + s.markAsNotified(key) + } + } + } +} + +func (s *Service) sendThresholdNotifications( + ctx context.Context, + companyID int64, + threshold float64, + currentBalance domain.Currency, + initialDeposit domain.Currency, +) { + // Get all recipients (branch managers, admins, super admins for this company) + recipients, err := s.getNotificationRecipients(ctx, companyID) + if err != nil { + s.logger.Error("failed to get notification recipients", "company_id", companyID, "error", err) + return + } + + thresholdPercent := int(threshold * 100) + message := buildNotificationMessage(thresholdPercent, currentBalance, initialDeposit) + + for _, recipientID := range recipients { + notification := &domain.Notification{ + RecipientID: recipientID, + Type: domain.NOTIFICATION_TYPE_WALLET, + Level: domain.NotificationLevelWarning, + Reciever: domain.NotificationRecieverSideAdmin, + DeliveryChannel: domain.DeliveryChannelInApp, + Payload: domain.NotificationPayload{ + Headline: "Wallet Threshold Alert", + Message: message, + }, + Priority: 2, // Medium priority + } + + if err := s.notificationSvc.SendNotification(ctx, notification); err != nil { + s.logger.Error("failed to send threshold notification", + "recipient_id", recipientID, + "company_id", companyID, + "threshold", thresholdPercent, + "error", err) + } + } + + s.logger.Info("sent wallet threshold notifications", + "company_id", companyID, + "threshold", thresholdPercent, + "recipient_count", len(recipients)) +} + +func (s *Service) getNotificationRecipients(ctx context.Context, companyID int64) ([]int64, error) { + // Get branch managers for this company + branches, err := s.branchSvc.GetBranchesByCompany(ctx, companyID) + if err != nil { + return nil, err + } + + var recipientIDs []int64 + + // Add branch managers + for _, branch := range branches { + if branch.BranchManagerID != 0 { + recipientIDs = append(recipientIDs, branch.BranchManagerID) + } + } + + // Add company admins (implementation depends on your user service) + // This would typically query users with admin role for this company + + return recipientIDs, nil +} + +func (s *Service) hasNotified(key string) bool { + fmt.Println(key) + // Implement your notification tracking logic here + // Could use a cache or database to track which thresholds have been notified + return false +} + +func (s *Service) markAsNotified(key string) { + // Implement your notification tracking logic here + // Mark that this threshold has been notified +} + +func notificationKey(companyID int64, threshold float64) string { + return fmt.Sprintf("%d_%.2f", companyID, threshold) +} + +func buildNotificationMessage(thresholdPercent int, currentBalance, initialDeposit domain.Currency) string { + return fmt.Sprintf( + "Company wallet balance has reached %d%% of initial deposit. Current balance: %.2f, Initial deposit: %.2f", + thresholdPercent, + float64(currentBalance)/100, // Assuming currency is in cents + float64(initialDeposit)/100, + ) +} diff --git a/internal/services/wallet/port.go b/internal/services/wallet/port.go index 9c3fcb9..c6ad52e 100644 --- a/internal/services/wallet/port.go +++ b/internal/services/wallet/port.go @@ -16,6 +16,8 @@ type WalletStore interface { GetAllBranchWallets(ctx context.Context) ([]domain.BranchWallet, error) UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error UpdateWalletActive(ctx context.Context, id int64, isActive bool) error + + GetBalanceSummary(ctx context.Context, filter domain.ReportFilter) (domain.BalanceSummary, error) } type TransferStore interface { diff --git a/internal/services/wallet/service.go b/internal/services/wallet/service.go index a8913c2..2d3b927 100644 --- a/internal/services/wallet/service.go +++ b/internal/services/wallet/service.go @@ -1,13 +1,23 @@ package wallet +import ( + "log/slog" + + notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" +) + type Service struct { - walletStore WalletStore - transferStore TransferStore + walletStore WalletStore + transferStore TransferStore + notificationStore notificationservice.NotificationStore + logger *slog.Logger } -func NewService(walletStore WalletStore, transferStore TransferStore) *Service { +func NewService(walletStore WalletStore, transferStore TransferStore, notificationStore notificationservice.NotificationStore, logger *slog.Logger) *Service { return &Service{ - walletStore: walletStore, - transferStore: transferStore, + walletStore: walletStore, + transferStore: transferStore, + notificationStore: notificationStore, + logger: logger, } } diff --git a/internal/services/wallet/transfer.go b/internal/services/wallet/transfer.go index 387f255..74c2a1b 100644 --- a/internal/services/wallet/transfer.go +++ b/internal/services/wallet/transfer.go @@ -3,15 +3,96 @@ package wallet import ( "context" "errors" + "fmt" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) var ( ErrWalletNotTransferable = errors.New("wallet is not transferable") + ErrInsufficientBalance = errors.New("wallet balance is insufficient") ) func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { + senderWallet, err := s.walletStore.GetWalletByID(ctx, transfer.SenderWalletID.Value) + receiverWallet, err := s.walletStore.GetWalletByID(ctx, transfer.ReceiverWalletID) + if err != nil { + return domain.Transfer{}, fmt.Errorf("failed to get sender wallet: %w", err) + } + + // Check if wallet has sufficient balance + if senderWallet.Balance < transfer.Amount || senderWallet.Balance == 0 { + // Send notification to customer + customerNotification := &domain.Notification{ + RecipientID: receiverWallet.UserID, + Type: domain.NOTIFICATION_TYPE_TRANSFER, + Level: domain.NotificationLevelError, + Reciever: domain.NotificationRecieverSideCustomer, + DeliveryChannel: domain.DeliveryChannelInApp, + Payload: domain.NotificationPayload{ + Headline: "Service Temporarily Unavailable", + Message: "Our payment system is currently under maintenance. Please try again later.", + }, + Priority: 2, + Metadata: []byte(fmt.Sprintf(`{ + "transfer_amount": %d, + "current_balance": %d, + "wallet_id": %d, + "notification_type": "customer_facing" + }`, transfer.Amount, senderWallet.Balance, transfer.SenderWalletID.Value)), + } + + // Send notification to admin team + adminNotification := &domain.Notification{ + RecipientID: senderWallet.UserID, + Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT, + Level: domain.NotificationLevelError, + Reciever: domain.NotificationRecieverSideAdmin, + DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel + Payload: domain.NotificationPayload{ + Headline: "CREDIT WARNING: System Running Out of Funds", + Message: fmt.Sprintf( + "Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f", + transfer.SenderWalletID.Value, + float64(senderWallet.Balance)/100, + float64(transfer.Amount)/100, + ), + }, + Priority: 1, // High priority for admin alerts + Metadata: fmt.Appendf(nil, `{ + "wallet_id": %d, + "balance": %d, + "required_amount": %d, + "notification_type": "admin_alert" + }`, transfer.SenderWalletID.Value, senderWallet.Balance, transfer.Amount), + } + + // Send both notifications + if err := s.notificationStore.SendNotification(ctx, customerNotification); err != nil { + s.logger.Error("failed to send customer notification", + "user_id", "", + "error", err) + } + + // Get admin recipients and send to all + adminRecipients, err := s.notificationStore.ListRecipientIDs(ctx, domain.NotificationRecieverSideAdmin) + if err != nil { + s.logger.Error("failed to get admin recipients", "error", err) + } else { + for _, adminID := range adminRecipients { + adminNotification.RecipientID = adminID + if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil { + s.logger.Error("failed to send admin notification", + "admin_id", adminID, + "error", err) + } + } + } + + return domain.Transfer{}, ErrInsufficientBalance + } + + // Proceed with transfer if balance is sufficient return s.transferStore.CreateTransfer(ctx, transfer) } diff --git a/internal/web_server/app.go b/internal/web_server/app.go index d7c0b46..5fd1bb5 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -14,6 +14,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" @@ -45,6 +46,7 @@ type App struct { userSvc *user.Service betSvc *bet.Service virtualGameSvc virtualgameservice.VirtualGameService + reportSvc *report.Service chapaSvc *chapa.Service walletSvc *wallet.Service transactionSvc *transaction.Service @@ -67,6 +69,7 @@ func NewApp( userSvc *user.Service, ticketSvc *ticket.Service, betSvc *bet.Service, + reportSvc *report.Service, chapaSvc *chapa.Service, walletSvc *wallet.Service, transactionSvc *transaction.Service, @@ -107,6 +110,7 @@ func NewApp( userSvc: userSvc, ticketSvc: ticketSvc, betSvc: betSvc, + reportSvc: reportSvc, chapaSvc: chapaSvc, walletSvc: walletSvc, transactionSvc: transactionSvc, diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 6c42024..b09cadc 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -14,6 +14,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -30,6 +31,7 @@ type Handler struct { notificationSvc *notificationservice.Service userSvc *user.Service referralSvc referralservice.ReferralStore + reportSvc report.ReportStore chapaSvc chapa.ChapaPort walletSvc *wallet.Service transactionSvc *transaction.Service @@ -53,6 +55,7 @@ func New( logger *slog.Logger, notificationSvc *notificationservice.Service, validator *customvalidator.CustomValidator, + reportSvc report.ReportStore, chapaSvc chapa.ChapaPort, walletSvc *wallet.Service, referralSvc referralservice.ReferralStore, @@ -75,6 +78,7 @@ func New( return &Handler{ logger: logger, notificationSvc: notificationSvc, + reportSvc: reportSvc, chapaSvc: chapaSvc, walletSvc: walletSvc, referralSvc: referralSvc, diff --git a/internal/web_server/handlers/report.go b/internal/web_server/handlers/report.go new file mode 100644 index 0000000..05c7115 --- /dev/null +++ b/internal/web_server/handlers/report.go @@ -0,0 +1,116 @@ +package handlers + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/gofiber/fiber/v2" +) + +// GetDashboardReport returns a comprehensive dashboard report +// @Summary Get dashboard report +// @Description Returns a comprehensive dashboard report with key metrics +// @Tags Reports +// @Accept json +// @Produce json +// @Param company_id query int false "Company ID filter" +// @Param branch_id query int false "Branch ID filter" +// @Param user_id query int false "User ID filter" +// @Param start_time query string false "Start time filter (RFC3339 format)" +// @Param end_time query string false "End time filter (RFC3339 format)" +// @Param sport_id query string false "Sport ID filter" +// @Param status query int false "Status filter (0=Pending, 1=Win, 2=Loss, 3=Half, 4=Void, 5=Error)" +// @Security ApiKeyAuth +// @Success 200 {object} report.DashboardSummary +// @Failure 400 {object} domain.ErrorResponse +// @Failure 401 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/reports/dashboard [get] +func (h *Handler) GetDashboardReport(c *fiber.Ctx) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Parse query parameters + filter, err := parseReportFilter(c) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid filter parameters", + Error: err.Error(), + }) + } + + // Get report data + summary, err := h.reportSvc.GetDashboardSummary(ctx, filter) + if err != nil { + h.logger.Error("failed to get dashboard report", "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to generate report", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(summary) +} + +// parseReportFilter parses query parameters into ReportFilter +func parseReportFilter(c *fiber.Ctx) (domain.ReportFilter, error) { + var filter domain.ReportFilter + var err error + + if c.Query("company_id") != "" { + companyID, err := strconv.ParseInt(c.Query("company_id"), 10, 64) + if err != nil { + return domain.ReportFilter{}, fmt.Errorf("invalid company_id: %w", err) + } + filter.CompanyID = domain.ValidInt64{Value: companyID, Valid: true} + } + + if c.Query("branch_id") != "" { + branchID, err := strconv.ParseInt(c.Query("branch_id"), 10, 64) + if err != nil { + return domain.ReportFilter{}, fmt.Errorf("invalid branch_id: %w", err) + } + filter.BranchID = domain.ValidInt64{Value: branchID, Valid: true} + } + + if c.Query("user_id") != "" { + userID, err := strconv.ParseInt(c.Query("user_id"), 10, 64) + if err != nil { + return domain.ReportFilter{}, fmt.Errorf("invalid user_id: %w", err) + } + filter.UserID = domain.ValidInt64{Value: userID, Valid: true} + } + + if c.Query("start_time") != "" { + startTime, err := time.Parse(time.RFC3339, c.Query("start_time")) + if err != nil { + return domain.ReportFilter{}, fmt.Errorf("invalid start_time: %w", err) + } + filter.StartTime = domain.ValidTime{Value: startTime, Valid: true} + } + + if c.Query("end_time") != "" { + endTime, err := time.Parse(time.RFC3339, c.Query("end_time")) + if err != nil { + return domain.ReportFilter{}, fmt.Errorf("invalid end_time: %w", err) + } + filter.EndTime = domain.ValidTime{Value: endTime, Valid: true} + } + + if c.Query("sport_id") != "" { + filter.SportID = domain.ValidString{Value: c.Query("sport_id"), Valid: true} + } + + if c.Query("status") != "" { + status, err := strconv.ParseInt(c.Query("status"), 10, 32) + if err != nil { + return domain.ReportFilter{}, fmt.Errorf("invalid status: %w", err) + } + filter.Status = domain.ValidOutcomeStatus{Value: domain.OutcomeStatus(status), Valid: true} + } + + return filter, err +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 88e8a2f..c7c582a 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -1,11 +1,15 @@ package httpserver import ( + "context" "fmt" "strconv" _ "github.com/SamuelTariku/FortuneBet-Backend/docs" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger" + + // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/handlers" @@ -18,6 +22,7 @@ func (a *App) initAppRoutes() { a.logger, a.NotidicationStore, a.validator, + a.reportSvc, a.chapaSvc, a.walletSvc, a.referralSvc, @@ -187,6 +192,22 @@ func (a *App) initAppRoutes() { group.Post("/chapa/payments/deposit", a.authMiddleware, h.DepositUsingChapa) group.Get("/chapa/banks", a.authMiddleware, h.ReadChapaBanks) + //Report Routes + group.Get("/reports/dashboard", a.authMiddleware, h.GetDashboardReport) + + //Wallet Monitor Service + // group.Get("/debug/wallet-monitor/status", func(c *fiber.Ctx) error { + // return c.JSON(fiber.Map{ + // "running": monitor.IsRunning(), + // "last_check": walletMonitorSvc.LastCheckTime(), + // }) + // }) + + // group.Post("/debug/wallet-monitor/trigger", func(c *fiber.Ctx) error { + // walletMonitorSvc.ForceCheck() + // return c.SendStatus(fiber.StatusOK) + // }) + // group.Post("/chapa/payments/initialize", h.InitializePayment) // group.Get("/chapa/payments/verify/:tx_ref", h.VerifyTransaction) // group.Post("/chapa/payments/callback", h.ReceiveWebhook) @@ -202,6 +223,10 @@ func (a *App) initAppRoutes() { group.Get("/veli-games/launch", h.LaunchVeliGame) group.Post("/webhooks/veli-games", h.HandleVeliCallback) + //mongoDB logs + ctx := context.Background() + group.Get("/logs", mongoLogger.GetLogsHandler(ctx)) + // Recommendation Routes group.Get("/virtual-games/recommendations/:userID", h.GetRecommendations)