report_service + wallet_monitoring
This commit is contained in:
parent
c2b547ff34
commit
c22a1bd6c4
75
cmd/main.go
75
cmd/main.go
|
|
@ -3,15 +3,21 @@ package main
|
||||||
import (
|
import (
|
||||||
// "context"
|
// "context"
|
||||||
|
|
||||||
|
// "context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
// "github.com/gofiber/fiber/v2"
|
// "github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||||
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
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"
|
mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email"
|
||||||
mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms"
|
mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
"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/odds"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
|
||||||
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
|
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/result"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||||
|
|
@ -35,6 +42,7 @@ import (
|
||||||
alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea"
|
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/virtualGame/veli"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor"
|
||||||
|
|
||||||
// "github.com/SamuelTariku/FortuneBet-Backend/internal/utils"
|
// "github.com/SamuelTariku/FortuneBet-Backend/internal/utils"
|
||||||
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
||||||
|
|
@ -70,6 +78,32 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
|
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)
|
store := repository.NewStore(db)
|
||||||
v := customvalidator.NewCustomValidator(validator.New())
|
v := customvalidator.NewCustomValidator(validator.New())
|
||||||
|
|
||||||
|
|
@ -82,18 +116,33 @@ func main() {
|
||||||
eventSvc := event.New(cfg.Bet365Token, store)
|
eventSvc := event.New(cfg.Bet365Token, store)
|
||||||
oddsSvc := odds.New(store, cfg, logger)
|
oddsSvc := odds.New(store, cfg, logger)
|
||||||
ticketSvc := ticket.NewService(store)
|
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)
|
transactionSvc := transaction.NewService(store)
|
||||||
branchSvc := branch.NewService(store)
|
branchSvc := branch.NewService(store)
|
||||||
companySvc := company.NewService(store)
|
companySvc := company.NewService(store)
|
||||||
betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger)
|
betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger)
|
||||||
resultSvc := result.NewService(store, cfg, logger, *betSvc)
|
resultSvc := result.NewService(store, cfg, logger, *betSvc)
|
||||||
notificationRepo := repository.NewNotificationRepository(store)
|
|
||||||
referalRepo := repository.NewReferralRepository(store)
|
referalRepo := repository.NewReferralRepository(store)
|
||||||
vitualGameRepo := repository.NewVirtualGameRepository(store)
|
vitualGameRepo := repository.NewVirtualGameRepository(store)
|
||||||
recommendationRepo := repository.NewRecommendationRepository(store)
|
recommendationRepo := repository.NewRecommendationRepository(store)
|
||||||
|
|
||||||
notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
|
|
||||||
referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger)
|
referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger)
|
||||||
virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger)
|
virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger)
|
||||||
aleaService := alea.NewAleaPlayService(
|
aleaService := alea.NewAleaPlayService(
|
||||||
|
|
@ -121,6 +170,24 @@ func main() {
|
||||||
store,
|
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.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc)
|
||||||
httpserver.StartTicketCrons(*ticketSvc)
|
httpserver.StartTicketCrons(*ticketSvc)
|
||||||
|
|
||||||
|
|
@ -128,7 +195,7 @@ func main() {
|
||||||
JwtAccessKey: cfg.JwtKey,
|
JwtAccessKey: cfg.JwtKey,
|
||||||
JwtAccessExpiry: cfg.AccessExpiry,
|
JwtAccessExpiry: cfg.AccessExpiry,
|
||||||
}, userSvc,
|
}, 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)
|
logger.Info("Starting server", "port", cfg.Port)
|
||||||
|
|
||||||
if err := app.Run(); err != nil {
|
if err := app.Run(); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -136,25 +136,25 @@ CREATE TABLE IF NOT EXISTS transactions (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
amount BIGINT NOT NULL,
|
amount BIGINT NOT NULL,
|
||||||
branch_id BIGINT NOT NULL,
|
branch_id BIGINT NOT NULL,
|
||||||
company_id BIGINT NOT NULL,
|
company_id BIGINT,
|
||||||
cashier_id BIGINT NOT NULL,
|
cashier_id BIGINT,
|
||||||
cashier_name VARCHAR(255) NOT NULL,
|
cashier_name VARCHAR(255)L,
|
||||||
bet_id BIGINT NOT NULL,
|
bet_id BIGINT,
|
||||||
number_of_outcomes BIGINT NOT NULL,
|
number_of_outcomes BIGINT,
|
||||||
type BIGINT NOT NULL,
|
type BIGINT,
|
||||||
payment_option BIGINT NOT NULL,
|
payment_option BIGINT,
|
||||||
full_name VARCHAR(255) NOT NULL,
|
full_name VARCHAR(255),
|
||||||
phone_number VARCHAR(255) NOT NULL,
|
phone_number VARCHAR(255),
|
||||||
bank_code VARCHAR(255) NOT NULL,
|
bank_code VARCHAR(255),
|
||||||
beneficiary_name VARCHAR(255) NOT NULL,
|
beneficiary_name VARCHAR(255),
|
||||||
account_name VARCHAR(255) NOT NULL,
|
account_name VARCHAR(255),
|
||||||
account_number VARCHAR(255) NOT NULL,
|
account_number VARCHAR(255),
|
||||||
reference_number VARCHAR(255) NOT NULL,
|
reference_number VARCHAR(255),
|
||||||
verified BOOLEAN NOT NULL DEFAULT false,
|
verified BOOLEAN NOT NULL DEFAULT false,
|
||||||
approved_by BIGINT,
|
approved_by BIGINT,
|
||||||
approver_name VARCHAR(255),
|
approver_name VARCHAR(255),
|
||||||
branch_location VARCHAR(255) NOT NULL,
|
branch_location VARCHAR(255),
|
||||||
branch_name VARCHAR(255) NOT NULL,
|
branch_name VARCHAR(255),
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
DROP TABLE notifications;
|
DROP TABLE notifications;
|
||||||
|
DROP TABLE wallet_threshold_notifications;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,15 @@ CREATE TABLE IF NOT EXISTS notifications (
|
||||||
metadata JSONB
|
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_recipient_id ON notifications (recipient_id);
|
||||||
|
|
||||||
CREATE INDEX idx_notifications_timestamp ON notifications (timestamp);
|
CREATE INDEX idx_notifications_timestamp ON notifications (timestamp);
|
||||||
|
|
|
||||||
24
db/query/monitor.sql
Normal file
24
db/query/monitor.sql
Normal file
|
|
@ -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);
|
||||||
|
|
@ -17,6 +17,25 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- 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:
|
migrate:
|
||||||
image: migrate/migrate
|
image: migrate/migrate
|
||||||
volumes:
|
volumes:
|
||||||
|
|
@ -44,14 +63,16 @@ services:
|
||||||
- ${PORT}:8080
|
- ${PORT}:8080
|
||||||
environment:
|
environment:
|
||||||
- DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable
|
- DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable
|
||||||
|
- MONGO_URI=mongodb://root:secret@mongo:27017
|
||||||
depends_on:
|
depends_on:
|
||||||
migrate:
|
migrate:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- app
|
- app
|
||||||
command: ["/app/bin/web"]
|
command: ["/app/bin/web"]
|
||||||
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
|
@ -69,3 +90,4 @@ networks:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
mongo_data:
|
||||||
|
|
|
||||||
151
docs/docs.go
151
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}": {
|
"/api/v1/virtual-games/recommendations/{userID}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns a list of recommended virtual games for a specific user",
|
"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": {
|
"domain.Odd": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"response.APIResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -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}": {
|
"/api/v1/virtual-games/recommendations/{userID}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns a list of recommended virtual games for a specific user",
|
"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": {
|
"domain.Odd": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"response.APIResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,13 @@ definitions:
|
||||||
- $ref: '#/definitions/domain.OutcomeStatus'
|
- $ref: '#/definitions/domain.OutcomeStatus'
|
||||||
example: 1
|
example: 1
|
||||||
type: object
|
type: object
|
||||||
|
domain.ErrorResponse:
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
domain.Odd:
|
domain.Odd:
|
||||||
properties:
|
properties:
|
||||||
category:
|
category:
|
||||||
|
|
@ -1415,6 +1422,39 @@ definitions:
|
||||||
example: false
|
example: false
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
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:
|
response.APIResponse:
|
||||||
properties:
|
properties:
|
||||||
data: {}
|
data: {}
|
||||||
|
|
@ -1766,6 +1806,64 @@ paths:
|
||||||
summary: Withdraw using Chapa
|
summary: Withdraw using Chapa
|
||||||
tags:
|
tags:
|
||||||
- Chapa
|
- 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}:
|
/api/v1/virtual-games/recommendations/{userID}:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
||||||
15
go.mod
15
go.mod
|
|
@ -56,11 +56,24 @@ require (
|
||||||
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
|
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // 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/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/sync v0.12.0 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
golang.org/x/tools v0.31.0 // indirect
|
golang.org/x/tools v0.31.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // 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
|
||||||
|
|
|
||||||
33
go.sum
33
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/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 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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=
|
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-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 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
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/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/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
|
||||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
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 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
|
||||||
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
|
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/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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
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.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 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
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-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-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.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 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
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.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 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
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-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-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-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-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-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-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-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 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
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-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-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 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
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=
|
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-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-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-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
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.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.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.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 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
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-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-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.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.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 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
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=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|
|
||||||
|
|
@ -192,9 +192,9 @@ func (r ChapaDepositRequest) Validate() error {
|
||||||
if r.PhoneNumber == "" {
|
if r.PhoneNumber == "" {
|
||||||
return errors.New("phone number is required")
|
return errors.New("phone number is required")
|
||||||
}
|
}
|
||||||
if r.BranchID == 0 {
|
// if r.BranchID == 0 {
|
||||||
return errors.New("branch ID is required")
|
// return errors.New("branch ID is required")
|
||||||
}
|
// }
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,18 @@ type NotificationDeliveryStatus string
|
||||||
type DeliveryChannel string
|
type DeliveryChannel string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NotificationTypeCashOutSuccess NotificationType = "cash_out_success"
|
NotificationTypeCashOutSuccess NotificationType = "cash_out_success"
|
||||||
NotificationTypeDepositSuccess NotificationType = "deposit_success"
|
NotificationTypeDepositSuccess NotificationType = "deposit_success"
|
||||||
NotificationTypeBetPlaced NotificationType = "bet_placed"
|
NotificationTypeBetPlaced NotificationType = "bet_placed"
|
||||||
NotificationTypeDailyReport NotificationType = "daily_report"
|
NotificationTypeDailyReport NotificationType = "daily_report"
|
||||||
NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet"
|
NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet"
|
||||||
NotificationTypeBetOverload NotificationType = "bet_overload"
|
NotificationTypeBetOverload NotificationType = "bet_overload"
|
||||||
NotificationTypeSignUpWelcome NotificationType = "signup_welcome"
|
NotificationTypeSignUpWelcome NotificationType = "signup_welcome"
|
||||||
NotificationTypeOTPSent NotificationType = "otp_sent"
|
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"
|
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
|
||||||
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
||||||
|
|
|
||||||
123
internal/domain/report.go
Normal file
123
internal/domain/report.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
package customlogger
|
package customlogger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"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{
|
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())
|
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 {
|
if err != nil {
|
||||||
panic("Failed to open log file: " + err.Error())
|
panic("Failed to open log file: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
@ -48,3 +55,59 @@ func NewLogger(env string, lvl slog.Level) *slog.Logger {
|
||||||
|
|
||||||
return 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)
|
||||||
|
}
|
||||||
|
|
|
||||||
47
internal/logger/mongoLogger/handler.go
Normal file
47
internal/logger/mongoLogger/handler.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
internal/logger/mongoLogger/init.go
Normal file
30
internal/logger/mongoLogger/init.go
Normal file
|
|
@ -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"))
|
||||||
|
}
|
||||||
89
internal/logger/mongoLogger/logger.go
Normal file
89
internal/logger/mongoLogger/logger.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,9 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
// "fmt"
|
// "fmt"
|
||||||
|
|
||||||
|
|
@ -10,6 +13,8 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var logger *slog.Logger
|
||||||
|
|
||||||
func convertDBBet(bet dbgen.Bet) domain.Bet {
|
func convertDBBet(bet dbgen.Bet) domain.Bet {
|
||||||
return domain.Bet{
|
return domain.Bet{
|
||||||
ID: bet.ID,
|
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) {
|
func (s *Store) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) {
|
||||||
newBet, err := s.queries.CreateBet(ctx, convertCreateBet(bet))
|
newBet, err := s.queries.CreateBet(ctx, convertCreateBet(bet))
|
||||||
if err != nil {
|
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 domain.Bet{}, err
|
||||||
}
|
}
|
||||||
return convertDBBet(newBet), 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 {
|
func (s *Store) DeleteBet(ctx context.Context, id int64) error {
|
||||||
return s.queries.DeleteBet(ctx, id)
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"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)
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NotificationRepository interface {
|
type NotificationRepository interface {
|
||||||
|
|
@ -27,6 +28,10 @@ func NewNotificationRepository(store *Store) NotificationRepository {
|
||||||
return &Repository{store: store}
|
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) {
|
func (r *Repository) CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error) {
|
||||||
var errorSeverity pgtype.Text
|
var errorSeverity pgtype.Text
|
||||||
if notification.ErrorSeverity != nil {
|
if notification.ErrorSeverity != nil {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
// "golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
|
|
@ -49,3 +50,12 @@ func (s *Store) BeginTx(ctx context.Context) (*dbgen.Queries, pgx.Tx, error) {
|
||||||
q := s.queries.WithTx(tx)
|
q := s.queries.WithTx(tx)
|
||||||
return q, tx, nil
|
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
|
||||||
|
// }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
|
@ -139,3 +140,106 @@ func (s *Store) UpdateTransactionVerified(ctx context.Context, id int64, verifie
|
||||||
})
|
})
|
||||||
return err
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -446,3 +446,250 @@ func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.User, is_c
|
||||||
Suspended: userRes.Suspended,
|
Suspended: userRes.Suspended,
|
||||||
}, nil
|
}, 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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
|
@ -163,3 +164,64 @@ func (s *Store) UpdateWalletActive(ctx context.Context, id int64, isActive bool)
|
||||||
})
|
})
|
||||||
return err
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package bet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
@ -20,4 +21,24 @@ type BetStore interface {
|
||||||
UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error
|
UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error
|
||||||
UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error)
|
UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error)
|
||||||
DeleteBet(ctx context.Context, id int64) 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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,4 +23,10 @@ type BranchStore interface {
|
||||||
CreateBranchCashier(ctx context.Context, branchID int64, userID int64) error
|
CreateBranchCashier(ctx context.Context, branchID int64, userID int64) error
|
||||||
GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error)
|
GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error)
|
||||||
DeleteBranchCashier(ctx context.Context, userID int64) 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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,3 +70,11 @@ func (s *Service) DeleteBranchOperation(ctx context.Context, branchID int64, ope
|
||||||
func (s *Service) DeleteBranchCashier(ctx context.Context, userID int64) error {
|
func (s *Service) DeleteBranchCashier(ctx context.Context, userID int64) error {
|
||||||
return s.branchStore.DeleteBranchCashier(ctx, userID)
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,13 +63,16 @@ func (c *Client) IssuePayment(ctx context.Context, payload domain.ChapaTransferP
|
||||||
|
|
||||||
// service/chapa_service.go
|
// service/chapa_service.go
|
||||||
func (c *Client) InitPayment(ctx context.Context, req domain.InitPaymentRequest) (string, error) {
|
func (c *Client) InitPayment(ctx context.Context, req domain.InitPaymentRequest) (string, error) {
|
||||||
|
fmt.Println("\n\nInit payment request: ", req)
|
||||||
payloadBytes, err := json.Marshal(req)
|
payloadBytes, err := json.Marshal(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Println("\n\nWe are here")
|
||||||
return "", fmt.Errorf("failed to serialize payload: %w", err)
|
return "", fmt.Errorf("failed to serialize payload: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/transaction/initialize", bytes.NewBuffer(payloadBytes))
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/transaction/initialize", bytes.NewBuffer(payloadBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Println("\n\nWe are here 2")
|
||||||
return "", fmt.Errorf("failed to create HTTP request: %w", err)
|
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)
|
resp, err := c.HTTPClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Println("\n\nWe are here 3")
|
||||||
return "", fmt.Errorf("chapa HTTP request failed: %w", err)
|
return "", fmt.Errorf("chapa HTTP request failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
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))
|
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"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n\nInit payment response body: %v\n\n", response)
|
||||||
|
|
||||||
if err := json.Unmarshal(body, &response); err != nil {
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
return "", fmt.Errorf("failed to parse chapa response: %w", err)
|
return "", fmt.Errorf("failed to parse chapa response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
// "log/slog"
|
// "log/slog"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -116,6 +117,7 @@ func (s *Service) HandleChapaPaymentWebhook(ctx context.Context, req domain.Chap
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if txn.Verified {
|
if txn.Verified {
|
||||||
return nil // already processed
|
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)
|
return fmt.Errorf("user not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID)
|
banks, err := s.GetSupportedBanks()
|
||||||
if err != nil {
|
validBank := false
|
||||||
return err
|
for _, bank := range banks {
|
||||||
}
|
if strconv.FormatInt(bank.Id, 10) == req.BankCode {
|
||||||
|
validBank = true
|
||||||
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
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !validBank {
|
||||||
if targetWallet == nil {
|
return fmt.Errorf("invalid bank code")
|
||||||
return fmt.Errorf("no wallet found with the specified ID")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
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,
|
BeneficiaryName: req.BeneficiaryName,
|
||||||
PaymentOption: domain.PaymentOption(domain.BANK),
|
PaymentOption: domain.PaymentOption(domain.BANK),
|
||||||
BranchID: req.BranchID,
|
BranchID: req.BranchID,
|
||||||
BranchName: branch.Name,
|
// BranchName: branch.Name,
|
||||||
BranchLocation: branch.Location,
|
// BranchLocation: branch.Location,
|
||||||
// CashierID: user.ID,
|
// CashierID: user.ID,
|
||||||
// CashierName: user.FullName,
|
// CashierName: user.FullName,
|
||||||
FullName: user.FirstName + " " + user.LastName,
|
FullName: user.FirstName + " " + user.LastName,
|
||||||
PhoneNumber: user.PhoneNumber,
|
PhoneNumber: user.PhoneNumber,
|
||||||
CompanyID: branch.CompanyID,
|
// CompanyID: branch.CompanyID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create transaction: %w", err)
|
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)
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
|
if req.Amount <= 0 {
|
||||||
|
return "", fmt.Errorf("amount must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
user, err := s.userStore.GetUserByID(ctx, userID)
|
user, err := s.userStore.GetUserByID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
@ -269,20 +287,22 @@ func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domai
|
||||||
|
|
||||||
txID := uuid.New().String()
|
txID := uuid.New().String()
|
||||||
|
|
||||||
_, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{
|
fmt.Printf("\n\nChapa deposit transaction created: %v%v\n\n", branch, user)
|
||||||
Amount: req.Amount,
|
|
||||||
Type: domain.TransactionType(domain.TRANSACTION_DEPOSIT),
|
// _, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{
|
||||||
ReferenceNumber: txID,
|
// Amount: req.Amount,
|
||||||
BranchID: req.BranchID,
|
// Type: domain.TransactionType(domain.TRANSACTION_DEPOSIT),
|
||||||
BranchName: branch.Name,
|
// ReferenceNumber: txID,
|
||||||
BranchLocation: branch.Location,
|
// BranchID: req.BranchID,
|
||||||
FullName: user.FirstName + " " + user.LastName,
|
// BranchName: branch.Name,
|
||||||
PhoneNumber: user.PhoneNumber,
|
// BranchLocation: branch.Location,
|
||||||
CompanyID: branch.CompanyID,
|
// FullName: user.FirstName + " " + user.LastName,
|
||||||
})
|
// PhoneNumber: user.PhoneNumber,
|
||||||
if err != nil {
|
// // CompanyID: branch.CompanyID,
|
||||||
return "", err
|
// })
|
||||||
}
|
// if err != nil {
|
||||||
|
// return "", err
|
||||||
|
// }
|
||||||
|
|
||||||
// Fetch user details for Chapa payment
|
// Fetch user details for Chapa payment
|
||||||
userInfo, err := s.userStore.GetUserByID(ctx, userID)
|
userInfo, err := s.userStore.GetUserByID(ctx, userID)
|
||||||
|
|
@ -290,6 +310,8 @@ func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domai
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fmt.Printf("\n\nCallbackURL is:%v\n\n", s.config.CHAPA_CALLBACK_URL)
|
||||||
|
|
||||||
// Build Chapa InitPaymentRequest (matches Chapa API)
|
// Build Chapa InitPaymentRequest (matches Chapa API)
|
||||||
paymentReq := domain.InitPaymentRequest{
|
paymentReq := domain.InitPaymentRequest{
|
||||||
Amount: req.Amount,
|
Amount: req.Amount,
|
||||||
|
|
@ -298,14 +320,19 @@ func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domai
|
||||||
FirstName: userInfo.FirstName,
|
FirstName: userInfo.FirstName,
|
||||||
LastName: userInfo.LastName,
|
LastName: userInfo.LastName,
|
||||||
TxRef: txID,
|
TxRef: txID,
|
||||||
CallbackURL: s.config.CHAPA_CALLBACK_URL,
|
CallbackURL: "https://fortunebet.com/api/v1/payments/callback",
|
||||||
ReturnURL: s.config.CHAPA_RETURN_URL,
|
ReturnURL: "https://fortunebet.com/api/v1/payment-success",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call Chapa to initialize payment
|
// Call Chapa to initialize payment
|
||||||
paymentURL, err := s.chapaClient.InitPayment(ctx, paymentReq)
|
var paymentURL string
|
||||||
if err != nil {
|
maxRetries := 3
|
||||||
return "", err
|
for range maxRetries {
|
||||||
|
paymentURL, err = s.chapaClient.InitPayment(ctx, paymentReq)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second) // Backoff
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit DB transaction
|
// Commit DB transaction
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
|
||||||
|
|
||||||
eventID, err := strconv.ParseInt(event.ID, 10, 64)
|
eventID, err := strconv.ParseInt(event.ID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to parse event id")
|
s.logger.Error("Failed to parse event id", "error", err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,6 +58,7 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ Failed to create request for event %d: %v", eventID, err)
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
15
internal/services/report/port.go
Normal file
15
internal/services/report/port.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
509
internal/services/report/service.go
Normal file
509
internal/services/report/service.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -12,4 +12,7 @@ type TransactionStore interface {
|
||||||
GetAllTransactions(ctx context.Context, filter domain.TransactionFilter) ([]domain.Transaction, error)
|
GetAllTransactions(ctx context.Context, filter domain.TransactionFilter) ([]domain.Transaction, error)
|
||||||
GetTransactionByBranch(ctx context.Context, id int64) ([]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
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@ type UserStore interface {
|
||||||
GetUserByPhone(ctx context.Context, phoneNum string) (domain.User, error)
|
GetUserByPhone(ctx context.Context, phoneNum string) (domain.User, error)
|
||||||
SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]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
|
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 {
|
type SmsGateway interface {
|
||||||
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
|
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
|
||||||
|
|
|
||||||
216
internal/services/wallet/monitor/service.go
Normal file
216
internal/services/wallet/monitor/service.go
Normal file
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,8 @@ type WalletStore interface {
|
||||||
GetAllBranchWallets(ctx context.Context) ([]domain.BranchWallet, error)
|
GetAllBranchWallets(ctx context.Context) ([]domain.BranchWallet, error)
|
||||||
UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error
|
UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error
|
||||||
UpdateWalletActive(ctx context.Context, id int64, isActive bool) error
|
UpdateWalletActive(ctx context.Context, id int64, isActive bool) error
|
||||||
|
|
||||||
|
GetBalanceSummary(ctx context.Context, filter domain.ReportFilter) (domain.BalanceSummary, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransferStore interface {
|
type TransferStore interface {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,23 @@
|
||||||
package wallet
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
||||||
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
walletStore WalletStore
|
walletStore WalletStore
|
||||||
transferStore TransferStore
|
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{
|
return &Service{
|
||||||
walletStore: walletStore,
|
walletStore: walletStore,
|
||||||
transferStore: transferStore,
|
transferStore: transferStore,
|
||||||
|
notificationStore: notificationStore,
|
||||||
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,96 @@ package wallet
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrWalletNotTransferable = errors.New("wallet is not transferable")
|
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) {
|
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)
|
return s.transferStore.CreateTransfer(ctx, transfer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
|
||||||
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
|
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/result"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||||
|
|
@ -45,6 +46,7 @@ type App struct {
|
||||||
userSvc *user.Service
|
userSvc *user.Service
|
||||||
betSvc *bet.Service
|
betSvc *bet.Service
|
||||||
virtualGameSvc virtualgameservice.VirtualGameService
|
virtualGameSvc virtualgameservice.VirtualGameService
|
||||||
|
reportSvc *report.Service
|
||||||
chapaSvc *chapa.Service
|
chapaSvc *chapa.Service
|
||||||
walletSvc *wallet.Service
|
walletSvc *wallet.Service
|
||||||
transactionSvc *transaction.Service
|
transactionSvc *transaction.Service
|
||||||
|
|
@ -67,6 +69,7 @@ func NewApp(
|
||||||
userSvc *user.Service,
|
userSvc *user.Service,
|
||||||
ticketSvc *ticket.Service,
|
ticketSvc *ticket.Service,
|
||||||
betSvc *bet.Service,
|
betSvc *bet.Service,
|
||||||
|
reportSvc *report.Service,
|
||||||
chapaSvc *chapa.Service,
|
chapaSvc *chapa.Service,
|
||||||
walletSvc *wallet.Service,
|
walletSvc *wallet.Service,
|
||||||
transactionSvc *transaction.Service,
|
transactionSvc *transaction.Service,
|
||||||
|
|
@ -107,6 +110,7 @@ func NewApp(
|
||||||
userSvc: userSvc,
|
userSvc: userSvc,
|
||||||
ticketSvc: ticketSvc,
|
ticketSvc: ticketSvc,
|
||||||
betSvc: betSvc,
|
betSvc: betSvc,
|
||||||
|
reportSvc: reportSvc,
|
||||||
chapaSvc: chapaSvc,
|
chapaSvc: chapaSvc,
|
||||||
walletSvc: walletSvc,
|
walletSvc: walletSvc,
|
||||||
transactionSvc: transactionSvc,
|
transactionSvc: transactionSvc,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
|
||||||
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
|
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/ticket"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
|
|
@ -30,6 +31,7 @@ type Handler struct {
|
||||||
notificationSvc *notificationservice.Service
|
notificationSvc *notificationservice.Service
|
||||||
userSvc *user.Service
|
userSvc *user.Service
|
||||||
referralSvc referralservice.ReferralStore
|
referralSvc referralservice.ReferralStore
|
||||||
|
reportSvc report.ReportStore
|
||||||
chapaSvc chapa.ChapaPort
|
chapaSvc chapa.ChapaPort
|
||||||
walletSvc *wallet.Service
|
walletSvc *wallet.Service
|
||||||
transactionSvc *transaction.Service
|
transactionSvc *transaction.Service
|
||||||
|
|
@ -53,6 +55,7 @@ func New(
|
||||||
logger *slog.Logger,
|
logger *slog.Logger,
|
||||||
notificationSvc *notificationservice.Service,
|
notificationSvc *notificationservice.Service,
|
||||||
validator *customvalidator.CustomValidator,
|
validator *customvalidator.CustomValidator,
|
||||||
|
reportSvc report.ReportStore,
|
||||||
chapaSvc chapa.ChapaPort,
|
chapaSvc chapa.ChapaPort,
|
||||||
walletSvc *wallet.Service,
|
walletSvc *wallet.Service,
|
||||||
referralSvc referralservice.ReferralStore,
|
referralSvc referralservice.ReferralStore,
|
||||||
|
|
@ -75,6 +78,7 @@ func New(
|
||||||
return &Handler{
|
return &Handler{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
notificationSvc: notificationSvc,
|
notificationSvc: notificationSvc,
|
||||||
|
reportSvc: reportSvc,
|
||||||
chapaSvc: chapaSvc,
|
chapaSvc: chapaSvc,
|
||||||
walletSvc: walletSvc,
|
walletSvc: walletSvc,
|
||||||
referralSvc: referralSvc,
|
referralSvc: referralSvc,
|
||||||
|
|
|
||||||
116
internal/web_server/handlers/report.go
Normal file
116
internal/web_server/handlers/report.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
package httpserver
|
package httpserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
_ "github.com/SamuelTariku/FortuneBet-Backend/docs"
|
_ "github.com/SamuelTariku/FortuneBet-Backend/docs"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"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"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/handlers"
|
||||||
|
|
||||||
|
|
@ -18,6 +22,7 @@ func (a *App) initAppRoutes() {
|
||||||
a.logger,
|
a.logger,
|
||||||
a.NotidicationStore,
|
a.NotidicationStore,
|
||||||
a.validator,
|
a.validator,
|
||||||
|
a.reportSvc,
|
||||||
a.chapaSvc,
|
a.chapaSvc,
|
||||||
a.walletSvc,
|
a.walletSvc,
|
||||||
a.referralSvc,
|
a.referralSvc,
|
||||||
|
|
@ -187,6 +192,22 @@ func (a *App) initAppRoutes() {
|
||||||
group.Post("/chapa/payments/deposit", a.authMiddleware, h.DepositUsingChapa)
|
group.Post("/chapa/payments/deposit", a.authMiddleware, h.DepositUsingChapa)
|
||||||
group.Get("/chapa/banks", a.authMiddleware, h.ReadChapaBanks)
|
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.Post("/chapa/payments/initialize", h.InitializePayment)
|
||||||
// group.Get("/chapa/payments/verify/:tx_ref", h.VerifyTransaction)
|
// group.Get("/chapa/payments/verify/:tx_ref", h.VerifyTransaction)
|
||||||
// group.Post("/chapa/payments/callback", h.ReceiveWebhook)
|
// group.Post("/chapa/payments/callback", h.ReceiveWebhook)
|
||||||
|
|
@ -202,6 +223,10 @@ func (a *App) initAppRoutes() {
|
||||||
group.Get("/veli-games/launch", h.LaunchVeliGame)
|
group.Get("/veli-games/launch", h.LaunchVeliGame)
|
||||||
group.Post("/webhooks/veli-games", h.HandleVeliCallback)
|
group.Post("/webhooks/veli-games", h.HandleVeliCallback)
|
||||||
|
|
||||||
|
//mongoDB logs
|
||||||
|
ctx := context.Background()
|
||||||
|
group.Get("/logs", mongoLogger.GetLogsHandler(ctx))
|
||||||
|
|
||||||
// Recommendation Routes
|
// Recommendation Routes
|
||||||
group.Get("/virtual-games/recommendations/:userID", h.GetRecommendations)
|
group.Get("/virtual-games/recommendations/:userID", h.GetRecommendations)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user