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/bet"
"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/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
@ -85,6 +87,7 @@ func main() {
transactionSvc := transaction.NewService(store)
branchSvc := branch.NewService(store)
companySvc := company.NewService(store)
leagueSvc := league.New(store)
betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger)
resultSvc := result.NewService(store, cfg, logger, *betSvc, oddsSvc, eventSvc)
notificationRepo := repository.NewNotificationRepository(store)
@ -108,6 +111,17 @@ func main() {
logger,
)
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.StartTicketCrons(*ticketSvc)
@ -116,7 +130,7 @@ func main() {
JwtAccessKey: cfg.JwtKey,
JwtAccessExpiry: cfg.AccessExpiry,
}, 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)
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 odds;
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 (
id TEXT PRIMARY KEY,
sport_id TEXT,
sport_id INT,
match_name TEXT,
home_team TEXT,
away_team TEXT,
home_team_id TEXT,
away_team_id TEXT,
home_team_id INT,
away_team_id INT,
home_kit_image TEXT,
away_kit_image TEXT,
league_id TEXT,
league_id INT,
league_name TEXT,
league_cc TEXT,
start_time TIMESTAMP,
@ -233,6 +233,20 @@ CREATE TABLE companies (
admin_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
CREATE VIEW companies_details AS
SELECT companies.*,
@ -297,6 +311,7 @@ ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERE
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_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE;
ALTER TABLE companies
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;

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": {
"get": {
"description": "Fetch all supported banks from Chapa",
"consumes": [
"application/json"
],
@ -316,20 +315,50 @@ const docTemplate = `{
"tags": [
"Chapa"
],
"summary": "Get list of banks",
"summary": "fetches chapa supported banks",
"responses": {
"200": {
"description": "OK",
"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": {
"description": "Endpoint to receive webhook payloads from Chapa",
"description": "Deposits money into user wallet from user account using Chapa",
"consumes": [
"application/json"
],
@ -339,31 +368,48 @@ const docTemplate = `{
"tags": [
"Chapa"
],
"summary": "Receive Chapa webhook",
"summary": "Deposit money into user wallet using Chapa",
"parameters": [
{
"description": "Webhook Payload (dynamic)",
"description": "Deposit request payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"type": "object"
"$ref": "#/definitions/domain.ChapaDepositRequest"
}
}
],
"responses": {
"200": {
"description": "ok",
"description": "OK",
"schema": {
"type": "string"
"$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/initialize": {
"/api/v1/chapa/payments/verify": {
"post": {
"description": "Initiate a payment through Chapa",
"consumes": [
"application/json"
],
@ -373,15 +419,15 @@ const docTemplate = `{
"tags": [
"Chapa"
],
"summary": "Initialize a payment transaction",
"summary": "Verifies Chapa webhook transaction",
"parameters": [
{
"description": "Payment initialization request",
"description": "Webhook Payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.InitPaymentRequest"
"$ref": "#/definitions/domain.ChapaTransactionType"
}
}
],
@ -389,47 +435,15 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.InitPaymentResponse"
"$ref": "#/definitions/domain.Response"
}
}
}
}
},
"/api/v1/chapa/payments/verify/{tx_ref}": {
"get": {
"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": {
"$ref": "#/definitions/domain.VerifyTransactionResponse"
}
}
}
}
},
"/api/v1/chapa/transfers": {
"/api/v1/chapa/payments/withdraw": {
"post": {
"description": "Initiate a transfer request via Chapa",
"description": "Initiates a withdrawal transaction using Chapa for the authenticated user.",
"consumes": [
"application/json"
],
@ -439,55 +453,59 @@ const docTemplate = `{
"tags": [
"Chapa"
],
"summary": "Create a money transfer",
"summary": "Withdraw using Chapa",
"parameters": [
{
"description": "Transfer request body",
"name": "payload",
"description": "Chapa Withdraw Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.TransferRequest"
"$ref": "#/definitions/domain.ChapaWithdrawRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"description": "Withdrawal requested successfully",
"schema": {
"$ref": "#/definitions/domain.CreateTransferResponse"
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/api/v1/chapa/transfers/verify/{transfer_ref}": {
"get": {
"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",
},
"400": {
"description": "Invalid request",
"schema": {
"$ref": "#/definitions/domain.VerifyTransferResponse"
"$ref": "#/definitions/domain.Response"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$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": {
"type": "object",
"properties": {
@ -4513,17 +4571,56 @@ const docTemplate = `{
}
}
},
"domain.ChapaSupportedBanksResponse": {
"domain.ChapaSupportedBanksResponseWrapper": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.ChapaSupportedBank"
}
},
"data": {},
"message": {
"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": {
"type": "object",
"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": {
"type": "string",
"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": {
"type": "object",
"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": {
"type": "object",
"properties": {

View File

@ -298,7 +298,6 @@
},
"/api/v1/chapa/banks": {
"get": {
"description": "Fetch all supported banks from Chapa",
"consumes": [
"application/json"
],
@ -308,20 +307,50 @@
"tags": [
"Chapa"
],
"summary": "Get list of banks",
"summary": "fetches chapa supported banks",
"responses": {
"200": {
"description": "OK",
"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": {
"description": "Endpoint to receive webhook payloads from Chapa",
"description": "Deposits money into user wallet from user account using Chapa",
"consumes": [
"application/json"
],
@ -331,31 +360,48 @@
"tags": [
"Chapa"
],
"summary": "Receive Chapa webhook",
"summary": "Deposit money into user wallet using Chapa",
"parameters": [
{
"description": "Webhook Payload (dynamic)",
"description": "Deposit request payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"type": "object"
"$ref": "#/definitions/domain.ChapaDepositRequest"
}
}
],
"responses": {
"200": {
"description": "ok",
"description": "OK",
"schema": {
"type": "string"
"$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/initialize": {
"/api/v1/chapa/payments/verify": {
"post": {
"description": "Initiate a payment through Chapa",
"consumes": [
"application/json"
],
@ -365,15 +411,15 @@
"tags": [
"Chapa"
],
"summary": "Initialize a payment transaction",
"summary": "Verifies Chapa webhook transaction",
"parameters": [
{
"description": "Payment initialization request",
"description": "Webhook Payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.InitPaymentRequest"
"$ref": "#/definitions/domain.ChapaTransactionType"
}
}
],
@ -381,47 +427,15 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.InitPaymentResponse"
"$ref": "#/definitions/domain.Response"
}
}
}
}
},
"/api/v1/chapa/payments/verify/{tx_ref}": {
"get": {
"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": {
"$ref": "#/definitions/domain.VerifyTransactionResponse"
}
}
}
}
},
"/api/v1/chapa/transfers": {
"/api/v1/chapa/payments/withdraw": {
"post": {
"description": "Initiate a transfer request via Chapa",
"description": "Initiates a withdrawal transaction using Chapa for the authenticated user.",
"consumes": [
"application/json"
],
@ -431,55 +445,59 @@
"tags": [
"Chapa"
],
"summary": "Create a money transfer",
"summary": "Withdraw using Chapa",
"parameters": [
{
"description": "Transfer request body",
"name": "payload",
"description": "Chapa Withdraw Request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.TransferRequest"
"$ref": "#/definitions/domain.ChapaWithdrawRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"description": "Withdrawal requested successfully",
"schema": {
"$ref": "#/definitions/domain.CreateTransferResponse"
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/api/v1/chapa/transfers/verify/{transfer_ref}": {
"get": {
"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",
},
"400": {
"description": "Invalid request",
"schema": {
"$ref": "#/definitions/domain.VerifyTransferResponse"
"$ref": "#/definitions/domain.Response"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$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": {
"type": "object",
"properties": {
@ -4505,17 +4563,56 @@
}
}
},
"domain.ChapaSupportedBanksResponse": {
"domain.ChapaSupportedBanksResponseWrapper": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.ChapaSupportedBank"
}
},
"data": {},
"message": {
"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": {
"type": "object",
"properties": {
@ -4871,6 +4898,21 @@
}
}
},
"domain.Response": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"status_code": {
"type": "integer"
},
"success": {
"type": "boolean"
}
}
},
"domain.Role": {
"type": "string",
"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": {
"type": "object",
"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": {
"type": "object",
"properties": {

View File

@ -124,6 +124,32 @@ definitions:
example: 2
type: integer
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:
properties:
acct_length:
@ -159,14 +185,40 @@ definitions:
updated_at:
type: string
type: object
domain.ChapaSupportedBanksResponse:
domain.ChapaSupportedBanksResponseWrapper:
properties:
data:
items:
$ref: '#/definitions/domain.ChapaSupportedBank'
type: array
data: {}
message:
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
domain.CreateBetOutcomeReq:
properties:
@ -203,52 +255,6 @@ definitions:
- $ref: '#/definitions/domain.OutcomeStatus'
example: 1
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:
properties:
category:
@ -408,6 +414,16 @@ definitions:
totalRewardEarned:
type: number
type: object
domain.Response:
properties:
data: {}
message:
type: string
status_code:
type: integer
success:
type: boolean
type: object
domain.Role:
enum:
- super_admin
@ -468,58 +484,6 @@ definitions:
example: 1
type: integer
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:
properties:
awayKitImage:
@ -598,24 +562,6 @@ definitions:
description: Veli's user identifier
type: string
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:
properties:
category:
@ -1691,123 +1637,133 @@ paths:
get:
consumes:
- application/json
description: Fetch all supported banks from Chapa
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.ChapaSupportedBanksResponse'
summary: Get list of banks
$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'
summary: fetches chapa supported banks
tags:
- Chapa
/api/v1/chapa/payments/callback:
/api/v1/chapa/payments/deposit:
post:
consumes:
- application/json
description: Endpoint to receive webhook payloads from Chapa
description: Deposits money into user wallet from user account using Chapa
parameters:
- description: Webhook Payload (dynamic)
- description: Deposit request payload
in: body
name: payload
required: true
schema:
type: object
$ref: '#/definitions/domain.ChapaDepositRequest'
produces:
- application/json
responses:
"200":
description: ok
description: OK
schema:
type: string
summary: Receive Chapa webhook
$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/initialize:
/api/v1/chapa/payments/verify:
post:
consumes:
- application/json
description: Initiate a payment through Chapa
parameters:
- description: Payment initialization request
- description: Webhook Payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/domain.InitPaymentRequest'
$ref: '#/definitions/domain.ChapaTransactionType'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.InitPaymentResponse'
summary: Initialize a payment transaction
$ref: '#/definitions/domain.Response'
summary: Verifies Chapa webhook transaction
tags:
- 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:
$ref: '#/definitions/domain.VerifyTransactionResponse'
summary: Verify a payment transaction
tags:
- Chapa
/api/v1/chapa/transfers:
/api/v1/chapa/payments/withdraw:
post:
consumes:
- application/json
description: Initiate a transfer request via Chapa
description: Initiates a withdrawal transaction using Chapa for the authenticated
user.
parameters:
- description: Transfer request body
- description: Chapa Withdraw Request
in: body
name: payload
name: request
required: true
schema:
$ref: '#/definitions/domain.TransferRequest'
$ref: '#/definitions/domain.ChapaWithdrawRequest'
produces:
- application/json
responses:
"200":
description: OK
description: Withdrawal requested successfully
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
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
type: string
type: object
"400":
description: Invalid request
schema:
$ref: '#/definitions/domain.VerifyTransferResponse'
summary: Verify a transfer
$ref: '#/definitions/domain.Response'
"401":
description: Unauthorized
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: Withdraw using Chapa
tags:
- Chapa
/api/v1/virtual-games/recommendations/{userID}:

View File

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

View File

@ -47,15 +47,15 @@ ORDER BY start_time ASC
type GetAllUpcomingEventsRow struct {
ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"`
SportID pgtype.Int4 `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"`
HomeTeamID pgtype.Int4 `json:"home_team_id"`
AwayTeamID pgtype.Int4 `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_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"`
LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"`
@ -132,15 +132,15 @@ ORDER BY start_time ASC
type GetExpiredUpcomingEventsRow struct {
ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"`
SportID pgtype.Int4 `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"`
HomeTeamID pgtype.Int4 `json:"home_team_id"`
AwayTeamID pgtype.Int4 `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_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"`
LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"`
@ -230,8 +230,8 @@ LIMIT $6 OFFSET $5
`
type GetPaginatedUpcomingEventsParams struct {
LeagueID pgtype.Text `json:"league_id"`
SportID pgtype.Text `json:"sport_id"`
LeagueID pgtype.Int4 `json:"league_id"`
SportID pgtype.Int4 `json:"sport_id"`
LastStartTime pgtype.Timestamp `json:"last_start_time"`
FirstStartTime pgtype.Timestamp `json:"first_start_time"`
Offset pgtype.Int4 `json:"offset"`
@ -240,15 +240,15 @@ type GetPaginatedUpcomingEventsParams struct {
type GetPaginatedUpcomingEventsRow struct {
ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"`
SportID pgtype.Int4 `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"`
HomeTeamID pgtype.Int4 `json:"home_team_id"`
AwayTeamID pgtype.Int4 `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_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"`
LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"`
@ -327,8 +327,8 @@ WHERE is_live = false
`
type GetTotalEventsParams struct {
LeagueID pgtype.Text `json:"league_id"`
SportID pgtype.Text `json:"sport_id"`
LeagueID pgtype.Int4 `json:"league_id"`
SportID pgtype.Int4 `json:"sport_id"`
LastStartTime pgtype.Timestamp `json:"last_start_time"`
FirstStartTime pgtype.Timestamp `json:"first_start_time"`
}
@ -372,15 +372,15 @@ LIMIT 1
type GetUpcomingByIDRow struct {
ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"`
SportID pgtype.Int4 `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"`
HomeTeamID pgtype.Int4 `json:"home_team_id"`
AwayTeamID pgtype.Int4 `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_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"`
LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"`
@ -488,15 +488,15 @@ SET sport_id = EXCLUDED.sport_id,
type InsertEventParams struct {
ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"`
SportID pgtype.Int4 `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"`
HomeTeamID pgtype.Int4 `json:"home_team_id"`
AwayTeamID pgtype.Int4 `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_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"`
LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"`
@ -595,15 +595,15 @@ SET sport_id = EXCLUDED.sport_id,
type InsertUpcomingEventParams struct {
ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"`
SportID pgtype.Int4 `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"`
HomeTeamID pgtype.Int4 `json:"home_team_id"`
AwayTeamID pgtype.Int4 `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_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"`
LeagueCc pgtype.Text `json:"league_cc"`
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 {
ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"`
SportID pgtype.Int4 `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"`
HomeTeamID pgtype.Int4 `json:"home_team_id"`
AwayTeamID pgtype.Int4 `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_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"`
LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"`
@ -201,6 +201,14 @@ type Event struct {
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 {
ID string `json:"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/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/jackc/pgx/v5 v5.7.4
github.com/joho/godotenv v1.5.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/swaggo/fiber-swagger v1.3.0
github.com/swaggo/swag v1.16.4
github.com/valyala/fasthttp v1.59.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 (
// 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/andybalholm/brotli v1.1.1 // indirect
// github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/bytedance/sonic/loader v0.2.4 // 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/go-openapi/jsonpointer v0.21.1 // 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-playground/locales v0.14.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/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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-isatty v0.0.20 // 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/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/twitchyliquid64/golang-asm v0.15.1 // 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/net v0.38.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/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
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/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/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.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.6.1/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
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
import "time"
import (
"errors"
"time"
)
var (
ChapaSecret string
@ -8,14 +11,14 @@ var (
)
type InitPaymentRequest struct {
Amount string `json:"amount"`
Currency string `json:"currency"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
TxRef string `json:"tx_ref"`
CallbackURL string `json:"callback_url"`
ReturnURL string `json:"return_url"`
Amount Currency `json:"amount"`
Currency string `json:"currency"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
TxRef string `json:"tx_ref"`
CallbackURL string `json:"callback_url"`
ReturnURL string `json:"return_url"`
}
type TransferRequest struct {
@ -105,3 +108,121 @@ type VerifyTransferResponse struct {
Message string `json:"message"`
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
Valid bool
}
type ValidInt32 struct {
Value int32
Valid bool
}
type ValidString struct {
Value string
@ -48,6 +52,18 @@ func (m Currency) String() string {
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 {
vat := amount.Float32() * 0.15

View File

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

View File

@ -1,66 +1,9 @@
package domain
// TODO Will make this dynamic by moving into the database
var SupportedLeagues = []int64{
// Football
10041282, //Premier League
10083364, //La Liga
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
type League struct {
ID int64
Name string
CountryCode string
Bet365ID int32
IsActive bool
}

View File

@ -1,7 +1,6 @@
package domain
import (
"encoding/json"
"time"
)
@ -15,10 +14,11 @@ type Market struct {
MarketName string
MarketID string
UpdatedAt time.Time
Odds []json.RawMessage
Odds []map[string]interface{}
Name string
Handicap string
OddsVal float64
Source string
}
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"`
}
type League struct {
type LeagueRes struct {
ID string `json:"id"`
Name string `json:"name"`
CC string `json:"cc"`
@ -39,14 +39,14 @@ type CommonResultResponse struct {
}
type FootballResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League League `json:"league"`
Home Team `json:"home"`
Away Team `json:"away"`
SS string `json:"ss"`
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League LeagueRes `json:"league"`
Home Team `json:"home"`
Away Team `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstHalf Score `json:"1"`
SecondHalf Score `json:"2"`
@ -78,14 +78,14 @@ type FootballResultResponse struct {
}
type BasketballResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League League `json:"league"`
Home Team `json:"home"`
Away Team `json:"away"`
SS string `json:"ss"`
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League LeagueRes `json:"league"`
Home Team `json:"home"`
Away Team `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstQuarter Score `json:"1"`
SecondQuarter Score `json:"2"`
@ -125,14 +125,14 @@ type BasketballResultResponse struct {
Bet365ID string `json:"bet365_id"`
}
type IceHockeyResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League League `json:"league"`
Home Team `json:"home"`
Away Team `json:"away"`
SS string `json:"ss"`
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League LeagueRes `json:"league"`
Home Team `json:"home"`
Away Team `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstPeriod Score `json:"1"`
SecondPeriod Score `json:"2"`

View File

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

View File

@ -18,13 +18,24 @@ var Environment = map[string]string{
func NewLogger(env string, lvl slog.Level) *slog.Logger {
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 {
case "development":
logHandler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
logHandler = slog.NewTextHandler(file, &slog.HandlerOptions{
Level: lvl,
})
default:
logHandler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
logHandler = slog.NewJSONHandler(file, &slog.HandlerOptions{
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{
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},
HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true},
AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true},
HomeTeamID: pgtype.Text{String: e.HomeTeamID, Valid: true},
AwayTeamID: pgtype.Text{String: e.AwayTeamID, Valid: true},
HomeTeamID: pgtype.Int4{Int32: e.HomeTeamID, Valid: true},
AwayTeamID: pgtype.Int4{Int32: e.AwayTeamID, Valid: true},
HomeKitImage: pgtype.Text{String: e.HomeKitImage, 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},
LeagueCc: pgtype.Text{String: e.LeagueCC, 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 {
return s.queries.InsertUpcomingEvent(ctx, dbgen.InsertUpcomingEventParams{
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},
HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true},
AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true},
HomeTeamID: pgtype.Text{String: e.HomeTeamID, Valid: true},
AwayTeamID: pgtype.Text{String: e.AwayTeamID, Valid: true},
HomeTeamID: pgtype.Int4{Int32: e.HomeTeamID, Valid: true},
AwayTeamID: pgtype.Int4{Int32: e.AwayTeamID, Valid: true},
HomeKitImage: pgtype.Text{String: e.HomeKitImage, 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},
LeagueCc: pgtype.Text{String: e.LeagueCC, 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 {
upcomingEvents[i] = domain.UpcomingEvent{
ID: e.ID,
SportID: e.SportID.String,
SportID: e.SportID.Int32,
MatchName: e.MatchName.String,
HomeTeam: e.HomeTeam.String,
AwayTeam: e.AwayTeam.String,
HomeTeamID: e.HomeTeamID.String,
AwayTeamID: e.AwayTeamID.String,
HomeTeamID: e.HomeTeamID.Int32,
AwayTeamID: e.AwayTeamID.Int32,
HomeKitImage: e.HomeKitImage.String,
AwayKitImage: e.AwayKitImage.String,
LeagueID: e.LeagueID.String,
LeagueID: e.LeagueID.Int32,
LeagueName: e.LeagueName.String,
LeagueCC: e.LeagueCc.String,
StartTime: e.StartTime.Time.UTC(),
@ -106,15 +106,15 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context, filter domain.Even
for i, e := range events {
upcomingEvents[i] = domain.UpcomingEvent{
ID: e.ID,
SportID: e.SportID.String,
SportID: e.SportID.Int32,
MatchName: e.MatchName.String,
HomeTeam: e.HomeTeam.String,
AwayTeam: e.AwayTeam.String,
HomeTeamID: e.HomeTeamID.String,
AwayTeamID: e.AwayTeamID.String,
HomeTeamID: e.HomeTeamID.Int32,
AwayTeamID: e.AwayTeamID.Int32,
HomeKitImage: e.HomeKitImage.String,
AwayKitImage: e.AwayKitImage.String,
LeagueID: e.LeagueID.String,
LeagueID: e.LeagueID.Int32,
LeagueName: e.LeagueName.String,
LeagueCC: e.LeagueCc.String,
StartTime: e.StartTime.Time.UTC(),
@ -160,15 +160,15 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev
for i, e := range events {
upcomingEvents[i] = domain.UpcomingEvent{
ID: e.ID,
SportID: e.SportID.String,
SportID: e.SportID.Int32,
MatchName: e.MatchName.String,
HomeTeam: e.HomeTeam.String,
AwayTeam: e.AwayTeam.String,
HomeTeamID: e.HomeTeamID.String,
AwayTeamID: e.AwayTeamID.String,
HomeTeamID: e.HomeTeamID.Int32,
AwayTeamID: e.AwayTeamID.Int32,
HomeKitImage: e.HomeKitImage.String,
AwayKitImage: e.AwayKitImage.String,
LeagueID: e.LeagueID.String,
LeagueID: e.LeagueID.Int32,
LeagueName: e.LeagueName.String,
LeagueCC: e.LeagueCc.String,
StartTime: e.StartTime.Time.UTC(),
@ -208,15 +208,15 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc
return domain.UpcomingEvent{
ID: event.ID,
SportID: event.SportID.String,
SportID: event.SportID.Int32,
MatchName: event.MatchName.String,
HomeTeam: event.HomeTeam.String,
AwayTeam: event.AwayTeam.String,
HomeTeamID: event.HomeTeamID.String,
AwayTeamID: event.AwayTeamID.String,
HomeTeamID: event.HomeTeamID.Int32,
AwayTeamID: event.AwayTeamID.Int32,
HomeKitImage: event.HomeKitImage.String,
AwayKitImage: event.AwayKitImage.String,
LeagueID: event.LeagueID.String,
LeagueID: event.LeagueID.Int32,
LeagueName: event.LeagueName.String,
LeagueCC: event.LeagueCc.String,
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
}
for _, raw := range m.Odds {
var item map[string]interface{}
if err := json.Unmarshal(raw, &item); err != nil {
continue
}
for _, item := range m.Odds {
var name string
var oddsVal float64
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"])
oddsVal := getFloat(item["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},
RawOdds: rawOddsBytes,
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},
}
@ -85,23 +89,6 @@ func writeFailedMarketLog(m domain.Market, err error) error {
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) {
odds, err := s.queries.GetPrematchOdds(ctx)
if err != nil {
@ -286,3 +273,34 @@ func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID stri
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"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
@ -39,3 +40,12 @@ func OpenDB(url string) (*pgxpool.Pool, func(), error) {
conn.Close()
}, 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 {
return domain.CreateBetOutcome{}, err
}
sportID, err := strconv.ParseInt(event.SportID, 10, 64)
if err != nil {
return domain.CreateBetOutcome{}, err
}
newOutcome := domain.CreateBetOutcome{
EventID: eventID,
OddID: oddID,
MarketID: marketID,
SportID: sportID,
SportID: int64(event.SportID),
HomeTeamName: event.HomeTeam,
AwayTeamName: event.AwayTeam,
MarketName: odds.MarketName,
@ -287,7 +283,7 @@ func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID
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 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)
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)
if err != nil {
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,
OddID: oddID,
MarketID: marketID,
SportID: sportID,
SportID: int64(sportID),
HomeTeamName: HomeTeam,
AwayTeamName: AwayTeam,
MarketName: marketName,
@ -388,7 +379,7 @@ func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportI
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

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)
}()
}
wg.Wait()
return nil
}
@ -75,7 +74,7 @@ func (s *service) fetchLiveEvents(ctx context.Context, url, source string) error
events := []domain.Event{}
switch source {
case "bet365":
events = handleBet365prematch(body, sportID)
events = handleBet365prematch(body, sportID, source)
case "betfair":
events = handleBetfairprematch(body, sportID, source)
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 {
Success int `json:"success"`
Results [][]map[string]interface{} `json:"results"`
@ -105,7 +104,7 @@ func handleBet365prematch(body []byte, sportID int) []domain.Event {
events := []domain.Event{}
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
}
@ -117,24 +116,24 @@ func handleBet365prematch(body []byte, sportID int) []domain.Event {
event := domain.Event{
ID: getString(ev["ID"]),
SportID: fmt.Sprintf("%d", sportID),
SportID: int32(sportID),
MatchName: getString(ev["NA"]),
Score: getString(ev["SS"]),
MatchMinute: getInt(ev["TM"]),
TimerStatus: getString(ev["TT"]),
HomeTeamID: getString(ev["HT"]),
AwayTeamID: getString(ev["AT"]),
HomeTeamID: getInt32(ev["HT"]),
AwayTeamID: getInt32(ev["AT"]),
HomeKitImage: getString(ev["K1"]),
AwayKitImage: getString(ev["K2"]),
LeagueName: getString(ev["CT"]),
LeagueID: getString(ev["C2"]),
LeagueID: getInt32(ev["C2"]),
LeagueCC: getString(ev["CB"]),
StartTime: time.Now().UTC().Format(time.RFC3339),
IsLive: true,
Status: "live",
MatchPeriod: getInt(ev["MD"]),
AddedTime: getInt(ev["TA"]),
Source: "bet365",
Source: source,
}
events = append(events, event)
@ -152,23 +151,20 @@ func handleBetfairprematch(body []byte, sportID int, source string) []domain.Eve
events := []domain.Event{}
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
}
for _, ev := range data.Results {
homeRaw, _ := ev["home"].(map[string]interface{})
homeId, _ := homeRaw["id"].(string)
awayRaw, _ := ev["home"].(map[string]interface{})
awayId, _ := awayRaw["id"].(string)
event := domain.Event{
ID: getString(ev["id"]),
SportID: fmt.Sprintf("%d", sportID),
SportID: int32(sportID),
TimerStatus: getString(ev["time_status"]),
HomeTeamID: homeId,
AwayTeamID: awayId,
HomeTeamID: getInt32(homeRaw["id"]),
AwayTeamID: getInt32(awayRaw["id"]),
StartTime: time.Now().UTC().Format(time.RFC3339),
IsLive: true,
Status: "live",
@ -221,8 +217,8 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
log.Printf("Sport ID %d", sportID)
for page <= totalPages {
page = page + 1
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", sportID, s.token, page)
log.Printf("📡 Fetching data for sport %d at page %d", sportID, page)
url := fmt.Sprintf(url, sportID, s.token, page)
log.Printf("📡 Fetching data from %s - sport %d, for event data page %d", source, sportID, page)
resp, err := http.Get(url)
if err != nil {
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
// }
// leagueID, err := strconv.ParseInt(ev.League.ID, 10, 64)
// if err != nil {
// log.Printf("❌ Invalid league id, leagueID %v", ev.League.ID)
// continue
// }
leagueID, err := strconv.ParseInt(ev.League.ID, 10, 64)
if err != nil {
log.Printf("❌ Invalid league id, leagueID %v", ev.League.ID)
continue
}
// if !slices.Contains(domain.SupportedLeagues, leagueID) {
// // fmt.Printf("⚠️ Skipping league %s (%d) as it is not supported\n", ev.League.Name, leagueID)
// _, err = fmt.Fprintf(b, "Skipped league %s (%d) in sport %d\n", ev.League.Name, leagueID, sportID)
// if err != nil {
// fmt.Printf(" Error while logging skipped league")
// }
// skippedLeague = append(skippedLeague, ev.League.Name)
// continue
// }
// doesn't make sense to save and check back to back, but for now it can be here
s.store.SaveLeague(ctx, domain.League{
ID: leagueID,
Name: ev.League.Name,
IsActive: true,
})
if supported, err := s.store.CheckLeagueSupport(ctx, leagueID); !supported || err != nil {
skippedLeague = append(skippedLeague, ev.League.Name)
continue
}
event := domain.UpcomingEvent{
ID: ev.ID,
SportID: ev.SportID,
SportID: convertInt32(ev.SportID),
MatchName: "",
HomeTeam: ev.Home.Name,
AwayTeam: "", // handle nil safely
HomeTeamID: ev.Home.ID,
AwayTeamID: "",
HomeTeamID: convertInt32(ev.Home.ID),
AwayTeamID: 0,
HomeKitImage: "",
AwayKitImage: "",
LeagueID: ev.League.ID,
LeagueID: convertInt32(ev.League.ID),
LeagueName: ev.League.Name,
LeagueCC: "",
StartTime: time.Unix(startUnix, 0).UTC(),
@ -281,7 +279,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
if ev.Away != nil {
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
}
@ -319,6 +317,20 @@ func getInt(v interface{}) int {
}
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) {
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"
"net/http"
"strconv"
"sync"
"time"
"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
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)
if err != nil {
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...)
}
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) {
eventID, err := strconv.ParseInt(eventIDStr, 10, 64)
@ -336,6 +543,13 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName
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{
EventID: eventID,
FI: fi,
@ -344,7 +558,9 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName
MarketName: market.Name,
MarketID: marketIDstr,
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)
@ -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) {
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
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 {
case "1":
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)
if err != nil {
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
}
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)
threshold, err := strconv.ParseFloat(outcome.OddName, 64)
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)
}
// evaluateRugbyMoneyLine evaluates Rugby money line bets
func evaluateRugbyMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// EvaluateRugbyMoneyLine Evaluates Rugby money line bets
func EvaluateRugbyMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":
if score.Home > score.Away {
@ -99,8 +99,8 @@ func evaluateRugbyMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away
}
}
// evaluateRugbySpread evaluates Rugby spread bets
func evaluateRugbySpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// EvaluateRugbySpread Evaluates Rugby spread bets
func EvaluateRugbySpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
handicap, err := strconv.ParseFloat(outcome.OddHandicap, 64)
if err != nil {
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
}
// evaluateRugbyTotalPoints evaluates Rugby total points bets
func evaluateRugbyTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// EvaluateRugbyTotalPoints Evaluates Rugby total points bets
func EvaluateRugbyTotalPoints(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalPoints := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64)
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)
}
// evaluateBaseballMoneyLine evaluates Baseball money line bets
func evaluateBaseballMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// EvaluateBaseballMoneyLine Evaluates Baseball money line bets
func EvaluateBaseballMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":
if score.Home > score.Away {
@ -175,8 +175,8 @@ func evaluateBaseballMoneyLine(outcome domain.BetOutcome, score struct{ Home, Aw
}
}
// evaluateBaseballSpread evaluates Baseball spread bets
func evaluateBaseballSpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// EvaluateBaseballSpread Evaluates Baseball spread bets
func EvaluateBaseballSpread(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
handicap, err := strconv.ParseFloat(outcome.OddHandicap, 64)
if err != nil {
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
}
// evaluateBaseballTotalRuns evaluates Baseball total runs bets
func evaluateBaseballTotalRuns(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// EvaluateBaseballTotalRuns Evaluates Baseball total runs bets
func EvaluateBaseballTotalRuns(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalRuns := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64)
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)
}
// evaluateBaseballFirstInning evaluates Baseball first inning bets
func evaluateBaseballFirstInning(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// EvaluateBaseballFirstInning Evaluates Baseball first inning bets
func EvaluateBaseballFirstInning(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":
if score.Home > score.Away {
@ -256,8 +256,8 @@ func evaluateBaseballFirstInning(outcome domain.BetOutcome, score struct{ Home,
}
}
// evaluateBaseballFirst5Innings evaluates Baseball first 5 innings bets
func evaluateBaseballFirst5Innings(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// EvaluateBaseballFirst5Innings Evaluates Baseball first 5 innings bets
func EvaluateBaseballFirst5Innings(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":
if score.Home > score.Away {

View File

@ -29,75 +29,75 @@ func TestNFLMarkets(t *testing.T) {
case int64(domain.AMERICAN_FOOTBALL_MONEY_LINE):
// Home win, away win, draw, and invalid OddHeader for Money Line
t.Run("Home Win", func(t *testing.T) {
status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 21, Away: 14})
status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 21, Away: 14})
t.Logf("Market: %s, Scenario: Home Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Away Win", func(t *testing.T) {
status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 14, Away: 21})
status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 14, Away: 21})
t.Logf("Market: %s, Scenario: Away Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Draw", func(t *testing.T) {
status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 17, Away: 17})
status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 17, Away: 17})
t.Logf("Market: %s, Scenario: Draw", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status)
})
t.Run("Invalid OddHeader", func(t *testing.T) {
status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7})
status, err := EvaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7})
t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
case int64(domain.AMERICAN_FOOTBALL_SPREAD):
t.Run("Home Win with Handicap", func(t *testing.T) {
status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-3.5"}, struct{ Home, Away int }{Home: 24, Away: 20})
status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-3.5"}, struct{ Home, Away int }{Home: 24, Away: 20})
t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Away Win with Handicap", func(t *testing.T) {
status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+3.5"}, struct{ Home, Away int }{Home: 20, Away: 24})
status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+3.5"}, struct{ Home, Away int }{Home: 20, Away: 24})
t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21})
status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21})
t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
})
t.Run("Non-numeric Handicap", func(t *testing.T) {
status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14})
status, err := EvaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14})
t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
case int64(domain.AMERICAN_FOOTBALL_TOTAL_POINTS):
t.Run("Over Win", func(t *testing.T) {
status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "44.5"}, struct{ Home, Away int }{Home: 30, Away: 20})
status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "44.5"}, struct{ Home, Away int }{Home: 30, Away: 20})
t.Logf("Market: %s, Scenario: Over Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Under Win", func(t *testing.T) {
status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "44.5"}, struct{ Home, Away int }{Home: 20, Away: 17})
status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "44.5"}, struct{ Home, Away int }{Home: 20, Away: 17})
t.Logf("Market: %s, Scenario: Under Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "37"}, struct{ Home, Away int }{Home: 20, Away: 17})
status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "37"}, struct{ Home, Away int }{Home: 20, Away: 17})
t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
})
t.Run("Non-numeric OddName", func(t *testing.T) {
status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 17})
status, err := EvaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 17})
t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
@ -128,75 +128,75 @@ func TestRugbyMarkets(t *testing.T) {
case int64(domain.RUGBY_MONEY_LINE):
// Home win, away win, draw, and invalid OddHeader for Money Line
t.Run("Home Win", func(t *testing.T) {
status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 30, Away: 20})
status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 30, Away: 20})
t.Logf("Market: %s, Scenario: Home Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Away Win", func(t *testing.T) {
status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 20, Away: 30})
status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 20, Away: 30})
t.Logf("Market: %s, Scenario: Away Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Draw", func(t *testing.T) {
status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 25, Away: 25})
status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 25, Away: 25})
t.Logf("Market: %s, Scenario: Draw", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status)
})
t.Run("Invalid OddHeader", func(t *testing.T) {
status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7})
status, err := EvaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7})
t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
case int64(domain.RUGBY_SPREAD), int64(domain.RUGBY_HANDICAP):
t.Run("Home Win with Handicap", func(t *testing.T) {
status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-6.5"}, struct{ Home, Away int }{Home: 28, Away: 20})
status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-6.5"}, struct{ Home, Away int }{Home: 28, Away: 20})
t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Away Win with Handicap", func(t *testing.T) {
status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+6.5"}, struct{ Home, Away int }{Home: 20, Away: 28})
status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+6.5"}, struct{ Home, Away int }{Home: 20, Away: 28})
t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21})
status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21})
t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
})
t.Run("Non-numeric Handicap", func(t *testing.T) {
status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14})
status, err := EvaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14})
t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
case int64(domain.RUGBY_TOTAL_POINTS):
t.Run("Over Win", func(t *testing.T) {
status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "40.5"}, struct{ Home, Away int }{Home: 25, Away: 20})
status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "40.5"}, struct{ Home, Away int }{Home: 25, Away: 20})
t.Logf("Market: %s, Scenario: Over Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Under Win", func(t *testing.T) {
status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "40.5"}, struct{ Home, Away int }{Home: 15, Away: 20})
status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "40.5"}, struct{ Home, Away int }{Home: 15, Away: 20})
t.Logf("Market: %s, Scenario: Under Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "35"}, struct{ Home, Away int }{Home: 20, Away: 15})
status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "35"}, struct{ Home, Away int }{Home: 20, Away: 15})
t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
})
t.Run("Non-numeric OddName", func(t *testing.T) {
status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 15})
status, err := EvaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 15})
t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
@ -226,75 +226,75 @@ func TestBaseballMarkets(t *testing.T) {
case int64(domain.BASEBALL_MONEY_LINE):
// Home win, away win, draw, and invalid OddHeader for Money Line
t.Run("Home Win", func(t *testing.T) {
status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 6, Away: 3})
status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 6, Away: 3})
t.Logf("Market: %s, Scenario: Home Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Away Win", func(t *testing.T) {
status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 2, Away: 5})
status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 2, Away: 5})
t.Logf("Market: %s, Scenario: Away Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Draw", func(t *testing.T) {
status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 4, Away: 4})
status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 4, Away: 4})
t.Logf("Market: %s, Scenario: Draw", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status)
})
t.Run("Invalid OddHeader", func(t *testing.T) {
status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7})
status, err := EvaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7})
t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
case int64(domain.BASEBALL_SPREAD):
t.Run("Home Win with Handicap", func(t *testing.T) {
status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-1.5"}, struct{ Home, Away int }{Home: 5, Away: 3})
status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-1.5"}, struct{ Home, Away int }{Home: 5, Away: 3})
t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Away Win with Handicap", func(t *testing.T) {
status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+1.5"}, struct{ Home, Away int }{Home: 3, Away: 5})
status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+1.5"}, struct{ Home, Away int }{Home: 3, Away: 5})
t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 4, Away: 4})
status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 4, Away: 4})
t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
})
t.Run("Non-numeric Handicap", func(t *testing.T) {
status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 5, Away: 3})
status, err := EvaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 5, Away: 3})
t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
case int64(domain.BASEBALL_TOTAL_RUNS):
t.Run("Over Win", func(t *testing.T) {
status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7.5"}, struct{ Home, Away int }{Home: 5, Away: 4})
status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7.5"}, struct{ Home, Away int }{Home: 5, Away: 4})
t.Logf("Market: %s, Scenario: Over Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Under Win", func(t *testing.T) {
status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "7.5"}, struct{ Home, Away int }{Home: 2, Away: 3})
status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "7.5"}, struct{ Home, Away int }{Home: 2, Away: 3})
t.Logf("Market: %s, Scenario: Under Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7"}, struct{ Home, Away int }{Home: 4, Away: 3})
status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7"}, struct{ Home, Away int }{Home: 4, Away: 3})
t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
})
t.Run("Non-numeric OddName", func(t *testing.T) {
status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 4, Away: 3})
status, err := EvaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 4, Away: 3})
t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
@ -338,7 +338,7 @@ func TestEvaluateFootballOutcome(t *testing.T) {
}
// 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)
@ -357,7 +357,7 @@ func TestEvaluateTotalLegs(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateTotalLegs(tt.outcome, tt.score)
status, _ := EvaluateTotalLegs(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -380,7 +380,7 @@ func TestEvaluateGameLines(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateGameLines(tt.outcome, tt.score)
status, _ := EvaluateGameLines(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -408,7 +408,7 @@ func TestEvaluateFirstTeamToScore(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateFirstTeamToScore(tt.outcome, tt.events)
status, _ := EvaluateFirstTeamToScore(tt.outcome, tt.events)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -430,7 +430,7 @@ func TestEvaluateGoalsOverUnder(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateGoalsOverUnder(tt.outcome, tt.score)
status, _ := EvaluateGoalsOverUnder(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -452,7 +452,7 @@ func TestEvaluateGoalsOddEven(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateGoalsOddEven(tt.outcome, tt.score)
status, _ := EvaluateGoalsOddEven(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -474,7 +474,7 @@ func TestEvaluateCorrectScore(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateCorrectScore(tt.outcome, tt.score)
status, _ := EvaluateCorrectScore(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -498,7 +498,7 @@ func TestEvaluateHighestScoringHalf(t *testing.T) {
for _, tt := range tests {
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 {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -551,7 +551,7 @@ func TestEvaluateHighestScoringQuarter(t *testing.T) {
for _, tt := range tests {
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 {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -577,7 +577,7 @@ func TestEvaluateWinningMargin(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateWinningMargin(tt.outcome, tt.score)
status, _ := EvaluateWinningMargin(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -605,7 +605,7 @@ func TestEvaluateDoubleResult(t *testing.T) {
for _, tt := range tests {
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 {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -632,7 +632,7 @@ func TestEvaluateHighestScoringPeriod(t *testing.T) {
for _, tt := range tests {
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 {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -656,7 +656,7 @@ func TestEvalauteTiedAfterRegulation(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateTiedAfterRegulation(tt.outcome, tt.score)
status, _ := EvaluateTiedAfterRegulation(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -680,7 +680,7 @@ func TestEvaluateTeamTotal(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateTeamTotal(tt.outcome, tt.score)
status, _ := EvaluateTeamTotal(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -703,7 +703,7 @@ func TestDrawNoBet(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateDrawNoBet(tt.outcome, tt.score)
status, _ := EvaluateDrawNoBet(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -727,7 +727,7 @@ func TestEvaluateMoneyLine(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateMoneyLine(tt.outcome, tt.score)
status, _ := EvaluateMoneyLine(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -751,7 +751,7 @@ func TestEvaluateDoubleChance(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateDoubleChance(tt.outcome, tt.score)
status, _ := EvaluateDoubleChance(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -775,7 +775,7 @@ func TestEvaluateResultAndTotal(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateResultAndTotal(tt.outcome, tt.score)
status, _ := EvaluateResultAndTotal(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -826,7 +826,7 @@ func TestEvaluateBTTSX(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateBTTSX(tt.outcome, tt.score)
status, _ := EvaluateBTTSX(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -852,7 +852,7 @@ func TestEvaluateResultAndBTTSX(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateResultAndBTTSX(tt.outcome, tt.score)
status, _ := EvaluateResultAndBTTSX(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -876,7 +876,7 @@ func TestEvaluateMoneyLine3Way(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateMoneyLine3Way(tt.outcome, tt.score)
status, _ := EvaluateMoneyLine3Way(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -937,7 +937,7 @@ func TestEvaluateAsianHandicap(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateAsianHandicap(tt.outcome, tt.score)
status, _ := EvaluateAsianHandicap(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
@ -971,7 +971,7 @@ func TestEvaluateHandicapAndTotal(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateHandicapAndTotal(tt.outcome, tt.score)
status, _ := EvaluateHandicapAndTotal(tt.outcome, tt.score)
if status != tt.expected {
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/bet"
"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/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
@ -44,6 +46,7 @@ type App struct {
userSvc *user.Service
betSvc *bet.Service
virtualGameSvc virtualgameservice.VirtualGameService
chapaSvc *chapa.Service
walletSvc *wallet.Service
transactionSvc *transaction.Service
ticketSvc *ticket.Service
@ -54,6 +57,7 @@ type App struct {
Logger *slog.Logger
prematchSvc *odds.ServiceImpl
eventSvc event.Service
leagueSvc league.Service
resultSvc *result.Service
}
@ -65,6 +69,7 @@ func NewApp(
userSvc *user.Service,
ticketSvc *ticket.Service,
betSvc *bet.Service,
chapaSvc *chapa.Service,
walletSvc *wallet.Service,
transactionSvc *transaction.Service,
branchSvc *branch.Service,
@ -72,6 +77,7 @@ func NewApp(
notidicationStore *notificationservice.Service,
prematchSvc *odds.ServiceImpl,
eventSvc event.Service,
leagueSvc league.Service,
referralSvc referralservice.ReferralStore,
virtualGameSvc virtualgameservice.VirtualGameService,
aleaVirtualGameService alea.AleaVirtualGameService,
@ -104,6 +110,7 @@ func NewApp(
userSvc: userSvc,
ticketSvc: ticketSvc,
betSvc: betSvc,
chapaSvc: chapaSvc,
walletSvc: walletSvc,
transactionSvc: transactionSvc,
branchSvc: branchSvc,
@ -113,6 +120,7 @@ func NewApp(
Logger: logger,
prematchSvc: prematchSvc,
eventSvc: eventSvc,
leagueSvc: leagueSvc,
virtualGameSvc: virtualGameSvc,
aleaVirtualGameService: aleaVirtualGameService,
veliVirtualGameService: veliVirtualGameService,

View File

@ -72,18 +72,28 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
// role := c.Locals("role").(domain.Role)
leagueIDQuery := c.Query("league_id")
sportIDQuery := c.Query("sport_id")
leagueIDQuery, err := strconv.Atoi(c.Query("league_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")
lastStartTimeQuery := c.Query("last_start_time")
leagueID := domain.ValidString{
Value: leagueIDQuery,
Valid: leagueIDQuery != "",
leagueID := domain.ValidInt32{
Value: int32(leagueIDQuery),
Valid: leagueIDQuery != 0,
}
sportID := domain.ValidString{
Value: sportIDQuery,
Valid: sportIDQuery != "",
sportID := domain.ValidInt32{
Value: int32(sportIDQuery),
Valid: sportIDQuery != 0,
}
var firstStartTime domain.ValidTime
@ -123,7 +133,6 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
}
var res domain.CreateBetRes
var err error
for i := 0; i < int(req.NumberOfBets); i++ {
res, err = h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, leagueID, sportID, firstStartTime, lastStartTime)

View File

@ -1,283 +1,464 @@
package handlers
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
// "bytes"
// "encoding/json"
// "fmt"
// "io"
// "net/http"
"fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
// GetBanks godoc
// @Summary Get list of banks
// @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)
// // GetBanks godoc
// // @Summary Get list of banks
// // @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()
// 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()})
}
// 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)
}
// 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(),
})
}
// // 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()
// // 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(),
})
}
// 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")
// 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()
// 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(),
})
}
// 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)
}
// 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",
})
}
// // 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)
// 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)
// 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()
// 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(),
})
}
// 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)
}
// return c.Status(resp.StatusCode).Type("json").Send(body)
// }
// ReceiveWebhook godoc
// @Summary Receive Chapa webhook
// @Description Endpoint to receive webhook payloads from Chapa
// // 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
// @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(),
})
// @Param payload body domain.ChapaTransactionType true "Webhook Payload"
// @Success 200 {object} domain.Response
// @Router /api/v1/chapa/payments/verify [post]
func (h *Handler) VerifyChapaPayment(c *fiber.Ctx) error {
var txType domain.ChapaTransactionType
if err := c.BodyParser(&txType); err != nil {
return domain.UnProcessableEntityResponse(c)
}
h.logger.Info("Chapa webhook received", "payload", payload)
switch txType.Type {
case "Payout":
var payload domain.ChapaWebHookTransfer
if err := c.BodyParser(&payload); err != nil {
return domain.UnProcessableEntityResponse(c)
}
// Optional: you can verify tx_ref here again if needed
if err := h.chapaSvc.HandleChapaTransferWebhook(c.Context(), payload); err != nil {
return domain.FiberErrorResponse(c, err)
}
return c.SendStatus(fiber.StatusOK)
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,
})
}
}
// 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
// WithdrawUsingChapa godoc
// @Summary Withdraw using Chapa
// @Description Initiates a withdrawal transaction using Chapa for the authenticated user.
// @Tags Chapa
// @Accept json
// @Produce json
// @Param request body domain.ChapaWithdrawRequest true "Chapa Withdraw Request"
// @Success 200 {object} domain.Response{data=string} "Withdrawal requested successfully"
// @Failure 400 {object} domain.Response "Invalid request"
// @Failure 401 {object} domain.Response "Unauthorized"
// @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 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid request",
"details": err.Error(),
return domain.UnProcessableEntityResponse(c)
}
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
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(),
})
if err := h.chapaSvc.WithdrawUsingChapa(c.Context(), userID, req); err != nil {
return domain.FiberErrorResponse(c, err)
}
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)
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Withdrawal requested successfully",
Success: true,
StatusCode: fiber.StatusOK,
})
}
// VerifyTransfer godoc
// @Summary Verify a transfer
// @Description Check the status of a money transfer via reference
// DepositUsingChapa godoc
// @Summary Deposit money into user wallet using Chapa
// @Description Deposits money into user wallet from user account using Chapa
// @Tags Chapa
// @Accept json
// @Produce json
// @Param payload body domain.ChapaDepositRequest true "Deposit request payload"
// @Success 200 {object} domain.ChapaPaymentUrlResponseWrapper
// @Failure 400 {object} domain.Response "Invalid request"
// @Failure 422 {object} domain.Response "Validation error"
// @Failure 500 {object} domain.Response "Internal server error"
// @Router /api/v1/chapa/payments/deposit [post]
func (h *Handler) DepositUsingChapa(c *fiber.Ctx) error {
// 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,
})
}
var req domain.ChapaDepositRequest
if err := c.BodyParser(&req); err != nil {
return domain.UnProcessableEntityResponse(c)
}
// 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
// @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)
// @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 {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to create HTTP request",
"details": err.Error(),
return c.Status(fiber.StatusInternalServerError).JSON(domain.Response{
Message: "Internal server error",
Success: false,
StatusCode: fiber.StatusInternalServerError,
})
}
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)
return c.Status(fiber.StatusOK).JSON(domain.ResponseWDataFactory[[]domain.ChapaSupportedBank]{
Data: banks,
Response: domain.Response{
Message: "read successful on chapa supported banks",
Success: true,
StatusCode: fiber.StatusOK,
},
})
}

View File

@ -7,8 +7,10 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"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/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
@ -30,6 +32,7 @@ type Handler struct {
notificationSvc *notificationservice.Service
userSvc *user.Service
referralSvc referralservice.ReferralStore
chapaSvc chapa.ChapaPort
walletSvc *wallet.Service
transactionSvc *transaction.Service
ticketSvc *ticket.Service
@ -38,6 +41,7 @@ type Handler struct {
companySvc *company.Service
prematchSvc *odds.ServiceImpl
eventSvc event.Service
leagueSvc league.Service
virtualGameSvc virtualgameservice.VirtualGameService
aleaVirtualGameSvc alea.AleaVirtualGameService
veliVirtualGameSvc veli.VeliVirtualGameService
@ -53,6 +57,7 @@ func New(
logger *slog.Logger,
notificationSvc *notificationservice.Service,
validator *customvalidator.CustomValidator,
chapaSvc chapa.ChapaPort,
walletSvc *wallet.Service,
referralSvc referralservice.ReferralStore,
virtualGameSvc virtualgameservice.VirtualGameService,
@ -69,12 +74,14 @@ func New(
companySvc *company.Service,
prematchSvc *odds.ServiceImpl,
eventSvc event.Service,
leagueSvc league.Service,
resultSvc result.Service,
cfg *config.Config,
) *Handler {
return &Handler{
logger: logger,
notificationSvc: notificationSvc,
chapaSvc: chapaSvc,
walletSvc: walletSvc,
referralSvc: referralSvc,
validator: validator,
@ -86,6 +93,7 @@ func New(
companySvc: companySvc,
prematchSvc: prematchSvc,
eventSvc: eventSvc,
leagueSvc: leagueSvc,
virtualGameSvc: virtualGameSvc,
aleaVirtualGameSvc: aleaVirtualGameSvc,
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 {
page := c.QueryInt("page", 1)
pageSize := c.QueryInt("page_size", 10)
leagueIDQuery := c.Query("league_id")
sportIDQuery := c.Query("sport_id")
leagueIDQuery, err := strconv.Atoi(c.Query("league_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")
lastStartTimeQuery := c.Query("last_start_time")
leagueID := domain.ValidString{
Value: leagueIDQuery,
Valid: leagueIDQuery != "",
leagueID := domain.ValidInt32{
Value: int32(leagueIDQuery),
Valid: leagueIDQuery != 0,
}
sportID := domain.ValidString{
Value: sportIDQuery,
Valid: sportIDQuery != "",
sportID := domain.ValidInt32{
Value: int32(sportIDQuery),
Valid: sportIDQuery != 0,
}
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.NotidicationStore,
a.validator,
a.chapaSvc,
a.walletSvc,
a.referralSvc,
a.virtualGameSvc,
@ -35,6 +36,7 @@ func (a *App) initAppRoutes() {
a.prematchSvc,
a.eventSvc,
*a.resultSvc,
a.leagueSvc,
a.cfg,
)
@ -114,13 +116,17 @@ func (a *App) initAppRoutes() {
a.fiber.Put("/managers/:id", a.authMiddleware, h.UpdateManagers)
a.fiber.Get("/manager/:id/branch", a.authMiddleware, h.GetBranchByManagerID)
a.fiber.Get("/prematch/odds/:event_id", h.GetPrematchOdds)
a.fiber.Get("/prematch/odds", h.GetALLPrematchOdds)
a.fiber.Get("/prematch/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID)
a.fiber.Get("/events/odds/:event_id", h.GetPrematchOdds)
a.fiber.Get("/events/odds", h.GetALLPrematchOdds)
a.fiber.Get("/events/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID)
a.fiber.Get("/prematch/events/:id", h.GetUpcomingEventByID)
a.fiber.Get("/prematch/events", h.GetAllUpcomingEvents)
a.fiber.Get("/prematch/odds/upcoming/:upcoming_id", h.GetPrematchOddsByUpcomingID)
a.fiber.Get("/events/:id", h.GetUpcomingEventByID)
a.fiber.Get("/events", h.GetAllUpcomingEvents)
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)
@ -184,13 +190,17 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet)
//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.Get("/chapa/payments/verify/:tx_ref", h.VerifyTransaction)
group.Post("/chapa/payments/callback", h.ReceiveWebhook)
group.Get("/chapa/banks", h.GetBanks)
group.Post("/chapa/transfers", h.CreateTransfer)
group.Get("/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer)
// group.Post("/chapa/payments/initialize", h.InitializePayment)
// group.Get("/chapa/payments/verify/:tx_ref", h.VerifyTransaction)
// group.Post("/chapa/payments/callback", h.ReceiveWebhook)
// group.Get("/chapa/banks", h.GetBanks)
// group.Post("/chapa/transfers", h.CreateTransfer)
// group.Get("/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer)
//Alea Play Virtual Game Routes
group.Get("/alea-play/launch", a.authMiddleware, h.LaunchAleaGame)

View File

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