Merge branch 'main' into ticket-bet
This commit is contained in:
commit
35a03e1959
97
cmd/main.go
97
cmd/main.go
|
|
@ -3,17 +3,25 @@ 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"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
// "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"
|
||||||
mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
||||||
mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms"
|
|
||||||
|
// mongologger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
||||||
|
|
||||||
|
// "github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||||
|
|
||||||
// "github.com/SamuelTariku/FortuneBet-Backend/internal/router"
|
// "github.com/SamuelTariku/FortuneBet-Backend/internal/router"
|
||||||
|
|
@ -28,6 +36,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"
|
||||||
|
|
@ -36,6 +45,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"
|
||||||
|
|
@ -71,31 +81,71 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
|
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
|
||||||
|
|
||||||
|
mongoLogger.Init()
|
||||||
|
mongoDBLogger := zap.L()
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
|
||||||
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
|
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
|
||||||
mockSms := mocksms.NewMockSMS()
|
|
||||||
mockEmail := mockemail.NewMockEmail()
|
|
||||||
|
|
||||||
userSvc := user.NewService(store, store, mockSms, mockEmail)
|
userSvc := user.NewService(store, store, cfg)
|
||||||
|
|
||||||
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)
|
||||||
leagueSvc := league.New(store)
|
leagueSvc := league.New(store)
|
||||||
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger)
|
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, mongoDBLogger)
|
||||||
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc)
|
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc)
|
||||||
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(
|
||||||
|
|
@ -123,6 +173,33 @@ func main() {
|
||||||
store,
|
store,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
reportSvc := report.NewService(
|
||||||
|
bet.BetStore(store), // Must implement BetStore
|
||||||
|
wallet.WalletStore(store), // Must implement WalletStore
|
||||||
|
transaction.TransactionStore(store),
|
||||||
|
branch.BranchStore(store),
|
||||||
|
user.UserStore(store),
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
|
@ -130,7 +207,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, leagueSvc, referalSvc, virtualGameSvc, aleaService, veliService, recommendationSvc, resultSvc, cfg)
|
ticketSvc, betSvc, reportSvc, chapaSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, leagueSvc, 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 {
|
||||||
|
|
|
||||||
|
|
@ -137,25 +137,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),
|
||||||
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
|
||||||
);
|
);
|
||||||
|
|
@ -163,6 +163,7 @@ CREATE TABLE IF NOT EXISTS branches (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
location VARCHAR(255) NOT NULL,
|
location VARCHAR(255) NOT NULL,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT false,
|
||||||
wallet_id BIGINT NOT NULL,
|
wallet_id BIGINT NOT NULL,
|
||||||
branch_manager_id BIGINT NOT NULL,
|
branch_manager_id BIGINT NOT NULL,
|
||||||
company_id BIGINT NOT NULL,
|
company_id BIGINT NOT NULL,
|
||||||
|
|
@ -261,6 +262,7 @@ FROM companies
|
||||||
JOIN wallets ON wallets.id = companies.wallet_id
|
JOIN wallets ON wallets.id = companies.wallet_id
|
||||||
JOIN users ON users.id = companies.admin_id;
|
JOIN users ON users.id = companies.admin_id;
|
||||||
;
|
;
|
||||||
|
|
||||||
CREATE VIEW branch_details AS
|
CREATE VIEW branch_details AS
|
||||||
SELECT branches.*,
|
SELECT branches.*,
|
||||||
CONCAT(users.first_name, ' ', users.last_name) AS manager_name,
|
CONCAT(users.first_name, ' ', users.last_name) AS manager_name,
|
||||||
|
|
@ -287,6 +289,10 @@ FROM tickets
|
||||||
LEFT JOIN ticket_outcomes ON tickets.id = ticket_outcomes.ticket_id
|
LEFT JOIN ticket_outcomes ON tickets.id = ticket_outcomes.ticket_id
|
||||||
GROUP BY tickets.id;
|
GROUP BY tickets.id;
|
||||||
-- Foreign Keys
|
-- Foreign Keys
|
||||||
|
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD CONSTRAINT unique_email UNIQUE (email),
|
||||||
|
ADD CONSTRAINT unique_phone_number UNIQUE (phone_number);
|
||||||
ALTER TABLE refresh_tokens
|
ALTER TABLE refresh_tokens
|
||||||
ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id);
|
ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id);
|
||||||
ALTER TABLE bets
|
ALTER TABLE bets
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
DROP TABLE notifications;
|
DROP TABLE notifications;
|
||||||
|
DROP TABLE wallet_threshold_notifications;
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,18 @@ 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);
|
||||||
|
|
||||||
CREATE INDEX idx_notifications_type ON notifications (type);
|
CREATE INDEX idx_notifications_type ON notifications (type);
|
||||||
|
|
||||||
|
|
|
||||||
24
db/query/monitor.sql
Normal file
24
db/query/monitor.sql
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
-- name: GetAllCompaniesBranch :many
|
||||||
|
SELECT id, name, wallet_id, admin_id
|
||||||
|
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);
|
||||||
|
|
@ -14,18 +14,8 @@ INSERT INTO users (
|
||||||
company_id
|
company_id
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1,
|
$1, $2, $3, $4, $5, $6,
|
||||||
$2,
|
$7, $8, $9, $10, $11, $12
|
||||||
$3,
|
|
||||||
$4,
|
|
||||||
$5,
|
|
||||||
$6,
|
|
||||||
$7,
|
|
||||||
$8,
|
|
||||||
$9,
|
|
||||||
$10,
|
|
||||||
$11,
|
|
||||||
$12
|
|
||||||
)
|
)
|
||||||
RETURNING id,
|
RETURNING id,
|
||||||
first_name,
|
first_name,
|
||||||
|
|
@ -39,6 +29,7 @@ RETURNING id,
|
||||||
updated_at,
|
updated_at,
|
||||||
suspended,
|
suspended,
|
||||||
company_id;
|
company_id;
|
||||||
|
|
||||||
-- name: GetUserByID :one
|
-- name: GetUserByID :one
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM users
|
FROM users
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
@ -4676,6 +4766,17 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.Odd": {
|
"domain.Odd": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -6332,6 +6433,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",
|
||||||
|
|
@ -4668,6 +4758,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.Odd": {
|
"domain.Odd": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -6324,6 +6425,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:
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ INSERT INTO branches (
|
||||||
is_self_owned
|
is_self_owned
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
RETURNING id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at
|
RETURNING id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateBranchParams struct {
|
type CreateBranchParams struct {
|
||||||
|
|
@ -47,6 +47,7 @@ func (q *Queries) CreateBranch(ctx context.Context, arg CreateBranchParams) (Bra
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Location,
|
&i.Location,
|
||||||
|
&i.IsActive,
|
||||||
&i.WalletID,
|
&i.WalletID,
|
||||||
&i.BranchManagerID,
|
&i.BranchManagerID,
|
||||||
&i.CompanyID,
|
&i.CompanyID,
|
||||||
|
|
@ -154,7 +155,7 @@ func (q *Queries) DeleteBranchOperation(ctx context.Context, arg DeleteBranchOpe
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetAllBranches = `-- name: GetAllBranches :many
|
const GetAllBranches = `-- name: GetAllBranches :many
|
||||||
SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
|
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
|
||||||
FROM branch_details
|
FROM branch_details
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -171,6 +172,7 @@ func (q *Queries) GetAllBranches(ctx context.Context) ([]BranchDetail, error) {
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Location,
|
&i.Location,
|
||||||
|
&i.IsActive,
|
||||||
&i.WalletID,
|
&i.WalletID,
|
||||||
&i.BranchManagerID,
|
&i.BranchManagerID,
|
||||||
&i.CompanyID,
|
&i.CompanyID,
|
||||||
|
|
@ -217,7 +219,7 @@ func (q *Queries) GetAllSupportedOperations(ctx context.Context) ([]SupportedOpe
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetBranchByCashier = `-- name: GetBranchByCashier :one
|
const GetBranchByCashier = `-- name: GetBranchByCashier :one
|
||||||
SELECT branches.id, branches.name, branches.location, branches.wallet_id, branches.branch_manager_id, branches.company_id, branches.is_self_owned, branches.created_at, branches.updated_at
|
SELECT branches.id, branches.name, branches.location, branches.is_active, branches.wallet_id, branches.branch_manager_id, branches.company_id, branches.is_self_owned, branches.created_at, branches.updated_at
|
||||||
FROM branch_cashiers
|
FROM branch_cashiers
|
||||||
JOIN branches ON branch_cashiers.branch_id = branches.id
|
JOIN branches ON branch_cashiers.branch_id = branches.id
|
||||||
WHERE branch_cashiers.user_id = $1
|
WHERE branch_cashiers.user_id = $1
|
||||||
|
|
@ -230,6 +232,7 @@ func (q *Queries) GetBranchByCashier(ctx context.Context, userID int64) (Branch,
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Location,
|
&i.Location,
|
||||||
|
&i.IsActive,
|
||||||
&i.WalletID,
|
&i.WalletID,
|
||||||
&i.BranchManagerID,
|
&i.BranchManagerID,
|
||||||
&i.CompanyID,
|
&i.CompanyID,
|
||||||
|
|
@ -241,7 +244,7 @@ func (q *Queries) GetBranchByCashier(ctx context.Context, userID int64) (Branch,
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetBranchByCompanyID = `-- name: GetBranchByCompanyID :many
|
const GetBranchByCompanyID = `-- name: GetBranchByCompanyID :many
|
||||||
SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
|
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
|
||||||
FROM branch_details
|
FROM branch_details
|
||||||
WHERE company_id = $1
|
WHERE company_id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -259,6 +262,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Location,
|
&i.Location,
|
||||||
|
&i.IsActive,
|
||||||
&i.WalletID,
|
&i.WalletID,
|
||||||
&i.BranchManagerID,
|
&i.BranchManagerID,
|
||||||
&i.CompanyID,
|
&i.CompanyID,
|
||||||
|
|
@ -280,7 +284,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetBranchByID = `-- name: GetBranchByID :one
|
const GetBranchByID = `-- name: GetBranchByID :one
|
||||||
SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
|
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
|
||||||
FROM branch_details
|
FROM branch_details
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -292,6 +296,7 @@ func (q *Queries) GetBranchByID(ctx context.Context, id int64) (BranchDetail, er
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Location,
|
&i.Location,
|
||||||
|
&i.IsActive,
|
||||||
&i.WalletID,
|
&i.WalletID,
|
||||||
&i.BranchManagerID,
|
&i.BranchManagerID,
|
||||||
&i.CompanyID,
|
&i.CompanyID,
|
||||||
|
|
@ -306,7 +311,7 @@ func (q *Queries) GetBranchByID(ctx context.Context, id int64) (BranchDetail, er
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetBranchByManagerID = `-- name: GetBranchByManagerID :many
|
const GetBranchByManagerID = `-- name: GetBranchByManagerID :many
|
||||||
SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
|
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
|
||||||
FROM branch_details
|
FROM branch_details
|
||||||
WHERE branch_manager_id = $1
|
WHERE branch_manager_id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -324,6 +329,7 @@ func (q *Queries) GetBranchByManagerID(ctx context.Context, branchManagerID int6
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Location,
|
&i.Location,
|
||||||
|
&i.IsActive,
|
||||||
&i.WalletID,
|
&i.WalletID,
|
||||||
&i.BranchManagerID,
|
&i.BranchManagerID,
|
||||||
&i.CompanyID,
|
&i.CompanyID,
|
||||||
|
|
@ -392,7 +398,7 @@ func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]Ge
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchBranchByName = `-- name: SearchBranchByName :many
|
const SearchBranchByName = `-- name: SearchBranchByName :many
|
||||||
SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
|
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance
|
||||||
FROM branch_details
|
FROM branch_details
|
||||||
WHERE name ILIKE '%' || $1 || '%'
|
WHERE name ILIKE '%' || $1 || '%'
|
||||||
`
|
`
|
||||||
|
|
@ -410,6 +416,7 @@ func (q *Queries) SearchBranchByName(ctx context.Context, dollar_1 pgtype.Text)
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Location,
|
&i.Location,
|
||||||
|
&i.IsActive,
|
||||||
&i.WalletID,
|
&i.WalletID,
|
||||||
&i.BranchManagerID,
|
&i.BranchManagerID,
|
||||||
&i.CompanyID,
|
&i.CompanyID,
|
||||||
|
|
@ -438,7 +445,7 @@ SET name = COALESCE($2, name),
|
||||||
company_id = COALESCE($5, company_id),
|
company_id = COALESCE($5, company_id),
|
||||||
is_self_owned = COALESCE($6, is_self_owned)
|
is_self_owned = COALESCE($6, is_self_owned)
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at
|
RETURNING id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateBranchParams struct {
|
type UpdateBranchParams struct {
|
||||||
|
|
@ -464,6 +471,7 @@ func (q *Queries) UpdateBranch(ctx context.Context, arg UpdateBranchParams) (Bra
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.Location,
|
&i.Location,
|
||||||
|
&i.IsActive,
|
||||||
&i.WalletID,
|
&i.WalletID,
|
||||||
&i.BranchManagerID,
|
&i.BranchManagerID,
|
||||||
&i.CompanyID,
|
&i.CompanyID,
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ type Branch struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Location string `json:"location"`
|
Location string `json:"location"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
WalletID int64 `json:"wallet_id"`
|
WalletID int64 `json:"wallet_id"`
|
||||||
BranchManagerID int64 `json:"branch_manager_id"`
|
BranchManagerID int64 `json:"branch_manager_id"`
|
||||||
CompanyID int64 `json:"company_id"`
|
CompanyID int64 `json:"company_id"`
|
||||||
|
|
@ -130,6 +131,7 @@ type BranchDetail struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Location string `json:"location"`
|
Location string `json:"location"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
WalletID int64 `json:"wallet_id"`
|
WalletID int64 `json:"wallet_id"`
|
||||||
BranchManagerID int64 `json:"branch_manager_id"`
|
BranchManagerID int64 `json:"branch_manager_id"`
|
||||||
CompanyID int64 `json:"company_id"`
|
CompanyID int64 `json:"company_id"`
|
||||||
|
|
@ -363,25 +365,25 @@ type Transaction struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
BranchID int64 `json:"branch_id"`
|
BranchID int64 `json:"branch_id"`
|
||||||
CompanyID int64 `json:"company_id"`
|
CompanyID pgtype.Int8 `json:"company_id"`
|
||||||
CashierID int64 `json:"cashier_id"`
|
CashierID pgtype.Int8 `json:"cashier_id"`
|
||||||
CashierName string `json:"cashier_name"`
|
CashierName pgtype.Text `json:"cashier_name"`
|
||||||
BetID int64 `json:"bet_id"`
|
BetID pgtype.Int8 `json:"bet_id"`
|
||||||
NumberOfOutcomes int64 `json:"number_of_outcomes"`
|
NumberOfOutcomes pgtype.Int8 `json:"number_of_outcomes"`
|
||||||
Type int64 `json:"type"`
|
Type pgtype.Int8 `json:"type"`
|
||||||
PaymentOption int64 `json:"payment_option"`
|
PaymentOption pgtype.Int8 `json:"payment_option"`
|
||||||
FullName string `json:"full_name"`
|
FullName pgtype.Text `json:"full_name"`
|
||||||
PhoneNumber string `json:"phone_number"`
|
PhoneNumber pgtype.Text `json:"phone_number"`
|
||||||
BankCode string `json:"bank_code"`
|
BankCode pgtype.Text `json:"bank_code"`
|
||||||
BeneficiaryName string `json:"beneficiary_name"`
|
BeneficiaryName pgtype.Text `json:"beneficiary_name"`
|
||||||
AccountName string `json:"account_name"`
|
AccountName pgtype.Text `json:"account_name"`
|
||||||
AccountNumber string `json:"account_number"`
|
AccountNumber pgtype.Text `json:"account_number"`
|
||||||
ReferenceNumber string `json:"reference_number"`
|
ReferenceNumber pgtype.Text `json:"reference_number"`
|
||||||
Verified bool `json:"verified"`
|
Verified bool `json:"verified"`
|
||||||
ApprovedBy pgtype.Int8 `json:"approved_by"`
|
ApprovedBy pgtype.Int8 `json:"approved_by"`
|
||||||
ApproverName pgtype.Text `json:"approver_name"`
|
ApproverName pgtype.Text `json:"approver_name"`
|
||||||
BranchLocation string `json:"branch_location"`
|
BranchLocation pgtype.Text `json:"branch_location"`
|
||||||
BranchName string `json:"branch_name"`
|
BranchName pgtype.Text `json:"branch_name"`
|
||||||
CreatedAt pgtype.Timestamp `json:"created_at"`
|
CreatedAt pgtype.Timestamp `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamp `json:"updated_at"`
|
UpdatedAt pgtype.Timestamp `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
@ -471,6 +473,12 @@ type Wallet struct {
|
||||||
CashBalance pgtype.Numeric `json:"cash_balance"`
|
CashBalance pgtype.Numeric `json:"cash_balance"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WalletThresholdNotification struct {
|
||||||
|
CompanyID int64 `json:"company_id"`
|
||||||
|
Threshold float64 `json:"threshold"`
|
||||||
|
NotifiedAt pgtype.Timestamptz `json:"notified_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type WalletTransfer struct {
|
type WalletTransfer struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
|
|
|
||||||
131
gen/db/monitor.sql.go
Normal file
131
gen/db/monitor.sql.go
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.29.0
|
||||||
|
// source: monitor.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CountThresholdNotifications = `-- name: CountThresholdNotifications :one
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM wallet_threshold_notifications
|
||||||
|
WHERE company_id = $1 AND threshold = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type CountThresholdNotificationsParams struct {
|
||||||
|
CompanyID int64 `json:"company_id"`
|
||||||
|
Threshold float64 `json:"threshold"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CountThresholdNotifications(ctx context.Context, arg CountThresholdNotificationsParams) (int64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountThresholdNotifications, arg.CompanyID, arg.Threshold)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateThresholdNotification = `-- name: CreateThresholdNotification :exec
|
||||||
|
INSERT INTO wallet_threshold_notifications (company_id, threshold)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateThresholdNotificationParams struct {
|
||||||
|
CompanyID int64 `json:"company_id"`
|
||||||
|
Threshold float64 `json:"threshold"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateThresholdNotification(ctx context.Context, arg CreateThresholdNotificationParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, CreateThresholdNotification, arg.CompanyID, arg.Threshold)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetAllCompaniesBranch = `-- name: GetAllCompaniesBranch :many
|
||||||
|
SELECT id, name, wallet_id, admin_id
|
||||||
|
FROM companies
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetAllCompaniesBranchRow struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
WalletID int64 `json:"wallet_id"`
|
||||||
|
AdminID int64 `json:"admin_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAllCompaniesBranch(ctx context.Context) ([]GetAllCompaniesBranchRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetAllCompaniesBranch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetAllCompaniesBranchRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetAllCompaniesBranchRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.WalletID,
|
||||||
|
&i.AdminID,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetBranchesByCompanyID = `-- name: GetBranchesByCompanyID :many
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
location,
|
||||||
|
wallet_id,
|
||||||
|
branch_manager_id,
|
||||||
|
company_id,
|
||||||
|
is_self_owned
|
||||||
|
FROM branches
|
||||||
|
WHERE company_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetBranchesByCompanyIDRow struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
WalletID int64 `json:"wallet_id"`
|
||||||
|
BranchManagerID int64 `json:"branch_manager_id"`
|
||||||
|
CompanyID int64 `json:"company_id"`
|
||||||
|
IsSelfOwned bool `json:"is_self_owned"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetBranchesByCompanyID(ctx context.Context, companyID int64) ([]GetBranchesByCompanyIDRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetBranchesByCompanyID, companyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetBranchesByCompanyIDRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetBranchesByCompanyIDRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Location,
|
||||||
|
&i.WalletID,
|
||||||
|
&i.BranchManagerID,
|
||||||
|
&i.CompanyID,
|
||||||
|
&i.IsSelfOwned,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
@ -58,22 +58,22 @@ RETURNING id, amount, branch_id, company_id, cashier_id, cashier_name, bet_id, n
|
||||||
type CreateTransactionParams struct {
|
type CreateTransactionParams struct {
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
BranchID int64 `json:"branch_id"`
|
BranchID int64 `json:"branch_id"`
|
||||||
CashierID int64 `json:"cashier_id"`
|
CashierID pgtype.Int8 `json:"cashier_id"`
|
||||||
BetID int64 `json:"bet_id"`
|
BetID pgtype.Int8 `json:"bet_id"`
|
||||||
Type int64 `json:"type"`
|
Type pgtype.Int8 `json:"type"`
|
||||||
PaymentOption int64 `json:"payment_option"`
|
PaymentOption pgtype.Int8 `json:"payment_option"`
|
||||||
FullName string `json:"full_name"`
|
FullName pgtype.Text `json:"full_name"`
|
||||||
PhoneNumber string `json:"phone_number"`
|
PhoneNumber pgtype.Text `json:"phone_number"`
|
||||||
BankCode string `json:"bank_code"`
|
BankCode pgtype.Text `json:"bank_code"`
|
||||||
BeneficiaryName string `json:"beneficiary_name"`
|
BeneficiaryName pgtype.Text `json:"beneficiary_name"`
|
||||||
AccountName string `json:"account_name"`
|
AccountName pgtype.Text `json:"account_name"`
|
||||||
AccountNumber string `json:"account_number"`
|
AccountNumber pgtype.Text `json:"account_number"`
|
||||||
ReferenceNumber string `json:"reference_number"`
|
ReferenceNumber pgtype.Text `json:"reference_number"`
|
||||||
NumberOfOutcomes int64 `json:"number_of_outcomes"`
|
NumberOfOutcomes pgtype.Int8 `json:"number_of_outcomes"`
|
||||||
BranchName string `json:"branch_name"`
|
BranchName pgtype.Text `json:"branch_name"`
|
||||||
BranchLocation string `json:"branch_location"`
|
BranchLocation pgtype.Text `json:"branch_location"`
|
||||||
CompanyID int64 `json:"company_id"`
|
CompanyID pgtype.Int8 `json:"company_id"`
|
||||||
CashierName string `json:"cashier_name"`
|
CashierName pgtype.Text `json:"cashier_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (Transaction, error) {
|
func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (Transaction, error) {
|
||||||
|
|
|
||||||
|
|
@ -59,18 +59,8 @@ INSERT INTO users (
|
||||||
company_id
|
company_id
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1,
|
$1, $2, $3, $4, $5, $6,
|
||||||
$2,
|
$7, $8, $9, $10, $11, $12
|
||||||
$3,
|
|
||||||
$4,
|
|
||||||
$5,
|
|
||||||
$6,
|
|
||||||
$7,
|
|
||||||
$8,
|
|
||||||
$9,
|
|
||||||
$10,
|
|
||||||
$11,
|
|
||||||
$12
|
|
||||||
)
|
)
|
||||||
RETURNING id,
|
RETURNING id,
|
||||||
first_name,
|
first_name,
|
||||||
|
|
|
||||||
18
go.mod
18
go.mod
|
|
@ -56,11 +56,27 @@ 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 (
|
||||||
|
github.com/resend/resend-go/v2 v2.20.0 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
)
|
||||||
|
|
|
||||||
35
go.sum
35
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=
|
||||||
|
|
@ -104,6 +110,8 @@ github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT9
|
||||||
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/resend/resend-go/v2 v2.20.0 h1:MrIrgV0aHhwRgmcRPw33Nexn6aGJvCvG2XwfFpAMBGM=
|
||||||
|
github.com/resend/resend-go/v2 v2.20.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
|
@ -147,30 +155,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 +212,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=
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ var (
|
||||||
ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid")
|
ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid")
|
||||||
ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid")
|
ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid")
|
||||||
ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid")
|
ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid")
|
||||||
|
ErrMissingResendApiKey = errors.New("missing Resend Api key")
|
||||||
|
ErrMissingResendSenderEmail = errors.New("missing Resend sender name")
|
||||||
)
|
)
|
||||||
|
|
||||||
type AleaPlayConfig struct {
|
type AleaPlayConfig struct {
|
||||||
|
|
@ -75,6 +77,8 @@ type Config struct {
|
||||||
PopOK domain.PopOKConfig
|
PopOK domain.PopOKConfig
|
||||||
AleaPlay AleaPlayConfig `mapstructure:"alea_play"`
|
AleaPlay AleaPlayConfig `mapstructure:"alea_play"`
|
||||||
VeliGames VeliGamesConfig `mapstructure:"veli_games"`
|
VeliGames VeliGamesConfig `mapstructure:"veli_games"`
|
||||||
|
ResendApiKey string
|
||||||
|
ResendSenderEmail string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig() (*Config, error) {
|
func NewConfig() (*Config, error) {
|
||||||
|
|
@ -287,6 +291,19 @@ func (c *Config) loadEnv() error {
|
||||||
return ErrMissingBetToken
|
return ErrMissingBetToken
|
||||||
}
|
}
|
||||||
c.Bet365Token = betToken
|
c.Bet365Token = betToken
|
||||||
|
|
||||||
|
resendApiKey := os.Getenv("RESEND_API_KEY")
|
||||||
|
if resendApiKey == "" {
|
||||||
|
return ErrMissingResendApiKey
|
||||||
|
}
|
||||||
|
c.ResendApiKey = resendApiKey
|
||||||
|
|
||||||
|
resendSenderEmail := os.Getenv("RESEND_SENDER_EMAIL")
|
||||||
|
if resendSenderEmail == "" {
|
||||||
|
return ErrMissingResendSenderEmail
|
||||||
|
}
|
||||||
|
c.ResendSenderEmail = resendSenderEmail
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ type Branch struct {
|
||||||
WalletID int64
|
WalletID int64
|
||||||
BranchManagerID int64
|
BranchManagerID int64
|
||||||
CompanyID int64
|
CompanyID int64
|
||||||
|
IsSuspended bool
|
||||||
IsSelfOwned bool
|
IsSelfOwned bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18,6 +19,7 @@ type BranchDetail struct {
|
||||||
Balance Currency
|
Balance Currency
|
||||||
BranchManagerID int64
|
BranchManagerID int64
|
||||||
CompanyID int64
|
CompanyID int64
|
||||||
|
IsSuspended bool
|
||||||
IsSelfOwned bool
|
IsSelfOwned bool
|
||||||
ManagerName string
|
ManagerName string
|
||||||
ManagerPhoneNumber string
|
ManagerPhoneNumber string
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
internal/domain/mongoLogs.go
Normal file
12
internal/domain/mongoLogs.go
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,10 @@ const (
|
||||||
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"`
|
||||||
|
}
|
||||||
|
|
@ -13,8 +13,8 @@ type User struct {
|
||||||
ID int64
|
ID int64
|
||||||
FirstName string
|
FirstName string
|
||||||
LastName string
|
LastName string
|
||||||
Email string
|
Email string `json:"email"`
|
||||||
PhoneNumber string
|
PhoneNumber string `json:"phone_number"`
|
||||||
Password []byte
|
Password []byte
|
||||||
Role Role
|
Role Role
|
||||||
//
|
//
|
||||||
|
|
@ -29,6 +29,7 @@ type User struct {
|
||||||
//
|
//
|
||||||
CompanyID ValidInt64
|
CompanyID ValidInt64
|
||||||
}
|
}
|
||||||
|
|
||||||
type RegisterUserReq struct {
|
type RegisterUserReq struct {
|
||||||
FirstName string
|
FirstName string
|
||||||
LastName string
|
LastName string
|
||||||
|
|
@ -62,6 +63,7 @@ type UpdateUserReq struct {
|
||||||
FirstName ValidString
|
FirstName ValidString
|
||||||
LastName ValidString
|
LastName ValidString
|
||||||
Suspended ValidBool
|
Suspended ValidBool
|
||||||
|
|
||||||
CompanyID ValidInt64
|
CompanyID ValidInt64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,17 @@
|
||||||
package helpers
|
package helpers
|
||||||
|
|
||||||
import "github.com/google/uuid"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
func GenerateID() string {
|
func GenerateID() string {
|
||||||
return uuid.New().String()
|
return uuid.New().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateOTP() string {
|
||||||
|
num := 100000 + rand.UintN(899999)
|
||||||
|
return fmt.Sprintf("%d", num) // 6 digit random number [100,000 - 999,999]
|
||||||
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||||
|
|
@ -257,3 +258,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"
|
||||||
|
|
@ -13,18 +14,18 @@ func convertDBTransaction(transaction dbgen.Transaction) domain.Transaction {
|
||||||
ID: transaction.ID,
|
ID: transaction.ID,
|
||||||
Amount: domain.Currency(transaction.Amount),
|
Amount: domain.Currency(transaction.Amount),
|
||||||
BranchID: transaction.BranchID,
|
BranchID: transaction.BranchID,
|
||||||
CashierID: transaction.CashierID,
|
CashierID: transaction.CashierID.Int64,
|
||||||
BetID: transaction.BetID,
|
BetID: transaction.BetID.Int64,
|
||||||
NumberOfOutcomes: transaction.NumberOfOutcomes,
|
NumberOfOutcomes: transaction.NumberOfOutcomes.Int64,
|
||||||
Type: domain.TransactionType(transaction.Type),
|
Type: domain.TransactionType(transaction.Type.Int64),
|
||||||
PaymentOption: domain.PaymentOption(transaction.PaymentOption),
|
PaymentOption: domain.PaymentOption(transaction.PaymentOption.Int64),
|
||||||
FullName: transaction.FullName,
|
FullName: transaction.FullName.String,
|
||||||
PhoneNumber: transaction.PhoneNumber,
|
PhoneNumber: transaction.PhoneNumber.String,
|
||||||
BankCode: transaction.BankCode,
|
BankCode: transaction.BankCode.String,
|
||||||
BeneficiaryName: transaction.BeneficiaryName,
|
BeneficiaryName: transaction.BeneficiaryName.String,
|
||||||
AccountName: transaction.AccountName,
|
AccountName: transaction.AccountName.String,
|
||||||
AccountNumber: transaction.AccountNumber,
|
AccountNumber: transaction.AccountNumber.String,
|
||||||
ReferenceNumber: transaction.ReferenceNumber,
|
ReferenceNumber: transaction.ReferenceNumber.String,
|
||||||
ApprovedBy: domain.ValidInt64{
|
ApprovedBy: domain.ValidInt64{
|
||||||
Value: transaction.ApprovedBy.Int64,
|
Value: transaction.ApprovedBy.Int64,
|
||||||
Valid: transaction.ApprovedBy.Valid,
|
Valid: transaction.ApprovedBy.Valid,
|
||||||
|
|
@ -32,10 +33,10 @@ func convertDBTransaction(transaction dbgen.Transaction) domain.Transaction {
|
||||||
CreatedAt: transaction.CreatedAt.Time,
|
CreatedAt: transaction.CreatedAt.Time,
|
||||||
UpdatedAt: transaction.UpdatedAt.Time,
|
UpdatedAt: transaction.UpdatedAt.Time,
|
||||||
Verified: transaction.Verified,
|
Verified: transaction.Verified,
|
||||||
BranchName: transaction.BranchName,
|
BranchName: transaction.BranchName.String,
|
||||||
BranchLocation: transaction.BranchLocation,
|
BranchLocation: transaction.BranchLocation.String,
|
||||||
CashierName: transaction.CashierName,
|
CashierName: transaction.CashierName.String,
|
||||||
CompanyID: transaction.CompanyID,
|
CompanyID: transaction.CompanyID.Int64,
|
||||||
ApproverName: domain.ValidString{
|
ApproverName: domain.ValidString{
|
||||||
Value: transaction.ApproverName.String,
|
Value: transaction.ApproverName.String,
|
||||||
Valid: transaction.ApprovedBy.Valid,
|
Valid: transaction.ApprovedBy.Valid,
|
||||||
|
|
@ -47,22 +48,22 @@ func convertCreateTransaction(transaction domain.CreateTransaction) dbgen.Create
|
||||||
return dbgen.CreateTransactionParams{
|
return dbgen.CreateTransactionParams{
|
||||||
Amount: int64(transaction.Amount),
|
Amount: int64(transaction.Amount),
|
||||||
BranchID: transaction.BranchID,
|
BranchID: transaction.BranchID,
|
||||||
CashierID: transaction.CashierID,
|
CashierID: pgtype.Int8{Int64: transaction.CashierID, Valid: true},
|
||||||
BetID: transaction.BetID,
|
BetID: pgtype.Int8{Int64: transaction.BetID, Valid: true},
|
||||||
Type: int64(transaction.Type),
|
Type: pgtype.Int8{Int64: int64(transaction.Type), Valid: true},
|
||||||
PaymentOption: int64(transaction.PaymentOption),
|
PaymentOption: pgtype.Int8{Int64: int64(transaction.PaymentOption), Valid: true},
|
||||||
FullName: transaction.FullName,
|
FullName: pgtype.Text{String: transaction.FullName, Valid: transaction.FullName != ""},
|
||||||
PhoneNumber: transaction.PhoneNumber,
|
PhoneNumber: pgtype.Text{String: transaction.PhoneNumber, Valid: transaction.PhoneNumber != ""},
|
||||||
BankCode: transaction.BankCode,
|
BankCode: pgtype.Text{String: transaction.BankCode, Valid: transaction.BankCode != ""},
|
||||||
BeneficiaryName: transaction.BeneficiaryName,
|
BeneficiaryName: pgtype.Text{String: transaction.BeneficiaryName, Valid: transaction.BeneficiaryName != ""},
|
||||||
AccountName: transaction.AccountName,
|
AccountName: pgtype.Text{String: transaction.AccountName, Valid: transaction.AccountName != ""},
|
||||||
AccountNumber: transaction.AccountNumber,
|
AccountNumber: pgtype.Text{String: transaction.AccountNumber, Valid: transaction.AccountNumber != ""},
|
||||||
ReferenceNumber: transaction.ReferenceNumber,
|
ReferenceNumber: pgtype.Text{String: transaction.ReferenceNumber, Valid: transaction.ReferenceNumber != ""},
|
||||||
NumberOfOutcomes: transaction.NumberOfOutcomes,
|
NumberOfOutcomes: pgtype.Int8{Int64: transaction.NumberOfOutcomes, Valid: true},
|
||||||
BranchName: transaction.BranchName,
|
BranchName: pgtype.Text{String: transaction.BranchName, Valid: transaction.BranchName != ""},
|
||||||
BranchLocation: transaction.BranchLocation,
|
BranchLocation: pgtype.Text{String: transaction.BranchLocation, Valid: transaction.BranchLocation != ""},
|
||||||
CashierName: transaction.CashierName,
|
CashierName: pgtype.Text{String: transaction.CashierName, Valid: transaction.CashierName != ""},
|
||||||
CompanyID: transaction.CompanyID,
|
CompanyID: pgtype.Int8{Int64: transaction.CompanyID, Valid: true},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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 = 1 THEN amount ELSE 0 END), 0) as deposits,
|
||||||
|
COALESCE(SUM(CASE WHEN type = 0 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 = 1 THEN amount ELSE 0 END), 0) as deposits,
|
||||||
|
COALESCE(SUM(CASE WHEN type = 0 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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -464,3 +464,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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ func (s *Service) Login(ctx context.Context, email, phone string, password strin
|
||||||
// If old refresh token is not revoked, revoke it
|
// If old refresh token is not revoked, revoke it
|
||||||
if err == nil && !oldRefreshToken.Revoked {
|
if err == nil && !oldRefreshToken.Revoked {
|
||||||
err = s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token)
|
err = s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token)
|
||||||
if(err != nil) {
|
if err != nil {
|
||||||
return LoginSuccess{}, err
|
return LoginSuccess{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -33,9 +34,10 @@ type Service struct {
|
||||||
walletSvc wallet.Service
|
walletSvc wallet.Service
|
||||||
branchSvc branch.Service
|
branchSvc branch.Service
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
mongoLogger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.ServiceImpl, walletSvc wallet.Service, branchSvc branch.Service, logger *slog.Logger) *Service {
|
func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.ServiceImpl, walletSvc wallet.Service, branchSvc branch.Service, logger *slog.Logger, mongoLogger *zap.Logger) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
betStore: betStore,
|
betStore: betStore,
|
||||||
eventSvc: eventSvc,
|
eventSvc: eventSvc,
|
||||||
|
|
@ -43,6 +45,7 @@ func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.Serv
|
||||||
walletSvc: walletSvc,
|
walletSvc: walletSvc,
|
||||||
branchSvc: branchSvc,
|
branchSvc: branchSvc,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
mongoLogger: mongoLogger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,37 +61,56 @@ func (s *Service) GenerateCashoutID() (string, error) {
|
||||||
const length int = 13
|
const length int = 13
|
||||||
charLen := big.NewInt(int64(len(chars)))
|
charLen := big.NewInt(int64(len(chars)))
|
||||||
result := make([]byte, length)
|
result := make([]byte, length)
|
||||||
|
|
||||||
for i := 0; i < length; i++ {
|
for i := 0; i < length; i++ {
|
||||||
index, err := rand.Int(rand.Reader, charLen)
|
index, err := rand.Int(rand.Reader, charLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to generate random index for cashout ID",
|
||||||
|
zap.Int("position", i),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
result[i] = chars[index.Int64()]
|
result[i] = chars[index.Int64()]
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(result), nil
|
return string(result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketID int64, oddID int64) (domain.CreateBetOutcome, error) {
|
func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketID int64, oddID int64) (domain.CreateBetOutcome, error) {
|
||||||
// TODO: Change this when you refactor the database code
|
|
||||||
eventIDStr := strconv.FormatInt(eventID, 10)
|
eventIDStr := strconv.FormatInt(eventID, 10)
|
||||||
marketIDStr := strconv.FormatInt(marketID, 10)
|
marketIDStr := strconv.FormatInt(marketID, 10)
|
||||||
oddIDStr := strconv.FormatInt(oddID, 10)
|
oddIDStr := strconv.FormatInt(oddID, 10)
|
||||||
|
|
||||||
event, err := s.eventSvc.GetUpcomingEventByID(ctx, eventIDStr)
|
event, err := s.eventSvc.GetUpcomingEventByID(ctx, eventIDStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to fetch upcoming event by ID",
|
||||||
|
zap.Int64("event_id", eventID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.CreateBetOutcome{}, ErrEventHasBeenRemoved
|
return domain.CreateBetOutcome{}, ErrEventHasBeenRemoved
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
if event.StartTime.Before(currentTime) {
|
if event.StartTime.Before(currentTime) {
|
||||||
|
s.mongoLogger.Error("event has already started",
|
||||||
|
zap.Int64("event_id", eventID),
|
||||||
|
zap.Time("event_start_time", event.StartTime),
|
||||||
|
zap.Time("current_time", currentTime),
|
||||||
|
)
|
||||||
return domain.CreateBetOutcome{}, ErrEventHasNotEnded
|
return domain.CreateBetOutcome{}, ErrEventHasNotEnded
|
||||||
}
|
}
|
||||||
|
|
||||||
odds, err := s.prematchSvc.GetRawOddsByMarketID(ctx, marketIDStr, eventIDStr)
|
odds, err := s.prematchSvc.GetRawOddsByMarketID(ctx, marketIDStr, eventIDStr)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to get raw odds by market ID",
|
||||||
|
zap.Int64("event_id", eventID),
|
||||||
|
zap.Int64("market_id", marketID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.CreateBetOutcome{}, err
|
return domain.CreateBetOutcome{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
type rawOddType struct {
|
type rawOddType struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
|
|
@ -98,29 +120,51 @@ func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketI
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedOdd rawOddType
|
var selectedOdd rawOddType
|
||||||
var isOddFound bool = false
|
var isOddFound bool
|
||||||
|
|
||||||
for _, raw := range odds.RawOdds {
|
for _, raw := range odds.RawOdds {
|
||||||
var rawOdd rawOddType
|
var rawOdd rawOddType
|
||||||
rawBytes, err := json.Marshal(raw)
|
rawBytes, err := json.Marshal(raw)
|
||||||
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to marshal raw odd",
|
||||||
|
zap.Any("raw", raw),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
err = json.Unmarshal(rawBytes, &rawOdd)
|
err = json.Unmarshal(rawBytes, &rawOdd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to unmarshal raw odd %v", err)
|
s.mongoLogger.Error("failed to unmarshal raw odd",
|
||||||
|
zap.ByteString("raw_bytes", rawBytes),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if rawOdd.ID == oddIDStr {
|
if rawOdd.ID == oddIDStr {
|
||||||
selectedOdd = rawOdd
|
selectedOdd = rawOdd
|
||||||
isOddFound = true
|
isOddFound = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !isOddFound {
|
if !isOddFound {
|
||||||
|
s.mongoLogger.Error("odd ID not found in raw odds",
|
||||||
|
zap.Int64("odd_id", oddID),
|
||||||
|
zap.Int64("market_id", marketID),
|
||||||
|
zap.Int64("event_id", eventID),
|
||||||
|
)
|
||||||
return domain.CreateBetOutcome{}, ErrRawOddInvalid
|
return domain.CreateBetOutcome{}, ErrRawOddInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
|
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to parse selected odd value",
|
||||||
|
zap.String("odd", selectedOdd.Odds),
|
||||||
|
zap.Int64("odd_id", oddID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.CreateBetOutcome{}, err
|
return domain.CreateBetOutcome{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
newOutcome := domain.CreateBetOutcome{
|
newOutcome := domain.CreateBetOutcome{
|
||||||
EventID: eventID,
|
EventID: eventID,
|
||||||
OddID: oddID,
|
OddID: oddID,
|
||||||
|
|
@ -137,13 +181,14 @@ func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketI
|
||||||
}
|
}
|
||||||
|
|
||||||
return newOutcome, nil
|
return newOutcome, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID int64, role domain.Role) (domain.CreateBetRes, error) {
|
func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID int64, role domain.Role) (domain.CreateBetRes, error) {
|
||||||
// You can move the loop over req.Outcomes and all the business logic here.
|
|
||||||
|
|
||||||
if len(req.Outcomes) > 30 {
|
if len(req.Outcomes) > 30 {
|
||||||
|
s.mongoLogger.Error("too many outcomes",
|
||||||
|
zap.Int("count", len(req.Outcomes)),
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
)
|
||||||
return domain.CreateBetRes{}, ErrOutcomeLimit
|
return domain.CreateBetRes{}, ErrOutcomeLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,17 +198,25 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
|
||||||
for _, outcomeReq := range req.Outcomes {
|
for _, outcomeReq := range req.Outcomes {
|
||||||
newOutcome, err := s.GenerateBetOutcome(ctx, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID)
|
newOutcome, err := s.GenerateBetOutcome(ctx, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to generate outcome",
|
||||||
|
zap.Int64("event_id", outcomeReq.EventID),
|
||||||
|
zap.Int64("market_id", outcomeReq.MarketID),
|
||||||
|
zap.Int64("odd_id", outcomeReq.OddID),
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.CreateBetRes{}, err
|
return domain.CreateBetRes{}, err
|
||||||
}
|
}
|
||||||
totalOdds = totalOdds * float32(newOutcome.Odd)
|
totalOdds *= float32(newOutcome.Odd)
|
||||||
outcomes = append(outcomes, newOutcome)
|
outcomes = append(outcomes, newOutcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle role-specific logic and wallet deduction if needed.
|
|
||||||
var cashoutID string
|
|
||||||
cashoutID, err := s.GenerateCashoutID()
|
cashoutID, err := s.GenerateCashoutID()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to generate cashout ID",
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.CreateBetRes{}, err
|
return domain.CreateBetRes{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,106 +228,117 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
|
||||||
PhoneNumber: req.PhoneNumber,
|
PhoneNumber: req.PhoneNumber,
|
||||||
CashoutID: cashoutID,
|
CashoutID: cashoutID,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch role {
|
switch role {
|
||||||
case domain.RoleCashier:
|
case domain.RoleCashier:
|
||||||
branch, err := s.branchSvc.GetBranchByCashier(ctx, userID)
|
branch, err := s.branchSvc.GetBranchByCashier(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to get branch by cashier",
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.CreateBetRes{}, err
|
return domain.CreateBetRes{}, err
|
||||||
}
|
}
|
||||||
// Deduct from wallet:
|
|
||||||
// TODO: Make this percentage come from the company
|
deductedAmount := req.Amount / 10
|
||||||
var deductedAmount = req.Amount / 10
|
|
||||||
err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount))
|
err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to deduct from wallet",
|
||||||
|
zap.Int64("wallet_id", branch.WalletID),
|
||||||
|
zap.Float32("amount", deductedAmount),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.CreateBetRes{}, err
|
return domain.CreateBetRes{}, err
|
||||||
}
|
}
|
||||||
newBet.BranchID = domain.ValidInt64{
|
|
||||||
Value: branch.ID,
|
|
||||||
Valid: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
newBet.CompanyID = domain.ValidInt64{
|
newBet.BranchID = domain.ValidInt64{Value: branch.ID, Valid: true}
|
||||||
Value: branch.CompanyID,
|
newBet.CompanyID = domain.ValidInt64{Value: branch.CompanyID, Valid: true}
|
||||||
Valid: true,
|
newBet.UserID = domain.ValidInt64{Value: userID, Valid: true}
|
||||||
}
|
|
||||||
newBet.UserID = domain.ValidInt64{
|
|
||||||
Value: userID,
|
|
||||||
Valid: true,
|
|
||||||
}
|
|
||||||
newBet.IsShopBet = true
|
newBet.IsShopBet = true
|
||||||
// bet, err = s.betStore.CreateBet(ctx)
|
|
||||||
case domain.RoleBranchManager, domain.RoleAdmin, domain.RoleSuperAdmin:
|
case domain.RoleBranchManager, domain.RoleAdmin, domain.RoleSuperAdmin:
|
||||||
// TODO: restrict the Branch ID of Admin and Branch Manager to only the branches within their own company
|
|
||||||
// If a non cashier wants to create a bet, they will need to provide the Branch ID
|
|
||||||
if req.BranchID == nil {
|
if req.BranchID == nil {
|
||||||
|
s.mongoLogger.Error("branch ID required for admin/manager",
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
)
|
||||||
return domain.CreateBetRes{}, ErrBranchIDRequired
|
return domain.CreateBetRes{}, ErrBranchIDRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
branch, err := s.branchSvc.GetBranchByID(ctx, *req.BranchID)
|
branch, err := s.branchSvc.GetBranchByID(ctx, *req.BranchID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to get branch by ID",
|
||||||
|
zap.Int64("branch_id", *req.BranchID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.CreateBetRes{}, err
|
return domain.CreateBetRes{}, err
|
||||||
}
|
}
|
||||||
// Deduct from wallet:
|
|
||||||
// TODO: Make this percentage come from the company
|
deductedAmount := req.Amount / 10
|
||||||
var deductedAmount = req.Amount / 10
|
|
||||||
err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount))
|
err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("wallet deduction failed",
|
||||||
|
zap.Int64("wallet_id", branch.WalletID),
|
||||||
|
zap.Float32("amount", deductedAmount),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.CreateBetRes{}, err
|
return domain.CreateBetRes{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
newBet.BranchID = domain.ValidInt64{
|
newBet.BranchID = domain.ValidInt64{Value: branch.ID, Valid: true}
|
||||||
Value: branch.ID,
|
newBet.CompanyID = domain.ValidInt64{Value: branch.CompanyID, Valid: true}
|
||||||
Valid: true,
|
newBet.UserID = domain.ValidInt64{Value: userID, Valid: true}
|
||||||
}
|
|
||||||
newBet.CompanyID = domain.ValidInt64{
|
|
||||||
Value: branch.CompanyID,
|
|
||||||
Valid: true,
|
|
||||||
}
|
|
||||||
newBet.UserID = domain.ValidInt64{
|
|
||||||
Value: userID,
|
|
||||||
Valid: true,
|
|
||||||
}
|
|
||||||
newBet.IsShopBet = true
|
newBet.IsShopBet = true
|
||||||
|
|
||||||
case domain.RoleCustomer:
|
case domain.RoleCustomer:
|
||||||
// Get User Wallet
|
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID)
|
||||||
|
|
||||||
wallet, err := s.walletSvc.GetWalletsByUser(ctx, userID)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to get customer wallets",
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.CreateBetRes{}, err
|
return domain.CreateBetRes{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userWallet := wallet[0]
|
userWallet := wallets[0]
|
||||||
|
|
||||||
err = s.walletSvc.DeductFromWallet(ctx, userWallet.ID, domain.ToCurrency(req.Amount))
|
err = s.walletSvc.DeductFromWallet(ctx, userWallet.ID, domain.ToCurrency(req.Amount))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("wallet deduction failed for customer",
|
||||||
|
zap.Int64("wallet_id", userWallet.ID),
|
||||||
|
zap.Float32("amount", req.Amount),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.CreateBetRes{}, err
|
return domain.CreateBetRes{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
newBet.UserID = domain.ValidInt64{
|
newBet.UserID = domain.ValidInt64{Value: userID, Valid: true}
|
||||||
Value: userID,
|
|
||||||
Valid: true,
|
|
||||||
}
|
|
||||||
newBet.IsShopBet = false
|
newBet.IsShopBet = false
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
s.mongoLogger.Error("unknown role type",
|
||||||
|
zap.String("role", string(role)),
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
)
|
||||||
return domain.CreateBetRes{}, fmt.Errorf("Unknown Role Type")
|
return domain.CreateBetRes{}, fmt.Errorf("Unknown Role Type")
|
||||||
}
|
}
|
||||||
|
|
||||||
bet, err := s.CreateBet(ctx, newBet)
|
bet, err := s.CreateBet(ctx, newBet)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to create bet",
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.CreateBetRes{}, err
|
return domain.CreateBetRes{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Associate outcomes with the bet.
|
|
||||||
for i := range outcomes {
|
for i := range outcomes {
|
||||||
outcomes[i].BetID = bet.ID
|
outcomes[i].BetID = bet.ID
|
||||||
}
|
}
|
||||||
rows, err := s.betStore.CreateBetOutcome(ctx, outcomes)
|
rows, err := s.betStore.CreateBetOutcome(ctx, outcomes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to create bet outcomes",
|
||||||
|
zap.Int64("bet_id", bet.ID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.CreateBetRes{}, err
|
return domain.CreateBetRes{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,14 +353,24 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string,
|
||||||
var totalOdds float32 = 1
|
var totalOdds float32 = 1
|
||||||
|
|
||||||
markets, err := s.prematchSvc.GetPrematchOddsByUpcomingID(ctx, eventID)
|
markets, err := s.prematchSvc.GetPrematchOddsByUpcomingID(ctx, eventID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("failed to get odds for event", "event id", eventID, "error", err)
|
s.logger.Error("failed to get odds for event", "event id", eventID, "error", err)
|
||||||
|
s.mongoLogger.Error("failed to get odds for event",
|
||||||
|
zap.String("eventID", eventID),
|
||||||
|
zap.Int32("sportID", sportID),
|
||||||
|
zap.String("homeTeam", HomeTeam),
|
||||||
|
zap.String("awayTeam", AwayTeam),
|
||||||
|
zap.Error(err))
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(markets) == 0 {
|
if len(markets) == 0 {
|
||||||
s.logger.Error("empty odds for event", "event id", eventID)
|
s.logger.Error("empty odds for event", "event id", eventID)
|
||||||
|
s.mongoLogger.Warn("empty odds for event",
|
||||||
|
zap.String("eventID", eventID),
|
||||||
|
zap.Int32("sportID", sportID),
|
||||||
|
zap.String("homeTeam", HomeTeam),
|
||||||
|
zap.String("awayTeam", AwayTeam))
|
||||||
return nil, 0, fmt.Errorf("empty odds or event %v", eventID)
|
return nil, 0, fmt.Errorf("empty odds or event %v", eventID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -325,35 +399,55 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string,
|
||||||
err = json.Unmarshal(rawBytes, &selectedOdd)
|
err = json.Unmarshal(rawBytes, &selectedOdd)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to unmarshal raw odd %v", err)
|
s.logger.Error("Failed to unmarshal raw odd", "error", err)
|
||||||
|
s.mongoLogger.Warn("Failed to unmarshal raw odd",
|
||||||
|
zap.String("eventID", eventID),
|
||||||
|
zap.Int32("sportID", sportID),
|
||||||
|
zap.Error(err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
|
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to parse odd", "error", err)
|
s.logger.Error("Failed to parse odd", "error", err)
|
||||||
|
s.mongoLogger.Warn("Failed to parse odd",
|
||||||
|
zap.String("eventID", eventID),
|
||||||
|
zap.String("oddValue", selectedOdd.Odds),
|
||||||
|
zap.Error(err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
eventID, err := strconv.ParseInt(eventID, 10, 64)
|
|
||||||
|
eventIDInt, err := strconv.ParseInt(eventID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to get event id", "error", err)
|
s.logger.Error("Failed to parse eventID", "error", err)
|
||||||
|
s.mongoLogger.Warn("Failed to parse eventID",
|
||||||
|
zap.String("eventID", eventID),
|
||||||
|
zap.Error(err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
oddID, err := strconv.ParseInt(selectedOdd.ID, 10, 64)
|
oddID, err := strconv.ParseInt(selectedOdd.ID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to get odd id", "error", err)
|
s.logger.Error("Failed to parse oddID", "error", err)
|
||||||
|
s.mongoLogger.Warn("Failed to parse oddID",
|
||||||
|
zap.String("oddID", selectedOdd.ID),
|
||||||
|
zap.Error(err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
marketID, err := strconv.ParseInt(market.MarketID, 10, 64)
|
marketID, err := strconv.ParseInt(market.MarketID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to get odd id", "error", err)
|
s.logger.Error("Failed to parse marketID", "error", err)
|
||||||
|
s.mongoLogger.Warn("Failed to parse marketID",
|
||||||
|
zap.String("marketID", market.MarketID),
|
||||||
|
zap.Error(err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
marketName := market.MarketName
|
marketName := market.MarketName
|
||||||
|
|
||||||
newOdds = append(newOdds, domain.CreateBetOutcome{
|
newOdds = append(newOdds, domain.CreateBetOutcome{
|
||||||
EventID: eventID,
|
EventID: eventIDInt,
|
||||||
OddID: oddID,
|
OddID: oddID,
|
||||||
MarketID: marketID,
|
MarketID: marketID,
|
||||||
SportID: int64(sportID),
|
SportID: int64(sportID),
|
||||||
|
|
@ -367,15 +461,27 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string,
|
||||||
Expires: StartTime,
|
Expires: StartTime,
|
||||||
})
|
})
|
||||||
|
|
||||||
totalOdds = totalOdds * float32(parsedOdd)
|
totalOdds *= float32(parsedOdd)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(newOdds) == 0 {
|
if len(newOdds) == 0 {
|
||||||
s.logger.Error("Bet Outcomes is empty for market", "selectedMarket", selectedMarkets[0].MarketName)
|
s.logger.Error("Bet Outcomes is empty for market", "selectedMarkets", len(selectedMarkets))
|
||||||
|
s.mongoLogger.Error("Bet Outcomes is empty for market",
|
||||||
|
zap.String("eventID", eventID),
|
||||||
|
zap.Int32("sportID", sportID),
|
||||||
|
zap.String("homeTeam", HomeTeam),
|
||||||
|
zap.String("awayTeam", AwayTeam),
|
||||||
|
zap.Int("selectedMarkets", len(selectedMarkets)))
|
||||||
return nil, 0, ErrGenerateRandomOutcome
|
return nil, 0, ErrGenerateRandomOutcome
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ Final success log (optional)
|
||||||
|
s.mongoLogger.Info("Random bet outcomes generated successfully",
|
||||||
|
zap.String("eventID", eventID),
|
||||||
|
zap.Int32("sportID", sportID),
|
||||||
|
zap.Int("numOutcomes", len(newOdds)),
|
||||||
|
zap.Float32("totalOdds", totalOdds))
|
||||||
|
|
||||||
return newOdds, totalOdds, nil
|
return newOdds, totalOdds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -392,10 +498,17 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to get paginated upcoming events",
|
||||||
|
zap.Int64("userID", userID),
|
||||||
|
zap.Int64("branchID", branchID),
|
||||||
|
zap.Error(err))
|
||||||
return domain.CreateBetRes{}, err
|
return domain.CreateBetRes{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(events) == 0 {
|
if len(events) == 0 {
|
||||||
|
s.mongoLogger.Warn("no events available for random bet",
|
||||||
|
zap.Int64("userID", userID),
|
||||||
|
zap.Int64("branchID", branchID))
|
||||||
return domain.CreateBetRes{}, ErrNoEventsAvailable
|
return domain.CreateBetRes{}, ErrNoEventsAvailable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -422,6 +535,11 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("failed to generate random bet outcome", "event id", event.ID, "error", err)
|
s.logger.Error("failed to generate random bet outcome", "event id", event.ID, "error", err)
|
||||||
|
s.mongoLogger.Error("failed to generate random bet outcome",
|
||||||
|
zap.Int64("userID", userID),
|
||||||
|
zap.Int64("branchID", branchID),
|
||||||
|
zap.String("eventID", event.ID),
|
||||||
|
zap.String("error", fmt.Sprintf("%v", err)))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,6 +549,9 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le
|
||||||
}
|
}
|
||||||
if len(randomOdds) == 0 {
|
if len(randomOdds) == 0 {
|
||||||
s.logger.Error("Failed to generate random any outcomes for all events")
|
s.logger.Error("Failed to generate random any outcomes for all events")
|
||||||
|
s.mongoLogger.Error("Failed to generate random any outcomes for all events",
|
||||||
|
zap.Int64("userID", userID),
|
||||||
|
zap.Int64("branchID", branchID))
|
||||||
return domain.CreateBetRes{}, ErrGenerateRandomOutcome
|
return domain.CreateBetRes{}, ErrGenerateRandomOutcome
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -440,6 +561,9 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le
|
||||||
|
|
||||||
cashoutID, err = s.GenerateCashoutID()
|
cashoutID, err = s.GenerateCashoutID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("Failed to generate cash out ID",
|
||||||
|
zap.Int64("userID", userID),
|
||||||
|
zap.Int64("branchID", branchID))
|
||||||
return domain.CreateBetRes{}, err
|
return domain.CreateBetRes{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -457,6 +581,10 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le
|
||||||
|
|
||||||
bet, err := s.CreateBet(ctx, newBet)
|
bet, err := s.CreateBet(ctx, newBet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("Failed to create a new random bet",
|
||||||
|
zap.Int64("userID", userID),
|
||||||
|
zap.Int64("branchID", branchID),
|
||||||
|
zap.String("bet", fmt.Sprintf("%+v", newBet)))
|
||||||
return domain.CreateBetRes{}, err
|
return domain.CreateBetRes{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -466,11 +594,19 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le
|
||||||
|
|
||||||
rows, err := s.betStore.CreateBetOutcome(ctx, randomOdds)
|
rows, err := s.betStore.CreateBetOutcome(ctx, randomOdds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("Failed to create a new random bet outcome",
|
||||||
|
zap.Int64("userID", userID),
|
||||||
|
zap.Int64("branchID", branchID),
|
||||||
|
zap.String("randomOdds", fmt.Sprintf("%+v", randomOdds)))
|
||||||
return domain.CreateBetRes{}, err
|
return domain.CreateBetRes{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res := domain.ConvertCreateBet(bet, rows)
|
res := domain.ConvertCreateBet(bet, rows)
|
||||||
|
|
||||||
|
s.mongoLogger.Info("Random bets placed successfully",
|
||||||
|
zap.Int64("userID", userID),
|
||||||
|
zap.Int64("branchID", branchID),
|
||||||
|
zap.String("response", fmt.Sprintf("%+v", res)))
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -505,10 +641,12 @@ func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error {
|
func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error {
|
||||||
|
|
||||||
bet, err := s.GetBetByID(ctx, id)
|
bet, err := s.GetBetByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to update bet status. Invalid bet id")
|
s.mongoLogger.Error("failed to update bet status: invalid bet ID",
|
||||||
|
zap.Int64("bet_id", id),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -521,22 +659,30 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
|
||||||
|
|
||||||
customerWallet, err := s.walletSvc.GetCustomerWallet(ctx, id)
|
customerWallet, err := s.walletSvc.GetCustomerWallet(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to update bet status. Invalid customer wallet id")
|
s.mongoLogger.Error("failed to get customer wallet",
|
||||||
|
zap.Int64("bet_id", id),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var amount domain.Currency
|
var amount domain.Currency
|
||||||
if status == domain.OUTCOME_STATUS_WIN {
|
switch status {
|
||||||
|
case domain.OUTCOME_STATUS_WIN:
|
||||||
amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds)
|
amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds)
|
||||||
} else if status == domain.OUTCOME_STATUS_HALF {
|
case domain.OUTCOME_STATUS_HALF:
|
||||||
amount = (domain.CalculateWinnings(bet.Amount, bet.TotalOdds)) / 2
|
amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds) / 2
|
||||||
} else {
|
default:
|
||||||
amount = bet.Amount
|
amount = bet.Amount
|
||||||
}
|
}
|
||||||
err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount)
|
|
||||||
|
|
||||||
|
err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to update bet status. Failed to update user wallet")
|
s.mongoLogger.Error("failed to add winnings to wallet",
|
||||||
|
zap.Int64("wallet_id", customerWallet.RegularID),
|
||||||
|
zap.Float32("amount", float32(amount)),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -546,92 +692,89 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
|
||||||
func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) {
|
func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) {
|
||||||
betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID)
|
betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to get bet outcomes",
|
||||||
|
zap.Int64("bet_id", betID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.OUTCOME_STATUS_PENDING, err
|
return domain.OUTCOME_STATUS_PENDING, err
|
||||||
}
|
}
|
||||||
|
|
||||||
status := domain.OUTCOME_STATUS_PENDING
|
status := domain.OUTCOME_STATUS_PENDING
|
||||||
|
|
||||||
for _, betOutcome := range betOutcomes {
|
for _, betOutcome := range betOutcomes {
|
||||||
// If any of the bet outcomes are pending return
|
|
||||||
if betOutcome.Status == domain.OUTCOME_STATUS_PENDING {
|
if betOutcome.Status == domain.OUTCOME_STATUS_PENDING {
|
||||||
|
s.mongoLogger.Info("outcome still pending",
|
||||||
|
zap.Int64("bet_id", betID),
|
||||||
|
)
|
||||||
return domain.OUTCOME_STATUS_PENDING, ErrOutcomesNotCompleted
|
return domain.OUTCOME_STATUS_PENDING, ErrOutcomesNotCompleted
|
||||||
}
|
}
|
||||||
|
|
||||||
if betOutcome.Status == domain.OUTCOME_STATUS_ERROR {
|
if betOutcome.Status == domain.OUTCOME_STATUS_ERROR {
|
||||||
|
s.mongoLogger.Info("outcome contains error",
|
||||||
|
zap.Int64("bet_id", betID),
|
||||||
|
)
|
||||||
return domain.OUTCOME_STATUS_ERROR, nil
|
return domain.OUTCOME_STATUS_ERROR, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// The bet status can only be updated if its not lost or error
|
|
||||||
// If all the bet outcomes are a win, then set the bet status to win
|
|
||||||
// If even one of the bet outcomes is a loss then set the bet status to loss
|
|
||||||
// If even one of the bet outcomes is an error, then set the bet status to error
|
|
||||||
switch status {
|
switch status {
|
||||||
case domain.OUTCOME_STATUS_PENDING:
|
case domain.OUTCOME_STATUS_PENDING:
|
||||||
status = betOutcome.Status
|
status = betOutcome.Status
|
||||||
case domain.OUTCOME_STATUS_WIN:
|
case domain.OUTCOME_STATUS_WIN:
|
||||||
if betOutcome.Status == domain.OUTCOME_STATUS_LOSS {
|
switch betOutcome.Status {
|
||||||
|
case domain.OUTCOME_STATUS_LOSS:
|
||||||
status = domain.OUTCOME_STATUS_LOSS
|
status = domain.OUTCOME_STATUS_LOSS
|
||||||
} else if betOutcome.Status == domain.OUTCOME_STATUS_HALF {
|
case domain.OUTCOME_STATUS_HALF:
|
||||||
status = domain.OUTCOME_STATUS_HALF
|
status = domain.OUTCOME_STATUS_HALF
|
||||||
} else if betOutcome.Status == domain.OUTCOME_STATUS_WIN {
|
case domain.OUTCOME_STATUS_VOID:
|
||||||
status = domain.OUTCOME_STATUS_WIN
|
|
||||||
} else if betOutcome.Status == domain.OUTCOME_STATUS_VOID {
|
|
||||||
status = domain.OUTCOME_STATUS_VOID
|
status = domain.OUTCOME_STATUS_VOID
|
||||||
} else {
|
case domain.OUTCOME_STATUS_WIN:
|
||||||
|
// remain win
|
||||||
|
default:
|
||||||
status = domain.OUTCOME_STATUS_ERROR
|
status = domain.OUTCOME_STATUS_ERROR
|
||||||
}
|
}
|
||||||
case domain.OUTCOME_STATUS_LOSS:
|
case domain.OUTCOME_STATUS_LOSS:
|
||||||
if betOutcome.Status == domain.OUTCOME_STATUS_LOSS {
|
// stay as LOSS regardless of others
|
||||||
status = domain.OUTCOME_STATUS_LOSS
|
|
||||||
} else if betOutcome.Status == domain.OUTCOME_STATUS_HALF {
|
|
||||||
status = domain.OUTCOME_STATUS_LOSS
|
|
||||||
} else if betOutcome.Status == domain.OUTCOME_STATUS_WIN {
|
|
||||||
status = domain.OUTCOME_STATUS_LOSS
|
|
||||||
} else if betOutcome.Status == domain.OUTCOME_STATUS_VOID {
|
|
||||||
status = domain.OUTCOME_STATUS_LOSS
|
|
||||||
} else {
|
|
||||||
status = domain.OUTCOME_STATUS_ERROR
|
|
||||||
}
|
|
||||||
case domain.OUTCOME_STATUS_VOID:
|
case domain.OUTCOME_STATUS_VOID:
|
||||||
if betOutcome.Status == domain.OUTCOME_STATUS_VOID ||
|
switch betOutcome.Status {
|
||||||
betOutcome.Status == domain.OUTCOME_STATUS_WIN ||
|
case domain.OUTCOME_STATUS_LOSS:
|
||||||
betOutcome.Status == domain.OUTCOME_STATUS_HALF {
|
|
||||||
status = domain.OUTCOME_STATUS_VOID
|
|
||||||
} else if betOutcome.Status == domain.OUTCOME_STATUS_LOSS {
|
|
||||||
status = domain.OUTCOME_STATUS_LOSS
|
status = domain.OUTCOME_STATUS_LOSS
|
||||||
|
case domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF, domain.OUTCOME_STATUS_VOID:
|
||||||
} else {
|
// remain VOID
|
||||||
|
default:
|
||||||
status = domain.OUTCOME_STATUS_ERROR
|
status = domain.OUTCOME_STATUS_ERROR
|
||||||
}
|
}
|
||||||
case domain.OUTCOME_STATUS_HALF:
|
case domain.OUTCOME_STATUS_HALF:
|
||||||
if betOutcome.Status == domain.OUTCOME_STATUS_HALF ||
|
switch betOutcome.Status {
|
||||||
betOutcome.Status == domain.OUTCOME_STATUS_WIN {
|
case domain.OUTCOME_STATUS_LOSS:
|
||||||
status = domain.OUTCOME_STATUS_HALF
|
|
||||||
} else if betOutcome.Status == domain.OUTCOME_STATUS_LOSS {
|
|
||||||
status = domain.OUTCOME_STATUS_LOSS
|
status = domain.OUTCOME_STATUS_LOSS
|
||||||
} else if betOutcome.Status == domain.OUTCOME_STATUS_VOID {
|
case domain.OUTCOME_STATUS_VOID:
|
||||||
status = domain.OUTCOME_STATUS_VOID
|
status = domain.OUTCOME_STATUS_VOID
|
||||||
} else {
|
case domain.OUTCOME_STATUS_HALF, domain.OUTCOME_STATUS_WIN:
|
||||||
|
// remain HALF
|
||||||
|
default:
|
||||||
status = domain.OUTCOME_STATUS_ERROR
|
status = domain.OUTCOME_STATUS_ERROR
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// If the status is not pending, win, loss or error, then set the status to error
|
|
||||||
status = domain.OUTCOME_STATUS_ERROR
|
status = domain.OUTCOME_STATUS_ERROR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if status == domain.OUTCOME_STATUS_PENDING || status == domain.OUTCOME_STATUS_ERROR {
|
if status == domain.OUTCOME_STATUS_PENDING || status == domain.OUTCOME_STATUS_ERROR {
|
||||||
// If the status is pending or error, then we don't need to update the bet
|
s.mongoLogger.Info("bet status not updated due to status",
|
||||||
s.logger.Info("bet not updated", "bet id", betID, "status", status)
|
zap.Int64("bet_id", betID),
|
||||||
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("Error when processing bet outcomes")
|
zap.String("final_status", string(status)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return status, nil
|
return status, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) {
|
func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) {
|
||||||
betOutcome, err := s.betStore.UpdateBetOutcomeStatus(ctx, id, status)
|
betOutcome, err := s.betStore.UpdateBetOutcomeStatus(ctx, id, status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to update bet outcome status",
|
||||||
|
zap.Int64("betID", id),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return domain.BetOutcome{}, err
|
return domain.BetOutcome{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,32 @@ package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
|
||||||
|
afro "github.com/amanuelabay/afrosms-go"
|
||||||
|
"github.com/resend/resend-go/v2"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium) error {
|
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium) error {
|
||||||
otpCode := "123456" // Generate OTP code
|
otpCode := helpers.GenerateOTP()
|
||||||
|
|
||||||
|
message := fmt.Sprintf("Welcome to Fortune bets, your OTP is %s please don't share with anyone.", otpCode)
|
||||||
|
|
||||||
|
switch medium {
|
||||||
|
case domain.OtpMediumSms:
|
||||||
|
if err := s.SendSMSOTP(ctx, sentTo, message); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case domain.OtpMediumEmail:
|
||||||
|
if err := s.SendEmailOTP(ctx, sentTo, message); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
otp := domain.Otp{
|
otp := domain.Otp{
|
||||||
SentTo: sentTo,
|
SentTo: sentTo,
|
||||||
|
|
@ -21,19 +39,9 @@ func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpF
|
||||||
ExpiresAt: time.Now().Add(OtpExpiry),
|
ExpiresAt: time.Now().Add(OtpExpiry),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.otpStore.CreateOtp(ctx, otp)
|
return s.otpStore.CreateOtp(ctx, otp)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch medium {
|
|
||||||
case domain.OtpMediumSms:
|
|
||||||
return s.smsGateway.SendSMSOTP(ctx, sentTo, otpCode)
|
|
||||||
case domain.OtpMediumEmail:
|
|
||||||
return s.emailGateway.SendEmailOTP(ctx, sentTo, otpCode)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func hashPassword(plaintextPassword string) ([]byte, error) {
|
func hashPassword(plaintextPassword string) ([]byte, error) {
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
|
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -42,3 +50,50 @@ func hashPassword(plaintextPassword string) ([]byte, error) {
|
||||||
|
|
||||||
return hash, nil
|
return hash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendSMSOTP(ctx context.Context, receiverPhone, message string) error {
|
||||||
|
apiKey := s.config.AFRO_SMS_API_KEY
|
||||||
|
senderName := s.config.AFRO_SMS_SENDER_NAME
|
||||||
|
hostURL := s.config.ADRO_SMS_HOST_URL
|
||||||
|
endpoint := "/api/send"
|
||||||
|
|
||||||
|
// API endpoint has been updated
|
||||||
|
// TODO: no need for package for the afro message operations (pretty simple stuff)
|
||||||
|
request := afro.GetRequest(apiKey, endpoint, hostURL)
|
||||||
|
request.BaseURL = "https://api.afromessage.com/api/send"
|
||||||
|
|
||||||
|
request.Method = "GET"
|
||||||
|
request.Sender(senderName)
|
||||||
|
request.To(receiverPhone, message)
|
||||||
|
|
||||||
|
response, err := afro.MakeRequestWithContext(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response["acknowledge"] == "success" {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
fmt.Println(response["response"].(map[string]interface{}))
|
||||||
|
return errors.New("SMS delivery failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendEmailOTP(ctx context.Context, receiverEmail, message string) error {
|
||||||
|
apiKey := s.config.ResendApiKey
|
||||||
|
client := resend.NewClient(apiKey)
|
||||||
|
formattedSenderEmail := "FortuneBets <" + s.config.ResendSenderEmail + ">"
|
||||||
|
params := &resend.SendEmailRequest{
|
||||||
|
From: formattedSenderEmail,
|
||||||
|
To: []string{receiverEmail},
|
||||||
|
Subject: "FortuneBets - One Time Password",
|
||||||
|
Text: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.Emails.Send(params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -11,19 +13,17 @@ const (
|
||||||
type Service struct {
|
type Service struct {
|
||||||
userStore UserStore
|
userStore UserStore
|
||||||
otpStore OtpStore
|
otpStore OtpStore
|
||||||
smsGateway SmsGateway
|
config *config.Config
|
||||||
emailGateway EmailGateway
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
userStore UserStore,
|
userStore UserStore,
|
||||||
otpStore OtpStore, smsGateway SmsGateway,
|
otpStore OtpStore,
|
||||||
emailGateway EmailGateway,
|
cfg *config.Config,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
userStore: userStore,
|
userStore: userStore,
|
||||||
otpStore: otpStore,
|
otpStore: otpStore,
|
||||||
smsGateway: smsGateway,
|
config: cfg,
|
||||||
emailGateway: emailGateway,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,6 +119,13 @@ func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransf
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to receiver
|
// Add to receiver
|
||||||
|
senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID.Value)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Transfer{}, err
|
||||||
|
} else if senderWallet.Balance < transfer.Amount {
|
||||||
|
return domain.Transfer{}, ErrInsufficientBalance
|
||||||
|
}
|
||||||
|
|
||||||
err = s.walletStore.UpdateBalance(ctx, receiverWallet.ID, receiverWallet.Balance+transfer.Amount)
|
err = s.walletStore.UpdateBalance(ctx, receiverWallet.ID, receiverWallet.Balance+transfer.Amount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Transfer{}, err
|
return domain.Transfer{}, err
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,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"
|
||||||
|
|
@ -46,6 +47,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
|
||||||
|
|
@ -69,6 +71,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,
|
||||||
|
|
@ -110,6 +113,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,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"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/result"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
|
||||||
|
"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"
|
||||||
|
|
@ -32,6 +33,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
|
||||||
|
|
@ -57,6 +59,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,
|
||||||
|
|
@ -81,6 +84,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,
|
||||||
|
|
|
||||||
38
internal/web_server/handlers/mongoLogger.go
Normal file
38
internal/web_server/handlers/mongoLogger.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 []domain.LogEntry
|
||||||
|
if err := cursor.All(appCtx, &logs); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Cursor decoding error: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return c.JSON(logs)
|
||||||
|
}
|
||||||
|
}
|
||||||
123
internal/web_server/handlers/report.go
Normal file
123
internal/web_server/handlers/report.go
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
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(domain.Response{
|
||||||
|
Message: "Dashboard reports generated successfully",
|
||||||
|
Success: true,
|
||||||
|
StatusCode: 200,
|
||||||
|
Data: summary,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -243,6 +244,7 @@ func (h *Handler) SendResetCode(c *fiber.Ctx) error {
|
||||||
|
|
||||||
if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo); err != nil {
|
if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo); err != nil {
|
||||||
h.logger.Error("Failed to send reset code", "error", err)
|
h.logger.Error("Failed to send reset code", "error", err)
|
||||||
|
fmt.Println(err)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to send reset code")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to send reset code")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,8 +252,8 @@ func (h *Handler) SendResetCode(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResetPasswordReq struct {
|
type ResetPasswordReq struct {
|
||||||
Email string `json:"email" validate:"email" example:"john.doe@example.com"`
|
Email string `json:"email,omitempty" validate:"required_without=PhoneNumber,omitempty,email" example:"john.doe@example.com"`
|
||||||
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
|
PhoneNumber string `json:"phone_number,omitempty" validate:"required_without=Email,omitempty" example:"1234567890"`
|
||||||
Password string `json:"password" validate:"required,min=8" example:"newpassword123"`
|
Password string `json:"password" validate:"required,min=8" example:"newpassword123"`
|
||||||
Otp string `json:"otp" validate:"required" example:"123456"`
|
Otp string `json:"otp" validate:"required" example:"123456"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -197,6 +202,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)
|
||||||
|
|
@ -212,6 +233,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", handlers.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