Merge branch 'main' into ticket-bet

This commit is contained in:
Samuel Tariku 2025-06-07 08:15:17 +03:00
commit 73c1db14c1
46 changed files with 2854 additions and 1332 deletions

View File

@ -20,8 +20,10 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"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"
@ -85,6 +87,7 @@ func main() {
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)
betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger) betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger)
resultSvc := result.NewService(store, cfg, logger, *betSvc, oddsSvc, eventSvc) resultSvc := result.NewService(store, cfg, logger, *betSvc, oddsSvc, eventSvc)
notificationRepo := repository.NewNotificationRepository(store) notificationRepo := repository.NewNotificationRepository(store)
@ -108,6 +111,17 @@ func main() {
logger, logger,
) )
recommendationSvc := recommendation.NewService(recommendationRepo) recommendationSvc := recommendation.NewService(recommendationRepo)
chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY)
chapaSvc := chapa.NewService(
transaction.TransactionStore(store),
wallet.WalletStore(store),
user.UserStore(store),
referalSvc,
branch.BranchStore(store),
chapaClient,
store,
)
httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc) httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc)
httpserver.StartTicketCrons(*ticketSvc) httpserver.StartTicketCrons(*ticketSvc)
@ -116,7 +130,7 @@ func main() {
JwtAccessKey: cfg.JwtKey, JwtAccessKey: cfg.JwtKey,
JwtAccessExpiry: cfg.AccessExpiry, JwtAccessExpiry: cfg.AccessExpiry,
}, userSvc, }, userSvc,
ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, aleaService, veliService, recommendationSvc, resultSvc, cfg) ticketSvc, betSvc, 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 {

View File

@ -76,3 +76,4 @@ DROP TABLE IF EXISTS refresh_tokens;
DROP TABLE IF EXISTS otps; DROP TABLE IF EXISTS otps;
DROP TABLE IF EXISTS odds; DROP TABLE IF EXISTS odds;
DROP TABLE IF EXISTS events; DROP TABLE IF EXISTS events;
DROP TABLE IF EXISTS leagues;

View File

@ -184,15 +184,15 @@ CREATE TABLE IF NOT EXISTS branch_cashiers (
); );
CREATE TABLE events ( CREATE TABLE events (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
sport_id TEXT, sport_id INT,
match_name TEXT, match_name TEXT,
home_team TEXT, home_team TEXT,
away_team TEXT, away_team TEXT,
home_team_id TEXT, home_team_id INT,
away_team_id TEXT, away_team_id INT,
home_kit_image TEXT, home_kit_image TEXT,
away_kit_image TEXT, away_kit_image TEXT,
league_id TEXT, league_id INT,
league_name TEXT, league_name TEXT,
league_cc TEXT, league_cc TEXT,
start_time TIMESTAMP, start_time TIMESTAMP,
@ -233,6 +233,20 @@ CREATE TABLE companies (
admin_id BIGINT NOT NULL, admin_id BIGINT NOT NULL,
wallet_id BIGINT NOT NULL wallet_id BIGINT NOT NULL
); );
CREATE TABLE leagues (
id BIGINT PRIMARY KEY,
name TEXT NOT NULL,
country_code TEXT,
bet365_id INT,
is_active BOOLEAN DEFAULT true
);
CREATE TABLE teams (
id TEXT PRIMARY KEY,
team_name TEXT NOT NULL,
country TEXT,
bet365_id INT,
logo_url TEXT
);
-- Views -- Views
CREATE VIEW companies_details AS CREATE VIEW companies_details AS
SELECT companies.*, SELECT companies.*,
@ -297,6 +311,7 @@ ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERE
ALTER TABLE branch_cashiers ALTER TABLE branch_cashiers
ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE; ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE;
ALTER TABLE companies ALTER TABLE companies
ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id), ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id),
ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE; ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE;

48
db/query/leagues.sql Normal file
View File

@ -0,0 +1,48 @@
-- name: InsertLeague :exec
INSERT INTO leagues (
id,
name,
country_code,
bet365_id,
is_active
) VALUES (
$1, $2, $3, $4, $5
)
ON CONFLICT (id) DO UPDATE
SET name = EXCLUDED.name,
country_code = EXCLUDED.country_code,
bet365_id = EXCLUDED.bet365_id,
is_active = EXCLUDED.is_active;
-- name: GetSupportedLeagues :many
SELECT id,
name,
country_code,
bet365_id,
is_active
FROM leagues
WHERE is_active = true;
-- name: GetAllLeagues :many
SELECT id,
name,
country_code,
bet365_id,
is_active
FROM leagues;
-- name: CheckLeagueSupport :one
SELECT EXISTS(
SELECT 1
FROM leagues
WHERE id = $1
AND is_active = true
);
-- name: UpdateLeague :exec
UPDATE leagues
SET name = $1,
country_code = $2,
bet365_id = $3,
is_active = $4
WHERE id = $5;
-- name: SetLeagueActive :exec
UPDATE leagues
SET is_active = true
WHERE id = $1;

View File

@ -306,7 +306,6 @@ const docTemplate = `{
}, },
"/api/v1/chapa/banks": { "/api/v1/chapa/banks": {
"get": { "get": {
"description": "Fetch all supported banks from Chapa",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -316,20 +315,50 @@ const docTemplate = `{
"tags": [ "tags": [
"Chapa" "Chapa"
], ],
"summary": "Get list of banks", "summary": "fetches chapa supported banks",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/domain.ChapaSupportedBanksResponse" "$ref": "#/definitions/domain.ChapaSupportedBanksResponseWrapper"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.Response"
} }
} }
} }
} }
}, },
"/api/v1/chapa/payments/callback": { "/api/v1/chapa/payments/deposit": {
"post": { "post": {
"description": "Endpoint to receive webhook payloads from Chapa", "description": "Deposits money into user wallet from user account using Chapa",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -339,155 +368,144 @@ const docTemplate = `{
"tags": [ "tags": [
"Chapa" "Chapa"
], ],
"summary": "Receive Chapa webhook", "summary": "Deposit money into user wallet using Chapa",
"parameters": [ "parameters": [
{ {
"description": "Webhook Payload (dynamic)", "description": "Deposit request payload",
"name": "payload", "name": "payload",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"type": "object" "$ref": "#/definitions/domain.ChapaDepositRequest"
} }
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "ok", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/domain.ChapaPaymentUrlResponseWrapper"
}
},
"400": {
"description": "Invalid request",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Validation error",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/domain.Response"
}
}
}
}
},
"/api/v1/chapa/payments/verify": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Verifies Chapa webhook transaction",
"parameters": [
{
"description": "Webhook Payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaTransactionType"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.Response"
}
}
}
}
},
"/api/v1/chapa/payments/withdraw": {
"post": {
"description": "Initiates a withdrawal transaction using Chapa for the authenticated user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Withdraw using Chapa",
"parameters": [
{
"description": "Chapa Withdraw Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaWithdrawRequest"
}
}
],
"responses": {
"200": {
"description": "Withdrawal requested successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"type": "string" "type": "string"
} }
} }
} }
]
} }
}, },
"/api/v1/chapa/payments/initialize": { "400": {
"post": { "description": "Invalid request",
"description": "Initiate a payment through Chapa",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Initialize a payment transaction",
"parameters": [
{
"description": "Payment initialization request",
"name": "payload",
"in": "body",
"required": true,
"schema": { "schema": {
"$ref": "#/definitions/domain.InitPaymentRequest" "$ref": "#/definitions/domain.Response"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.InitPaymentResponse"
}
}
}
} }
}, },
"/api/v1/chapa/payments/verify/{tx_ref}": { "401": {
"get": { "description": "Unauthorized",
"description": "Verify the transaction status from Chapa using tx_ref",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Verify a payment transaction",
"parameters": [
{
"type": "string",
"description": "Transaction Reference",
"name": "tx_ref",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/domain.VerifyTransactionResponse" "$ref": "#/definitions/domain.Response"
}
}
}
} }
}, },
"/api/v1/chapa/transfers": { "422": {
"post": { "description": "Unprocessable Entity",
"description": "Initiate a transfer request via Chapa",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Create a money transfer",
"parameters": [
{
"description": "Transfer request body",
"name": "payload",
"in": "body",
"required": true,
"schema": { "schema": {
"$ref": "#/definitions/domain.TransferRequest" "$ref": "#/definitions/domain.Response"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.CreateTransferResponse"
}
}
}
} }
}, },
"/api/v1/chapa/transfers/verify/{transfer_ref}": { "500": {
"get": { "description": "Internal Server Error",
"description": "Check the status of a money transfer via reference",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Verify a transfer",
"parameters": [
{
"type": "string",
"description": "Transfer Reference",
"name": "transfer_ref",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/domain.VerifyTransferResponse" "$ref": "#/definitions/domain.Response"
} }
} }
} }
@ -4460,6 +4478,46 @@ const docTemplate = `{
} }
} }
}, },
"domain.ChapaDepositRequest": {
"type": "object",
"properties": {
"amount": {
"type": "integer"
},
"branch_id": {
"type": "integer"
},
"currency": {
"type": "string"
},
"phone_number": {
"type": "string"
}
}
},
"domain.ChapaPaymentUrlResponse": {
"type": "object",
"properties": {
"payment_url": {
"type": "string"
}
}
},
"domain.ChapaPaymentUrlResponseWrapper": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.ChapaSupportedBank": { "domain.ChapaSupportedBank": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4513,17 +4571,56 @@ const docTemplate = `{
} }
} }
}, },
"domain.ChapaSupportedBanksResponse": { "domain.ChapaSupportedBanksResponseWrapper": {
"type": "object", "type": "object",
"properties": { "properties": {
"data": { "data": {},
"type": "array",
"items": {
"$ref": "#/definitions/domain.ChapaSupportedBank"
}
},
"message": { "message": {
"type": "string" "type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.ChapaTransactionType": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
}
},
"domain.ChapaWithdrawRequest": {
"type": "object",
"properties": {
"account_name": {
"type": "string"
},
"account_number": {
"type": "string"
},
"amount": {
"type": "integer"
},
"bank_code": {
"type": "string"
},
"beneficiary_name": {
"type": "string"
},
"branch_id": {
"type": "integer"
},
"currency": {
"type": "string"
},
"wallet_id": {
"description": "add this",
"type": "integer"
} }
} }
}, },
@ -4579,76 +4676,6 @@ const docTemplate = `{
} }
} }
}, },
"domain.CreateTransferResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.TransferData"
},
"message": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.InitPaymentData": {
"type": "object",
"properties": {
"checkout_url": {
"type": "string"
},
"tx_ref": {
"type": "string"
}
}
},
"domain.InitPaymentRequest": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"callback_url": {
"type": "string"
},
"currency": {
"type": "string"
},
"email": {
"type": "string"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"return_url": {
"type": "string"
},
"tx_ref": {
"type": "string"
}
}
},
"domain.InitPaymentResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.InitPaymentData"
},
"message": {
"description": "e.g., \"Payment initialized\"",
"type": "string"
},
"status": {
"description": "\"success\"",
"type": "string"
}
}
},
"domain.Odd": { "domain.Odd": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4879,6 +4906,21 @@ const docTemplate = `{
} }
} }
}, },
"domain.Response": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.Role": { "domain.Role": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -4961,86 +5003,6 @@ const docTemplate = `{
} }
} }
}, },
"domain.TransactionData": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency": {
"type": "string"
},
"email": {
"type": "string"
},
"status": {
"type": "string"
},
"tx_ref": {
"type": "string"
}
}
},
"domain.TransferData": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency": {
"type": "string"
},
"reference": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.TransferRequest": {
"type": "object",
"properties": {
"account_number": {
"type": "string"
},
"amount": {
"type": "string"
},
"bank_code": {
"type": "string"
},
"currency": {
"type": "string"
},
"reason": {
"type": "string"
},
"recipient_name": {
"type": "string"
},
"reference": {
"type": "string"
}
}
},
"domain.TransferVerificationData": {
"type": "object",
"properties": {
"account_name": {
"type": "string"
},
"bank_code": {
"type": "string"
},
"reference": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.UpcomingEvent": { "domain.UpcomingEvent": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5147,34 +5109,6 @@ const docTemplate = `{
} }
} }
}, },
"domain.VerifyTransactionResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.TransactionData"
},
"message": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.VerifyTransferResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.TransferVerificationData"
},
"message": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.VirtualGame": { "domain.VirtualGame": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -298,7 +298,6 @@
}, },
"/api/v1/chapa/banks": { "/api/v1/chapa/banks": {
"get": { "get": {
"description": "Fetch all supported banks from Chapa",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -308,20 +307,50 @@
"tags": [ "tags": [
"Chapa" "Chapa"
], ],
"summary": "Get list of banks", "summary": "fetches chapa supported banks",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/domain.ChapaSupportedBanksResponse" "$ref": "#/definitions/domain.ChapaSupportedBanksResponseWrapper"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.Response"
} }
} }
} }
} }
}, },
"/api/v1/chapa/payments/callback": { "/api/v1/chapa/payments/deposit": {
"post": { "post": {
"description": "Endpoint to receive webhook payloads from Chapa", "description": "Deposits money into user wallet from user account using Chapa",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -331,155 +360,144 @@
"tags": [ "tags": [
"Chapa" "Chapa"
], ],
"summary": "Receive Chapa webhook", "summary": "Deposit money into user wallet using Chapa",
"parameters": [ "parameters": [
{ {
"description": "Webhook Payload (dynamic)", "description": "Deposit request payload",
"name": "payload", "name": "payload",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"type": "object" "$ref": "#/definitions/domain.ChapaDepositRequest"
} }
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "ok", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/domain.ChapaPaymentUrlResponseWrapper"
}
},
"400": {
"description": "Invalid request",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Validation error",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/domain.Response"
}
}
}
}
},
"/api/v1/chapa/payments/verify": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Verifies Chapa webhook transaction",
"parameters": [
{
"description": "Webhook Payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaTransactionType"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.Response"
}
}
}
}
},
"/api/v1/chapa/payments/withdraw": {
"post": {
"description": "Initiates a withdrawal transaction using Chapa for the authenticated user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Withdraw using Chapa",
"parameters": [
{
"description": "Chapa Withdraw Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ChapaWithdrawRequest"
}
}
],
"responses": {
"200": {
"description": "Withdrawal requested successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"type": "string" "type": "string"
} }
} }
} }
]
} }
}, },
"/api/v1/chapa/payments/initialize": { "400": {
"post": { "description": "Invalid request",
"description": "Initiate a payment through Chapa",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Initialize a payment transaction",
"parameters": [
{
"description": "Payment initialization request",
"name": "payload",
"in": "body",
"required": true,
"schema": { "schema": {
"$ref": "#/definitions/domain.InitPaymentRequest" "$ref": "#/definitions/domain.Response"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.InitPaymentResponse"
}
}
}
} }
}, },
"/api/v1/chapa/payments/verify/{tx_ref}": { "401": {
"get": { "description": "Unauthorized",
"description": "Verify the transaction status from Chapa using tx_ref",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Verify a payment transaction",
"parameters": [
{
"type": "string",
"description": "Transaction Reference",
"name": "tx_ref",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/domain.VerifyTransactionResponse" "$ref": "#/definitions/domain.Response"
}
}
}
} }
}, },
"/api/v1/chapa/transfers": { "422": {
"post": { "description": "Unprocessable Entity",
"description": "Initiate a transfer request via Chapa",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Create a money transfer",
"parameters": [
{
"description": "Transfer request body",
"name": "payload",
"in": "body",
"required": true,
"schema": { "schema": {
"$ref": "#/definitions/domain.TransferRequest" "$ref": "#/definitions/domain.Response"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.CreateTransferResponse"
}
}
}
} }
}, },
"/api/v1/chapa/transfers/verify/{transfer_ref}": { "500": {
"get": { "description": "Internal Server Error",
"description": "Check the status of a money transfer via reference",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chapa"
],
"summary": "Verify a transfer",
"parameters": [
{
"type": "string",
"description": "Transfer Reference",
"name": "transfer_ref",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/domain.VerifyTransferResponse" "$ref": "#/definitions/domain.Response"
} }
} }
} }
@ -4452,6 +4470,46 @@
} }
} }
}, },
"domain.ChapaDepositRequest": {
"type": "object",
"properties": {
"amount": {
"type": "integer"
},
"branch_id": {
"type": "integer"
},
"currency": {
"type": "string"
},
"phone_number": {
"type": "string"
}
}
},
"domain.ChapaPaymentUrlResponse": {
"type": "object",
"properties": {
"payment_url": {
"type": "string"
}
}
},
"domain.ChapaPaymentUrlResponseWrapper": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.ChapaSupportedBank": { "domain.ChapaSupportedBank": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4505,17 +4563,56 @@
} }
} }
}, },
"domain.ChapaSupportedBanksResponse": { "domain.ChapaSupportedBanksResponseWrapper": {
"type": "object", "type": "object",
"properties": { "properties": {
"data": { "data": {},
"type": "array",
"items": {
"$ref": "#/definitions/domain.ChapaSupportedBank"
}
},
"message": { "message": {
"type": "string" "type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.ChapaTransactionType": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
}
},
"domain.ChapaWithdrawRequest": {
"type": "object",
"properties": {
"account_name": {
"type": "string"
},
"account_number": {
"type": "string"
},
"amount": {
"type": "integer"
},
"bank_code": {
"type": "string"
},
"beneficiary_name": {
"type": "string"
},
"branch_id": {
"type": "integer"
},
"currency": {
"type": "string"
},
"wallet_id": {
"description": "add this",
"type": "integer"
} }
} }
}, },
@ -4571,76 +4668,6 @@
} }
} }
}, },
"domain.CreateTransferResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.TransferData"
},
"message": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.InitPaymentData": {
"type": "object",
"properties": {
"checkout_url": {
"type": "string"
},
"tx_ref": {
"type": "string"
}
}
},
"domain.InitPaymentRequest": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"callback_url": {
"type": "string"
},
"currency": {
"type": "string"
},
"email": {
"type": "string"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"return_url": {
"type": "string"
},
"tx_ref": {
"type": "string"
}
}
},
"domain.InitPaymentResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.InitPaymentData"
},
"message": {
"description": "e.g., \"Payment initialized\"",
"type": "string"
},
"status": {
"description": "\"success\"",
"type": "string"
}
}
},
"domain.Odd": { "domain.Odd": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4871,6 +4898,21 @@
} }
} }
}, },
"domain.Response": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.Role": { "domain.Role": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -4953,86 +4995,6 @@
} }
} }
}, },
"domain.TransactionData": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency": {
"type": "string"
},
"email": {
"type": "string"
},
"status": {
"type": "string"
},
"tx_ref": {
"type": "string"
}
}
},
"domain.TransferData": {
"type": "object",
"properties": {
"amount": {
"type": "string"
},
"currency": {
"type": "string"
},
"reference": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.TransferRequest": {
"type": "object",
"properties": {
"account_number": {
"type": "string"
},
"amount": {
"type": "string"
},
"bank_code": {
"type": "string"
},
"currency": {
"type": "string"
},
"reason": {
"type": "string"
},
"recipient_name": {
"type": "string"
},
"reference": {
"type": "string"
}
}
},
"domain.TransferVerificationData": {
"type": "object",
"properties": {
"account_name": {
"type": "string"
},
"bank_code": {
"type": "string"
},
"reference": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.UpcomingEvent": { "domain.UpcomingEvent": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5139,34 +5101,6 @@
} }
} }
}, },
"domain.VerifyTransactionResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.TransactionData"
},
"message": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.VerifyTransferResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.TransferVerificationData"
},
"message": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"domain.VirtualGame": { "domain.VirtualGame": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -124,6 +124,32 @@ definitions:
example: 2 example: 2
type: integer type: integer
type: object type: object
domain.ChapaDepositRequest:
properties:
amount:
type: integer
branch_id:
type: integer
currency:
type: string
phone_number:
type: string
type: object
domain.ChapaPaymentUrlResponse:
properties:
payment_url:
type: string
type: object
domain.ChapaPaymentUrlResponseWrapper:
properties:
data: {}
message:
type: string
status_code:
type: integer
success:
type: boolean
type: object
domain.ChapaSupportedBank: domain.ChapaSupportedBank:
properties: properties:
acct_length: acct_length:
@ -159,14 +185,40 @@ definitions:
updated_at: updated_at:
type: string type: string
type: object type: object
domain.ChapaSupportedBanksResponse: domain.ChapaSupportedBanksResponseWrapper:
properties: properties:
data: data: {}
items:
$ref: '#/definitions/domain.ChapaSupportedBank'
type: array
message: message:
type: string type: string
status_code:
type: integer
success:
type: boolean
type: object
domain.ChapaTransactionType:
properties:
type:
type: string
type: object
domain.ChapaWithdrawRequest:
properties:
account_name:
type: string
account_number:
type: string
amount:
type: integer
bank_code:
type: string
beneficiary_name:
type: string
branch_id:
type: integer
currency:
type: string
wallet_id:
description: add this
type: integer
type: object type: object
domain.CreateBetOutcomeReq: domain.CreateBetOutcomeReq:
properties: properties:
@ -203,52 +255,6 @@ definitions:
- $ref: '#/definitions/domain.OutcomeStatus' - $ref: '#/definitions/domain.OutcomeStatus'
example: 1 example: 1
type: object type: object
domain.CreateTransferResponse:
properties:
data:
$ref: '#/definitions/domain.TransferData'
message:
type: string
status:
type: string
type: object
domain.InitPaymentData:
properties:
checkout_url:
type: string
tx_ref:
type: string
type: object
domain.InitPaymentRequest:
properties:
amount:
type: string
callback_url:
type: string
currency:
type: string
email:
type: string
first_name:
type: string
last_name:
type: string
return_url:
type: string
tx_ref:
type: string
type: object
domain.InitPaymentResponse:
properties:
data:
$ref: '#/definitions/domain.InitPaymentData'
message:
description: e.g., "Payment initialized"
type: string
status:
description: '"success"'
type: string
type: object
domain.Odd: domain.Odd:
properties: properties:
category: category:
@ -408,6 +414,16 @@ definitions:
totalRewardEarned: totalRewardEarned:
type: number type: number
type: object type: object
domain.Response:
properties:
data: {}
message:
type: string
status_code:
type: integer
success:
type: boolean
type: object
domain.Role: domain.Role:
enum: enum:
- super_admin - super_admin
@ -468,58 +484,6 @@ definitions:
example: 1 example: 1
type: integer type: integer
type: object type: object
domain.TransactionData:
properties:
amount:
type: string
currency:
type: string
email:
type: string
status:
type: string
tx_ref:
type: string
type: object
domain.TransferData:
properties:
amount:
type: string
currency:
type: string
reference:
type: string
status:
type: string
type: object
domain.TransferRequest:
properties:
account_number:
type: string
amount:
type: string
bank_code:
type: string
currency:
type: string
reason:
type: string
recipient_name:
type: string
reference:
type: string
type: object
domain.TransferVerificationData:
properties:
account_name:
type: string
bank_code:
type: string
reference:
type: string
status:
type: string
type: object
domain.UpcomingEvent: domain.UpcomingEvent:
properties: properties:
awayKitImage: awayKitImage:
@ -598,24 +562,6 @@ definitions:
description: Veli's user identifier description: Veli's user identifier
type: string type: string
type: object type: object
domain.VerifyTransactionResponse:
properties:
data:
$ref: '#/definitions/domain.TransactionData'
message:
type: string
status:
type: string
type: object
domain.VerifyTransferResponse:
properties:
data:
$ref: '#/definitions/domain.TransferVerificationData'
message:
type: string
status:
type: string
type: object
domain.VirtualGame: domain.VirtualGame:
properties: properties:
category: category:
@ -1691,123 +1637,133 @@ paths:
get: get:
consumes: consumes:
- application/json - application/json
description: Fetch all supported banks from Chapa
produces: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/domain.ChapaSupportedBanksResponse' $ref: '#/definitions/domain.ChapaSupportedBanksResponseWrapper'
summary: Get list of banks "400":
description: Bad Request
schema:
$ref: '#/definitions/domain.Response'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/domain.Response'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.Response'
"422":
description: Unprocessable Entity
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.Response'
summary: fetches chapa supported banks
tags: tags:
- Chapa - Chapa
/api/v1/chapa/payments/callback: /api/v1/chapa/payments/deposit:
post: post:
consumes: consumes:
- application/json - application/json
description: Endpoint to receive webhook payloads from Chapa description: Deposits money into user wallet from user account using Chapa
parameters: parameters:
- description: Webhook Payload (dynamic) - description: Deposit request payload
in: body in: body
name: payload name: payload
required: true required: true
schema: schema:
$ref: '#/definitions/domain.ChapaDepositRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.ChapaPaymentUrlResponseWrapper'
"400":
description: Invalid request
schema:
$ref: '#/definitions/domain.Response'
"422":
description: Validation error
schema:
$ref: '#/definitions/domain.Response'
"500":
description: Internal server error
schema:
$ref: '#/definitions/domain.Response'
summary: Deposit money into user wallet using Chapa
tags:
- Chapa
/api/v1/chapa/payments/verify:
post:
consumes:
- application/json
parameters:
- description: Webhook Payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/domain.ChapaTransactionType'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.Response'
summary: Verifies Chapa webhook transaction
tags:
- Chapa
/api/v1/chapa/payments/withdraw:
post:
consumes:
- application/json
description: Initiates a withdrawal transaction using Chapa for the authenticated
user.
parameters:
- description: Chapa Withdraw Request
in: body
name: request
required: true
schema:
$ref: '#/definitions/domain.ChapaWithdrawRequest'
produces:
- application/json
responses:
"200":
description: Withdrawal requested successfully
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
type: string
type: object type: object
produces: "400":
- application/json description: Invalid request
responses:
"200":
description: ok
schema: schema:
type: string $ref: '#/definitions/domain.Response'
summary: Receive Chapa webhook "401":
tags: description: Unauthorized
- Chapa
/api/v1/chapa/payments/initialize:
post:
consumes:
- application/json
description: Initiate a payment through Chapa
parameters:
- description: Payment initialization request
in: body
name: payload
required: true
schema: schema:
$ref: '#/definitions/domain.InitPaymentRequest' $ref: '#/definitions/domain.Response'
produces: "422":
- application/json description: Unprocessable Entity
responses:
"200":
description: OK
schema: schema:
$ref: '#/definitions/domain.InitPaymentResponse' $ref: '#/definitions/domain.Response'
summary: Initialize a payment transaction "500":
tags: description: Internal Server Error
- Chapa
/api/v1/chapa/payments/verify/{tx_ref}:
get:
consumes:
- application/json
description: Verify the transaction status from Chapa using tx_ref
parameters:
- description: Transaction Reference
in: path
name: tx_ref
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema: schema:
$ref: '#/definitions/domain.VerifyTransactionResponse' $ref: '#/definitions/domain.Response'
summary: Verify a payment transaction summary: Withdraw using Chapa
tags:
- Chapa
/api/v1/chapa/transfers:
post:
consumes:
- application/json
description: Initiate a transfer request via Chapa
parameters:
- description: Transfer request body
in: body
name: payload
required: true
schema:
$ref: '#/definitions/domain.TransferRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.CreateTransferResponse'
summary: Create a money transfer
tags:
- Chapa
/api/v1/chapa/transfers/verify/{transfer_ref}:
get:
consumes:
- application/json
description: Check the status of a money transfer via reference
parameters:
- description: Transfer Reference
in: path
name: transfer_ref
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.VerifyTransferResponse'
summary: Verify a transfer
tags: tags:
- Chapa - Chapa
/api/v1/virtual-games/recommendations/{userID}: /api/v1/virtual-games/recommendations/{userID}:

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.28.0 // sqlc v1.29.0
// source: cashier.sql // source: cashier.sql
package dbgen package dbgen

View File

@ -47,15 +47,15 @@ ORDER BY start_time ASC
type GetAllUpcomingEventsRow struct { type GetAllUpcomingEventsRow struct {
ID string `json:"id"` ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"` MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"` HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"` AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"` HomeTeamID pgtype.Int4 `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"` AwayTeamID pgtype.Int4 `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_kit_image"` HomeKitImage pgtype.Text `json:"home_kit_image"`
AwayKitImage pgtype.Text `json:"away_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"`
LeagueID pgtype.Text `json:"league_id"` LeagueID pgtype.Int4 `json:"league_id"`
LeagueName pgtype.Text `json:"league_name"` LeagueName pgtype.Text `json:"league_name"`
LeagueCc pgtype.Text `json:"league_cc"` LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"` StartTime pgtype.Timestamp `json:"start_time"`
@ -132,15 +132,15 @@ ORDER BY start_time ASC
type GetExpiredUpcomingEventsRow struct { type GetExpiredUpcomingEventsRow struct {
ID string `json:"id"` ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"` MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"` HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"` AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"` HomeTeamID pgtype.Int4 `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"` AwayTeamID pgtype.Int4 `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_kit_image"` HomeKitImage pgtype.Text `json:"home_kit_image"`
AwayKitImage pgtype.Text `json:"away_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"`
LeagueID pgtype.Text `json:"league_id"` LeagueID pgtype.Int4 `json:"league_id"`
LeagueName pgtype.Text `json:"league_name"` LeagueName pgtype.Text `json:"league_name"`
LeagueCc pgtype.Text `json:"league_cc"` LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"` StartTime pgtype.Timestamp `json:"start_time"`
@ -230,8 +230,8 @@ LIMIT $6 OFFSET $5
` `
type GetPaginatedUpcomingEventsParams struct { type GetPaginatedUpcomingEventsParams struct {
LeagueID pgtype.Text `json:"league_id"` LeagueID pgtype.Int4 `json:"league_id"`
SportID pgtype.Text `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
LastStartTime pgtype.Timestamp `json:"last_start_time"` LastStartTime pgtype.Timestamp `json:"last_start_time"`
FirstStartTime pgtype.Timestamp `json:"first_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"`
Offset pgtype.Int4 `json:"offset"` Offset pgtype.Int4 `json:"offset"`
@ -240,15 +240,15 @@ type GetPaginatedUpcomingEventsParams struct {
type GetPaginatedUpcomingEventsRow struct { type GetPaginatedUpcomingEventsRow struct {
ID string `json:"id"` ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"` MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"` HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"` AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"` HomeTeamID pgtype.Int4 `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"` AwayTeamID pgtype.Int4 `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_kit_image"` HomeKitImage pgtype.Text `json:"home_kit_image"`
AwayKitImage pgtype.Text `json:"away_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"`
LeagueID pgtype.Text `json:"league_id"` LeagueID pgtype.Int4 `json:"league_id"`
LeagueName pgtype.Text `json:"league_name"` LeagueName pgtype.Text `json:"league_name"`
LeagueCc pgtype.Text `json:"league_cc"` LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"` StartTime pgtype.Timestamp `json:"start_time"`
@ -327,8 +327,8 @@ WHERE is_live = false
` `
type GetTotalEventsParams struct { type GetTotalEventsParams struct {
LeagueID pgtype.Text `json:"league_id"` LeagueID pgtype.Int4 `json:"league_id"`
SportID pgtype.Text `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
LastStartTime pgtype.Timestamp `json:"last_start_time"` LastStartTime pgtype.Timestamp `json:"last_start_time"`
FirstStartTime pgtype.Timestamp `json:"first_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"`
} }
@ -372,15 +372,15 @@ LIMIT 1
type GetUpcomingByIDRow struct { type GetUpcomingByIDRow struct {
ID string `json:"id"` ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"` MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"` HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"` AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"` HomeTeamID pgtype.Int4 `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"` AwayTeamID pgtype.Int4 `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_kit_image"` HomeKitImage pgtype.Text `json:"home_kit_image"`
AwayKitImage pgtype.Text `json:"away_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"`
LeagueID pgtype.Text `json:"league_id"` LeagueID pgtype.Int4 `json:"league_id"`
LeagueName pgtype.Text `json:"league_name"` LeagueName pgtype.Text `json:"league_name"`
LeagueCc pgtype.Text `json:"league_cc"` LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"` StartTime pgtype.Timestamp `json:"start_time"`
@ -488,15 +488,15 @@ SET sport_id = EXCLUDED.sport_id,
type InsertEventParams struct { type InsertEventParams struct {
ID string `json:"id"` ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"` MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"` HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"` AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"` HomeTeamID pgtype.Int4 `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"` AwayTeamID pgtype.Int4 `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_kit_image"` HomeKitImage pgtype.Text `json:"home_kit_image"`
AwayKitImage pgtype.Text `json:"away_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"`
LeagueID pgtype.Text `json:"league_id"` LeagueID pgtype.Int4 `json:"league_id"`
LeagueName pgtype.Text `json:"league_name"` LeagueName pgtype.Text `json:"league_name"`
LeagueCc pgtype.Text `json:"league_cc"` LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"` StartTime pgtype.Timestamp `json:"start_time"`
@ -595,15 +595,15 @@ SET sport_id = EXCLUDED.sport_id,
type InsertUpcomingEventParams struct { type InsertUpcomingEventParams struct {
ID string `json:"id"` ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"` MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"` HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"` AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"` HomeTeamID pgtype.Int4 `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"` AwayTeamID pgtype.Int4 `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_kit_image"` HomeKitImage pgtype.Text `json:"home_kit_image"`
AwayKitImage pgtype.Text `json:"away_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"`
LeagueID pgtype.Text `json:"league_id"` LeagueID pgtype.Int4 `json:"league_id"`
LeagueName pgtype.Text `json:"league_name"` LeagueName pgtype.Text `json:"league_name"`
LeagueCc pgtype.Text `json:"league_cc"` LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"` StartTime pgtype.Timestamp `json:"start_time"`

174
gen/db/leagues.sql.go Normal file
View File

@ -0,0 +1,174 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: leagues.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CheckLeagueSupport = `-- name: CheckLeagueSupport :one
SELECT EXISTS(
SELECT 1
FROM leagues
WHERE id = $1
AND is_active = true
)
`
func (q *Queries) CheckLeagueSupport(ctx context.Context, id int64) (bool, error) {
row := q.db.QueryRow(ctx, CheckLeagueSupport, id)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const GetAllLeagues = `-- name: GetAllLeagues :many
SELECT id,
name,
country_code,
bet365_id,
is_active
FROM leagues
`
func (q *Queries) GetAllLeagues(ctx context.Context) ([]League, error) {
rows, err := q.db.Query(ctx, GetAllLeagues)
if err != nil {
return nil, err
}
defer rows.Close()
var items []League
for rows.Next() {
var i League
if err := rows.Scan(
&i.ID,
&i.Name,
&i.CountryCode,
&i.Bet365ID,
&i.IsActive,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetSupportedLeagues = `-- name: GetSupportedLeagues :many
SELECT id,
name,
country_code,
bet365_id,
is_active
FROM leagues
WHERE is_active = true
`
func (q *Queries) GetSupportedLeagues(ctx context.Context) ([]League, error) {
rows, err := q.db.Query(ctx, GetSupportedLeagues)
if err != nil {
return nil, err
}
defer rows.Close()
var items []League
for rows.Next() {
var i League
if err := rows.Scan(
&i.ID,
&i.Name,
&i.CountryCode,
&i.Bet365ID,
&i.IsActive,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const InsertLeague = `-- name: InsertLeague :exec
INSERT INTO leagues (
id,
name,
country_code,
bet365_id,
is_active
) VALUES (
$1, $2, $3, $4, $5
)
ON CONFLICT (id) DO UPDATE
SET name = EXCLUDED.name,
country_code = EXCLUDED.country_code,
bet365_id = EXCLUDED.bet365_id,
is_active = EXCLUDED.is_active
`
type InsertLeagueParams struct {
ID int64 `json:"id"`
Name string `json:"name"`
CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"`
IsActive pgtype.Bool `json:"is_active"`
}
func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) error {
_, err := q.db.Exec(ctx, InsertLeague,
arg.ID,
arg.Name,
arg.CountryCode,
arg.Bet365ID,
arg.IsActive,
)
return err
}
const SetLeagueActive = `-- name: SetLeagueActive :exec
UPDATE leagues
SET is_active = true
WHERE id = $1
`
func (q *Queries) SetLeagueActive(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, SetLeagueActive, id)
return err
}
const UpdateLeague = `-- name: UpdateLeague :exec
UPDATE leagues
SET name = $1,
country_code = $2,
bet365_id = $3,
is_active = $4
WHERE id = $5
`
type UpdateLeagueParams struct {
Name string `json:"name"`
CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"`
IsActive pgtype.Bool `json:"is_active"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateLeague(ctx context.Context, arg UpdateLeagueParams) error {
_, err := q.db.Exec(ctx, UpdateLeague,
arg.Name,
arg.CountryCode,
arg.Bet365ID,
arg.IsActive,
arg.ID,
)
return err
}

View File

@ -178,15 +178,15 @@ type CustomerWallet struct {
type Event struct { type Event struct {
ID string `json:"id"` ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"` SportID pgtype.Int4 `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"` MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"` HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"` AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"` HomeTeamID pgtype.Int4 `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"` AwayTeamID pgtype.Int4 `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_kit_image"` HomeKitImage pgtype.Text `json:"home_kit_image"`
AwayKitImage pgtype.Text `json:"away_kit_image"` AwayKitImage pgtype.Text `json:"away_kit_image"`
LeagueID pgtype.Text `json:"league_id"` LeagueID pgtype.Int4 `json:"league_id"`
LeagueName pgtype.Text `json:"league_name"` LeagueName pgtype.Text `json:"league_name"`
LeagueCc pgtype.Text `json:"league_cc"` LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"` StartTime pgtype.Timestamp `json:"start_time"`
@ -201,6 +201,14 @@ type Event struct {
Source pgtype.Text `json:"source"` Source pgtype.Text `json:"source"`
} }
type League struct {
ID int64 `json:"id"`
Name string `json:"name"`
CountryCode pgtype.Text `json:"country_code"`
Bet365ID pgtype.Int4 `json:"bet365_id"`
IsActive pgtype.Bool `json:"is_active"`
}
type Notification struct { type Notification struct {
ID string `json:"id"` ID string `json:"id"`
RecipientID int64 `json:"recipient_id"` RecipientID int64 `json:"recipient_id"`

18
go.mod
View File

@ -9,28 +9,28 @@ require (
github.com/gofiber/fiber/v2 v2.52.6 github.com/gofiber/fiber/v2 v2.52.6
github.com/golang-jwt/jwt/v5 v5.2.2 github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/jackc/pgx/v5 v5.7.4 github.com/jackc/pgx/v5 v5.7.4
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shopspring/decimal v1.4.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
// github.com/stretchr/testify v1.10.0
github.com/swaggo/fiber-swagger v1.3.0 github.com/swaggo/fiber-swagger v1.3.0
github.com/swaggo/swag v1.16.4 github.com/swaggo/swag v1.16.4
github.com/valyala/fasthttp v1.59.0
golang.org/x/crypto v0.36.0 golang.org/x/crypto v0.36.0
) )
require github.com/gorilla/websocket v1.5.3 // indirect
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
)
require ( require (
// github.com/davecgh/go-spew v1.1.1 // indirect
// github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
// github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect
@ -38,7 +38,6 @@ require (
github.com/go-openapi/swag v0.23.1 // indirect github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/gorilla/websocket v1.5.3
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
@ -50,12 +49,13 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
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
github.com/valyala/fasthttp v1.59.0
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 // indirect
golang.org/x/sync v0.12.0 // indirect golang.org/x/sync v0.12.0 // indirect

4
go.sum
View File

@ -112,12 +112,16 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -289,3 +289,8 @@ func (c *Config) loadEnv() error {
c.Bet365Token = betToken c.Bet365Token = betToken
return nil return nil
} }
type ChapaConfig struct {
ChapaPaymentType string `mapstructure:"chapa_payment_type"`
ChapaTransferType string `mapstructure:"chapa_transfer_type"`
}

View File

@ -1,6 +1,9 @@
package domain package domain
import "time" import (
"errors"
"time"
)
var ( var (
ChapaSecret string ChapaSecret string
@ -8,7 +11,7 @@ var (
) )
type InitPaymentRequest struct { type InitPaymentRequest struct {
Amount string `json:"amount"` Amount Currency `json:"amount"`
Currency string `json:"currency"` Currency string `json:"currency"`
Email string `json:"email"` Email string `json:"email"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
@ -105,3 +108,121 @@ type VerifyTransferResponse struct {
Message string `json:"message"` Message string `json:"message"`
Data TransferVerificationData `json:"data"` Data TransferVerificationData `json:"data"`
} }
type ChapaTransactionType struct {
Type string `json:"type"`
}
type ChapaWebHookTransfer struct {
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
BankId string `json:"bank_id"`
BankName string `json:"bank_name"`
Currency string `json:"currency"`
Amount string `json:"amount"`
Type string `json:"type"`
Status string `json:"status"`
Reference string `json:"reference"`
TxRef string `json:"tx_ref"`
ChapaReference string `json:"chapa_reference"`
CreatedAt time.Time `json:"created_at"`
}
type ChapaWebHookPayment struct {
Event string `json:"event"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Mobile interface{} `json:"mobile"`
Currency string `json:"currency"`
Amount string `json:"amount"`
Charge string `json:"charge"`
Status string `json:"status"`
Mode string `json:"mode"`
Reference string `json:"reference"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Type string `json:"type"`
TxRef string `json:"tx_ref"`
PaymentMethod string `json:"payment_method"`
Customization struct {
Title interface{} `json:"title"`
Description interface{} `json:"description"`
Logo interface{} `json:"logo"`
} `json:"customization"`
Meta string `json:"meta"`
}
type ChapaWithdrawRequest struct {
WalletID int64 `json:"wallet_id"` // add this
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
BeneficiaryName string `json:"beneficiary_name"`
BankCode string `json:"bank_code"`
BranchID int64 `json:"branch_id"`
}
type ChapaTransferPayload struct {
AccountName string
AccountNumber string
Amount string
Currency string
BeneficiaryName string
TxRef string
Reference string
BankCode string
}
type ChapaDepositRequest struct {
Amount Currency `json:"amount"`
PhoneNumber string `json:"phone_number"`
Currency string `json:"currency"`
BranchID int64 `json:"branch_id"`
}
func (r ChapaDepositRequest) Validate() error {
if r.Amount <= 0 {
return errors.New("amount must be greater than zero")
}
if r.Currency == "" {
return errors.New("currency is required")
}
if r.PhoneNumber == "" {
return errors.New("phone number is required")
}
if r.BranchID == 0 {
return errors.New("branch ID is required")
}
return nil
}
type AcceptChapaPaymentRequest struct {
Amount string `json:"amount"`
Currency string `json:"currency"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhoneNumber string `json:"phone_number"`
TxRef string `json:"tx_ref"`
CallbackUrl string `json:"callback_url"`
ReturnUrl string `json:"return_url"`
CustomizationTitle string `json:"customization[title]"`
CustomizationDescription string `json:"customization[description]"`
}
type ChapaPaymentUrlResponse struct {
PaymentURL string `json:"payment_url"`
}
type ChapaPaymentUrlResponseWrapper struct {
Data ChapaPaymentUrlResponse `json:"data"`
Response
}
type ChapaSupportedBanksResponseWrapper struct {
Data []ChapaSupportedBank `json:"data"`
Response
}

View File

@ -13,6 +13,10 @@ type ValidInt struct {
Value int Value int
Valid bool Valid bool
} }
type ValidInt32 struct {
Value int32
Valid bool
}
type ValidString struct { type ValidString struct {
Value string Value string
@ -48,6 +52,18 @@ func (m Currency) String() string {
return fmt.Sprintf("$%.2f", x) return fmt.Sprintf("$%.2f", x)
} }
type ResponseWDataFactory[T any] struct {
Data T `json:"data"`
Response
}
type Response struct {
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Success bool `json:"success"`
StatusCode int `json:"status_code"`
}
func CalculateWinnings(amount Currency, totalOdds float32) Currency { func CalculateWinnings(amount Currency, totalOdds float32) Currency {
vat := amount.Float32() * 0.15 vat := amount.Float32() * 0.15

View File

@ -37,15 +37,15 @@ const (
type Event struct { type Event struct {
ID string ID string
SportID string SportID int32
MatchName string MatchName string
HomeTeam string HomeTeam string
AwayTeam string AwayTeam string
HomeTeamID string HomeTeamID int32
AwayTeamID string AwayTeamID int32
HomeKitImage string HomeKitImage string
AwayKitImage string AwayKitImage string
LeagueID string LeagueID int32
LeagueName string LeagueName string
LeagueCC string LeagueCC string
StartTime string StartTime string
@ -87,15 +87,15 @@ type BetResult struct {
type UpcomingEvent struct { type UpcomingEvent struct {
ID string // Event ID ID string // Event ID
SportID string // Sport ID SportID int32 // Sport ID
MatchName string // Match or event name MatchName string // Match or event name
HomeTeam string // Home team name (if available) HomeTeam string // Home team name (if available)
AwayTeam string // Away team name (can be empty/null) AwayTeam string // Away team name (can be empty/null)
HomeTeamID string // Home team ID HomeTeamID int32 // Home team ID
AwayTeamID string // Away team ID (can be empty/null) AwayTeamID int32 // Away team ID (can be empty/null)
HomeKitImage string // Kit or image for home team (optional) HomeKitImage string // Kit or image for home team (optional)
AwayKitImage string // Kit or image for away team (optional) AwayKitImage string // Kit or image for away team (optional)
LeagueID string // League ID LeagueID int32 // League ID
LeagueName string // League name LeagueName string // League name
LeagueCC string // League country code LeagueCC string // League country code
StartTime time.Time // Converted from "time" field in UNIX format StartTime time.Time // Converted from "time" field in UNIX format

View File

@ -1,66 +1,9 @@
package domain package domain
// TODO Will make this dynamic by moving into the database type League struct {
ID int64
var SupportedLeagues = []int64{ Name string
// Football CountryCode string
10041282, //Premier League Bet365ID int32
10083364, //La Liga IsActive bool
10041095, //German Bundesliga
10041100, //Ligue 1
10041809, //UEFA Champions League
10041957, //UEFA Europa League
10079560, //UEFA Conference League
10047168, // US MLS
10044469, // Ethiopian Premier League
10050282, //UEFA Nations League
10044685, //FIFA Club World Cup
10082328, //Kings League World Cup
10081269, //CONCACAF Champions Cup
10040162, //Asia - World Cup Qualifying
10067624, //South America - World Cup Qualifying
10067913, // Europe - World Cup Qualifying
10067624, // South America - World Cup Qualifying
10043156, //England FA Cup
10042103, //France Cup
10041088, //Premier League 2
10084250, //Turkiye Super League
10041187, //Kenya Super League
10041315, //Italian Serie A
10041391, //Netherlands Eredivisie
10036538, //Spain Segunda
10041058, //Denmark Superligaen
10077480, //Womens International
10046936, // USA NPSL
10085159, //Baller League UK
10040601, //Argentina Cup
10037440, //Brazil Serie A
10043205, //Copa Sudamericana
10037327, //Austria Landesliga
10082020, //USA USL League One Cup
10037075, //International Match
10046648, //Kenya Cup
10040485, //Kenya Super League
10041369, //Norway Eliteserien
// Basketball
173998768, //NBA
10041830, //NBA
10049984, //WNBA
10037165, //German Bundesliga
10036608, //Italian Lega 1
10040795, //EuroLeague
10084178, //Kenya Premier League
10043548, //International Women
// Ice Hockey
10037477, //NHL
10037447, //AHL
10074238, // AIHL
10069385, //IIHF World Championship
// Cricket
} }

View File

@ -1,7 +1,6 @@
package domain package domain
import ( import (
"encoding/json"
"time" "time"
) )
@ -15,10 +14,11 @@ type Market struct {
MarketName string MarketName string
MarketID string MarketID string
UpdatedAt time.Time UpdatedAt time.Time
Odds []json.RawMessage Odds []map[string]interface{}
Name string Name string
Handicap string Handicap string
OddsVal float64 OddsVal float64
Source string
} }
type Odd struct { type Odd struct {

View File

@ -0,0 +1,53 @@
package domain
import (
"errors"
"github.com/gofiber/fiber/v2"
)
func UnProcessableEntityResponse(c *fiber.Ctx) error {
return c.Status(fiber.StatusUnprocessableEntity).JSON(Response{
Message: "failed to parse request body",
StatusCode: fiber.StatusUnprocessableEntity,
Success: false,
})
}
func FiberErrorResponse(c *fiber.Ctx, err error) error {
var statusCode int
var message string
switch {
case errors.Is(err, fiber.ErrNotFound):
statusCode = fiber.StatusNotFound
message = "resource not found"
case errors.Is(err, fiber.ErrBadRequest):
statusCode = fiber.StatusBadRequest
message = "bad request"
case errors.Is(err, fiber.ErrUnauthorized):
statusCode = fiber.StatusUnauthorized
message = "unauthorized"
case errors.Is(err, fiber.ErrForbidden):
statusCode = fiber.StatusForbidden
message = "forbidden"
case errors.Is(err, fiber.ErrConflict):
statusCode = fiber.StatusConflict
message = "conflict occurred"
default:
statusCode = fiber.StatusInternalServerError
message = "unexpected server error"
}
return c.Status(statusCode).JSON(fiber.Map{
"success": false,
"status_code": statusCode,
"error": message,
"details": err.Error(),
})
}

View File

@ -9,7 +9,7 @@ type BaseResultResponse struct {
Results []json.RawMessage `json:"results"` Results []json.RawMessage `json:"results"`
} }
type League struct { type LeagueRes struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
CC string `json:"cc"` CC string `json:"cc"`
@ -43,7 +43,7 @@ type FootballResultResponse struct {
SportID string `json:"sport_id"` SportID string `json:"sport_id"`
Time string `json:"time"` Time string `json:"time"`
TimeStatus string `json:"time_status"` TimeStatus string `json:"time_status"`
League League `json:"league"` League LeagueRes `json:"league"`
Home Team `json:"home"` Home Team `json:"home"`
Away Team `json:"away"` Away Team `json:"away"`
SS string `json:"ss"` SS string `json:"ss"`
@ -82,7 +82,7 @@ type BasketballResultResponse struct {
SportID string `json:"sport_id"` SportID string `json:"sport_id"`
Time string `json:"time"` Time string `json:"time"`
TimeStatus string `json:"time_status"` TimeStatus string `json:"time_status"`
League League `json:"league"` League LeagueRes `json:"league"`
Home Team `json:"home"` Home Team `json:"home"`
Away Team `json:"away"` Away Team `json:"away"`
SS string `json:"ss"` SS string `json:"ss"`
@ -129,7 +129,7 @@ type IceHockeyResultResponse struct {
SportID string `json:"sport_id"` SportID string `json:"sport_id"`
Time string `json:"time"` Time string `json:"time"`
TimeStatus string `json:"time_status"` TimeStatus string `json:"time_status"`
League League `json:"league"` League LeagueRes `json:"league"`
Home Team `json:"home"` Home Team `json:"home"`
Away Team `json:"away"` Away Team `json:"away"`
SS string `json:"ss"` SS string `json:"ss"`

View File

@ -63,7 +63,6 @@ type CreateTransaction struct {
PaymentOption PaymentOption PaymentOption PaymentOption
FullName string FullName string
PhoneNumber string PhoneNumber string
// Payment Details for bank
BankCode string BankCode string
BeneficiaryName string BeneficiaryName string
AccountName string AccountName string

View File

@ -18,13 +18,24 @@ var Environment = map[string]string{
func NewLogger(env string, lvl slog.Level) *slog.Logger { func NewLogger(env string, lvl slog.Level) *slog.Logger {
var logHandler slog.Handler var logHandler slog.Handler
err := os.MkdirAll("logs", os.ModePerm)
if err != nil {
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)
if err != nil {
panic("Failed to open log file: " + err.Error())
}
switch env { switch env {
case "development": case "development":
logHandler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ logHandler = slog.NewTextHandler(file, &slog.HandlerOptions{
Level: lvl, Level: lvl,
}) })
default: default:
logHandler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ logHandler = slog.NewJSONHandler(file, &slog.HandlerOptions{
Level: lvl, Level: lvl,
}) })
} }

View File

@ -21,15 +21,15 @@ func (s *Store) SaveEvent(ctx context.Context, e domain.Event) error {
return s.queries.InsertEvent(ctx, dbgen.InsertEventParams{ return s.queries.InsertEvent(ctx, dbgen.InsertEventParams{
ID: e.ID, ID: e.ID,
SportID: pgtype.Text{String: e.SportID, Valid: true}, SportID: pgtype.Int4{Int32: e.SportID, Valid: true},
MatchName: pgtype.Text{String: e.MatchName, Valid: true}, MatchName: pgtype.Text{String: e.MatchName, Valid: true},
HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true}, HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true},
AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true}, AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true},
HomeTeamID: pgtype.Text{String: e.HomeTeamID, Valid: true}, HomeTeamID: pgtype.Int4{Int32: e.HomeTeamID, Valid: true},
AwayTeamID: pgtype.Text{String: e.AwayTeamID, Valid: true}, AwayTeamID: pgtype.Int4{Int32: e.AwayTeamID, Valid: true},
HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true}, HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true},
AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true}, AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true},
LeagueID: pgtype.Text{String: e.LeagueID, Valid: true}, LeagueID: pgtype.Int4{Int32: e.LeagueID, Valid: true},
LeagueName: pgtype.Text{String: e.LeagueName, Valid: true}, LeagueName: pgtype.Text{String: e.LeagueName, Valid: true},
LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true}, LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true},
StartTime: pgtype.Timestamp{Time: parsedTime, Valid: true}, StartTime: pgtype.Timestamp{Time: parsedTime, Valid: true},
@ -46,15 +46,15 @@ func (s *Store) SaveEvent(ctx context.Context, e domain.Event) error {
func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) error { func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) error {
return s.queries.InsertUpcomingEvent(ctx, dbgen.InsertUpcomingEventParams{ return s.queries.InsertUpcomingEvent(ctx, dbgen.InsertUpcomingEventParams{
ID: e.ID, ID: e.ID,
SportID: pgtype.Text{String: e.SportID, Valid: true}, SportID: pgtype.Int4{Int32: e.SportID, Valid: true},
MatchName: pgtype.Text{String: e.MatchName, Valid: true}, MatchName: pgtype.Text{String: e.MatchName, Valid: true},
HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true}, HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true},
AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true}, AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true},
HomeTeamID: pgtype.Text{String: e.HomeTeamID, Valid: true}, HomeTeamID: pgtype.Int4{Int32: e.HomeTeamID, Valid: true},
AwayTeamID: pgtype.Text{String: e.AwayTeamID, Valid: true}, AwayTeamID: pgtype.Int4{Int32: e.AwayTeamID, Valid: true},
HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true}, HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true},
AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true}, AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true},
LeagueID: pgtype.Text{String: e.LeagueID, Valid: true}, LeagueID: pgtype.Int4{Int32: e.LeagueID, Valid: true},
LeagueName: pgtype.Text{String: e.LeagueName, Valid: true}, LeagueName: pgtype.Text{String: e.LeagueName, Valid: true},
LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true}, LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true},
StartTime: pgtype.Timestamp{Time: e.StartTime, Valid: true}, StartTime: pgtype.Timestamp{Time: e.StartTime, Valid: true},
@ -75,15 +75,15 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven
for i, e := range events { for i, e := range events {
upcomingEvents[i] = domain.UpcomingEvent{ upcomingEvents[i] = domain.UpcomingEvent{
ID: e.ID, ID: e.ID,
SportID: e.SportID.String, SportID: e.SportID.Int32,
MatchName: e.MatchName.String, MatchName: e.MatchName.String,
HomeTeam: e.HomeTeam.String, HomeTeam: e.HomeTeam.String,
AwayTeam: e.AwayTeam.String, AwayTeam: e.AwayTeam.String,
HomeTeamID: e.HomeTeamID.String, HomeTeamID: e.HomeTeamID.Int32,
AwayTeamID: e.AwayTeamID.String, AwayTeamID: e.AwayTeamID.Int32,
HomeKitImage: e.HomeKitImage.String, HomeKitImage: e.HomeKitImage.String,
AwayKitImage: e.AwayKitImage.String, AwayKitImage: e.AwayKitImage.String,
LeagueID: e.LeagueID.String, LeagueID: e.LeagueID.Int32,
LeagueName: e.LeagueName.String, LeagueName: e.LeagueName.String,
LeagueCC: e.LeagueCc.String, LeagueCC: e.LeagueCc.String,
StartTime: e.StartTime.Time.UTC(), StartTime: e.StartTime.Time.UTC(),
@ -106,15 +106,15 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context, filter domain.Even
for i, e := range events { for i, e := range events {
upcomingEvents[i] = domain.UpcomingEvent{ upcomingEvents[i] = domain.UpcomingEvent{
ID: e.ID, ID: e.ID,
SportID: e.SportID.String, SportID: e.SportID.Int32,
MatchName: e.MatchName.String, MatchName: e.MatchName.String,
HomeTeam: e.HomeTeam.String, HomeTeam: e.HomeTeam.String,
AwayTeam: e.AwayTeam.String, AwayTeam: e.AwayTeam.String,
HomeTeamID: e.HomeTeamID.String, HomeTeamID: e.HomeTeamID.Int32,
AwayTeamID: e.AwayTeamID.String, AwayTeamID: e.AwayTeamID.Int32,
HomeKitImage: e.HomeKitImage.String, HomeKitImage: e.HomeKitImage.String,
AwayKitImage: e.AwayKitImage.String, AwayKitImage: e.AwayKitImage.String,
LeagueID: e.LeagueID.String, LeagueID: e.LeagueID.Int32,
LeagueName: e.LeagueName.String, LeagueName: e.LeagueName.String,
LeagueCC: e.LeagueCc.String, LeagueCC: e.LeagueCc.String,
StartTime: e.StartTime.Time.UTC(), StartTime: e.StartTime.Time.UTC(),
@ -160,15 +160,15 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev
for i, e := range events { for i, e := range events {
upcomingEvents[i] = domain.UpcomingEvent{ upcomingEvents[i] = domain.UpcomingEvent{
ID: e.ID, ID: e.ID,
SportID: e.SportID.String, SportID: e.SportID.Int32,
MatchName: e.MatchName.String, MatchName: e.MatchName.String,
HomeTeam: e.HomeTeam.String, HomeTeam: e.HomeTeam.String,
AwayTeam: e.AwayTeam.String, AwayTeam: e.AwayTeam.String,
HomeTeamID: e.HomeTeamID.String, HomeTeamID: e.HomeTeamID.Int32,
AwayTeamID: e.AwayTeamID.String, AwayTeamID: e.AwayTeamID.Int32,
HomeKitImage: e.HomeKitImage.String, HomeKitImage: e.HomeKitImage.String,
AwayKitImage: e.AwayKitImage.String, AwayKitImage: e.AwayKitImage.String,
LeagueID: e.LeagueID.String, LeagueID: e.LeagueID.Int32,
LeagueName: e.LeagueName.String, LeagueName: e.LeagueName.String,
LeagueCC: e.LeagueCc.String, LeagueCC: e.LeagueCc.String,
StartTime: e.StartTime.Time.UTC(), StartTime: e.StartTime.Time.UTC(),
@ -208,15 +208,15 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc
return domain.UpcomingEvent{ return domain.UpcomingEvent{
ID: event.ID, ID: event.ID,
SportID: event.SportID.String, SportID: event.SportID.Int32,
MatchName: event.MatchName.String, MatchName: event.MatchName.String,
HomeTeam: event.HomeTeam.String, HomeTeam: event.HomeTeam.String,
AwayTeam: event.AwayTeam.String, AwayTeam: event.AwayTeam.String,
HomeTeamID: event.HomeTeamID.String, HomeTeamID: event.HomeTeamID.Int32,
AwayTeamID: event.AwayTeamID.String, AwayTeamID: event.AwayTeamID.Int32,
HomeKitImage: event.HomeKitImage.String, HomeKitImage: event.HomeKitImage.String,
AwayKitImage: event.AwayKitImage.String, AwayKitImage: event.AwayKitImage.String,
LeagueID: event.LeagueID.String, LeagueID: event.LeagueID.Int32,
LeagueName: event.LeagueName.String, LeagueName: event.LeagueName.String,
LeagueCC: event.LeagueCc.String, LeagueCC: event.LeagueCc.String,
StartTime: event.StartTime.Time.UTC(), StartTime: event.StartTime.Time.UTC(),

View File

@ -0,0 +1,75 @@
package repository
import (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
)
func (s *Store) SaveLeague(ctx context.Context, l domain.League) error {
return s.queries.InsertLeague(ctx, dbgen.InsertLeagueParams{
ID: l.ID,
Name: l.Name,
CountryCode: pgtype.Text{String: l.CountryCode, Valid: true},
Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true},
IsActive: pgtype.Bool{Bool: l.IsActive, Valid: true},
})
}
func (s *Store) GetSupportedLeagues(ctx context.Context) ([]domain.League, error) {
leagues, err := s.queries.GetSupportedLeagues(ctx)
if err != nil {
return nil, err
}
supportedLeagues := make([]domain.League, len(leagues))
for i, league := range leagues {
supportedLeagues[i] = domain.League{
ID: league.ID,
Name: league.Name,
CountryCode: league.CountryCode.String,
Bet365ID: league.Bet365ID.Int32,
IsActive: league.IsActive.Bool,
}
}
return supportedLeagues, nil
}
func (s *Store) GetAllLeagues(ctx context.Context) ([]domain.League, error) {
l, err := s.queries.GetAllLeagues(ctx)
if err != nil {
return nil, err
}
leagues := make([]domain.League, len(l))
for i, league := range l {
leagues[i] = domain.League{
ID: league.ID,
Name: league.Name,
CountryCode: league.CountryCode.String,
Bet365ID: league.Bet365ID.Int32,
IsActive: league.IsActive.Bool,
}
}
return leagues, nil
}
func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64) (bool, error) {
return s.queries.CheckLeagueSupport(ctx, leagueID)
}
func (s *Store) SetLeagueActive(ctx context.Context, leagueId int64) error {
return s.queries.SetLeagueActive(ctx, leagueId)
}
// TODO: update based on id, no need for the entire league (same as the set active one)
func (s *Store) SetLeagueInActive(ctx context.Context, l domain.League) error {
return s.queries.UpdateLeague(ctx, dbgen.UpdateLeagueParams{
Name: l.Name,
CountryCode: pgtype.Text{String: l.CountryCode, Valid: true},
Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true},
IsActive: pgtype.Bool{Bool: false, Valid: true},
})
}

View File

@ -17,15 +17,19 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error {
return nil return nil
} }
for _, raw := range m.Odds { for _, item := range m.Odds {
var item map[string]interface{} var name string
if err := json.Unmarshal(raw, &item); err != nil { var oddsVal float64
continue
}
name := getString(item["name"]) if m.Source == "bwin" {
nameValue := getMap(item["name"])
name = getString(nameValue["value"])
oddsVal = getFloat(item["odds"])
} else {
name = getString(item["name"])
oddsVal = getConvertedFloat(item["odds"])
}
handicap := getString(item["handicap"]) handicap := getString(item["handicap"])
oddsVal := getFloat(item["odds"])
rawOddsBytes, _ := json.Marshal(m.Odds) rawOddsBytes, _ := json.Marshal(m.Odds)
@ -43,7 +47,7 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error {
Category: pgtype.Text{Valid: false}, Category: pgtype.Text{Valid: false},
RawOdds: rawOddsBytes, RawOdds: rawOddsBytes,
IsActive: pgtype.Bool{Bool: true, Valid: true}, IsActive: pgtype.Bool{Bool: true, Valid: true},
Source: pgtype.Text{String: "b365api", Valid: true}, Source: pgtype.Text{String: m.Source, Valid: true},
FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
} }
@ -85,23 +89,6 @@ func writeFailedMarketLog(m domain.Market, err error) error {
return writeErr return writeErr
} }
func getString(v interface{}) string {
if s, ok := v.(string); ok {
return s
}
return ""
}
func getFloat(v interface{}) float64 {
if s, ok := v.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return f
}
}
return 0
}
func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) {
odds, err := s.queries.GetPrematchOdds(ctx) odds, err := s.queries.GetPrematchOdds(ctx)
if err != nil { if err != nil {
@ -286,3 +273,34 @@ func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID stri
return domainOdds, nil return domainOdds, nil
} }
func getString(v interface{}) string {
if s, ok := v.(string); ok {
return s
}
return ""
}
func getConvertedFloat(v interface{}) float64 {
if s, ok := v.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return f
}
}
return 0
}
func getFloat(v interface{}) float64 {
if n, ok := v.(float64); ok {
return n
}
return 0
}
func getMap(v interface{}) map[string]interface{} {
if m, ok := v.(map[string]interface{}); ok {
return m
}
return nil
}

View File

@ -5,6 +5,7 @@ import (
"time" "time"
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/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
) )
@ -39,3 +40,12 @@ func OpenDB(url string) (*pgxpool.Pool, func(), error) {
conn.Close() conn.Close()
}, nil }, nil
} }
func (s *Store) BeginTx(ctx context.Context) (*dbgen.Queries, pgx.Tx, error) {
tx, err := s.conn.Begin(ctx)
if err != nil {
return nil, nil, err
}
q := s.queries.WithTx(tx)
return q, tx, nil
}

View File

@ -121,15 +121,11 @@ func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketI
if err != nil { if err != nil {
return domain.CreateBetOutcome{}, err return domain.CreateBetOutcome{}, err
} }
sportID, err := strconv.ParseInt(event.SportID, 10, 64)
if err != nil {
return domain.CreateBetOutcome{}, err
}
newOutcome := domain.CreateBetOutcome{ newOutcome := domain.CreateBetOutcome{
EventID: eventID, EventID: eventID,
OddID: oddID, OddID: oddID,
MarketID: marketID, MarketID: marketID,
SportID: sportID, SportID: int64(event.SportID),
HomeTeamName: event.HomeTeam, HomeTeamName: event.HomeTeam,
AwayTeamName: event.AwayTeam, AwayTeamName: event.AwayTeam,
MarketName: odds.MarketName, MarketName: odds.MarketName,
@ -287,7 +283,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
return res, nil return res, nil
} }
func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportID, HomeTeam, AwayTeam string, StartTime time.Time, numMarkets int) ([]domain.CreateBetOutcome, float32, error) { func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID string, sportID int32, HomeTeam, AwayTeam string, StartTime time.Time, numMarkets int) ([]domain.CreateBetOutcome, float32, error) {
var newOdds []domain.CreateBetOutcome var newOdds []domain.CreateBetOutcome
var totalOdds float32 = 1 var totalOdds float32 = 1
@ -337,11 +333,6 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI
s.logger.Error("Failed to parse odd", "error", err) s.logger.Error("Failed to parse odd", "error", err)
continue continue
} }
sportID, err := strconv.ParseInt(sportID, 10, 64)
if err != nil {
s.logger.Error("Failed to get sport id", "error", err)
continue
}
eventID, err := strconv.ParseInt(eventID, 10, 64) eventID, 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 get event id", "error", err)
@ -365,7 +356,7 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI
EventID: eventID, EventID: eventID,
OddID: oddID, OddID: oddID,
MarketID: marketID, MarketID: marketID,
SportID: sportID, SportID: int64(sportID),
HomeTeamName: HomeTeam, HomeTeamName: HomeTeam,
AwayTeamName: AwayTeam, AwayTeamName: AwayTeam,
MarketName: marketName, MarketName: marketName,
@ -388,7 +379,7 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI
return newOdds, totalOdds, nil return newOdds, totalOdds, nil
} }
func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, leagueID, sportID domain.ValidString, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) { func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, leagueID, sportID domain.ValidInt32, firstStartTime, lastStartTime domain.ValidTime) (domain.CreateBetRes, error) {
// Get a unexpired event id // Get a unexpired event id

View File

@ -0,0 +1,126 @@
package chapa
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type ChapaClient interface {
IssuePayment(ctx context.Context, payload domain.ChapaTransferPayload) (bool, error)
InitPayment(ctx context.Context, req domain.InitPaymentRequest) (string, error)
FetchBanks() ([]domain.ChapaSupportedBank, error)
}
type Client struct {
BaseURL string
SecretKey string
HTTPClient *http.Client
UserAgent string
}
func NewClient(baseURL, secretKey string) *Client {
return &Client{
BaseURL: baseURL,
SecretKey: secretKey,
HTTPClient: http.DefaultClient,
UserAgent: "FortuneBet/1.0",
}
}
func (c *Client) IssuePayment(ctx context.Context, payload domain.ChapaTransferPayload) (bool, error) {
payloadBytes, err := json.Marshal(payload)
if err != nil {
return false, fmt.Errorf("failed to serialize payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/transfers", bytes.NewBuffer(payloadBytes))
if err != nil {
return false, fmt.Errorf("failed to create HTTP request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.SecretKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return false, fmt.Errorf("chapa HTTP request failed: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return true, nil
}
return false, fmt.Errorf("chapa error: status %d, body: %s", resp.StatusCode, string(body))
}
// service/chapa_service.go
func (c *Client) InitPayment(ctx context.Context, req domain.InitPaymentRequest) (string, error) {
payloadBytes, err := json.Marshal(req)
if err != nil {
return "", fmt.Errorf("failed to serialize payload: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.BaseURL+"/transaction/initialize", bytes.NewBuffer(payloadBytes))
if err != nil {
return "", fmt.Errorf("failed to create HTTP request: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+c.SecretKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.HTTPClient.Do(httpReq)
if err != nil {
return "", fmt.Errorf("chapa HTTP request failed: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("chapa error: status %d, body: %s", resp.StatusCode, string(body))
}
var response struct {
Data struct {
CheckoutURL string `json:"checkout_url"`
} `json:"data"`
}
if err := json.Unmarshal(body, &response); err != nil {
return "", fmt.Errorf("failed to parse chapa response: %w", err)
}
return response.Data.CheckoutURL, nil
}
func (c *Client) FetchBanks() ([]domain.ChapaSupportedBank, error) {
req, _ := http.NewRequest("GET", c.BaseURL+"/banks", nil)
req.Header.Set("Authorization", "Bearer "+c.SecretKey)
fmt.Printf("\n\nbase URL is: %s\n\n", c.BaseURL)
res, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp struct {
Message string `json:"message"`
Data []domain.ChapaSupportedBank `json:"data"`
}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
fmt.Printf("\n\nclient fetched banks: %+v\n\n", resp.Data)
return resp.Data, nil
}

View File

@ -0,0 +1,15 @@
package chapa
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type ChapaPort interface {
HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error
HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error
WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error
DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error)
GetSupportedBanks() ([]domain.ChapaSupportedBank, error)
}

View File

@ -0,0 +1,351 @@
package chapa
import (
"context"
"database/sql"
"errors"
"fmt"
// "log/slog"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/google/uuid"
"github.com/shopspring/decimal"
)
type Service struct {
transactionStore transaction.TransactionStore
walletStore wallet.WalletStore
userStore user.UserStore
referralStore referralservice.ReferralStore
branchStore branch.BranchStore
chapaClient ChapaClient
config *config.Config
// logger *slog.Logger
store *repository.Store
}
func NewService(
txStore transaction.TransactionStore,
walletStore wallet.WalletStore,
userStore user.UserStore,
referralStore referralservice.ReferralStore,
branchStore branch.BranchStore,
chapaClient ChapaClient,
store *repository.Store,
) *Service {
return &Service{
transactionStore: txStore,
walletStore: walletStore,
userStore: userStore,
referralStore: referralStore,
branchStore: branchStore,
chapaClient: chapaClient,
store: store,
}
}
func (s *Service) HandleChapaTransferWebhook(ctx context.Context, req domain.ChapaWebHookTransfer) error {
_, tx, err := s.store.BeginTx(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
// Use your services normally (they dont use the transaction, unless you wire `q`)
referenceID, err := strconv.ParseInt(req.Reference, 10, 64)
if err != nil {
return fmt.Errorf("invalid reference ID: %w", err)
}
txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("transaction with ID %d not found", referenceID)
}
return err
}
if txn.Verified {
return nil
}
webhookAmount, _ := decimal.NewFromString(req.Amount)
storedAmount, _ := decimal.NewFromString(txn.Amount.String())
if !webhookAmount.Equal(storedAmount) {
return fmt.Errorf("amount mismatch")
}
txn.Verified = true
if err := s.transactionStore.UpdateTransactionVerified(ctx, txn.ID, txn.Verified, txn.ApprovedBy.Value, txn.ApproverName.Value); err != nil {
return err
}
return tx.Commit(ctx)
}
func (s *Service) HandleChapaPaymentWebhook(ctx context.Context, req domain.ChapaWebHookPayment) error {
_, tx, err := s.store.BeginTx(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if req.Status != "success" {
return fmt.Errorf("payment status not successful")
}
// 1. Parse reference ID
referenceID, err := strconv.ParseInt(req.TxRef, 10, 64)
if err != nil {
return fmt.Errorf("invalid tx_ref: %w", err)
}
// 2. Fetch transaction
txn, err := s.transactionStore.GetTransactionByID(ctx, referenceID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("transaction with ID %d not found", referenceID)
}
return err
}
if txn.Verified {
return nil // already processed
}
webhookAmount, _ := strconv.ParseFloat(req.Amount, 32)
if webhookAmount < float64(txn.Amount) {
return fmt.Errorf("webhook amount is less than expected")
}
// 4. Fetch wallet
wallet, err := s.walletStore.GetWalletByID(ctx, txn.ID)
if err != nil {
return err
}
// 5. Update wallet balance
newBalance := wallet.Balance + txn.Amount
if err := s.walletStore.UpdateBalance(ctx, wallet.ID, newBalance); err != nil {
return err
}
// 6. Mark transaction as verified
if err := s.transactionStore.UpdateTransactionVerified(ctx, txn.ID, true, txn.ApprovedBy.Value, txn.ApproverName.Value); err != nil {
return err
}
// 7. Check & Create Referral
stats, err := s.referralStore.GetReferralStats(ctx, strconv.FormatInt(wallet.UserID, 10))
if err != nil {
return err
}
if stats == nil {
if err := s.referralStore.CreateReferral(ctx, wallet.UserID); err != nil {
return err
}
}
return tx.Commit(ctx)
}
func (s *Service) WithdrawUsingChapa(ctx context.Context, userID int64, req domain.ChapaWithdrawRequest) error {
_, tx, err := s.store.BeginTx(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
// Get the requesting user
user, err := s.userStore.GetUserByID(ctx, userID)
if err != nil {
return fmt.Errorf("user not found: %w", err)
}
branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID)
if err != nil {
return err
}
wallets, err := s.walletStore.GetWalletsByUser(ctx, userID)
if err != nil {
return err
}
var targetWallet *domain.Wallet
for _, w := range wallets {
if w.ID == req.WalletID {
targetWallet = &w
break
}
}
if targetWallet == nil {
return fmt.Errorf("no wallet found with the specified ID")
}
if !targetWallet.IsWithdraw || !targetWallet.IsActive {
return fmt.Errorf("wallet not eligible for withdrawal")
}
if targetWallet.Balance < domain.Currency(req.Amount) {
return fmt.Errorf("insufficient balance")
}
txID := uuid.New().String()
payload := domain.ChapaTransferPayload{
AccountName: req.AccountName,
AccountNumber: req.AccountNumber,
Amount: strconv.FormatInt(req.Amount, 10),
Currency: req.Currency,
BeneficiaryName: req.BeneficiaryName,
TxRef: txID,
Reference: txID,
BankCode: req.BankCode,
}
ok, err := s.chapaClient.IssuePayment(ctx, payload)
if err != nil || !ok {
return fmt.Errorf("chapa transfer failed: %v", err)
}
// Create transaction using user and wallet info
_, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{
Amount: domain.Currency(req.Amount),
Type: domain.TransactionType(domain.TRANSACTION_CASHOUT),
ReferenceNumber: txID,
AccountName: req.AccountName,
AccountNumber: req.AccountNumber,
BankCode: req.BankCode,
BeneficiaryName: req.BeneficiaryName,
PaymentOption: domain.PaymentOption(domain.BANK),
BranchID: req.BranchID,
BranchName: branch.Name,
BranchLocation: branch.Location,
// CashierID: user.ID,
// CashierName: user.FullName,
FullName: user.FirstName + " " + user.LastName,
PhoneNumber: user.PhoneNumber,
CompanyID: branch.CompanyID,
})
if err != nil {
return fmt.Errorf("failed to create transaction: %w", err)
}
newBalance := domain.Currency(req.Amount)
err = s.walletStore.UpdateBalance(ctx, targetWallet.ID, newBalance)
if err != nil {
return fmt.Errorf("failed to update wallet balance: %w", err)
}
return tx.Commit(ctx)
}
func (s *Service) DepositUsingChapa(ctx context.Context, userID int64, req domain.ChapaDepositRequest) (string, error) {
_, tx, err := s.store.BeginTx(ctx)
if err != nil {
return "", err
}
defer tx.Rollback(ctx)
user, err := s.userStore.GetUserByID(ctx, userID)
if err != nil {
return "", err
}
branch, err := s.branchStore.GetBranchByID(ctx, req.BranchID)
if err != nil {
return "", err
}
txID := uuid.New().String()
_, err = s.transactionStore.CreateTransaction(ctx, domain.CreateTransaction{
Amount: req.Amount,
Type: domain.TransactionType(domain.TRANSACTION_DEPOSIT),
ReferenceNumber: txID,
BranchID: req.BranchID,
BranchName: branch.Name,
BranchLocation: branch.Location,
FullName: user.FirstName + " " + user.LastName,
PhoneNumber: user.PhoneNumber,
CompanyID: branch.CompanyID,
})
if err != nil {
return "", err
}
// Fetch user details for Chapa payment
userInfo, err := s.userStore.GetUserByID(ctx, userID)
if err != nil {
return "", err
}
// Build Chapa InitPaymentRequest (matches Chapa API)
paymentReq := domain.InitPaymentRequest{
Amount: req.Amount,
Currency: req.Currency,
Email: userInfo.Email,
FirstName: userInfo.FirstName,
LastName: userInfo.LastName,
TxRef: txID,
CallbackURL: s.config.CHAPA_CALLBACK_URL,
ReturnURL: s.config.CHAPA_RETURN_URL,
}
// Call Chapa to initialize payment
paymentURL, err := s.chapaClient.InitPayment(ctx, paymentReq)
if err != nil {
return "", err
}
// Commit DB transaction
if err := tx.Commit(ctx); err != nil {
return "", err
}
return paymentURL, nil
}
func (s *Service) GetSupportedBanks() ([]domain.ChapaSupportedBank, error) {
banks, err := s.chapaClient.FetchBanks()
fmt.Printf("\n\nfetched banks: %+v\n\n", banks)
if err != nil {
return nil, err
}
// Add formatting logic (same as in original controller)
for i := range banks {
if banks[i].IsMobilemoney != nil && *(banks[i].IsMobilemoney) == 1 {
banks[i].AcctNumberRegex = "/^09[0-9]{8}$/"
banks[i].ExampleValue = "0952097177"
} else {
switch banks[i].AcctLength {
case 8:
banks[i].ExampleValue = "16967608"
case 13:
banks[i].ExampleValue = "1000222215735"
case 14:
banks[i].ExampleValue = "01320089280800"
case 16:
banks[i].ExampleValue = "1000222215735123"
}
banks[i].AcctNumberRegex = formatRegex(banks[i].AcctLength)
}
}
return banks, nil
}
func formatRegex(length int) string {
return fmt.Sprintf("/^[0-9]{%d}$/", length)
}

View File

@ -47,7 +47,6 @@ func (s *service) FetchLiveEvents(ctx context.Context) error {
s.fetchLiveEvents(ctx, url.name, url.source) s.fetchLiveEvents(ctx, url.name, url.source)
}() }()
} }
wg.Wait() wg.Wait()
return nil return nil
} }
@ -75,7 +74,7 @@ func (s *service) fetchLiveEvents(ctx context.Context, url, source string) error
events := []domain.Event{} events := []domain.Event{}
switch source { switch source {
case "bet365": case "bet365":
events = handleBet365prematch(body, sportID) events = handleBet365prematch(body, sportID, source)
case "betfair": case "betfair":
events = handleBetfairprematch(body, sportID, source) events = handleBetfairprematch(body, sportID, source)
case "1xbet": case "1xbet":
@ -97,7 +96,7 @@ func (s *service) fetchLiveEvents(ctx context.Context, url, source string) error
} }
func handleBet365prematch(body []byte, sportID int) []domain.Event { func handleBet365prematch(body []byte, sportID int, source string) []domain.Event {
var data struct { var data struct {
Success int `json:"success"` Success int `json:"success"`
Results [][]map[string]interface{} `json:"results"` Results [][]map[string]interface{} `json:"results"`
@ -105,7 +104,7 @@ func handleBet365prematch(body []byte, sportID int) []domain.Event {
events := []domain.Event{} events := []domain.Event{}
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
fmt.Printf(" Decode failed for sport_id=%d\nRaw: %s\n", sportID, string(body)) fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body))
return events return events
} }
@ -117,24 +116,24 @@ func handleBet365prematch(body []byte, sportID int) []domain.Event {
event := domain.Event{ event := domain.Event{
ID: getString(ev["ID"]), ID: getString(ev["ID"]),
SportID: fmt.Sprintf("%d", sportID), SportID: int32(sportID),
MatchName: getString(ev["NA"]), MatchName: getString(ev["NA"]),
Score: getString(ev["SS"]), Score: getString(ev["SS"]),
MatchMinute: getInt(ev["TM"]), MatchMinute: getInt(ev["TM"]),
TimerStatus: getString(ev["TT"]), TimerStatus: getString(ev["TT"]),
HomeTeamID: getString(ev["HT"]), HomeTeamID: getInt32(ev["HT"]),
AwayTeamID: getString(ev["AT"]), AwayTeamID: getInt32(ev["AT"]),
HomeKitImage: getString(ev["K1"]), HomeKitImage: getString(ev["K1"]),
AwayKitImage: getString(ev["K2"]), AwayKitImage: getString(ev["K2"]),
LeagueName: getString(ev["CT"]), LeagueName: getString(ev["CT"]),
LeagueID: getString(ev["C2"]), LeagueID: getInt32(ev["C2"]),
LeagueCC: getString(ev["CB"]), LeagueCC: getString(ev["CB"]),
StartTime: time.Now().UTC().Format(time.RFC3339), StartTime: time.Now().UTC().Format(time.RFC3339),
IsLive: true, IsLive: true,
Status: "live", Status: "live",
MatchPeriod: getInt(ev["MD"]), MatchPeriod: getInt(ev["MD"]),
AddedTime: getInt(ev["TA"]), AddedTime: getInt(ev["TA"]),
Source: "bet365", Source: source,
} }
events = append(events, event) events = append(events, event)
@ -152,23 +151,20 @@ func handleBetfairprematch(body []byte, sportID int, source string) []domain.Eve
events := []domain.Event{} events := []domain.Event{}
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
fmt.Printf(" Decode failed for sport_id=%d\nRaw: %s\n", sportID, string(body)) fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body))
return events return events
} }
for _, ev := range data.Results { for _, ev := range data.Results {
homeRaw, _ := ev["home"].(map[string]interface{}) homeRaw, _ := ev["home"].(map[string]interface{})
homeId, _ := homeRaw["id"].(string)
awayRaw, _ := ev["home"].(map[string]interface{}) awayRaw, _ := ev["home"].(map[string]interface{})
awayId, _ := awayRaw["id"].(string)
event := domain.Event{ event := domain.Event{
ID: getString(ev["id"]), ID: getString(ev["id"]),
SportID: fmt.Sprintf("%d", sportID), SportID: int32(sportID),
TimerStatus: getString(ev["time_status"]), TimerStatus: getString(ev["time_status"]),
HomeTeamID: homeId, HomeTeamID: getInt32(homeRaw["id"]),
AwayTeamID: awayId, AwayTeamID: getInt32(awayRaw["id"]),
StartTime: time.Now().UTC().Format(time.RFC3339), StartTime: time.Now().UTC().Format(time.RFC3339),
IsLive: true, IsLive: true,
Status: "live", Status: "live",
@ -221,8 +217,8 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
log.Printf("Sport ID %d", sportID) log.Printf("Sport ID %d", sportID)
for page <= totalPages { for page <= totalPages {
page = page + 1 page = page + 1
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", sportID, s.token, page) url := fmt.Sprintf(url, sportID, s.token, page)
log.Printf("📡 Fetching data for sport %d at page %d", sportID, page) log.Printf("📡 Fetching data from %s - sport %d, for event data page %d", source, sportID, page)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
log.Printf("❌ Failed to fetch event data for page %d: %v", page, err) log.Printf("❌ Failed to fetch event data for page %d: %v", page, err)
@ -246,33 +242,35 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
// continue // continue
// } // }
// leagueID, err := strconv.ParseInt(ev.League.ID, 10, 64) leagueID, err := strconv.ParseInt(ev.League.ID, 10, 64)
// if err != nil { if err != nil {
// log.Printf("❌ Invalid league id, leagueID %v", ev.League.ID) log.Printf("❌ Invalid league id, leagueID %v", ev.League.ID)
// continue continue
// } }
// if !slices.Contains(domain.SupportedLeagues, leagueID) { // doesn't make sense to save and check back to back, but for now it can be here
// // fmt.Printf("⚠️ Skipping league %s (%d) as it is not supported\n", ev.League.Name, leagueID) s.store.SaveLeague(ctx, domain.League{
// _, err = fmt.Fprintf(b, "Skipped league %s (%d) in sport %d\n", ev.League.Name, leagueID, sportID) ID: leagueID,
// if err != nil { Name: ev.League.Name,
// fmt.Printf(" Error while logging skipped league") IsActive: true,
// } })
// skippedLeague = append(skippedLeague, ev.League.Name)
// continue if supported, err := s.store.CheckLeagueSupport(ctx, leagueID); !supported || err != nil {
// } skippedLeague = append(skippedLeague, ev.League.Name)
continue
}
event := domain.UpcomingEvent{ event := domain.UpcomingEvent{
ID: ev.ID, ID: ev.ID,
SportID: ev.SportID, SportID: convertInt32(ev.SportID),
MatchName: "", MatchName: "",
HomeTeam: ev.Home.Name, HomeTeam: ev.Home.Name,
AwayTeam: "", // handle nil safely AwayTeam: "", // handle nil safely
HomeTeamID: ev.Home.ID, HomeTeamID: convertInt32(ev.Home.ID),
AwayTeamID: "", AwayTeamID: 0,
HomeKitImage: "", HomeKitImage: "",
AwayKitImage: "", AwayKitImage: "",
LeagueID: ev.League.ID, LeagueID: convertInt32(ev.League.ID),
LeagueName: ev.League.Name, LeagueName: ev.League.Name,
LeagueCC: "", LeagueCC: "",
StartTime: time.Unix(startUnix, 0).UTC(), StartTime: time.Unix(startUnix, 0).UTC(),
@ -281,7 +279,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
if ev.Away != nil { if ev.Away != nil {
event.AwayTeam = ev.Away.Name event.AwayTeam = ev.Away.Name
event.AwayTeamID = ev.Away.ID event.AwayTeamID = convertInt32(ev.Away.ID)
event.MatchName = ev.Home.Name + " vs " + ev.Away.Name event.MatchName = ev.Home.Name + " vs " + ev.Away.Name
} }
@ -319,6 +317,20 @@ func getInt(v interface{}) int {
} }
return 0 return 0
} }
func getInt32(v interface{}) int32 {
if n, err := strconv.Atoi(getString(v)); err == nil {
return int32(n)
}
return 0
}
func convertInt32(num string) int32 {
if n, err := strconv.Atoi(num); err == nil {
return int32(n)
}
return 0
}
func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) {
return s.store.GetAllUpcomingEvents(ctx) return s.store.GetAllUpcomingEvents(ctx)
} }

View File

@ -0,0 +1,12 @@
package league
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type Service interface {
GetAllLeagues(ctx context.Context) ([]domain.League, error)
SetLeagueActive(ctx context.Context, leagueId int64) error
}

View File

@ -0,0 +1,26 @@
package league
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
)
type service struct {
store *repository.Store
}
func New(store *repository.Store) Service {
return &service{
store: store,
}
}
func (s *service) GetAllLeagues(ctx context.Context) ([]domain.League, error) {
return s.store.GetAllLeagues(ctx)
}
func (s *service) SetLeagueActive(ctx context.Context, leagueId int64) error {
return s.store.SetLeagueActive(ctx, leagueId)
}

View File

@ -10,6 +10,7 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/config"
@ -35,6 +36,38 @@ func New(store *repository.Store, cfg *config.Config, logger *slog.Logger) *Serv
// TODO Add the optimization to get 10 events at the same time // TODO Add the optimization to get 10 events at the same time
func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
var wg sync.WaitGroup
errChan := make(chan error, 2)
// wg.Add(2)
wg.Add(1)
// go func() {
// defer wg.Done()
// if err := s.fetchBet365Odds(ctx); err != nil {
// errChan <- fmt.Errorf("bet365 odds fetching error: %w", err)
// }
// }()
go func() {
defer wg.Done()
if err := s.fetchBwinOdds(ctx); err != nil {
errChan <- fmt.Errorf("bwin odds fetching error: %w", err)
}
}()
var errs []error
for err := range errChan {
errs = append(errs, err)
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error {
eventIDs, err := s.store.GetAllUpcomingEvents(ctx) eventIDs, err := s.store.GetAllUpcomingEvents(ctx)
if err != nil { if err != nil {
log.Printf("❌ Failed to fetch upcoming event IDs: %v", err) log.Printf("❌ Failed to fetch upcoming event IDs: %v", err)
@ -95,6 +128,180 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
return errors.Join(errs...) return errors.Join(errs...)
} }
func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error {
// getting odds for a specific event is not possible for bwin, most specific we can get is fetch odds on a single sport
// so instead of having event and odds fetched separetly event will also be fetched along with the odds
sportIds := []int{4, 12, 7}
for _, sportId := range sportIds {
url := fmt.Sprintf("https://api.b365api.com/v1/bwin/prematch?sport_id=%d&token=%s", sportId, s.config.Bet365Token)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
log.Printf("❌ Failed to create request for sportId %d: %v", sportId, err)
continue
}
resp, err := s.client.Do(req)
if err != nil {
log.Printf("❌ Failed to fetch request for sportId %d: %v", sportId, err)
continue
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("❌ Failed to read response body for sportId %d: %v", sportId, err)
continue
}
var data struct {
Success int `json:"success"`
Results []map[string]interface{} `json:"results"`
}
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
fmt.Printf("Decode failed for sport_id=%d\nRaw: %s\n", sportId, string(body))
continue
}
for _, res := range data.Results {
if getInt(res["Id"]) == -1 {
continue
}
event := domain.Event{
ID: strconv.Itoa(getInt(res["Id"])),
SportID: int32(getInt(res["SportId"])),
LeagueID: int32(getInt(res["LeagueId"])),
LeagueName: getString(res["Leaguename"]),
HomeTeam: getString(res["HomeTeam"]),
HomeTeamID: int32(getInt(res["HomeTeamId"])),
AwayTeam: getString(res["AwayTeam"]),
AwayTeamID: int32(getInt(res["AwayTeamId"])),
StartTime: time.Now().UTC().Format(time.RFC3339),
TimerStatus: "1",
IsLive: true,
Status: "live",
Source: "bwin",
}
if err := s.store.SaveEvent(ctx, event); err != nil {
fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err)
continue
}
for _, market := range []string{"Markets, optionMarkets"} {
for _, m := range getMapArray(res[market]) {
name := getMap(m["name"])
marketName := getString(name["value"])
market := domain.Market{
EventID: event.ID,
MarketID: getString(m["id"]),
MarketCategory: getString(m["category"]),
MarketName: marketName,
Source: "bwin",
}
results := getMapArray(m["results"])
market.Odds = results
s.store.SaveNonLiveMarket(ctx, market)
}
}
}
}
return nil
}
func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error {
// getting odds for a specific event is not possible for bwin, most specific we can get is fetch odds on a single sport
// so instead of having event and odds fetched separetly event will also be fetched along with the odds
sportIds := []int{4, 12, 7}
for _, sportId := range sportIds {
url := fmt.Sprintf("https://api.b365api.com/v1/bwin/prematch?sport_id=%d&token=%s", sportId, s.config.Bet365Token)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
log.Printf("❌ Failed to create request for sportId %d: %v", sportId, err)
continue
}
resp, err := s.client.Do(req)
if err != nil {
log.Printf("❌ Failed to fetch request for sportId %d: %v", sportId, err)
continue
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("❌ Failed to read response body for sportId %d: %v", sportId, err)
continue
}
var data struct {
Success int `json:"success"`
Results []map[string]interface{} `json:"results"`
}
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
fmt.Printf("Decode failed for sport_id=%d\nRaw: %s\n", sportId, string(body))
continue
}
for _, res := range data.Results {
if getInt(res["Id"]) == -1 {
continue
}
event := domain.Event{
ID: strconv.Itoa(getInt(res["Id"])),
SportID: int32(getInt(res["SportId"])),
LeagueID: int32(getInt(res["LeagueId"])),
LeagueName: getString(res["Leaguename"]),
HomeTeam: getString(res["HomeTeam"]),
HomeTeamID: int32(getInt(res["HomeTeamId"])),
AwayTeam: getString(res["AwayTeam"]),
AwayTeamID: int32(getInt(res["AwayTeamId"])),
StartTime: time.Now().UTC().Format(time.RFC3339),
TimerStatus: "1",
IsLive: true,
Status: "live",
Source: "bwin",
}
if err := s.store.SaveEvent(ctx, event); err != nil {
fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err)
continue
}
for _, market := range []string{"Markets, optionMarkets"} {
for _, m := range getMapArray(res[market]) {
name := getMap(m["name"])
marketName := getString(name["value"])
market := domain.Market{
EventID: event.ID,
MarketID: getString(m["id"]),
MarketCategory: getString(m["category"]),
MarketName: marketName,
Source: "bwin",
}
results := getMapArray(m["results"])
market.Odds = results
s.store.SaveNonLiveMarket(ctx, market)
}
}
}
}
return nil
}
func (s *ServiceImpl) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr string) (domain.BaseNonLiveOddResponse, error) { func (s *ServiceImpl) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr string) (domain.BaseNonLiveOddResponse, error) {
eventID, err := strconv.ParseInt(eventIDStr, 10, 64) eventID, err := strconv.ParseInt(eventIDStr, 10, 64)
@ -336,6 +543,13 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName
continue continue
} }
marketOdds, err := convertRawMessage(market.Odds)
if err != nil {
s.logger.Error("failed to conver json.RawMessage to []map[string]interface{} for market_id: ", market.ID)
errs = append(errs, err)
continue
}
marketRecord := domain.Market{ marketRecord := domain.Market{
EventID: eventID, EventID: eventID,
FI: fi, FI: fi,
@ -344,7 +558,9 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName
MarketName: market.Name, MarketName: market.Name,
MarketID: marketIDstr, MarketID: marketIDstr,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
Odds: market.Odds, Odds: marketOdds,
// bwin won't reach this code so bet365 is hardcoded for now
Source: "bet365",
} }
err = s.store.SaveNonLiveMarket(ctx, marketRecord) err = s.store.SaveNonLiveMarket(ctx, marketRecord)
@ -385,3 +601,49 @@ func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingI
func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset domain.ValidInt64) ([]domain.Odd, error) { func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset domain.ValidInt64) ([]domain.Odd, error) {
return s.store.GetPaginatedPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) return s.store.GetPaginatedPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset)
} }
func getString(v interface{}) string {
if str, ok := v.(string); ok {
return str
}
return ""
}
func getInt(v interface{}) int {
if n, ok := v.(float64); ok {
return int(n)
}
return -1
}
func getMap(v interface{}) map[string]interface{} {
if m, ok := v.(map[string]interface{}); ok {
return m
}
return nil
}
func getMapArray(v interface{}) []map[string]interface{} {
result := []map[string]interface{}{}
if arr, ok := v.([]interface{}); ok {
for _, item := range arr {
if m, ok := item.(map[string]interface{}); ok {
result = append(result, m)
}
}
}
return result
}
func convertRawMessage(rawMessages []json.RawMessage) ([]map[string]interface{}, error) {
var result []map[string]interface{}
for _, raw := range rawMessages {
var m map[string]interface{}
if err := json.Unmarshal(raw, &m); err != nil {
return nil, err
}
result = append(result, m)
}
return result, nil
}

View File

@ -8,7 +8,7 @@ import (
) )
// NFL evaluations // NFL evaluations
func evaluateNFLMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { func EvaluateNFLMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader { switch outcome.OddHeader {
case "1": case "1":
if score.Home > score.Away { if score.Home > score.Away {
@ -25,7 +25,7 @@ func evaluateNFLMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away in
} }
} }
func evaluateNFLSpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { func EvaluateNFLSpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
handicap, err := strconv.ParseFloat(outcome.OddHandicap, 64) handicap, err := strconv.ParseFloat(outcome.OddHandicap, 64)
if err != nil { if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap)
@ -56,7 +56,7 @@ func evaluateNFLSpread(outcome domain.BetOutcome, score struct{ Home, Away int }
return domain.OUTCOME_STATUS_VOID, nil return domain.OUTCOME_STATUS_VOID, nil
} }
func evaluateNFLTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { func EvaluateNFLTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalPoints := float64(score.Home + score.Away) totalPoints := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64) threshold, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil { if err != nil {
@ -81,8 +81,8 @@ func evaluateNFLTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
} }
// evaluateRugbyMoneyLine evaluates Rugby money line bets // EvaluateRugbyMoneyLine Evaluates Rugby money line bets
func evaluateRugbyMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { func EvaluateRugbyMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader { switch outcome.OddHeader {
case "1": case "1":
if score.Home > score.Away { if score.Home > score.Away {
@ -99,8 +99,8 @@ func evaluateRugbyMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away
} }
} }
// evaluateRugbySpread evaluates Rugby spread bets // EvaluateRugbySpread Evaluates Rugby spread bets
func evaluateRugbySpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { func EvaluateRugbySpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
handicap, err := strconv.ParseFloat(outcome.OddHandicap, 64) handicap, err := strconv.ParseFloat(outcome.OddHandicap, 64)
if err != nil { if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap)
@ -131,8 +131,8 @@ func evaluateRugbySpread(outcome domain.BetOutcome, score struct{ Home, Away int
return domain.OUTCOME_STATUS_VOID, nil return domain.OUTCOME_STATUS_VOID, nil
} }
// evaluateRugbyTotalPoints evaluates Rugby total points bets // EvaluateRugbyTotalPoints Evaluates Rugby total points bets
func evaluateRugbyTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { func EvaluateRugbyTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalPoints := float64(score.Home + score.Away) totalPoints := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64) threshold, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil { if err != nil {
@ -157,8 +157,8 @@ func evaluateRugbyTotalPoints(outcome domain.BetOutcome, score struct{ Home, Awa
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
} }
// evaluateBaseballMoneyLine evaluates Baseball money line bets // EvaluateBaseballMoneyLine Evaluates Baseball money line bets
func evaluateBaseballMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { func EvaluateBaseballMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader { switch outcome.OddHeader {
case "1": case "1":
if score.Home > score.Away { if score.Home > score.Away {
@ -175,8 +175,8 @@ func evaluateBaseballMoneyLine(outcome domain.BetOutcome, score struct{ Home, Aw
} }
} }
// evaluateBaseballSpread evaluates Baseball spread bets // EvaluateBaseballSpread Evaluates Baseball spread bets
func evaluateBaseballSpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { func EvaluateBaseballSpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
handicap, err := strconv.ParseFloat(outcome.OddHandicap, 64) handicap, err := strconv.ParseFloat(outcome.OddHandicap, 64)
if err != nil { if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap)
@ -207,8 +207,8 @@ func evaluateBaseballSpread(outcome domain.BetOutcome, score struct{ Home, Away
return domain.OUTCOME_STATUS_VOID, nil return domain.OUTCOME_STATUS_VOID, nil
} }
// evaluateBaseballTotalRuns evaluates Baseball total runs bets // EvaluateBaseballTotalRuns Evaluates Baseball total runs bets
func evaluateBaseballTotalRuns(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { func EvaluateBaseballTotalRuns(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalRuns := float64(score.Home + score.Away) totalRuns := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64) threshold, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil { if err != nil {
@ -233,8 +233,8 @@ func evaluateBaseballTotalRuns(outcome domain.BetOutcome, score struct{ Home, Aw
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
} }
// evaluateBaseballFirstInning evaluates Baseball first inning bets // EvaluateBaseballFirstInning Evaluates Baseball first inning bets
func evaluateBaseballFirstInning(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { func EvaluateBaseballFirstInning(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader { switch outcome.OddHeader {
case "1": case "1":
if score.Home > score.Away { if score.Home > score.Away {
@ -256,8 +256,8 @@ func evaluateBaseballFirstInning(outcome domain.BetOutcome, score struct{ Home,
} }
} }
// evaluateBaseballFirst5Innings evaluates Baseball first 5 innings bets // EvaluateBaseballFirst5Innings Evaluates Baseball first 5 innings bets
func evaluateBaseballFirst5Innings(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { func EvaluateBaseballFirst5Innings(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader { switch outcome.OddHeader {
case "1": case "1":
if score.Home > score.Away { if score.Home > score.Away {

View File

@ -29,75 +29,75 @@ func TestNFLMarkets(t *testing.T) {
case int64(domain.AMERICAN_FOOTBALL_MONEY_LINE): case int64(domain.AMERICAN_FOOTBALL_MONEY_LINE):
// Home win, away win, draw, and invalid OddHeader for Money Line // Home win, away win, draw, and invalid OddHeader for Money Line
t.Run("Home Win", func(t *testing.T) { t.Run("Home Win", func(t *testing.T) {
status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 21, Away: 14}) status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 21, Away: 14})
t.Logf("Market: %s, Scenario: Home Win", m.name) t.Logf("Market: %s, Scenario: Home Win", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Away Win", func(t *testing.T) { t.Run("Away Win", func(t *testing.T) {
status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 14, Away: 21}) status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 14, Away: 21})
t.Logf("Market: %s, Scenario: Away Win", m.name) t.Logf("Market: %s, Scenario: Away Win", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Draw", func(t *testing.T) { t.Run("Draw", func(t *testing.T) {
status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 17, Away: 17}) status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 17, Away: 17})
t.Logf("Market: %s, Scenario: Draw", m.name) t.Logf("Market: %s, Scenario: Draw", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status) assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status)
}) })
t.Run("Invalid OddHeader", func(t *testing.T) { t.Run("Invalid OddHeader", func(t *testing.T) {
status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7})
t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name) t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
}) })
case int64(domain.AMERICAN_FOOTBALL_SPREAD): case int64(domain.AMERICAN_FOOTBALL_SPREAD):
t.Run("Home Win with Handicap", func(t *testing.T) { t.Run("Home Win with Handicap", func(t *testing.T) {
status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-3.5"}, struct{ Home, Away int }{Home: 24, Away: 20}) status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-3.5"}, struct{ Home, Away int }{Home: 24, Away: 20})
t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name) t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Away Win with Handicap", func(t *testing.T) { t.Run("Away Win with Handicap", func(t *testing.T) {
status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+3.5"}, struct{ Home, Away int }{Home: 20, Away: 24}) status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+3.5"}, struct{ Home, Away int }{Home: 20, Away: 24})
t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name) t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Push (Void)", func(t *testing.T) { t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21}) status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21})
t.Logf("Market: %s, Scenario: Push (Void)", m.name) t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
}) })
t.Run("Non-numeric Handicap", func(t *testing.T) { t.Run("Non-numeric Handicap", func(t *testing.T) {
status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14}) status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14})
t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name) t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
}) })
case int64(domain.AMERICAN_FOOTBALL_TOTAL_POINTS): case int64(domain.AMERICAN_FOOTBALL_TOTAL_POINTS):
t.Run("Over Win", func(t *testing.T) { t.Run("Over Win", func(t *testing.T) {
status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "44.5"}, struct{ Home, Away int }{Home: 30, Away: 20}) status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "44.5"}, struct{ Home, Away int }{Home: 30, Away: 20})
t.Logf("Market: %s, Scenario: Over Win", m.name) t.Logf("Market: %s, Scenario: Over Win", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Under Win", func(t *testing.T) { t.Run("Under Win", func(t *testing.T) {
status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "44.5"}, struct{ Home, Away int }{Home: 20, Away: 17}) status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "44.5"}, struct{ Home, Away int }{Home: 20, Away: 17})
t.Logf("Market: %s, Scenario: Under Win", m.name) t.Logf("Market: %s, Scenario: Under Win", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Push (Void)", func(t *testing.T) { t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "37"}, struct{ Home, Away int }{Home: 20, Away: 17}) status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "37"}, struct{ Home, Away int }{Home: 20, Away: 17})
t.Logf("Market: %s, Scenario: Push (Void)", m.name) t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
}) })
t.Run("Non-numeric OddName", func(t *testing.T) { t.Run("Non-numeric OddName", func(t *testing.T) {
status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 17}) status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 17})
t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name) t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
@ -128,75 +128,75 @@ func TestRugbyMarkets(t *testing.T) {
case int64(domain.RUGBY_MONEY_LINE): case int64(domain.RUGBY_MONEY_LINE):
// Home win, away win, draw, and invalid OddHeader for Money Line // Home win, away win, draw, and invalid OddHeader for Money Line
t.Run("Home Win", func(t *testing.T) { t.Run("Home Win", func(t *testing.T) {
status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 30, Away: 20}) status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 30, Away: 20})
t.Logf("Market: %s, Scenario: Home Win", m.name) t.Logf("Market: %s, Scenario: Home Win", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Away Win", func(t *testing.T) { t.Run("Away Win", func(t *testing.T) {
status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 20, Away: 30}) status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 20, Away: 30})
t.Logf("Market: %s, Scenario: Away Win", m.name) t.Logf("Market: %s, Scenario: Away Win", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Draw", func(t *testing.T) { t.Run("Draw", func(t *testing.T) {
status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 25, Away: 25}) status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 25, Away: 25})
t.Logf("Market: %s, Scenario: Draw", m.name) t.Logf("Market: %s, Scenario: Draw", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status) assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status)
}) })
t.Run("Invalid OddHeader", func(t *testing.T) { t.Run("Invalid OddHeader", func(t *testing.T) {
status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7})
t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name) t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
}) })
case int64(domain.RUGBY_SPREAD), int64(domain.RUGBY_HANDICAP): case int64(domain.RUGBY_SPREAD), int64(domain.RUGBY_HANDICAP):
t.Run("Home Win with Handicap", func(t *testing.T) { t.Run("Home Win with Handicap", func(t *testing.T) {
status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-6.5"}, struct{ Home, Away int }{Home: 28, Away: 20}) status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-6.5"}, struct{ Home, Away int }{Home: 28, Away: 20})
t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name) t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Away Win with Handicap", func(t *testing.T) { t.Run("Away Win with Handicap", func(t *testing.T) {
status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+6.5"}, struct{ Home, Away int }{Home: 20, Away: 28}) status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+6.5"}, struct{ Home, Away int }{Home: 20, Away: 28})
t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name) t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Push (Void)", func(t *testing.T) { t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21}) status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21})
t.Logf("Market: %s, Scenario: Push (Void)", m.name) t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
}) })
t.Run("Non-numeric Handicap", func(t *testing.T) { t.Run("Non-numeric Handicap", func(t *testing.T) {
status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14}) status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14})
t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name) t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
}) })
case int64(domain.RUGBY_TOTAL_POINTS): case int64(domain.RUGBY_TOTAL_POINTS):
t.Run("Over Win", func(t *testing.T) { t.Run("Over Win", func(t *testing.T) {
status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "40.5"}, struct{ Home, Away int }{Home: 25, Away: 20}) status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "40.5"}, struct{ Home, Away int }{Home: 25, Away: 20})
t.Logf("Market: %s, Scenario: Over Win", m.name) t.Logf("Market: %s, Scenario: Over Win", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Under Win", func(t *testing.T) { t.Run("Under Win", func(t *testing.T) {
status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "40.5"}, struct{ Home, Away int }{Home: 15, Away: 20}) status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "40.5"}, struct{ Home, Away int }{Home: 15, Away: 20})
t.Logf("Market: %s, Scenario: Under Win", m.name) t.Logf("Market: %s, Scenario: Under Win", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Push (Void)", func(t *testing.T) { t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "35"}, struct{ Home, Away int }{Home: 20, Away: 15}) status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "35"}, struct{ Home, Away int }{Home: 20, Away: 15})
t.Logf("Market: %s, Scenario: Push (Void)", m.name) t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
}) })
t.Run("Non-numeric OddName", func(t *testing.T) { t.Run("Non-numeric OddName", func(t *testing.T) {
status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 15}) status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 15})
t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name) t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
@ -226,75 +226,75 @@ func TestBaseballMarkets(t *testing.T) {
case int64(domain.BASEBALL_MONEY_LINE): case int64(domain.BASEBALL_MONEY_LINE):
// Home win, away win, draw, and invalid OddHeader for Money Line // Home win, away win, draw, and invalid OddHeader for Money Line
t.Run("Home Win", func(t *testing.T) { t.Run("Home Win", func(t *testing.T) {
status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 6, Away: 3}) status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 6, Away: 3})
t.Logf("Market: %s, Scenario: Home Win", m.name) t.Logf("Market: %s, Scenario: Home Win", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Away Win", func(t *testing.T) { t.Run("Away Win", func(t *testing.T) {
status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 2, Away: 5}) status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 2, Away: 5})
t.Logf("Market: %s, Scenario: Away Win", m.name) t.Logf("Market: %s, Scenario: Away Win", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Draw", func(t *testing.T) { t.Run("Draw", func(t *testing.T) {
status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 4, Away: 4}) status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 4, Away: 4})
t.Logf("Market: %s, Scenario: Draw", m.name) t.Logf("Market: %s, Scenario: Draw", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status) assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status)
}) })
t.Run("Invalid OddHeader", func(t *testing.T) { t.Run("Invalid OddHeader", func(t *testing.T) {
status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7}) status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7})
t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name) t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
}) })
case int64(domain.BASEBALL_SPREAD): case int64(domain.BASEBALL_SPREAD):
t.Run("Home Win with Handicap", func(t *testing.T) { t.Run("Home Win with Handicap", func(t *testing.T) {
status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-1.5"}, struct{ Home, Away int }{Home: 5, Away: 3}) status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-1.5"}, struct{ Home, Away int }{Home: 5, Away: 3})
t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name) t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Away Win with Handicap", func(t *testing.T) { t.Run("Away Win with Handicap", func(t *testing.T) {
status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+1.5"}, struct{ Home, Away int }{Home: 3, Away: 5}) status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+1.5"}, struct{ Home, Away int }{Home: 3, Away: 5})
t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name) t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Push (Void)", func(t *testing.T) { t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 4, Away: 4}) status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 4, Away: 4})
t.Logf("Market: %s, Scenario: Push (Void)", m.name) t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
}) })
t.Run("Non-numeric Handicap", func(t *testing.T) { t.Run("Non-numeric Handicap", func(t *testing.T) {
status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 5, Away: 3}) status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 5, Away: 3})
t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name) t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
}) })
case int64(domain.BASEBALL_TOTAL_RUNS): case int64(domain.BASEBALL_TOTAL_RUNS):
t.Run("Over Win", func(t *testing.T) { t.Run("Over Win", func(t *testing.T) {
status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7.5"}, struct{ Home, Away int }{Home: 5, Away: 4}) status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7.5"}, struct{ Home, Away int }{Home: 5, Away: 4})
t.Logf("Market: %s, Scenario: Over Win", m.name) t.Logf("Market: %s, Scenario: Over Win", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Under Win", func(t *testing.T) { t.Run("Under Win", func(t *testing.T) {
status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "7.5"}, struct{ Home, Away int }{Home: 2, Away: 3}) status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "7.5"}, struct{ Home, Away int }{Home: 2, Away: 3})
t.Logf("Market: %s, Scenario: Under Win", m.name) t.Logf("Market: %s, Scenario: Under Win", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status) assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
}) })
t.Run("Push (Void)", func(t *testing.T) { t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7"}, struct{ Home, Away int }{Home: 4, Away: 3}) status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7"}, struct{ Home, Away int }{Home: 4, Away: 3})
t.Logf("Market: %s, Scenario: Push (Void)", m.name) t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status) assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
}) })
t.Run("Non-numeric OddName", func(t *testing.T) { t.Run("Non-numeric OddName", func(t *testing.T) {
status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 4, Away: 3}) status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 4, Away: 3})
t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name) t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name)
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status) assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
@ -338,7 +338,7 @@ func TestEvaluateFootballOutcome(t *testing.T) {
} }
// Act // Act
status, _ := service.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, events) status, _ := service.EvaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, events)
fmt.Printf("\n\nBet Outcome: %v\n\n", &status) fmt.Printf("\n\nBet Outcome: %v\n\n", &status)
@ -357,7 +357,7 @@ func TestEvaluateTotalLegs(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateTotalLegs(tt.outcome, tt.score) status, _ := EvaluateTotalLegs(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -380,7 +380,7 @@ func TestEvaluateGameLines(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateGameLines(tt.outcome, tt.score) status, _ := EvaluateGameLines(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -408,7 +408,7 @@ func TestEvaluateFirstTeamToScore(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateFirstTeamToScore(tt.outcome, tt.events) status, _ := EvaluateFirstTeamToScore(tt.outcome, tt.events)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -430,7 +430,7 @@ func TestEvaluateGoalsOverUnder(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateGoalsOverUnder(tt.outcome, tt.score) status, _ := EvaluateGoalsOverUnder(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -452,7 +452,7 @@ func TestEvaluateGoalsOddEven(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateGoalsOddEven(tt.outcome, tt.score) status, _ := EvaluateGoalsOddEven(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -474,7 +474,7 @@ func TestEvaluateCorrectScore(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateCorrectScore(tt.outcome, tt.score) status, _ := EvaluateCorrectScore(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -498,7 +498,7 @@ func TestEvaluateHighestScoringHalf(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateHighestScoringHalf(tt.outcome, tt.firstScore, tt.secondScore) status, _ := EvaluateHighestScoringHalf(tt.outcome, tt.firstScore, tt.secondScore)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -551,7 +551,7 @@ func TestEvaluateHighestScoringQuarter(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateHighestScoringQuarter(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore, tt.fourthScore) status, _ := EvaluateHighestScoringQuarter(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore, tt.fourthScore)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -577,7 +577,7 @@ func TestEvaluateWinningMargin(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateWinningMargin(tt.outcome, tt.score) status, _ := EvaluateWinningMargin(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -605,7 +605,7 @@ func TestEvaluateDoubleResult(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateDoubleResult(tt.outcome, tt.firstHalfScore, tt.fullTimeScore) status, _ := EvaluateDoubleResult(tt.outcome, tt.firstHalfScore, tt.fullTimeScore)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -632,7 +632,7 @@ func TestEvaluateHighestScoringPeriod(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateHighestScoringPeriod(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore) status, _ := EvaluateHighestScoringPeriod(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -656,7 +656,7 @@ func TestEvalauteTiedAfterRegulation(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateTiedAfterRegulation(tt.outcome, tt.score) status, _ := EvaluateTiedAfterRegulation(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -680,7 +680,7 @@ func TestEvaluateTeamTotal(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateTeamTotal(tt.outcome, tt.score) status, _ := EvaluateTeamTotal(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -703,7 +703,7 @@ func TestDrawNoBet(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateDrawNoBet(tt.outcome, tt.score) status, _ := EvaluateDrawNoBet(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -727,7 +727,7 @@ func TestEvaluateMoneyLine(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateMoneyLine(tt.outcome, tt.score) status, _ := EvaluateMoneyLine(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -751,7 +751,7 @@ func TestEvaluateDoubleChance(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateDoubleChance(tt.outcome, tt.score) status, _ := EvaluateDoubleChance(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -775,7 +775,7 @@ func TestEvaluateResultAndTotal(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateResultAndTotal(tt.outcome, tt.score) status, _ := EvaluateResultAndTotal(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -826,7 +826,7 @@ func TestEvaluateBTTSX(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateBTTSX(tt.outcome, tt.score) status, _ := EvaluateBTTSX(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -852,7 +852,7 @@ func TestEvaluateResultAndBTTSX(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateResultAndBTTSX(tt.outcome, tt.score) status, _ := EvaluateResultAndBTTSX(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -876,7 +876,7 @@ func TestEvaluateMoneyLine3Way(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateMoneyLine3Way(tt.outcome, tt.score) status, _ := EvaluateMoneyLine3Way(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -937,7 +937,7 @@ func TestEvaluateAsianHandicap(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateAsianHandicap(tt.outcome, tt.score) status, _ := EvaluateAsianHandicap(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }
@ -971,7 +971,7 @@ func TestEvaluateHandicapAndTotal(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateHandicapAndTotal(tt.outcome, tt.score) status, _ := EvaluateHandicapAndTotal(tt.outcome, tt.score)
if status != tt.expected { if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status) t.Errorf("expected %d, got %d", tt.expected, status)
} }

View File

@ -8,8 +8,10 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
"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"
@ -44,6 +46,7 @@ type App struct {
userSvc *user.Service userSvc *user.Service
betSvc *bet.Service betSvc *bet.Service
virtualGameSvc virtualgameservice.VirtualGameService virtualGameSvc virtualgameservice.VirtualGameService
chapaSvc *chapa.Service
walletSvc *wallet.Service walletSvc *wallet.Service
transactionSvc *transaction.Service transactionSvc *transaction.Service
ticketSvc *ticket.Service ticketSvc *ticket.Service
@ -54,6 +57,7 @@ type App struct {
Logger *slog.Logger Logger *slog.Logger
prematchSvc *odds.ServiceImpl prematchSvc *odds.ServiceImpl
eventSvc event.Service eventSvc event.Service
leagueSvc league.Service
resultSvc *result.Service resultSvc *result.Service
} }
@ -65,6 +69,7 @@ func NewApp(
userSvc *user.Service, userSvc *user.Service,
ticketSvc *ticket.Service, ticketSvc *ticket.Service,
betSvc *bet.Service, betSvc *bet.Service,
chapaSvc *chapa.Service,
walletSvc *wallet.Service, walletSvc *wallet.Service,
transactionSvc *transaction.Service, transactionSvc *transaction.Service,
branchSvc *branch.Service, branchSvc *branch.Service,
@ -72,6 +77,7 @@ func NewApp(
notidicationStore *notificationservice.Service, notidicationStore *notificationservice.Service,
prematchSvc *odds.ServiceImpl, prematchSvc *odds.ServiceImpl,
eventSvc event.Service, eventSvc event.Service,
leagueSvc league.Service,
referralSvc referralservice.ReferralStore, referralSvc referralservice.ReferralStore,
virtualGameSvc virtualgameservice.VirtualGameService, virtualGameSvc virtualgameservice.VirtualGameService,
aleaVirtualGameService alea.AleaVirtualGameService, aleaVirtualGameService alea.AleaVirtualGameService,
@ -104,6 +110,7 @@ func NewApp(
userSvc: userSvc, userSvc: userSvc,
ticketSvc: ticketSvc, ticketSvc: ticketSvc,
betSvc: betSvc, betSvc: betSvc,
chapaSvc: chapaSvc,
walletSvc: walletSvc, walletSvc: walletSvc,
transactionSvc: transactionSvc, transactionSvc: transactionSvc,
branchSvc: branchSvc, branchSvc: branchSvc,
@ -113,6 +120,7 @@ func NewApp(
Logger: logger, Logger: logger,
prematchSvc: prematchSvc, prematchSvc: prematchSvc,
eventSvc: eventSvc, eventSvc: eventSvc,
leagueSvc: leagueSvc,
virtualGameSvc: virtualGameSvc, virtualGameSvc: virtualGameSvc,
aleaVirtualGameService: aleaVirtualGameService, aleaVirtualGameService: aleaVirtualGameService,
veliVirtualGameService: veliVirtualGameService, veliVirtualGameService: veliVirtualGameService,

View File

@ -72,18 +72,28 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64) userID := c.Locals("user_id").(int64)
// role := c.Locals("role").(domain.Role) // role := c.Locals("role").(domain.Role)
leagueIDQuery := c.Query("league_id") leagueIDQuery, err := strconv.Atoi(c.Query("league_id"))
sportIDQuery := c.Query("sport_id") if err != nil {
h.logger.Error("invalid league id", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil)
}
sportIDQuery, err := strconv.Atoi(c.Query("sport_id"))
if err != nil {
h.logger.Error("invalid sport id", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil)
}
firstStartTimeQuery := c.Query("first_start_time") firstStartTimeQuery := c.Query("first_start_time")
lastStartTimeQuery := c.Query("last_start_time") lastStartTimeQuery := c.Query("last_start_time")
leagueID := domain.ValidString{ leagueID := domain.ValidInt32{
Value: leagueIDQuery, Value: int32(leagueIDQuery),
Valid: leagueIDQuery != "", Valid: leagueIDQuery != 0,
} }
sportID := domain.ValidString{ sportID := domain.ValidInt32{
Value: sportIDQuery, Value: int32(sportIDQuery),
Valid: sportIDQuery != "", Valid: sportIDQuery != 0,
} }
var firstStartTime domain.ValidTime var firstStartTime domain.ValidTime
@ -123,7 +133,6 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
} }
var res domain.CreateBetRes var res domain.CreateBetRes
var err error
for i := 0; i < int(req.NumberOfBets); i++ { for i := 0; i < int(req.NumberOfBets); i++ {
res, err = h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, leagueID, sportID, firstStartTime, lastStartTime) res, err = h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, leagueID, sportID, firstStartTime, lastStartTime)

View File

@ -1,283 +1,464 @@
package handlers package handlers
import ( import (
"bytes" // "bytes"
"encoding/json" // "encoding/json"
"fmt" // "fmt"
"io" // "io"
"net/http" // "net/http"
"fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/google/uuid"
) )
// GetBanks godoc // // GetBanks godoc
// @Summary Get list of banks // // @Summary Get list of banks
// @Description Fetch all supported banks from Chapa // // @Description Fetch all supported banks from Chapa
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Success 200 {object} domain.ChapaSupportedBanksResponse
// // @Router /api/v1/chapa/banks [get]
// func (h *Handler) GetBanks(c *fiber.Ctx) error {
// httpReq, err := http.NewRequest("GET", h.Cfg.CHAPA_BASE_URL+"/banks", nil)
// // log.Printf("\n\nbase url is: %v\n\n", h.Cfg.CHAPA_BASE_URL)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{"error": "Failed to create request", "details": err.Error()})
// }
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{"error": "Failed to fetch banks", "details": err.Error()})
// }
// defer resp.Body.Close()
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{"error": "Failed to read response", "details": err.Error()})
// }
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// // InitializePayment godoc
// // @Summary Initialize a payment transaction
// // @Description Initiate a payment through Chapa
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param payload body domain.InitPaymentRequest true "Payment initialization request"
// // @Success 200 {object} domain.InitPaymentResponse
// // @Router /api/v1/chapa/payments/initialize [post]
// func (h *Handler) InitializePayment(c *fiber.Ctx) error {
// var req InitPaymentRequest
// if err := c.BodyParser(&req); err != nil {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Invalid request body",
// "details": err.Error(),
// })
// }
// // Generate and assign a unique transaction reference
// req.TxRef = uuid.New().String()
// payload, err := json.Marshal(req)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to serialize request",
// "details": err.Error(),
// })
// }
// httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transaction/initialize", bytes.NewBuffer(payload))
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to create request",
// "details": err.Error(),
// })
// }
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// httpReq.Header.Set("Content-Type", "application/json")
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to initialize payment",
// "details": err.Error(),
// })
// }
// defer resp.Body.Close()
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to read response",
// "details": err.Error(),
// })
// }
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// // VerifyTransaction godoc
// // @Summary Verify a payment transaction
// // @Description Verify the transaction status from Chapa using tx_ref
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param tx_ref path string true "Transaction Reference"
// // @Success 200 {object} domain.VerifyTransactionResponse
// // @Router /api/v1/chapa/payments/verify/{tx_ref} [get]
// func (h *Handler) VerifyTransaction(c *fiber.Ctx) error {
// txRef := c.Params("tx_ref")
// if txRef == "" {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Missing transaction reference",
// })
// }
// url := fmt.Sprintf("%s/transaction/verify/%s", h.Cfg.CHAPA_BASE_URL, txRef)
// httpReq, err := http.NewRequest("GET", url, nil)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to create request",
// "details": err.Error(),
// })
// }
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to verify transaction",
// "details": err.Error(),
// })
// }
// defer resp.Body.Close()
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(500).JSON(fiber.Map{
// "error": "Failed to read response",
// "details": err.Error(),
// })
// }
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// // ReceiveWebhook godoc
// // @Summary Receive Chapa webhook
// // @Description Endpoint to receive webhook payloads from Chapa
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param payload body object true "Webhook Payload (dynamic)"
// // @Success 200 {string} string "ok"
// // @Router /api/v1/chapa/payments/callback [post]
// func (h *Handler) ReceiveWebhook(c *fiber.Ctx) error {
// var payload map[string]interface{}
// if err := c.BodyParser(&payload); err != nil {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Invalid webhook data",
// "details": err.Error(),
// })
// }
// h.logger.Info("Chapa webhook received", "payload", payload)
// // Optional: you can verify tx_ref here again if needed
// return c.SendStatus(fiber.StatusOK)
// }
// // CreateTransfer godoc
// // @Summary Create a money transfer
// // @Description Initiate a transfer request via Chapa
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param payload body domain.TransferRequest true "Transfer request body"
// // @Success 200 {object} domain.CreateTransferResponse
// // @Router /api/v1/chapa/transfers [post]
// func (h *Handler) CreateTransfer(c *fiber.Ctx) error {
// var req TransferRequest
// if err := c.BodyParser(&req); err != nil {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Invalid request",
// "details": err.Error(),
// })
// }
// // Inject unique transaction reference
// req.Reference = uuid.New().String()
// payload, err := json.Marshal(req)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to serialize request",
// "details": err.Error(),
// })
// }
// httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transfers", bytes.NewBuffer(payload))
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to create HTTP request",
// "details": err.Error(),
// })
// }
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// httpReq.Header.Set("Content-Type", "application/json")
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Transfer request failed",
// "details": err.Error(),
// })
// }
// defer resp.Body.Close()
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to read response",
// "details": err.Error(),
// })
// }
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// // VerifyTransfer godoc
// // @Summary Verify a transfer
// // @Description Check the status of a money transfer via reference
// // @Tags Chapa
// // @Accept json
// // @Produce json
// // @Param transfer_ref path string true "Transfer Reference"
// // @Success 200 {object} domain.VerifyTransferResponse
// // @Router /api/v1/chapa/transfers/verify/{transfer_ref} [get]
// func (h *Handler) VerifyTransfer(c *fiber.Ctx) error {
// transferRef := c.Params("transfer_ref")
// if transferRef == "" {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "error": "Missing transfer reference in URL",
// })
// }
// url := fmt.Sprintf("%s/transfers/verify/%s", h.Cfg.CHAPA_BASE_URL, transferRef)
// httpReq, err := http.NewRequest("GET", url, nil)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to create HTTP request",
// "details": err.Error(),
// })
// }
// httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
// resp, err := http.DefaultClient.Do(httpReq)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Verification request failed",
// "details": err.Error(),
// })
// }
// defer resp.Body.Close()
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
// "error": "Failed to read response body",
// "details": err.Error(),
// })
// }
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// VerifyChapaPayment godoc
// @Summary Verifies Chapa webhook transaction
// @Tags Chapa // @Tags Chapa
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} domain.ChapaSupportedBanksResponse // @Param payload body domain.ChapaTransactionType true "Webhook Payload"
// @Router /api/v1/chapa/banks [get] // @Success 200 {object} domain.Response
func (h *Handler) GetBanks(c *fiber.Ctx) error { // @Router /api/v1/chapa/payments/verify [post]
httpReq, err := http.NewRequest("GET", h.Cfg.CHAPA_BASE_URL+"/banks", nil) func (h *Handler) VerifyChapaPayment(c *fiber.Ctx) error {
// log.Printf("\n\nbase url is: %v\n\n", h.Cfg.CHAPA_BASE_URL) var txType domain.ChapaTransactionType
if err != nil { if err := c.BodyParser(&txType); err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Failed to create request", "details": err.Error()}) return domain.UnProcessableEntityResponse(c)
}
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Failed to fetch banks", "details": err.Error()})
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Failed to read response", "details": err.Error()})
} }
return c.Status(resp.StatusCode).Type("json").Send(body) switch txType.Type {
} case "Payout":
var payload domain.ChapaWebHookTransfer
// InitializePayment godoc
// @Summary Initialize a payment transaction
// @Description Initiate a payment through Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Param payload body domain.InitPaymentRequest true "Payment initialization request"
// @Success 200 {object} domain.InitPaymentResponse
// @Router /api/v1/chapa/payments/initialize [post]
func (h *Handler) InitializePayment(c *fiber.Ctx) error {
var req InitPaymentRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request body",
"details": err.Error(),
})
}
// Generate and assign a unique transaction reference
req.TxRef = uuid.New().String()
payload, err := json.Marshal(req)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to serialize request",
"details": err.Error(),
})
}
httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transaction/initialize", bytes.NewBuffer(payload))
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to create request",
"details": err.Error(),
})
}
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to initialize payment",
"details": err.Error(),
})
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to read response",
"details": err.Error(),
})
}
return c.Status(resp.StatusCode).Type("json").Send(body)
}
// VerifyTransaction godoc
// @Summary Verify a payment transaction
// @Description Verify the transaction status from Chapa using tx_ref
// @Tags Chapa
// @Accept json
// @Produce json
// @Param tx_ref path string true "Transaction Reference"
// @Success 200 {object} domain.VerifyTransactionResponse
// @Router /api/v1/chapa/payments/verify/{tx_ref} [get]
func (h *Handler) VerifyTransaction(c *fiber.Ctx) error {
txRef := c.Params("tx_ref")
if txRef == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Missing transaction reference",
})
}
url := fmt.Sprintf("%s/transaction/verify/%s", h.Cfg.CHAPA_BASE_URL, txRef)
httpReq, err := http.NewRequest("GET", url, nil)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to create request",
"details": err.Error(),
})
}
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to verify transaction",
"details": err.Error(),
})
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return c.Status(500).JSON(fiber.Map{
"error": "Failed to read response",
"details": err.Error(),
})
}
return c.Status(resp.StatusCode).Type("json").Send(body)
}
// ReceiveWebhook godoc
// @Summary Receive Chapa webhook
// @Description Endpoint to receive webhook payloads from Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Param payload body object true "Webhook Payload (dynamic)"
// @Success 200 {string} string "ok"
// @Router /api/v1/chapa/payments/callback [post]
func (h *Handler) ReceiveWebhook(c *fiber.Ctx) error {
var payload map[string]interface{}
if err := c.BodyParser(&payload); err != nil { if err := c.BodyParser(&payload); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return domain.UnProcessableEntityResponse(c)
"error": "Invalid webhook data", }
"details": err.Error(),
if err := h.chapaSvc.HandleChapaTransferWebhook(c.Context(), payload); err != nil {
return domain.FiberErrorResponse(c, err)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa transfer verified successfully",
Success: true,
StatusCode: fiber.StatusOK,
})
case "API":
var payload domain.ChapaWebHookPayment
if err := c.BodyParser(&payload); err != nil {
return domain.UnProcessableEntityResponse(c)
}
if err := h.chapaSvc.HandleChapaPaymentWebhook(c.Context(), payload); err != nil {
return domain.FiberErrorResponse(c, err)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa payment verified successfully",
Success: true,
StatusCode: fiber.StatusOK,
})
default:
return c.Status(fiber.StatusBadRequest).JSON(domain.Response{
Message: "Invalid Chapa transaction type",
Success: false,
StatusCode: fiber.StatusBadRequest,
}) })
} }
h.logger.Info("Chapa webhook received", "payload", payload)
// Optional: you can verify tx_ref here again if needed
return c.SendStatus(fiber.StatusOK)
} }
// CreateTransfer godoc // WithdrawUsingChapa godoc
// @Summary Create a money transfer // @Summary Withdraw using Chapa
// @Description Initiate a transfer request via Chapa // @Description Initiates a withdrawal transaction using Chapa for the authenticated user.
// @Tags Chapa // @Tags Chapa
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param payload body domain.TransferRequest true "Transfer request body" // @Param request body domain.ChapaWithdrawRequest true "Chapa Withdraw Request"
// @Success 200 {object} domain.CreateTransferResponse // @Success 200 {object} domain.Response{data=string} "Withdrawal requested successfully"
// @Router /api/v1/chapa/transfers [post] // @Failure 400 {object} domain.Response "Invalid request"
func (h *Handler) CreateTransfer(c *fiber.Ctx) error { // @Failure 401 {object} domain.Response "Unauthorized"
var req TransferRequest // @Failure 422 {object} domain.Response "Unprocessable Entity"
// @Failure 500 {object} domain.Response "Internal Server Error"
// @Router /api/v1/chapa/payments/withdraw [post]
func (h *Handler) WithdrawUsingChapa(c *fiber.Ctx) error {
var req domain.ChapaWithdrawRequest
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return domain.UnProcessableEntityResponse(c)
"error": "Invalid request", }
"details": err.Error(),
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
return c.Status(fiber.StatusUnauthorized).JSON(domain.Response{
Message: "Unauthorized",
Success: false,
StatusCode: fiber.StatusUnauthorized,
}) })
} }
// Inject unique transaction reference if err := h.chapaSvc.WithdrawUsingChapa(c.Context(), userID, req); err != nil {
req.Reference = uuid.New().String() return domain.FiberErrorResponse(c, err)
}
payload, err := json.Marshal(req) return c.Status(fiber.StatusOK).JSON(domain.Response{
if err != nil { Message: "Withdrawal requested successfully",
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ Success: true,
"error": "Failed to serialize request", StatusCode: fiber.StatusOK,
"details": err.Error(),
}) })
} }
httpReq, err := http.NewRequest("POST", h.Cfg.CHAPA_BASE_URL+"/transfers", bytes.NewBuffer(payload)) // DepositUsingChapa godoc
if err != nil { // @Summary Deposit money into user wallet using Chapa
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ // @Description Deposits money into user wallet from user account using Chapa
"error": "Failed to create HTTP request",
"details": err.Error(),
})
}
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Transfer request failed",
"details": err.Error(),
})
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to read response",
"details": err.Error(),
})
}
return c.Status(resp.StatusCode).Type("json").Send(body)
}
// VerifyTransfer godoc
// @Summary Verify a transfer
// @Description Check the status of a money transfer via reference
// @Tags Chapa // @Tags Chapa
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param transfer_ref path string true "Transfer Reference" // @Param payload body domain.ChapaDepositRequest true "Deposit request payload"
// @Success 200 {object} domain.VerifyTransferResponse // @Success 200 {object} domain.ChapaPaymentUrlResponseWrapper
// @Router /api/v1/chapa/transfers/verify/{transfer_ref} [get] // @Failure 400 {object} domain.Response "Invalid request"
func (h *Handler) VerifyTransfer(c *fiber.Ctx) error { // @Failure 422 {object} domain.Response "Validation error"
transferRef := c.Params("transfer_ref") // @Failure 500 {object} domain.Response "Internal server error"
if transferRef == "" { // @Router /api/v1/chapa/payments/deposit [post]
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ func (h *Handler) DepositUsingChapa(c *fiber.Ctx) error {
"error": "Missing transfer reference in URL", // Extract user info from token (adjust as per your auth middleware)
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
return c.Status(fiber.StatusUnauthorized).JSON(domain.Response{
Message: "Unauthorized",
Success: false,
StatusCode: fiber.StatusUnauthorized,
}) })
} }
url := fmt.Sprintf("%s/transfers/verify/%s", h.Cfg.CHAPA_BASE_URL, transferRef) var req domain.ChapaDepositRequest
if err := c.BodyParser(&req); err != nil {
return domain.UnProcessableEntityResponse(c)
}
httpReq, err := http.NewRequest("GET", url, nil) // Validate input in domain/domain (you may have a Validate method)
if err := req.Validate(); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.Response{
Message: err.Error(),
Success: false,
StatusCode: fiber.StatusBadRequest,
})
}
// Call service to handle the deposit logic and get payment URL
paymentUrl, svcErr := h.chapaSvc.DepositUsingChapa(c.Context(), userID, req)
if svcErr != nil {
return domain.FiberErrorResponse(c, svcErr)
}
return c.Status(fiber.StatusOK).JSON(domain.ResponseWDataFactory[domain.ChapaPaymentUrlResponse]{
Data: domain.ChapaPaymentUrlResponse{
PaymentURL: paymentUrl,
},
Response: domain.Response{
Message: "Deposit process started on wallet, fulfill payment using the URL provided",
Success: true,
StatusCode: fiber.StatusOK,
},
})
}
// ReadChapaBanks godoc
// @Summary fetches chapa supported banks
// @Tags Chapa
// @Accept json
// @Produce json
// @Success 200 {object} domain.ChapaSupportedBanksResponseWrapper
// @Failure 400,401,404,422,500 {object} domain.Response
// @Router /api/v1/chapa/banks [get]
func (h *Handler) ReadChapaBanks(c *fiber.Ctx) error {
banks, err := h.chapaSvc.GetSupportedBanks()
fmt.Printf("\n\nhandler fetched banks: %+v\n\n", banks)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(domain.Response{
"error": "Failed to create HTTP request", Message: "Internal server error",
"details": err.Error(), Success: false,
StatusCode: fiber.StatusInternalServerError,
}) })
} }
httpReq.Header.Set("Authorization", "Bearer "+h.Cfg.CHAPA_SECRET_KEY) return c.Status(fiber.StatusOK).JSON(domain.ResponseWDataFactory[[]domain.ChapaSupportedBank]{
Data: banks,
resp, err := http.DefaultClient.Do(httpReq) Response: domain.Response{
if err != nil { Message: "read successful on chapa supported banks",
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ Success: true,
"error": "Verification request failed", StatusCode: fiber.StatusOK,
"details": err.Error(), },
}) })
} }
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to read response body",
"details": err.Error(),
})
}
return c.Status(resp.StatusCode).Type("json").Send(body)
}

View File

@ -7,8 +7,10 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"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"
@ -30,6 +32,7 @@ type Handler struct {
notificationSvc *notificationservice.Service notificationSvc *notificationservice.Service
userSvc *user.Service userSvc *user.Service
referralSvc referralservice.ReferralStore referralSvc referralservice.ReferralStore
chapaSvc chapa.ChapaPort
walletSvc *wallet.Service walletSvc *wallet.Service
transactionSvc *transaction.Service transactionSvc *transaction.Service
ticketSvc *ticket.Service ticketSvc *ticket.Service
@ -38,6 +41,7 @@ type Handler struct {
companySvc *company.Service companySvc *company.Service
prematchSvc *odds.ServiceImpl prematchSvc *odds.ServiceImpl
eventSvc event.Service eventSvc event.Service
leagueSvc league.Service
virtualGameSvc virtualgameservice.VirtualGameService virtualGameSvc virtualgameservice.VirtualGameService
aleaVirtualGameSvc alea.AleaVirtualGameService aleaVirtualGameSvc alea.AleaVirtualGameService
veliVirtualGameSvc veli.VeliVirtualGameService veliVirtualGameSvc veli.VeliVirtualGameService
@ -53,6 +57,7 @@ func New(
logger *slog.Logger, logger *slog.Logger,
notificationSvc *notificationservice.Service, notificationSvc *notificationservice.Service,
validator *customvalidator.CustomValidator, validator *customvalidator.CustomValidator,
chapaSvc chapa.ChapaPort,
walletSvc *wallet.Service, walletSvc *wallet.Service,
referralSvc referralservice.ReferralStore, referralSvc referralservice.ReferralStore,
virtualGameSvc virtualgameservice.VirtualGameService, virtualGameSvc virtualgameservice.VirtualGameService,
@ -69,12 +74,14 @@ func New(
companySvc *company.Service, companySvc *company.Service,
prematchSvc *odds.ServiceImpl, prematchSvc *odds.ServiceImpl,
eventSvc event.Service, eventSvc event.Service,
leagueSvc league.Service,
resultSvc result.Service, resultSvc result.Service,
cfg *config.Config, cfg *config.Config,
) *Handler { ) *Handler {
return &Handler{ return &Handler{
logger: logger, logger: logger,
notificationSvc: notificationSvc, notificationSvc: notificationSvc,
chapaSvc: chapaSvc,
walletSvc: walletSvc, walletSvc: walletSvc,
referralSvc: referralSvc, referralSvc: referralSvc,
validator: validator, validator: validator,
@ -86,6 +93,7 @@ func New(
companySvc: companySvc, companySvc: companySvc,
prematchSvc: prematchSvc, prematchSvc: prematchSvc,
eventSvc: eventSvc, eventSvc: eventSvc,
leagueSvc: leagueSvc,
virtualGameSvc: virtualGameSvc, virtualGameSvc: virtualGameSvc,
aleaVirtualGameSvc: aleaVirtualGameSvc, aleaVirtualGameSvc: aleaVirtualGameSvc,
veliVirtualGameSvc: veliVirtualGameSvc, veliVirtualGameSvc: veliVirtualGameSvc,

View File

@ -0,0 +1,34 @@
package handlers
import (
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2"
)
func (h *Handler) GetAllLeagues(c *fiber.Ctx) error {
leagues, err := h.leagueSvc.GetAllLeagues(c.Context())
if err != nil {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get leagues", err, nil)
}
return response.WriteJSON(c, fiber.StatusOK, "All leagues retrived", leagues, nil)
}
func (h *Handler) SetLeagueActive(c *fiber.Ctx) error {
leagueIdStr := c.Params("id")
if leagueIdStr == "" {
response.WriteJSON(c, fiber.StatusBadRequest, "Missing league id", nil, nil)
}
leagueId, err := strconv.Atoi(leagueIdStr)
if err != nil {
response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil)
}
if err := h.leagueSvc.SetLeagueActive(c.Context(), int64(leagueId)); err != nil {
response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update league", err, nil)
}
return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil)
}

View File

@ -107,18 +107,27 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error {
func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
page := c.QueryInt("page", 1) page := c.QueryInt("page", 1)
pageSize := c.QueryInt("page_size", 10) pageSize := c.QueryInt("page_size", 10)
leagueIDQuery := c.Query("league_id") leagueIDQuery, err := strconv.Atoi(c.Query("league_id"))
sportIDQuery := c.Query("sport_id") if err != nil {
h.logger.Error("invalid league id", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil)
}
sportIDQuery, err := strconv.Atoi(c.Query("sport_id"))
if err != nil {
h.logger.Error("invalid sport id", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil)
}
firstStartTimeQuery := c.Query("first_start_time") firstStartTimeQuery := c.Query("first_start_time")
lastStartTimeQuery := c.Query("last_start_time") lastStartTimeQuery := c.Query("last_start_time")
leagueID := domain.ValidString{ leagueID := domain.ValidInt32{
Value: leagueIDQuery, Value: int32(leagueIDQuery),
Valid: leagueIDQuery != "", Valid: leagueIDQuery != 0,
} }
sportID := domain.ValidString{ sportID := domain.ValidInt32{
Value: sportIDQuery, Value: int32(sportIDQuery),
Valid: sportIDQuery != "", Valid: sportIDQuery != 0,
} }
var firstStartTime domain.ValidTime var firstStartTime domain.ValidTime

View File

@ -0,0 +1,131 @@
package handlers
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"testing"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
// --- Mock service ---
type MockChapaService struct {
mock.Mock
}
func (m *MockChapaService) GetSupportedBanks() ([]domain.ChapaSupportedBank, error) {
args := m.Called()
return args.Get(0).([]domain.ChapaSupportedBank), args.Error(1)
}
// --- Tests ---
func (h *Handler) TestReadChapaBanks_Success(t *testing.T) {
app := fiber.New()
mockService := new(MockChapaService)
now := time.Now()
isMobile := 1
isRtgs := 1
is24hrs := 1
mockBanks := []domain.ChapaSupportedBank{
{
Id: 101,
Slug: "bank-a",
Swift: "BKAETHAA",
Name: "Bank A",
AcctLength: 13,
AcctNumberRegex: "^[0-9]{13}$",
ExampleValue: "1000222215735",
CountryId: 1,
IsMobilemoney: &isMobile,
IsActive: 1,
IsRtgs: &isRtgs,
Active: 1,
Is24Hrs: &is24hrs,
CreatedAt: now,
UpdatedAt: now,
Currency: "ETB",
},
}
mockService.On("GetSupportedBanks").Return(mockBanks, nil)
// handler := handlers.NewChapaHandler(mockService)
app.Post("/chapa/banks", h.ReadChapaBanks)
req := createTestRequest(t, "POST", "/chapa/banks", nil)
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
var body domain.ResponseWDataFactory[[]domain.ChapaSupportedBank]
err = parseJSONBody(resp, &body)
require.NoError(t, err)
assert.True(t, body.Success)
assert.Equal(t, "read successful on chapa supported banks", body.Message)
require.Len(t, body.Data, 1)
assert.Equal(t, mockBanks[0].Name, body.Data[0].Name)
assert.Equal(t, mockBanks[0].AcctNumberRegex, body.Data[0].AcctNumberRegex)
mockService.AssertExpectations(t)
}
func (h *Handler) TestReadChapaBanks_Failure(t *testing.T) {
app := fiber.New()
mockService := new(MockChapaService)
mockService.On("GetSupportedBanks").Return(nil, errors.New("chapa service unavailable"))
// handler := handlers.NewChapaHandler(mockService)
app.Post("/chapa/banks", h.ReadChapaBanks)
req := createTestRequest(t, "POST", "/chapa/banks", nil)
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)
var body domain.Response
err = parseJSONBody(resp, &body)
require.NoError(t, err)
assert.False(t, body.Success)
assert.Equal(t, "Internal server error", body.Message)
mockService.AssertExpectations(t)
}
func createTestRequest(t *testing.T, method, url string, body interface{}) *http.Request {
var buf io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
t.Fatal(err)
}
buf = bytes.NewBuffer(b)
}
req, err := http.NewRequest(method, url, buf)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
return req
}
func parseJSONBody(resp *http.Response, target interface{}) error {
return json.NewDecoder(resp.Body).Decode(target)
}

View File

@ -18,6 +18,7 @@ func (a *App) initAppRoutes() {
a.logger, a.logger,
a.NotidicationStore, a.NotidicationStore,
a.validator, a.validator,
a.chapaSvc,
a.walletSvc, a.walletSvc,
a.referralSvc, a.referralSvc,
a.virtualGameSvc, a.virtualGameSvc,
@ -35,6 +36,7 @@ func (a *App) initAppRoutes() {
a.prematchSvc, a.prematchSvc,
a.eventSvc, a.eventSvc,
*a.resultSvc, *a.resultSvc,
a.leagueSvc,
a.cfg, a.cfg,
) )
@ -114,13 +116,17 @@ func (a *App) initAppRoutes() {
a.fiber.Put("/managers/:id", a.authMiddleware, h.UpdateManagers) a.fiber.Put("/managers/:id", a.authMiddleware, h.UpdateManagers)
a.fiber.Get("/manager/:id/branch", a.authMiddleware, h.GetBranchByManagerID) a.fiber.Get("/manager/:id/branch", a.authMiddleware, h.GetBranchByManagerID)
a.fiber.Get("/prematch/odds/:event_id", h.GetPrematchOdds) a.fiber.Get("/events/odds/:event_id", h.GetPrematchOdds)
a.fiber.Get("/prematch/odds", h.GetALLPrematchOdds) a.fiber.Get("/events/odds", h.GetALLPrematchOdds)
a.fiber.Get("/prematch/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID) a.fiber.Get("/events/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID)
a.fiber.Get("/prematch/events/:id", h.GetUpcomingEventByID) a.fiber.Get("/events/:id", h.GetUpcomingEventByID)
a.fiber.Get("/prematch/events", h.GetAllUpcomingEvents) a.fiber.Get("/events", h.GetAllUpcomingEvents)
a.fiber.Get("/prematch/odds/upcoming/:upcoming_id", h.GetPrematchOddsByUpcomingID) a.fiber.Get("/events/odds/upcoming/:upcoming_id", h.GetPrematchOddsByUpcomingID)
// Leagues
a.fiber.Get("/leagues", h.GetAllLeagues)
a.fiber.Get("/leagues/:id/set-active", h.SetLeagueActive)
a.fiber.Get("/result/:id", h.GetResultsByEventID) a.fiber.Get("/result/:id", h.GetResultsByEventID)
@ -184,13 +190,17 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet) a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet)
//Chapa Routes //Chapa Routes
group.Post("/chapa/payments/verify", a.authMiddleware, h.VerifyChapaPayment)
group.Post("/chapa/payments/withdraw", a.authMiddleware, h.WithdrawUsingChapa)
group.Post("/chapa/payments/deposit", a.authMiddleware, h.DepositUsingChapa)
group.Get("/chapa/banks", a.authMiddleware, h.ReadChapaBanks)
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)
group.Get("/chapa/banks", h.GetBanks) // group.Get("/chapa/banks", h.GetBanks)
group.Post("/chapa/transfers", h.CreateTransfer) // group.Post("/chapa/transfers", h.CreateTransfer)
group.Get("/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer) // group.Get("/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer)
//Alea Play Virtual Game Routes //Alea Play Virtual Game Routes
group.Get("/alea-play/launch", a.authMiddleware, h.LaunchAleaGame) group.Get("/alea-play/launch", a.authMiddleware, h.LaunchAleaGame)

View File

@ -19,7 +19,7 @@ build:
.PHONY: run .PHONY: run
run: run:
@docker compose up -d @docker compose up
.PHONY: stop .PHONY: stop
stop: stop:
@ -56,8 +56,6 @@ db-up:
db-down: db-down:
@docker compose down @docker compose down
@docker volume rm fortunebet-backend_postgres_data @docker volume rm fortunebet-backend_postgres_data
postgres:
@docker exec -it fortunebet-backend-postgres-1 psql -U root -d gh
.PHONY: sqlc-gen .PHONY: sqlc-gen
sqlc-gen: sqlc-gen:
@sqlc generate @sqlc generate