manager resp struct fix+merge conflict fix

This commit is contained in:
Yared Yemane 2025-05-25 14:11:46 +03:00
commit 25ded17b09
86 changed files with 7005 additions and 1646 deletions

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
# Builder stage
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -o ./bin/web ./cmd/main.go
# Runner stage
FROM alpine:3.21 AS runner
WORKDIR /app
COPY .env .
COPY --from=builder /app/bin/web /app/bin/web
RUN apk add --no-cache ca-certificates
EXPOSE ${PORT}
CMD ["/app/bin/web"]

View File

@ -77,15 +77,14 @@ func main() {
userSvc := user.NewService(store, store, mockSms, mockEmail)
eventSvc := event.New(cfg.Bet365Token, store)
oddsSvc := odds.New(cfg.Bet365Token, store)
resultSvc := result.NewService(store, cfg, logger)
oddsSvc := odds.New(store, cfg, logger)
ticketSvc := ticket.NewService(store)
betSvc := bet.NewService(store)
walletSvc := wallet.NewService(store, store)
transactionSvc := transaction.NewService(store)
branchSvc := branch.NewService(store)
companySvc := company.NewService(store)
betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger)
resultSvc := result.NewService(store, cfg, logger, *betSvc)
notificationRepo := repository.NewNotificationRepository(store)
referalRepo := repository.NewReferralRepository(store)
vitualGameRepo := repository.NewVirtualGameRepository(store)
@ -108,6 +107,7 @@ func main() {
)
httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc)
httpserver.StartTicketCrons(*ticketSvc)
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
JwtAccessKey: cfg.JwtKey,

View File

@ -114,7 +114,6 @@ CREATE TABLE IF NOT EXISTS wallets (
CREATE TABLE IF NOT EXISTS customer_wallets (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL,
company_id BIGINT NOT NULL,
regular_wallet_id BIGINT NOT NULL,
static_wallet_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -234,12 +233,17 @@ CREATE TABLE companies (
wallet_id BIGINT NOT NULL
);
-- Views
CREATE VIEW companies_with_wallets AS
CREATE VIEW companies_details AS
SELECT companies.*,
wallets.balance,
wallets.is_active
wallets.is_active,
users.first_name AS admin_first_name,
users.last_name AS admin_last_name,
users.phone_number AS admin_phone_number
FROM companies
JOIN wallets ON wallets.id = companies.wallet_id;
JOIN wallets ON wallets.id = companies.wallet_id
JOIN users ON users.id = companies.admin_id;
;
CREATE VIEW branch_details AS
SELECT branches.*,
CONCAT(users.first_name, ' ', users.last_name) AS manager_name,
@ -290,11 +294,11 @@ ALTER TABLE branch_operations
ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERENCES supported_operations(id) ON DELETE CASCADE,
ADD CONSTRAINT fk_branch_operations_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE;
ALTER TABLE branch_cashiers
ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id),
ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id);
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);
ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE;
----------------------------------------------seed data-------------------------------------------------------------
-------------------------------------- DO NOT USE IN PRODUCTION-------------------------------------------------
CREATE EXTENSION IF NOT EXISTS pgcrypto;
@ -340,15 +344,43 @@ INSERT INTO users (
suspended_at,
suspended
)
VALUES (
'Test',
'Admin',
'test.admin@gmail.com',
'0988554466',
crypt('password123', gen_salt('bf'))::bytea,
'admin',
TRUE,
TRUE,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP,
NULL,
FALSE
);
INSERT INTO users (
first_name,
last_name,
email,
phone_number,
password,
role,
email_verified,
phone_verified,
created_at,
updated_at,
suspended_at,
suspended
)
VALUES (
'Samuel',
'Tariku',
'cybersamt@gmail.com',
NULL,
'0911111111',
crypt('password@123', gen_salt('bf'))::bytea,
'super_admin',
TRUE,
FALSE,
TRUE,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP,
NULL,
@ -372,11 +404,11 @@ VALUES (
'Kirubel',
'Kibru',
'kirubeljkl679 @gmail.com',
NULL,
'0911554486',
crypt('password@123', gen_salt('bf'))::bytea,
'super_admin',
TRUE,
FALSE,
TRUE,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP,
NULL,
@ -384,8 +416,7 @@ VALUES (
);
INSERT INTO supported_operations (name, description)
VALUES ('SportBook', 'Sportbook operations'),
('Virtual', 'Virtual operations'),
('GameZone', 'GameZone operations');
('Virtual', 'Virtual operations');
INSERT INTO wallets (
balance,
is_withdraw,
@ -406,3 +437,53 @@ VALUES (
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
INSERT INTO companies (
name,
admin_id,
wallet_id
)
values (
'Test Company',
2,
1
);
INSERT INTO wallets (
balance,
is_withdraw,
is_bettable,
is_transferable,
user_id,
is_active,
created_at,
updated_at
)
VALUES (
10000,
TRUE,
TRUE,
TRUE,
2,
TRUE,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
INSERT INTO branches (
name,
location,
wallet_id,
branch_manager_id,
company_id,
is_self_owned,
created_at,
updated_at
)
values (
'Test Branch',
'Addis Ababa',
2,
2,
1,
TRUE,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);

View File

@ -62,21 +62,25 @@ WHERE branch_id = $1;
SELECT *
FROM bet_outcomes
WHERE event_id = $1;
-- name: GetBetOutcomeByBetID :many
SELECT *
FROM bet_outcomes
WHERE bet_id = $1;
-- name: UpdateCashOut :exec
UPDATE bets
SET cashed_out = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: UpdateBetOutcomeStatus :exec
-- name: UpdateBetOutcomeStatus :one
UPDATE bet_outcomes
SET status = $1
WHERE id = $2;
WHERE id = $2
RETURNING *;
-- name: UpdateStatus :exec
UPDATE bets
SET status = $2,
SET status = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
WHERE id = $2;
-- name: DeleteBet :exec
DELETE FROM bets
WHERE id = $1;

View File

@ -55,15 +55,6 @@ SELECT branches.*
FROM branch_cashiers
JOIN branches ON branch_cashiers.branch_id = branches.id
WHERE branch_cashiers.user_id = $1;
-- name: GetCashiersByBranch :many
SELECT users.*
FROM branch_cashiers
JOIN users ON branch_cashiers.user_id = users.id
WHERE branch_cashiers.branch_id = $1;
-- name: GetAllCashiers :many
SELECT users.*
FROM branch_cashiers
JOIN users ON branch_cashiers.user_id = users.id;
-- name: UpdateBranch :one
UPDATE branches
SET name = COALESCE(sqlc.narg(name), name),

15
db/query/cashier.sql Normal file
View File

@ -0,0 +1,15 @@
-- name: GetCashiersByBranch :many
SELECT users.*
FROM branch_cashiers
JOIN users ON branch_cashiers.user_id = users.id
WHERE branch_cashiers.branch_id = $1;
-- name: GetAllCashiers :many
SELECT users.*,
branch_id
FROM branch_cashiers
JOIN users ON branch_cashiers.user_id = users.id;
-- name: GetCashierByID :one
SELECT users.*,
branch_id
FROM branch_cashiers
JOIN users ON branch_cashiers.user_id = $1;

View File

@ -8,14 +8,14 @@ VALUES ($1, $2, $3)
RETURNING *;
-- name: GetAllCompanies :many
SELECT *
FROM companies_with_wallets;
FROM companies_details;
-- name: GetCompanyByID :one
SELECT *
FROM companies_with_wallets
FROM companies_details
WHERE id = $1;
-- name: SearchCompanyByName :many
SELECT *
FROM companies_with_wallets
FROM companies_details
WHERE name ILIKE '%' || $1 || '%';
-- name: UpdateCompany :one
UPDATE companies

View File

@ -158,9 +158,7 @@ SELECT id,
status,
fetched_at
FROM events
WHERE is_live = false
AND status = 'upcoming'
AND start_time < now()
WHERE start_time < now()
ORDER BY start_time ASC;
-- name: GetTotalEvents :one
SELECT COUNT(*)
@ -168,12 +166,20 @@ FROM events
WHERE is_live = false
AND status = 'upcoming'
AND (
league_id = $1
OR $1 IS NULL
league_id = sqlc.narg('league_id')
OR sqlc.narg('league_id') IS NULL
)
AND (
sport_id = $2
OR $2 IS NULL
sport_id = sqlc.narg('sport_id')
OR sqlc.narg('sport_id') IS NULL
)
AND (
start_time < sqlc.narg('last_start_time')
OR sqlc.narg('last_start_time') IS NULL
)
AND (
start_time > sqlc.narg('first_start_time')
OR sqlc.narg('first_start_time') IS NULL
);
-- name: GetPaginatedUpcomingEvents :many
SELECT id,
@ -196,15 +202,23 @@ FROM events
WHERE is_live = false
AND status = 'upcoming'
AND (
league_id = $3
OR $3 IS NULL
league_id = sqlc.narg('league_id')
OR sqlc.narg('league_id') IS NULL
)
AND (
sport_id = $4
OR $4 IS NULL
sport_id = sqlc.narg('sport_id')
OR sqlc.narg('sport_id') IS NULL
)
AND (
start_time < sqlc.narg('last_start_time')
OR sqlc.narg('last_start_time') IS NULL
)
AND (
start_time > sqlc.narg('first_start_time')
OR sqlc.narg('first_start_time') IS NULL
)
ORDER BY start_time ASC
LIMIT $1 OFFSET $2;
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: GetUpcomingByID :one
SELECT id,
sport_id,

View File

@ -1,21 +1,71 @@
-- name: CreateNotification :one
INSERT INTO notifications (
id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, timestamp, metadata
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
) RETURNING *;
id,
recipient_id,
type,
level,
error_severity,
reciever,
is_read,
delivery_status,
delivery_channel,
payload,
priority,
timestamp,
metadata
)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13
)
RETURNING *;
-- name: GetNotification :one
SELECT * FROM notifications WHERE id = $1 LIMIT 1;
SELECT *
FROM notifications
WHERE id = $1
LIMIT 1;
-- name: GetAllNotifications :many
SELECT *
FROM notifications
ORDER BY timestamp DESC
LIMIT $1 OFFSET $2;
-- name: ListNotifications :many
SELECT * FROM notifications WHERE recipient_id = $1 ORDER BY timestamp DESC LIMIT $2 OFFSET $3;
SELECT *
FROM notifications
WHERE recipient_id = $1
ORDER BY timestamp DESC
LIMIT $2 OFFSET $3;
-- name: CountUnreadNotifications :one
SELECT count(id)
FROM notifications
WHERE recipient_id = $1
AND is_read = false;
-- name: UpdateNotificationStatus :one
UPDATE notifications SET delivery_status = $2, is_read = $3, metadata = $4 WHERE id = $1 RETURNING *;
UPDATE notifications
SET delivery_status = $2,
is_read = $3,
metadata = $4
WHERE id = $1
RETURNING *;
-- name: ListFailedNotifications :many
SELECT * FROM notifications WHERE delivery_status = 'failed' AND timestamp < NOW() - INTERVAL '1 hour' ORDER BY timestamp ASC LIMIT $1;
SELECT *
FROM notifications
WHERE delivery_status = 'failed'
AND timestamp < NOW() - INTERVAL '1 hour'
ORDER BY timestamp ASC
LIMIT $1;
-- name: ListRecipientIDsByReceiver :many
SELECT recipient_id FROM notifications WHERE reciever = $1;
SELECT recipient_id
FROM notifications
WHERE reciever = $1;

View File

@ -94,23 +94,17 @@ WHERE market_id = $1
AND fi = $2
AND is_active = true
AND source = 'b365api';
-- name: GetPrematchOddsByUpcomingID :many
SELECT o.event_id,
o.fi,
o.market_type,
o.market_name,
o.market_category,
o.market_id,
o.name,
o.handicap,
o.odds_value,
o.section,
o.category,
o.raw_odds,
o.fetched_at,
o.source,
o.is_active
SELECT o.*
FROM odds o
JOIN events e ON o.fi = e.id
WHERE e.id = $1
AND e.is_live = false
AND e.status = 'upcoming'
AND o.is_active = true
AND o.source = 'b365api';
-- name: GetPaginatedPrematchOddsByUpcomingID :many
SELECT o.*
FROM odds o
JOIN events e ON o.fi = e.id
WHERE e.id = $1
@ -118,4 +112,4 @@ WHERE e.id = $1
AND e.status = 'upcoming'
AND o.is_active = true
AND o.source = 'b365api'
LIMIT $2 OFFSET $3;
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');

View File

@ -66,7 +66,7 @@ wHERE (
company_id = $2
OR $2 IS NULL
)
LIMIT $3 OFFSET $4;
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: GetTotalUsers :one
SELECT COUNT(*)
FROM users
@ -93,23 +93,30 @@ SELECT id,
suspended_at,
company_id
FROM users
WHERE first_name ILIKE '%' || $1 || '%'
OR last_name ILIKE '%' || $1 || '%'
OR phone_number LIKE '%' || $1 || '%';
WHERE (
first_name ILIKE '%' || $1 || '%'
OR last_name ILIKE '%' || $1 || '%'
OR phone_number LIKE '%' || $1 || '%'
)
AND (
role = sqlc.narg('role')
OR sqlc.narg('role') IS NULL
)
AND (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
);
-- name: UpdateUser :exec
UPDATE users
SET first_name = $1,
last_name = $2,
email = $3,
phone_number = $4,
role = $5,
updated_at = $6
WHERE id = $7;
suspended = $3,
updated_at = CURRENT_TIMESTAMP
WHERE id = $4;
-- name: UpdateUserCompany :exec
UPDATE users
SET company_id = $1
WHERE id = $2;
-- name: SuspendUser :exec
UPDATE users
SET suspended = $1,

View File

@ -10,11 +10,10 @@ RETURNING *;
-- name: CreateCustomerWallet :one
INSERT INTO customer_wallets (
customer_id,
company_id,
regular_wallet_id,
static_wallet_id
)
VALUES ($1, $2, $3, $4)
VALUES ($1, $2, $3)
RETURNING *;
-- name: GetAllWallets :many
SELECT *
@ -30,7 +29,6 @@ WHERE user_id = $1;
-- name: GetCustomerWallet :one
SELECT cw.id,
cw.customer_id,
cw.company_id,
rw.id AS regular_id,
rw.balance AS regular_balance,
sw.id AS static_id,
@ -41,8 +39,7 @@ SELECT cw.id,
FROM customer_wallets cw
JOIN wallets rw ON cw.regular_wallet_id = rw.id
JOIN wallets sw ON cw.static_wallet_id = sw.id
WHERE cw.customer_id = $1
AND cw.company_id = $2;
WHERE cw.customer_id = $1;
-- name: GetAllBranchWallets :many
SELECT wallets.id,
wallets.balance,

View File

@ -14,6 +14,9 @@ services:
interval: 5s
timeout: 3s
retries: 5
volumes:
- postgres_data:/var/lib/postgresql/data
migrate:
image: migrate/migrate
volumes:
@ -32,6 +35,37 @@ services:
networks:
- app
app:
build:
context: .
dockerfile: Dockerfile
target: runner
ports:
- ${PORT}:8080
environment:
- DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable
depends_on:
migrate:
condition: service_completed_successfully
networks:
- app
command: ["/app/bin/web"]
test:
build:
context: .
dockerfile: Dockerfile
target: builder
volumes:
- .:/app
command: ["tail", "-f", "/dev/null"]
networks:
- app
networks:
app:
driver: bridge
volumes:
postgres_data:

View File

@ -129,6 +129,106 @@ const docTemplate = `{
}
}
},
"/admin/{id}": {
"get": {
"description": "Get a single admin by id",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Get admin by id",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.AdminRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
},
"put": {
"description": "Update Admin",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Update Admin",
"parameters": [
{
"description": "Update Admin",
"name": "admin",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.updateAdminReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/alea-games/launch": {
"get": {
"security": [
@ -687,7 +787,7 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.BetRes"
"$ref": "#/definitions/domain.BetRes"
}
}
},
@ -724,7 +824,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.CreateBetReq"
"$ref": "#/definitions/domain.CreateBetReq"
}
}
],
@ -732,7 +832,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.BetRes"
"$ref": "#/definitions/domain.BetRes"
}
},
"400": {
@ -776,7 +876,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.BetRes"
"$ref": "#/definitions/domain.BetRes"
}
},
"400": {
@ -820,7 +920,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.BetRes"
"$ref": "#/definitions/domain.BetRes"
}
},
"400": {
@ -1169,7 +1269,54 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.BetRes"
"$ref": "#/definitions/domain.BetRes"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/branch/{id}/cashier": {
"get": {
"description": "Gets branch cashiers",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"branch"
],
"summary": "Gets branch cashiers",
"parameters": [
{
"type": "integer",
"description": "Branch ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.GetCashierRes"
}
}
},
@ -1324,6 +1471,56 @@ const docTemplate = `{
}
}
},
"/cashier/{id}": {
"get": {
"description": "Get a single cashier by id",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"cashier"
],
"summary": "Get cashier by id",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.UserProfileRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/cashiers": {
"get": {
"description": "Get all cashiers",
@ -1456,7 +1653,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.updateUserReq"
"$ref": "#/definitions/handlers.updateCashierReq"
}
}
],
@ -1907,6 +2104,54 @@ const docTemplate = `{
}
},
"/managers/{id}": {
"get": {
"description": "Get a single manager by id",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"manager"
],
"summary": "Get manager by id",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.ManagersRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
},
"put": {
"description": "Update Managers",
"consumes": [
@ -1916,7 +2161,7 @@ const docTemplate = `{
"application/json"
],
"tags": [
"Managers"
"manager"
],
"summary": "Update Managers",
"parameters": [
@ -1926,7 +2171,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.updateUserReq"
"$ref": "#/definitions/handlers.updateManagerReq"
}
}
],
@ -2041,6 +2286,18 @@ const docTemplate = `{
"description": "Sport ID Filter",
"name": "sport_id",
"in": "query"
},
{
"type": "string",
"description": "Start Time",
"name": "first_start_time",
"in": "query"
},
{
"type": "string",
"description": "End Time",
"name": "last_start_time",
"in": "query"
}
],
"responses": {
@ -2298,6 +2555,52 @@ const docTemplate = `{
}
}
},
"/random/bet": {
"post": {
"description": "Generate a random bet",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"bet"
],
"summary": "Generate a random bet",
"parameters": [
{
"description": "Create Random bet",
"name": "createBet",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.RandomBetReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.BetRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/referral/settings": {
"get": {
"security": [
@ -3093,6 +3396,50 @@ const docTemplate = `{
}
}
},
"/user/delete/{id}": {
"delete": {
"description": "Delete a user by their ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Delete user by ID",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/user/profile": {
"get": {
"security": [
@ -3389,7 +3736,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/handlers.UserProfileRes"
}
},
"400": {
@ -3413,6 +3760,52 @@ const docTemplate = `{
}
}
},
"/user/suspend": {
"post": {
"description": "Suspend or unsuspend a user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Suspend or unsuspend a user",
"parameters": [
{
"description": "Suspend or unsuspend a user",
"name": "updateUserSuspend",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.UpdateUserSuspendReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.UpdateUserSuspendRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/user/wallet": {
"get": {
"security": [
@ -3881,6 +4274,65 @@ const docTemplate = `{
}
}
},
"domain.BetRes": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"branch_id": {
"type": "integer",
"example": 2
},
"cashed_id": {
"type": "string",
"example": "21234"
},
"cashed_out": {
"type": "boolean",
"example": false
},
"full_name": {
"type": "string",
"example": "John"
},
"id": {
"type": "integer",
"example": 1
},
"is_shop_bet": {
"type": "boolean",
"example": false
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.BetOutcome"
}
},
"phone_number": {
"type": "string",
"example": "1234567890"
},
"status": {
"allOf": [
{
"$ref": "#/definitions/domain.OutcomeStatus"
}
],
"example": 1
},
"total_odds": {
"type": "number",
"example": 4.22
},
"user_id": {
"type": "integer",
"example": 2
}
}
},
"domain.ChapaSupportedBank": {
"type": "object",
"properties": {
@ -3948,6 +4400,58 @@ const docTemplate = `{
}
}
},
"domain.CreateBetOutcomeReq": {
"type": "object",
"properties": {
"event_id": {
"type": "integer",
"example": 1
},
"market_id": {
"type": "integer",
"example": 1
},
"odd_id": {
"type": "integer",
"example": 1
}
}
},
"domain.CreateBetReq": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"branch_id": {
"type": "integer",
"example": 1
},
"full_name": {
"type": "string",
"example": "John"
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.CreateBetOutcomeReq"
}
},
"phone_number": {
"type": "string",
"example": "1234567890"
},
"status": {
"allOf": [
{
"$ref": "#/definitions/domain.OutcomeStatus"
}
],
"example": 1
}
}
},
"domain.CreateTransferResponse": {
"type": "object",
"properties": {
@ -4076,9 +4580,11 @@ const docTemplate = `{
1,
2,
3,
4
4,
5
],
"x-enum-comments": {
"OUTCOME_STATUS_ERROR": "Half Win and Half Given Back",
"OUTCOME_STATUS_HALF": "Half Win and Half Given Back",
"OUTCOME_STATUS_VOID": "Give Back"
},
@ -4087,7 +4593,8 @@ const docTemplate = `{
"OUTCOME_STATUS_WIN",
"OUTCOME_STATUS_LOSS",
"OUTCOME_STATUS_VOID",
"OUTCOME_STATUS_HALF"
"OUTCOME_STATUS_HALF",
"OUTCOME_STATUS_ERROR"
]
},
"domain.PaymentOption": {
@ -4133,6 +4640,23 @@ const docTemplate = `{
}
}
},
"domain.RandomBetReq": {
"type": "object",
"required": [
"branch_id",
"number_of_bets"
],
"properties": {
"branch_id": {
"type": "integer",
"example": 1
},
"number_of_bets": {
"type": "integer",
"example": 1
}
}
},
"domain.RawOddsByMarketID": {
"type": "object",
"properties": {
@ -4542,65 +5066,6 @@ const docTemplate = `{
}
}
},
"handlers.BetRes": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"branch_id": {
"type": "integer",
"example": 2
},
"cashed_id": {
"type": "string",
"example": "21234"
},
"cashed_out": {
"type": "boolean",
"example": false
},
"full_name": {
"type": "string",
"example": "John"
},
"id": {
"type": "integer",
"example": 1
},
"is_shop_bet": {
"type": "boolean",
"example": false
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.BetOutcome"
}
},
"phone_number": {
"type": "string",
"example": "1234567890"
},
"status": {
"allOf": [
{
"$ref": "#/definitions/domain.OutcomeStatus"
}
],
"example": 1
},
"total_odds": {
"type": "number",
"example": 4.22
},
"user_id": {
"type": "integer",
"example": 2
}
}
},
"handlers.BranchDetailRes": {
"type": "object",
"properties": {
@ -4762,58 +5227,6 @@ const docTemplate = `{
}
}
},
"handlers.CreateBetOutcomeReq": {
"type": "object",
"properties": {
"event_id": {
"type": "integer",
"example": 1
},
"market_id": {
"type": "integer",
"example": 1
},
"odd_id": {
"type": "integer",
"example": 1
}
}
},
"handlers.CreateBetReq": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"branch_id": {
"type": "integer",
"example": 1
},
"full_name": {
"type": "string",
"example": "John"
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.CreateBetOutcomeReq"
}
},
"phone_number": {
"type": "string",
"example": "1234567890"
},
"status": {
"allOf": [
{
"$ref": "#/definitions/domain.OutcomeStatus"
}
],
"example": 1
}
}
},
"handlers.CreateBranchOperationReq": {
"type": "object",
"properties": {
@ -5074,10 +5487,6 @@ const docTemplate = `{
"handlers.CustomerWalletRes": {
"type": "object",
"properties": {
"company_id": {
"type": "integer",
"example": 1
},
"created_at": {
"type": "string"
},
@ -5113,6 +5522,53 @@ const docTemplate = `{
}
}
},
"handlers.GetCashierRes": {
"type": "object",
"properties": {
"branch_id": {
"type": "integer"
},
"created_at": {
"type": "string"
},
"email": {
"type": "string"
},
"email_verified": {
"type": "boolean"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"phone_verified": {
"type": "boolean"
},
"role": {
"$ref": "#/definitions/domain.Role"
},
"suspended": {
"type": "boolean"
},
"suspended_at": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"handlers.ManagersRes": {
"type": "object",
"properties": {
@ -5245,8 +5701,11 @@ const docTemplate = `{
"handlers.SearchUserByNameOrPhoneReq": {
"type": "object",
"properties": {
"searchString": {
"query": {
"type": "string"
},
"role": {
"$ref": "#/definitions/domain.Role"
}
}
},
@ -5463,6 +5922,34 @@ const docTemplate = `{
}
}
},
"handlers.UpdateUserSuspendReq": {
"type": "object",
"required": [
"suspended",
"user_id"
],
"properties": {
"suspended": {
"type": "boolean",
"example": true
},
"user_id": {
"type": "integer",
"example": 123
}
}
},
"handlers.UpdateUserSuspendRes": {
"type": "object",
"properties": {
"suspended": {
"type": "boolean"
},
"user_id": {
"type": "integer"
}
}
},
"handlers.UpdateWalletActiveReq": {
"type": "object",
"required": [
@ -5655,9 +6142,51 @@ const docTemplate = `{
}
}
},
"handlers.updateUserReq": {
"handlers.updateAdminReq": {
"type": "object",
"properties": {
"company_id": {
"type": "integer",
"example": 1
},
"first_name": {
"type": "string",
"example": "John"
},
"last_name": {
"type": "string",
"example": "Doe"
},
"suspended": {
"type": "boolean",
"example": false
}
}
},
"handlers.updateCashierReq": {
"type": "object",
"properties": {
"first_name": {
"type": "string",
"example": "John"
},
"last_name": {
"type": "string",
"example": "Doe"
},
"suspended": {
"type": "boolean",
"example": false
}
}
},
"handlers.updateManagerReq": {
"type": "object",
"properties": {
"company_id": {
"type": "integer",
"example": 1
},
"first_name": {
"type": "string",
"example": "John"

View File

@ -121,6 +121,106 @@
}
}
},
"/admin/{id}": {
"get": {
"description": "Get a single admin by id",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Get admin by id",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.AdminRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
},
"put": {
"description": "Update Admin",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Update Admin",
"parameters": [
{
"description": "Update Admin",
"name": "admin",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.updateAdminReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/alea-games/launch": {
"get": {
"security": [
@ -679,7 +779,7 @@
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.BetRes"
"$ref": "#/definitions/domain.BetRes"
}
}
},
@ -716,7 +816,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.CreateBetReq"
"$ref": "#/definitions/domain.CreateBetReq"
}
}
],
@ -724,7 +824,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.BetRes"
"$ref": "#/definitions/domain.BetRes"
}
},
"400": {
@ -768,7 +868,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.BetRes"
"$ref": "#/definitions/domain.BetRes"
}
},
"400": {
@ -812,7 +912,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.BetRes"
"$ref": "#/definitions/domain.BetRes"
}
},
"400": {
@ -1161,7 +1261,54 @@
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.BetRes"
"$ref": "#/definitions/domain.BetRes"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/branch/{id}/cashier": {
"get": {
"description": "Gets branch cashiers",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"branch"
],
"summary": "Gets branch cashiers",
"parameters": [
{
"type": "integer",
"description": "Branch ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.GetCashierRes"
}
}
},
@ -1316,6 +1463,56 @@
}
}
},
"/cashier/{id}": {
"get": {
"description": "Get a single cashier by id",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"cashier"
],
"summary": "Get cashier by id",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.UserProfileRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/cashiers": {
"get": {
"description": "Get all cashiers",
@ -1448,7 +1645,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.updateUserReq"
"$ref": "#/definitions/handlers.updateCashierReq"
}
}
],
@ -1899,6 +2096,54 @@
}
},
"/managers/{id}": {
"get": {
"description": "Get a single manager by id",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"manager"
],
"summary": "Get manager by id",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.ManagersRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
},
"put": {
"description": "Update Managers",
"consumes": [
@ -1908,7 +2153,7 @@
"application/json"
],
"tags": [
"Managers"
"manager"
],
"summary": "Update Managers",
"parameters": [
@ -1918,7 +2163,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.updateUserReq"
"$ref": "#/definitions/handlers.updateManagerReq"
}
}
],
@ -2033,6 +2278,18 @@
"description": "Sport ID Filter",
"name": "sport_id",
"in": "query"
},
{
"type": "string",
"description": "Start Time",
"name": "first_start_time",
"in": "query"
},
{
"type": "string",
"description": "End Time",
"name": "last_start_time",
"in": "query"
}
],
"responses": {
@ -2290,6 +2547,52 @@
}
}
},
"/random/bet": {
"post": {
"description": "Generate a random bet",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"bet"
],
"summary": "Generate a random bet",
"parameters": [
{
"description": "Create Random bet",
"name": "createBet",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.RandomBetReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.BetRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/referral/settings": {
"get": {
"security": [
@ -3085,6 +3388,50 @@
}
}
},
"/user/delete/{id}": {
"delete": {
"description": "Delete a user by their ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Delete user by ID",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/user/profile": {
"get": {
"security": [
@ -3381,7 +3728,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/handlers.UserProfileRes"
}
},
"400": {
@ -3405,6 +3752,52 @@
}
}
},
"/user/suspend": {
"post": {
"description": "Suspend or unsuspend a user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Suspend or unsuspend a user",
"parameters": [
{
"description": "Suspend or unsuspend a user",
"name": "updateUserSuspend",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.UpdateUserSuspendReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.UpdateUserSuspendRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/user/wallet": {
"get": {
"security": [
@ -3873,6 +4266,65 @@
}
}
},
"domain.BetRes": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"branch_id": {
"type": "integer",
"example": 2
},
"cashed_id": {
"type": "string",
"example": "21234"
},
"cashed_out": {
"type": "boolean",
"example": false
},
"full_name": {
"type": "string",
"example": "John"
},
"id": {
"type": "integer",
"example": 1
},
"is_shop_bet": {
"type": "boolean",
"example": false
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.BetOutcome"
}
},
"phone_number": {
"type": "string",
"example": "1234567890"
},
"status": {
"allOf": [
{
"$ref": "#/definitions/domain.OutcomeStatus"
}
],
"example": 1
},
"total_odds": {
"type": "number",
"example": 4.22
},
"user_id": {
"type": "integer",
"example": 2
}
}
},
"domain.ChapaSupportedBank": {
"type": "object",
"properties": {
@ -3940,6 +4392,58 @@
}
}
},
"domain.CreateBetOutcomeReq": {
"type": "object",
"properties": {
"event_id": {
"type": "integer",
"example": 1
},
"market_id": {
"type": "integer",
"example": 1
},
"odd_id": {
"type": "integer",
"example": 1
}
}
},
"domain.CreateBetReq": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"branch_id": {
"type": "integer",
"example": 1
},
"full_name": {
"type": "string",
"example": "John"
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.CreateBetOutcomeReq"
}
},
"phone_number": {
"type": "string",
"example": "1234567890"
},
"status": {
"allOf": [
{
"$ref": "#/definitions/domain.OutcomeStatus"
}
],
"example": 1
}
}
},
"domain.CreateTransferResponse": {
"type": "object",
"properties": {
@ -4068,9 +4572,11 @@
1,
2,
3,
4
4,
5
],
"x-enum-comments": {
"OUTCOME_STATUS_ERROR": "Half Win and Half Given Back",
"OUTCOME_STATUS_HALF": "Half Win and Half Given Back",
"OUTCOME_STATUS_VOID": "Give Back"
},
@ -4079,7 +4585,8 @@
"OUTCOME_STATUS_WIN",
"OUTCOME_STATUS_LOSS",
"OUTCOME_STATUS_VOID",
"OUTCOME_STATUS_HALF"
"OUTCOME_STATUS_HALF",
"OUTCOME_STATUS_ERROR"
]
},
"domain.PaymentOption": {
@ -4125,6 +4632,23 @@
}
}
},
"domain.RandomBetReq": {
"type": "object",
"required": [
"branch_id",
"number_of_bets"
],
"properties": {
"branch_id": {
"type": "integer",
"example": 1
},
"number_of_bets": {
"type": "integer",
"example": 1
}
}
},
"domain.RawOddsByMarketID": {
"type": "object",
"properties": {
@ -4534,65 +5058,6 @@
}
}
},
"handlers.BetRes": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"branch_id": {
"type": "integer",
"example": 2
},
"cashed_id": {
"type": "string",
"example": "21234"
},
"cashed_out": {
"type": "boolean",
"example": false
},
"full_name": {
"type": "string",
"example": "John"
},
"id": {
"type": "integer",
"example": 1
},
"is_shop_bet": {
"type": "boolean",
"example": false
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.BetOutcome"
}
},
"phone_number": {
"type": "string",
"example": "1234567890"
},
"status": {
"allOf": [
{
"$ref": "#/definitions/domain.OutcomeStatus"
}
],
"example": 1
},
"total_odds": {
"type": "number",
"example": 4.22
},
"user_id": {
"type": "integer",
"example": 2
}
}
},
"handlers.BranchDetailRes": {
"type": "object",
"properties": {
@ -4754,58 +5219,6 @@
}
}
},
"handlers.CreateBetOutcomeReq": {
"type": "object",
"properties": {
"event_id": {
"type": "integer",
"example": 1
},
"market_id": {
"type": "integer",
"example": 1
},
"odd_id": {
"type": "integer",
"example": 1
}
}
},
"handlers.CreateBetReq": {
"type": "object",
"properties": {
"amount": {
"type": "number",
"example": 100
},
"branch_id": {
"type": "integer",
"example": 1
},
"full_name": {
"type": "string",
"example": "John"
},
"outcomes": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.CreateBetOutcomeReq"
}
},
"phone_number": {
"type": "string",
"example": "1234567890"
},
"status": {
"allOf": [
{
"$ref": "#/definitions/domain.OutcomeStatus"
}
],
"example": 1
}
}
},
"handlers.CreateBranchOperationReq": {
"type": "object",
"properties": {
@ -5066,10 +5479,6 @@
"handlers.CustomerWalletRes": {
"type": "object",
"properties": {
"company_id": {
"type": "integer",
"example": 1
},
"created_at": {
"type": "string"
},
@ -5105,6 +5514,53 @@
}
}
},
"handlers.GetCashierRes": {
"type": "object",
"properties": {
"branch_id": {
"type": "integer"
},
"created_at": {
"type": "string"
},
"email": {
"type": "string"
},
"email_verified": {
"type": "boolean"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"phone_verified": {
"type": "boolean"
},
"role": {
"$ref": "#/definitions/domain.Role"
},
"suspended": {
"type": "boolean"
},
"suspended_at": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"handlers.ManagersRes": {
"type": "object",
"properties": {
@ -5237,8 +5693,11 @@
"handlers.SearchUserByNameOrPhoneReq": {
"type": "object",
"properties": {
"searchString": {
"query": {
"type": "string"
},
"role": {
"$ref": "#/definitions/domain.Role"
}
}
},
@ -5455,6 +5914,34 @@
}
}
},
"handlers.UpdateUserSuspendReq": {
"type": "object",
"required": [
"suspended",
"user_id"
],
"properties": {
"suspended": {
"type": "boolean",
"example": true
},
"user_id": {
"type": "integer",
"example": 123
}
}
},
"handlers.UpdateUserSuspendRes": {
"type": "object",
"properties": {
"suspended": {
"type": "boolean"
},
"user_id": {
"type": "integer"
}
}
},
"handlers.UpdateWalletActiveReq": {
"type": "object",
"required": [
@ -5647,9 +6134,51 @@
}
}
},
"handlers.updateUserReq": {
"handlers.updateAdminReq": {
"type": "object",
"properties": {
"company_id": {
"type": "integer",
"example": 1
},
"first_name": {
"type": "string",
"example": "John"
},
"last_name": {
"type": "string",
"example": "Doe"
},
"suspended": {
"type": "boolean",
"example": false
}
}
},
"handlers.updateCashierReq": {
"type": "object",
"properties": {
"first_name": {
"type": "string",
"example": "John"
},
"last_name": {
"type": "string",
"example": "Doe"
},
"suspended": {
"type": "boolean",
"example": false
}
}
},
"handlers.updateManagerReq": {
"type": "object",
"properties": {
"company_id": {
"type": "integer",
"example": 1
},
"first_name": {
"type": "string",
"example": "John"

View File

@ -80,6 +80,47 @@ definitions:
- $ref: '#/definitions/domain.OutcomeStatus'
example: 1
type: object
domain.BetRes:
properties:
amount:
example: 100
type: number
branch_id:
example: 2
type: integer
cashed_id:
example: "21234"
type: string
cashed_out:
example: false
type: boolean
full_name:
example: John
type: string
id:
example: 1
type: integer
is_shop_bet:
example: false
type: boolean
outcomes:
items:
$ref: '#/definitions/domain.BetOutcome'
type: array
phone_number:
example: "1234567890"
type: string
status:
allOf:
- $ref: '#/definitions/domain.OutcomeStatus'
example: 1
total_odds:
example: 4.22
type: number
user_id:
example: 2
type: integer
type: object
domain.ChapaSupportedBank:
properties:
acct_length:
@ -124,6 +165,41 @@ definitions:
message:
type: string
type: object
domain.CreateBetOutcomeReq:
properties:
event_id:
example: 1
type: integer
market_id:
example: 1
type: integer
odd_id:
example: 1
type: integer
type: object
domain.CreateBetReq:
properties:
amount:
example: 100
type: number
branch_id:
example: 1
type: integer
full_name:
example: John
type: string
outcomes:
items:
$ref: '#/definitions/domain.CreateBetOutcomeReq'
type: array
phone_number:
example: "1234567890"
type: string
status:
allOf:
- $ref: '#/definitions/domain.OutcomeStatus'
example: 1
type: object
domain.CreateTransferResponse:
properties:
data:
@ -211,8 +287,10 @@ definitions:
- 2
- 3
- 4
- 5
type: integer
x-enum-comments:
OUTCOME_STATUS_ERROR: Half Win and Half Given Back
OUTCOME_STATUS_HALF: Half Win and Half Given Back
OUTCOME_STATUS_VOID: Give Back
x-enum-varnames:
@ -221,6 +299,7 @@ definitions:
- OUTCOME_STATUS_LOSS
- OUTCOME_STATUS_VOID
- OUTCOME_STATUS_HALF
- OUTCOME_STATUS_ERROR
domain.PaymentOption:
enum:
- 0
@ -252,6 +331,18 @@ definitions:
description: BET, WIN, REFUND, JACKPOT_WIN
type: string
type: object
domain.RandomBetReq:
properties:
branch_id:
example: 1
type: integer
number_of_bets:
example: 1
type: integer
required:
- branch_id
- number_of_bets
type: object
domain.RawOddsByMarketID:
properties:
fetched_at:
@ -534,47 +625,6 @@ definitions:
updated_at:
type: string
type: object
handlers.BetRes:
properties:
amount:
example: 100
type: number
branch_id:
example: 2
type: integer
cashed_id:
example: "21234"
type: string
cashed_out:
example: false
type: boolean
full_name:
example: John
type: string
id:
example: 1
type: integer
is_shop_bet:
example: false
type: boolean
outcomes:
items:
$ref: '#/definitions/domain.BetOutcome'
type: array
phone_number:
example: "1234567890"
type: string
status:
allOf:
- $ref: '#/definitions/domain.OutcomeStatus'
example: 1
total_odds:
example: 4.22
type: number
user_id:
example: 2
type: integer
type: object
handlers.BranchDetailRes:
properties:
branch_manager_id:
@ -690,41 +740,6 @@ definitions:
example: "1234567890"
type: string
type: object
handlers.CreateBetOutcomeReq:
properties:
event_id:
example: 1
type: integer
market_id:
example: 1
type: integer
odd_id:
example: 1
type: integer
type: object
handlers.CreateBetReq:
properties:
amount:
example: 100
type: number
branch_id:
example: 1
type: integer
full_name:
example: John
type: string
outcomes:
items:
$ref: '#/definitions/handlers.CreateBetOutcomeReq'
type: array
phone_number:
example: "1234567890"
type: string
status:
allOf:
- $ref: '#/definitions/domain.OutcomeStatus'
example: 1
type: object
handlers.CreateBranchOperationReq:
properties:
branch_id:
@ -909,9 +924,6 @@ definitions:
type: object
handlers.CustomerWalletRes:
properties:
company_id:
example: 1
type: integer
created_at:
type: string
customer_id:
@ -937,6 +949,37 @@ definitions:
static_updated_at:
type: string
type: object
handlers.GetCashierRes:
properties:
branch_id:
type: integer
created_at:
type: string
email:
type: string
email_verified:
type: boolean
first_name:
type: string
id:
type: integer
last_login:
type: string
last_name:
type: string
phone_number:
type: string
phone_verified:
type: boolean
role:
$ref: '#/definitions/domain.Role'
suspended:
type: boolean
suspended_at:
type: string
updated_at:
type: string
type: object
handlers.ManagersRes:
properties:
created_at:
@ -1029,8 +1072,10 @@ definitions:
type: object
handlers.SearchUserByNameOrPhoneReq:
properties:
searchString:
query:
type: string
role:
$ref: '#/definitions/domain.Role'
type: object
handlers.SupportedOperationRes:
properties:
@ -1182,6 +1227,25 @@ definitions:
example: true
type: boolean
type: object
handlers.UpdateUserSuspendReq:
properties:
suspended:
example: true
type: boolean
user_id:
example: 123
type: integer
required:
- suspended
- user_id
type: object
handlers.UpdateUserSuspendRes:
properties:
suspended:
type: boolean
user_id:
type: integer
type: object
handlers.UpdateWalletActiveReq:
properties:
is_active:
@ -1314,8 +1378,38 @@ definitions:
- access_token
- refresh_token
type: object
handlers.updateUserReq:
handlers.updateAdminReq:
properties:
company_id:
example: 1
type: integer
first_name:
example: John
type: string
last_name:
example: Doe
type: string
suspended:
example: false
type: boolean
type: object
handlers.updateCashierReq:
properties:
first_name:
example: John
type: string
last_name:
example: Doe
type: string
suspended:
example: false
type: boolean
type: object
handlers.updateManagerReq:
properties:
company_id:
example: 1
type: integer
first_name:
example: John
type: string
@ -1431,6 +1525,72 @@ paths:
summary: Create Admin
tags:
- admin
/admin/{id}:
get:
consumes:
- application/json
description: Get a single admin by id
parameters:
- description: User ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.AdminRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Get admin by id
tags:
- admin
put:
consumes:
- application/json
description: Update Admin
parameters:
- description: Update Admin
in: body
name: admin
required: true
schema:
$ref: '#/definitions/handlers.updateAdminReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Update Admin
tags:
- admin
/api/v1/alea-games/launch:
get:
consumes:
@ -1791,7 +1951,7 @@ paths:
description: OK
schema:
items:
$ref: '#/definitions/handlers.BetRes'
$ref: '#/definitions/domain.BetRes'
type: array
"400":
description: Bad Request
@ -1814,14 +1974,14 @@ paths:
name: createBet
required: true
schema:
$ref: '#/definitions/handlers.CreateBetReq'
$ref: '#/definitions/domain.CreateBetReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.BetRes'
$ref: '#/definitions/domain.BetRes'
"400":
description: Bad Request
schema:
@ -1878,7 +2038,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.BetRes'
$ref: '#/definitions/domain.BetRes'
"400":
description: Bad Request
schema:
@ -1941,7 +2101,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.BetRes'
$ref: '#/definitions/domain.BetRes'
"400":
description: Bad Request
schema:
@ -2110,7 +2270,7 @@ paths:
description: OK
schema:
items:
$ref: '#/definitions/handlers.BetRes'
$ref: '#/definitions/domain.BetRes'
type: array
"400":
description: Bad Request
@ -2123,6 +2283,37 @@ paths:
summary: Gets bets by its branch id
tags:
- branch
/branch/{id}/cashier:
get:
consumes:
- application/json
description: Gets branch cashiers
parameters:
- description: Branch ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/handlers.GetCashierRes'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Gets branch cashiers
tags:
- branch
/branch/{id}/operation:
get:
consumes:
@ -2213,6 +2404,39 @@ paths:
summary: Get all branch wallets
tags:
- wallet
/cashier/{id}:
get:
consumes:
- application/json
description: Get a single cashier by id
parameters:
- description: User ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.UserProfileRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Get cashier by id
tags:
- cashier
/cashiers:
get:
consumes:
@ -2298,7 +2522,7 @@ paths:
name: cashier
required: true
schema:
$ref: '#/definitions/handlers.updateUserReq'
$ref: '#/definitions/handlers.updateCashierReq'
produces:
- application/json
responses:
@ -2598,6 +2822,38 @@ paths:
tags:
- manager
/managers/{id}:
get:
consumes:
- application/json
description: Get a single manager by id
parameters:
- description: User ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.ManagersRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Get manager by id
tags:
- manager
put:
consumes:
- application/json
@ -2608,7 +2864,7 @@ paths:
name: Managers
required: true
schema:
$ref: '#/definitions/handlers.updateUserReq'
$ref: '#/definitions/handlers.updateManagerReq'
produces:
- application/json
responses:
@ -2630,7 +2886,7 @@ paths:
$ref: '#/definitions/response.APIResponse'
summary: Update Managers
tags:
- Managers
- manager
/operation:
post:
consumes:
@ -2683,6 +2939,14 @@ paths:
in: query
name: sport_id
type: string
- description: Start Time
in: query
name: first_start_time
type: string
- description: End Time
in: query
name: last_start_time
type: string
produces:
- application/json
responses:
@ -2856,6 +3120,36 @@ paths:
summary: Retrieve raw odds by Market ID
tags:
- prematch
/random/bet:
post:
consumes:
- application/json
description: Generate a random bet
parameters:
- description: Create Random bet
in: body
name: createBet
required: true
schema:
$ref: '#/definitions/domain.RandomBetReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.BetRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Generate a random bet
tags:
- bet
/referral/settings:
get:
consumes:
@ -3375,6 +3669,35 @@ paths:
summary: Check if phone number or email exist
tags:
- user
/user/delete/{id}:
delete:
consumes:
- application/json
description: Delete a user by their ID
parameters:
- description: User ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Delete user by ID
tags:
- user
/user/profile:
get:
consumes:
@ -3567,7 +3890,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
$ref: '#/definitions/handlers.UserProfileRes'
"400":
description: Bad Request
schema:
@ -3583,6 +3906,36 @@ paths:
summary: Get user by id
tags:
- user
/user/suspend:
post:
consumes:
- application/json
description: Suspend or unsuspend a user
parameters:
- description: Suspend or unsuspend a user
in: body
name: updateUserSuspend
required: true
schema:
$ref: '#/definitions/handlers.UpdateUserSuspendReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlers.UpdateUserSuspendRes'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Suspend or unsuspend a user
tags:
- user
/user/wallet:
get:
consumes:

View File

@ -243,6 +243,48 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (BetWithOutcome, err
return i, err
}
const GetBetOutcomeByBetID = `-- name: GetBetOutcomeByBetID :many
SELECT id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires
FROM bet_outcomes
WHERE bet_id = $1
`
func (q *Queries) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]BetOutcome, error) {
rows, err := q.db.Query(ctx, GetBetOutcomeByBetID, betID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BetOutcome
for rows.Next() {
var i BetOutcome
if err := rows.Scan(
&i.ID,
&i.BetID,
&i.SportID,
&i.EventID,
&i.OddID,
&i.HomeTeamName,
&i.AwayTeamName,
&i.MarketID,
&i.MarketName,
&i.Odd,
&i.OddName,
&i.OddHeader,
&i.OddHandicap,
&i.Status,
&i.Expires,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetBetOutcomeByEventID = `-- name: GetBetOutcomeByEventID :many
SELECT id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires
FROM bet_outcomes
@ -285,10 +327,11 @@ func (q *Queries) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]
return items, nil
}
const UpdateBetOutcomeStatus = `-- name: UpdateBetOutcomeStatus :exec
const UpdateBetOutcomeStatus = `-- name: UpdateBetOutcomeStatus :one
UPDATE bet_outcomes
SET status = $1
WHERE id = $2
RETURNING id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires
`
type UpdateBetOutcomeStatusParams struct {
@ -296,9 +339,27 @@ type UpdateBetOutcomeStatusParams struct {
ID int64 `json:"id"`
}
func (q *Queries) UpdateBetOutcomeStatus(ctx context.Context, arg UpdateBetOutcomeStatusParams) error {
_, err := q.db.Exec(ctx, UpdateBetOutcomeStatus, arg.Status, arg.ID)
return err
func (q *Queries) UpdateBetOutcomeStatus(ctx context.Context, arg UpdateBetOutcomeStatusParams) (BetOutcome, error) {
row := q.db.QueryRow(ctx, UpdateBetOutcomeStatus, arg.Status, arg.ID)
var i BetOutcome
err := row.Scan(
&i.ID,
&i.BetID,
&i.SportID,
&i.EventID,
&i.OddID,
&i.HomeTeamName,
&i.AwayTeamName,
&i.MarketID,
&i.MarketName,
&i.Odd,
&i.OddName,
&i.OddHeader,
&i.OddHandicap,
&i.Status,
&i.Expires,
)
return i, err
}
const UpdateCashOut = `-- name: UpdateCashOut :exec
@ -320,17 +381,17 @@ func (q *Queries) UpdateCashOut(ctx context.Context, arg UpdateCashOutParams) er
const UpdateStatus = `-- name: UpdateStatus :exec
UPDATE bets
SET status = $2,
SET status = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
WHERE id = $2
`
type UpdateStatusParams struct {
ID int64 `json:"id"`
Status int32 `json:"status"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateStatus(ctx context.Context, arg UpdateStatusParams) error {
_, err := q.db.Exec(ctx, UpdateStatus, arg.ID, arg.Status)
_, err := q.db.Exec(ctx, UpdateStatus, arg.Status, arg.ID)
return err
}

View File

@ -190,49 +190,6 @@ func (q *Queries) GetAllBranches(ctx context.Context) ([]BranchDetail, error) {
return items, nil
}
const GetAllCashiers = `-- name: GetAllCashiers :many
SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by
FROM branch_cashiers
JOIN users ON branch_cashiers.user_id = users.id
`
func (q *Queries) GetAllCashiers(ctx context.Context) ([]User, error) {
rows, err := q.db.Query(ctx, GetAllCashiers)
if err != nil {
return nil, err
}
defer rows.Close()
var items []User
for rows.Next() {
var i User
if err := rows.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Role,
&i.Password,
&i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt,
&i.UpdatedAt,
&i.CompanyID,
&i.SuspendedAt,
&i.Suspended,
&i.ReferralCode,
&i.ReferredBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetAllSupportedOperations = `-- name: GetAllSupportedOperations :many
SELECT id, name, description
FROM supported_operations
@ -430,50 +387,6 @@ func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]Ge
return items, nil
}
const GetCashiersByBranch = `-- name: GetCashiersByBranch :many
SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by
FROM branch_cashiers
JOIN users ON branch_cashiers.user_id = users.id
WHERE branch_cashiers.branch_id = $1
`
func (q *Queries) GetCashiersByBranch(ctx context.Context, branchID int64) ([]User, error) {
rows, err := q.db.Query(ctx, GetCashiersByBranch, branchID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []User
for rows.Next() {
var i User
if err := rows.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Role,
&i.Password,
&i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt,
&i.UpdatedAt,
&i.CompanyID,
&i.SuspendedAt,
&i.Suspended,
&i.ReferralCode,
&i.ReferredBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const SearchBranchByName = `-- name: SearchBranchByName :many
SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number
FROM branch_details

173
gen/db/cashier.sql.go Normal file
View File

@ -0,0 +1,173 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// source: cashier.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const GetAllCashiers = `-- name: GetAllCashiers :many
SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by,
branch_id
FROM branch_cashiers
JOIN users ON branch_cashiers.user_id = users.id
`
type GetAllCashiersRow struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"`
Password []byte `json:"password"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
CompanyID pgtype.Int8 `json:"company_id"`
SuspendedAt pgtype.Timestamptz `json:"suspended_at"`
Suspended bool `json:"suspended"`
ReferralCode pgtype.Text `json:"referral_code"`
ReferredBy pgtype.Text `json:"referred_by"`
BranchID int64 `json:"branch_id"`
}
func (q *Queries) GetAllCashiers(ctx context.Context) ([]GetAllCashiersRow, error) {
rows, err := q.db.Query(ctx, GetAllCashiers)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllCashiersRow
for rows.Next() {
var i GetAllCashiersRow
if err := rows.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Role,
&i.Password,
&i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt,
&i.UpdatedAt,
&i.CompanyID,
&i.SuspendedAt,
&i.Suspended,
&i.ReferralCode,
&i.ReferredBy,
&i.BranchID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetCashierByID = `-- name: GetCashierByID :one
SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by,
branch_id
FROM branch_cashiers
JOIN users ON branch_cashiers.user_id = $1
`
type GetCashierByIDRow struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"`
Password []byte `json:"password"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
CompanyID pgtype.Int8 `json:"company_id"`
SuspendedAt pgtype.Timestamptz `json:"suspended_at"`
Suspended bool `json:"suspended"`
ReferralCode pgtype.Text `json:"referral_code"`
ReferredBy pgtype.Text `json:"referred_by"`
BranchID int64 `json:"branch_id"`
}
func (q *Queries) GetCashierByID(ctx context.Context, userID int64) (GetCashierByIDRow, error) {
row := q.db.QueryRow(ctx, GetCashierByID, userID)
var i GetCashierByIDRow
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Role,
&i.Password,
&i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt,
&i.UpdatedAt,
&i.CompanyID,
&i.SuspendedAt,
&i.Suspended,
&i.ReferralCode,
&i.ReferredBy,
&i.BranchID,
)
return i, err
}
const GetCashiersByBranch = `-- name: GetCashiersByBranch :many
SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by
FROM branch_cashiers
JOIN users ON branch_cashiers.user_id = users.id
WHERE branch_cashiers.branch_id = $1
`
func (q *Queries) GetCashiersByBranch(ctx context.Context, branchID int64) ([]User, error) {
rows, err := q.db.Query(ctx, GetCashiersByBranch, branchID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []User
for rows.Next() {
var i User
if err := rows.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Role,
&i.Password,
&i.EmailVerified,
&i.PhoneVerified,
&i.CreatedAt,
&i.UpdatedAt,
&i.CompanyID,
&i.SuspendedAt,
&i.Suspended,
&i.ReferralCode,
&i.ReferredBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -50,19 +50,19 @@ func (q *Queries) DeleteCompany(ctx context.Context, id int64) error {
}
const GetAllCompanies = `-- name: GetAllCompanies :many
SELECT id, name, admin_id, wallet_id, balance, is_active
FROM companies_with_wallets
SELECT id, name, admin_id, wallet_id, balance, is_active, admin_first_name, admin_last_name, admin_phone_number
FROM companies_details
`
func (q *Queries) GetAllCompanies(ctx context.Context) ([]CompaniesWithWallet, error) {
func (q *Queries) GetAllCompanies(ctx context.Context) ([]CompaniesDetail, error) {
rows, err := q.db.Query(ctx, GetAllCompanies)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CompaniesWithWallet
var items []CompaniesDetail
for rows.Next() {
var i CompaniesWithWallet
var i CompaniesDetail
if err := rows.Scan(
&i.ID,
&i.Name,
@ -70,6 +70,9 @@ func (q *Queries) GetAllCompanies(ctx context.Context) ([]CompaniesWithWallet, e
&i.WalletID,
&i.Balance,
&i.IsActive,
&i.AdminFirstName,
&i.AdminLastName,
&i.AdminPhoneNumber,
); err != nil {
return nil, err
}
@ -82,14 +85,14 @@ func (q *Queries) GetAllCompanies(ctx context.Context) ([]CompaniesWithWallet, e
}
const GetCompanyByID = `-- name: GetCompanyByID :one
SELECT id, name, admin_id, wallet_id, balance, is_active
FROM companies_with_wallets
SELECT id, name, admin_id, wallet_id, balance, is_active, admin_first_name, admin_last_name, admin_phone_number
FROM companies_details
WHERE id = $1
`
func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (CompaniesWithWallet, error) {
func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (CompaniesDetail, error) {
row := q.db.QueryRow(ctx, GetCompanyByID, id)
var i CompaniesWithWallet
var i CompaniesDetail
err := row.Scan(
&i.ID,
&i.Name,
@ -97,25 +100,28 @@ func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (CompaniesWithWa
&i.WalletID,
&i.Balance,
&i.IsActive,
&i.AdminFirstName,
&i.AdminLastName,
&i.AdminPhoneNumber,
)
return i, err
}
const SearchCompanyByName = `-- name: SearchCompanyByName :many
SELECT id, name, admin_id, wallet_id, balance, is_active
FROM companies_with_wallets
SELECT id, name, admin_id, wallet_id, balance, is_active, admin_first_name, admin_last_name, admin_phone_number
FROM companies_details
WHERE name ILIKE '%' || $1 || '%'
`
func (q *Queries) SearchCompanyByName(ctx context.Context, dollar_1 pgtype.Text) ([]CompaniesWithWallet, error) {
func (q *Queries) SearchCompanyByName(ctx context.Context, dollar_1 pgtype.Text) ([]CompaniesDetail, error) {
rows, err := q.db.Query(ctx, SearchCompanyByName, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CompaniesWithWallet
var items []CompaniesDetail
for rows.Next() {
var i CompaniesWithWallet
var i CompaniesDetail
if err := rows.Scan(
&i.ID,
&i.Name,
@ -123,6 +129,9 @@ func (q *Queries) SearchCompanyByName(ctx context.Context, dollar_1 pgtype.Text)
&i.WalletID,
&i.Balance,
&i.IsActive,
&i.AdminFirstName,
&i.AdminLastName,
&i.AdminPhoneNumber,
); err != nil {
return nil, err
}

View File

@ -118,9 +118,7 @@ SELECT id,
status,
fetched_at
FROM events
WHERE is_live = false
AND status = 'upcoming'
AND start_time < now()
WHERE start_time < now()
ORDER BY start_time ASC
`
@ -201,22 +199,32 @@ FROM events
WHERE is_live = false
AND status = 'upcoming'
AND (
league_id = $3
league_id = $1
OR $1 IS NULL
)
AND (
sport_id = $2
OR $2 IS NULL
)
AND (
start_time < $3
OR $3 IS NULL
)
AND (
sport_id = $4
start_time > $4
OR $4 IS NULL
)
ORDER BY start_time ASC
LIMIT $1 OFFSET $2
LIMIT $6 OFFSET $5
`
type GetPaginatedUpcomingEventsParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
LeagueID pgtype.Text `json:"league_id"`
SportID pgtype.Text `json:"sport_id"`
LeagueID pgtype.Text `json:"league_id"`
SportID pgtype.Text `json:"sport_id"`
LastStartTime pgtype.Timestamp `json:"last_start_time"`
FirstStartTime pgtype.Timestamp `json:"first_start_time"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type GetPaginatedUpcomingEventsRow struct {
@ -240,10 +248,12 @@ type GetPaginatedUpcomingEventsRow struct {
func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginatedUpcomingEventsParams) ([]GetPaginatedUpcomingEventsRow, error) {
rows, err := q.db.Query(ctx, GetPaginatedUpcomingEvents,
arg.Limit,
arg.Offset,
arg.LeagueID,
arg.SportID,
arg.LastStartTime,
arg.FirstStartTime,
arg.Offset,
arg.Limit,
)
if err != nil {
return nil, err
@ -293,15 +303,30 @@ WHERE is_live = false
sport_id = $2
OR $2 IS NULL
)
AND (
start_time < $3
OR $3 IS NULL
)
AND (
start_time > $4
OR $4 IS NULL
)
`
type GetTotalEventsParams struct {
LeagueID pgtype.Text `json:"league_id"`
SportID pgtype.Text `json:"sport_id"`
LeagueID pgtype.Text `json:"league_id"`
SportID pgtype.Text `json:"sport_id"`
LastStartTime pgtype.Timestamp `json:"last_start_time"`
FirstStartTime pgtype.Timestamp `json:"first_start_time"`
}
func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) (int64, error) {
row := q.db.QueryRow(ctx, GetTotalEvents, arg.LeagueID, arg.SportID)
row := q.db.QueryRow(ctx, GetTotalEvents,
arg.LeagueID,
arg.SportID,
arg.LastStartTime,
arg.FirstStartTime,
)
var count int64
err := row.Scan(&count)
return count, err

View File

@ -146,13 +146,16 @@ type BranchOperation struct {
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type CompaniesWithWallet struct {
ID int64 `json:"id"`
Name string `json:"name"`
AdminID int64 `json:"admin_id"`
WalletID int64 `json:"wallet_id"`
Balance int64 `json:"balance"`
IsActive bool `json:"is_active"`
type CompaniesDetail struct {
ID int64 `json:"id"`
Name string `json:"name"`
AdminID int64 `json:"admin_id"`
WalletID int64 `json:"wallet_id"`
Balance int64 `json:"balance"`
IsActive bool `json:"is_active"`
AdminFirstName string `json:"admin_first_name"`
AdminLastName string `json:"admin_last_name"`
AdminPhoneNumber pgtype.Text `json:"admin_phone_number"`
}
type Company struct {
@ -165,7 +168,6 @@ type Company struct {
type CustomerWallet struct {
ID int64 `json:"id"`
CustomerID int64 `json:"customer_id"`
CompanyID int64 `json:"company_id"`
RegularWalletID int64 `json:"regular_wallet_id"`
StaticWalletID int64 `json:"static_wallet_id"`
CreatedAt pgtype.Timestamp `json:"created_at"`

View File

@ -11,12 +11,52 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const CountUnreadNotifications = `-- name: CountUnreadNotifications :one
SELECT count(id)
FROM notifications
WHERE recipient_id = $1
AND is_read = false
`
func (q *Queries) CountUnreadNotifications(ctx context.Context, recipientID int64) (int64, error) {
row := q.db.QueryRow(ctx, CountUnreadNotifications, recipientID)
var count int64
err := row.Scan(&count)
return count, err
}
const CreateNotification = `-- name: CreateNotification :one
INSERT INTO notifications (
id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, timestamp, metadata
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
) RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
id,
recipient_id,
type,
level,
error_severity,
reciever,
is_read,
delivery_status,
delivery_channel,
payload,
priority,
timestamp,
metadata
)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13
)
RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
`
type CreateNotificationParams struct {
@ -71,8 +111,58 @@ func (q *Queries) CreateNotification(ctx context.Context, arg CreateNotification
return i, err
}
const GetAllNotifications = `-- name: GetAllNotifications :many
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
FROM notifications
ORDER BY timestamp DESC
LIMIT $1 OFFSET $2
`
type GetAllNotificationsParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) GetAllNotifications(ctx context.Context, arg GetAllNotificationsParams) ([]Notification, error) {
rows, err := q.db.Query(ctx, GetAllNotifications, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Notification
for rows.Next() {
var i Notification
if err := rows.Scan(
&i.ID,
&i.RecipientID,
&i.Type,
&i.Level,
&i.ErrorSeverity,
&i.Reciever,
&i.IsRead,
&i.DeliveryStatus,
&i.DeliveryChannel,
&i.Payload,
&i.Priority,
&i.Version,
&i.Timestamp,
&i.Metadata,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetNotification = `-- name: GetNotification :one
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata FROM notifications WHERE id = $1 LIMIT 1
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
FROM notifications
WHERE id = $1
LIMIT 1
`
func (q *Queries) GetNotification(ctx context.Context, id string) (Notification, error) {
@ -98,7 +188,12 @@ func (q *Queries) GetNotification(ctx context.Context, id string) (Notification,
}
const ListFailedNotifications = `-- name: ListFailedNotifications :many
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata FROM notifications WHERE delivery_status = 'failed' AND timestamp < NOW() - INTERVAL '1 hour' ORDER BY timestamp ASC LIMIT $1
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
FROM notifications
WHERE delivery_status = 'failed'
AND timestamp < NOW() - INTERVAL '1 hour'
ORDER BY timestamp ASC
LIMIT $1
`
func (q *Queries) ListFailedNotifications(ctx context.Context, limit int32) ([]Notification, error) {
@ -137,7 +232,11 @@ func (q *Queries) ListFailedNotifications(ctx context.Context, limit int32) ([]N
}
const ListNotifications = `-- name: ListNotifications :many
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata FROM notifications WHERE recipient_id = $1 ORDER BY timestamp DESC LIMIT $2 OFFSET $3
SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
FROM notifications
WHERE recipient_id = $1
ORDER BY timestamp DESC
LIMIT $2 OFFSET $3
`
type ListNotificationsParams struct {
@ -182,7 +281,9 @@ func (q *Queries) ListNotifications(ctx context.Context, arg ListNotificationsPa
}
const ListRecipientIDsByReceiver = `-- name: ListRecipientIDsByReceiver :many
SELECT recipient_id FROM notifications WHERE reciever = $1
SELECT recipient_id
FROM notifications
WHERE reciever = $1
`
func (q *Queries) ListRecipientIDsByReceiver(ctx context.Context, reciever string) ([]int64, error) {
@ -206,7 +307,12 @@ func (q *Queries) ListRecipientIDsByReceiver(ctx context.Context, reciever strin
}
const UpdateNotificationStatus = `-- name: UpdateNotificationStatus :one
UPDATE notifications SET delivery_status = $2, is_read = $3, metadata = $4 WHERE id = $1 RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
UPDATE notifications
SET delivery_status = $2,
is_read = $3,
metadata = $4
WHERE id = $1
RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata
`
type UpdateNotificationStatusParams struct {

View File

@ -86,6 +86,61 @@ func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]GetALLPrematchOddsR
return items, nil
}
const GetPaginatedPrematchOddsByUpcomingID = `-- name: GetPaginatedPrematchOddsByUpcomingID :many
SELECT o.id, o.event_id, o.fi, o.market_type, o.market_name, o.market_category, o.market_id, o.name, o.handicap, o.odds_value, o.section, o.category, o.raw_odds, o.fetched_at, o.source, o.is_active
FROM odds o
JOIN events e ON o.fi = e.id
WHERE e.id = $1
AND e.is_live = false
AND e.status = 'upcoming'
AND o.is_active = true
AND o.source = 'b365api'
LIMIT $3 OFFSET $2
`
type GetPaginatedPrematchOddsByUpcomingIDParams struct {
ID string `json:"id"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
func (q *Queries) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, arg GetPaginatedPrematchOddsByUpcomingIDParams) ([]Odd, error) {
rows, err := q.db.Query(ctx, GetPaginatedPrematchOddsByUpcomingID, arg.ID, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Odd
for rows.Next() {
var i Odd
if err := rows.Scan(
&i.ID,
&i.EventID,
&i.Fi,
&i.MarketType,
&i.MarketName,
&i.MarketCategory,
&i.MarketID,
&i.Name,
&i.Handicap,
&i.OddsValue,
&i.Section,
&i.Category,
&i.RawOdds,
&i.FetchedAt,
&i.Source,
&i.IsActive,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetPrematchOdds = `-- name: GetPrematchOdds :many
SELECT event_id,
fi,
@ -162,21 +217,7 @@ func (q *Queries) GetPrematchOdds(ctx context.Context) ([]GetPrematchOddsRow, er
}
const GetPrematchOddsByUpcomingID = `-- name: GetPrematchOddsByUpcomingID :many
SELECT o.event_id,
o.fi,
o.market_type,
o.market_name,
o.market_category,
o.market_id,
o.name,
o.handicap,
o.odds_value,
o.section,
o.category,
o.raw_odds,
o.fetched_at,
o.source,
o.is_active
SELECT o.id, o.event_id, o.fi, o.market_type, o.market_name, o.market_category, o.market_id, o.name, o.handicap, o.odds_value, o.section, o.category, o.raw_odds, o.fetched_at, o.source, o.is_active
FROM odds o
JOIN events e ON o.fi = e.id
WHERE e.id = $1
@ -184,43 +225,19 @@ WHERE e.id = $1
AND e.status = 'upcoming'
AND o.is_active = true
AND o.source = 'b365api'
LIMIT $2 OFFSET $3
`
type GetPrematchOddsByUpcomingIDParams struct {
ID string `json:"id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type GetPrematchOddsByUpcomingIDRow struct {
EventID pgtype.Text `json:"event_id"`
Fi pgtype.Text `json:"fi"`
MarketType string `json:"market_type"`
MarketName pgtype.Text `json:"market_name"`
MarketCategory pgtype.Text `json:"market_category"`
MarketID pgtype.Text `json:"market_id"`
Name pgtype.Text `json:"name"`
Handicap pgtype.Text `json:"handicap"`
OddsValue pgtype.Float8 `json:"odds_value"`
Section string `json:"section"`
Category pgtype.Text `json:"category"`
RawOdds []byte `json:"raw_odds"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
Source pgtype.Text `json:"source"`
IsActive pgtype.Bool `json:"is_active"`
}
func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, arg GetPrematchOddsByUpcomingIDParams) ([]GetPrematchOddsByUpcomingIDRow, error) {
rows, err := q.db.Query(ctx, GetPrematchOddsByUpcomingID, arg.ID, arg.Limit, arg.Offset)
func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, id string) ([]Odd, error) {
rows, err := q.db.Query(ctx, GetPrematchOddsByUpcomingID, id)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetPrematchOddsByUpcomingIDRow
var items []Odd
for rows.Next() {
var i GetPrematchOddsByUpcomingIDRow
var i Odd
if err := rows.Scan(
&i.ID,
&i.EventID,
&i.Fi,
&i.MarketType,

View File

@ -182,14 +182,14 @@ wHERE (
company_id = $2
OR $2 IS NULL
)
LIMIT $3 OFFSET $4
LIMIT $4 OFFSET $3
`
type GetAllUsersParams struct {
Role string `json:"role"`
CompanyID pgtype.Int8 `json:"company_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type GetAllUsersRow struct {
@ -212,8 +212,8 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
rows, err := q.db.Query(ctx, GetAllUsers,
arg.Role,
arg.CompanyID,
arg.Limit,
arg.Offset,
arg.Limit,
)
if err != nil {
return nil, err
@ -427,11 +427,27 @@ SELECT id,
suspended_at,
company_id
FROM users
WHERE first_name ILIKE '%' || $1 || '%'
OR last_name ILIKE '%' || $1 || '%'
OR phone_number LIKE '%' || $1 || '%'
WHERE (
first_name ILIKE '%' || $1 || '%'
OR last_name ILIKE '%' || $1 || '%'
OR phone_number LIKE '%' || $1 || '%'
)
AND (
role = $2
OR $2 IS NULL
)
AND (
company_id = $3
OR $3 IS NULL
)
`
type SearchUserByNameOrPhoneParams struct {
Column1 pgtype.Text `json:"column_1"`
Role pgtype.Text `json:"role"`
CompanyID pgtype.Int8 `json:"company_id"`
}
type SearchUserByNameOrPhoneRow struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
@ -448,8 +464,8 @@ type SearchUserByNameOrPhoneRow struct {
CompanyID pgtype.Int8 `json:"company_id"`
}
func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, dollar_1 pgtype.Text) ([]SearchUserByNameOrPhoneRow, error) {
rows, err := q.db.Query(ctx, SearchUserByNameOrPhone, dollar_1)
func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByNameOrPhoneParams) ([]SearchUserByNameOrPhoneRow, error) {
rows, err := q.db.Query(ctx, SearchUserByNameOrPhone, arg.Column1, arg.Role, arg.CompanyID)
if err != nil {
return nil, err
}
@ -532,31 +548,23 @@ const UpdateUser = `-- name: UpdateUser :exec
UPDATE users
SET first_name = $1,
last_name = $2,
email = $3,
phone_number = $4,
role = $5,
updated_at = $6
WHERE id = $7
suspended = $3,
updated_at = CURRENT_TIMESTAMP
WHERE id = $4
`
type UpdateUserParams struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Suspended bool `json:"suspended"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
_, err := q.db.Exec(ctx, UpdateUser,
arg.FirstName,
arg.LastName,
arg.Email,
arg.PhoneNumber,
arg.Role,
arg.UpdatedAt,
arg.Suspended,
arg.ID,
)
return err

View File

@ -14,33 +14,25 @@ import (
const CreateCustomerWallet = `-- name: CreateCustomerWallet :one
INSERT INTO customer_wallets (
customer_id,
company_id,
regular_wallet_id,
static_wallet_id
)
VALUES ($1, $2, $3, $4)
RETURNING id, customer_id, company_id, regular_wallet_id, static_wallet_id, created_at, updated_at
VALUES ($1, $2, $3)
RETURNING id, customer_id, regular_wallet_id, static_wallet_id, created_at, updated_at
`
type CreateCustomerWalletParams struct {
CustomerID int64 `json:"customer_id"`
CompanyID int64 `json:"company_id"`
RegularWalletID int64 `json:"regular_wallet_id"`
StaticWalletID int64 `json:"static_wallet_id"`
}
func (q *Queries) CreateCustomerWallet(ctx context.Context, arg CreateCustomerWalletParams) (CustomerWallet, error) {
row := q.db.QueryRow(ctx, CreateCustomerWallet,
arg.CustomerID,
arg.CompanyID,
arg.RegularWalletID,
arg.StaticWalletID,
)
row := q.db.QueryRow(ctx, CreateCustomerWallet, arg.CustomerID, arg.RegularWalletID, arg.StaticWalletID)
var i CustomerWallet
err := row.Scan(
&i.ID,
&i.CustomerID,
&i.CompanyID,
&i.RegularWalletID,
&i.StaticWalletID,
&i.CreatedAt,
@ -190,7 +182,6 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) {
const GetCustomerWallet = `-- name: GetCustomerWallet :one
SELECT cw.id,
cw.customer_id,
cw.company_id,
rw.id AS regular_id,
rw.balance AS regular_balance,
sw.id AS static_id,
@ -202,18 +193,11 @@ FROM customer_wallets cw
JOIN wallets rw ON cw.regular_wallet_id = rw.id
JOIN wallets sw ON cw.static_wallet_id = sw.id
WHERE cw.customer_id = $1
AND cw.company_id = $2
`
type GetCustomerWalletParams struct {
CustomerID int64 `json:"customer_id"`
CompanyID int64 `json:"company_id"`
}
type GetCustomerWalletRow struct {
ID int64 `json:"id"`
CustomerID int64 `json:"customer_id"`
CompanyID int64 `json:"company_id"`
RegularID int64 `json:"regular_id"`
RegularBalance int64 `json:"regular_balance"`
StaticID int64 `json:"static_id"`
@ -223,13 +207,12 @@ type GetCustomerWalletRow struct {
CreatedAt pgtype.Timestamp `json:"created_at"`
}
func (q *Queries) GetCustomerWallet(ctx context.Context, arg GetCustomerWalletParams) (GetCustomerWalletRow, error) {
row := q.db.QueryRow(ctx, GetCustomerWallet, arg.CustomerID, arg.CompanyID)
func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (GetCustomerWalletRow, error) {
row := q.db.QueryRow(ctx, GetCustomerWallet, customerID)
var i GetCustomerWalletRow
err := row.Scan(
&i.ID,
&i.CustomerID,
&i.CompanyID,
&i.RegularID,
&i.RegularBalance,
&i.StaticID,

13
go.mod
View File

@ -7,24 +7,28 @@ require (
github.com/bytedance/sonic v1.13.2
github.com/go-playground/validator/v10 v10.26.0
github.com/gofiber/fiber/v2 v2.52.6
github.com/gofiber/websocket/v2 v2.2.1
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
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/stretchr/testify v1.10.0
github.com/swaggo/fiber-swagger v1.3.0
github.com/swaggo/swag v1.16.4
golang.org/x/crypto v0.36.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
)
require (
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/fasthttp/websocket v1.5.8 // 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
@ -32,7 +36,7 @@ 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/gofiber/contrib/websocket v1.3.4
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
@ -46,11 +50,10 @@ require (
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // 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 // 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

8
go.sum
View File

@ -22,8 +22,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fasthttp/websocket v1.5.8 h1:k5DpirKkftIF/w1R8ZzjSgARJrs54Je9YJK37DL/Ah8=
github.com/fasthttp/websocket v1.5.8/go.mod h1:d08g8WaT6nnyvg9uMm8K9zMYyDjfKyj3170AtPRuVU0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@ -52,13 +50,13 @@ github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAu
github.com/gofiber/fiber/v2 v2.32.0/go.mod h1:CMy5ZLiXkn6qwthrl03YMyW1NLfj0rhxz2LKl4t7ZTY=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@ -114,8 +112,6 @@ 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/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8=
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
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=

View File

@ -80,3 +80,83 @@ type CreateBet struct {
IsShopBet bool
CashoutID string
}
type CreateBetOutcomeReq struct {
EventID int64 `json:"event_id" example:"1"`
OddID int64 `json:"odd_id" example:"1"`
MarketID int64 `json:"market_id" example:"1"`
}
type CreateBetReq struct {
Outcomes []CreateBetOutcomeReq `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
Status OutcomeStatus `json:"status" example:"1"`
FullName string `json:"full_name" example:"John"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
BranchID *int64 `json:"branch_id,omitempty" example:"1"`
}
type RandomBetReq struct {
BranchID int64 `json:"branch_id" validate:"required" example:"1"`
NumberOfBets int64 `json:"number_of_bets" validate:"required" example:"1"`
}
type CreateBetRes struct {
ID int64 `json:"id" example:"1"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
Status OutcomeStatus `json:"status" example:"1"`
FullName string `json:"full_name" example:"John"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
BranchID int64 `json:"branch_id" example:"2"`
UserID int64 `json:"user_id" example:"2"`
IsShopBet bool `json:"is_shop_bet" example:"false"`
CreatedNumber int64 `json:"created_number" example:"2"`
CashedID string `json:"cashed_id" example:"21234"`
}
type BetRes struct {
ID int64 `json:"id" example:"1"`
Outcomes []BetOutcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
Status OutcomeStatus `json:"status" example:"1"`
FullName string `json:"full_name" example:"John"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
BranchID int64 `json:"branch_id" example:"2"`
UserID int64 `json:"user_id" example:"2"`
IsShopBet bool `json:"is_shop_bet" example:"false"`
CashedOut bool `json:"cashed_out" example:"false"`
CashedID string `json:"cashed_id" example:"21234"`
}
func ConvertCreateBet(bet Bet, createdNumber int64) CreateBetRes {
return CreateBetRes{
ID: bet.ID,
Amount: bet.Amount.Float32(),
TotalOdds: bet.TotalOdds,
Status: bet.Status,
FullName: bet.FullName,
PhoneNumber: bet.PhoneNumber,
BranchID: bet.BranchID.Value,
UserID: bet.UserID.Value,
CreatedNumber: createdNumber,
CashedID: bet.CashoutID,
}
}
func ConvertBet(bet GetBet) BetRes {
return BetRes{
ID: bet.ID,
Amount: bet.Amount.Float32(),
TotalOdds: bet.TotalOdds,
Status: bet.Status,
FullName: bet.FullName,
PhoneNumber: bet.PhoneNumber,
BranchID: bet.BranchID.Value,
UserID: bet.UserID.Value,
Outcomes: bet.Outcomes,
IsShopBet: bet.IsShopBet,
CashedOut: bet.CashedOut,
CashedID: bet.CashoutID,
}
}

View File

@ -1,16 +1,27 @@
package domain
import "fmt"
import (
"fmt"
"time"
)
type ValidInt64 struct {
Value int64
Valid bool
}
type ValidInt struct {
Value int
Valid bool
}
type ValidString struct {
Value string
Valid bool
}
type ValidTime struct {
Value time.Time
Valid bool
}
type ValidBool struct {
Value bool
Valid bool

View File

@ -11,12 +11,15 @@ type Company struct {
}
type GetCompany struct {
ID int64
Name string
AdminID int64
WalletID int64
WalletBalance Currency
IsWalletActive bool
ID int64
Name string
AdminID int64
AdminFirstName string
AdminLastName string
AdminPhoneNumber string
WalletID int64
WalletBalance Currency
IsWalletActive bool
}
type CreateCompany struct {

View File

@ -12,9 +12,8 @@ var SupportedLeagues = []int64{
10041957, //UEFA Europa League
10079560, //UEFA Conference League
10047168, // US MLS
10044469, // Ethiopian Premier League
10050282, //UEFA Nations League
10040795, //EuroLeague
10043156, //England FA Cup
10042103, //France Cup
@ -26,5 +25,15 @@ var SupportedLeagues = []int64{
// Basketball
173998768, //NBA
10041830, //NBA
10049984, //WNBA
10037165, //German Bundesliga
10036608, //Italian Lega 1
10040795, //EuroLeague
// Ice Hockey
10037477, //NHL
10037447, //AHL
10069385, //IIHF World Championship
}

51
internal/domain/oddres.go Normal file
View File

@ -0,0 +1,51 @@
package domain
import "encoding/json"
type BaseNonLiveOddResponse struct {
Success int `json:"success"`
Results []json.RawMessage `json:"results"`
}
type OddsSection struct {
UpdatedAt string `json:"updated_at"`
Sp map[string]OddsMarket `json:"sp"`
}
// The Market ID for the json data can be either string / int which is causing problems when UnMarshalling
type OddsMarket struct {
ID json.RawMessage `json:"id"`
Name string `json:"name"`
Odds []json.RawMessage `json:"odds"`
Header string `json:"header,omitempty"`
Handicap string `json:"handicap,omitempty"`
Open int64 `json:"open,omitempty"`
}
type FootballOddsResponse struct {
EventID string `json:"event_id"`
FI string `json:"FI"`
Main OddsSection `json:"main"`
AsianLines OddsSection `json:"asian_lines"`
Goals OddsSection `json:"goals"`
Half OddsSection `json:"half"`
}
type BasketballOddsResponse struct {
EventID string `json:"event_id"`
FI string `json:"FI"`
Main OddsSection `json:"main"`
HalfProps OddsSection `json:"half_props"`
QuarterProps OddsSection `json:"quarter_props"`
TeamProps OddsSection `json:"team_props"`
Others []OddsSection `json:"others"`
}
type IceHockeyOddsResponse struct {
EventID string `json:"event_id"`
FI string `json:"FI"`
Main OddsSection `json:"main"`
Main2 OddsSection `json:"main_2"`
FirstPeriod OddsSection `json:"1st_period"`
Others []OddsSection `json:"others"`
}

View File

@ -1,185 +1,9 @@
package domain
import (
"encoding/json"
"time"
)
type BaseResultResponse struct {
Success int `json:"success"`
Results []json.RawMessage `json:"results"`
}
type FootballResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League struct {
ID string `json:"id"`
Name string `json:"name"`
CC string `json:"cc"`
} `json:"league"`
Home struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"home"`
Away struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstHalf Score `json:"1"`
SecondHalf Score `json:"2"`
} `json:"scores"`
Stats struct {
Attacks []string `json:"attacks"`
Corners []string `json:"corners"`
DangerousAttacks []string `json:"dangerous_attacks"`
Goals []string `json:"goals"`
OffTarget []string `json:"off_target"`
OnTarget []string `json:"on_target"`
Penalties []string `json:"penalties"`
PossessionRT []string `json:"possession_rt"`
RedCards []string `json:"redcards"`
Substitutions []string `json:"substitutions"`
YellowCards []string `json:"yellowcards"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
StadiumData map[string]string `json:"stadium_data"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}
type BasketballResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League struct {
ID string `json:"id"`
Name string `json:"name"`
CC string `json:"cc"`
} `json:"league"`
Home struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"home"`
Away struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstQuarter Score `json:"1"`
SecondQuarter Score `json:"2"`
FirstHalf Score `json:"3"`
ThirdQuarter Score `json:"4"`
FourthQuarter Score `json:"5"`
TotalScore Score `json:"7"`
} `json:"scores"`
Stats struct {
TwoPoints []string `json:"2points"`
ThreePoints []string `json:"3points"`
BiggestLead []string `json:"biggest_lead"`
Fouls []string `json:"fouls"`
FreeThrows []string `json:"free_throws"`
FreeThrowRate []string `json:"free_throws_rate"`
LeadChanges []string `json:"lead_changes"`
MaxpointsInarow []string `json:"maxpoints_inarow"`
Possession []string `json:"possession"`
SuccessAttempts []string `json:"success_attempts"`
TimeSpendInLead []string `json:"timespent_inlead"`
Timeuts []string `json:"time_outs"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
AwayManager map[string]string `json:"away_manager"`
HomeManager map[string]string `json:"home_manager"`
NumberOfPeriods string `json:"numberofperiods"`
PeriodLength string `json:"periodlength"`
StadiumData map[string]string `json:"stadium_data"`
Length string `json:"length"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
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 struct {
ID string `json:"id"`
Name string `json:"name"`
CC string `json:"cc"`
} `json:"league"`
Home struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"home"`
Away struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstPeriod Score `json:"1"`
SecondPeriod Score `json:"2"`
ThirdPeriod Score `json:"3"`
TotalScore Score `json:"5"`
} `json:"scores"`
Stats struct {
Shots []string `json:"shots"`
Penalties []string `json:"penalties"`
GoalsOnPowerPlay []string `json:"goals_on_power_play"`
SSeven []string `json:"s7"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
AwayManager map[string]string `json:"away_manager"`
HomeManager map[string]string `json:"home_manager"`
NumberOfPeriods string `json:"numberofperiods"`
PeriodLength string `json:"periodlength"`
StadiumData map[string]string `json:"stadium_data"`
Length string `json:"length"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}
type Score struct {
Home string `json:"home"`
Away string `json:"away"`
}
type MarketConfig struct {
Sport string
MarketCategories map[string]bool
@ -219,4 +43,42 @@ const (
OUTCOME_STATUS_LOSS OutcomeStatus = 2
OUTCOME_STATUS_VOID OutcomeStatus = 3 //Give Back
OUTCOME_STATUS_HALF OutcomeStatus = 4 //Half Win and Half Given Back
OUTCOME_STATUS_ERROR OutcomeStatus = 5 //Half Win and Half Given Back
)
func (o *OutcomeStatus) String() string {
switch *o {
case OUTCOME_STATUS_PENDING:
return "PENDING"
case OUTCOME_STATUS_WIN:
return "WIN"
case OUTCOME_STATUS_LOSS:
return "LOSS"
case OUTCOME_STATUS_VOID:
return "VOID"
case OUTCOME_STATUS_HALF:
return "HALF"
case OUTCOME_STATUS_ERROR:
return "ERROR"
default:
return "UNKNOWN"
}
}
type TimeStatus int32
const (
TIME_STATUS_NOT_STARTED TimeStatus = 0
TIME_STATUS_IN_PLAY TimeStatus = 1
TIME_STATUS_TO_BE_FIXED TimeStatus = 2
TIME_STATUS_ENDED TimeStatus = 3
TIME_STATUS_POSTPONED TimeStatus = 4
TIME_STATUS_CANCELLED TimeStatus = 5
TIME_STATUS_WALKOVER TimeStatus = 6
TIME_STATUS_INTERRUPTED TimeStatus = 7
TIME_STATUS_ABANDONED TimeStatus = 8
TIME_STATUS_RETIRED TimeStatus = 9
TIME_STATUS_SUSPENDED TimeStatus = 10
TIME_STATUS_DECIDED_BY_FA TimeStatus = 11
TIME_STATUS_REMOVED TimeStatus = 99
)

View File

@ -0,0 +1,153 @@
package domain
import (
"encoding/json"
)
type BaseResultResponse struct {
Success int `json:"success"`
Results []json.RawMessage `json:"results"`
}
type League struct {
ID string `json:"id"`
Name string `json:"name"`
CC string `json:"cc"`
}
type Team struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
}
type Score struct {
Home string `json:"home"`
Away string `json:"away"`
}
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"`
Scores struct {
FirstHalf Score `json:"1"`
SecondHalf Score `json:"2"`
} `json:"scores"`
Stats struct {
Attacks []string `json:"attacks"`
Corners []string `json:"corners"`
HalfTimeCorners []string `json:"corner_h"`
DangerousAttacks []string `json:"dangerous_attacks"`
Goals []string `json:"goals"`
OffTarget []string `json:"off_target"`
OnTarget []string `json:"on_target"`
Penalties []string `json:"penalties"`
PossessionRT []string `json:"possession_rt"`
RedCards []string `json:"redcards"`
Substitutions []string `json:"substitutions"`
YellowCards []string `json:"yellowcards"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
StadiumData map[string]string `json:"stadium_data"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}
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"`
Scores struct {
FirstQuarter Score `json:"1"`
SecondQuarter Score `json:"2"`
FirstHalf Score `json:"3"`
ThirdQuarter Score `json:"4"`
FourthQuarter Score `json:"5"`
TotalScore Score `json:"7"`
} `json:"scores"`
Stats struct {
TwoPoints []string `json:"2points"`
ThreePoints []string `json:"3points"`
BiggestLead []string `json:"biggest_lead"`
Fouls []string `json:"fouls"`
FreeThrows []string `json:"free_throws"`
FreeThrowRate []string `json:"free_throws_rate"`
LeadChanges []string `json:"lead_changes"`
MaxpointsInarow []string `json:"maxpoints_inarow"`
Possession []string `json:"possession"`
SuccessAttempts []string `json:"success_attempts"`
TimeSpendInLead []string `json:"timespent_inlead"`
TimeOuts []string `json:"time_outs"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
AwayManager map[string]string `json:"away_manager"`
HomeManager map[string]string `json:"home_manager"`
NumberOfPeriods string `json:"numberofperiods"`
PeriodLength string `json:"periodlength"`
StadiumData map[string]string `json:"stadium_data"`
Length int `json:"length"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
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"`
Scores struct {
FirstPeriod Score `json:"1"`
SecondPeriod Score `json:"2"`
ThirdPeriod Score `json:"3"`
TotalScore Score `json:"5"`
} `json:"scores"`
Stats struct {
Shots []string `json:"shots"`
Penalties []string `json:"penalties"`
GoalsOnPowerPlay []string `json:"goals_on_power_play"`
SSeven []string `json:"s7"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
AwayManager map[string]string `json:"away_manager"`
HomeManager map[string]string `json:"home_manager"`
NumberOfPeriods string `json:"numberofperiods"`
PeriodLength string `json:"periodlength"`
StadiumData map[string]string `json:"stadium_data"`
Length int `json:"length"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}

View File

@ -3,18 +3,28 @@ package domain
type FootballMarket int64
const (
FOOTBALL_FULL_TIME_RESULT FootballMarket = 40 //"full_time_result"
FOOTBALL_DOUBLE_CHANCE FootballMarket = 10114 //"double_chance"
FOOTBALL_GOALS_OVER_UNDER FootballMarket = 981 //"goals_over_under"
FOOTBALL_CORRECT_SCORE FootballMarket = 43 //"correct_score"
FOOTBALL_ASIAN_HANDICAP FootballMarket = 938 //"asian_handicap"
FOOTBALL_GOAL_LINE FootballMarket = 10143 //"goal_line"
FOOTBALL_FULL_TIME_RESULT FootballMarket = 40 //"full_time_result"
FOOTBALL_DOUBLE_CHANCE FootballMarket = 10114 //"double_chance"
FOOTBALL_GOALS_OVER_UNDER FootballMarket = 981 //"goals_over_under"
FOOTBALL_CORRECT_SCORE FootballMarket = 43 //"correct_score"
FOOTBALL_ASIAN_HANDICAP FootballMarket = 938 //"asian_handicap"
FOOTBALL_GOAL_LINE FootballMarket = 10143 //"goal_line"
FOOTBALL_HALF_TIME_RESULT FootballMarket = 1579 //"half_time_result"
FOOTBALL_FIRST_HALF_ASIAN_HANDICAP FootballMarket = 50137 //"1st_half_asian_handicap"
FOOTBALL_FIRST_HALF_GOAL_LINE FootballMarket = 50136 //"1st_half_goal_line"
FOOTBALL_FIRST_TEAM_TO_SCORE FootballMarket = 1178 //"first_team_to_score"
FOOTBALL_GOALS_ODD_EVEN FootballMarket = 10111 //"goals_odd_even"
FOOTBALL_DRAW_NO_BET FootballMarket = 10544 //"draw_no_bet"
FOOTBALL_CORNERS FootballMarket = 760 //"corners"
FOOTBALL_CORNERS_TWO_WAY FootballMarket = 10235 //"corners_2_way"
FOOTBALL_FIRST_HALF_CORNERS FootballMarket = 10539 //"first_half_corners"
FOOTBALL_ASIAN_TOTAL_CORNERS FootballMarket = 10164 //"asian_total_corners"
FOOTBALL_FIRST_HALF_ASIAN_CORNERS FootballMarket = 10233 //"1st_half_asian_corners"
FOOTBALL_FIRST_HALF_GOALS_ODD_EVEN FootballMarket = 10206 //"1st_half_goals_odd_even"
FOOTBALL_SECOND_HALF_GOALS_ODD_EVEN FootballMarket = 50433 //"2nd_half_goals_odd_even"
)
type BasketBallMarket int64
@ -91,23 +101,62 @@ const (
ICE_HOCKEY_ALTERNATIVE_TOTAL_TWO_WAY IceHockeyMarket = 170240
)
type AmericanFootballMarket int64
const (
// Main
AMERICAN_FOOTBALL_MONEY_LINE AmericanFootballMarket = 170001
AMERICAN_FOOTBALL_SPREAD AmericanFootballMarket = 170002
AMERICAN_FOOTBALL_TOTAL_POINTS AmericanFootballMarket = 170003
)
type RugbyMarket int64
const (
// Main
RUGBY_MONEY_LINE RugbyMarket = 180001
RUGBY_SPREAD RugbyMarket = 180002
RUGBY_TOTAL_POINTS RugbyMarket = 180003
RUGBY_HANDICAP RugbyMarket = 180004
RUGBY_FIRST_HALF RugbyMarket = 180005
RUGBY_SECOND_HALF RugbyMarket = 180006
)
type BaseballMarket int64
const (
// Main
BASEBALL_MONEY_LINE BaseballMarket = 190001
BASEBALL_SPREAD BaseballMarket = 190002
BASEBALL_TOTAL_RUNS BaseballMarket = 190003
BASEBALL_FIRST_INNING BaseballMarket = 190004
BASEBALL_FIRST_5_INNINGS BaseballMarket = 190005
)
// TODO: Move this into the database so that it can be modified dynamically
var SupportedMarkets = map[int64]bool{
// Football Markets
int64(FOOTBALL_FULL_TIME_RESULT): true, //"full_time_result"
int64(FOOTBALL_DOUBLE_CHANCE): true, //"double_chance"
int64(FOOTBALL_GOALS_OVER_UNDER): true, //"goals_over_under"
int64(FOOTBALL_CORRECT_SCORE): true, //"correct_score"
int64(FOOTBALL_ASIAN_HANDICAP): true, //"asian_handicap"
int64(FOOTBALL_GOAL_LINE): true, //"goal_line"
int64(FOOTBALL_HALF_TIME_RESULT): true, //"half_time_result"
int64(FOOTBALL_FIRST_HALF_ASIAN_HANDICAP): true, //"1st_half_asian_handicap"
int64(FOOTBALL_FIRST_HALF_GOAL_LINE): true, //"1st_half_goal_line"
int64(FOOTBALL_FIRST_TEAM_TO_SCORE): true, //"first_team_to_score"
int64(FOOTBALL_GOALS_ODD_EVEN): true, //"goals_odd_even"
int64(FOOTBALL_DRAW_NO_BET): true, //"draw_no_bet"
int64(FOOTBALL_FULL_TIME_RESULT): true, //"full_time_result"
int64(FOOTBALL_DOUBLE_CHANCE): true, //"double_chance"
int64(FOOTBALL_GOALS_OVER_UNDER): true, //"goals_over_under"
int64(FOOTBALL_CORRECT_SCORE): true, //"correct_score"
int64(FOOTBALL_ASIAN_HANDICAP): true, //"asian_handicap"
int64(FOOTBALL_GOAL_LINE): true, //"goal_line"
int64(FOOTBALL_HALF_TIME_RESULT): true, //"half_time_result"
int64(FOOTBALL_FIRST_HALF_ASIAN_HANDICAP): true, //"1st_half_asian_handicap"
int64(FOOTBALL_FIRST_HALF_GOAL_LINE): true, //"1st_half_goal_line"
int64(FOOTBALL_FIRST_TEAM_TO_SCORE): true, //"first_team_to_score"
int64(FOOTBALL_GOALS_ODD_EVEN): true, //"goals_odd_even"
int64(FOOTBALL_DRAW_NO_BET): true, //"draw_no_bet"
int64(FOOTBALL_CORNERS): true,
int64(FOOTBALL_CORNERS_TWO_WAY): true,
int64(FOOTBALL_FIRST_HALF_CORNERS): true,
int64(FOOTBALL_ASIAN_TOTAL_CORNERS): true,
int64(FOOTBALL_FIRST_HALF_ASIAN_CORNERS): true,
int64(FOOTBALL_FIRST_HALF_GOALS_ODD_EVEN): true,
int64(FOOTBALL_SECOND_HALF_GOALS_ODD_EVEN): true,
// Basketball Markets
int64(BASKETBALL_GAME_LINES): true,
@ -164,4 +213,24 @@ var SupportedMarkets = map[int64]bool{
int64(ICE_HOCKEY_ALTERNATIVE_PUCK_LINE_TWO_WAY): false,
int64(ICE_HOCKEY_ALTERNATIVE_TOTAL_TWO_WAY): false,
// American Football Markets
int64(AMERICAN_FOOTBALL_MONEY_LINE): true,
int64(AMERICAN_FOOTBALL_SPREAD): true,
int64(AMERICAN_FOOTBALL_TOTAL_POINTS): true,
// Rugby Markets
int64(RUGBY_MONEY_LINE): true,
int64(RUGBY_SPREAD): true,
int64(RUGBY_TOTAL_POINTS): true,
int64(RUGBY_HANDICAP): true,
int64(RUGBY_FIRST_HALF): true,
int64(RUGBY_SECOND_HALF): true,
// Baseball Markets
int64(BASEBALL_MONEY_LINE): true,
int64(BASEBALL_SPREAD): true,
int64(BASEBALL_TOTAL_RUNS): true,
int64(BASEBALL_FIRST_INNING): true,
int64(BASEBALL_FIRST_5_INNINGS): true,
}

View File

@ -0,0 +1,290 @@
package domain
import (
"encoding/json"
"strconv"
"strings"
)
// NFLResultResponse represents the structure for NFL game results
type NFLResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League struct {
ID string `json:"id"`
Name string `json:"name"`
CC string `json:"cc"`
} `json:"league"`
Home struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"home"`
Away struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstQuarter Score `json:"1"`
SecondQuarter Score `json:"2"`
ThirdQuarter Score `json:"3"`
FourthQuarter Score `json:"4"`
Overtime Score `json:"5"`
TotalScore Score `json:"7"`
} `json:"scores"`
Stats struct {
FirstDowns []string `json:"first_downs"`
TotalYards []string `json:"total_yards"`
PassingYards []string `json:"passing_yards"`
RushingYards []string `json:"rushing_yards"`
Turnovers []string `json:"turnovers"`
TimeOfPossession []string `json:"time_of_possession"`
ThirdDownEfficiency []string `json:"third_down_efficiency"`
FourthDownEfficiency []string `json:"fourth_down_efficiency"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
StadiumData map[string]string `json:"stadium_data"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}
// RugbyResultResponse represents the structure for Rugby game results
type RugbyResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League struct {
ID string `json:"id"`
Name string `json:"name"`
CC string `json:"cc"`
} `json:"league"`
Home struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"home"`
Away struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstHalf Score `json:"1"`
SecondHalf Score `json:"2"`
TotalScore Score `json:"7"`
} `json:"scores"`
Stats struct {
Tries []string `json:"tries"`
Conversions []string `json:"conversions"`
Penalties []string `json:"penalties"`
DropGoals []string `json:"drop_goals"`
Possession []string `json:"possession"`
Territory []string `json:"territory"`
Lineouts []string `json:"lineouts"`
Scrums []string `json:"scrums"`
PenaltiesConceded []string `json:"penalties_conceded"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
StadiumData map[string]string `json:"stadium_data"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}
// BaseballResultResponse represents the structure for Baseball game results
type BaseballResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League struct {
ID string `json:"id"`
Name string `json:"name"`
CC string `json:"cc"`
} `json:"league"`
Home struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"home"`
Away struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstInning Score `json:"1"`
SecondInning Score `json:"2"`
ThirdInning Score `json:"3"`
FourthInning Score `json:"4"`
FifthInning Score `json:"5"`
SixthInning Score `json:"6"`
SeventhInning Score `json:"7"`
EighthInning Score `json:"8"`
NinthInning Score `json:"9"`
ExtraInnings Score `json:"10"`
TotalScore Score `json:"7"`
} `json:"scores"`
Stats struct {
Hits []string `json:"hits"`
Errors []string `json:"errors"`
LeftOnBase []string `json:"left_on_base"`
Strikeouts []string `json:"strikeouts"`
Walks []string `json:"walks"`
HomeRuns []string `json:"home_runs"`
TotalBases []string `json:"total_bases"`
BattingAverage []string `json:"batting_average"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
StadiumData map[string]string `json:"stadium_data"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}
// ParseNFLResult parses NFL result from raw JSON data
func ParseNFLResult(data json.RawMessage) (*NFLResultResponse, error) {
var result NFLResultResponse
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
return &result, nil
}
// ParseRugbyResult parses Rugby result from raw JSON data
func ParseRugbyResult(data json.RawMessage) (*RugbyResultResponse, error) {
var result RugbyResultResponse
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
return &result, nil
}
// ParseRugbyUnionResult parses Rugby Union result from raw JSON data
func ParseRugbyUnionResult(data json.RawMessage) (*RugbyResultResponse, error) {
return ParseRugbyResult(data)
}
// ParseRugbyLeagueResult parses Rugby League result from raw JSON data
func ParseRugbyLeagueResult(data json.RawMessage) (*RugbyResultResponse, error) {
return ParseRugbyResult(data)
}
// ParseBaseballResult parses Baseball result from raw JSON data
func ParseBaseballResult(data json.RawMessage) (*BaseballResultResponse, error) {
var result BaseballResultResponse
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
return &result, nil
}
// GetNFLWinner determines the winner of an NFL game
func GetNFLWinner(result *NFLResultResponse) (string, error) {
homeScore, err := strconv.Atoi(result.Scores.TotalScore.Home)
if err != nil {
return "", err
}
awayScore, err := strconv.Atoi(result.Scores.TotalScore.Away)
if err != nil {
return "", err
}
if homeScore > awayScore {
return result.Home.Name, nil
} else if awayScore > homeScore {
return result.Away.Name, nil
}
return "Draw", nil
}
// GetRugbyWinner determines the winner of a Rugby game
func GetRugbyWinner(result *RugbyResultResponse) (string, error) {
homeScore, err := strconv.Atoi(result.Scores.TotalScore.Home)
if err != nil {
return "", err
}
awayScore, err := strconv.Atoi(result.Scores.TotalScore.Away)
if err != nil {
return "", err
}
if homeScore > awayScore {
return result.Home.Name, nil
} else if awayScore > homeScore {
return result.Away.Name, nil
}
return "Draw", nil
}
// GetBaseballWinner determines the winner of a Baseball game
func GetBaseballWinner(result *BaseballResultResponse) (string, error) {
homeScore, err := strconv.Atoi(result.Scores.TotalScore.Home)
if err != nil {
return "", err
}
awayScore, err := strconv.Atoi(result.Scores.TotalScore.Away)
if err != nil {
return "", err
}
if homeScore > awayScore {
return result.Home.Name, nil
} else if awayScore > homeScore {
return result.Away.Name, nil
}
return "Draw", nil
}
// FormatNFLScore formats the NFL score in a readable format
func FormatNFLScore(result *NFLResultResponse) string {
return strings.Join([]string{
result.Home.Name + " " + result.Scores.TotalScore.Home,
result.Away.Name + " " + result.Scores.TotalScore.Away,
}, " - ")
}
// FormatRugbyScore formats the Rugby score in a readable format
func FormatRugbyScore(result *RugbyResultResponse) string {
return strings.Join([]string{
result.Home.Name + " " + result.Scores.TotalScore.Home,
result.Away.Name + " " + result.Scores.TotalScore.Away,
}, " - ")
}
// FormatBaseballScore formats the Baseball score in a readable format
func FormatBaseballScore(result *BaseballResultResponse) string {
return strings.Join([]string{
result.Home.Name + " " + result.Scores.TotalScore.Home,
result.Away.Name + " " + result.Scores.TotalScore.Away,
}, " - ")
}

View File

@ -62,9 +62,26 @@ type UpdateUserReq struct {
FirstName ValidString
LastName ValidString
Suspended ValidBool
CompanyID ValidInt64
}
type UpdateUserReferalCode struct {
UserID int64
Code string
}
type GetCashier struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
PhoneNumber string `json:"phone_number"`
Role Role `json:"role"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
SuspendedAt time.Time `json:"suspended_at"`
Suspended bool `json:"suspended"`
BranchID int64 `json:"branch_id"`
}

View File

@ -19,7 +19,6 @@ type CustomerWallet struct {
RegularID int64
StaticID int64
CustomerID int64
CompanyID int64
}
type GetCustomerWallet struct {
ID int64
@ -28,7 +27,6 @@ type GetCustomerWallet struct {
StaticID int64
StaticBalance Currency
CustomerID int64
CompanyID int64
RegularUpdatedAt time.Time
StaticUpdatedAt time.Time
CreatedAt time.Time
@ -56,7 +54,6 @@ type CreateWallet struct {
type CreateCustomerWallet struct {
CustomerID int64
CompanyID int64
RegularWalletID int64
StaticWalletID int64
}

View File

@ -2,6 +2,7 @@ package repository
import (
"context"
// "fmt"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
@ -225,12 +226,26 @@ func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]do
}
return result, nil
}
func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error {
err := s.queries.UpdateBetOutcomeStatus(ctx, dbgen.UpdateBetOutcomeStatusParams{
func (s *Store) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error) {
outcomes, err := s.queries.GetBetOutcomeByBetID(ctx, betID)
if err != nil {
return nil, nil
}
var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes))
for _, outcome := range outcomes {
result = append(result, convertDBBetOutcomes(outcome))
}
return result, nil
}
func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) {
update, err := s.queries.UpdateBetOutcomeStatus(ctx, dbgen.UpdateBetOutcomeStatusParams{
Status: int32(status),
ID: id,
})
return err
res := convertDBBetOutcomes(update)
return res, err
}
func (s *Store) DeleteBet(ctx context.Context, id int64) error {

View File

@ -25,14 +25,17 @@ func convertDBCompany(dbCompany dbgen.Company) domain.Company {
}
}
func convertDBCompanyWithWallet(dbCompany dbgen.CompaniesWithWallet) domain.GetCompany {
func convertDBCompanyDetails(dbCompany dbgen.CompaniesDetail) domain.GetCompany {
return domain.GetCompany{
ID: dbCompany.ID,
Name: dbCompany.Name,
AdminID: dbCompany.AdminID,
WalletID: dbCompany.WalletID,
WalletBalance: domain.Currency(dbCompany.Balance),
IsWalletActive: dbCompany.IsActive,
ID: dbCompany.ID,
Name: dbCompany.Name,
AdminID: dbCompany.AdminID,
WalletID: dbCompany.WalletID,
WalletBalance: domain.Currency(dbCompany.Balance),
IsWalletActive: dbCompany.IsActive,
AdminFirstName: dbCompany.AdminFirstName,
AdminLastName: dbCompany.AdminLastName,
AdminPhoneNumber: dbCompany.AdminPhoneNumber.String,
}
}
@ -74,7 +77,7 @@ func (s *Store) GetAllCompanies(ctx context.Context) ([]domain.GetCompany, error
var companies []domain.GetCompany = make([]domain.GetCompany, 0, len(dbCompanies))
for _, dbCompany := range dbCompanies {
companies = append(companies, convertDBCompanyWithWallet(dbCompany))
companies = append(companies, convertDBCompanyDetails(dbCompany))
}
return companies, nil
@ -92,7 +95,7 @@ func (s *Store) SearchCompanyByName(ctx context.Context, name string) ([]domain.
var companies []domain.GetCompany = make([]domain.GetCompany, 0, len(dbCompanies))
for _, dbCompany := range dbCompanies {
companies = append(companies, convertDBCompanyWithWallet(dbCompany))
companies = append(companies, convertDBCompanyDetails(dbCompany))
}
return companies, nil
}
@ -103,7 +106,7 @@ func (s *Store) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany
if err != nil {
return domain.GetCompany{}, err
}
return convertDBCompanyWithWallet(dbCompany), nil
return convertDBCompanyDetails(dbCompany), nil
}
func (s *Store) UpdateCompany(ctx context.Context, company domain.UpdateCompany) (domain.Company, error) {

View File

@ -117,7 +117,8 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcoming
return upcomingEvents, nil
}
func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32, leagueID domain.ValidString, sportID domain.ValidString) ([]domain.UpcomingEvent, int64, error) {
func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidString, sportID domain.ValidString, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) {
events, err := s.queries.GetPaginatedUpcomingEvents(ctx, dbgen.GetPaginatedUpcomingEventsParams{
LeagueID: pgtype.Text{
String: leagueID.Value,
@ -127,14 +128,27 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, off
String: sportID.Value,
Valid: sportID.Valid,
},
Limit: limit,
Offset: offset * limit,
Limit: pgtype.Int4{
Int32: int32(limit.Value),
Valid: limit.Valid,
},
Offset: pgtype.Int4{
Int32: int32(offset.Value * limit.Value),
Valid: offset.Valid,
},
FirstStartTime: pgtype.Timestamp{
Time: firstStartTime.Value.UTC(),
Valid: firstStartTime.Valid,
},
LastStartTime: pgtype.Timestamp{
Time: lastStartTime.Value.UTC(),
Valid: lastStartTime.Valid,
},
})
if err != nil {
return nil, 0, err
}
upcomingEvents := make([]domain.UpcomingEvent, len(events))
for i, e := range events {
upcomingEvents[i] = domain.UpcomingEvent{
@ -162,12 +176,20 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, off
String: sportID.Value,
Valid: sportID.Valid,
},
FirstStartTime: pgtype.Timestamp{
Time: firstStartTime.Value.UTC(),
Valid: firstStartTime.Valid,
},
LastStartTime: pgtype.Timestamp{
Time: lastStartTime.Value.UTC(),
Valid: lastStartTime.Valid,
},
})
if err != nil {
return nil, 0, err
}
numberOfPages := math.Ceil(float64(totalCount) / float64(limit))
numberOfPages := math.Ceil(float64(totalCount) / float64(limit.Value))
return upcomingEvents, int64(numberOfPages), nil
}
func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) {

View File

@ -15,6 +15,8 @@ type NotificationRepository interface {
ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error)
ListFailedNotifications(ctx context.Context, limit int) ([]domain.Notification, error)
ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error)
CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error)
GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error)
}
type Repository struct {
@ -105,6 +107,24 @@ func (r *Repository) ListNotifications(ctx context.Context, recipientID int64, l
return result, nil
}
func (r *Repository) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
dbNotifications, err := r.store.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{
Limit: int32(limit),
Offset: int32(offset),
})
if err != nil {
return nil, err
}
var result []domain.Notification = make([]domain.Notification, 0, len(dbNotifications))
for _, dbNotif := range dbNotifications {
domainNotif := r.mapDBToDomain(&dbNotif)
result = append(result, *domainNotif)
}
return result, nil
}
func (r *Repository) ListFailedNotifications(ctx context.Context, limit int) ([]domain.Notification, error) {
dbNotifications, err := r.store.queries.ListFailedNotifications(ctx, int32(limit))
if err != nil {
@ -177,3 +197,7 @@ func unmarshalPayload(data []byte) (domain.NotificationPayload, error) {
}
return payload, nil
}
func (r *Repository) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) {
return r.store.queries.CountUnreadNotifications(ctx, recipient_id)
}

View File

@ -205,15 +205,54 @@ func (s *Store) GetRawOddsByMarketID(ctx context.Context, rawOddsID string, upco
FetchedAt: odds.FetchedAt.Time,
}, nil
}
func (s *Store) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt64, offset domain.ValidInt64) ([]domain.Odd, error) {
odds, err := s.queries.GetPaginatedPrematchOddsByUpcomingID(ctx, dbgen.GetPaginatedPrematchOddsByUpcomingIDParams{
ID: upcomingID,
Limit: pgtype.Int4{
Int32: int32(limit.Value),
Valid: limit.Valid,
},
Offset: pgtype.Int4{
Int32: int32(offset.Value),
Valid: offset.Valid,
},
})
if err != nil {
return nil, err
}
// Map the results to domain.Odd
domainOdds := make([]domain.Odd, len(odds))
for i, odd := range odds {
var rawOdds []domain.RawMessage
if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil {
rawOdds = nil
}
func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) {
params := dbgen.GetPrematchOddsByUpcomingIDParams{
ID: upcomingID,
Limit: limit,
Offset: offset,
domainOdds[i] = domain.Odd{
EventID: odd.EventID.String,
Fi: odd.Fi.String,
MarketType: odd.MarketType,
MarketName: odd.MarketName.String,
MarketCategory: odd.MarketCategory.String,
MarketID: odd.MarketID.String,
Name: odd.Name.String,
Handicap: odd.Handicap.String,
OddsValue: odd.OddsValue.Float64,
Section: odd.Section,
Category: odd.Category.String,
RawOdds: rawOdds,
FetchedAt: odd.FetchedAt.Time,
Source: odd.Source.String,
IsActive: odd.IsActive.Bool,
}
}
odds, err := s.queries.GetPrematchOddsByUpcomingID(ctx, params)
return domainOdds, nil
}
func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error) {
odds, err := s.queries.GetPrematchOddsByUpcomingID(ctx, upcomingID)
if err != nil {
return nil, err
}

View File

@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"errors"
"fmt"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
@ -90,8 +91,14 @@ func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.U
Int64: filter.CompanyID.Value,
Valid: filter.CompanyID.Valid,
},
Limit: int32(filter.PageSize),
Offset: int32(filter.Page),
Limit: pgtype.Int4{
Int32: int32(filter.PageSize.Value),
Valid: filter.PageSize.Valid,
},
Offset: pgtype.Int4{
Int32: int32(filter.Page.Value),
Valid: filter.Page.Valid,
},
})
if err != nil {
return nil, 0, err
@ -123,14 +130,14 @@ func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.U
return userList, totalCount, nil
}
func (s *Store) GetAllCashiers(ctx context.Context) ([]domain.User, error) {
func (s *Store) GetAllCashiers(ctx context.Context) ([]domain.GetCashier, error) {
users, err := s.queries.GetAllCashiers(ctx)
if err != nil {
return nil, err
}
userList := make([]domain.User, len(users))
userList := make([]domain.GetCashier, len(users))
for i, user := range users {
userList[i] = domain.User{
userList[i] = domain.GetCashier{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
@ -148,6 +155,28 @@ func (s *Store) GetAllCashiers(ctx context.Context) ([]domain.User, error) {
return userList, nil
}
func (s *Store) GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, error) {
user, err := s.queries.GetCashierByID(ctx, cashierID)
if err != nil {
return domain.GetCashier{}, err
}
return domain.GetCashier{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email.String,
PhoneNumber: user.PhoneNumber.String,
Role: domain.Role(user.Role),
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt.Time,
UpdatedAt: user.UpdatedAt.Time,
SuspendedAt: user.SuspendedAt.Time,
Suspended: user.Suspended,
BranchID: user.BranchID,
}, nil
}
func (s *Store) GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error) {
users, err := s.queries.GetCashiersByBranch(ctx, branchID)
if err != nil {
@ -173,11 +202,28 @@ func (s *Store) GetCashiersByBranch(ctx context.Context, branchID int64) ([]doma
return userList, nil
}
func (s *Store) SearchUserByNameOrPhone(ctx context.Context, searchString string) ([]domain.User, error) {
users, err := s.queries.SearchUserByNameOrPhone(ctx, pgtype.Text{
String: searchString,
Valid: true,
})
func (s *Store) SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error) {
query := dbgen.SearchUserByNameOrPhoneParams{
Column1: pgtype.Text{
String: searchString,
Valid: true,
},
CompanyID: pgtype.Int8{
Int64: companyID.Value,
Valid: companyID.Valid,
},
}
if role != nil {
query.Role = pgtype.Text{
String: string(*role),
Valid: true,
}
}
users, err := s.queries.SearchUserByNameOrPhone(ctx, query)
if err != nil {
return nil, err
}
@ -204,13 +250,12 @@ func (s *Store) SearchUserByNameOrPhone(ctx context.Context, searchString string
func (s *Store) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error {
err := s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
// ID: user.ID,
// FirstName: user.FirstName,
// LastName: user.LastName,
// Email: user.Email,
// PhoneNumber: user.PhoneNumber,
ID: user.UserId,
FirstName: user.FirstName.Value,
LastName: user.LastName.Value,
Suspended: user.Suspended.Value,
})
fmt.Printf("Updating User %v with values %v", user.UserId, user)
if err != nil {
return err
}
@ -230,6 +275,22 @@ func (s *Store) UpdateUserCompany(ctx context.Context, id int64, companyID int64
}
return nil
}
func (s *Store) UpdateUserSuspend(ctx context.Context, id int64, status bool) error {
err := s.queries.SuspendUser(ctx, dbgen.SuspendUserParams{
ID: id,
Suspended: status,
SuspendedAt: pgtype.Timestamptz{
Time: time.Now(),
Valid: true,
},
})
if err != nil {
return err
}
return nil
}
func (s *Store) DeleteUser(ctx context.Context, id int64) error {
err := s.queries.DeleteUser(ctx, id)
if err != nil {

View File

@ -36,13 +36,11 @@ func convertDBCustomerWallet(customerWallet dbgen.CustomerWallet) domain.Custome
RegularID: customerWallet.RegularWalletID,
StaticID: customerWallet.StaticWalletID,
CustomerID: customerWallet.CustomerID,
CompanyID: customerWallet.CompanyID,
}
}
func convertCreateCustomerWallet(customerWallet domain.CreateCustomerWallet) dbgen.CreateCustomerWalletParams {
return dbgen.CreateCustomerWalletParams{
CustomerID: customerWallet.CustomerID,
CompanyID: customerWallet.CompanyID,
RegularWalletID: customerWallet.RegularWalletID,
StaticWalletID: customerWallet.StaticWalletID,
}
@ -56,7 +54,6 @@ func convertDBGetCustomerWallet(customerWallet dbgen.GetCustomerWalletRow) domai
StaticID: customerWallet.StaticID,
StaticBalance: domain.Currency(customerWallet.StaticBalance),
CustomerID: customerWallet.CustomerID,
CompanyID: customerWallet.CompanyID,
RegularUpdatedAt: customerWallet.RegularUpdatedAt.Time,
StaticUpdatedAt: customerWallet.StaticUpdatedAt.Time,
CreatedAt: customerWallet.CreatedAt.Time,
@ -117,11 +114,8 @@ func (s *Store) GetWalletsByUser(ctx context.Context, userID int64) ([]domain.Wa
return result, nil
}
func (s *Store) GetCustomerWallet(ctx context.Context, customerID int64, companyID int64) (domain.GetCustomerWallet, error) {
customerWallet, err := s.queries.GetCustomerWallet(ctx, dbgen.GetCustomerWalletParams{
CustomerID: customerID,
CompanyID: companyID,
})
func (s *Store) GetCustomerWallet(ctx context.Context, customerID int64) (domain.GetCustomerWallet, error) {
customerWallet, err := s.queries.GetCustomerWallet(ctx, customerID)
if err != nil {
return domain.GetCustomerWallet{}, err

View File

@ -13,8 +13,10 @@ type BetStore interface {
GetBetByID(ctx context.Context, id int64) (domain.GetBet, error)
GetAllBets(ctx context.Context) ([]domain.GetBet, error)
GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.GetBet, error)
GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, error)
GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error)
UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error
UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error
UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error
UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error)
DeleteBet(ctx context.Context, id int64) error
}

View File

@ -3,21 +3,56 @@ package bet
import (
"context"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"log/slog"
"math/big"
random "math/rand"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
)
var (
ErrNoEventsAvailable = errors.New("Not enough events available with the given filters")
ErrGenerateRandomOutcome = errors.New("Failed to generate any random outcome for events")
ErrOutcomesNotCompleted = errors.New("Some bet outcomes are still pending")
ErrEventHasBeenRemoved = errors.New("Event has been removed")
)
type Service struct {
betStore BetStore
betStore BetStore
eventSvc event.Service
prematchSvc odds.Service
walletSvc wallet.Service
branchSvc branch.Service
logger *slog.Logger
}
func NewService(betStore BetStore) *Service {
func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.Service, walletSvc wallet.Service, branchSvc branch.Service, logger *slog.Logger) *Service {
return &Service{
betStore: betStore,
betStore: betStore,
eventSvc: eventSvc,
prematchSvc: prematchSvc,
walletSvc: walletSvc,
branchSvc: branchSvc,
logger: logger,
}
}
var (
ErrEventHasNotEnded = errors.New("Event has not ended yet")
ErrRawOddInvalid = errors.New("Prematch Raw Odd is Invalid")
ErrBranchIDRequired = errors.New("Branch ID required for this role")
ErrOutcomeLimit = errors.New("Too many outcomes on a single bet")
)
func (s *Service) GenerateCashoutID() (string, error) {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
const length int = 13
@ -33,8 +68,402 @@ func (s *Service) GenerateCashoutID() (string, error) {
return string(result), nil
}
func (s *Service) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) {
func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketID int64, oddID int64) (domain.CreateBetOutcome, error) {
// TODO: Change this when you refactor the database code
eventIDStr := strconv.FormatInt(eventID, 10)
marketIDStr := strconv.FormatInt(marketID, 10)
oddIDStr := strconv.FormatInt(oddID, 10)
event, err := s.eventSvc.GetUpcomingEventByID(ctx, eventIDStr)
if err != nil {
return domain.CreateBetOutcome{}, ErrEventHasBeenRemoved
}
currentTime := time.Now()
if event.StartTime.Before(currentTime) {
return domain.CreateBetOutcome{}, ErrEventHasNotEnded
}
odds, err := s.prematchSvc.GetRawOddsByMarketID(ctx, marketIDStr, eventIDStr)
if err != nil {
return domain.CreateBetOutcome{}, err
}
type rawOddType struct {
ID string
Name string
Odds string
Header string
Handicap string
}
var selectedOdd rawOddType
var isOddFound bool = false
for _, raw := range odds.RawOdds {
var rawOdd rawOddType
rawBytes, err := json.Marshal(raw)
err = json.Unmarshal(rawBytes, &rawOdd)
if err != nil {
fmt.Printf("Failed to unmarshal raw odd %v", err)
continue
}
if rawOdd.ID == oddIDStr {
selectedOdd = rawOdd
isOddFound = true
}
}
if !isOddFound {
return domain.CreateBetOutcome{}, ErrRawOddInvalid
}
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
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,
HomeTeamName: event.HomeTeam,
AwayTeamName: event.AwayTeam,
MarketName: odds.MarketName,
Odd: float32(parsedOdd),
OddName: selectedOdd.Name,
OddHeader: selectedOdd.Header,
OddHandicap: selectedOdd.Handicap,
Expires: event.StartTime,
}
return newOutcome, nil
}
func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID int64, role domain.Role) (domain.CreateBetRes, error) {
// You can move the loop over req.Outcomes and all the business logic here.
if len(req.Outcomes) > 30 {
return domain.CreateBetRes{}, ErrOutcomeLimit
}
var outcomes []domain.CreateBetOutcome = make([]domain.CreateBetOutcome, 0, len(req.Outcomes))
var totalOdds float32 = 1
for _, outcomeReq := range req.Outcomes {
newOutcome, err := s.GenerateBetOutcome(ctx, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID)
if err != nil {
return domain.CreateBetRes{}, err
}
totalOdds = totalOdds * float32(newOutcome.Odd)
outcomes = append(outcomes, newOutcome)
}
// Handle role-specific logic and wallet deduction if needed.
var cashoutID string
cashoutID, err := s.GenerateCashoutID()
if err != nil {
return domain.CreateBetRes{}, err
}
newBet := domain.CreateBet{
Amount: domain.ToCurrency(req.Amount),
TotalOdds: totalOdds,
Status: req.Status,
FullName: req.FullName,
PhoneNumber: req.PhoneNumber,
CashoutID: cashoutID,
}
switch role {
case domain.RoleCashier:
branch, err := s.branchSvc.GetBranchByCashier(ctx, userID)
if err != nil {
return domain.CreateBetRes{}, err
}
// Deduct from wallet:
// TODO: Make this percentage come from the company
var deductedAmount = req.Amount / 10
err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount))
if err != nil {
return domain.CreateBetRes{}, err
}
newBet.BranchID = domain.ValidInt64{
Value: branch.ID,
Valid: true,
}
newBet.UserID = domain.ValidInt64{
Value: userID,
Valid: true,
}
newBet.IsShopBet = true
// bet, err = s.betStore.CreateBet(ctx)
case domain.RoleBranchManager, domain.RoleAdmin, domain.RoleSuperAdmin:
// TODO: restrict the Branch ID of Admin and Branch Manager to only the branches within their own company
// If a non cashier wants to create a bet, they will need to provide the Branch ID
if req.BranchID == nil {
return domain.CreateBetRes{}, ErrBranchIDRequired
}
branch, err := s.branchSvc.GetBranchByID(ctx, *req.BranchID)
if err != nil {
return domain.CreateBetRes{}, err
}
// Deduct from wallet:
// TODO: Make this percentage come from the company
var deductedAmount = req.Amount / 10
err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount))
if err != nil {
return domain.CreateBetRes{}, err
}
newBet.BranchID = domain.ValidInt64{
Value: branch.ID,
Valid: true,
}
newBet.UserID = domain.ValidInt64{
Value: userID,
Valid: true,
}
newBet.IsShopBet = true
case domain.RoleCustomer:
// Get User Wallet
wallet, err := s.walletSvc.GetWalletsByUser(ctx, userID)
if err != nil {
return domain.CreateBetRes{}, err
}
userWallet := wallet[0]
err = s.walletSvc.DeductFromWallet(ctx, userWallet.ID, domain.ToCurrency(req.Amount))
if err != nil {
return domain.CreateBetRes{}, err
}
default:
return domain.CreateBetRes{}, fmt.Errorf("Unknown Role Type")
}
bet, err := s.CreateBet(ctx, newBet)
if err != nil {
return domain.CreateBetRes{}, err
}
// Associate outcomes with the bet.
for i := range outcomes {
outcomes[i].BetID = bet.ID
}
rows, err := s.betStore.CreateBetOutcome(ctx, outcomes)
if err != nil {
return domain.CreateBetRes{}, err
}
res := domain.ConvertCreateBet(bet, rows)
return res, nil
}
func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportID, HomeTeam, AwayTeam string, StartTime time.Time, numMarkets int) ([]domain.CreateBetOutcome, float32, error) {
var newOdds []domain.CreateBetOutcome
var totalOdds float32 = 1
markets, err := s.prematchSvc.GetPrematchOddsByUpcomingID(ctx, eventID)
if err != nil {
s.logger.Error("failed to get odds for event", "event id", eventID, "error", err)
return nil, 0, err
}
if len(markets) == 0 {
s.logger.Error("empty odds for event", "event id", eventID)
return nil, 0, fmt.Errorf("empty odds or event %v", eventID)
}
var selectedMarkets []domain.Odd
numMarkets = min(numMarkets, len(markets))
for i := 0; i < numMarkets; i++ {
randomIndex := random.Intn(len(markets))
selectedMarkets = append(selectedMarkets, markets[randomIndex])
markets = append(markets[:randomIndex], markets[randomIndex+1:]...)
}
for _, market := range selectedMarkets {
randomRawOdd := market.RawOdds[random.Intn(len(market.RawOdds))]
type rawOddType struct {
ID string
Name string
Odds string
Header string
Handicap string
}
var selectedOdd rawOddType
rawBytes, err := json.Marshal(randomRawOdd)
err = json.Unmarshal(rawBytes, &selectedOdd)
if err != nil {
fmt.Printf("Failed to unmarshal raw odd %v", err)
continue
}
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
if err != nil {
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)
continue
}
oddID, err := strconv.ParseInt(selectedOdd.ID, 10, 64)
if err != nil {
s.logger.Error("Failed to get odd id", "error", err)
continue
}
marketID, err := strconv.ParseInt(market.MarketID, 10, 64)
if err != nil {
s.logger.Error("Failed to get odd id", "error", err)
continue
}
marketName := market.MarketName
newOdds = append(newOdds, domain.CreateBetOutcome{
EventID: eventID,
OddID: oddID,
MarketID: marketID,
SportID: sportID,
HomeTeamName: HomeTeam,
AwayTeamName: AwayTeam,
MarketName: marketName,
Odd: float32(parsedOdd),
OddName: selectedOdd.Name,
OddHeader: selectedOdd.Header,
OddHandicap: selectedOdd.Handicap,
Expires: StartTime,
})
totalOdds = totalOdds * float32(parsedOdd)
}
if len(newOdds) == 0 {
s.logger.Error("Bet Outcomes is empty for market", "selectedMarket", selectedMarkets[0].MarketName)
return nil, 0, ErrGenerateRandomOutcome
}
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) {
// Get a unexpired event id
events, _, err := s.eventSvc.GetPaginatedUpcomingEvents(ctx,
domain.ValidInt64{}, domain.ValidInt64{}, leagueID, sportID, firstStartTime, lastStartTime)
if err != nil {
return domain.CreateBetRes{}, err
}
if len(events) == 0 {
return domain.CreateBetRes{}, ErrNoEventsAvailable
}
// TODO: Add the option of passing number of created events
var selectedUpcomingEvents []domain.UpcomingEvent
numEventsPerBet := min(random.Intn(4)+1, len(events)) //Eliminate the option of 0
for i := 0; i < int(numEventsPerBet); i++ {
randomIndex := random.Intn(len(events))
selectedUpcomingEvents = append(selectedUpcomingEvents, events[randomIndex])
events = append(events[:randomIndex], events[randomIndex+1:]...)
}
// s.logger.Info("Generating random bet events", "selectedUpcomingEvents", len(selectedUpcomingEvents))
// Get market and odds for that
var randomOdds []domain.CreateBetOutcome
var totalOdds float32 = 1
numMarketsPerBet := random.Intn(2) + 1
for _, event := range selectedUpcomingEvents {
newOdds, total, err := s.GenerateRandomBetOutcomes(ctx, event.ID, event.SportID, event.HomeTeam, event.AwayTeam, event.StartTime, numMarketsPerBet)
if err != nil {
s.logger.Error("failed to generate random bet outcome", "event id", event.ID, "error", err)
continue
}
randomOdds = append(randomOdds, newOdds...)
totalOdds = totalOdds * total
}
if len(randomOdds) == 0 {
s.logger.Error("Failed to generate random any outcomes for all events")
return domain.CreateBetRes{}, ErrGenerateRandomOutcome
}
// s.logger.Info("Generated Random bet Outcome", "randomOdds", len(randomOdds))
var cashoutID string
cashoutID, err = s.GenerateCashoutID()
if err != nil {
return domain.CreateBetRes{}, err
}
randomNumber := strconv.FormatInt(int64(random.Intn(100000000000)), 10)
newBet := domain.CreateBet{
Amount: domain.ToCurrency(123.5),
TotalOdds: totalOdds,
Status: domain.OUTCOME_STATUS_PENDING,
FullName: "test" + randomNumber,
PhoneNumber: "0900000000",
CashoutID: cashoutID,
BranchID: domain.ValidInt64{Valid: true, Value: branchID},
UserID: domain.ValidInt64{Valid: true, Value: userID},
}
bet, err := s.CreateBet(ctx, newBet)
if err != nil {
return domain.CreateBetRes{}, err
}
for i := range randomOdds {
randomOdds[i].BetID = bet.ID
}
rows, err := s.betStore.CreateBetOutcome(ctx, randomOdds)
if err != nil {
return domain.CreateBetRes{}, err
}
res := domain.ConvertCreateBet(bet, rows)
return res, nil
}
func (s *Service) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) {
return s.betStore.CreateBet(ctx, bet)
}
@ -64,8 +493,100 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
return s.betStore.UpdateStatus(ctx, id, status)
}
func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error {
return s.betStore.UpdateBetOutcomeStatus(ctx, id, status)
func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) {
betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, err
}
status := domain.OUTCOME_STATUS_PENDING
for _, betOutcome := range betOutcomes {
// If any of the bet outcomes are pending return
if betOutcome.Status == domain.OUTCOME_STATUS_PENDING {
return domain.OUTCOME_STATUS_PENDING, ErrOutcomesNotCompleted
}
if betOutcome.Status == domain.OUTCOME_STATUS_ERROR {
return domain.OUTCOME_STATUS_ERROR, nil
}
// The bet status can only be updated if its not lost or error
// If all the bet outcomes are a win, then set the bet status to win
// If even one of the bet outcomes is a loss then set the bet status to loss
// If even one of the bet outcomes is an error, then set the bet status to error
switch status {
case domain.OUTCOME_STATUS_PENDING:
status = betOutcome.Status
case domain.OUTCOME_STATUS_WIN:
if betOutcome.Status == domain.OUTCOME_STATUS_LOSS {
status = domain.OUTCOME_STATUS_LOSS
} else if betOutcome.Status == domain.OUTCOME_STATUS_HALF {
status = domain.OUTCOME_STATUS_HALF
} else if betOutcome.Status == domain.OUTCOME_STATUS_WIN {
status = domain.OUTCOME_STATUS_WIN
} else if betOutcome.Status == domain.OUTCOME_STATUS_VOID {
status = domain.OUTCOME_STATUS_VOID
} else {
status = domain.OUTCOME_STATUS_ERROR
}
case domain.OUTCOME_STATUS_LOSS:
if betOutcome.Status == domain.OUTCOME_STATUS_LOSS {
status = domain.OUTCOME_STATUS_LOSS
} else if betOutcome.Status == domain.OUTCOME_STATUS_HALF {
status = domain.OUTCOME_STATUS_LOSS
} else if betOutcome.Status == domain.OUTCOME_STATUS_WIN {
status = domain.OUTCOME_STATUS_LOSS
} else if betOutcome.Status == domain.OUTCOME_STATUS_VOID {
status = domain.OUTCOME_STATUS_LOSS
} else {
status = domain.OUTCOME_STATUS_ERROR
}
case domain.OUTCOME_STATUS_VOID:
if betOutcome.Status == domain.OUTCOME_STATUS_VOID ||
betOutcome.Status == domain.OUTCOME_STATUS_WIN ||
betOutcome.Status == domain.OUTCOME_STATUS_HALF {
status = domain.OUTCOME_STATUS_VOID
} else if betOutcome.Status == domain.OUTCOME_STATUS_LOSS {
status = domain.OUTCOME_STATUS_LOSS
} else {
status = domain.OUTCOME_STATUS_ERROR
}
case domain.OUTCOME_STATUS_HALF:
if betOutcome.Status == domain.OUTCOME_STATUS_HALF ||
betOutcome.Status == domain.OUTCOME_STATUS_WIN {
status = domain.OUTCOME_STATUS_HALF
} else if betOutcome.Status == domain.OUTCOME_STATUS_LOSS {
status = domain.OUTCOME_STATUS_LOSS
} else if betOutcome.Status == domain.OUTCOME_STATUS_VOID {
status = domain.OUTCOME_STATUS_VOID
} else {
status = domain.OUTCOME_STATUS_ERROR
}
default:
// If the status is not pending, win, loss or error, then set the status to error
status = domain.OUTCOME_STATUS_ERROR
}
}
if status == domain.OUTCOME_STATUS_PENDING || status == domain.OUTCOME_STATUS_ERROR {
// If the status is pending or error, then we don't need to update the bet
s.logger.Info("bet not updated", "bet id", betID, "status", status)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("Error when processing bet outcomes")
}
return status, nil
}
func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) {
betOutcome, err := s.betStore.UpdateBetOutcomeStatus(ctx, id, status)
if err != nil {
return domain.BetOutcome{}, err
}
return betOutcome, err
}
func (s *Service) DeleteBet(ctx context.Context, id int64) error {

View File

@ -11,7 +11,7 @@ type Service interface {
FetchUpcomingEvents(ctx context.Context) error
GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error)
GetExpiredUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error)
GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32, leagueID domain.ValidString, sportID domain.ValidString) ([]domain.UpcomingEvent, int64, error)
GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidString, sportID domain.ValidString, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error)
GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error)
// GetAndStoreMatchResult(ctx context.Context, eventID string) error

View File

@ -99,18 +99,17 @@ func (s *service) FetchLiveEvents(ctx context.Context) error {
}
func (s *service) FetchUpcomingEvents(ctx context.Context) error {
sportIDs := []int{1, 18}
var totalPages int = 1
var page int = 0
var limit int = 100
var count int = 0
for _, sportID := range sportIDs {
for page != totalPages {
time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour
sportIDs := []int{1, 18, 17}
for _, sportID := range sportIDs {
var totalPages int = 1
var page int = 0
var limit int = 10
var count int = 0
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 event data page %d", page)
log.Printf("📡 Fetching data for sport %d event data page %d/%d", sportID, page, min(limit, totalPages))
resp, err := http.Get(url)
if err != nil {
log.Printf("❌ Failed to fetch event data for page %d: %v", page, err)
@ -145,9 +144,10 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
} `json:"results"`
}
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
log.Printf("❌ Failed to parse json data")
continue
}
skippedLeague := 0
var skippedLeague []string
for _, ev := range data.Results {
startUnix, _ := strconv.ParseInt(ev.Time, 10, 64)
// eventID, err := strconv.ParseInt(ev.ID, 10, 64)
@ -163,14 +163,15 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
}
if !slices.Contains(domain.SupportedLeagues, leagueID) {
skippedLeague++
// fmt.Printf("⚠️ Skipping league %s (%d) as it is not supported\n", ev.League.Name, leagueID)
skippedLeague = append(skippedLeague, ev.League.Name)
continue
}
event := domain.UpcomingEvent{
ID: ev.ID,
SportID: ev.SportID,
MatchName: ev.Home.Name,
MatchName: "",
HomeTeam: ev.Home.Name,
AwayTeam: "", // handle nil safely
HomeTeamID: ev.Home.ID,
@ -186,13 +187,23 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
if ev.Away != nil {
event.AwayTeam = ev.Away.Name
event.AwayTeamID = ev.Away.ID
event.MatchName = ev.Home.Name + " vs " + ev.Away.Name
}
_ = s.store.SaveUpcomingEvent(ctx, event)
err = s.store.SaveUpcomingEvent(ctx, event)
if err != nil {
log.Printf("❌ Failed to save upcoming event %s", event.ID)
}
}
totalPages = data.Pager.Total
if count > limit {
log.Printf("⚠️ Skipped leagues %v", len(skippedLeague))
// log.Printf("⚠️ Total pages %v", data.Pager.Total)
totalPages = data.Pager.Total / data.Pager.PerPage
if count >= limit {
break
}
if page > totalPages {
break
}
count++
@ -223,8 +234,8 @@ func (s *service) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcomi
return s.store.GetExpiredUpcomingEvents(ctx)
}
func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32, leagueID domain.ValidString, sportID domain.ValidString) ([]domain.UpcomingEvent, int64, error) {
return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset, leagueID, sportID)
func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidString, sportID domain.ValidString, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) {
return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset, leagueID, sportID, firstStartTime, lastStartTime)
}
func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) {

View File

@ -4,7 +4,7 @@ import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/websocket/v2"
"github.com/gorilla/websocket"
)
type NotificationStore interface {
@ -16,4 +16,6 @@ type NotificationStore interface {
SendSMS(ctx context.Context, recipientID int64, message string) error
SendEmail(ctx context.Context, recipientID int64, subject, message string) error
ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) // New method
CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error)
GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error)
}

View File

@ -11,12 +11,14 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws"
afro "github.com/amanuelabay/afrosms-go"
"github.com/gofiber/websocket/v2"
"github.com/gorilla/websocket"
)
type Service struct {
repo repository.NotificationRepository
Hub *ws.NotificationHub
connections sync.Map
notificationCh chan *domain.Notification
stopCh chan struct{}
@ -24,9 +26,11 @@ type Service struct {
logger *slog.Logger
}
func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *config.Config) NotificationStore {
func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *config.Config) *Service {
hub := ws.NewNotificationHub()
svc := &Service{
repo: repo,
Hub: hub,
logger: logger,
connections: sync.Map{},
notificationCh: make(chan *domain.Notification, 1000),
@ -34,6 +38,7 @@ func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *confi
config: cfg,
}
go hub.Run()
go svc.startWorker()
go svc.startRetryWorker()
@ -63,22 +68,48 @@ func (s *Service) SendNotification(ctx context.Context, notification *domain.Not
notification = created
if notification.DeliveryChannel == domain.DeliveryChannelInApp {
s.Hub.Broadcast <- map[string]interface{}{
"type": "CREATED_NOTIFICATION",
"recipient_id": notification.RecipientID,
"payload": notification,
}
}
select {
case s.notificationCh <- notification:
default:
s.logger.Error("[NotificationSvc.SendNotification] Notification channel full, dropping notification", "id", notification.ID)
s.logger.Warn("[NotificationSvc.SendNotification] Notification channel full, dropping notification", "id", notification.ID)
}
return nil
}
func (s *Service) MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error {
_, err := s.repo.UpdateNotificationStatus(ctx, notificationID, string(domain.DeliveryStatusSent), true, nil)
if err != nil {
s.logger.Error("[NotificationSvc.MarkAsRead] Failed to mark notification as read", "notificationID", notificationID, "recipientID", recipientID, "error", err)
return err
func (s *Service) MarkAsRead(ctx context.Context, notificationIDs []string, recipientID int64) error {
for _, notificationID := range notificationIDs {
_, err := s.repo.UpdateNotificationStatus(ctx, notificationID, string(domain.DeliveryStatusSent), true, nil)
if err != nil {
s.logger.Error("[NotificationSvc.MarkAsRead] Failed to mark notification as read", "notificationID", notificationID, "recipientID", recipientID, "error", err)
return err
}
// count, err := s.repo.CountUnreadNotifications(ctx, recipientID)
// if err != nil {
// s.logger.Error("[NotificationSvc.MarkAsRead] Failed to count unread notifications", "recipientID", recipientID, "error", err)
// return err
// }
// s.Hub.Broadcast <- map[string]interface{}{
// "type": "COUNT_NOT_OPENED_NOTIFICATION",
// "recipient_id": recipientID,
// "payload": map[string]int{
// "not_opened_notifications_count": int(count),
// },
// }
s.logger.Info("[NotificationSvc.MarkAsRead] Notification marked as read", "notificationID", notificationID, "recipientID", recipientID)
}
s.logger.Info("[NotificationSvc.MarkAsRead] Notification marked as read", "notificationID", notificationID, "recipientID", recipientID)
return nil
}
@ -92,6 +123,16 @@ func (s *Service) ListNotifications(ctx context.Context, recipientID int64, limi
return notifications, nil
}
func (s *Service) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
notifications, err := s.repo.GetAllNotifications(ctx, limit, offset)
if err != nil {
s.logger.Error("[NotificationSvc.ListNotifications] Failed to get all notifications")
return nil, err
}
s.logger.Info("[NotificationSvc.ListNotifications] Successfully retrieved all notifications", "count", len(notifications))
return notifications, nil
}
func (s *Service) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error {
s.addConnection(ctx, recipientID, c)
s.logger.Info("[NotificationSvc.ConnectWebSocket] WebSocket connection established", "recipientID", recipientID)
@ -99,7 +140,6 @@ func (s *Service) ConnectWebSocket(ctx context.Context, recipientID int64, c *we
}
func (s *Service) DisconnectWebSocket(recipientID int64) {
s.connections.Delete(recipientID)
if conn, loaded := s.connections.LoadAndDelete(recipientID); loaded {
conn.(*websocket.Conn).Close()
s.logger.Info("[NotificationSvc.DisconnectWebSocket] Disconnected WebSocket", "recipientID", recipientID)
@ -160,21 +200,26 @@ func (s *Service) ListRecipientIDs(ctx context.Context, receiver domain.Notifica
func (s *Service) handleNotification(notification *domain.Notification) {
ctx := context.Background()
if conn, ok := s.connections.Load(notification.RecipientID); ok {
data, err := notification.ToJSON()
switch notification.DeliveryChannel {
case domain.DeliveryChannelSMS:
err := s.SendSMS(ctx, notification.RecipientID, notification.Payload.Message)
if err != nil {
s.logger.Error("[NotificationSvc.HandleNotification] Failed to serialize notification", "id", notification.ID, "error", err)
return
}
if err := conn.(*websocket.Conn).WriteMessage(websocket.TextMessage, data); err != nil {
s.logger.Error("[NotificationSvc.HandleNotification] Failed to send WebSocket message", "id", notification.ID, "error", err)
notification.DeliveryStatus = domain.DeliveryStatusFailed
} else {
notification.DeliveryStatus = domain.DeliveryStatusSent
}
} else {
s.logger.Warn("[NotificationSvc.HandleNotification] No WebSocket connection for recipient", "recipientID", notification.RecipientID)
notification.DeliveryStatus = domain.DeliveryStatusFailed
case domain.DeliveryChannelEmail:
err := s.SendEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message)
if err != nil {
notification.DeliveryStatus = domain.DeliveryStatusFailed
} else {
notification.DeliveryStatus = domain.DeliveryStatusSent
}
default:
if notification.DeliveryChannel != domain.DeliveryChannelInApp {
s.logger.Warn("[NotificationSvc.HandleNotification] Unsupported delivery channel", "channel", notification.DeliveryChannel)
notification.DeliveryStatus = domain.DeliveryStatusFailed
}
}
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
@ -210,13 +255,17 @@ func (s *Service) retryFailedNotifications() {
go func(notification *domain.Notification) {
for attempt := 0; attempt < 3; attempt++ {
time.Sleep(time.Duration(attempt) * time.Second)
if conn, ok := s.connections.Load(notification.RecipientID); ok {
data, err := notification.ToJSON()
if err != nil {
s.logger.Error("[NotificationSvc.RetryFailedNotifications] Failed to serialize notification for retry", "id", notification.ID, "error", err)
continue
if notification.DeliveryChannel == domain.DeliveryChannelSMS {
if err := s.SendSMS(ctx, notification.RecipientID, notification.Payload.Message); err == nil {
notification.DeliveryStatus = domain.DeliveryStatusSent
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
s.logger.Error("[NotificationSvc.RetryFailedNotifications] Failed to update after retry", "id", notification.ID, "error", err)
}
s.logger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification", "id", notification.ID)
return
}
if err := conn.(*websocket.Conn).WriteMessage(websocket.TextMessage, data); err == nil {
} else if notification.DeliveryChannel == domain.DeliveryChannelEmail {
if err := s.SendEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message); err == nil {
notification.DeliveryStatus = domain.DeliveryStatusSent
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
s.logger.Error("[NotificationSvc.RetryFailedNotifications] Failed to update after retry", "id", notification.ID, "error", err)
@ -230,3 +279,7 @@ func (s *Service) retryFailedNotifications() {
}(notification)
}
}
func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) {
return s.repo.CountUnreadNotifications(ctx, recipient_id)
}

View File

@ -9,6 +9,8 @@ import (
type Service interface {
FetchNonLiveOdds(ctx context.Context) error
GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error)
GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error)
GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt64, offset domain.ValidInt64) ([]domain.Odd, error)
GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error)
GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error)
}

View File

@ -3,26 +3,37 @@ package odds
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
)
type ServiceImpl struct {
token string
store *repository.Store
store *repository.Store
config *config.Config
logger *slog.Logger
client *http.Client
}
func New(token string, store *repository.Store) *ServiceImpl {
return &ServiceImpl{token: token, store: store}
func New(store *repository.Store, cfg *config.Config, logger *slog.Logger) *ServiceImpl {
return &ServiceImpl{
store: store,
config: cfg,
logger: logger,
client: &http.Client{Timeout: 10 * time.Second},
}
}
// TODO this is only getting the main odds, this must be fixed
// TODO Add the optimization to get 10 events at the same time
func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
eventIDs, err := s.store.GetAllUpcomingEvents(ctx)
if err != nil {
@ -30,86 +41,252 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
return err
}
for _, event := range eventIDs {
// time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour
var errs []error
eventID := event.ID
prematchURL := "https://api.b365api.com/v3/bet365/prematch?token=" + s.token + "&FI=" + eventID
log.Printf("📡 Fetching prematch odds for event ID: %s", eventID)
resp, err := http.Get(prematchURL)
for index, event := range eventIDs {
eventID, err := strconv.ParseInt(event.ID, 10, 64)
if err != nil {
log.Printf("❌ Failed to fetch prematch odds for event %s: %v", eventID, err)
s.logger.Error("Failed to parse event id")
return err
}
url := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%d", s.config.Bet365Token, eventID)
log.Printf("📡 Fetching prematch odds for event ID: %d (%d/%d) ", eventID, index, len(eventIDs))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
log.Printf("❌ Failed to create request for event %d: %v", eventID, err)
continue
}
resp, err := s.client.Do(req)
if err != nil {
log.Printf("❌ Failed to fetch prematch odds for event %d: %v", eventID, err)
continue
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var oddsData struct {
Success int `json:"success"`
Results []struct {
EventID string `json:"event_id"`
FI string `json:"FI"`
Main OddsSection `json:"main"`
} `json:"results"`
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("❌ Failed to read response body for event %d: %v", eventID, err)
continue
}
var oddsData domain.BaseNonLiveOddResponse
if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 {
log.Printf("❌ Invalid prematch data for event %s", eventID)
log.Printf("❌ Invalid prematch data for event %d", eventID)
continue
}
result := oddsData.Results[0]
finalID := result.EventID
if finalID == "" {
finalID = result.FI
sportID, err := strconv.ParseInt(event.SportID, 10, 64)
switch sportID {
case domain.FOOTBALL:
if err := s.parseFootball(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Error while inserting football odd")
errs = append(errs, err)
}
case domain.BASKETBALL:
if err := s.parseBasketball(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Error while inserting basketball odd")
errs = append(errs, err)
}
case domain.ICE_HOCKEY:
if err := s.parseIceHockey(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Error while inserting ice hockey odd")
errs = append(errs, err)
}
}
if finalID == "" {
log.Printf("⚠️ Skipping event %s with no valid ID", eventID)
continue
}
s.storeSection(ctx, finalID, result.FI, "main", result.Main)
// result := oddsData.Results[0]
}
return nil
}
func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section OddsSection) {
func (s *ServiceImpl) parseFootball(ctx context.Context, res json.RawMessage) error {
var footballRes domain.FootballOddsResponse
if err := json.Unmarshal(res, &footballRes); err != nil {
s.logger.Error("Failed to unmarshal football result", "error", err)
return err
}
if footballRes.EventID == "" && footballRes.FI == "" {
s.logger.Error("Skipping football result with no valid Event ID", "eventID", footballRes.EventID, "fi", footballRes.FI)
return fmt.Errorf("Skipping football result with no valid Event ID Event ID %v", footballRes.EventID)
}
sections := map[string]domain.OddsSection{
"main": footballRes.Main,
"asian_lines": footballRes.AsianLines,
"goals": footballRes.Goals,
"half": footballRes.Half,
}
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, footballRes.EventID, footballRes.FI, oddCategory, section); err != nil {
s.logger.Error("Error storing football section", "eventID", footballRes.FI, "odd", oddCategory)
log.Printf("⚠️ Error when storing football %v", err)
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) parseBasketball(ctx context.Context, res json.RawMessage) error {
var basketballRes domain.BasketballOddsResponse
if err := json.Unmarshal(res, &basketballRes); err != nil {
s.logger.Error("Failed to unmarshal basketball result", "error", err)
return err
}
if basketballRes.EventID == "" && basketballRes.FI == "" {
s.logger.Error("Skipping basketball result with no valid Event ID")
return fmt.Errorf("Skipping basketball result with no valid Event ID")
}
sections := map[string]domain.OddsSection{
"main": basketballRes.Main,
"half_props": basketballRes.HalfProps,
"quarter_props": basketballRes.QuarterProps,
"team_props": basketballRes.TeamProps,
}
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, basketballRes.EventID, basketballRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
for _, section := range basketballRes.Others {
if err := s.storeSection(ctx, basketballRes.EventID, basketballRes.FI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) parseIceHockey(ctx context.Context, res json.RawMessage) error {
var iceHockeyRes domain.IceHockeyOddsResponse
if err := json.Unmarshal(res, &iceHockeyRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return err
}
if iceHockeyRes.EventID == "" && iceHockeyRes.FI == "" {
s.logger.Error("Skipping result with no valid Event ID")
return fmt.Errorf("Skipping result with no valid Event ID")
}
sections := map[string]domain.OddsSection{
"main": iceHockeyRes.Main,
"main_2": iceHockeyRes.Main2,
"1st_period": iceHockeyRes.FirstPeriod,
}
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, iceHockeyRes.EventID, iceHockeyRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
for _, section := range iceHockeyRes.Others {
if err := s.storeSection(ctx, iceHockeyRes.EventID, iceHockeyRes.FI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section domain.OddsSection) error {
if len(section.Sp) == 0 {
return
return nil
}
updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64)
updatedAt := time.Unix(updatedAtUnix, 0)
var errs []error
for marketType, market := range section.Sp {
if len(market.Odds) == 0 {
continue
}
// Check if the market id is a string
var marketIDstr string
err := json.Unmarshal(market.ID, &marketIDstr)
if err != nil {
// check if its int
var marketIDint int
err := json.Unmarshal(market.ID, &marketIDint)
if err != nil {
s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name)
errs = append(errs, err)
}
}
marketIDint, err := strconv.ParseInt(marketIDstr, 10, 64)
if err != nil {
s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name)
errs = append(errs, err)
continue
}
isSupported, ok := domain.SupportedMarkets[marketIDint]
if !ok || !isSupported {
// s.logger.Info("Unsupported market_id", "marketID", marketIDint, "marketName", market.Name)
continue
}
marketRecord := domain.Market{
EventID: eventID,
FI: fi,
MarketCategory: sectionName,
MarketType: marketType,
MarketName: market.Name,
MarketID: market.ID.String(),
MarketID: marketIDstr,
UpdatedAt: updatedAt,
Odds: market.Odds,
}
_ = s.store.SaveNonLiveMarket(ctx, marketRecord)
err = s.store.SaveNonLiveMarket(ctx, marketRecord)
if err != nil {
s.logger.Error("failed to save market", "market_id", market.ID, "error", err)
errs = append(errs, fmt.Errorf("market %s: %w", market.ID, err))
continue
}
}
}
type OddsMarket struct {
ID json.Number `json:"id"`
Name string `json:"name"`
Odds []json.RawMessage `json:"odds"`
Header string `json:"header,omitempty"`
Handicap string `json:"handicap,omitempty"`
}
type OddsSection struct {
UpdatedAt string `json:"updated_at"`
Sp map[string]OddsMarket `json:"sp"`
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) {
@ -129,6 +306,10 @@ func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string,
return rows, nil
}
func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) {
return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset)
func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error) {
return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID)
}
func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset domain.ValidInt64) ([]domain.Odd, error) {
return s.store.GetPaginatedPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset)
}

View File

@ -9,6 +9,8 @@ import (
)
// Football evaluations
// Full Time Result betting is a type of bet where the bettor predicts the outcome of a match at the end of the full 90 minutes of play.
func evaluateFullTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddName {
case "1": // Home win
@ -27,15 +29,16 @@ func evaluateFullTimeResult(outcome domain.BetOutcome, score struct{ Home, Away
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
}
// Over/Under betting is a type of bet where the bettor predicts whether the total number of goals scored in a match will be over or under a specified number.
func evaluateGoalsOverUnder(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalGoals := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
if outcome.OddHeader == "Over" {
@ -53,9 +56,10 @@ func evaluateGoalsOverUnder(outcome domain.BetOutcome, score struct{ Home, Away
}
return domain.OUTCOME_STATUS_LOSS, nil
}
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
// Correct Score betting is a type of bet where the bettor predicts the exact final score of a match.
func evaluateCorrectScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
expectedScore := fmt.Sprintf("%d-%d", score.Home, score.Away)
if outcome.OddName == expectedScore {
@ -64,6 +68,8 @@ func evaluateCorrectScore(outcome domain.BetOutcome, score struct{ Home, Away in
return domain.OUTCOME_STATUS_LOSS, nil
}
// Half Time Result betting is a type of bet where the bettor predicts the outcome of a match at the end of the first half.
// This is the same as the full time result but only for the first half of the game
func evaluateHalfTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
return evaluateFullTimeResult(outcome, score)
}
@ -71,43 +77,94 @@ func evaluateHalfTimeResult(outcome domain.BetOutcome, score struct{ Home, Away
// This is a multiple outcome checker for the asian handicap and other kinds of bets
// The only outcome that are allowed are "Both Bets win", "Both Bets Lose", "Half Win and Half Void"
func checkMultiOutcome(outcome domain.OutcomeStatus, secondOutcome domain.OutcomeStatus) (domain.OutcomeStatus, error) {
if secondOutcome == domain.OUTCOME_STATUS_PENDING {
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("cannot check pending outcome")
}
if outcome == domain.OUTCOME_STATUS_ERROR || secondOutcome == domain.OUTCOME_STATUS_ERROR {
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("❌ mutli outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
}
// fmt.Printf("| Multi Outcome | %v -> %v \n", outcome.String(), secondOutcome.String())
switch outcome {
case domain.OUTCOME_STATUS_PENDING:
return secondOutcome, nil
case domain.OUTCOME_STATUS_WIN:
if secondOutcome == domain.OUTCOME_STATUS_WIN {
return domain.OUTCOME_STATUS_WIN, nil
} else if secondOutcome == domain.OUTCOME_STATUS_LOSS {
return domain.OUTCOME_STATUS_LOSS, nil
} else if secondOutcome == domain.OUTCOME_STATUS_HALF {
return domain.OUTCOME_STATUS_VOID, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID {
return domain.OUTCOME_STATUS_HALF, nil
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid multi outcome")
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
}
case domain.OUTCOME_STATUS_LOSS:
if secondOutcome == domain.OUTCOME_STATUS_LOSS {
if secondOutcome == domain.OUTCOME_STATUS_LOSS ||
secondOutcome == domain.OUTCOME_STATUS_WIN ||
secondOutcome == domain.OUTCOME_STATUS_HALF {
return domain.OUTCOME_STATUS_LOSS, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID {
return domain.OUTCOME_STATUS_HALF, nil
return domain.OUTCOME_STATUS_VOID, nil
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid multi outcome")
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
}
case domain.OUTCOME_STATUS_VOID:
if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_LOSS {
return domain.OUTCOME_STATUS_HALF, nil
return domain.OUTCOME_STATUS_VOID, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID || secondOutcome == domain.OUTCOME_STATUS_HALF {
return domain.OUTCOME_STATUS_VOID, nil
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid multi outcome")
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
}
case domain.OUTCOME_STATUS_HALF:
if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_HALF {
return domain.OUTCOME_STATUS_VOID, nil
} else if secondOutcome == domain.OUTCOME_STATUS_LOSS {
return domain.OUTCOME_STATUS_LOSS, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID {
return domain.OUTCOME_STATUS_VOID, nil
} else {
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
}
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid multi outcome")
fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String())
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome")
}
}
// Asian Handicap betting is a type of betting that eliminates the possibility of a draw by giving one team a virtual advantage or disadvantage.
// When the handicap has two values like "+0.5, +1.0" or "-0.5, -1.0", then it a multi outcome bet
// .
//
// {
// "id": "548319135",
// "odds": "1.750",
// "header": "1",
// "handicap": "+0.5, +1.0"
// },
//
// {
// "id": "548319139",
// "odds": "1.950",
// "header": "2",
// "handicap": "-0.5, -1.0"
// }
func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
handicapList := strings.Split(outcome.OddHandicap, ",")
newOutcome := domain.OUTCOME_STATUS_PENDING
for _, handicapStr := range handicapList {
handicapStr = strings.TrimSpace(handicapStr)
handicap, err := strconv.ParseFloat(handicapStr, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap)
}
adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away)
@ -116,49 +173,123 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i
} else if outcome.OddHeader == "2" { // Away team
adjustedAwayScore += handicap
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
if adjustedHomeScore > adjustedAwayScore {
if outcome.OddHeader == "1" {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
if err != nil {
fmt.Printf("multi outcome check error")
return domain.OUTCOME_STATUS_PENDING, err
return domain.OUTCOME_STATUS_ERROR, err
}
continue
}
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS)
if err != nil {
fmt.Printf("multi outcome check error")
return domain.OUTCOME_STATUS_PENDING, err
return domain.OUTCOME_STATUS_ERROR, err
}
continue
} else if adjustedHomeScore < adjustedAwayScore {
if outcome.OddHeader == "2" {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
if err != nil {
fmt.Printf("multi outcome check error")
return domain.OUTCOME_STATUS_PENDING, err
return domain.OUTCOME_STATUS_ERROR, err
}
continue
}
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS)
if err != nil {
fmt.Printf("multi outcome check error")
return domain.OUTCOME_STATUS_PENDING, err
return domain.OUTCOME_STATUS_ERROR, err
}
}
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID)
if err != nil {
fmt.Printf("multi outcome check error")
return domain.OUTCOME_STATUS_PENDING, err
continue
} else if adjustedHomeScore == adjustedAwayScore {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID)
if err != nil {
return domain.OUTCOME_STATUS_ERROR, err
}
continue
}
}
return newOutcome, nil
}
// Goal Line betting, also known as Over/Under betting,
// involves predicting the total number of goals scored in a match, regardless of which team wins.
//
// {
// "id": "548319141",
// "odds": "1.800",
// "header": "Over",
// "name": "1.5, 2.0"
// },
//
// {
// "id": "548319146",
// "odds": "1.900",
// "header": "Under",
// "name": "1.5, 2.0"
// }
func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
return evaluateGoalsOverUnder(outcome, score)
totalGoals := float64(score.Home + score.Away)
thresholdList := strings.Split(outcome.OddName, ",")
newOutcome := domain.OUTCOME_STATUS_PENDING
for _, thresholdStr := range thresholdList {
thresholdStr = strings.TrimSpace(thresholdStr)
threshold, err := strconv.ParseFloat(thresholdStr, 64)
if err != nil {
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: '%s', %v", thresholdStr, err)
}
oddHeader := strings.TrimSpace(outcome.OddHeader)
if oddHeader == "Over" {
if totalGoals > threshold {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
if err != nil {
return domain.OUTCOME_STATUS_ERROR, err
}
} else if totalGoals == threshold {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID)
if err != nil {
return domain.OUTCOME_STATUS_ERROR, err
}
}
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS)
if err != nil {
return domain.OUTCOME_STATUS_ERROR, err
}
} else if oddHeader == "Under" {
if totalGoals < threshold {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
if err != nil {
return domain.OUTCOME_STATUS_ERROR, err
}
} else if totalGoals == threshold {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID)
if err != nil {
return domain.OUTCOME_STATUS_ERROR, err
}
}
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS)
if err != nil {
return domain.OUTCOME_STATUS_ERROR, err
}
} else {
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: '%s'", oddHeader)
}
}
return newOutcome, nil
}
// First Team To Score betting is a type of bet where the bettor predicts which team will score first in a match.
// We can get this from the "events" field on the result json
func evaluateFirstTeamToScore(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) {
for _, event := range events {
if strings.Contains(event["text"], "1st Goal") || strings.Contains(event["text"], "Goal 1") {
@ -173,6 +304,7 @@ func evaluateFirstTeamToScore(outcome domain.BetOutcome, events []map[string]str
return domain.OUTCOME_STATUS_VOID, nil // No goals scored
}
// Goals Odd/Even betting is a type of bet where the bettor predicts whether the total number of goals scored in a match will be odd or even.
func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalGoals := score.Home + score.Away
isOdd := totalGoals%2 == 1
@ -184,32 +316,70 @@ func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away in
return domain.OUTCOME_STATUS_LOSS, nil
}
func evaluateTeamOddEven(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":
if outcome.OddHandicap == "Odd" {
if score.Home%2 == 1 {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHandicap == "Even" {
if score.Home%2 == 0 {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else {
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap)
}
case "2":
if outcome.OddHandicap == "Odd" {
if score.Away%2 == 1 {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHandicap == "Even" {
if score.Away%2 == 0 {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else {
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap)
}
default:
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
}
// Double Chance betting is a type of bet where the bettor predicts two of the three possible outcomes of a match.
func evaluateDoubleChance(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
isHomeWin := score.Home > score.Away
isDraw := score.Home == score.Away
isAwayWin := score.Away > score.Home
switch outcome.OddName {
case "1 or Draw", (outcome.HomeTeamName + " or " + "Draw"):
case "1 or Draw", (outcome.HomeTeamName + " or " + "Draw"), ("Draw" + " or " + outcome.HomeTeamName):
if isHomeWin || isDraw {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "Draw or 2", ("Draw" + " or " + outcome.AwayTeamName):
case "Draw or 2", ("Draw" + " or " + outcome.AwayTeamName), (outcome.AwayTeamName + " or " + "Draw"):
if isDraw || isAwayWin {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "1 or 2", (outcome.HomeTeamName + " or " + outcome.AwayTeamName):
case "1 or 2", (outcome.HomeTeamName + " or " + outcome.AwayTeamName), (outcome.AwayTeamName + " or " + outcome.HomeTeamName):
if isHomeWin || isAwayWin {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
}
// Draw No Bet betting is a type of bet where the bettor predicts the outcome of a match, but if the match ends in a draw, the bet is voided.
func evaluateDrawNoBet(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
if score.Home == score.Away {
return domain.OUTCOME_STATUS_VOID, nil
@ -222,8 +392,37 @@ func evaluateDrawNoBet(outcome domain.BetOutcome, score struct{ Home, Away int }
return domain.OUTCOME_STATUS_LOSS, nil
}
// basketball evaluations
func evaluateCorners(outcome domain.BetOutcome, corners struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalCorners := corners.Home + corners.Away
threshold, err := strconv.ParseFloat(outcome.OddName, 10)
if err != nil {
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
switch outcome.OddHeader {
case "Over":
if totalCorners > int(threshold) {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "Under":
if totalCorners < int(threshold) {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "Exactly":
if totalCorners == int(threshold) {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
}
// Basketball evaluations
// Game Lines is an aggregate of money line, spread and total betting markets in one
func evaluateGameLines(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddName {
case "Money Line":
@ -235,10 +434,11 @@ func evaluateGameLines(outcome domain.BetOutcome, score struct{ Home, Away int }
case "Total":
return evaluateTotalOverUnder(outcome, score)
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
}
// Money Line betting is a type of bet where the bettor predicts the outcome of a match without any point spread.
func evaluateMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":
@ -258,21 +458,22 @@ func evaluateMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
}
// Total Over/Under betting is a type of bet where the bettor predicts whether the total number of points scored in a match will be over or under a specified number.
func evaluateTotalOverUnder(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// The handicap will be in the format "U {float}" or "O {float}"
// U and O denoting over and under for this case
overUnderStr := strings.Split(outcome.OddHandicap, " ")
if len(overUnderStr) != 2 {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
// Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet
@ -294,26 +495,28 @@ func evaluateTotalOverUnder(outcome domain.BetOutcome, score struct{ Home, Away
}
return domain.OUTCOME_STATUS_LOSS, nil
}
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
// Result and Total betting is a type of bet where the bettor predicts
// the outcome of a match and whether the total number of points scored will be over or under a specified number.
func evaluateResultAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// The handicap will be in the format "U {float}" or "O {float}"
// U and O denoting over and under for this case
overUnderStr := strings.Split(outcome.OddHandicap, " ")
if len(overUnderStr) != 2 {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
overUnder := overUnderStr[0]
if overUnder != "Over" && overUnder != "Under" {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader)
}
threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
// Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet
@ -321,6 +524,10 @@ func evaluateResultAndTotal(outcome domain.BetOutcome, score struct{ Home, Away
switch outcome.OddHeader {
case "1":
if score.Home < score.Away {
return domain.OUTCOME_STATUS_LOSS, nil
}
if overUnder == "Over" && totalScore > threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if overUnder == "Under" && totalScore < threshold {
@ -328,6 +535,9 @@ func evaluateResultAndTotal(outcome domain.BetOutcome, score struct{ Home, Away
}
return domain.OUTCOME_STATUS_LOSS, nil
case "2":
if score.Away < score.Home {
return domain.OUTCOME_STATUS_LOSS, nil
}
if overUnder == "Over" && totalScore > threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if overUnder == "Under" && totalScore < threshold {
@ -336,27 +546,29 @@ func evaluateResultAndTotal(outcome domain.BetOutcome, score struct{ Home, Away
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed to parse over and under: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName)
}
}
// Team Total betting is a type of bet where the bettor predicts the total number of points scored by a specific team in a match
// is over or under a specified number.
func evaluateTeamTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// The handicap will be in the format "U {float}" or "O {float}"
// U and O denoting over and under for this case
overUnderStr := strings.Split(outcome.OddHandicap, " ")
if len(overUnderStr) != 2 {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap)
}
overUnder := overUnderStr[0]
if overUnder != "Over" && overUnder != "Under" {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader)
}
threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap)
}
// Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet
@ -380,11 +592,12 @@ func evaluateTeamTotal(outcome domain.BetOutcome, score struct{ Home, Away int }
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed to parse over and under: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName)
}
}
// Evaluate Result and Both Teams To Score X Points
// Result and Both Teams To Score X Points is a type of bet where the bettor predicts whether both teams will score a certain number of points
// and also the result fo the match
func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
// The name parameter will hold value "name": "{team_name} and {Yes | No}"
@ -400,14 +613,14 @@ func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away
} else if scoreCheckSplit == "No" {
isScorePoints = false
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
teamName := strings.TrimSpace(strings.Join(oddNameSplit[:len(oddNameSplit)-2], ""))
threshold, err := strconv.ParseInt(outcome.OddHeader, 10, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddHeader)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHeader)
}
switch teamName {
@ -428,18 +641,18 @@ func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away
}
}
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("team name error: %s", teamName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("team name error: %s", teamName)
}
return domain.OUTCOME_STATUS_LOSS, nil
}
// Both Teams To Score X Points
// Both Teams To Score X Points is a type of bet where the bettor predicts whether both teams will score a certain number of points.
func evaluateBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
threshold, err := strconv.ParseInt(outcome.OddName, 10, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
switch outcome.OddHeader {
@ -453,12 +666,13 @@ func evaluateBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (d
}
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
return domain.OUTCOME_STATUS_LOSS, nil
}
// Money Line 3 Way betting is a type of bet where the bettor predicts the outcome of a match with three possible outcomes: home win, away win, or draw.
func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddName {
case "1": // Home win
@ -477,23 +691,24 @@ func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away i
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
}
// Double Result betting is a type of bet where the bettor predicts the outcome of a match at both half-time and full-time.
func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home, Away int }, secondHalfScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
halfWins := strings.Split(outcome.OddName, "-")
if len(halfWins) != 2 {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName)
}
firstHalfWinner := strings.TrimSpace(halfWins[0])
secondHalfWinner := strings.TrimSpace(halfWins[1])
if firstHalfWinner != outcome.HomeTeamName && firstHalfWinner != outcome.AwayTeamName && firstHalfWinner != "Tie" {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", firstHalfWinner)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner)
}
if secondHalfWinner != outcome.HomeTeamName && secondHalfWinner != outcome.AwayTeamName && secondHalfWinner != "Tie" {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", firstHalfWinner)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner)
}
switch {
@ -517,6 +732,7 @@ func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home
return domain.OUTCOME_STATUS_WIN, nil
}
// Highest Scoring Half betting is a type of bet where the bettor predicts which half of the match will have the highest total score.
func evaluateHighestScoringHalf(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
firstHalfTotal := firstScore.Home + firstScore.Away
secondHalfTotal := secondScore.Home + secondScore.Away
@ -534,11 +750,12 @@ func evaluateHighestScoringHalf(outcome domain.BetOutcome, firstScore struct{ Ho
return domain.OUTCOME_STATUS_WIN, nil
}
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
}
return domain.OUTCOME_STATUS_LOSS, nil
}
// Highest Scoring Quarter betting is a type of bet where the bettor predicts which quarter of the match will have the highest score.
func evaluateHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }, fourthScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
firstQuarterTotal := firstScore.Home + firstScore.Away
secondQuarterTotal := secondScore.Home + secondScore.Away
@ -567,18 +784,44 @@ func evaluateHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{
return domain.OUTCOME_STATUS_WIN, nil
}
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
}
return domain.OUTCOME_STATUS_LOSS, nil
}
// Team With Highest Scoring Quarter betting is a type of bet where the bettor predicts which team will have the highest score in a specific quarter.
func evaluateTeamWithHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }, fourthScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
homeTeamHighestQuarter := max(firstScore.Home, secondScore.Home, thirdScore.Home, fourthScore.Home)
awayTeamHighestQuarter := max(firstScore.Away, secondScore.Away, thirdScore.Away, fourthScore.Away)
switch outcome.OddName {
case "1":
if homeTeamHighestQuarter > awayTeamHighestQuarter {
return domain.OUTCOME_STATUS_WIN, nil
}
case "2":
if awayTeamHighestQuarter > homeTeamHighestQuarter {
return domain.OUTCOME_STATUS_WIN, nil
}
case "Tie":
if homeTeamHighestQuarter == awayTeamHighestQuarter {
return domain.OUTCOME_STATUS_WIN, nil
}
default:
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
}
return domain.OUTCOME_STATUS_LOSS, nil
}
// Handicap and Total betting is a combination of spread betting and total points betting
// where the bettor predicts the outcome of a match with a point spread and the total number of points scored is over or under a specified number.
func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
nameSplit := strings.Split(outcome.OddName, " ")
// Evaluate from bottom to get the threshold and find out if its over or under
threshold, err := strconv.ParseFloat(nameSplit[len(nameSplit)-1], 10)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed parsing threshold: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing threshold: %s", outcome.OddName)
}
total := float64(score.Home + score.Away)
overUnder := nameSplit[len(nameSplit)-2]
@ -591,12 +834,12 @@ func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Awa
return domain.OUTCOME_STATUS_LOSS, nil
}
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed parsing over and under: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over and under: %s", outcome.OddName)
}
handicap, err := strconv.ParseFloat(nameSplit[len(nameSplit)-4], 10)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed parsing handicap: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing handicap: %s", outcome.OddName)
}
teamName := strings.TrimSpace(strings.Join(nameSplit[:len(nameSplit)-4], ""))
@ -618,21 +861,22 @@ func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Awa
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("failed parsing team name: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing team name: %s", outcome.OddName)
}
}
// Winning Margin betting is a type of bet where the bettor predicts the margin of victory in a match.
func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
marginSplit := strings.Split(outcome.OddName, "")
if len(marginSplit) < 1 {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
}
margin, err := strconv.ParseInt(marginSplit[0], 10, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
}
isGtr := false
@ -656,9 +900,10 @@ func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away i
return domain.OUTCOME_STATUS_LOSS, nil
}
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddheader: %s", outcome.OddHeader)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddheader: %s", outcome.OddHeader)
}
// Highest Scoring Period betting is a type of bet where the bettor predicts which period of the match will have the highest total score.
func evaluateHighestScoringPeriod(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
firstPeriodTotal := firstScore.Home + firstScore.Away
secondPeriodTotal := secondScore.Home + secondScore.Away
@ -682,11 +927,12 @@ func evaluateHighestScoringPeriod(outcome domain.BetOutcome, firstScore struct{
return domain.OUTCOME_STATUS_WIN, nil
}
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
}
return domain.OUTCOME_STATUS_LOSS, nil
}
// Tied After Regulation is a type of bet where the bettor predicts whether the match will end in a tie after regulation time.
func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalScore := struct{ Home, Away int }{0, 0}
for _, score := range scores {
@ -706,6 +952,37 @@ func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Hom
return domain.OUTCOME_STATUS_LOSS, nil
}
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName)
return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName)
}
// evaluateRugbyOutcome evaluates the outcome of a Rugby bet
func evaluateRugbyOutcome(outcome domain.BetOutcome, result *domain.RugbyResultResponse) (domain.OutcomeStatus, error) {
finalScore := parseSS(result.SS)
switch outcome.MarketName {
case "Money Line":
return evaluateRugbyMoneyLine(outcome, finalScore)
case "Spread":
return evaluateRugbySpread(outcome, finalScore)
case "Total Points":
return evaluateRugbyTotalPoints(outcome, finalScore)
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported rugby market: %s", outcome.MarketName)
}
}
// evaluateBaseballOutcome evaluates the outcome of a Baseball bet
func evaluateBaseballOutcome(outcome domain.BetOutcome, result *domain.BaseballResultResponse) (domain.OutcomeStatus, error) {
finalScore := parseSS(result.SS)
switch outcome.MarketName {
case "Money Line":
return evaluateBaseballMoneyLine(outcome, finalScore)
case "Spread":
return evaluateBaseballSpread(outcome, finalScore)
case "Total Runs":
return evaluateBaseballTotalRuns(outcome, finalScore)
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported baseball market: %s", outcome.MarketName)
}
}

View File

@ -0,0 +1,30 @@
package result
import (
"testing"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func TestEvaluateFullTimeResult(t *testing.T) {
tests := []struct {
name string
outcome domain.BetOutcome
score struct{ Home, Away int }
expected domain.OutcomeStatus
}{
{"Home win", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN},
{"Away win", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN},
{"Draw", domain.BetOutcome{OddName: "Draw"}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_WIN},
{"Home selected, but Draw", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_LOSS},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status, _ := evaluateFullTimeResult(tt.outcome, tt.score)
if status != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, status)
}
})
}
}

View File

@ -13,6 +13,7 @@ import (
"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/bet"
)
type Service struct {
@ -20,19 +21,22 @@ type Service struct {
config *config.Config
logger *slog.Logger
client *http.Client
betSvc bet.Service
}
func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger) *Service {
func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service) *Service {
return &Service{
repo: repo,
config: cfg,
logger: logger,
client: &http.Client{Timeout: 10 * time.Second},
betSvc: betSvc,
}
}
type ResultCheck struct {
}
var (
ErrEventIsNotActive = fmt.Errorf("Event has been cancelled or postponed")
)
func (s *Service) FetchAndProcessResults(ctx context.Context) error {
// TODO: Optimize this because there could be many bet outcomes for the same odd
@ -42,9 +46,10 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
s.logger.Error("Failed to fetch events")
return err
}
fmt.Printf("Expired Events: %d \n", len(events))
fmt.Printf("⚠️ Expired Events: %d \n", len(events))
removed := 0
for i, event := range events {
for _, event := range events {
eventID, err := strconv.ParseInt(event.ID, 10, 64)
if err != nil {
s.logger.Error("Failed to parse event id")
@ -56,48 +61,102 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
return err
}
for _, outcome := range outcomes {
if len(outcomes) == 0 {
fmt.Printf("🕛 No bets have been placed on event %v (%d/%d) \n", event.ID, i+1, len(events))
} else {
fmt.Printf("✅ %d bets have been placed on event %v (%d/%d) \n", len(outcomes), event.ID, i+1, len(events))
}
isDeleted := true
for j, outcome := range outcomes {
fmt.Printf("⚙️ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
outcome.MarketName,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
if outcome.Expires.After(time.Now()) {
isDeleted = false
s.logger.Warn("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID)
continue
}
sportID, err := strconv.ParseInt(event.SportID, 10, 64)
if err != nil {
s.logger.Error("Sport ID is invalid", "event_id", outcome.EventID, "error", err)
isDeleted = false
continue
}
// TODO: optimize this because the result is being fetched for each outcome which will have the same event id but different market id
result, err := s.fetchResult(ctx, outcome.EventID, outcome.OddID, outcome.MarketID, sportID, outcome)
if err != nil {
s.logger.Error("Failed to fetch result", "event_id", outcome.EventID, "error", err)
if err == ErrEventIsNotActive {
s.logger.Warn("Event is not active", "event_id", outcome.EventID, "error", err)
continue
}
fmt.Printf("❌ failed to parse 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
outcome.MarketName,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
s.logger.Error("Failed to fetch result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "market_id", outcome.MarketID, "market", outcome.MarketName, "error", err)
isDeleted = false
continue
}
// _, err = s.repo.CreateResult(ctx, domain.CreateResult{
// BetOutcomeID: outcome.ID,
// EventID: outcome.EventID,
// OddID: outcome.OddID,
// MarketID: outcome.MarketID,
// Status: result.Status,
// Score: result.Score,
// })
// if err != nil {
// s.logger.Error("Failed to store result", "bet_outcome_id", outcome.ID, "error", err)
// continue
// }
err = s.repo.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status)
outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status)
if err != nil {
isDeleted = false
s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
continue
}
if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING {
fmt.Printf("❌ Error while updating 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
outcome.MarketName,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID)
isDeleted = false
continue
}
fmt.Printf("✅ Successfully updated 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n",
outcome.MarketName,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID)
if err != nil {
if err != bet.ErrOutcomesNotCompleted {
s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err)
}
isDeleted = false
continue
}
fmt.Printf("🧾 Updating bet %v - event %v (%d/%d) to %v\n", outcome.BetID, event.ID, j+1, len(outcomes), status.String())
err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status)
if err != nil {
s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err)
isDeleted = false
continue
}
fmt.Printf("✅ Successfully updated 🎫 Bet %v - event %v(%v) (%d/%d) \n",
outcome.BetID,
event.HomeTeam+" "+event.AwayTeam, event.ID,
j+1, len(outcomes))
}
err = s.repo.DeleteEvent(ctx, event.ID)
if err != nil {
s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err)
return err
if isDeleted {
removed += 1
fmt.Printf("⚠️ Removing Event %v \n", event.ID)
err = s.repo.DeleteEvent(ctx, event.ID)
if err != nil {
s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err)
return err
}
}
}
fmt.Printf("🗑️ Removed Events: %d \n", removed)
return nil
}
@ -202,29 +261,51 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, spo
switch sportID {
case domain.FOOTBALL:
result, err = s.parseFootball(resultResp.Results[0], eventID, oddID, marketID, outcome)
if err != nil {
s.logger.Error("Failed to parse football", "event_id", eventID, "market_id", marketID, "error", err)
return domain.CreateResult{}, err
}
case domain.BASKETBALL:
result, err = s.parseBasketball(resultResp.Results[0], eventID, oddID, marketID, outcome)
if err != nil {
s.logger.Error("Failed to parse basketball", "event_id", eventID, "market_id", marketID, "error", err)
return domain.CreateResult{}, err
}
case domain.ICE_HOCKEY:
result, err = s.parseIceHockey(resultResp.Results[0], eventID, oddID, marketID, outcome)
if err != nil {
s.logger.Error("Failed to parse ice hockey", "event id", eventID, "market_id", marketID, "error", err)
return domain.CreateResult{}, err
}
case domain.AMERICAN_FOOTBALL:
result, err = s.parseNFL(resultResp.Results[0], eventID, oddID, marketID, outcome)
case domain.RUGBY_UNION:
result, err = s.parseRugbyUnion(resultResp.Results[0], eventID, oddID, marketID, outcome)
case domain.RUGBY_LEAGUE:
result, err = s.parseRugbyLeague(resultResp.Results[0], eventID, oddID, marketID, outcome)
case domain.BASEBALL:
result, err = s.parseBaseball(resultResp.Results[0], eventID, oddID, marketID, outcome)
default:
s.logger.Error("Unsupported sport", "sport", sportID)
return domain.CreateResult{}, fmt.Errorf("unsupported sport: %v", sportID)
}
return result, nil
}
func (s *Service) parseTimeStatus(timeStatusStr string) (bool, error) {
timeStatusParsed, err := strconv.ParseInt(strings.TrimSpace(timeStatusStr), 10, 64)
if err != nil {
s.logger.Error("Failed to parse time status", "time_status", timeStatusStr, "error", err)
return false, fmt.Errorf("failed to parse time status: %w", err)
}
timeStatus := domain.TimeStatus(timeStatusParsed)
switch timeStatus {
case domain.TIME_STATUS_NOT_STARTED, domain.TIME_STATUS_IN_PLAY, domain.TIME_STATUS_TO_BE_FIXED, domain.TIME_STATUS_ENDED:
return true, nil
case domain.TIME_STATUS_POSTPONED,
domain.TIME_STATUS_CANCELLED,
domain.TIME_STATUS_WALKOVER,
domain.TIME_STATUS_INTERRUPTED,
domain.TIME_STATUS_ABANDONED,
domain.TIME_STATUS_RETIRED,
domain.TIME_STATUS_SUSPENDED,
domain.TIME_STATUS_DECIDED_BY_FA,
domain.TIME_STATUS_REMOVED:
return false, nil
default:
s.logger.Error("Invalid time status", "time_status", timeStatus)
return false, fmt.Errorf("invalid time status: %d", timeStatus)
}
}
@ -235,18 +316,26 @@ func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marke
return domain.CreateResult{}, err
}
result := fbResp
if result.TimeStatus != "3" {
s.logger.Warn("Match not yet completed", "event_id", eventID)
return domain.CreateResult{}, fmt.Errorf("match not yet completed")
isEventActive, err := s.parseTimeStatus(result.TimeStatus)
if err != nil {
s.logger.Error("Failed to parse time status", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
if !isEventActive {
s.logger.Warn("Event is not active", "event_id", eventID)
return domain.CreateResult{}, ErrEventIsNotActive
}
finalScore := parseSS(result.SS)
firstHalfScore := parseSS(fmt.Sprintf("%s-%s", result.Scores.FirstHalf.Home, result.Scores.FirstHalf.Away))
firstHalfScore := parseScore(result.Scores.FirstHalf.Home, result.Scores.FirstHalf.Away)
secondHalfScore := parseScore(result.Scores.SecondHalf.Home, result.Scores.SecondHalf.Away)
corners := parseStats(result.Stats.Corners)
status, err := s.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, corners, result.Events)
halfTimeCorners := parseStats(result.Stats.HalfTimeCorners)
status, err := s.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, result.Events)
if err != nil {
s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err)
s.logger.Error("Failed to evaluate football outcome", "event_id", eventID, "market_id", marketID, "error", err)
return domain.CreateResult{}, err
}
@ -264,12 +353,17 @@ func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marke
func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) {
var basketBallRes domain.BasketballResultResponse
if err := json.Unmarshal(response, &basketBallRes); err != nil {
s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err)
s.logger.Error("Failed to unmarshal basketball result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
if basketBallRes.TimeStatus != "3" {
s.logger.Warn("Match not yet completed", "event_id", eventID)
return domain.CreateResult{}, fmt.Errorf("match not yet completed")
isEventActive, err := s.parseTimeStatus(basketBallRes.TimeStatus)
if err != nil {
s.logger.Error("Failed to parse time status", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
if !isEventActive {
s.logger.Warn("Event is not active", "event_id", eventID)
return domain.CreateResult{}, ErrEventIsNotActive
}
status, err := s.evaluateBasketballOutcome(outcome, basketBallRes)
@ -292,12 +386,17 @@ func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, mark
func (s *Service) parseIceHockey(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) {
var iceHockeyRes domain.IceHockeyResultResponse
if err := json.Unmarshal(response, &iceHockeyRes); err != nil {
s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err)
s.logger.Error("Failed to unmarshal ice hockey result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
if iceHockeyRes.TimeStatus != "3" {
s.logger.Warn("Match not yet completed", "event_id", eventID)
return domain.CreateResult{}, fmt.Errorf("match not yet completed")
isEventActive, err := s.parseTimeStatus(iceHockeyRes.TimeStatus)
if err != nil {
s.logger.Error("Failed to parse time status", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
if !isEventActive {
s.logger.Warn("Event is not active", "event_id", eventID)
return domain.CreateResult{}, ErrEventIsNotActive
}
status, err := s.evaluateIceHockeyOutcome(outcome, iceHockeyRes)
@ -317,6 +416,124 @@ func (s *Service) parseIceHockey(response json.RawMessage, eventID, oddID, marke
}
func (s *Service) parseNFL(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) {
var nflResp domain.NFLResultResponse
if err := json.Unmarshal(resultRes, &nflResp); err != nil {
s.logger.Error("Failed to unmarshal NFL result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
if nflResp.TimeStatus != "3" {
s.logger.Warn("Match not yet completed", "event_id", eventID)
return domain.CreateResult{}, fmt.Errorf("match not yet completed")
}
finalScore := parseSS(nflResp.SS)
var status domain.OutcomeStatus
var err error
switch outcome.MarketName {
case "Money Line":
status, err = evaluateNFLMoneyLine(outcome, finalScore)
case "Spread":
status, err = evaluateNFLSpread(outcome, finalScore)
case "Total Points":
status, err = evaluateNFLTotalPoints(outcome, finalScore)
default:
return domain.CreateResult{}, fmt.Errorf("unsupported market: %s", outcome.MarketName)
}
if err != nil {
s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err)
return domain.CreateResult{}, err
}
return domain.CreateResult{
BetOutcomeID: outcome.ID,
EventID: eventID,
OddID: oddID,
MarketID: marketID,
Status: status,
Score: nflResp.SS,
}, nil
}
func (s *Service) parseRugbyUnion(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) {
var rugbyResp domain.RugbyResultResponse
if err := json.Unmarshal(resultRes, &rugbyResp); err != nil {
s.logger.Error("Failed to unmarshal Rugby Union result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
if rugbyResp.TimeStatus != "3" {
s.logger.Warn("Match not yet completed", "event_id", eventID)
return domain.CreateResult{}, fmt.Errorf("match not yet completed")
}
status, err := evaluateRugbyOutcome(outcome, &rugbyResp)
if err != nil {
s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err)
return domain.CreateResult{}, err
}
return domain.CreateResult{
BetOutcomeID: outcome.ID,
EventID: eventID,
OddID: oddID,
MarketID: marketID,
Status: status,
Score: rugbyResp.SS,
}, nil
}
func (s *Service) parseRugbyLeague(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) {
var rugbyResp domain.RugbyResultResponse
if err := json.Unmarshal(resultRes, &rugbyResp); err != nil {
s.logger.Error("Failed to unmarshal Rugby League result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
if rugbyResp.TimeStatus != "3" {
s.logger.Warn("Match not yet completed", "event_id", eventID)
return domain.CreateResult{}, fmt.Errorf("match not yet completed")
}
status, err := evaluateRugbyOutcome(outcome, &rugbyResp)
if err != nil {
s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err)
return domain.CreateResult{}, err
}
return domain.CreateResult{
BetOutcomeID: outcome.ID,
EventID: eventID,
OddID: oddID,
MarketID: marketID,
Status: status,
Score: rugbyResp.SS,
}, nil
}
func (s *Service) parseBaseball(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) {
var baseballResp domain.BaseballResultResponse
if err := json.Unmarshal(resultRes, &baseballResp); err != nil {
s.logger.Error("Failed to unmarshal Baseball result", "event_id", eventID, "error", err)
return domain.CreateResult{}, err
}
if baseballResp.TimeStatus != "3" {
s.logger.Warn("Match not yet completed", "event_id", eventID)
return domain.CreateResult{}, fmt.Errorf("match not yet completed")
}
status, err := evaluateBaseballOutcome(outcome, &baseballResp)
if err != nil {
s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err)
return domain.CreateResult{}, err
}
return domain.CreateResult{
BetOutcomeID: outcome.ID,
EventID: eventID,
OddID: oddID,
MarketID: marketID,
Status: status,
Score: baseballResp.SS,
}, nil
}
func parseScore(home string, away string) struct{ Home, Away int } {
homeVal, _ := strconv.Atoi(strings.TrimSpace(home))
awaVal, _ := strconv.Atoi(strings.TrimSpace(away))
@ -343,7 +560,10 @@ func parseStats(stats []string) struct{ Home, Away int } {
}
// evaluateOutcome determines the outcome status based on market type and odd
func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore, firstHalfScore struct{ Home, Away int }, corners struct{ Home, Away int }, events []map[string]string) (domain.OutcomeStatus, error) {
func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore,
firstHalfScore struct{ Home, Away int }, secondHalfScore struct{ Home, Away int },
corners struct{ Home, Away int }, halfTimeCorners struct{ Home, Away int },
events []map[string]string) (domain.OutcomeStatus, error) {
if !domain.SupportedMarkets[outcome.MarketID] {
s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName)
@ -375,6 +595,21 @@ func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore,
return evaluateDoubleChance(outcome, finalScore)
case int64(domain.FOOTBALL_DRAW_NO_BET):
return evaluateDrawNoBet(outcome, finalScore)
case int64(domain.FOOTBALL_CORNERS):
return evaluateCorners(outcome, corners)
case int64(domain.FOOTBALL_CORNERS_TWO_WAY):
return evaluateCorners(outcome, corners)
case int64(domain.FOOTBALL_FIRST_HALF_CORNERS):
return evaluateCorners(outcome, halfTimeCorners)
case int64(domain.FOOTBALL_ASIAN_TOTAL_CORNERS):
return evaluateCorners(outcome, corners)
case int64(domain.FOOTBALL_FIRST_HALF_ASIAN_CORNERS):
return evaluateCorners(outcome, halfTimeCorners)
case int64(domain.FOOTBALL_FIRST_HALF_GOALS_ODD_EVEN):
return evaluateGoalsOddEven(outcome, firstHalfScore)
case int64(domain.FOOTBALL_SECOND_HALF_GOALS_ODD_EVEN):
return evaluateGoalsOddEven(outcome, secondHalfScore)
default:
s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName)
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("market type not implemented: %s", outcome.MarketName)
@ -411,7 +646,9 @@ func (s *Service) evaluateBasketballOutcome(outcome domain.BetOutcome, res domai
case int64(domain.BASKETBALL_GAME_TOTAL_ODD_EVEN):
return evaluateGoalsOddEven(outcome, finalScore)
case int64(domain.BASKETBALL_TEAM_TOTALS):
return evaluateGoalsOddEven(outcome, finalScore)
return evaluateTeamTotal(outcome, finalScore)
case int64(domain.BASKETBALL_TEAM_TOTAL_ODD_EVEN):
return evaluateTeamOddEven(outcome, finalScore)
case int64(domain.BASKETBALL_FIRST_HALF):
return evaluateGameLines(outcome, firstHalfScore)
@ -442,6 +679,11 @@ func (s *Service) evaluateBasketballOutcome(outcome domain.BetOutcome, res domai
return evaluateDoubleChance(outcome, firstQuarter)
case int64(domain.BASKETBALL_HIGHEST_SCORING_QUARTER):
return evaluateHighestScoringQuarter(outcome, firstQuarter, secondQuarter, thirdQuarter, fourthQuarter)
case int64(domain.BASKETBALL_FIRST_QUARTER_RESULT_AND_TOTAL):
return evaluateResultAndTotal(outcome, firstQuarter)
case int64(domain.BASKETBALL_TEAM_WITH_HIGHEST_SCORING_QUARTER):
return evaluateTeamWithHighestScoringQuarter(outcome, firstQuarter, secondQuarter, thirdQuarter, fourthQuarter)
default:
s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName)
@ -487,3 +729,22 @@ func (s *Service) evaluateIceHockeyOutcome(outcome domain.BetOutcome, res domain
return domain.OUTCOME_STATUS_PENDING, nil
}
func (s *Service) evaluateNFLOutcome(outcome domain.BetOutcome, finalScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
if !domain.SupportedMarkets[outcome.MarketID] {
s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName)
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName)
}
switch outcome.MarketID {
case int64(domain.AMERICAN_FOOTBALL_MONEY_LINE):
return evaluateNFLMoneyLine(outcome, finalScore)
case int64(domain.AMERICAN_FOOTBALL_SPREAD):
return evaluateNFLSpread(outcome, finalScore)
case int64(domain.AMERICAN_FOOTBALL_TOTAL_POINTS):
return evaluateNFLTotalPoints(outcome, finalScore)
default:
s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName)
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("market type not implemented: %s", outcome.MarketName)
}
}

View File

@ -1 +0,0 @@
package result

View File

@ -0,0 +1,280 @@
package result
import (
"fmt"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
// NFL evaluations
func evaluateNFLMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader {
case "1":
if score.Home > score.Away {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "2":
if score.Away > score.Home {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
}
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)
}
adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away)
if outcome.OddHeader == "1" {
adjustedHomeScore += handicap
} else if outcome.OddHeader == "2" {
adjustedAwayScore += handicap
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
if adjustedHomeScore > adjustedAwayScore {
if outcome.OddHeader == "1" {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if adjustedHomeScore < adjustedAwayScore {
if outcome.OddHeader == "2" {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
return domain.OUTCOME_STATUS_VOID, nil
}
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 {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
if outcome.OddHeader == "Over" {
if totalPoints > threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalPoints == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHeader == "Under" {
if totalPoints < threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalPoints == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
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) {
switch outcome.OddHeader {
case "1":
if score.Home > score.Away {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "2":
if score.Away > score.Home {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
}
// 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)
}
adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away)
if outcome.OddHeader == "1" {
adjustedHomeScore += handicap
} else if outcome.OddHeader == "2" {
adjustedAwayScore += handicap
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
if adjustedHomeScore > adjustedAwayScore {
if outcome.OddHeader == "1" {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if adjustedHomeScore < adjustedAwayScore {
if outcome.OddHeader == "2" {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
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) {
totalPoints := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
if outcome.OddHeader == "Over" {
if totalPoints > threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalPoints == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHeader == "Under" {
if totalPoints < threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalPoints == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
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) {
switch outcome.OddHeader {
case "1":
if score.Home > score.Away {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "2":
if score.Away > score.Home {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
}
// 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)
}
adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away)
if outcome.OddHeader == "1" {
adjustedHomeScore += handicap
} else if outcome.OddHeader == "2" {
adjustedAwayScore += handicap
} else {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
if adjustedHomeScore > adjustedAwayScore {
if outcome.OddHeader == "1" {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if adjustedHomeScore < adjustedAwayScore {
if outcome.OddHeader == "2" {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
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) {
totalRuns := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil {
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName)
}
if outcome.OddHeader == "Over" {
if totalRuns > threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalRuns == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
} else if outcome.OddHeader == "Under" {
if totalRuns < threshold {
return domain.OUTCOME_STATUS_WIN, nil
} else if totalRuns == threshold {
return domain.OUTCOME_STATUS_VOID, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
}
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) {
switch outcome.OddHeader {
case "1":
if score.Home > score.Away {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "2":
if score.Away > score.Home {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "X":
if score.Home == score.Away {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
}
// 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 {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "2":
if score.Away > score.Home {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
case "X":
if score.Home == score.Away {
return domain.OUTCOME_STATUS_WIN, nil
}
return domain.OUTCOME_STATUS_LOSS, nil
default:
return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader)
}
}

View File

@ -0,0 +1,303 @@
package result
import (
"testing"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/stretchr/testify/assert"
)
// TestNFLMarkets covers all American Football (NFL) market types defined in the domain.
// For each market (Money Line, Spread, Total Points), it tests home/away win, draw, void, and invalid input scenarios.
func TestNFLMarkets(t *testing.T) {
t.Log("Testing NFL (American Football) Markets")
markets := []struct {
marketID int64
name string
}{
{int64(domain.AMERICAN_FOOTBALL_MONEY_LINE), "MONEY_LINE"},
{int64(domain.AMERICAN_FOOTBALL_SPREAD), "SPREAD"},
{int64(domain.AMERICAN_FOOTBALL_TOTAL_POINTS), "TOTAL_POINTS"},
}
for _, m := range markets {
t.Run(m.name, func(t *testing.T) {
// Each subtest below covers a key scenario for the given NFL market.
switch m.marketID {
case int64(domain.AMERICAN_FOOTBALL_MONEY_LINE):
// Home win, away win, draw, and invalid OddHeader for Money Line
t.Run("Home Win", func(t *testing.T) {
status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 21, Away: 14})
t.Logf("Market: %s, Scenario: Home Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Away Win", func(t *testing.T) {
status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 14, Away: 21})
t.Logf("Market: %s, Scenario: Away Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Draw", func(t *testing.T) {
status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 17, Away: 17})
t.Logf("Market: %s, Scenario: Draw", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status)
})
t.Run("Invalid OddHeader", func(t *testing.T) {
status, err := evaluateNFLMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7})
t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
case int64(domain.AMERICAN_FOOTBALL_SPREAD):
t.Run("Home Win with Handicap", func(t *testing.T) {
status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-3.5"}, struct{ Home, Away int }{Home: 24, Away: 20})
t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Away Win with Handicap", func(t *testing.T) {
status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+3.5"}, struct{ Home, Away int }{Home: 20, Away: 24})
t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21})
t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
})
t.Run("Non-numeric Handicap", func(t *testing.T) {
status, err := evaluateNFLSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14})
t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
case int64(domain.AMERICAN_FOOTBALL_TOTAL_POINTS):
t.Run("Over Win", func(t *testing.T) {
status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "44.5"}, struct{ Home, Away int }{Home: 30, Away: 20})
t.Logf("Market: %s, Scenario: Over Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Under Win", func(t *testing.T) {
status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "44.5"}, struct{ Home, Away int }{Home: 20, Away: 17})
t.Logf("Market: %s, Scenario: Under Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "37"}, struct{ Home, Away int }{Home: 20, Away: 17})
t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
})
t.Run("Non-numeric OddName", func(t *testing.T) {
status, err := evaluateNFLTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 17})
t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
}
})
}
}
// TestRugbyMarkets covers all Rugby (Union & League) market types defined in the domain.
// For each market (Money Line, Spread, Handicap, Total Points), it tests home/away win, draw, void, and invalid input scenarios.
func TestRugbyMarkets(t *testing.T) {
t.Log("Testing Rugby Markets (Union & League)")
markets := []struct {
marketID int64
name string
}{
{int64(domain.RUGBY_MONEY_LINE), "MONEY_LINE"},
{int64(domain.RUGBY_SPREAD), "SPREAD"},
{int64(domain.RUGBY_TOTAL_POINTS), "TOTAL_POINTS"},
{int64(domain.RUGBY_HANDICAP), "HANDICAP"},
}
for _, m := range markets {
t.Run(m.name, func(t *testing.T) {
// Each subtest below covers a key scenario for the given Rugby market.
switch m.marketID {
case int64(domain.RUGBY_MONEY_LINE):
// Home win, away win, draw, and invalid OddHeader for Money Line
t.Run("Home Win", func(t *testing.T) {
status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 30, Away: 20})
t.Logf("Market: %s, Scenario: Home Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Away Win", func(t *testing.T) {
status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 20, Away: 30})
t.Logf("Market: %s, Scenario: Away Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Draw", func(t *testing.T) {
status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 25, Away: 25})
t.Logf("Market: %s, Scenario: Draw", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status)
})
t.Run("Invalid OddHeader", func(t *testing.T) {
status, err := evaluateRugbyMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7})
t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
case int64(domain.RUGBY_SPREAD), int64(domain.RUGBY_HANDICAP):
t.Run("Home Win with Handicap", func(t *testing.T) {
status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-6.5"}, struct{ Home, Away int }{Home: 28, Away: 20})
t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Away Win with Handicap", func(t *testing.T) {
status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+6.5"}, struct{ Home, Away int }{Home: 20, Away: 28})
t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 21, Away: 21})
t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
})
t.Run("Non-numeric Handicap", func(t *testing.T) {
status, err := evaluateRugbySpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 21, Away: 14})
t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
case int64(domain.RUGBY_TOTAL_POINTS):
t.Run("Over Win", func(t *testing.T) {
status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "40.5"}, struct{ Home, Away int }{Home: 25, Away: 20})
t.Logf("Market: %s, Scenario: Over Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Under Win", func(t *testing.T) {
status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "40.5"}, struct{ Home, Away int }{Home: 15, Away: 20})
t.Logf("Market: %s, Scenario: Under Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "35"}, struct{ Home, Away int }{Home: 20, Away: 15})
t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
})
t.Run("Non-numeric OddName", func(t *testing.T) {
status, err := evaluateRugbyTotalPoints(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 20, Away: 15})
t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
}
})
}
}
// TestBaseballMarkets covers all Baseball market types defined in the domain.
// For each market (Money Line, Spread, Total Runs), it tests home/away win, draw, void, and invalid input scenarios.
func TestBaseballMarkets(t *testing.T) {
t.Log("Testing Baseball Markets")
markets := []struct {
marketID int64
name string
}{
{int64(domain.BASEBALL_MONEY_LINE), "MONEY_LINE"},
{int64(domain.BASEBALL_SPREAD), "SPREAD"},
{int64(domain.BASEBALL_TOTAL_RUNS), "TOTAL_RUNS"},
}
for _, m := range markets {
t.Run(m.name, func(t *testing.T) {
// Each subtest below covers a key scenario for the given Baseball market.
switch m.marketID {
case int64(domain.BASEBALL_MONEY_LINE):
// Home win, away win, draw, and invalid OddHeader for Money Line
t.Run("Home Win", func(t *testing.T) {
status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 6, Away: 3})
t.Logf("Market: %s, Scenario: Home Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Away Win", func(t *testing.T) {
status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2"}, struct{ Home, Away int }{Home: 2, Away: 5})
t.Logf("Market: %s, Scenario: Away Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Draw", func(t *testing.T) {
status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1"}, struct{ Home, Away int }{Home: 4, Away: 4})
t.Logf("Market: %s, Scenario: Draw", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_LOSS, status)
})
t.Run("Invalid OddHeader", func(t *testing.T) {
status, err := evaluateBaseballMoneyLine(domain.BetOutcome{MarketID: m.marketID, OddHeader: "X"}, struct{ Home, Away int }{Home: 10, Away: 7})
t.Logf("Market: %s, Scenario: Invalid OddHeader", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
case int64(domain.BASEBALL_SPREAD):
t.Run("Home Win with Handicap", func(t *testing.T) {
status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "-1.5"}, struct{ Home, Away int }{Home: 5, Away: 3})
t.Logf("Market: %s, Scenario: Home Win with Handicap", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Away Win with Handicap", func(t *testing.T) {
status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "2", OddHandicap: "+1.5"}, struct{ Home, Away int }{Home: 3, Away: 5})
t.Logf("Market: %s, Scenario: Away Win with Handicap", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "0"}, struct{ Home, Away int }{Home: 4, Away: 4})
t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
})
t.Run("Non-numeric Handicap", func(t *testing.T) {
status, err := evaluateBaseballSpread(domain.BetOutcome{MarketID: m.marketID, OddHeader: "1", OddHandicap: "notanumber"}, struct{ Home, Away int }{Home: 5, Away: 3})
t.Logf("Market: %s, Scenario: Non-numeric Handicap", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
case int64(domain.BASEBALL_TOTAL_RUNS):
t.Run("Over Win", func(t *testing.T) {
status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7.5"}, struct{ Home, Away int }{Home: 5, Away: 4})
t.Logf("Market: %s, Scenario: Over Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Under Win", func(t *testing.T) {
status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Under", OddName: "7.5"}, struct{ Home, Away int }{Home: 2, Away: 3})
t.Logf("Market: %s, Scenario: Under Win", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_WIN, status)
})
t.Run("Push (Void)", func(t *testing.T) {
status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "7"}, struct{ Home, Away int }{Home: 4, Away: 3})
t.Logf("Market: %s, Scenario: Push (Void)", m.name)
assert.NoError(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_VOID, status)
})
t.Run("Non-numeric OddName", func(t *testing.T) {
status, err := evaluateBaseballTotalRuns(domain.BetOutcome{MarketID: m.marketID, OddHeader: "Over", OddName: "notanumber"}, struct{ Home, Away int }{Home: 4, Away: 3})
t.Logf("Market: %s, Scenario: Non-numeric OddName", m.name)
assert.Error(t, err)
assert.Equal(t, domain.OUTCOME_STATUS_PENDING, status)
})
}
})
}
}

View File

@ -0,0 +1,189 @@
package services
import (
"encoding/json"
"fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
// ResultCheckerService handles the checking of game results
type ResultCheckerService struct {
// Add any dependencies here (e.g., repositories, external APIs)
}
// NewResultCheckerService creates a new instance of ResultCheckerService
func NewResultCheckerService() *ResultCheckerService {
return &ResultCheckerService{}
}
// CheckNFLResult checks the result of an NFL game
func (s *ResultCheckerService) CheckNFLResult(data json.RawMessage) (*domain.Result, error) {
nflResult, err := domain.ParseNFLResult(data)
if err != nil {
return nil, fmt.Errorf("failed to parse NFL result: %w", err)
}
winner, err := domain.GetNFLWinner(nflResult)
if err != nil {
return nil, fmt.Errorf("failed to determine NFL winner: %w", err)
}
score := domain.FormatNFLScore(nflResult)
return &domain.Result{
Status: determineOutcomeStatus(winner, nflResult.Home.Name, nflResult.Away.Name),
Score: score,
FullTimeScore: score,
SS: nflResult.SS,
Scores: map[string]domain.Score{
"1": nflResult.Scores.FirstQuarter,
"2": nflResult.Scores.SecondQuarter,
"3": nflResult.Scores.ThirdQuarter,
"4": nflResult.Scores.FourthQuarter,
"5": nflResult.Scores.Overtime,
"7": nflResult.Scores.TotalScore,
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}, nil
}
// determineOutcomeStatus determines the outcome status based on the winner and teams
func determineOutcomeStatus(winner, homeTeam, awayTeam string) domain.OutcomeStatus {
if winner == "Draw" {
return domain.OUTCOME_STATUS_VOID
}
if winner == homeTeam {
return domain.OUTCOME_STATUS_WIN
}
if winner == awayTeam {
return domain.OUTCOME_STATUS_LOSS
}
return domain.OUTCOME_STATUS_PENDING
}
// CheckRugbyResult checks the result of a Rugby game
func (s *ResultCheckerService) CheckRugbyResult(data json.RawMessage) (*domain.Result, error) {
rugbyResult, err := domain.ParseRugbyResult(data)
if err != nil {
return nil, fmt.Errorf("failed to parse Rugby result: %w", err)
}
winner, err := domain.GetRugbyWinner(rugbyResult)
if err != nil {
return nil, fmt.Errorf("failed to determine Rugby winner: %w", err)
}
score := domain.FormatRugbyScore(rugbyResult)
return &domain.Result{
Status: determineOutcomeStatus(winner, rugbyResult.Home.Name, rugbyResult.Away.Name),
Score: score,
FullTimeScore: score,
SS: rugbyResult.SS,
Scores: map[string]domain.Score{
"1": rugbyResult.Scores.FirstHalf,
"2": rugbyResult.Scores.SecondHalf,
"7": rugbyResult.Scores.TotalScore,
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}, nil
}
// CheckBaseballResult checks the result of a Baseball game
func (s *ResultCheckerService) CheckBaseballResult(data json.RawMessage) (*domain.Result, error) {
baseballResult, err := domain.ParseBaseballResult(data)
if err != nil {
return nil, fmt.Errorf("failed to parse Baseball result: %w", err)
}
winner, err := domain.GetBaseballWinner(baseballResult)
if err != nil {
return nil, fmt.Errorf("failed to determine Baseball winner: %w", err)
}
score := domain.FormatBaseballScore(baseballResult)
return &domain.Result{
Status: determineOutcomeStatus(winner, baseballResult.Home.Name, baseballResult.Away.Name),
Score: score,
FullTimeScore: score,
SS: baseballResult.SS,
Scores: map[string]domain.Score{
"1": baseballResult.Scores.FirstInning,
"2": baseballResult.Scores.SecondInning,
"3": baseballResult.Scores.ThirdInning,
"4": baseballResult.Scores.FourthInning,
"5": baseballResult.Scores.FifthInning,
"6": baseballResult.Scores.SixthInning,
"7": baseballResult.Scores.SeventhInning,
"8": baseballResult.Scores.EighthInning,
"9": baseballResult.Scores.NinthInning,
"10": baseballResult.Scores.ExtraInnings,
"T": baseballResult.Scores.TotalScore,
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}, nil
}
// CheckRugbyUnionResult checks the result of a Rugby Union game
func (s *ResultCheckerService) CheckRugbyUnionResult(data json.RawMessage) (*domain.Result, error) {
rugbyResult, err := domain.ParseRugbyUnionResult(data)
if err != nil {
return nil, fmt.Errorf("failed to parse Rugby Union result: %w", err)
}
winner, err := domain.GetRugbyWinner(rugbyResult)
if err != nil {
return nil, fmt.Errorf("failed to determine Rugby Union winner: %w", err)
}
score := domain.FormatRugbyScore(rugbyResult)
return &domain.Result{
Status: determineOutcomeStatus(winner, rugbyResult.Home.Name, rugbyResult.Away.Name),
Score: score,
FullTimeScore: score,
SS: rugbyResult.SS,
Scores: map[string]domain.Score{
"1": rugbyResult.Scores.FirstHalf,
"2": rugbyResult.Scores.SecondHalf,
"7": rugbyResult.Scores.TotalScore,
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}, nil
}
// CheckRugbyLeagueResult checks the result of a Rugby League game
func (s *ResultCheckerService) CheckRugbyLeagueResult(data json.RawMessage) (*domain.Result, error) {
rugbyResult, err := domain.ParseRugbyLeagueResult(data)
if err != nil {
return nil, fmt.Errorf("failed to parse Rugby League result: %w", err)
}
winner, err := domain.GetRugbyWinner(rugbyResult)
if err != nil {
return nil, fmt.Errorf("failed to determine Rugby League winner: %w", err)
}
score := domain.FormatRugbyScore(rugbyResult)
return &domain.Result{
Status: determineOutcomeStatus(winner, rugbyResult.Home.Name, rugbyResult.Away.Name),
Score: score,
FullTimeScore: score,
SS: rugbyResult.SS,
Scores: map[string]domain.Score{
"1": rugbyResult.Scores.FirstHalf,
"2": rugbyResult.Scores.SecondHalf,
"7": rugbyResult.Scores.TotalScore,
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}, nil
}

View File

@ -37,3 +37,7 @@ func (s *Service) UpdateTicketOutcomeStatus(ctx context.Context, id int64, statu
func (s *Service) DeleteTicket(ctx context.Context, id int64) error {
return s.ticketStore.DeleteTicket(ctx, id)
}
func (s *Service) DeleteOldTickets(ctx context.Context) error {
return s.ticketStore.DeleteOldTickets(ctx)
}

View File

@ -46,8 +46,8 @@ func (s *Service) DeleteUser(ctx context.Context, id int64) error {
type Filter struct {
Role string
CompanyID domain.ValidInt64
Page int
PageSize int
Page domain.ValidInt
PageSize domain.ValidInt
}
type ValidRole struct {
Value domain.Role
@ -71,6 +71,10 @@ func (s *Service) GetCashiersByBranch(ctx context.Context, branchID int64) ([]do
return s.userStore.GetCashiersByBranch(ctx, branchID)
}
func (s *Service) GetAllCashiers(ctx context.Context) ([]domain.User, error) {
func (s *Service) GetAllCashiers(ctx context.Context) ([]domain.GetCashier, error) {
return s.userStore.GetAllCashiers(ctx)
}
func (s *Service) GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, error) {
return s.userStore.GetCashierByID(ctx, cashierID)
}

View File

@ -11,15 +11,17 @@ type UserStore interface {
CreateUserWithoutOtp(ctx context.Context, user domain.User, is_company bool) (domain.User, error)
GetUserByID(ctx context.Context, id int64) (domain.User, error)
GetAllUsers(ctx context.Context, filter Filter) ([]domain.User, int64, error)
GetAllCashiers(ctx context.Context) ([]domain.User, error)
GetAllCashiers(ctx context.Context) ([]domain.GetCashier, error)
GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, error)
GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error)
UpdateUser(ctx context.Context, user domain.UpdateUserReq) error
UpdateUserCompany(ctx context.Context, id int64, companyID int64) error
UpdateUserSuspend(ctx context.Context, id int64, status bool) error
DeleteUser(ctx context.Context, id int64) error
CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error)
GetUserByEmail(ctx context.Context, email string) (domain.User, error)
GetUserByPhone(ctx context.Context, phoneNum string) (domain.User, error)
SearchUserByNameOrPhone(ctx context.Context, searchString string) ([]domain.User, error)
SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error)
UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error // identifier verified email or phone
}
type SmsGateway interface {

View File

@ -13,7 +13,6 @@ type Service struct {
otpStore OtpStore
smsGateway SmsGateway
emailGateway EmailGateway
}
func NewService(

View File

@ -6,9 +6,9 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func (s *Service) SearchUserByNameOrPhone(ctx context.Context, searchString string) ([]domain.User, error) {
func (s *Service) SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error) {
// Search user
return s.userStore.SearchUserByNameOrPhone(ctx, searchString)
return s.userStore.SearchUserByNameOrPhone(ctx, searchString, role, companyID)
}
func (s *Service) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error {
@ -20,7 +20,11 @@ func (s *Service) UpdateUser(ctx context.Context, user domain.UpdateUserReq) err
func (s *Service) UpdateUserCompany(ctx context.Context, id int64, companyID int64) error {
// update user
return s.userStore.UpdateUserCompany(ctx, id, companyID)
}
func (s *Service) UpdateUserSuspend(ctx context.Context, id int64, status bool) error {
// update user
return s.userStore.UpdateUserSuspend(ctx, id, status)
}
func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
return s.userStore.GetUserByID(ctx, id)

View File

@ -12,7 +12,7 @@ type WalletStore interface {
GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error)
GetAllWallets(ctx context.Context) ([]domain.Wallet, error)
GetWalletsByUser(ctx context.Context, id int64) ([]domain.Wallet, error)
GetCustomerWallet(ctx context.Context, customerID int64, companyID int64) (domain.GetCustomerWallet, error)
GetCustomerWallet(ctx context.Context, customerID int64) (domain.GetCustomerWallet, error)
GetAllBranchWallets(ctx context.Context) ([]domain.BranchWallet, error)
UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error
UpdateWalletActive(ctx context.Context, id int64, isActive bool) error

View File

@ -15,7 +15,7 @@ func (s *Service) CreateWallet(ctx context.Context, wallet domain.CreateWallet)
return s.walletStore.CreateWallet(ctx, wallet)
}
func (s *Service) CreateCustomerWallet(ctx context.Context, customerID int64, companyID int64) (domain.CustomerWallet, error) {
func (s *Service) CreateCustomerWallet(ctx context.Context, customerID int64) (domain.CustomerWallet, error) {
regularWallet, err := s.CreateWallet(ctx, domain.CreateWallet{
IsWithdraw: true,
@ -39,7 +39,6 @@ func (s *Service) CreateCustomerWallet(ctx context.Context, customerID int64, co
return s.walletStore.CreateCustomerWallet(ctx, domain.CreateCustomerWallet{
CustomerID: customerID,
CompanyID: companyID,
RegularWalletID: regularWallet.ID,
StaticWalletID: staticWallet.ID,
})
@ -57,8 +56,8 @@ func (s *Service) GetWalletsByUser(ctx context.Context, id int64) ([]domain.Wall
return s.walletStore.GetWalletsByUser(ctx, id)
}
func (s *Service) GetCustomerWallet(ctx context.Context, customerID int64, companyID int64) (domain.GetCustomerWallet, error) {
return s.walletStore.GetCustomerWallet(ctx, customerID, companyID)
func (s *Service) GetCustomerWallet(ctx context.Context, customerID int64) (domain.GetCustomerWallet, error) {
return s.walletStore.GetCustomerWallet(ctx, customerID)
}
func (s *Service) GetAllBranchWallets(ctx context.Context) ([]domain.BranchWallet, error) {
@ -91,8 +90,6 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
return s.walletStore.UpdateBalance(ctx, id, wallet.Balance+amount)
}
func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive bool) error {
return s.walletStore.UpdateWalletActive(ctx, id, isActive)
}

View File

@ -31,16 +31,17 @@ import (
type App struct {
fiber *fiber.App
aleaVirtualGameService alea.AleaVirtualGameService
veliVirtualGameService veli.VeliVirtualGameService
cfg *config.Config
logger *slog.Logger
NotidicationStore notificationservice.NotificationStore
NotidicationStore *notificationservice.Service
referralSvc referralservice.ReferralStore
port int
authSvc *authentication.Service
userSvc *user.Service
betSvc *bet.Service
virtualGameSvc virtualgameservice.VirtualGameService
aleaVirtualGameService alea.AleaVirtualGameService
veliVirtualGameService veli.VeliVirtualGameService
walletSvc *wallet.Service
transactionSvc *transaction.Service
ticketSvc *ticket.Service
@ -52,7 +53,6 @@ type App struct {
prematchSvc *odds.ServiceImpl
eventSvc event.Service
resultSvc *result.Service
cfg *config.Config
}
func NewApp(
@ -67,7 +67,7 @@ func NewApp(
transactionSvc *transaction.Service,
branchSvc *branch.Service,
companySvc *company.Service,
notidicationStore notificationservice.NotificationStore,
notidicationStore *notificationservice.Service,
prematchSvc *odds.ServiceImpl,
eventSvc event.Service,
referralSvc referralservice.ReferralStore,
@ -85,9 +85,9 @@ func NewApp(
})
app.Use(cors.New(cors.Config{
AllowOrigins: "*", // Specify your frontend's origin
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", // Specify the allowed HTTP methods
AllowHeaders: "Content-Type,Authorization,platform", // Specify the allowed headers
AllowOrigins: "*",
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
AllowHeaders: "Content-Type,Authorization,platform",
// AllowCredentials: true,
}))

View File

@ -1,8 +1,8 @@
package httpserver
import (
// "context"
"context"
"log"
// "time"
@ -10,6 +10,7 @@ import (
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/robfig/cron/v3"
)
@ -20,53 +21,24 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
spec string
task func()
}{
{
spec: "0 0 * * * *", // Every 1 hour
task: func() {
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
log.Printf("FetchUpcomingEvents error: %v", err)
}
},
},
// {
// spec: "*/5 * * * * *", // Every 5 seconds
// task: func() {
// if err := eventService.FetchLiveEvents(context.Background()); err != nil {
// log.Printf("FetchLiveEvents error: %v", err)
// }
// },
// },
{
spec: "0 */15 * * * *", // Every 15 minutes
task: func() {
if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
log.Printf("FetchNonLiveOdds error: %v", err)
}
},
},
// {
// spec: "0 */15 * * * *",
// spec: "0 0 * * * *", // Every 1 hour
// task: func() {
// log.Println("Fetching results for upcoming events...")
// upcomingEvents, err := eventService.GetAllUpcomingEvents(context.Background())
// if err != nil {
// log.Printf("Failed to fetch upcoming events: %v", err)
// return
// if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
// log.Printf("FetchUpcomingEvents error: %v", err)
// }
// for _, event := range upcomingEvents {
// if err := resultService.FetchAndStoreResult(context.Background(), event.ID); err != nil {
// log.Printf(" Failed to fetch/store result for event %s: %v", event.ID, err)
// } else {
// log.Printf(" Successfully stored result for event %s", event.ID)
// }
// },
// },
// {
// spec: "0 */15 * * * *", // Every 15 minutes
// task: func() {
// if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
// log.Printf("FetchNonLiveOdds error: %v", err)
// }
// },
// },
{
spec: "0 */15 * * * *",
spec: "0 */15 * * * *", // Every 15 Minutes
task: func() {
log.Println("Fetching results for upcoming events...")
@ -80,6 +52,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
}
for _, job := range schedule {
job.task()
if _, err := c.AddFunc(job.spec, job.task); err != nil {
log.Fatalf("Failed to schedule cron job: %v", err)
}
@ -88,3 +61,33 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
c.Start()
log.Println("Cron jobs started for event and odds services")
}
func StartTicketCrons(ticketService ticket.Service) {
c := cron.New(cron.WithSeconds())
schedule := []struct {
spec string
task func()
}{
{
spec: "0 0 * * * *", // Every hour
task: func() {
log.Println("Deleting old tickets...")
if err := ticketService.DeleteOldTickets(context.Background()); err != nil {
log.Printf("Failed to remove old ticket: %v", err)
} else {
log.Printf("Successfully deleted old tickets")
}
},
},
}
for _, job := range schedule {
if _, err := c.AddFunc(job.spec, job.task); err != nil {
log.Fatalf("Failed to schedule cron job: %v", err)
}
}
c.Start()
log.Println("Cron jobs started for ticket service")
}

View File

@ -2,6 +2,7 @@ package handlers
import (
"log/slog"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -124,14 +125,21 @@ type AdminRes struct {
// @Failure 500 {object} response.APIResponse
// @Router /admin [get]
func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
filter := user.Filter{
Role: string(domain.RoleAdmin),
CompanyID: domain.ValidInt64{
Value: int64(c.QueryInt("company_id")),
Valid: false,
},
Page: domain.ValidInt{
Value: c.QueryInt("page", 1) - 1,
Valid: true,
},
PageSize: domain.ValidInt{
Value: c.QueryInt("page_size", 10),
Valid: true,
},
Page: c.QueryInt("page", 1) - 1,
PageSize: c.QueryInt("page_size", 10),
}
valErrs, ok := h.validator.Validate(c, filter)
if !ok {
@ -171,6 +179,155 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
}
}
return response.WritePaginatedJSON(c, fiber.StatusOK, "Admins retrieved successfully", result, nil, filter.Page, int(total))
return response.WritePaginatedJSON(c, fiber.StatusOK, "Admins retrieved successfully", result, nil, filter.Page.Value, int(total))
}
// GetAdminByID godoc
// @Summary Get admin by id
// @Description Get a single admin by id
// @Tags admin
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} AdminRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/{id} [get]
func (h *Handler) GetAdminByID(c *fiber.Ctx) error {
// branchId := int64(12) //c.Locals("branch_id").(int64)
// filter := user.Filter{
// Role: string(domain.RoleUser),
// BranchId: user.ValidBranchId{
// Value: branchId,
// Valid: true,
// },
// Page: c.QueryInt("page", 1),
// PageSize: c.QueryInt("page_size", 10),
// }
// valErrs, ok := validator.Validate(c, filter)
// if !ok {
// return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
// }
userIDstr := c.Params("id")
userID, err := strconv.ParseInt(userIDstr, 10, 64)
if err != nil {
h.logger.Error("failed to fetch user using UserID", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid admin ID", nil, nil)
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.logger.Error("Get User By ID failed", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get admin", nil, nil)
}
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil {
if err != authentication.ErrRefreshTokenNotFound {
h.logger.Error("Failed to get user last login", "userID", user.ID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login")
}
lastLogin = &user.CreatedAt
}
res := AdminRes{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
SuspendedAt: user.SuspendedAt,
Suspended: user.Suspended,
LastLogin: *lastLogin,
}
return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil)
}
type updateAdminReq struct {
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"`
Suspended bool `json:"suspended" example:"false"`
CompanyID *int64 `json:"company_id,omitempty" example:"1"`
}
// UpdateAdmin godoc
// @Summary Update Admin
// @Description Update Admin
// @Tags admin
// @Accept json
// @Produce json
// @Param admin body updateAdminReq true "Update Admin"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/{id} [put]
func (h *Handler) UpdateAdmin(c *fiber.Ctx) error {
var req updateAdminReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("UpdateAdmin failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)
}
valErrs, ok := h.validator.Validate(c, req)
if !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
AdminIDStr := c.Params("id")
AdminID, err := strconv.ParseInt(AdminIDStr, 10, 64)
if err != nil {
h.logger.Error("UpdateAdmin failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid Admin ID", nil, nil)
}
var companyID domain.ValidInt64
if req.CompanyID != nil {
companyID = domain.ValidInt64{
Value: *req.CompanyID,
Valid: true,
}
}
err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{
UserId: AdminID,
FirstName: domain.ValidString{
Value: req.FirstName,
Valid: req.FirstName != "",
},
LastName: domain.ValidString{
Value: req.LastName,
Valid: req.LastName != "",
},
Suspended: domain.ValidBool{
Value: req.Suspended,
Valid: true,
},
CompanyID: companyID,
},
)
if err != nil {
h.logger.Error("UpdateAdmin failed", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update admin", nil, nil)
}
if req.CompanyID != nil {
_, err := h.companySvc.UpdateCompany(c.Context(), domain.UpdateCompany{
ID: *req.CompanyID,
AdminID: &AdminID,
})
if err != nil {
h.logger.Error("CreateAdmin failed to update company", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update company", nil, nil)
}
}
return response.WriteJSON(c, fiber.StatusOK, "Managers updated successfully", nil, nil)
}

View File

@ -1,98 +1,24 @@
package handlers
import (
"encoding/json"
"log/slog"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2"
)
type CreateBetOutcomeReq struct {
EventID int64 `json:"event_id" example:"1"`
OddID int64 `json:"odd_id" example:"1"`
MarketID int64 `json:"market_id" example:"1"`
}
type CreateBetReq struct {
Outcomes []CreateBetOutcomeReq `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
Status domain.OutcomeStatus `json:"status" example:"1"`
FullName string `json:"full_name" example:"John"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
BranchID *int64 `json:"branch_id,omitempty" example:"1"`
}
type CreateBetRes struct {
ID int64 `json:"id" example:"1"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
Status domain.OutcomeStatus `json:"status" example:"1"`
FullName string `json:"full_name" example:"John"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
BranchID int64 `json:"branch_id" example:"2"`
UserID int64 `json:"user_id" example:"2"`
IsShopBet bool `json:"is_shop_bet" example:"false"`
CreatedNumber int64 `json:"created_number" example:"2"`
CashedID string `json:"cashed_id" example:"21234"`
}
type BetRes struct {
ID int64 `json:"id" example:"1"`
Outcomes []domain.BetOutcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
Status domain.OutcomeStatus `json:"status" example:"1"`
FullName string `json:"full_name" example:"John"`
PhoneNumber string `json:"phone_number" example:"1234567890"`
BranchID int64 `json:"branch_id" example:"2"`
UserID int64 `json:"user_id" example:"2"`
IsShopBet bool `json:"is_shop_bet" example:"false"`
CashedOut bool `json:"cashed_out" example:"false"`
CashedID string `json:"cashed_id" example:"21234"`
}
func convertCreateBet(bet domain.Bet, createdNumber int64) CreateBetRes {
return CreateBetRes{
ID: bet.ID,
Amount: bet.Amount.Float32(),
TotalOdds: bet.TotalOdds,
Status: bet.Status,
FullName: bet.FullName,
PhoneNumber: bet.PhoneNumber,
BranchID: bet.BranchID.Value,
UserID: bet.UserID.Value,
CreatedNumber: createdNumber,
CashedID: bet.CashoutID,
}
}
func convertBet(bet domain.GetBet) BetRes {
return BetRes{
ID: bet.ID,
Amount: bet.Amount.Float32(),
TotalOdds: bet.TotalOdds,
Status: bet.Status,
FullName: bet.FullName,
PhoneNumber: bet.PhoneNumber,
BranchID: bet.BranchID.Value,
UserID: bet.UserID.Value,
Outcomes: bet.Outcomes,
IsShopBet: bet.IsShopBet,
CashedOut: bet.CashedOut,
CashedID: bet.CashoutID,
}
}
// CreateBet godoc
// @Summary Create a bet
// @Description Creates a bet
// @Tags bet
// @Accept json
// @Produce json
// @Param createBet body CreateBetReq true "Creates bet"
// @Success 200 {object} BetRes
// @Param createBet body domain.CreateBetReq true "Creates bet"
// @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /bet [post]
@ -102,7 +28,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role)
var req CreateBetReq
var req domain.CreateBetReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse CreateBet request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
@ -113,199 +39,102 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
// TODO Validate Outcomes Here and make sure they didn't expire
// Validation for creating tickets
if len(req.Outcomes) > 30 {
return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil)
res, err := h.betSvc.PlaceBet(c.Context(), req, userID, role)
if err != nil {
h.logger.Error("PlaceBet failed", "error", err)
switch err {
case bet.ErrEventHasBeenRemoved, bet.ErrEventHasNotEnded, bet.ErrRawOddInvalid, wallet.ErrBalanceInsufficient:
return fiber.NewError(fiber.StatusBadGateway, err.Error())
}
return fiber.NewError(fiber.StatusInternalServerError, "Unable to create bet")
}
var outcomes []domain.CreateBetOutcome = make([]domain.CreateBetOutcome, 0, len(req.Outcomes))
var totalOdds float32 = 1
for _, outcome := range req.Outcomes {
eventIDStr := strconv.FormatInt(outcome.EventID, 10)
marketIDStr := strconv.FormatInt(outcome.MarketID, 10)
oddIDStr := strconv.FormatInt(outcome.OddID, 10)
event, err := h.eventSvc.GetUpcomingEventByID(c.Context(), eventIDStr)
return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil)
}
// RandomBet godoc
// @Summary Generate a random bet
// @Description Generate a random bet
// @Tags bet
// @Accept json
// @Produce json
// @Param createBet body domain.RandomBetReq true "Create Random bet"
// @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /random/bet [post]
func (h *Handler) RandomBet(c *fiber.Ctx) error {
// Get user_id from middleware
userID := c.Locals("user_id").(int64)
// role := c.Locals("role").(domain.Role)
leagueIDQuery := c.Query("league_id")
sportIDQuery := c.Query("sport_id")
firstStartTimeQuery := c.Query("first_start_time")
lastStartTimeQuery := c.Query("last_start_time")
leagueID := domain.ValidString{
Value: leagueIDQuery,
Valid: leagueIDQuery != "",
}
sportID := domain.ValidString{
Value: sportIDQuery,
Valid: sportIDQuery != "",
}
var firstStartTime domain.ValidTime
if firstStartTimeQuery != "" {
firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery)
if err != nil {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid event id", err, nil)
h.logger.Error("invalid start_time format", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil)
}
firstStartTime = domain.ValidTime{
Value: firstStartTimeParsed,
Valid: true,
}
}
var lastStartTime domain.ValidTime
if lastStartTimeQuery != "" {
lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery)
if err != nil {
h.logger.Error("invalid start_time format", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil)
}
lastStartTime = domain.ValidTime{
Value: lastStartTimeParsed,
Valid: true,
}
}
// Checking to make sure the event hasn't already started
// currentTime := time.Now()
// if event.StartTime.Before(currentTime) {
// return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil)
// }
var req domain.RandomBetReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse RandomBet request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
odds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr)
valErrs, ok := h.validator.Validate(c, req)
if !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
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)
if err != nil {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil)
}
type rawOddType struct {
ID string
Name string
Odds string
Header string
Handicap string
}
var selectedOdd rawOddType
var isOddFound bool = false
for _, raw := range odds.RawOdds {
var rawOdd rawOddType
rawBytes, err := json.Marshal(raw)
err = json.Unmarshal(rawBytes, &rawOdd)
if err != nil {
h.logger.Error("Failed to unmarshal raw odd", "error", err)
continue
}
if rawOdd.ID == oddIDStr {
selectedOdd = rawOdd
isOddFound = true
h.logger.Error("Random Bet failed", "error", err)
switch err {
case bet.ErrNoEventsAvailable:
return fiber.NewError(fiber.StatusBadRequest, "No events found")
}
return fiber.NewError(fiber.StatusInternalServerError, "Unable to create random bet")
}
if !isOddFound {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil)
}
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
totalOdds = totalOdds * float32(parsedOdd)
sportID, err := strconv.ParseInt(event.SportID, 10, 64)
if err != nil {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid sport id", nil, nil)
}
h.logger.Info("Create Bet", slog.Int64("sportId", sportID))
outcomes = append(outcomes, domain.CreateBetOutcome{
EventID: outcome.EventID,
OddID: outcome.OddID,
MarketID: outcome.MarketID,
SportID: sportID,
HomeTeamName: event.HomeTeam,
AwayTeamName: event.AwayTeam,
MarketName: odds.MarketName,
Odd: float32(parsedOdd),
OddName: selectedOdd.Name,
OddHeader: selectedOdd.Header,
OddHandicap: selectedOdd.Handicap,
Expires: event.StartTime,
})
}
// Validating user by role
// Differentiating between offline and online bets
cashoutID, err := h.betSvc.GenerateCashoutID()
if err != nil {
h.logger.Error("CreateBetReq failed, unable to create cashout id")
return response.WriteJSON(c, fiber.StatusInternalServerError, "Invalid request", err, nil)
}
var bet domain.Bet
if role == domain.RoleCashier {
// Get the branch from the branch ID
branch, err := h.branchSvc.GetBranchByCashier(c.Context(), userID)
if err != nil {
h.logger.Error("CreateBetReq failed, branch id invalid")
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil)
}
// Deduct a percentage of the amount
// TODO move to service layer. Make it fetch dynamically from company
var deductedAmount = req.Amount / 10
err = h.walletSvc.DeductFromWallet(c.Context(), branch.WalletID, domain.ToCurrency(deductedAmount))
if err != nil {
h.logger.Error("CreateBetReq failed, unable to deduct from WalletID")
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil)
}
bet, err = h.betSvc.CreateBet(c.Context(), domain.CreateBet{
Amount: domain.ToCurrency(req.Amount),
TotalOdds: totalOdds,
Status: req.Status,
FullName: req.FullName,
PhoneNumber: req.PhoneNumber,
BranchID: domain.ValidInt64{
Value: branch.ID,
Valid: true,
},
UserID: domain.ValidInt64{
Value: userID,
Valid: false,
},
IsShopBet: true,
CashoutID: cashoutID,
})
} else if role == domain.RoleSuperAdmin || role == domain.RoleAdmin || role == domain.RoleBranchManager {
// If a non cashier wants to create a bet, they will need to provide the Branch ID
// TODO: restrict the Branch ID of Admin and Branch Manager to only the branches within their own company
if req.BranchID == nil {
h.logger.Error("CreateBetReq failed, Branch ID is required for this type of user")
return response.WriteJSON(c, fiber.StatusBadRequest, "Branch ID is required for this type of user", nil, nil)
}
// h.logger.Info("Branch ID", slog.Int64("branch_id", *req.BranchID))
bet, err = h.betSvc.CreateBet(c.Context(), domain.CreateBet{
Amount: domain.ToCurrency(req.Amount),
TotalOdds: totalOdds,
Status: req.Status,
FullName: req.FullName,
PhoneNumber: req.PhoneNumber,
BranchID: domain.ValidInt64{
Value: *req.BranchID,
Valid: true,
},
UserID: domain.ValidInt64{
Value: userID,
Valid: true,
},
IsShopBet: true,
CashoutID: cashoutID,
})
} else {
// TODO if user is customer, get id from the token then get the wallet id from there and reduce the amount
bet, err = h.betSvc.CreateBet(c.Context(), domain.CreateBet{
Amount: domain.ToCurrency(req.Amount),
TotalOdds: totalOdds,
Status: req.Status,
FullName: req.FullName,
PhoneNumber: req.PhoneNumber,
BranchID: domain.ValidInt64{
Value: 0,
Valid: false,
},
UserID: domain.ValidInt64{
Value: userID,
Valid: true,
},
IsShopBet: false,
CashoutID: cashoutID,
})
}
if err != nil {
h.logger.Error("CreateBetReq failed", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil)
}
// Updating the bet id for outcomes
for index := range outcomes {
outcomes[index].BetID = bet.ID
}
rows, err := h.betSvc.CreateBetOutcome(c.Context(), outcomes)
if err != nil {
h.logger.Error("CreateBetReq failed to create outcomes", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
}
res := convertCreateBet(bet, rows)
return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil)
}
@ -316,7 +145,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
// @Tags bet
// @Accept json
// @Produce json
// @Success 200 {array} BetRes
// @Success 200 {array} domain.BetRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /bet [get]
@ -327,9 +156,9 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets")
}
res := make([]BetRes, len(bets))
res := make([]domain.BetRes, len(bets))
for i, bet := range bets {
res[i] = convertBet(bet)
res[i] = domain.ConvertBet(bet)
}
return response.WriteJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil)
@ -342,7 +171,7 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param id path int true "Bet ID"
// @Success 200 {object} BetRes
// @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [get]
@ -356,11 +185,12 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error {
bet, err := h.betSvc.GetBetByID(c.Context(), id)
if err != nil {
// TODO: handle all the errors types
h.logger.Error("Failed to get bet by ID", "betID", id, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bet")
return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve bet")
}
res := convertBet(bet)
res := domain.ConvertBet(bet)
return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil)
@ -373,7 +203,7 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param id path string true "cashout ID"
// @Success 200 {object} BetRes
// @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /bet/cashout/{id} [get]
@ -392,7 +222,7 @@ func (h *Handler) GetBetByCashoutID(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bet", err, nil)
}
res := convertBet(bet)
res := domain.ConvertBet(bet)
return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil)

View File

@ -4,6 +4,7 @@ import (
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2"
)
@ -141,7 +142,8 @@ func (h *Handler) CreateBranch(c *fiber.Ctx) error {
checkedCompanyID = *req.CompanyID
} else {
IsSelfOwned = false
checkedCompanyID = companyID.Value //the company id is always valid when its not a super admin
checkedCompanyID = companyID.Value
//TODO:check that the company id is always valid when its not a super admin
}
// Create Branch Wallet
@ -492,13 +494,71 @@ func (h *Handler) GetBranchOperations(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Branch Operations retrieved successfully", result, nil)
}
// GetBranchCashiers godoc
// @Summary Gets branch cashiers
// @Description Gets branch cashiers
// @Tags branch
// @Accept json
// @Produce json
// @Param id path int true "Branch ID"
// @Success 200 {array} GetCashierRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /branch/{id}/cashier [get]
func (h *Handler) GetBranchCashiers(c *fiber.Ctx) error {
branchID := c.Params("id")
id, err := strconv.ParseInt(branchID, 10, 64)
if err != nil {
h.logger.Error("Invalid branch ID", "branchID", branchID, "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid branch ID", err, nil)
}
cashiers, err := h.userSvc.GetCashiersByBranch(c.Context(), id)
if err != nil {
h.logger.Error("Failed to get cashier by branch ID", "branchID", id, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve cashier", err, nil)
}
var result []GetCashierRes = make([]GetCashierRes, 0, len(cashiers))
for _, cashier := range cashiers {
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), cashier.ID)
if err != nil {
if err == authentication.ErrRefreshTokenNotFound {
lastLogin = &cashier.CreatedAt
} else {
h.logger.Error("Failed to get user last login", "userID", cashier.ID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login")
}
}
result = append(result, GetCashierRes{
ID: cashier.ID,
FirstName: cashier.FirstName,
LastName: cashier.LastName,
Email: cashier.Email,
PhoneNumber: cashier.PhoneNumber,
Role: cashier.Role,
EmailVerified: cashier.EmailVerified,
PhoneVerified: cashier.PhoneVerified,
CreatedAt: cashier.CreatedAt,
UpdatedAt: cashier.UpdatedAt,
SuspendedAt: cashier.SuspendedAt,
Suspended: cashier.Suspended,
LastLogin: *lastLogin,
})
}
return response.WriteJSON(c, fiber.StatusOK, "Branch Cashiers retrieved successfully", result, nil)
}
// GetBetByBranchID godoc
// @Summary Gets bets by its branch id
// @Description Gets bets by its branch id
// @Tags branch
// @Accept json
// @Produce json
// @Success 200 {array} BetRes
// @Success 200 {array} domain.BetRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /branch/{id}/bets [get]
@ -517,9 +577,9 @@ func (h *Handler) GetBetByBranchID(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bets", err, nil)
}
var res []BetRes = make([]BetRes, 0, len(bets))
var res []domain.BetRes = make([]domain.BetRes, 0, len(bets))
for _, bet := range bets {
res = append(res, convertBet(bet))
res = append(res, domain.ConvertBet(bet))
}
return response.WriteJSON(c, fiber.StatusOK, "Branch Bets Retrieved", res, nil)

View File

@ -7,6 +7,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2"
)
@ -87,6 +88,7 @@ type GetCashierRes struct {
SuspendedAt time.Time `json:"suspended_at"`
Suspended bool `json:"suspended"`
LastLogin time.Time `json:"last_login"`
BranchID int64 `json:"branch_id"`
}
// GetAllCashiers godoc
@ -103,22 +105,31 @@ type GetCashierRes struct {
// @Failure 500 {object} response.APIResponse
// @Router /cashiers [get]
func (h *Handler) GetAllCashiers(c *fiber.Ctx) error {
// branchId := int64(12) //c.Locals("branch_id").(int64)
// filter := user.Filter{
// Role: string(domain.RoleCashier),
// BranchId: user.ValidBranchId{
// Value: branchId,
// Valid: true,
// },
// Page: c.QueryInt("page", 1),
// PageSize: c.QueryInt("page_size", 10),
// }
// valErrs, ok := validator.Validate(c, filter)
// if !ok {
// return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
// }
role := c.Locals("role").(domain.Role)
companyId := c.Locals("company_id").(domain.ValidInt64)
cashiers, err := h.userSvc.GetAllCashiers(c.Context())
if role != domain.RoleSuperAdmin && !companyId.Valid {
return fiber.NewError(fiber.StatusInternalServerError, "Cannot get company ID")
}
filter := user.Filter{
Role: string(domain.RoleCashier),
CompanyID: companyId,
Page: domain.ValidInt{
Value: c.QueryInt("page", 1) - 1,
Valid: true,
},
PageSize: domain.ValidInt{
Value: c.QueryInt("page_size", 10),
Valid: true,
},
}
valErrs, ok := h.validator.Validate(c, filter)
if !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
cashiers, total, err := h.userSvc.GetAllUsers(c.Context(), filter)
if err != nil {
h.logger.Error("GetAllCashiers failed", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil)
@ -154,11 +165,80 @@ func (h *Handler) GetAllCashiers(c *fiber.Ctx) error {
})
}
return response.WriteJSON(c, fiber.StatusOK, "Cashiers retrieved successfully", result, nil)
return response.WritePaginatedJSON(c, fiber.StatusOK, "Cashiers retrieved successfully", result, nil, filter.Page.Value, int(total))
}
type updateUserReq struct {
// GetCashierByID godoc
// @Summary Get cashier by id
// @Description Get a single cashier by id
// @Tags cashier
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} UserProfileRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /cashier/{id} [get]
func (h *Handler) GetCashierByID(c *fiber.Ctx) error {
// branchId := int64(12) //c.Locals("branch_id").(int64)
// filter := user.Filter{
// Role: string(domain.RoleUser),
// BranchId: user.ValidBranchId{
// Value: branchId,
// Valid: true,
// },
// Page: c.QueryInt("page", 1),
// PageSize: c.QueryInt("page_size", 10),
// }
// valErrs, ok := validator.Validate(c, filter)
// if !ok {
// return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
// }
stringID := c.Params("id")
cashierID, err := strconv.ParseInt(stringID, 10, 64)
if err != nil {
h.logger.Error("failed to fetch user using UserID", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashier ID", nil, nil)
}
user, err := h.userSvc.GetCashierByID(c.Context(), cashierID)
if err != nil {
h.logger.Error("Get User By ID failed", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil)
}
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil {
if err != authentication.ErrRefreshTokenNotFound {
h.logger.Error("Failed to get user last login", "cashierID", user.ID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login")
}
lastLogin = &user.CreatedAt
}
res := GetCashierRes{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
SuspendedAt: user.SuspendedAt,
Suspended: user.Suspended,
LastLogin: *lastLogin,
BranchID: user.BranchID,
}
return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil)
}
type updateCashierReq struct {
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"`
Suspended bool `json:"suspended" example:"false"`
@ -171,7 +251,7 @@ type updateUserReq struct {
// @Accept json
// @Produce json
// @Param id path int true "Cashier ID"
// @Param cashier body updateUserReq true "Update cashier"
// @Param cashier body updateCashierReq true "Update cashier"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
@ -184,7 +264,7 @@ func (h *Handler) UpdateCashier(c *fiber.Ctx) error {
h.logger.Error("UpdateCashier failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashier ID", nil, nil)
}
var req updateUserReq
var req updateCashierReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("UpdateCashier failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)

View File

@ -25,12 +25,15 @@ type CompanyRes struct {
}
type GetCompanyRes struct {
ID int64 `json:"id" example:"1"`
Name string `json:"name" example:"CompanyName"`
AdminID int64 `json:"admin_id" example:"1"`
WalletID int64 `json:"wallet_id" example:"1"`
WalletBalance float32 `json:"balance" example:"1"`
IsActive bool `json:"is_active" example:"false"`
ID int64 `json:"id" example:"1"`
Name string `json:"name" example:"CompanyName"`
AdminID int64 `json:"admin_id" example:"1"`
WalletID int64 `json:"wallet_id" example:"1"`
WalletBalance float32 `json:"balance" example:"1"`
IsActive bool `json:"is_active" example:"false"`
AdminFirstName string `json:"admin_first_name" example:"John"`
AdminLastName string `json:"admin_last_name" example:"Doe"`
AdminPhoneNumber string `json:"admin_phone_number" example:"1234567890"`
}
func convertCompany(company domain.Company) CompanyRes {
@ -44,12 +47,15 @@ func convertCompany(company domain.Company) CompanyRes {
func convertGetCompany(company domain.GetCompany) GetCompanyRes {
return GetCompanyRes{
ID: company.ID,
Name: company.Name,
AdminID: company.AdminID,
WalletID: company.WalletID,
WalletBalance: company.WalletBalance.Float32(),
IsActive: company.IsWalletActive,
ID: company.ID,
Name: company.Name,
AdminID: company.AdminID,
WalletID: company.WalletID,
WalletBalance: company.WalletBalance.Float32(),
IsActive: company.IsWalletActive,
AdminFirstName: company.AdminFirstName,
AdminLastName: company.AdminLastName,
AdminPhoneNumber: company.AdminPhoneNumber,
}
}
@ -235,7 +241,7 @@ func (h *Handler) UpdateCompany(c *fiber.Ctx) error {
var req UpdateCompanyReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("CreateCompanyReq failed", "error", err)
h.logger.Error("UpdateCompanyReq failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil)
}
valErrs, ok := h.validator.Validate(c, req)

View File

@ -25,7 +25,7 @@ import (
type Handler struct {
logger *slog.Logger
notificationSvc notificationservice.NotificationStore
notificationSvc *notificationservice.Service
userSvc *user.Service
referralSvc referralservice.ReferralStore
walletSvc *wallet.Service
@ -47,7 +47,7 @@ type Handler struct {
func New(
logger *slog.Logger,
notificationSvc notificationservice.NotificationStore,
notificationSvc *notificationservice.Service,
validator *customvalidator.CustomValidator,
walletSvc *wallet.Service,
referralSvc referralservice.ReferralStore,

View File

@ -109,14 +109,23 @@ type ManagersRes struct {
// @Failure 500 {object} response.APIResponse
// @Router /managers [get]
func (h *Handler) GetAllManagers(c *fiber.Ctx) error {
role := c.Locals("role").(domain.Role)
companyId := c.Locals("company_id").(domain.ValidInt64)
if role != domain.RoleSuperAdmin && !companyId.Valid {
return fiber.NewError(fiber.StatusInternalServerError, "Cannot get company ID")
}
filter := user.Filter{
Role: string(domain.RoleBranchManager),
CompanyID: domain.ValidInt64{
Value: int64(c.QueryInt("company_id")),
Role: string(domain.RoleBranchManager),
CompanyID: companyId,
Page: domain.ValidInt{
Value: c.QueryInt("page", 1) - 1,
Valid: true,
},
PageSize: domain.ValidInt{
Value: c.QueryInt("page_size", 10),
Valid: true,
},
Page: c.QueryInt("page", 1) - 1,
PageSize: c.QueryInt("page_size", 10),
}
valErrs, ok := h.validator.Validate(c, filter)
if !ok {
@ -156,24 +165,101 @@ func (h *Handler) GetAllManagers(c *fiber.Ctx) error {
}
}
return response.WritePaginatedJSON(c, fiber.StatusOK, "Managers retrieved successfully", result, nil, filter.Page, int(total))
return response.WritePaginatedJSON(c, fiber.StatusOK, "Managers retrieved successfully", result, nil, filter.Page.Value, int(total))
}
// GetManagerByID godoc
// @Summary Get manager by id
// @Description Get a single manager by id
// @Tags manager
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} ManagersRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /managers/{id} [get]
func (h *Handler) GetManagerByID(c *fiber.Ctx) error {
// branchId := int64(12) //c.Locals("branch_id").(int64)
// filter := user.Filter{
// Role: string(domain.RoleUser),
// BranchId: user.ValidBranchId{
// Value: branchId,
// Valid: true,
// },
// Page: c.QueryInt("page", 1),
// PageSize: c.QueryInt("page_size", 10),
// }
// valErrs, ok := validator.Validate(c, filter)
// if !ok {
// return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
// }
userIDstr := c.Params("id")
userID, err := strconv.ParseInt(userIDstr, 10, 64)
if err != nil {
h.logger.Error("failed to fetch user using UserID", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid managers ID", nil, nil)
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.logger.Error("Get User By ID failed", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get managers", nil, nil)
}
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil {
if err != authentication.ErrRefreshTokenNotFound {
h.logger.Error("Failed to get user last login", "userID", user.ID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login")
}
lastLogin = &user.CreatedAt
}
res := ManagersRes{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
SuspendedAt: user.SuspendedAt,
Suspended: user.Suspended,
LastLogin: *lastLogin,
}
return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil)
}
type updateManagerReq struct {
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"`
Suspended bool `json:"suspended" example:"false"`
CompanyID *int64 `json:"company_id,omitempty" example:"1"`
}
// UpdateManagers godoc
// @Summary Update Managers
// @Description Update Managers
// @Tags Managers
// @Tags manager
// @Accept json
// @Produce json
// @Param Managers body updateUserReq true "Update Managers"
// @Param Managers body updateManagerReq true "Update Managers"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /managers/{id} [put]
func (h *Handler) UpdateManagers(c *fiber.Ctx) error {
var req updateUserReq
var req updateManagerReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("UpdateManagers failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)
@ -190,6 +276,19 @@ func (h *Handler) UpdateManagers(c *fiber.Ctx) error {
h.logger.Error("UpdateManagers failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid Managers ID", nil, nil)
}
var companyID domain.ValidInt64
role := c.Locals("role").(domain.Role)
if req.CompanyID != nil {
if role != domain.RoleSuperAdmin {
h.logger.Error("UpdateManagers failed", "error", err)
return response.WriteJSON(c, fiber.StatusUnauthorized, "This user role cannot modify company ID", nil, nil)
}
companyID = domain.ValidInt64{
Value: *req.CompanyID,
Valid: true,
}
}
err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{
UserId: ManagersId,
FirstName: domain.ValidString{
@ -204,6 +303,7 @@ func (h *Handler) UpdateManagers(c *fiber.Ctx) error {
Value: req.Suspended,
Valid: true,
},
CompanyID: companyID,
},
)
if err != nil {

View File

@ -3,58 +3,107 @@ package handlers
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/websocket/v2"
"github.com/gofiber/fiber/v2/middleware/adaptor"
"github.com/gorilla/websocket"
"github.com/valyala/fasthttp/fasthttpadaptor"
)
func (h *Handler) ConnectSocket(c *fiber.Ctx) error {
if !websocket.IsWebSocketUpgrade(c) {
h.logger.Warn("WebSocket upgrade required")
return fiber.ErrUpgradeRequired
}
func hijackHTTP(c *fiber.Ctx) (net.Conn, http.ResponseWriter, error) {
var rw http.ResponseWriter
var conn net.Conn
// This is a trick: fasthttpadaptor gives us the HTTP interfaces
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hj, ok := w.(http.Hijacker)
if !ok {
return
}
var err error
conn, _, err = hj.Hijack()
if err != nil {
return
}
rw = w
})
fasthttpadaptor.NewFastHTTPHandler(handler)(c.Context())
if conn == nil || rw == nil {
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to hijack connection")
}
return conn, rw, nil
}
func (h *Handler) ConnectSocket(c *fiber.Ctx) error {
userID, ok := c.Locals("userID").(int64)
if !ok || userID == 0 {
h.logger.Error("Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "invalid user identification")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
}
c.Locals("allowed", true)
// Convert *fiber.Ctx to *http.Request
req, err := adaptor.ConvertRequest(c, false)
if err != nil {
h.logger.Error("Failed to convert request", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert request")
}
return websocket.New(func(conn *websocket.Conn) {
ctx := context.Background()
logger := h.logger.With("userID", userID, "remoteAddr", conn.RemoteAddr())
// Create a net.Conn hijacked from the fasthttp context
netConn, rw, err := hijackHTTP(c)
if err != nil {
h.logger.Error("Failed to hijack connection", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to hijack connection")
}
if err := h.notificationSvc.ConnectWebSocket(ctx, userID, conn); err != nil {
logger.Error("Failed to connect WebSocket", "error", err)
_ = conn.Close()
return
}
// Upgrade the connection using Gorilla's Upgrader
conn, err := ws.Upgrader.Upgrade(rw, req, nil)
if err != nil {
h.logger.Error("WebSocket upgrade failed", "error", err)
netConn.Close()
return fiber.NewError(fiber.StatusInternalServerError, "WebSocket upgrade failed")
}
logger.Info("WebSocket connection established")
client := &ws.Client{
Conn: conn,
RecipientID: userID,
}
defer func() {
h.notificationSvc.DisconnectWebSocket(userID)
logger.Info("WebSocket connection closed")
_ = conn.Close()
}()
h.notificationSvc.Hub.Register <- client
h.logger.Info("WebSocket connection established", "userID", userID)
for {
if _, _, err := conn.ReadMessage(); err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
logger.Warn("WebSocket unexpected close", "error", err)
}
break
defer func() {
h.notificationSvc.Hub.Unregister <- client
h.logger.Info("WebSocket connection closed", "userID", userID)
conn.Close()
}()
for {
_, _, err := conn.ReadMessage()
if err != nil {
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
h.logger.Info("WebSocket closed normally", "userID", userID)
} else {
h.logger.Warn("Unexpected WebSocket closure", "userID", userID, "error", err)
}
break
}
})(c)
}
return nil
}
func (h *Handler) MarkNotificationAsRead(c *fiber.Ctx) error {
type Request struct {
NotificationID string `json:"notification_id" validate:"required"`
NotificationIDs []string `json:"notification_ids" validate:"required"`
}
var req Request
@ -63,14 +112,15 @@ func (h *Handler) MarkNotificationAsRead(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
userID, ok := c.Locals("userID").(int64)
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.logger.Error("Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "invalid user identification")
}
if err := h.notificationSvc.MarkAsRead(context.Background(), req.NotificationID, userID); err != nil {
h.logger.Error("Failed to mark notification as read", "notificationID", req.NotificationID, "error", err)
fmt.Printf("Notification IDs: %v \n", req.NotificationIDs)
if err := h.notificationSvc.MarkAsRead(context.Background(), req.NotificationIDs, userID); err != nil {
h.logger.Error("Failed to mark notifications as read", "notificationID", req.NotificationIDs, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update notification status")
}
@ -97,18 +147,18 @@ func (h *Handler) CreateAndSendNotification(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
userID, ok := c.Locals("userID").(int64)
if !ok || userID == 0 {
h.logger.Error("[NotificationSvc.CreateAndSendNotification] Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
}
// userID, ok := c.Locals("userID").(int64)
// if !ok || userID == 0 {
// h.logger.Error("[NotificationSvc.CreateAndSendNotification] Invalid user ID in context")
// return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
// }
switch req.DeliveryScheme {
case domain.NotificationDeliverySchemeSingle:
if req.Reciever == domain.NotificationRecieverSideCustomer && req.RecipientID != userID {
h.logger.Warn("[NotificationSvc.CreateAndSendNotification] Unauthorized attempt to send notification", "userID", userID, "recipientID", req.RecipientID)
return fiber.NewError(fiber.StatusForbidden, "Unauthorized to send notification to this recipient")
}
// if req.Reciever == domain.NotificationRecieverSideCustomer {
// h.logger.Warn("[NotificationSvc.CreateAndSendNotification] Unauthorized attempt to send notification", "recipientID", req.RecipientID)
// return fiber.NewError(fiber.StatusForbidden, "Unauthorized to send notification to this recipient")
// }
notification := &domain.Notification{
ID: "",
@ -134,17 +184,21 @@ func (h *Handler) CreateAndSendNotification(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"message": "Single notification sent successfully", "notification_id": notification.ID})
case domain.NotificationDeliverySchemeBulk:
recipients, err := h.getAllRecipientIDs(context.Background(), req.Reciever)
recipients, _, err := h.userSvc.GetAllUsers(context.Background(), user.Filter{
Role: string(req.Reciever),
})
if err != nil {
h.logger.Error("[NotificationSvc.CreateAndSendNotification] Failed to fetch recipients for bulk notification", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch recipients")
}
fmt.Printf("Number of Recipients %d \n", len(recipients))
notificationIDs := make([]string, 0, len(recipients))
for _, recipientID := range recipients {
for _, user := range recipients {
notification := &domain.Notification{
ID: "",
RecipientID: recipientID,
RecipientID: user.ID,
Type: req.Type,
Level: req.Level,
ErrorSeverity: req.ErrorSeverity,
@ -158,7 +212,7 @@ func (h *Handler) CreateAndSendNotification(c *fiber.Ctx) error {
}
if err := h.notificationSvc.SendNotification(context.Background(), notification); err != nil {
h.logger.Error("[NotificationSvc.CreateAndSendNotification] Failed to send bulk notification", "recipientID", recipientID, "error", err)
h.logger.Error("[NotificationSvc.CreateAndSendNotification] Failed to send bulk notification", "UserID", user.ID, "error", err)
continue
}
notificationIDs = append(notificationIDs, notification.ID)
@ -177,6 +231,94 @@ func (h *Handler) CreateAndSendNotification(c *fiber.Ctx) error {
}
}
func (h *Handler) getAllRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) {
func (h *Handler) GetNotifications(c *fiber.Ctx) error {
limitStr := c.Query("limit", "10")
offsetStr := c.Query("offset", "0")
// Convert limit and offset to integers
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
h.logger.Error("[NotificationSvc.GetNotifications] Invalid limit value", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid limit value")
}
offset, err := strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
h.logger.Error("[NotificationSvc.GetNotifications] Invalid offset value", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid offset value")
}
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.logger.Error("[NotificationSvc.GetNotifications] Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
}
notifications, err := h.notificationSvc.ListNotifications(context.Background(), userID, limit, offset)
if err != nil {
h.logger.Error("[NotificationSvc.GetNotifications] Failed to fetch notifications", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications")
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"notifications": notifications,
"total_count": len(notifications),
"limit": limit,
"offset": offset,
})
}
func (h *Handler) GetAllRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) {
return h.notificationSvc.ListRecipientIDs(ctx, receiver)
}
func (h *Handler) CountUnreadNotifications(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.logger.Error("[NotificationSvc.GetNotifications] Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
}
total, err := h.notificationSvc.CountUnreadNotifications(c.Context(), userID)
if err != nil {
h.logger.Error("[NotificationSvc.CountUnreadNotifications] Failed to fetch unread notification count", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications")
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"unread": total,
})
}
func (h *Handler) GetAllNotifications(c *fiber.Ctx) error {
limitStr := c.Query("limit", "10")
pageStr := c.Query("page", "1")
// Convert limit and offset to integers
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
h.logger.Error("[NotificationSvc.GetNotifications] Invalid limit value", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid limit value")
}
page, err := strconv.Atoi(pageStr)
if err != nil || page <= 0 {
h.logger.Error("[NotificationSvc.GetNotifications] Invalid page value", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid page value")
}
notifications, err := h.notificationSvc.GetAllNotifications(context.Background(), limit, ((page - 1) * limit))
if err != nil {
h.logger.Error("[NotificationSvc.GetNotifications] Failed to fetch notifications", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications")
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"notifications": notifications,
"total_count": len(notifications),
"limit": limit,
"page": page,
})
}

View File

@ -2,6 +2,7 @@ package handlers
import (
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
@ -98,6 +99,8 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error {
// @Param page_size query int false "Page size"
// @Param league_id query string false "League ID Filter"
// @Param sport_id query string false "Sport ID Filter"
// @Param first_start_time query string false "Start Time"
// @Param last_start_time query string false "End Time"
// @Success 200 {array} domain.UpcomingEvent
// @Failure 500 {object} response.APIResponse
// @Router /prematch/events [get]
@ -106,6 +109,8 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
pageSize := c.QueryInt("page_size", 10)
leagueIDQuery := c.Query("league_id")
sportIDQuery := c.Query("sport_id")
firstStartTimeQuery := c.Query("first_start_time")
lastStartTimeQuery := c.Query("last_start_time")
leagueID := domain.ValidString{
Value: leagueIDQuery,
@ -116,7 +121,42 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
Valid: sportIDQuery != "",
}
events, total, err := h.eventSvc.GetPaginatedUpcomingEvents(c.Context(), int32(pageSize), int32(page)-1, leagueID, sportID)
var firstStartTime domain.ValidTime
if firstStartTimeQuery != "" {
firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery)
if err != nil {
h.logger.Error("invalid start_time format", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil)
}
firstStartTime = domain.ValidTime{
Value: firstStartTimeParsed,
Valid: true,
}
}
var lastStartTime domain.ValidTime
if lastStartTimeQuery != "" {
lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery)
if err != nil {
h.logger.Error("invalid start_time format", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil)
}
lastStartTime = domain.ValidTime{
Value: lastStartTimeParsed,
Valid: true,
}
}
limit := domain.ValidInt64{
Value: int64(pageSize),
Valid: true,
}
offset := domain.ValidInt64{
Value: int64(page - 1),
Valid: true,
}
events, total, err := h.eventSvc.GetPaginatedUpcomingEvents(
c.Context(), limit, offset, leagueID, sportID, firstStartTime, lastStartTime)
// fmt.Printf("League ID: %v", leagueID)
if err != nil {
@ -183,7 +223,7 @@ func (h *Handler) GetPrematchOddsByUpcomingID(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid offset value", nil, nil)
}
odds, err := h.prematchSvc.GetPrematchOddsByUpcomingID(c.Context(), upcomingID, int32(limit), int32(offset))
odds, err := h.prematchSvc.GetPrematchOddsByUpcomingID(c.Context(), upcomingID)
if err != nil {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve prematch odds", nil, nil)
}

View File

@ -3,6 +3,7 @@ package handlers
import (
"encoding/json"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
@ -76,10 +77,10 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error {
}
// Checking to make sure the event hasn't already started
// currentTime := time.Now()
// if event.StartTime.Before(currentTime) {
// return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil)
// }
currentTime := time.Now()
if event.StartTime.Before(currentTime) {
return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil)
}
odds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr)
@ -182,7 +183,7 @@ func (h *Handler) GetTicketByID(c *fiber.Ctx) error {
ticket, err := h.ticketSvc.GetTicketByID(c.Context(), id)
if err != nil {
// h.logger.Error("Failed to get ticket by ID", "ticketID", id, "error", err)
h.logger.Error("Failed to get ticket by ID", "ticketID", id, "error", err)
return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve ticket")
}

View File

@ -150,9 +150,7 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
medium, err := getMedium(req.Email, req.PhoneNumber)
if err != nil {
h.logger.Error("RegisterUser failed", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
user.OtpMedium = medium
@ -160,24 +158,22 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
newUser, err := h.userSvc.RegisterUser(c.Context(), user)
if err != nil {
if errors.Is(err, domain.ErrOtpAlreadyUsed) {
return response.WriteJSON(c, fiber.StatusBadRequest, "Otp already used", nil, nil)
return fiber.NewError(fiber.StatusBadRequest, "Otp already used")
}
if errors.Is(err, domain.ErrOtpExpired) {
return response.WriteJSON(c, fiber.StatusBadRequest, "Otp expired", nil, nil)
return fiber.NewError(fiber.StatusBadRequest, "Otp expired")
}
if errors.Is(err, domain.ErrInvalidOtp) {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid otp", nil, nil)
return fiber.NewError(fiber.StatusBadRequest, "Invalid otp")
}
if errors.Is(err, domain.ErrOtpNotFound) {
return response.WriteJSON(c, fiber.StatusBadRequest, "User already exist", nil, nil)
return fiber.NewError(fiber.StatusBadRequest, "User already exist")
}
h.logger.Error("RegisterUser failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
return fiber.NewError(fiber.StatusInternalServerError, "Unknown Error")
}
_, err = h.walletSvc.CreateWallet(c.Context(), domain.CreateWallet{
newWallet, err := h.walletSvc.CreateWallet(c.Context(), domain.CreateWallet{
UserID: newUser.ID,
IsWithdraw: true,
IsBettable: true,
@ -194,6 +190,14 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
}
}
// TODO: Remove later
err = h.walletSvc.AddToWallet(c.Context(), newWallet.ID, domain.ToCurrency(100.0))
if err != nil {
h.logger.Error("Failed to update wallet for user", "userID", newUser.ID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update user wallet")
}
return response.WriteJSON(c, fiber.StatusOK, "Registration successful", nil, nil)
}
@ -377,7 +381,8 @@ func getMedium(email, phoneNumber string) (domain.OtpMedium, error) {
}
type SearchUserByNameOrPhoneReq struct {
SearchString string
SearchString string `json:"query"`
Role *domain.Role `json:"role,omitempty"`
}
// SearchUserByNameOrPhone godoc
@ -405,7 +410,8 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return nil
}
users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString)
companyID := c.Locals("company_id").(domain.ValidInt64)
users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString, req.Role, companyID)
if err != nil {
h.logger.Error("SearchUserByNameOrPhone failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
@ -450,7 +456,7 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} response.APIResponse
// @Success 200 {object} UserProfileRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
@ -474,13 +480,13 @@ func (h *Handler) GetUserByID(c *fiber.Ctx) error {
userIDstr := c.Params("id")
userID, err := strconv.ParseInt(userIDstr, 10, 64)
if err != nil {
h.logger.Error("UpdateCashier failed", "error", err)
h.logger.Error("failed to fetch user using UserID", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashier ID", nil, nil)
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.logger.Error("GetAllCashiers failed", "error", err)
h.logger.Error("Get User By ID failed", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil)
}
@ -510,6 +516,78 @@ func (h *Handler) GetUserByID(c *fiber.Ctx) error {
LastLogin: *lastLogin,
}
return response.WriteJSON(c, fiber.StatusOK, "Cashiers retrieved successfully", res, nil)
return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil)
}
// DeleteUser godoc
// @Summary Delete user by ID
// @Description Delete a user by their ID
// @Tags user
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/delete/{id} [delete]
func (h *Handler) DeleteUser(c *fiber.Ctx) error {
userIDstr := c.Params("id")
userID, err := strconv.ParseInt(userIDstr, 10, 64)
if err != nil {
h.logger.Error("DeleteUser failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid user ID", nil, nil)
}
err = h.userSvc.DeleteUser(c.Context(), userID)
if err != nil {
h.logger.Error("Failed to delete user", "userID", userID, "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to delete user", nil, nil)
}
return response.WriteJSON(c, fiber.StatusOK, "User deleted successfully", nil, nil)
}
type UpdateUserSuspendReq struct {
UserID int64 `json:"user_id" validate:"required" example:"123"`
Suspended bool `json:"suspended" validate:"required" example:"true"`
}
type UpdateUserSuspendRes struct {
UserID int64 `json:"user_id"`
Suspended bool `json:"suspended"`
}
// UpdateUserSuspend godoc
// @Summary Suspend or unsuspend a user
// @Description Suspend or unsuspend a user
// @Tags user
// @Accept json
// @Produce json
// @Param updateUserSuspend body UpdateUserSuspendReq true "Suspend or unsuspend a user"
// @Success 200 {object} UpdateUserSuspendRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /user/suspend [post]
func (h *Handler) UpdateUserSuspend(c *fiber.Ctx) error {
var req UpdateUserSuspendReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse UpdateUserSuspend request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
err := h.userSvc.UpdateUserSuspend(c.Context(), req.UserID, req.Suspended)
if err != nil {
h.logger.Error("Failed to update user suspend status", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update user suspend status")
}
res := UpdateUserSuspendRes{
UserID: req.UserID,
Suspended: req.Suspended,
}
return response.WriteJSON(c, fiber.StatusOK, "User suspend status updated successfully", res, nil)
}

View File

@ -45,13 +45,12 @@ type CustomerWalletRes struct {
StaticID int64 `json:"static_id" example:"1"`
StaticBalance float32 `json:"static_balance" example:"100.0"`
CustomerID int64 `json:"customer_id" example:"1"`
CompanyID int64 `json:"company_id" example:"1"`
RegularUpdatedAt time.Time `json:"regular_updated_at"`
StaticUpdatedAt time.Time `json:"static_updated_at"`
CreatedAt time.Time `json:"created_at"`
}
func convertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes {
func ConvertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes {
return CustomerWalletRes{
ID: wallet.ID,
RegularID: wallet.RegularID,
@ -59,7 +58,6 @@ func convertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes {
StaticID: wallet.StaticID,
StaticBalance: wallet.StaticBalance.Float32(),
CustomerID: wallet.CustomerID,
CompanyID: wallet.CompanyID,
RegularUpdatedAt: wallet.RegularUpdatedAt,
StaticUpdatedAt: wallet.StaticUpdatedAt,
CreatedAt: wallet.CreatedAt,
@ -249,21 +247,21 @@ func (h *Handler) GetCustomerWallet(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized access")
}
companyID, err := strconv.ParseInt(c.Get("company_id"), 10, 64)
if err != nil {
h.logger.Error("Invalid company_id header", "value", c.Get("company_id"), "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid company_id")
}
// companyID, err := strconv.ParseInt(c.Get("company_id"), 10, 64)
// if err != nil {
// h.logger.Error("Invalid company_id header", "value", c.Get("company_id"), "error", err)
// return fiber.NewError(fiber.StatusBadRequest, "Invalid company_id")
// }
h.logger.Info("Fetching customer wallet", "userID", userID, "companyID", companyID)
h.logger.Info("Fetching customer wallet", "userID", userID)
wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), userID, companyID)
wallet, err := h.walletSvc.GetWalletsByUser(c.Context(), userID)
if err != nil {
h.logger.Error("Failed to get customer wallet", "userID", userID, "companyID", companyID, "error", err)
h.logger.Error("Failed to get customer wallet", "userID", userID, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve wallet")
}
res := convertCustomerWallet(wallet)
res := convertWallet(wallet[0])
return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil)
}

View File

@ -44,7 +44,7 @@ func (a *App) authMiddleware(c *fiber.Ctx) error {
}
// Asserting to make sure that there is no company role without a valid company id
if claim.Role != domain.RoleSuperAdmin && !claim.CompanyID.Valid {
if claim.Role != domain.RoleSuperAdmin && claim.Role != domain.RoleCustomer && !claim.CompanyID.Valid {
fmt.Println("Company Role without Company ID")
return fiber.NewError(fiber.StatusInternalServerError, "Company Role without Company ID")
}
@ -71,3 +71,31 @@ func (a *App) CompanyOnly(c *fiber.Ctx) error {
}
return c.Next()
}
func (a *App) WebsocketAuthMiddleware(c *fiber.Ctx) error {
tokenStr := c.Query("token")
if tokenStr == "" {
a.logger.Error("Missing token in query parameter")
return fiber.NewError(fiber.StatusUnauthorized, "Missing token")
}
claim, err := jwtutil.ParseJwt(tokenStr, a.JwtConfig.JwtAccessKey)
if err != nil {
if errors.Is(err, jwtutil.ErrExpiredToken) {
a.logger.Error("Token expired")
return fiber.NewError(fiber.StatusUnauthorized, "Token expired")
}
a.logger.Error("Invalid token", "error", err)
return fiber.NewError(fiber.StatusUnauthorized, "Invalid token")
}
userID := claim.UserId
if userID == 0 {
a.logger.Error("Invalid user ID in token claims")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user ID")
}
c.Locals("userID", userID)
a.logger.Info("Authenticated WebSocket connection", "userID", userID)
return c.Next()
}

View File

@ -39,7 +39,7 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"message": "Welcome to the FortuneBet API",
"version": "1.0.1",
"version": "1.0dev2",
})
})
@ -81,6 +81,8 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist)
a.fiber.Get("/user/profile", a.authMiddleware, h.UserProfile)
a.fiber.Get("/user/single/:id", a.authMiddleware, h.GetUserByID)
a.fiber.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser)
a.fiber.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend)
a.fiber.Get("/user/wallet", a.authMiddleware, h.GetCustomerWallet)
a.fiber.Post("/user/search", a.authMiddleware, h.SearchUserByNameOrPhone)
@ -92,13 +94,17 @@ func (a *App) initAppRoutes() {
a.fiber.Patch("/referral/settings", a.authMiddleware, h.UpdateReferralSettings)
a.fiber.Get("/cashiers", a.authMiddleware, h.GetAllCashiers)
a.fiber.Get("/cashiers/:id", a.authMiddleware, h.GetCashierByID)
a.fiber.Post("/cashiers", a.authMiddleware, h.CreateCashier)
a.fiber.Put("/cashiers/:id", a.authMiddleware, h.UpdateCashier)
a.fiber.Get("/admin", a.authMiddleware, h.GetAllAdmins)
a.fiber.Get("/admin/:id", a.authMiddleware, h.GetAdminByID)
a.fiber.Post("/admin", a.authMiddleware, h.CreateAdmin)
a.fiber.Put("/admin/:id", a.authMiddleware, h.UpdateAdmin)
a.fiber.Get("/managers", a.authMiddleware, h.GetAllManagers)
a.fiber.Get("/managers/:id", a.authMiddleware, h.GetManagerByID)
a.fiber.Post("/managers", a.authMiddleware, h.CreateManager)
a.fiber.Put("/managers/:id", a.authMiddleware, h.UpdateManagers)
a.fiber.Get("/manager/:id/branch", a.authMiddleware, h.GetBranchByManagerID)
@ -124,12 +130,14 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/search/branch", a.authMiddleware, h.SearchBranch)
// /branch/search
// branch/wallet
a.fiber.Get("/branch/:id/cashiers", a.authMiddleware, h.GetBranchCashiers)
// Branch Operation
a.fiber.Get("/supportedOperation", a.authMiddleware, h.GetAllSupportedOperations)
a.fiber.Post("/supportedOperation", a.authMiddleware, h.CreateSupportedOperation)
a.fiber.Post("/operation", a.authMiddleware, h.CreateBranchOperation)
a.fiber.Get("/branch/:id/operation", a.authMiddleware, h.GetBranchOperations)
a.fiber.Delete("/branch/:id/operation/:opID", a.authMiddleware, h.DeleteBranchOperation)
// Company
@ -149,11 +157,13 @@ func (a *App) initAppRoutes() {
// Bet Routes
a.fiber.Post("/bet", a.authMiddleware, h.CreateBet)
a.fiber.Get("/bet", a.authMiddleware, h.GetAllBet)
a.fiber.Get("/bet/:id", a.authMiddleware, h.GetBetByID)
a.fiber.Get("/bet/:id", h.GetBetByID)
a.fiber.Get("/bet/cashout/:id", a.authMiddleware, h.GetBetByCashoutID)
a.fiber.Patch("/bet/:id", a.authMiddleware, h.UpdateCashOut)
a.fiber.Delete("/bet/:id", a.authMiddleware, h.DeleteBet)
a.fiber.Post("/random/bet", a.authMiddleware, h.RandomBet)
// Wallet
a.fiber.Get("/wallet", h.GetAllWallets)
a.fiber.Get("/wallet/:id", h.GetWalletByID)
@ -191,13 +201,17 @@ func (a *App) initAppRoutes() {
a.fiber.Put("/transaction/:id", a.authMiddleware, h.UpdateTransactionVerified)
// Notification Routes
a.fiber.Get("/notifications/ws/connect/:recipientID", h.ConnectSocket)
a.fiber.Post("/notifications/mark-as-read", h.MarkNotificationAsRead)
a.fiber.Get("/ws/connect", a.WebsocketAuthMiddleware, h.ConnectSocket)
a.fiber.Get("/notifications", a.authMiddleware, h.GetNotifications)
a.fiber.Get("/notifications/all", a.authMiddleware, h.GetAllNotifications)
a.fiber.Post("/notifications/mark-as-read", a.authMiddleware, h.MarkNotificationAsRead)
a.fiber.Get("/notifications/unread", a.authMiddleware, h.CountUnreadNotifications)
a.fiber.Post("/notifications/create", h.CreateAndSendNotification)
// Virtual Game Routes
a.fiber.Post("/virtual-game/launch", a.authMiddleware, h.LaunchVirtualGame)
a.fiber.Post("/virtual-game/callback", h.HandleVirtualGameCallback)
}
///user/profile get

View File

@ -0,0 +1,73 @@
package ws
import (
"log"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
type Client struct {
Conn *websocket.Conn
RecipientID int64
}
type NotificationHub struct {
Clients map[*Client]bool
Broadcast chan interface{}
Register chan *Client
Unregister chan *Client
mu sync.Mutex
}
func NewNotificationHub() *NotificationHub {
return &NotificationHub{
Clients: make(map[*Client]bool),
Broadcast: make(chan interface{}, 1000),
Register: make(chan *Client),
Unregister: make(chan *Client),
}
}
func (h *NotificationHub) Run() {
for {
select {
case client := <-h.Register:
h.mu.Lock()
h.Clients[client] = true
h.mu.Unlock()
log.Printf("Client registered: %d", client.RecipientID)
case client := <-h.Unregister:
h.mu.Lock()
if _, ok := h.Clients[client]; ok {
delete(h.Clients, client)
client.Conn.Close()
}
h.mu.Unlock()
log.Printf("Client unregistered: %d", client.RecipientID)
case message := <-h.Broadcast:
h.mu.Lock()
for client := range h.Clients {
if payload, ok := message.(map[string]interface{}); ok {
if recipient, ok := payload["recipient_id"].(int64); ok && recipient == client.RecipientID {
err := client.Conn.WriteJSON(payload)
if err != nil {
delete(h.Clients, client)
client.Conn.Close()
}
}
}
}
h.mu.Unlock()
}
}
}
var Upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}

View File

@ -1,41 +1,58 @@
include .env
.PHONY: test
test:
@go test ./app
@docker compose up -d test
@docker compose exec test go test ./...
@docker compose stop test
.PHONY: coverage
coverage:
@mkdir -p coverage
@go test -coverprofile=coverage.out ./internal/...
@go tool cover -func=coverage.out -o coverage/coverage.txt
@docker compose up -d test
@docker compose exec test sh -c "go test -coverprofile=coverage.out ./internal/... && go tool cover -func=coverage.out -o coverage/coverage.txt"
@docker cp $(shell docker ps -q -f "name=fortunebet-test-1"):/app/coverage ./ || true
@docker compose stop test
.PHONY: build
build:
@go build -ldflags="-s" -o ./bin/web ./cmd/main.go
@docker compose build app
.PHONY: run
run:
@echo "Running Go application"
@go run ./cmd/main.go
@docker compose up -d
.PHONY: stop
stop:
@docker compose down
.PHONY: air
air:
@echo "Running air"
@echo "Running air locally (not in Docker)"
@air -c .air.toml
.PHONY: migrations/new
.PHONY: migrations/up
migrations/new:
@echo 'Creating migration files for DB_URL'
@migrate create -seq -ext=.sql -dir=./db/migrations $(name)
.PHONY: migrations/up
migrations/up:
@echo 'Running up migrations...'
@migrate -path ./db/migrations -database $(DB_URL) up
@docker compose up migrate
.PHONY: swagger
swagger:
@swag init -g cmd/main.go
.PHONY: db-up
db-up:
docker compose -f compose.db.yaml up
@docker compose up -d postgres migrate
.PHONY: db-down
db-down:
docker compose -f compose.db.yaml down
@docker compose down
postgres:
@docker exec -it fortunebet-backend-postgres-1 psql -U root -d gh
.PHONY: sqlc-gen
sqlc-gen:
@sqlc generate