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

View File

@ -114,7 +114,6 @@ CREATE TABLE IF NOT EXISTS wallets (
CREATE TABLE IF NOT EXISTS customer_wallets ( CREATE TABLE IF NOT EXISTS customer_wallets (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL, customer_id BIGINT NOT NULL,
company_id BIGINT NOT NULL,
regular_wallet_id BIGINT NOT NULL, regular_wallet_id BIGINT NOT NULL,
static_wallet_id BIGINT NOT NULL, static_wallet_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@ -234,12 +233,17 @@ CREATE TABLE companies (
wallet_id BIGINT NOT NULL wallet_id BIGINT NOT NULL
); );
-- Views -- Views
CREATE VIEW companies_with_wallets AS CREATE VIEW companies_details AS
SELECT companies.*, SELECT companies.*,
wallets.balance, 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 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 CREATE VIEW branch_details AS
SELECT branches.*, SELECT branches.*,
CONCAT(users.first_name, ' ', users.last_name) AS manager_name, CONCAT(users.first_name, ' ', users.last_name) AS manager_name,
@ -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_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; ADD CONSTRAINT fk_branch_operations_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE;
ALTER TABLE branch_cashiers ALTER TABLE branch_cashiers
ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(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); ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE;
ALTER TABLE companies ALTER TABLE companies
ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id), ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id),
ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id); ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE;
----------------------------------------------seed data------------------------------------------------------------- ----------------------------------------------seed data-------------------------------------------------------------
-------------------------------------- DO NOT USE IN PRODUCTION------------------------------------------------- -------------------------------------- DO NOT USE IN PRODUCTION-------------------------------------------------
CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS pgcrypto;
@ -340,15 +344,43 @@ INSERT INTO users (
suspended_at, suspended_at,
suspended 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 ( VALUES (
'Samuel', 'Samuel',
'Tariku', 'Tariku',
'cybersamt@gmail.com', 'cybersamt@gmail.com',
NULL, '0911111111',
crypt('password@123', gen_salt('bf'))::bytea, crypt('password@123', gen_salt('bf'))::bytea,
'super_admin', 'super_admin',
TRUE, TRUE,
FALSE, TRUE,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP,
NULL, NULL,
@ -372,11 +404,11 @@ VALUES (
'Kirubel', 'Kirubel',
'Kibru', 'Kibru',
'kirubeljkl679 @gmail.com', 'kirubeljkl679 @gmail.com',
NULL, '0911554486',
crypt('password@123', gen_salt('bf'))::bytea, crypt('password@123', gen_salt('bf'))::bytea,
'super_admin', 'super_admin',
TRUE, TRUE,
FALSE, TRUE,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP,
NULL, NULL,
@ -384,8 +416,7 @@ VALUES (
); );
INSERT INTO supported_operations (name, description) INSERT INTO supported_operations (name, description)
VALUES ('SportBook', 'Sportbook operations'), VALUES ('SportBook', 'Sportbook operations'),
('Virtual', 'Virtual operations'), ('Virtual', 'Virtual operations');
('GameZone', 'GameZone operations');
INSERT INTO wallets ( INSERT INTO wallets (
balance, balance,
is_withdraw, is_withdraw,
@ -405,4 +436,54 @@ VALUES (
TRUE, TRUE,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP,
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 * SELECT *
FROM bet_outcomes FROM bet_outcomes
WHERE event_id = $1; WHERE event_id = $1;
-- name: GetBetOutcomeByBetID :many
SELECT *
FROM bet_outcomes
WHERE bet_id = $1;
-- name: UpdateCashOut :exec -- name: UpdateCashOut :exec
UPDATE bets UPDATE bets
SET cashed_out = $2, SET cashed_out = $2,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $1; WHERE id = $1;
-- name: UpdateBetOutcomeStatus :exec -- name: UpdateBetOutcomeStatus :one
UPDATE bet_outcomes UPDATE bet_outcomes
SET status = $1 SET status = $1
WHERE id = $2; WHERE id = $2
RETURNING *;
-- name: UpdateStatus :exec -- name: UpdateStatus :exec
UPDATE bets UPDATE bets
SET status = $2, SET status = $1,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $1; WHERE id = $2;
-- name: DeleteBet :exec -- name: DeleteBet :exec
DELETE FROM bets DELETE FROM bets
WHERE id = $1; WHERE id = $1;

View File

@ -55,15 +55,6 @@ SELECT branches.*
FROM branch_cashiers FROM branch_cashiers
JOIN branches ON branch_cashiers.branch_id = branches.id JOIN branches ON branch_cashiers.branch_id = branches.id
WHERE branch_cashiers.user_id = $1; WHERE branch_cashiers.user_id = $1;
-- 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 -- name: UpdateBranch :one
UPDATE branches UPDATE branches
SET name = COALESCE(sqlc.narg(name), name), 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 *; RETURNING *;
-- name: GetAllCompanies :many -- name: GetAllCompanies :many
SELECT * SELECT *
FROM companies_with_wallets; FROM companies_details;
-- name: GetCompanyByID :one -- name: GetCompanyByID :one
SELECT * SELECT *
FROM companies_with_wallets FROM companies_details
WHERE id = $1; WHERE id = $1;
-- name: SearchCompanyByName :many -- name: SearchCompanyByName :many
SELECT * SELECT *
FROM companies_with_wallets FROM companies_details
WHERE name ILIKE '%' || $1 || '%'; WHERE name ILIKE '%' || $1 || '%';
-- name: UpdateCompany :one -- name: UpdateCompany :one
UPDATE companies UPDATE companies

View File

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

View File

@ -1,21 +1,71 @@
-- name: CreateNotification :one -- name: CreateNotification :one
INSERT INTO notifications ( INSERT INTO notifications (
id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, timestamp, metadata id,
) VALUES ( recipient_id,
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 type,
) RETURNING *; 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 -- 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 -- 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 -- 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 -- 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 -- 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 fi = $2
AND is_active = true AND is_active = true
AND source = 'b365api'; AND source = 'b365api';
-- name: GetPrematchOddsByUpcomingID :many -- name: GetPrematchOddsByUpcomingID :many
SELECT o.event_id, SELECT o.*
o.fi, FROM odds o
o.market_type, JOIN events e ON o.fi = e.id
o.market_name, WHERE e.id = $1
o.market_category, AND e.is_live = false
o.market_id, AND e.status = 'upcoming'
o.name, AND o.is_active = true
o.handicap, AND o.source = 'b365api';
o.odds_value, -- name: GetPaginatedPrematchOddsByUpcomingID :many
o.section, SELECT o.*
o.category,
o.raw_odds,
o.fetched_at,
o.source,
o.is_active
FROM odds o FROM odds o
JOIN events e ON o.fi = e.id JOIN events e ON o.fi = e.id
WHERE e.id = $1 WHERE e.id = $1
@ -118,4 +112,4 @@ WHERE e.id = $1
AND e.status = 'upcoming' AND e.status = 'upcoming'
AND o.is_active = true AND o.is_active = true
AND o.source = 'b365api' 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 company_id = $2
OR $2 IS NULL OR $2 IS NULL
) )
LIMIT $3 OFFSET $4; LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: GetTotalUsers :one -- name: GetTotalUsers :one
SELECT COUNT(*) SELECT COUNT(*)
FROM users FROM users
@ -93,23 +93,30 @@ SELECT id,
suspended_at, suspended_at,
company_id company_id
FROM users FROM users
WHERE first_name ILIKE '%' || $1 || '%' WHERE (
OR last_name ILIKE '%' || $1 || '%' first_name ILIKE '%' || $1 || '%'
OR phone_number LIKE '%' || $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 -- name: UpdateUser :exec
UPDATE users UPDATE users
SET first_name = $1, SET first_name = $1,
last_name = $2, last_name = $2,
email = $3, suspended = $3,
phone_number = $4, updated_at = CURRENT_TIMESTAMP
role = $5, WHERE id = $4;
updated_at = $6
WHERE id = $7;
-- name: UpdateUserCompany :exec -- name: UpdateUserCompany :exec
UPDATE users UPDATE users
SET company_id = $1 SET company_id = $1
WHERE id = $2; WHERE id = $2;
-- name: SuspendUser :exec -- name: SuspendUser :exec
UPDATE users UPDATE users
SET suspended = $1, SET suspended = $1,

View File

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

View File

@ -14,6 +14,9 @@ services:
interval: 5s interval: 5s
timeout: 3s timeout: 3s
retries: 5 retries: 5
volumes:
- postgres_data:/var/lib/postgresql/data
migrate: migrate:
image: migrate/migrate image: migrate/migrate
volumes: volumes:
@ -32,6 +35,37 @@ services:
networks: networks:
- app - 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: networks:
app: app:
driver: bridge 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": { "/api/v1/alea-games/launch": {
"get": { "get": {
"security": [ "security": [
@ -687,7 +787,7 @@ const docTemplate = `{
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
} }
}, },
@ -724,7 +824,7 @@ const docTemplate = `{
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/handlers.CreateBetReq" "$ref": "#/definitions/domain.CreateBetReq"
} }
} }
], ],
@ -732,7 +832,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
}, },
"400": { "400": {
@ -776,7 +876,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
}, },
"400": { "400": {
@ -820,7 +920,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
}, },
"400": { "400": {
@ -1169,7 +1269,54 @@ const docTemplate = `{
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "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": { "/cashiers": {
"get": { "get": {
"description": "Get all cashiers", "description": "Get all cashiers",
@ -1456,7 +1653,7 @@ const docTemplate = `{
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/handlers.updateUserReq" "$ref": "#/definitions/handlers.updateCashierReq"
} }
} }
], ],
@ -1907,6 +2104,54 @@ const docTemplate = `{
} }
}, },
"/managers/{id}": { "/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": { "put": {
"description": "Update Managers", "description": "Update Managers",
"consumes": [ "consumes": [
@ -1916,7 +2161,7 @@ const docTemplate = `{
"application/json" "application/json"
], ],
"tags": [ "tags": [
"Managers" "manager"
], ],
"summary": "Update Managers", "summary": "Update Managers",
"parameters": [ "parameters": [
@ -1926,7 +2171,7 @@ const docTemplate = `{
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/handlers.updateUserReq" "$ref": "#/definitions/handlers.updateManagerReq"
} }
} }
], ],
@ -2041,6 +2286,18 @@ const docTemplate = `{
"description": "Sport ID Filter", "description": "Sport ID Filter",
"name": "sport_id", "name": "sport_id",
"in": "query" "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": { "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": { "/referral/settings": {
"get": { "get": {
"security": [ "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": { "/user/profile": {
"get": { "get": {
"security": [ "security": [
@ -3389,7 +3736,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/handlers.UserProfileRes"
} }
}, },
"400": { "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": { "/user/wallet": {
"get": { "get": {
"security": [ "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": { "domain.ChapaSupportedBank": {
"type": "object", "type": "object",
"properties": { "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": { "domain.CreateTransferResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4076,9 +4580,11 @@ const docTemplate = `{
1, 1,
2, 2,
3, 3,
4 4,
5
], ],
"x-enum-comments": { "x-enum-comments": {
"OUTCOME_STATUS_ERROR": "Half Win and Half Given Back",
"OUTCOME_STATUS_HALF": "Half Win and Half Given Back", "OUTCOME_STATUS_HALF": "Half Win and Half Given Back",
"OUTCOME_STATUS_VOID": "Give Back" "OUTCOME_STATUS_VOID": "Give Back"
}, },
@ -4087,7 +4593,8 @@ const docTemplate = `{
"OUTCOME_STATUS_WIN", "OUTCOME_STATUS_WIN",
"OUTCOME_STATUS_LOSS", "OUTCOME_STATUS_LOSS",
"OUTCOME_STATUS_VOID", "OUTCOME_STATUS_VOID",
"OUTCOME_STATUS_HALF" "OUTCOME_STATUS_HALF",
"OUTCOME_STATUS_ERROR"
] ]
}, },
"domain.PaymentOption": { "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": { "domain.RawOddsByMarketID": {
"type": "object", "type": "object",
"properties": { "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": { "handlers.BranchDetailRes": {
"type": "object", "type": "object",
"properties": { "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": { "handlers.CreateBranchOperationReq": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5074,10 +5487,6 @@ const docTemplate = `{
"handlers.CustomerWalletRes": { "handlers.CustomerWalletRes": {
"type": "object", "type": "object",
"properties": { "properties": {
"company_id": {
"type": "integer",
"example": 1
},
"created_at": { "created_at": {
"type": "string" "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": { "handlers.ManagersRes": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5245,8 +5701,11 @@ const docTemplate = `{
"handlers.SearchUserByNameOrPhoneReq": { "handlers.SearchUserByNameOrPhoneReq": {
"type": "object", "type": "object",
"properties": { "properties": {
"searchString": { "query": {
"type": "string" "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": { "handlers.UpdateWalletActiveReq": {
"type": "object", "type": "object",
"required": [ "required": [
@ -5655,9 +6142,51 @@ const docTemplate = `{
} }
} }
}, },
"handlers.updateUserReq": { "handlers.updateAdminReq": {
"type": "object", "type": "object",
"properties": { "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": { "first_name": {
"type": "string", "type": "string",
"example": "John" "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": { "/api/v1/alea-games/launch": {
"get": { "get": {
"security": [ "security": [
@ -679,7 +779,7 @@
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
} }
}, },
@ -716,7 +816,7 @@
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/handlers.CreateBetReq" "$ref": "#/definitions/domain.CreateBetReq"
} }
} }
], ],
@ -724,7 +824,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
}, },
"400": { "400": {
@ -768,7 +868,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
}, },
"400": { "400": {
@ -812,7 +912,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.BetRes" "$ref": "#/definitions/domain.BetRes"
} }
}, },
"400": { "400": {
@ -1161,7 +1261,54 @@
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "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": { "/cashiers": {
"get": { "get": {
"description": "Get all cashiers", "description": "Get all cashiers",
@ -1448,7 +1645,7 @@
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/handlers.updateUserReq" "$ref": "#/definitions/handlers.updateCashierReq"
} }
} }
], ],
@ -1899,6 +2096,54 @@
} }
}, },
"/managers/{id}": { "/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": { "put": {
"description": "Update Managers", "description": "Update Managers",
"consumes": [ "consumes": [
@ -1908,7 +2153,7 @@
"application/json" "application/json"
], ],
"tags": [ "tags": [
"Managers" "manager"
], ],
"summary": "Update Managers", "summary": "Update Managers",
"parameters": [ "parameters": [
@ -1918,7 +2163,7 @@
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/handlers.updateUserReq" "$ref": "#/definitions/handlers.updateManagerReq"
} }
} }
], ],
@ -2033,6 +2278,18 @@
"description": "Sport ID Filter", "description": "Sport ID Filter",
"name": "sport_id", "name": "sport_id",
"in": "query" "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": { "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": { "/referral/settings": {
"get": { "get": {
"security": [ "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": { "/user/profile": {
"get": { "get": {
"security": [ "security": [
@ -3381,7 +3728,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/handlers.UserProfileRes"
} }
}, },
"400": { "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": { "/user/wallet": {
"get": { "get": {
"security": [ "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": { "domain.ChapaSupportedBank": {
"type": "object", "type": "object",
"properties": { "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": { "domain.CreateTransferResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4068,9 +4572,11 @@
1, 1,
2, 2,
3, 3,
4 4,
5
], ],
"x-enum-comments": { "x-enum-comments": {
"OUTCOME_STATUS_ERROR": "Half Win and Half Given Back",
"OUTCOME_STATUS_HALF": "Half Win and Half Given Back", "OUTCOME_STATUS_HALF": "Half Win and Half Given Back",
"OUTCOME_STATUS_VOID": "Give Back" "OUTCOME_STATUS_VOID": "Give Back"
}, },
@ -4079,7 +4585,8 @@
"OUTCOME_STATUS_WIN", "OUTCOME_STATUS_WIN",
"OUTCOME_STATUS_LOSS", "OUTCOME_STATUS_LOSS",
"OUTCOME_STATUS_VOID", "OUTCOME_STATUS_VOID",
"OUTCOME_STATUS_HALF" "OUTCOME_STATUS_HALF",
"OUTCOME_STATUS_ERROR"
] ]
}, },
"domain.PaymentOption": { "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": { "domain.RawOddsByMarketID": {
"type": "object", "type": "object",
"properties": { "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": { "handlers.BranchDetailRes": {
"type": "object", "type": "object",
"properties": { "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": { "handlers.CreateBranchOperationReq": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5066,10 +5479,6 @@
"handlers.CustomerWalletRes": { "handlers.CustomerWalletRes": {
"type": "object", "type": "object",
"properties": { "properties": {
"company_id": {
"type": "integer",
"example": 1
},
"created_at": { "created_at": {
"type": "string" "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": { "handlers.ManagersRes": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5237,8 +5693,11 @@
"handlers.SearchUserByNameOrPhoneReq": { "handlers.SearchUserByNameOrPhoneReq": {
"type": "object", "type": "object",
"properties": { "properties": {
"searchString": { "query": {
"type": "string" "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": { "handlers.UpdateWalletActiveReq": {
"type": "object", "type": "object",
"required": [ "required": [
@ -5647,9 +6134,51 @@
} }
} }
}, },
"handlers.updateUserReq": { "handlers.updateAdminReq": {
"type": "object", "type": "object",
"properties": { "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": { "first_name": {
"type": "string", "type": "string",
"example": "John" "example": "John"

View File

@ -80,6 +80,47 @@ definitions:
- $ref: '#/definitions/domain.OutcomeStatus' - $ref: '#/definitions/domain.OutcomeStatus'
example: 1 example: 1
type: object 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: domain.ChapaSupportedBank:
properties: properties:
acct_length: acct_length:
@ -124,6 +165,41 @@ definitions:
message: message:
type: string type: string
type: object 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: domain.CreateTransferResponse:
properties: properties:
data: data:
@ -211,8 +287,10 @@ definitions:
- 2 - 2
- 3 - 3
- 4 - 4
- 5
type: integer type: integer
x-enum-comments: x-enum-comments:
OUTCOME_STATUS_ERROR: Half Win and Half Given Back
OUTCOME_STATUS_HALF: Half Win and Half Given Back OUTCOME_STATUS_HALF: Half Win and Half Given Back
OUTCOME_STATUS_VOID: Give Back OUTCOME_STATUS_VOID: Give Back
x-enum-varnames: x-enum-varnames:
@ -221,6 +299,7 @@ definitions:
- OUTCOME_STATUS_LOSS - OUTCOME_STATUS_LOSS
- OUTCOME_STATUS_VOID - OUTCOME_STATUS_VOID
- OUTCOME_STATUS_HALF - OUTCOME_STATUS_HALF
- OUTCOME_STATUS_ERROR
domain.PaymentOption: domain.PaymentOption:
enum: enum:
- 0 - 0
@ -252,6 +331,18 @@ definitions:
description: BET, WIN, REFUND, JACKPOT_WIN description: BET, WIN, REFUND, JACKPOT_WIN
type: string type: string
type: object 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: domain.RawOddsByMarketID:
properties: properties:
fetched_at: fetched_at:
@ -534,47 +625,6 @@ definitions:
updated_at: updated_at:
type: string type: string
type: object 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: handlers.BranchDetailRes:
properties: properties:
branch_manager_id: branch_manager_id:
@ -690,41 +740,6 @@ definitions:
example: "1234567890" example: "1234567890"
type: string type: string
type: object 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: handlers.CreateBranchOperationReq:
properties: properties:
branch_id: branch_id:
@ -909,9 +924,6 @@ definitions:
type: object type: object
handlers.CustomerWalletRes: handlers.CustomerWalletRes:
properties: properties:
company_id:
example: 1
type: integer
created_at: created_at:
type: string type: string
customer_id: customer_id:
@ -937,6 +949,37 @@ definitions:
static_updated_at: static_updated_at:
type: string type: string
type: object 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: handlers.ManagersRes:
properties: properties:
created_at: created_at:
@ -1029,8 +1072,10 @@ definitions:
type: object type: object
handlers.SearchUserByNameOrPhoneReq: handlers.SearchUserByNameOrPhoneReq:
properties: properties:
searchString: query:
type: string type: string
role:
$ref: '#/definitions/domain.Role'
type: object type: object
handlers.SupportedOperationRes: handlers.SupportedOperationRes:
properties: properties:
@ -1182,6 +1227,25 @@ definitions:
example: true example: true
type: boolean type: boolean
type: object 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: handlers.UpdateWalletActiveReq:
properties: properties:
is_active: is_active:
@ -1314,8 +1378,38 @@ definitions:
- access_token - access_token
- refresh_token - refresh_token
type: object type: object
handlers.updateUserReq: handlers.updateAdminReq:
properties: 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: first_name:
example: John example: John
type: string type: string
@ -1431,6 +1525,72 @@ paths:
summary: Create Admin summary: Create Admin
tags: tags:
- admin - 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: /api/v1/alea-games/launch:
get: get:
consumes: consumes:
@ -1791,7 +1951,7 @@ paths:
description: OK description: OK
schema: schema:
items: items:
$ref: '#/definitions/handlers.BetRes' $ref: '#/definitions/domain.BetRes'
type: array type: array
"400": "400":
description: Bad Request description: Bad Request
@ -1814,14 +1974,14 @@ paths:
name: createBet name: createBet
required: true required: true
schema: schema:
$ref: '#/definitions/handlers.CreateBetReq' $ref: '#/definitions/domain.CreateBetReq'
produces: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/handlers.BetRes' $ref: '#/definitions/domain.BetRes'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -1878,7 +2038,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/handlers.BetRes' $ref: '#/definitions/domain.BetRes'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -1941,7 +2101,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/handlers.BetRes' $ref: '#/definitions/domain.BetRes'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -2110,7 +2270,7 @@ paths:
description: OK description: OK
schema: schema:
items: items:
$ref: '#/definitions/handlers.BetRes' $ref: '#/definitions/domain.BetRes'
type: array type: array
"400": "400":
description: Bad Request description: Bad Request
@ -2123,6 +2283,37 @@ paths:
summary: Gets bets by its branch id summary: Gets bets by its branch id
tags: tags:
- branch - 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: /branch/{id}/operation:
get: get:
consumes: consumes:
@ -2213,6 +2404,39 @@ paths:
summary: Get all branch wallets summary: Get all branch wallets
tags: tags:
- wallet - 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: /cashiers:
get: get:
consumes: consumes:
@ -2298,7 +2522,7 @@ paths:
name: cashier name: cashier
required: true required: true
schema: schema:
$ref: '#/definitions/handlers.updateUserReq' $ref: '#/definitions/handlers.updateCashierReq'
produces: produces:
- application/json - application/json
responses: responses:
@ -2598,6 +2822,38 @@ paths:
tags: tags:
- manager - manager
/managers/{id}: /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: put:
consumes: consumes:
- application/json - application/json
@ -2608,7 +2864,7 @@ paths:
name: Managers name: Managers
required: true required: true
schema: schema:
$ref: '#/definitions/handlers.updateUserReq' $ref: '#/definitions/handlers.updateManagerReq'
produces: produces:
- application/json - application/json
responses: responses:
@ -2630,7 +2886,7 @@ paths:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/response.APIResponse'
summary: Update Managers summary: Update Managers
tags: tags:
- Managers - manager
/operation: /operation:
post: post:
consumes: consumes:
@ -2683,6 +2939,14 @@ paths:
in: query in: query
name: sport_id name: sport_id
type: string 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: produces:
- application/json - application/json
responses: responses:
@ -2856,6 +3120,36 @@ paths:
summary: Retrieve raw odds by Market ID summary: Retrieve raw odds by Market ID
tags: tags:
- prematch - 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: /referral/settings:
get: get:
consumes: consumes:
@ -3375,6 +3669,35 @@ paths:
summary: Check if phone number or email exist summary: Check if phone number or email exist
tags: tags:
- user - 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: /user/profile:
get: get:
consumes: consumes:
@ -3567,7 +3890,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/handlers.UserProfileRes'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -3583,6 +3906,36 @@ paths:
summary: Get user by id summary: Get user by id
tags: tags:
- user - 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: /user/wallet:
get: get:
consumes: consumes:

View File

@ -243,6 +243,48 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (BetWithOutcome, err
return i, 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 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 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 FROM bet_outcomes
@ -285,10 +327,11 @@ func (q *Queries) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]
return items, nil return items, nil
} }
const UpdateBetOutcomeStatus = `-- name: UpdateBetOutcomeStatus :exec const UpdateBetOutcomeStatus = `-- name: UpdateBetOutcomeStatus :one
UPDATE bet_outcomes UPDATE bet_outcomes
SET status = $1 SET status = $1
WHERE id = $2 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 { type UpdateBetOutcomeStatusParams struct {
@ -296,9 +339,27 @@ type UpdateBetOutcomeStatusParams struct {
ID int64 `json:"id"` ID int64 `json:"id"`
} }
func (q *Queries) UpdateBetOutcomeStatus(ctx context.Context, arg UpdateBetOutcomeStatusParams) error { func (q *Queries) UpdateBetOutcomeStatus(ctx context.Context, arg UpdateBetOutcomeStatusParams) (BetOutcome, error) {
_, err := q.db.Exec(ctx, UpdateBetOutcomeStatus, arg.Status, arg.ID) row := q.db.QueryRow(ctx, UpdateBetOutcomeStatus, arg.Status, arg.ID)
return err 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 const UpdateCashOut = `-- name: UpdateCashOut :exec
@ -320,17 +381,17 @@ func (q *Queries) UpdateCashOut(ctx context.Context, arg UpdateCashOutParams) er
const UpdateStatus = `-- name: UpdateStatus :exec const UpdateStatus = `-- name: UpdateStatus :exec
UPDATE bets UPDATE bets
SET status = $2, SET status = $1,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $1 WHERE id = $2
` `
type UpdateStatusParams struct { type UpdateStatusParams struct {
ID int64 `json:"id"`
Status int32 `json:"status"` Status int32 `json:"status"`
ID int64 `json:"id"`
} }
func (q *Queries) UpdateStatus(ctx context.Context, arg UpdateStatusParams) error { 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 return err
} }

View File

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

View File

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

View File

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

View File

@ -11,12 +11,52 @@ import (
"github.com/jackc/pgx/v5/pgtype" "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 const CreateNotification = `-- name: CreateNotification :one
INSERT INTO notifications ( INSERT INTO notifications (
id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, timestamp, metadata id,
) VALUES ( recipient_id,
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 type,
) RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata 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 { type CreateNotificationParams struct {
@ -71,8 +111,58 @@ func (q *Queries) CreateNotification(ctx context.Context, arg CreateNotification
return i, err 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 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) { 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 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) { 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 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 { type ListNotificationsParams struct {
@ -182,7 +281,9 @@ func (q *Queries) ListNotifications(ctx context.Context, arg ListNotificationsPa
} }
const ListRecipientIDsByReceiver = `-- name: ListRecipientIDsByReceiver :many 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) { 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 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 { type UpdateNotificationStatusParams struct {

View File

@ -86,6 +86,61 @@ func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]GetALLPrematchOddsR
return items, nil 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 const GetPrematchOdds = `-- name: GetPrematchOdds :many
SELECT event_id, SELECT event_id,
fi, fi,
@ -162,21 +217,7 @@ func (q *Queries) GetPrematchOdds(ctx context.Context) ([]GetPrematchOddsRow, er
} }
const GetPrematchOddsByUpcomingID = `-- name: GetPrematchOddsByUpcomingID :many const GetPrematchOddsByUpcomingID = `-- name: GetPrematchOddsByUpcomingID :many
SELECT o.event_id, 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
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 FROM odds o
JOIN events e ON o.fi = e.id JOIN events e ON o.fi = e.id
WHERE e.id = $1 WHERE e.id = $1
@ -184,43 +225,19 @@ WHERE e.id = $1
AND e.status = 'upcoming' AND e.status = 'upcoming'
AND o.is_active = true AND o.is_active = true
AND o.source = 'b365api' AND o.source = 'b365api'
LIMIT $2 OFFSET $3
` `
type GetPrematchOddsByUpcomingIDParams struct { func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, id string) ([]Odd, error) {
ID string `json:"id"` rows, err := q.db.Query(ctx, GetPrematchOddsByUpcomingID, 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)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []GetPrematchOddsByUpcomingIDRow var items []Odd
for rows.Next() { for rows.Next() {
var i GetPrematchOddsByUpcomingIDRow var i Odd
if err := rows.Scan( if err := rows.Scan(
&i.ID,
&i.EventID, &i.EventID,
&i.Fi, &i.Fi,
&i.MarketType, &i.MarketType,

View File

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

View File

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

13
go.mod
View File

@ -7,24 +7,28 @@ require (
github.com/bytedance/sonic v1.13.2 github.com/bytedance/sonic v1.13.2
github.com/go-playground/validator/v10 v10.26.0 github.com/go-playground/validator/v10 v10.26.0
github.com/gofiber/fiber/v2 v2.52.6 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/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.4 github.com/jackc/pgx/v5 v5.7.4
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.10.0
github.com/swaggo/fiber-swagger v1.3.0 github.com/swaggo/fiber-swagger v1.3.0
github.com/swaggo/swag v1.16.4 github.com/swaggo/swag v1.16.4
golang.org/x/crypto v0.36.0 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 ( require (
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
// github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/fasthttp/websocket v1.5.8 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect
@ -32,7 +36,7 @@ require (
github.com/go-openapi/swag v0.23.1 // indirect github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
// github.com/gofiber/contrib/websocket v1.3.4 github.com/gorilla/websocket v1.5.3
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
@ -46,11 +50,10 @@ require (
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.59.0 // indirect github.com/valyala/fasthttp v1.59.0
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/net v0.38.0 // indirect golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.12.0 // indirect golang.org/x/sync v0.12.0 // indirect

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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 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.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 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/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 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/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/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=

View File

@ -80,3 +80,83 @@ type CreateBet struct {
IsShopBet bool IsShopBet bool
CashoutID string 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 package domain
import "fmt" import (
"fmt"
"time"
)
type ValidInt64 struct { type ValidInt64 struct {
Value int64 Value int64
Valid bool Valid bool
} }
type ValidInt struct {
Value int
Valid bool
}
type ValidString struct { type ValidString struct {
Value string Value string
Valid bool Valid bool
} }
type ValidTime struct {
Value time.Time
Valid bool
}
type ValidBool struct { type ValidBool struct {
Value bool Value bool
Valid bool Valid bool

View File

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

View File

@ -12,9 +12,8 @@ var SupportedLeagues = []int64{
10041957, //UEFA Europa League 10041957, //UEFA Europa League
10079560, //UEFA Conference League 10079560, //UEFA Conference League
10047168, // US MLS 10047168, // US MLS
10044469, // Ethiopian Premier League
10050282, //UEFA Nations League 10050282, //UEFA Nations League
10040795, //EuroLeague
10043156, //England FA Cup 10043156, //England FA Cup
10042103, //France Cup 10042103, //France Cup
@ -26,5 +25,15 @@ var SupportedLeagues = []int64{
// Basketball // Basketball
173998768, //NBA 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 package domain
import ( import (
"encoding/json"
"time" "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 { type MarketConfig struct {
Sport string Sport string
MarketCategories map[string]bool MarketCategories map[string]bool
@ -219,4 +43,42 @@ const (
OUTCOME_STATUS_LOSS OutcomeStatus = 2 OUTCOME_STATUS_LOSS OutcomeStatus = 2
OUTCOME_STATUS_VOID OutcomeStatus = 3 //Give Back OUTCOME_STATUS_VOID OutcomeStatus = 3 //Give Back
OUTCOME_STATUS_HALF OutcomeStatus = 4 //Half Win and Half Given 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 type FootballMarket int64
const ( const (
FOOTBALL_FULL_TIME_RESULT FootballMarket = 40 //"full_time_result" FOOTBALL_FULL_TIME_RESULT FootballMarket = 40 //"full_time_result"
FOOTBALL_DOUBLE_CHANCE FootballMarket = 10114 //"double_chance" FOOTBALL_DOUBLE_CHANCE FootballMarket = 10114 //"double_chance"
FOOTBALL_GOALS_OVER_UNDER FootballMarket = 981 //"goals_over_under" FOOTBALL_GOALS_OVER_UNDER FootballMarket = 981 //"goals_over_under"
FOOTBALL_CORRECT_SCORE FootballMarket = 43 //"correct_score" FOOTBALL_CORRECT_SCORE FootballMarket = 43 //"correct_score"
FOOTBALL_ASIAN_HANDICAP FootballMarket = 938 //"asian_handicap" FOOTBALL_ASIAN_HANDICAP FootballMarket = 938 //"asian_handicap"
FOOTBALL_GOAL_LINE FootballMarket = 10143 //"goal_line" FOOTBALL_GOAL_LINE FootballMarket = 10143 //"goal_line"
FOOTBALL_HALF_TIME_RESULT FootballMarket = 1579 //"half_time_result" FOOTBALL_HALF_TIME_RESULT FootballMarket = 1579 //"half_time_result"
FOOTBALL_FIRST_HALF_ASIAN_HANDICAP FootballMarket = 50137 //"1st_half_asian_handicap" FOOTBALL_FIRST_HALF_ASIAN_HANDICAP FootballMarket = 50137 //"1st_half_asian_handicap"
FOOTBALL_FIRST_HALF_GOAL_LINE FootballMarket = 50136 //"1st_half_goal_line" FOOTBALL_FIRST_HALF_GOAL_LINE FootballMarket = 50136 //"1st_half_goal_line"
FOOTBALL_FIRST_TEAM_TO_SCORE FootballMarket = 1178 //"first_team_to_score" FOOTBALL_FIRST_TEAM_TO_SCORE FootballMarket = 1178 //"first_team_to_score"
FOOTBALL_GOALS_ODD_EVEN FootballMarket = 10111 //"goals_odd_even" FOOTBALL_GOALS_ODD_EVEN FootballMarket = 10111 //"goals_odd_even"
FOOTBALL_DRAW_NO_BET FootballMarket = 10544 //"draw_no_bet" 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 type BasketBallMarket int64
@ -91,24 +101,63 @@ const (
ICE_HOCKEY_ALTERNATIVE_TOTAL_TWO_WAY IceHockeyMarket = 170240 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 // TODO: Move this into the database so that it can be modified dynamically
var SupportedMarkets = map[int64]bool{ var SupportedMarkets = map[int64]bool{
// Football Markets // Football Markets
int64(FOOTBALL_FULL_TIME_RESULT): true, //"full_time_result" int64(FOOTBALL_FULL_TIME_RESULT): true, //"full_time_result"
int64(FOOTBALL_DOUBLE_CHANCE): true, //"double_chance" int64(FOOTBALL_DOUBLE_CHANCE): true, //"double_chance"
int64(FOOTBALL_GOALS_OVER_UNDER): true, //"goals_over_under" int64(FOOTBALL_GOALS_OVER_UNDER): true, //"goals_over_under"
int64(FOOTBALL_CORRECT_SCORE): true, //"correct_score" int64(FOOTBALL_CORRECT_SCORE): true, //"correct_score"
int64(FOOTBALL_ASIAN_HANDICAP): true, //"asian_handicap" int64(FOOTBALL_ASIAN_HANDICAP): true, //"asian_handicap"
int64(FOOTBALL_GOAL_LINE): true, //"goal_line" int64(FOOTBALL_GOAL_LINE): true, //"goal_line"
int64(FOOTBALL_HALF_TIME_RESULT): true, //"half_time_result" int64(FOOTBALL_HALF_TIME_RESULT): true, //"half_time_result"
int64(FOOTBALL_FIRST_HALF_ASIAN_HANDICAP): true, //"1st_half_asian_handicap" 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_HALF_GOAL_LINE): true, //"1st_half_goal_line"
int64(FOOTBALL_FIRST_TEAM_TO_SCORE): true, //"first_team_to_score" int64(FOOTBALL_FIRST_TEAM_TO_SCORE): true, //"first_team_to_score"
int64(FOOTBALL_GOALS_ODD_EVEN): true, //"goals_odd_even" int64(FOOTBALL_GOALS_ODD_EVEN): true, //"goals_odd_even"
int64(FOOTBALL_DRAW_NO_BET): true, //"draw_no_bet" 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 // Basketball Markets
int64(BASKETBALL_GAME_LINES): true, int64(BASKETBALL_GAME_LINES): true,
int64(BASKETBALL_RESULT_AND_BOTH_TEAMS_TO_SCORE_X_POINTS): true, int64(BASKETBALL_RESULT_AND_BOTH_TEAMS_TO_SCORE_X_POINTS): true,
@ -164,4 +213,24 @@ var SupportedMarkets = map[int64]bool{
int64(ICE_HOCKEY_ALTERNATIVE_PUCK_LINE_TWO_WAY): false, int64(ICE_HOCKEY_ALTERNATIVE_PUCK_LINE_TWO_WAY): false,
int64(ICE_HOCKEY_ALTERNATIVE_TOTAL_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 FirstName ValidString
LastName ValidString LastName ValidString
Suspended ValidBool Suspended ValidBool
CompanyID ValidInt64
} }
type UpdateUserReferalCode struct { type UpdateUserReferalCode struct {
UserID int64 UserID int64
Code string 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 RegularID int64
StaticID int64 StaticID int64
CustomerID int64 CustomerID int64
CompanyID int64
} }
type GetCustomerWallet struct { type GetCustomerWallet struct {
ID int64 ID int64
@ -28,7 +27,6 @@ type GetCustomerWallet struct {
StaticID int64 StaticID int64
StaticBalance Currency StaticBalance Currency
CustomerID int64 CustomerID int64
CompanyID int64
RegularUpdatedAt time.Time RegularUpdatedAt time.Time
StaticUpdatedAt time.Time StaticUpdatedAt time.Time
CreatedAt time.Time CreatedAt time.Time
@ -56,7 +54,6 @@ type CreateWallet struct {
type CreateCustomerWallet struct { type CreateCustomerWallet struct {
CustomerID int64 CustomerID int64
CompanyID int64
RegularWalletID int64 RegularWalletID int64
StaticWalletID int64 StaticWalletID int64
} }

View File

@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
// "fmt" // "fmt"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" 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 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), Status: int32(status),
ID: id, ID: id,
}) })
return err res := convertDBBetOutcomes(update)
return res, err
} }
func (s *Store) DeleteBet(ctx context.Context, id int64) error { 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{ return domain.GetCompany{
ID: dbCompany.ID, ID: dbCompany.ID,
Name: dbCompany.Name, Name: dbCompany.Name,
AdminID: dbCompany.AdminID, AdminID: dbCompany.AdminID,
WalletID: dbCompany.WalletID, WalletID: dbCompany.WalletID,
WalletBalance: domain.Currency(dbCompany.Balance), WalletBalance: domain.Currency(dbCompany.Balance),
IsWalletActive: dbCompany.IsActive, 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)) var companies []domain.GetCompany = make([]domain.GetCompany, 0, len(dbCompanies))
for _, dbCompany := range dbCompanies { for _, dbCompany := range dbCompanies {
companies = append(companies, convertDBCompanyWithWallet(dbCompany)) companies = append(companies, convertDBCompanyDetails(dbCompany))
} }
return companies, nil 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)) var companies []domain.GetCompany = make([]domain.GetCompany, 0, len(dbCompanies))
for _, dbCompany := range dbCompanies { for _, dbCompany := range dbCompanies {
companies = append(companies, convertDBCompanyWithWallet(dbCompany)) companies = append(companies, convertDBCompanyDetails(dbCompany))
} }
return companies, nil return companies, nil
} }
@ -103,7 +106,7 @@ func (s *Store) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany
if err != nil { if err != nil {
return domain.GetCompany{}, err 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) { 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 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{ events, err := s.queries.GetPaginatedUpcomingEvents(ctx, dbgen.GetPaginatedUpcomingEventsParams{
LeagueID: pgtype.Text{ LeagueID: pgtype.Text{
String: leagueID.Value, String: leagueID.Value,
@ -127,14 +128,27 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, off
String: sportID.Value, String: sportID.Value,
Valid: sportID.Valid, Valid: sportID.Valid,
}, },
Limit: limit, Limit: pgtype.Int4{
Offset: offset * limit, 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 { if err != nil {
return nil, 0, err return nil, 0, err
} }
upcomingEvents := make([]domain.UpcomingEvent, len(events)) upcomingEvents := make([]domain.UpcomingEvent, len(events))
for i, e := range events { for i, e := range events {
upcomingEvents[i] = domain.UpcomingEvent{ upcomingEvents[i] = domain.UpcomingEvent{
@ -162,12 +176,20 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, off
String: sportID.Value, String: sportID.Value,
Valid: sportID.Valid, 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 { if err != nil {
return nil, 0, err return nil, 0, err
} }
numberOfPages := math.Ceil(float64(totalCount) / float64(limit)) numberOfPages := math.Ceil(float64(totalCount) / float64(limit.Value))
return upcomingEvents, int64(numberOfPages), nil return upcomingEvents, int64(numberOfPages), nil
} }
func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { 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) ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error)
ListFailedNotifications(ctx context.Context, limit int) ([]domain.Notification, error) ListFailedNotifications(ctx context.Context, limit int) ([]domain.Notification, error)
ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, 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 { type Repository struct {
@ -105,6 +107,24 @@ func (r *Repository) ListNotifications(ctx context.Context, recipientID int64, l
return result, nil 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) { func (r *Repository) ListFailedNotifications(ctx context.Context, limit int) ([]domain.Notification, error) {
dbNotifications, err := r.store.queries.ListFailedNotifications(ctx, int32(limit)) dbNotifications, err := r.store.queries.ListFailedNotifications(ctx, int32(limit))
if err != nil { if err != nil {
@ -177,3 +197,7 @@ func unmarshalPayload(data []byte) (domain.NotificationPayload, error) {
} }
return payload, nil 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, FetchedAt: odds.FetchedAt.Time,
}, nil }, 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) { domainOdds[i] = domain.Odd{
params := dbgen.GetPrematchOddsByUpcomingIDParams{ EventID: odd.EventID.String,
ID: upcomingID, Fi: odd.Fi.String,
Limit: limit, MarketType: odd.MarketType,
Offset: offset, 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"fmt"
"time" "time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" 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, Int64: filter.CompanyID.Value,
Valid: filter.CompanyID.Valid, Valid: filter.CompanyID.Valid,
}, },
Limit: int32(filter.PageSize), Limit: pgtype.Int4{
Offset: int32(filter.Page), Int32: int32(filter.PageSize.Value),
Valid: filter.PageSize.Valid,
},
Offset: pgtype.Int4{
Int32: int32(filter.Page.Value),
Valid: filter.Page.Valid,
},
}) })
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@ -123,14 +130,14 @@ func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.U
return userList, totalCount, nil 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) users, err := s.queries.GetAllCashiers(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
userList := make([]domain.User, len(users)) userList := make([]domain.GetCashier, len(users))
for i, user := range users { for i, user := range users {
userList[i] = domain.User{ userList[i] = domain.GetCashier{
ID: user.ID, ID: user.ID,
FirstName: user.FirstName, FirstName: user.FirstName,
LastName: user.LastName, LastName: user.LastName,
@ -148,6 +155,28 @@ func (s *Store) GetAllCashiers(ctx context.Context) ([]domain.User, error) {
return userList, nil 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) { func (s *Store) GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error) {
users, err := s.queries.GetCashiersByBranch(ctx, branchID) users, err := s.queries.GetCashiersByBranch(ctx, branchID)
if err != nil { if err != nil {
@ -173,11 +202,28 @@ func (s *Store) GetCashiersByBranch(ctx context.Context, branchID int64) ([]doma
return userList, nil return userList, nil
} }
func (s *Store) SearchUserByNameOrPhone(ctx context.Context, searchString string) ([]domain.User, error) { func (s *Store) SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error) {
users, err := s.queries.SearchUserByNameOrPhone(ctx, pgtype.Text{
String: searchString, query := dbgen.SearchUserByNameOrPhoneParams{
Valid: true, 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 { if err != nil {
return nil, err 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 { func (s *Store) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error {
err := s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{ err := s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
// ID: user.ID, ID: user.UserId,
// FirstName: user.FirstName, FirstName: user.FirstName.Value,
// LastName: user.LastName, LastName: user.LastName.Value,
// Email: user.Email, Suspended: user.Suspended.Value,
// PhoneNumber: user.PhoneNumber,
}) })
fmt.Printf("Updating User %v with values %v", user.UserId, user)
if err != nil { if err != nil {
return err return err
} }
@ -230,6 +275,22 @@ func (s *Store) UpdateUserCompany(ctx context.Context, id int64, companyID int64
} }
return nil 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 { func (s *Store) DeleteUser(ctx context.Context, id int64) error {
err := s.queries.DeleteUser(ctx, id) err := s.queries.DeleteUser(ctx, id)
if err != nil { if err != nil {

View File

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

View File

@ -13,8 +13,10 @@ type BetStore interface {
GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error)
GetAllBets(ctx context.Context) ([]domain.GetBet, error) GetAllBets(ctx context.Context) ([]domain.GetBet, error)
GetBetByBranchID(ctx context.Context, BranchID int64) ([]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 UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error
UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error
UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error)
DeleteBet(ctx context.Context, id int64) error DeleteBet(ctx context.Context, id int64) error
} }

View File

@ -3,21 +3,56 @@ package bet
import ( import (
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/json"
"errors"
"fmt"
"log/slog"
"math/big" "math/big"
random "math/rand"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "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 { 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{ 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) { func (s *Service) GenerateCashoutID() (string, error) {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789" const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
const length int = 13 const length int = 13
@ -33,8 +68,402 @@ func (s *Service) GenerateCashoutID() (string, error) {
return string(result), nil 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) 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) return s.betStore.UpdateStatus(ctx, id, status)
} }
func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) {
return s.betStore.UpdateBetOutcomeStatus(ctx, id, status) 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 { func (s *Service) DeleteBet(ctx context.Context, id int64) error {

View File

@ -11,7 +11,7 @@ type Service interface {
FetchUpcomingEvents(ctx context.Context) error FetchUpcomingEvents(ctx context.Context) error
GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error)
GetExpiredUpcomingEvents(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) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error)
// GetAndStoreMatchResult(ctx context.Context, eventID string) 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 { func (s *service) FetchUpcomingEvents(ctx context.Context) error {
sportIDs := []int{1, 18} sportIDs := []int{1, 18, 17}
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
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 page = page + 1
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", sportID, s.token, page) url := fmt.Sprintf("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) resp, err := http.Get(url)
if err != nil { if err != nil {
log.Printf("❌ Failed to fetch event data for page %d: %v", page, err) log.Printf("❌ Failed to fetch event data for page %d: %v", page, err)
@ -145,9 +144,10 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
} `json:"results"` } `json:"results"`
} }
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
log.Printf("❌ Failed to parse json data")
continue continue
} }
skippedLeague := 0 var skippedLeague []string
for _, ev := range data.Results { for _, ev := range data.Results {
startUnix, _ := strconv.ParseInt(ev.Time, 10, 64) startUnix, _ := strconv.ParseInt(ev.Time, 10, 64)
// eventID, err := strconv.ParseInt(ev.ID, 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) { 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 continue
} }
event := domain.UpcomingEvent{ event := domain.UpcomingEvent{
ID: ev.ID, ID: ev.ID,
SportID: ev.SportID, SportID: ev.SportID,
MatchName: ev.Home.Name, MatchName: "",
HomeTeam: ev.Home.Name, HomeTeam: ev.Home.Name,
AwayTeam: "", // handle nil safely AwayTeam: "", // handle nil safely
HomeTeamID: ev.Home.ID, HomeTeamID: ev.Home.ID,
@ -186,13 +187,23 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
if ev.Away != nil { if ev.Away != nil {
event.AwayTeam = ev.Away.Name event.AwayTeam = ev.Away.Name
event.AwayTeamID = ev.Away.ID event.AwayTeamID = 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 break
} }
count++ count++
@ -223,8 +234,8 @@ func (s *service) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcomi
return s.store.GetExpiredUpcomingEvents(ctx) 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) { 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) return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset, leagueID, sportID, firstStartTime, lastStartTime)
} }
func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) {

View File

@ -4,7 +4,7 @@ import (
"context" "context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/websocket/v2" "github.com/gorilla/websocket"
) )
type NotificationStore interface { type NotificationStore interface {
@ -16,4 +16,6 @@ type NotificationStore interface {
SendSMS(ctx context.Context, recipientID int64, message string) error SendSMS(ctx context.Context, recipientID int64, message string) error
SendEmail(ctx context.Context, recipientID int64, subject, message string) error SendEmail(ctx context.Context, recipientID int64, subject, message string) error
ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) // New method 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/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws"
afro "github.com/amanuelabay/afrosms-go" afro "github.com/amanuelabay/afrosms-go"
"github.com/gofiber/websocket/v2" "github.com/gorilla/websocket"
) )
type Service struct { type Service struct {
repo repository.NotificationRepository repo repository.NotificationRepository
Hub *ws.NotificationHub
connections sync.Map connections sync.Map
notificationCh chan *domain.Notification notificationCh chan *domain.Notification
stopCh chan struct{} stopCh chan struct{}
@ -24,9 +26,11 @@ type Service struct {
logger *slog.Logger 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{ svc := &Service{
repo: repo, repo: repo,
Hub: hub,
logger: logger, logger: logger,
connections: sync.Map{}, connections: sync.Map{},
notificationCh: make(chan *domain.Notification, 1000), notificationCh: make(chan *domain.Notification, 1000),
@ -34,6 +38,7 @@ func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *confi
config: cfg, config: cfg,
} }
go hub.Run()
go svc.startWorker() go svc.startWorker()
go svc.startRetryWorker() go svc.startRetryWorker()
@ -63,22 +68,48 @@ func (s *Service) SendNotification(ctx context.Context, notification *domain.Not
notification = created notification = created
if notification.DeliveryChannel == domain.DeliveryChannelInApp {
s.Hub.Broadcast <- map[string]interface{}{
"type": "CREATED_NOTIFICATION",
"recipient_id": notification.RecipientID,
"payload": notification,
}
}
select { select {
case s.notificationCh <- notification: case s.notificationCh <- notification:
default: 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 return nil
} }
func (s *Service) MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error { func (s *Service) MarkAsRead(ctx context.Context, notificationIDs []string, recipientID int64) error {
_, err := s.repo.UpdateNotificationStatus(ctx, notificationID, string(domain.DeliveryStatusSent), true, nil) for _, notificationID := range notificationIDs {
if err != nil { _, err := s.repo.UpdateNotificationStatus(ctx, notificationID, string(domain.DeliveryStatusSent), true, nil)
s.logger.Error("[NotificationSvc.MarkAsRead] Failed to mark notification as read", "notificationID", notificationID, "recipientID", recipientID, "error", err) if err != nil {
return err 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 return nil
} }
@ -92,6 +123,16 @@ func (s *Service) ListNotifications(ctx context.Context, recipientID int64, limi
return notifications, nil 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 { func (s *Service) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error {
s.addConnection(ctx, recipientID, c) s.addConnection(ctx, recipientID, c)
s.logger.Info("[NotificationSvc.ConnectWebSocket] WebSocket connection established", "recipientID", recipientID) 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) { func (s *Service) DisconnectWebSocket(recipientID int64) {
s.connections.Delete(recipientID)
if conn, loaded := s.connections.LoadAndDelete(recipientID); loaded { if conn, loaded := s.connections.LoadAndDelete(recipientID); loaded {
conn.(*websocket.Conn).Close() conn.(*websocket.Conn).Close()
s.logger.Info("[NotificationSvc.DisconnectWebSocket] Disconnected WebSocket", "recipientID", recipientID) 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) { func (s *Service) handleNotification(notification *domain.Notification) {
ctx := context.Background() ctx := context.Background()
if conn, ok := s.connections.Load(notification.RecipientID); ok { switch notification.DeliveryChannel {
data, err := notification.ToJSON() case domain.DeliveryChannelSMS:
err := s.SendSMS(ctx, notification.RecipientID, notification.Payload.Message)
if err != nil { 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 notification.DeliveryStatus = domain.DeliveryStatusFailed
} else { } else {
notification.DeliveryStatus = domain.DeliveryStatusSent notification.DeliveryStatus = domain.DeliveryStatusSent
} }
} else { case domain.DeliveryChannelEmail:
s.logger.Warn("[NotificationSvc.HandleNotification] No WebSocket connection for recipient", "recipientID", notification.RecipientID) err := s.SendEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message)
notification.DeliveryStatus = domain.DeliveryStatusFailed 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 { 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) { go func(notification *domain.Notification) {
for attempt := 0; attempt < 3; attempt++ { for attempt := 0; attempt < 3; attempt++ {
time.Sleep(time.Duration(attempt) * time.Second) time.Sleep(time.Duration(attempt) * time.Second)
if conn, ok := s.connections.Load(notification.RecipientID); ok { if notification.DeliveryChannel == domain.DeliveryChannelSMS {
data, err := notification.ToJSON() if err := s.SendSMS(ctx, notification.RecipientID, notification.Payload.Message); err == nil {
if err != nil { notification.DeliveryStatus = domain.DeliveryStatusSent
s.logger.Error("[NotificationSvc.RetryFailedNotifications] Failed to serialize notification for retry", "id", notification.ID, "error", err) if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
continue 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 notification.DeliveryStatus = domain.DeliveryStatusSent
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil { 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.Error("[NotificationSvc.RetryFailedNotifications] Failed to update after retry", "id", notification.ID, "error", err)
@ -230,3 +279,7 @@ func (s *Service) retryFailedNotifications() {
}(notification) }(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 { type Service interface {
FetchNonLiveOdds(ctx context.Context) error FetchNonLiveOdds(ctx context.Context) error
GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, 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) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error)
GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error)
} }

View File

@ -3,26 +3,37 @@ package odds
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt"
"io" "io"
"log" "log"
"log/slog"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
) )
type ServiceImpl struct { 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 { func New(store *repository.Store, cfg *config.Config, logger *slog.Logger) *ServiceImpl {
return &ServiceImpl{token: token, store: store} 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 { func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
eventIDs, err := s.store.GetAllUpcomingEvents(ctx) eventIDs, err := s.store.GetAllUpcomingEvents(ctx)
if err != nil { if err != nil {
@ -30,86 +41,252 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
return err return err
} }
for _, event := range eventIDs { var errs []error
// time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour
eventID := event.ID for index, event := range eventIDs {
prematchURL := "https://api.b365api.com/v3/bet365/prematch?token=" + s.token + "&FI=" + eventID
log.Printf("📡 Fetching prematch odds for event ID: %s", eventID) eventID, err := strconv.ParseInt(event.ID, 10, 64)
resp, err := http.Get(prematchURL)
if err != nil { 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 continue
} }
defer resp.Body.Close() defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
var oddsData struct { if err != nil {
Success int `json:"success"` log.Printf("❌ Failed to read response body for event %d: %v", eventID, err)
Results []struct { continue
EventID string `json:"event_id"`
FI string `json:"FI"`
Main OddsSection `json:"main"`
} `json:"results"`
} }
var oddsData domain.BaseNonLiveOddResponse
if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { 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 continue
} }
result := oddsData.Results[0] sportID, err := strconv.ParseInt(event.SportID, 10, 64)
finalID := result.EventID
if finalID == "" { switch sportID {
finalID = result.FI 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) // result := oddsData.Results[0]
continue
}
s.storeSection(ctx, finalID, result.FI, "main", result.Main)
} }
return nil 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 { if len(section.Sp) == 0 {
return return nil
} }
updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64) updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64)
updatedAt := time.Unix(updatedAtUnix, 0) updatedAt := time.Unix(updatedAtUnix, 0)
var errs []error
for marketType, market := range section.Sp { for marketType, market := range section.Sp {
if len(market.Odds) == 0 { if len(market.Odds) == 0 {
continue 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{ marketRecord := domain.Market{
EventID: eventID, EventID: eventID,
FI: fi, FI: fi,
MarketCategory: sectionName, MarketCategory: sectionName,
MarketType: marketType, MarketType: marketType,
MarketName: market.Name, MarketName: market.Name,
MarketID: market.ID.String(), MarketID: marketIDstr,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
Odds: market.Odds, 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 { if len(errs) > 0 {
ID json.Number `json:"id"` return errors.Join(errs...)
Name string `json:"name"` }
Odds []json.RawMessage `json:"odds"` return nil
Header string `json:"header,omitempty"`
Handicap string `json:"handicap,omitempty"`
}
type OddsSection struct {
UpdatedAt string `json:"updated_at"`
Sp map[string]OddsMarket `json:"sp"`
} }
func (s *ServiceImpl) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { 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 return rows, nil
} }
func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) { func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error) {
return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) 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 // 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) { func evaluateFullTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddName { switch outcome.OddName {
case "1": // Home win case "1": // Home win
@ -27,15 +29,16 @@ func evaluateFullTimeResult(outcome domain.BetOutcome, score struct{ Home, Away
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
default: 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) { func evaluateGoalsOverUnder(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalGoals := float64(score.Home + score.Away) totalGoals := float64(score.Home + score.Away)
threshold, err := strconv.ParseFloat(outcome.OddName, 64) threshold, err := strconv.ParseFloat(outcome.OddName, 64)
if err != nil { if err != nil {
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" { 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_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) { func evaluateCorrectScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
expectedScore := fmt.Sprintf("%d-%d", score.Home, score.Away) expectedScore := fmt.Sprintf("%d-%d", score.Home, score.Away)
if outcome.OddName == expectedScore { if outcome.OddName == expectedScore {
@ -64,6 +68,8 @@ func evaluateCorrectScore(outcome domain.BetOutcome, score struct{ Home, Away in
return domain.OUTCOME_STATUS_LOSS, nil 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) { func evaluateHalfTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
return evaluateFullTimeResult(outcome, score) 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 // 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" // 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) { 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 { switch outcome {
case domain.OUTCOME_STATUS_PENDING: case domain.OUTCOME_STATUS_PENDING:
return secondOutcome, nil return secondOutcome, nil
case domain.OUTCOME_STATUS_WIN: case domain.OUTCOME_STATUS_WIN:
if secondOutcome == domain.OUTCOME_STATUS_WIN { if secondOutcome == domain.OUTCOME_STATUS_WIN {
return domain.OUTCOME_STATUS_WIN, nil 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 { } else if secondOutcome == domain.OUTCOME_STATUS_VOID {
return domain.OUTCOME_STATUS_HALF, nil return domain.OUTCOME_STATUS_HALF, nil
} else { } 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: 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 return domain.OUTCOME_STATUS_LOSS, nil
} else if secondOutcome == domain.OUTCOME_STATUS_VOID { } else if secondOutcome == domain.OUTCOME_STATUS_VOID {
return domain.OUTCOME_STATUS_HALF, nil return domain.OUTCOME_STATUS_VOID, nil
} else { } 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: case domain.OUTCOME_STATUS_VOID:
if secondOutcome == domain.OUTCOME_STATUS_WIN || secondOutcome == domain.OUTCOME_STATUS_LOSS { 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 { } 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: 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) { func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
handicapList := strings.Split(outcome.OddHandicap, ",") handicapList := strings.Split(outcome.OddHandicap, ",")
newOutcome := domain.OUTCOME_STATUS_PENDING newOutcome := domain.OUTCOME_STATUS_PENDING
for _, handicapStr := range handicapList { for _, handicapStr := range handicapList {
handicapStr = strings.TrimSpace(handicapStr)
handicap, err := strconv.ParseFloat(handicapStr, 64) handicap, err := strconv.ParseFloat(handicapStr, 64)
if err != nil { 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) adjustedHomeScore := float64(score.Home)
adjustedAwayScore := float64(score.Away) 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 } else if outcome.OddHeader == "2" { // Away team
adjustedAwayScore += handicap adjustedAwayScore += handicap
} else { } 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 adjustedHomeScore > adjustedAwayScore {
if outcome.OddHeader == "1" { if outcome.OddHeader == "1" {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
if err != nil { if err != nil {
fmt.Printf("multi outcome check error") return domain.OUTCOME_STATUS_ERROR, err
return domain.OUTCOME_STATUS_PENDING, err
} }
continue
} }
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS)
if err != nil { if err != nil {
fmt.Printf("multi outcome check error") return domain.OUTCOME_STATUS_ERROR, err
return domain.OUTCOME_STATUS_PENDING, err
} }
continue
} else if adjustedHomeScore < adjustedAwayScore { } else if adjustedHomeScore < adjustedAwayScore {
if outcome.OddHeader == "2" { if outcome.OddHeader == "2" {
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN)
if err != nil { if err != nil {
fmt.Printf("multi outcome check error") return domain.OUTCOME_STATUS_ERROR, err
return domain.OUTCOME_STATUS_PENDING, err
} }
continue
} }
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS)
if err != nil { if err != nil {
fmt.Printf("multi outcome check error") return domain.OUTCOME_STATUS_ERROR, err
return domain.OUTCOME_STATUS_PENDING, err
} }
} continue
newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID) } else if adjustedHomeScore == adjustedAwayScore {
if err != nil { newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID)
fmt.Printf("multi outcome check error") if err != nil {
return domain.OUTCOME_STATUS_PENDING, err return domain.OUTCOME_STATUS_ERROR, err
}
continue
} }
} }
return newOutcome, nil 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) { 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) { func evaluateFirstTeamToScore(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) {
for _, event := range events { for _, event := range events {
if strings.Contains(event["text"], "1st Goal") || strings.Contains(event["text"], "Goal 1") { 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 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) { func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalGoals := score.Home + score.Away totalGoals := score.Home + score.Away
isOdd := totalGoals%2 == 1 isOdd := totalGoals%2 == 1
@ -184,32 +316,70 @@ func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away in
return domain.OUTCOME_STATUS_LOSS, nil 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) { func evaluateDoubleChance(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
isHomeWin := score.Home > score.Away isHomeWin := score.Home > score.Away
isDraw := score.Home == score.Away isDraw := score.Home == score.Away
isAwayWin := score.Away > score.Home isAwayWin := score.Away > score.Home
switch outcome.OddName { 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 { if isHomeWin || isDraw {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
return domain.OUTCOME_STATUS_LOSS, 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 { if isDraw || isAwayWin {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
return domain.OUTCOME_STATUS_LOSS, 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 { if isHomeWin || isAwayWin {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
default: 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) { func evaluateDrawNoBet(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
if score.Home == score.Away { if score.Home == score.Away {
return domain.OUTCOME_STATUS_VOID, nil 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 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) { func evaluateGameLines(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddName { switch outcome.OddName {
case "Money Line": case "Money Line":
@ -235,10 +434,11 @@ func evaluateGameLines(outcome domain.BetOutcome, score struct{ Home, Away int }
case "Total": case "Total":
return evaluateTotalOverUnder(outcome, score) return evaluateTotalOverUnder(outcome, score)
default: 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) { func evaluateMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddHeader { switch outcome.OddHeader {
case "1": case "1":
@ -258,21 +458,22 @@ func evaluateMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int }
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
default: 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) { 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}" // The handicap will be in the format "U {float}" or "O {float}"
// U and O denoting over and under for this case // U and O denoting over and under for this case
overUnderStr := strings.Split(outcome.OddHandicap, " ") overUnderStr := strings.Split(outcome.OddHandicap, " ")
if len(overUnderStr) != 2 { 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) threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
if err != nil { 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 // 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_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) { 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}" // The handicap will be in the format "U {float}" or "O {float}"
// U and O denoting over and under for this case // U and O denoting over and under for this case
overUnderStr := strings.Split(outcome.OddHandicap, " ") overUnderStr := strings.Split(outcome.OddHandicap, " ")
if len(overUnderStr) != 2 { 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] overUnder := overUnderStr[0]
if overUnder != "Over" && overUnder != "Under" { 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) threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
if err != nil { 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 // 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 { switch outcome.OddHeader {
case "1": case "1":
if score.Home < score.Away {
return domain.OUTCOME_STATUS_LOSS, nil
}
if overUnder == "Over" && totalScore > threshold { if overUnder == "Over" && totalScore > threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} else if overUnder == "Under" && totalScore < threshold { } 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 return domain.OUTCOME_STATUS_LOSS, nil
case "2": case "2":
if score.Away < score.Home {
return domain.OUTCOME_STATUS_LOSS, nil
}
if overUnder == "Over" && totalScore > threshold { if overUnder == "Over" && totalScore > threshold {
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} else if overUnder == "Under" && totalScore < threshold { } 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 return domain.OUTCOME_STATUS_LOSS, nil
default: 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) { 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}" // The handicap will be in the format "U {float}" or "O {float}"
// U and O denoting over and under for this case // U and O denoting over and under for this case
overUnderStr := strings.Split(outcome.OddHandicap, " ") overUnderStr := strings.Split(outcome.OddHandicap, " ")
if len(overUnderStr) != 2 { 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] overUnder := overUnderStr[0]
if overUnder != "Over" && overUnder != "Under" { 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) threshold, err := strconv.ParseFloat(overUnderStr[1], 64)
if err != nil { 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 // 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 return domain.OUTCOME_STATUS_LOSS, nil
default: 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) { 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}" // 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" { } else if scoreCheckSplit == "No" {
isScorePoints = false isScorePoints = false
} else { } 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], "")) teamName := strings.TrimSpace(strings.Join(oddNameSplit[:len(oddNameSplit)-2], ""))
threshold, err := strconv.ParseInt(outcome.OddHeader, 10, 64) threshold, err := strconv.ParseInt(outcome.OddHeader, 10, 64)
if err != nil { 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 { switch teamName {
@ -428,18 +641,18 @@ func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away
} }
} }
default: 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 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) { func evaluateBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
threshold, err := strconv.ParseInt(outcome.OddName, 10, 64) threshold, err := strconv.ParseInt(outcome.OddName, 10, 64)
if err != nil { 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 { switch outcome.OddHeader {
@ -453,12 +666,13 @@ func evaluateBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (d
} }
default: 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 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) { func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
switch outcome.OddName { switch outcome.OddName {
case "1": // Home win case "1": // Home win
@ -477,23 +691,24 @@ func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away i
} }
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
default: 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) { func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home, Away int }, secondHalfScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
halfWins := strings.Split(outcome.OddName, "-") halfWins := strings.Split(outcome.OddName, "-")
if len(halfWins) != 2 { 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]) firstHalfWinner := strings.TrimSpace(halfWins[0])
secondHalfWinner := strings.TrimSpace(halfWins[1]) secondHalfWinner := strings.TrimSpace(halfWins[1])
if firstHalfWinner != outcome.HomeTeamName && firstHalfWinner != outcome.AwayTeamName && firstHalfWinner != "Tie" { 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" { 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 { switch {
@ -517,6 +732,7 @@ func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home
return domain.OUTCOME_STATUS_WIN, nil 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) { func evaluateHighestScoringHalf(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }) (domain.OutcomeStatus, error) {
firstHalfTotal := firstScore.Home + firstScore.Away firstHalfTotal := firstScore.Home + firstScore.Away
secondHalfTotal := secondScore.Home + secondScore.Away secondHalfTotal := secondScore.Home + secondScore.Away
@ -534,11 +750,12 @@ func evaluateHighestScoringHalf(outcome domain.BetOutcome, firstScore struct{ Ho
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
default: 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 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) { 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 firstQuarterTotal := firstScore.Home + firstScore.Away
secondQuarterTotal := secondScore.Home + secondScore.Away secondQuarterTotal := secondScore.Home + secondScore.Away
@ -567,18 +784,44 @@ func evaluateHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
default: 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 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) { func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
nameSplit := strings.Split(outcome.OddName, " ") nameSplit := strings.Split(outcome.OddName, " ")
// Evaluate from bottom to get the threshold and find out if its over or under // Evaluate from bottom to get the threshold and find out if its over or under
threshold, err := strconv.ParseFloat(nameSplit[len(nameSplit)-1], 10) threshold, err := strconv.ParseFloat(nameSplit[len(nameSplit)-1], 10)
if err != nil { 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) total := float64(score.Home + score.Away)
overUnder := nameSplit[len(nameSplit)-2] overUnder := nameSplit[len(nameSplit)-2]
@ -591,12 +834,12 @@ func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Awa
return domain.OUTCOME_STATUS_LOSS, nil return domain.OUTCOME_STATUS_LOSS, nil
} }
} else { } 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) handicap, err := strconv.ParseFloat(nameSplit[len(nameSplit)-4], 10)
if err != nil { 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], "")) 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 return domain.OUTCOME_STATUS_LOSS, nil
default: 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) { func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) {
marginSplit := strings.Split(outcome.OddName, "") marginSplit := strings.Split(outcome.OddName, "")
if len(marginSplit) < 1 { 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) margin, err := strconv.ParseInt(marginSplit[0], 10, 64)
if err != nil { 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 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_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) { 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 firstPeriodTotal := firstScore.Home + firstScore.Away
secondPeriodTotal := secondScore.Home + secondScore.Away secondPeriodTotal := secondScore.Home + secondScore.Away
@ -682,11 +927,12 @@ func evaluateHighestScoringPeriod(outcome domain.BetOutcome, firstScore struct{
return domain.OUTCOME_STATUS_WIN, nil return domain.OUTCOME_STATUS_WIN, nil
} }
default: 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 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) { func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Home, Away int }) (domain.OutcomeStatus, error) {
totalScore := struct{ Home, Away int }{0, 0} totalScore := struct{ Home, Away int }{0, 0}
for _, score := range scores { 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_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/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
) )
type Service struct { type Service struct {
@ -20,19 +21,22 @@ type Service struct {
config *config.Config config *config.Config
logger *slog.Logger logger *slog.Logger
client *http.Client 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{ return &Service{
repo: repo, repo: repo,
config: cfg, config: cfg,
logger: logger, logger: logger,
client: &http.Client{Timeout: 10 * time.Second}, 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 { func (s *Service) FetchAndProcessResults(ctx context.Context) error {
// TODO: Optimize this because there could be many bet outcomes for the same odd // 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") s.logger.Error("Failed to fetch events")
return err 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) eventID, err := strconv.ParseInt(event.ID, 10, 64)
if err != nil { if err != nil {
s.logger.Error("Failed to parse event id") s.logger.Error("Failed to parse event id")
@ -56,48 +61,102 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
return err 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()) { 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 continue
} }
sportID, err := strconv.ParseInt(event.SportID, 10, 64) sportID, err := strconv.ParseInt(event.SportID, 10, 64)
if err != nil { if err != nil {
s.logger.Error("Sport ID is invalid", "event_id", outcome.EventID, "error", err) s.logger.Error("Sport ID is invalid", "event_id", outcome.EventID, "error", err)
isDeleted = false
continue 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) result, err := s.fetchResult(ctx, outcome.EventID, outcome.OddID, outcome.MarketID, sportID, outcome)
if err != nil { 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 continue
} }
// _, err = s.repo.CreateResult(ctx, domain.CreateResult{ outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status)
// 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)
if err != nil { if err != nil {
isDeleted = false
s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err) s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
continue 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 isDeleted {
if err != nil { removed += 1
s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err) fmt.Printf("⚠️ Removing Event %v \n", event.ID)
return err 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 return nil
} }
@ -202,29 +261,51 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, spo
switch sportID { switch sportID {
case domain.FOOTBALL: case domain.FOOTBALL:
result, err = s.parseFootball(resultResp.Results[0], eventID, oddID, marketID, outcome) 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: case domain.BASKETBALL:
result, err = s.parseBasketball(resultResp.Results[0], eventID, oddID, marketID, outcome) 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: case domain.ICE_HOCKEY:
result, err = s.parseIceHockey(resultResp.Results[0], eventID, oddID, marketID, outcome) result, err = s.parseIceHockey(resultResp.Results[0], eventID, oddID, marketID, outcome)
if err != nil { case domain.AMERICAN_FOOTBALL:
s.logger.Error("Failed to parse ice hockey", "event id", eventID, "market_id", marketID, "error", err) result, err = s.parseNFL(resultResp.Results[0], eventID, oddID, marketID, outcome)
return domain.CreateResult{}, err 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: default:
s.logger.Error("Unsupported sport", "sport", sportID) s.logger.Error("Unsupported sport", "sport", sportID)
return domain.CreateResult{}, fmt.Errorf("unsupported sport: %v", sportID) return domain.CreateResult{}, fmt.Errorf("unsupported sport: %v", sportID)
} }
return result, nil 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 return domain.CreateResult{}, err
} }
result := fbResp result := fbResp
if result.TimeStatus != "3" {
s.logger.Warn("Match not yet completed", "event_id", eventID) isEventActive, err := s.parseTimeStatus(result.TimeStatus)
return domain.CreateResult{}, fmt.Errorf("match not yet completed") 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) 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) 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 { 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 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) { func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) {
var basketBallRes domain.BasketballResultResponse var basketBallRes domain.BasketballResultResponse
if err := json.Unmarshal(response, &basketBallRes); err != nil { 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 return domain.CreateResult{}, err
} }
if basketBallRes.TimeStatus != "3" { isEventActive, err := s.parseTimeStatus(basketBallRes.TimeStatus)
s.logger.Warn("Match not yet completed", "event_id", eventID) if err != nil {
return domain.CreateResult{}, fmt.Errorf("match not yet completed") 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) 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) { func (s *Service) parseIceHockey(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) {
var iceHockeyRes domain.IceHockeyResultResponse var iceHockeyRes domain.IceHockeyResultResponse
if err := json.Unmarshal(response, &iceHockeyRes); err != nil { 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 return domain.CreateResult{}, err
} }
if iceHockeyRes.TimeStatus != "3" { isEventActive, err := s.parseTimeStatus(iceHockeyRes.TimeStatus)
s.logger.Warn("Match not yet completed", "event_id", eventID) if err != nil {
return domain.CreateResult{}, fmt.Errorf("match not yet completed") 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) 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 } { func parseScore(home string, away string) struct{ Home, Away int } {
homeVal, _ := strconv.Atoi(strings.TrimSpace(home)) homeVal, _ := strconv.Atoi(strings.TrimSpace(home))
awaVal, _ := strconv.Atoi(strings.TrimSpace(away)) 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 // 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] { if !domain.SupportedMarkets[outcome.MarketID] {
s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) 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) return evaluateDoubleChance(outcome, finalScore)
case int64(domain.FOOTBALL_DRAW_NO_BET): case int64(domain.FOOTBALL_DRAW_NO_BET):
return evaluateDrawNoBet(outcome, finalScore) 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: default:
s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName) 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) 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): case int64(domain.BASKETBALL_GAME_TOTAL_ODD_EVEN):
return evaluateGoalsOddEven(outcome, finalScore) return evaluateGoalsOddEven(outcome, finalScore)
case int64(domain.BASKETBALL_TEAM_TOTALS): 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): case int64(domain.BASKETBALL_FIRST_HALF):
return evaluateGameLines(outcome, firstHalfScore) return evaluateGameLines(outcome, firstHalfScore)
@ -442,6 +679,11 @@ func (s *Service) evaluateBasketballOutcome(outcome domain.BetOutcome, res domai
return evaluateDoubleChance(outcome, firstQuarter) return evaluateDoubleChance(outcome, firstQuarter)
case int64(domain.BASKETBALL_HIGHEST_SCORING_QUARTER): case int64(domain.BASKETBALL_HIGHEST_SCORING_QUARTER):
return evaluateHighestScoringQuarter(outcome, firstQuarter, secondQuarter, thirdQuarter, fourthQuarter) 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: default:
s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName) 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 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 { func (s *Service) DeleteTicket(ctx context.Context, id int64) error {
return s.ticketStore.DeleteTicket(ctx, id) 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 { type Filter struct {
Role string Role string
CompanyID domain.ValidInt64 CompanyID domain.ValidInt64
Page int Page domain.ValidInt
PageSize int PageSize domain.ValidInt
} }
type ValidRole struct { type ValidRole struct {
Value domain.Role Value domain.Role
@ -71,6 +71,10 @@ func (s *Service) GetCashiersByBranch(ctx context.Context, branchID int64) ([]do
return s.userStore.GetCashiersByBranch(ctx, branchID) 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) 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) CreateUserWithoutOtp(ctx context.Context, user domain.User, is_company bool) (domain.User, error)
GetUserByID(ctx context.Context, id int64) (domain.User, error) GetUserByID(ctx context.Context, id int64) (domain.User, error)
GetAllUsers(ctx context.Context, filter Filter) ([]domain.User, int64, 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) GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error)
UpdateUser(ctx context.Context, user domain.UpdateUserReq) error UpdateUser(ctx context.Context, user domain.UpdateUserReq) error
UpdateUserCompany(ctx context.Context, id int64, companyID int64) 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 DeleteUser(ctx context.Context, id int64) error
CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error)
GetUserByEmail(ctx context.Context, email string) (domain.User, error) GetUserByEmail(ctx context.Context, email string) (domain.User, error)
GetUserByPhone(ctx context.Context, phoneNum 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 UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error // identifier verified email or phone
} }
type SmsGateway interface { type SmsGateway interface {

View File

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

View File

@ -6,9 +6,9 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "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 // 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 { 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 { func (s *Service) UpdateUserCompany(ctx context.Context, id int64, companyID int64) error {
// update user // update user
return s.userStore.UpdateUserCompany(ctx, id, companyID) 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) { func (s *Service) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
return s.userStore.GetUserByID(ctx, id) return s.userStore.GetUserByID(ctx, id)

View File

@ -12,7 +12,7 @@ type WalletStore interface {
GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error) GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error)
GetAllWallets(ctx context.Context) ([]domain.Wallet, error) GetAllWallets(ctx context.Context) ([]domain.Wallet, error)
GetWalletsByUser(ctx context.Context, id int64) ([]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) GetAllBranchWallets(ctx context.Context) ([]domain.BranchWallet, error)
UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error
UpdateWalletActive(ctx context.Context, id int64, isActive bool) error UpdateWalletActive(ctx context.Context, id int64, isActive bool) error

View File

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

View File

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

View File

@ -1,8 +1,8 @@
package httpserver package httpserver
import ( import (
// "context"
"context" "context"
"log" "log"
// "time" // "time"
@ -10,6 +10,7 @@ import (
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
) )
@ -20,53 +21,24 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
spec string spec string
task func() 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 // spec: "0 0 * * * *", // Every 1 hour
// 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 * * * *",
// task: func() { // task: func() {
// log.Println("Fetching results for upcoming events...") // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
// log.Printf("FetchUpcomingEvents error: %v", err)
// upcomingEvents, err := eventService.GetAllUpcomingEvents(context.Background())
// if err != nil {
// log.Printf("Failed to fetch upcoming events: %v", err)
// return
// } // }
// },
// 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) // spec: "0 */15 * * * *", // Every 15 minutes
// } else { // task: func() {
// log.Printf(" Successfully stored result for event %s", event.ID) // 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() { task: func() {
log.Println("Fetching results for upcoming events...") log.Println("Fetching results for upcoming events...")
@ -80,6 +52,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
} }
for _, job := range schedule { for _, job := range schedule {
job.task()
if _, err := c.AddFunc(job.spec, job.task); err != nil { if _, err := c.AddFunc(job.spec, job.task); err != nil {
log.Fatalf("Failed to schedule cron job: %v", err) log.Fatalf("Failed to schedule cron job: %v", err)
} }
@ -88,3 +61,33 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
c.Start() c.Start()
log.Println("Cron jobs started for event and odds services") 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 ( import (
"log/slog" "log/slog"
"strconv"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -124,14 +125,21 @@ type AdminRes struct {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /admin [get] // @Router /admin [get]
func (h *Handler) GetAllAdmins(c *fiber.Ctx) error { func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
filter := user.Filter{ filter := user.Filter{
Role: string(domain.RoleAdmin), Role: string(domain.RoleAdmin),
CompanyID: domain.ValidInt64{ CompanyID: domain.ValidInt64{
Value: int64(c.QueryInt("company_id")), 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, Valid: true,
}, },
Page: c.QueryInt("page", 1) - 1,
PageSize: c.QueryInt("page_size", 10),
} }
valErrs, ok := h.validator.Validate(c, filter) valErrs, ok := h.validator.Validate(c, filter)
if !ok { 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 package handlers
import ( import (
"encoding/json"
"log/slog"
"strconv" "strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "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/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2" "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 // CreateBet godoc
// @Summary Create a bet // @Summary Create a bet
// @Description Creates a bet // @Description Creates a bet
// @Tags bet // @Tags bet
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param createBet body CreateBetReq true "Creates bet" // @Param createBet body domain.CreateBetReq true "Creates bet"
// @Success 200 {object} BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [post] // @Router /bet [post]
@ -102,7 +28,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64) userID := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role) role := c.Locals("role").(domain.Role)
var req CreateBetReq var req domain.CreateBetReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse CreateBet request", "error", err) h.logger.Error("Failed to parse CreateBet request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") 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) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
} }
// TODO Validate Outcomes Here and make sure they didn't expire res, err := h.betSvc.PlaceBet(c.Context(), req, userID, role)
// Validation for creating tickets
if len(req.Outcomes) > 30 { if err != nil {
return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, 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 return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil)
for _, outcome := range req.Outcomes {
eventIDStr := strconv.FormatInt(outcome.EventID, 10) }
marketIDStr := strconv.FormatInt(outcome.MarketID, 10)
oddIDStr := strconv.FormatInt(outcome.OddID, 10) // RandomBet godoc
event, err := h.eventSvc.GetUpcomingEventByID(c.Context(), eventIDStr) // @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 { 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 var req domain.RandomBetReq
// currentTime := time.Now() if err := c.BodyParser(&req); err != nil {
// if event.StartTime.Before(currentTime) { h.logger.Error("Failed to parse RandomBet request", "error", err)
// return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) 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 { if err != nil {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil) h.logger.Error("Random Bet failed", "error", err)
} switch err {
type rawOddType struct { case bet.ErrNoEventsAvailable:
ID string return fiber.NewError(fiber.StatusBadRequest, "No events found")
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
} }
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) return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil)
} }
@ -316,7 +145,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
// @Tags bet // @Tags bet
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {array} BetRes // @Success 200 {array} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet [get] // @Router /bet [get]
@ -327,9 +156,9 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets") 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 { 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) 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 // @Accept json
// @Produce json // @Produce json
// @Param id path int true "Bet ID" // @Param id path int true "Bet ID"
// @Success 200 {object} BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/{id} [get] // @Router /bet/{id} [get]
@ -356,11 +185,12 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error {
bet, err := h.betSvc.GetBetByID(c.Context(), id) bet, err := h.betSvc.GetBetByID(c.Context(), id)
if err != nil { if err != nil {
// TODO: handle all the errors types
h.logger.Error("Failed to get bet by ID", "betID", id, "error", err) 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) 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 // @Accept json
// @Produce json // @Produce json
// @Param id path string true "cashout ID" // @Param id path string true "cashout ID"
// @Success 200 {object} BetRes // @Success 200 {object} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /bet/cashout/{id} [get] // @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) 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) return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil)

View File

@ -4,6 +4,7 @@ import (
"strconv" "strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "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/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@ -141,7 +142,8 @@ func (h *Handler) CreateBranch(c *fiber.Ctx) error {
checkedCompanyID = *req.CompanyID checkedCompanyID = *req.CompanyID
} else { } else {
IsSelfOwned = false 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 // 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) 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 // GetBetByBranchID godoc
// @Summary Gets bets by its branch id // @Summary Gets bets by its branch id
// @Description Gets bets by its branch id // @Description Gets bets by its branch id
// @Tags branch // @Tags branch
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {array} BetRes // @Success 200 {array} domain.BetRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /branch/{id}/bets [get] // @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) 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 { 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) 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/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "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/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@ -87,6 +88,7 @@ type GetCashierRes struct {
SuspendedAt time.Time `json:"suspended_at"` SuspendedAt time.Time `json:"suspended_at"`
Suspended bool `json:"suspended"` Suspended bool `json:"suspended"`
LastLogin time.Time `json:"last_login"` LastLogin time.Time `json:"last_login"`
BranchID int64 `json:"branch_id"`
} }
// GetAllCashiers godoc // GetAllCashiers godoc
@ -103,22 +105,31 @@ type GetCashierRes struct {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /cashiers [get] // @Router /cashiers [get]
func (h *Handler) GetAllCashiers(c *fiber.Ctx) error { func (h *Handler) GetAllCashiers(c *fiber.Ctx) error {
// branchId := int64(12) //c.Locals("branch_id").(int64) role := c.Locals("role").(domain.Role)
// filter := user.Filter{ companyId := c.Locals("company_id").(domain.ValidInt64)
// 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)
// }
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 { if err != nil {
h.logger.Error("GetAllCashiers failed", "error", err) h.logger.Error("GetAllCashiers failed", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil) 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"` FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Doe"` LastName string `json:"last_name" example:"Doe"`
Suspended bool `json:"suspended" example:"false"` Suspended bool `json:"suspended" example:"false"`
@ -171,7 +251,7 @@ type updateUserReq struct {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path int true "Cashier ID" // @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 // @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 401 {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) h.logger.Error("UpdateCashier failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashier ID", nil, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashier ID", nil, nil)
} }
var req updateUserReq var req updateCashierReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.logger.Error("UpdateCashier failed", "error", err) h.logger.Error("UpdateCashier failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)

View File

@ -25,12 +25,15 @@ type CompanyRes struct {
} }
type GetCompanyRes struct { type GetCompanyRes struct {
ID int64 `json:"id" example:"1"` ID int64 `json:"id" example:"1"`
Name string `json:"name" example:"CompanyName"` Name string `json:"name" example:"CompanyName"`
AdminID int64 `json:"admin_id" example:"1"` AdminID int64 `json:"admin_id" example:"1"`
WalletID int64 `json:"wallet_id" example:"1"` WalletID int64 `json:"wallet_id" example:"1"`
WalletBalance float32 `json:"balance" example:"1"` WalletBalance float32 `json:"balance" example:"1"`
IsActive bool `json:"is_active" example:"false"` 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 { func convertCompany(company domain.Company) CompanyRes {
@ -44,12 +47,15 @@ func convertCompany(company domain.Company) CompanyRes {
func convertGetCompany(company domain.GetCompany) GetCompanyRes { func convertGetCompany(company domain.GetCompany) GetCompanyRes {
return GetCompanyRes{ return GetCompanyRes{
ID: company.ID, ID: company.ID,
Name: company.Name, Name: company.Name,
AdminID: company.AdminID, AdminID: company.AdminID,
WalletID: company.WalletID, WalletID: company.WalletID,
WalletBalance: company.WalletBalance.Float32(), WalletBalance: company.WalletBalance.Float32(),
IsActive: company.IsWalletActive, 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 var req UpdateCompanyReq
if err := c.BodyParser(&req); err != nil { 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) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil)
} }
valErrs, ok := h.validator.Validate(c, req) valErrs, ok := h.validator.Validate(c, req)

View File

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

View File

@ -109,14 +109,23 @@ type ManagersRes struct {
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /managers [get] // @Router /managers [get]
func (h *Handler) GetAllManagers(c *fiber.Ctx) error { 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{ filter := user.Filter{
Role: string(domain.RoleBranchManager), Role: string(domain.RoleBranchManager),
CompanyID: domain.ValidInt64{ CompanyID: companyId,
Value: int64(c.QueryInt("company_id")), Page: domain.ValidInt{
Value: c.QueryInt("page", 1) - 1,
Valid: true,
},
PageSize: domain.ValidInt{
Value: c.QueryInt("page_size", 10),
Valid: true, Valid: true,
}, },
Page: c.QueryInt("page", 1) - 1,
PageSize: c.QueryInt("page_size", 10),
} }
valErrs, ok := h.validator.Validate(c, filter) valErrs, ok := h.validator.Validate(c, filter)
if !ok { 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 // UpdateManagers godoc
// @Summary Update Managers // @Summary Update Managers
// @Description Update Managers // @Description Update Managers
// @Tags Managers // @Tags manager
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param Managers body updateUserReq true "Update Managers" // @Param Managers body updateManagerReq true "Update Managers"
// @Success 200 {object} response.APIResponse // @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /managers/{id} [put] // @Router /managers/{id} [put]
func (h *Handler) UpdateManagers(c *fiber.Ctx) error { func (h *Handler) UpdateManagers(c *fiber.Ctx) error {
var req updateUserReq var req updateManagerReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.logger.Error("UpdateManagers failed", "error", err) h.logger.Error("UpdateManagers failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) 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) h.logger.Error("UpdateManagers failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid Managers ID", nil, nil) 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{ err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{
UserId: ManagersId, UserId: ManagersId,
FirstName: domain.ValidString{ FirstName: domain.ValidString{
@ -204,6 +303,7 @@ func (h *Handler) UpdateManagers(c *fiber.Ctx) error {
Value: req.Suspended, Value: req.Suspended,
Valid: true, Valid: true,
}, },
CompanyID: companyID,
}, },
) )
if err != nil { if err != nil {

View File

@ -3,58 +3,107 @@ package handlers
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "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/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 { func hijackHTTP(c *fiber.Ctx) (net.Conn, http.ResponseWriter, error) {
if !websocket.IsWebSocketUpgrade(c) { var rw http.ResponseWriter
h.logger.Warn("WebSocket upgrade required") var conn net.Conn
return fiber.ErrUpgradeRequired
}
// 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) userID, ok := c.Locals("userID").(int64)
if !ok || userID == 0 { if !ok || userID == 0 {
h.logger.Error("Invalid user ID in context") 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) { // Create a net.Conn hijacked from the fasthttp context
ctx := context.Background() netConn, rw, err := hijackHTTP(c)
logger := h.logger.With("userID", userID, "remoteAddr", conn.RemoteAddr()) 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 { // Upgrade the connection using Gorilla's Upgrader
logger.Error("Failed to connect WebSocket", "error", err) conn, err := ws.Upgrader.Upgrade(rw, req, nil)
_ = conn.Close() if err != nil {
return 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.Hub.Register <- client
h.notificationSvc.DisconnectWebSocket(userID) h.logger.Info("WebSocket connection established", "userID", userID)
logger.Info("WebSocket connection closed")
_ = conn.Close()
}()
for { defer func() {
if _, _, err := conn.ReadMessage(); err != nil { h.notificationSvc.Hub.Unregister <- client
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { h.logger.Info("WebSocket connection closed", "userID", userID)
logger.Warn("WebSocket unexpected close", "error", err) conn.Close()
} }()
break
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 { func (h *Handler) MarkNotificationAsRead(c *fiber.Ctx) error {
type Request struct { type Request struct {
NotificationID string `json:"notification_id" validate:"required"` NotificationIDs []string `json:"notification_ids" validate:"required"`
} }
var req Request var req Request
@ -63,14 +112,15 @@ func (h *Handler) MarkNotificationAsRead(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") 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 { if !ok || userID == 0 {
h.logger.Error("Invalid user ID in context") h.logger.Error("Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "invalid user identification") return fiber.NewError(fiber.StatusUnauthorized, "invalid user identification")
} }
if err := h.notificationSvc.MarkAsRead(context.Background(), req.NotificationID, userID); err != nil { fmt.Printf("Notification IDs: %v \n", req.NotificationIDs)
h.logger.Error("Failed to mark notification as read", "notificationID", req.NotificationID, "error", err) 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") 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") return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
} }
userID, ok := c.Locals("userID").(int64) // userID, ok := c.Locals("userID").(int64)
if !ok || userID == 0 { // if !ok || userID == 0 {
h.logger.Error("[NotificationSvc.CreateAndSendNotification] Invalid user ID in context") // h.logger.Error("[NotificationSvc.CreateAndSendNotification] Invalid user ID in context")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") // return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
} // }
switch req.DeliveryScheme { switch req.DeliveryScheme {
case domain.NotificationDeliverySchemeSingle: case domain.NotificationDeliverySchemeSingle:
if req.Reciever == domain.NotificationRecieverSideCustomer && req.RecipientID != userID { // if req.Reciever == domain.NotificationRecieverSideCustomer {
h.logger.Warn("[NotificationSvc.CreateAndSendNotification] Unauthorized attempt to send notification", "userID", userID, "recipientID", req.RecipientID) // 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") // return fiber.NewError(fiber.StatusForbidden, "Unauthorized to send notification to this recipient")
} // }
notification := &domain.Notification{ notification := &domain.Notification{
ID: "", 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}) return c.Status(fiber.StatusCreated).JSON(fiber.Map{"message": "Single notification sent successfully", "notification_id": notification.ID})
case domain.NotificationDeliverySchemeBulk: 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 { if err != nil {
h.logger.Error("[NotificationSvc.CreateAndSendNotification] Failed to fetch recipients for bulk notification", "error", err) h.logger.Error("[NotificationSvc.CreateAndSendNotification] Failed to fetch recipients for bulk notification", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch recipients") return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch recipients")
} }
fmt.Printf("Number of Recipients %d \n", len(recipients))
notificationIDs := make([]string, 0, len(recipients)) notificationIDs := make([]string, 0, len(recipients))
for _, recipientID := range recipients { for _, user := range recipients {
notification := &domain.Notification{ notification := &domain.Notification{
ID: "", ID: "",
RecipientID: recipientID, RecipientID: user.ID,
Type: req.Type, Type: req.Type,
Level: req.Level, Level: req.Level,
ErrorSeverity: req.ErrorSeverity, 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 { 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 continue
} }
notificationIDs = append(notificationIDs, notification.ID) 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) 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 ( import (
"strconv" "strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "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 page_size query int false "Page size"
// @Param league_id query string false "League ID Filter" // @Param league_id query string false "League ID Filter"
// @Param sport_id query string false "Sport 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 // @Success 200 {array} domain.UpcomingEvent
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /prematch/events [get] // @Router /prematch/events [get]
@ -106,6 +109,8 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
pageSize := c.QueryInt("page_size", 10) pageSize := c.QueryInt("page_size", 10)
leagueIDQuery := c.Query("league_id") leagueIDQuery := c.Query("league_id")
sportIDQuery := c.Query("sport_id") sportIDQuery := c.Query("sport_id")
firstStartTimeQuery := c.Query("first_start_time")
lastStartTimeQuery := c.Query("last_start_time")
leagueID := domain.ValidString{ leagueID := domain.ValidString{
Value: leagueIDQuery, Value: leagueIDQuery,
@ -116,7 +121,42 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
Valid: sportIDQuery != "", 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) // fmt.Printf("League ID: %v", leagueID)
if err != nil { 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) 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 { if err != nil {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve prematch odds", nil, nil) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve prematch odds", nil, nil)
} }

View File

@ -3,6 +3,7 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"strconv" "strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "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 // Checking to make sure the event hasn't already started
// currentTime := time.Now() currentTime := time.Now()
// if event.StartTime.Before(currentTime) { if event.StartTime.Before(currentTime) {
// return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil)
// } }
odds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr) 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) ticket, err := h.ticketSvc.GetTicketByID(c.Context(), id)
if err != nil { 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") 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) medium, err := getMedium(req.Email, req.PhoneNumber)
if err != nil { if err != nil {
h.logger.Error("RegisterUser failed", "error", err) h.logger.Error("RegisterUser failed", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return fiber.NewError(fiber.StatusBadRequest, err.Error())
"error": err.Error(),
})
} }
user.OtpMedium = medium user.OtpMedium = medium
@ -160,24 +158,22 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
newUser, err := h.userSvc.RegisterUser(c.Context(), user) newUser, err := h.userSvc.RegisterUser(c.Context(), user)
if err != nil { if err != nil {
if errors.Is(err, domain.ErrOtpAlreadyUsed) { 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) { 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) { 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) { 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) h.logger.Error("RegisterUser failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return fiber.NewError(fiber.StatusInternalServerError, "Unknown Error")
"error": "Internal server error",
})
} }
_, err = h.walletSvc.CreateWallet(c.Context(), domain.CreateWallet{ newWallet, err := h.walletSvc.CreateWallet(c.Context(), domain.CreateWallet{
UserID: newUser.ID, UserID: newUser.ID,
IsWithdraw: true, IsWithdraw: true,
IsBettable: 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) 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 { type SearchUserByNameOrPhoneReq struct {
SearchString string SearchString string `json:"query"`
Role *domain.Role `json:"role,omitempty"`
} }
// SearchUserByNameOrPhone godoc // SearchUserByNameOrPhone godoc
@ -405,7 +410,8 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
return 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 { if err != nil {
h.logger.Error("SearchUserByNameOrPhone failed", "error", err) h.logger.Error("SearchUserByNameOrPhone failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
@ -450,7 +456,7 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param id path int true "User ID" // @Param id path int true "User ID"
// @Success 200 {object} response.APIResponse // @Success 200 {object} UserProfileRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
@ -474,13 +480,13 @@ func (h *Handler) GetUserByID(c *fiber.Ctx) error {
userIDstr := c.Params("id") userIDstr := c.Params("id")
userID, err := strconv.ParseInt(userIDstr, 10, 64) userID, err := strconv.ParseInt(userIDstr, 10, 64)
if err != nil { 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) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashier ID", nil, nil)
} }
user, err := h.userSvc.GetUserByID(c.Context(), userID) user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil { 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) 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, 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"` StaticID int64 `json:"static_id" example:"1"`
StaticBalance float32 `json:"static_balance" example:"100.0"` StaticBalance float32 `json:"static_balance" example:"100.0"`
CustomerID int64 `json:"customer_id" example:"1"` CustomerID int64 `json:"customer_id" example:"1"`
CompanyID int64 `json:"company_id" example:"1"`
RegularUpdatedAt time.Time `json:"regular_updated_at"` RegularUpdatedAt time.Time `json:"regular_updated_at"`
StaticUpdatedAt time.Time `json:"static_updated_at"` StaticUpdatedAt time.Time `json:"static_updated_at"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }
func convertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes { func ConvertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes {
return CustomerWalletRes{ return CustomerWalletRes{
ID: wallet.ID, ID: wallet.ID,
RegularID: wallet.RegularID, RegularID: wallet.RegularID,
@ -59,7 +58,6 @@ func convertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes {
StaticID: wallet.StaticID, StaticID: wallet.StaticID,
StaticBalance: wallet.StaticBalance.Float32(), StaticBalance: wallet.StaticBalance.Float32(),
CustomerID: wallet.CustomerID, CustomerID: wallet.CustomerID,
CompanyID: wallet.CompanyID,
RegularUpdatedAt: wallet.RegularUpdatedAt, RegularUpdatedAt: wallet.RegularUpdatedAt,
StaticUpdatedAt: wallet.StaticUpdatedAt, StaticUpdatedAt: wallet.StaticUpdatedAt,
CreatedAt: wallet.CreatedAt, CreatedAt: wallet.CreatedAt,
@ -249,21 +247,21 @@ func (h *Handler) GetCustomerWallet(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized access") return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized access")
} }
companyID, err := strconv.ParseInt(c.Get("company_id"), 10, 64) // companyID, err := strconv.ParseInt(c.Get("company_id"), 10, 64)
if err != nil { // if err != nil {
h.logger.Error("Invalid company_id header", "value", c.Get("company_id"), "error", err) // h.logger.Error("Invalid company_id header", "value", c.Get("company_id"), "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid company_id") // 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 { 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") 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) 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 // 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") fmt.Println("Company Role without Company ID")
return fiber.NewError(fiber.StatusInternalServerError, "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() 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 { a.fiber.Get("/", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
"message": "Welcome to the FortuneBet API", "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.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist)
a.fiber.Get("/user/profile", a.authMiddleware, h.UserProfile) a.fiber.Get("/user/profile", a.authMiddleware, h.UserProfile)
a.fiber.Get("/user/single/:id", a.authMiddleware, h.GetUserByID) 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.Get("/user/wallet", a.authMiddleware, h.GetCustomerWallet)
a.fiber.Post("/user/search", a.authMiddleware, h.SearchUserByNameOrPhone) 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.Patch("/referral/settings", a.authMiddleware, h.UpdateReferralSettings)
a.fiber.Get("/cashiers", a.authMiddleware, h.GetAllCashiers) 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.Post("/cashiers", a.authMiddleware, h.CreateCashier)
a.fiber.Put("/cashiers/:id", a.authMiddleware, h.UpdateCashier) a.fiber.Put("/cashiers/:id", a.authMiddleware, h.UpdateCashier)
a.fiber.Get("/admin", a.authMiddleware, h.GetAllAdmins) 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.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", a.authMiddleware, h.GetAllManagers)
a.fiber.Get("/managers/:id", a.authMiddleware, h.GetManagerByID)
a.fiber.Post("/managers", a.authMiddleware, h.CreateManager) a.fiber.Post("/managers", a.authMiddleware, h.CreateManager)
a.fiber.Put("/managers/:id", a.authMiddleware, h.UpdateManagers) a.fiber.Put("/managers/:id", a.authMiddleware, h.UpdateManagers)
a.fiber.Get("/manager/:id/branch", a.authMiddleware, h.GetBranchByManagerID) a.fiber.Get("/manager/:id/branch", a.authMiddleware, h.GetBranchByManagerID)
@ -124,12 +130,14 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/search/branch", a.authMiddleware, h.SearchBranch) a.fiber.Get("/search/branch", a.authMiddleware, h.SearchBranch)
// /branch/search // /branch/search
// branch/wallet // branch/wallet
a.fiber.Get("/branch/:id/cashiers", a.authMiddleware, h.GetBranchCashiers)
// Branch Operation // Branch Operation
a.fiber.Get("/supportedOperation", a.authMiddleware, h.GetAllSupportedOperations) a.fiber.Get("/supportedOperation", a.authMiddleware, h.GetAllSupportedOperations)
a.fiber.Post("/supportedOperation", a.authMiddleware, h.CreateSupportedOperation) a.fiber.Post("/supportedOperation", a.authMiddleware, h.CreateSupportedOperation)
a.fiber.Post("/operation", a.authMiddleware, h.CreateBranchOperation) a.fiber.Post("/operation", a.authMiddleware, h.CreateBranchOperation)
a.fiber.Get("/branch/:id/operation", a.authMiddleware, h.GetBranchOperations) a.fiber.Get("/branch/:id/operation", a.authMiddleware, h.GetBranchOperations)
a.fiber.Delete("/branch/:id/operation/:opID", a.authMiddleware, h.DeleteBranchOperation) a.fiber.Delete("/branch/:id/operation/:opID", a.authMiddleware, h.DeleteBranchOperation)
// Company // Company
@ -149,11 +157,13 @@ func (a *App) initAppRoutes() {
// Bet Routes // Bet Routes
a.fiber.Post("/bet", a.authMiddleware, h.CreateBet) a.fiber.Post("/bet", a.authMiddleware, h.CreateBet)
a.fiber.Get("/bet", a.authMiddleware, h.GetAllBet) 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.Get("/bet/cashout/:id", a.authMiddleware, h.GetBetByCashoutID)
a.fiber.Patch("/bet/:id", a.authMiddleware, h.UpdateCashOut) a.fiber.Patch("/bet/:id", a.authMiddleware, h.UpdateCashOut)
a.fiber.Delete("/bet/:id", a.authMiddleware, h.DeleteBet) a.fiber.Delete("/bet/:id", a.authMiddleware, h.DeleteBet)
a.fiber.Post("/random/bet", a.authMiddleware, h.RandomBet)
// Wallet // Wallet
a.fiber.Get("/wallet", h.GetAllWallets) a.fiber.Get("/wallet", h.GetAllWallets)
a.fiber.Get("/wallet/:id", h.GetWalletByID) a.fiber.Get("/wallet/:id", h.GetWalletByID)
@ -191,13 +201,17 @@ func (a *App) initAppRoutes() {
a.fiber.Put("/transaction/:id", a.authMiddleware, h.UpdateTransactionVerified) a.fiber.Put("/transaction/:id", a.authMiddleware, h.UpdateTransactionVerified)
// Notification Routes // Notification Routes
a.fiber.Get("/notifications/ws/connect/:recipientID", h.ConnectSocket) a.fiber.Get("/ws/connect", a.WebsocketAuthMiddleware, h.ConnectSocket)
a.fiber.Post("/notifications/mark-as-read", h.MarkNotificationAsRead) 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) a.fiber.Post("/notifications/create", h.CreateAndSendNotification)
// Virtual Game Routes // Virtual Game Routes
a.fiber.Post("/virtual-game/launch", a.authMiddleware, h.LaunchVirtualGame) a.fiber.Post("/virtual-game/launch", a.authMiddleware, h.LaunchVirtualGame)
a.fiber.Post("/virtual-game/callback", h.HandleVirtualGameCallback) a.fiber.Post("/virtual-game/callback", h.HandleVirtualGameCallback)
} }
///user/profile get ///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 include .env
.PHONY: test .PHONY: test
test: test:
@go test ./app @docker compose up -d test
@docker compose exec test go test ./...
@docker compose stop test
.PHONY: coverage .PHONY: coverage
coverage: coverage:
@mkdir -p coverage @mkdir -p coverage
@go test -coverprofile=coverage.out ./internal/... @docker compose up -d test
@go tool cover -func=coverage.out -o coverage/coverage.txt @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 .PHONY: build
build: build:
@go build -ldflags="-s" -o ./bin/web ./cmd/main.go @docker compose build app
.PHONY: run .PHONY: run
run: run:
@echo "Running Go application" @docker compose up -d
@go run ./cmd/main.go
.PHONY: stop
stop:
@docker compose down
.PHONY: air .PHONY: air
air: air:
@echo "Running air" @echo "Running air locally (not in Docker)"
@air -c .air.toml @air -c .air.toml
.PHONY: migrations/new .PHONY: migrations/up
migrations/new: migrations/new:
@echo 'Creating migration files for DB_URL' @echo 'Creating migration files for DB_URL'
@migrate create -seq -ext=.sql -dir=./db/migrations $(name) @migrate create -seq -ext=.sql -dir=./db/migrations $(name)
.PHONY: migrations/up .PHONY: migrations/up
migrations/up: migrations/up:
@echo 'Running up migrations...' @echo 'Running up migrations...'
@migrate -path ./db/migrations -database $(DB_URL) up @docker compose up migrate
.PHONY: swagger .PHONY: swagger
swagger: swagger:
@swag init -g cmd/main.go @swag init -g cmd/main.go
.PHONY: db-up .PHONY: db-up
db-up: db-up:
docker compose -f compose.db.yaml up @docker compose up -d postgres migrate
.PHONY: db-down .PHONY: db-down
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 .PHONY: sqlc-gen
sqlc-gen: sqlc-gen:
@sqlc generate @sqlc generate