small fixes and getting it ready for deployment

This commit is contained in:
Samuel Tariku 2025-04-29 21:11:25 +03:00
parent 9e4b0d9942
commit 98e30b5051
16 changed files with 611 additions and 257 deletions

View File

@ -161,6 +161,7 @@ CREATE TABLE IF NOT EXISTS transactions (
account_number VARCHAR(255) NOT NULL,
reference_number VARCHAR(255) NOT NULL,
verified BOOLEAN NOT NULL DEFAULT false,
approved_by BIGINT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
@ -243,7 +244,6 @@ CREATE TABLE odds (
UNIQUE (event_id, market_id, name, handicap),
UNIQUE (event_id, market_id)
);
CREATE TABLE companies (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
@ -397,4 +397,4 @@ VALUES (
TRUE,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
);

View File

@ -1,16 +1,51 @@
-- name: CreateTransaction :one
INSERT INTO transactions (amount, branch_id, cashier_id, bet_id, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *;
INSERT INTO transactions (
amount,
branch_id,
cashier_id,
bet_id,
type,
payment_option,
full_name,
phone_number,
bank_code,
beneficiary_name,
account_name,
account_number,
reference_number,
number_of_outcomes
)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13,
$14
)
RETURNING *;
-- name: GetAllTransactions :many
SELECT * FROM transactions;
SELECT *
FROM transactions;
-- name: GetTransactionByID :one
SELECT * FROM transactions WHERE id = $1;
SELECT *
FROM transactions
WHERE id = $1;
-- name: GetTransactionByBranch :many
SELECT * FROM transactions WHERE branch_id = $1;
SELECT *
FROM transactions
WHERE branch_id = $1;
-- name: UpdateTransactionVerified :exec
UPDATE transactions SET verified = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1;
UPDATE transactions
SET verified = $2,
approved_by = $3,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;

View File

@ -55,7 +55,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/handlers.AdminRes"
}
},
"400": {
@ -1449,7 +1449,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/handlers.ManagersRes"
}
},
"400": {
@ -3445,7 +3445,7 @@ const docTemplate = `{
"OUTCOME_STATUS_PENDING",
"OUTCOME_STATUS_WIN",
"OUTCOME_STATUS_LOSS",
"OUTCOME_STATUS_ERROR"
"OUTCOME_STATUS_VOID"
]
},
"domain.PaymentOption": {
@ -3703,6 +3703,50 @@ const docTemplate = `{
}
}
},
"handlers.AdminRes": {
"type": "object",
"properties": {
"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.BetRes": {
"type": "object",
"properties": {
@ -3851,9 +3895,6 @@ const docTemplate = `{
},
"handlers.CheckPhoneEmailExistReq": {
"type": "object",
"required": [
"phone_number"
],
"properties": {
"email": {
"type": "string",
@ -3971,10 +4012,6 @@ const docTemplate = `{
}
],
"example": 1
},
"total_odds": {
"type": "number",
"example": 4.22
}
}
},
@ -4140,10 +4177,6 @@ const docTemplate = `{
"items": {
"$ref": "#/definitions/handlers.CreateTicketOutcomeReq"
}
},
"total_odds": {
"type": "number",
"example": 4.22
}
}
},
@ -4267,6 +4300,50 @@ const docTemplate = `{
}
}
},
"handlers.ManagersRes": {
"type": "object",
"properties": {
"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.RegisterCodeReq": {
"type": "object",
"properties": {

View File

@ -47,7 +47,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/handlers.AdminRes"
}
},
"400": {
@ -1441,7 +1441,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/handlers.ManagersRes"
}
},
"400": {
@ -3437,7 +3437,7 @@
"OUTCOME_STATUS_PENDING",
"OUTCOME_STATUS_WIN",
"OUTCOME_STATUS_LOSS",
"OUTCOME_STATUS_ERROR"
"OUTCOME_STATUS_VOID"
]
},
"domain.PaymentOption": {
@ -3695,6 +3695,50 @@
}
}
},
"handlers.AdminRes": {
"type": "object",
"properties": {
"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.BetRes": {
"type": "object",
"properties": {
@ -3843,9 +3887,6 @@
},
"handlers.CheckPhoneEmailExistReq": {
"type": "object",
"required": [
"phone_number"
],
"properties": {
"email": {
"type": "string",
@ -3963,10 +4004,6 @@
}
],
"example": 1
},
"total_odds": {
"type": "number",
"example": 4.22
}
}
},
@ -4132,10 +4169,6 @@
"items": {
"$ref": "#/definitions/handlers.CreateTicketOutcomeReq"
}
},
"total_odds": {
"type": "number",
"example": 4.22
}
}
},
@ -4259,6 +4292,50 @@
}
}
},
"handlers.ManagersRes": {
"type": "object",
"properties": {
"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.RegisterCodeReq": {
"type": "object",
"properties": {

View File

@ -90,7 +90,7 @@ definitions:
- OUTCOME_STATUS_PENDING
- OUTCOME_STATUS_WIN
- OUTCOME_STATUS_LOSS
- OUTCOME_STATUS_ERROR
- OUTCOME_STATUS_VOID
domain.PaymentOption:
enum:
- 0
@ -272,6 +272,35 @@ definitions:
description: Converted from "time" field in UNIX format
type: string
type: object
handlers.AdminRes:
properties:
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.BetRes:
properties:
amount:
@ -384,8 +413,6 @@ definitions:
phone_number:
example: "1234567890"
type: string
required:
- phone_number
type: object
handlers.CheckPhoneEmailExistRes:
properties:
@ -461,9 +488,6 @@ definitions:
allOf:
- $ref: '#/definitions/domain.OutcomeStatus'
example: 1
total_odds:
example: 4.22
type: number
type: object
handlers.CreateBranchOperationReq:
properties:
@ -581,9 +605,6 @@ definitions:
items:
$ref: '#/definitions/handlers.CreateTicketOutcomeReq'
type: array
total_odds:
example: 4.22
type: number
type: object
handlers.CreateTicketRes:
properties:
@ -668,6 +689,35 @@ definitions:
static_updated_at:
type: string
type: object
handlers.ManagersRes:
properties:
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.RegisterCodeReq:
properties:
email:
@ -1055,7 +1105,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
$ref: '#/definitions/handlers.AdminRes'
"400":
description: Bad Request
schema:
@ -1975,7 +2025,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
$ref: '#/definitions/handlers.ManagersRes'
"400":
description: Bad Request
schema:

View File

@ -350,6 +350,7 @@ type Transaction struct {
AccountNumber string `json:"account_number"`
ReferenceNumber string `json:"reference_number"`
Verified bool `json:"verified"`
ApprovedBy pgtype.Int8 `json:"approved_by"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}

View File

@ -7,26 +7,61 @@ package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CreateTransaction = `-- name: CreateTransaction :one
INSERT INTO transactions (amount, branch_id, cashier_id, bet_id, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at
INSERT INTO transactions (
amount,
branch_id,
cashier_id,
bet_id,
type,
payment_option,
full_name,
phone_number,
bank_code,
beneficiary_name,
account_name,
account_number,
reference_number,
number_of_outcomes
)
VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12,
$13,
$14
)
RETURNING id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, approved_by, created_at, updated_at
`
type CreateTransactionParams struct {
Amount int64 `json:"amount"`
BranchID int64 `json:"branch_id"`
CashierID int64 `json:"cashier_id"`
BetID int64 `json:"bet_id"`
Type int64 `json:"type"`
PaymentOption int64 `json:"payment_option"`
FullName string `json:"full_name"`
PhoneNumber string `json:"phone_number"`
BankCode string `json:"bank_code"`
BeneficiaryName string `json:"beneficiary_name"`
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
ReferenceNumber string `json:"reference_number"`
Amount int64 `json:"amount"`
BranchID int64 `json:"branch_id"`
CashierID int64 `json:"cashier_id"`
BetID int64 `json:"bet_id"`
Type int64 `json:"type"`
PaymentOption int64 `json:"payment_option"`
FullName string `json:"full_name"`
PhoneNumber string `json:"phone_number"`
BankCode string `json:"bank_code"`
BeneficiaryName string `json:"beneficiary_name"`
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
ReferenceNumber string `json:"reference_number"`
NumberOfOutcomes int64 `json:"number_of_outcomes"`
}
func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (Transaction, error) {
@ -44,6 +79,7 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa
arg.AccountName,
arg.AccountNumber,
arg.ReferenceNumber,
arg.NumberOfOutcomes,
)
var i Transaction
err := row.Scan(
@ -63,6 +99,7 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa
&i.AccountNumber,
&i.ReferenceNumber,
&i.Verified,
&i.ApprovedBy,
&i.CreatedAt,
&i.UpdatedAt,
)
@ -70,7 +107,8 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa
}
const GetAllTransactions = `-- name: GetAllTransactions :many
SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at FROM transactions
SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, approved_by, created_at, updated_at
FROM transactions
`
func (q *Queries) GetAllTransactions(ctx context.Context) ([]Transaction, error) {
@ -99,6 +137,7 @@ func (q *Queries) GetAllTransactions(ctx context.Context) ([]Transaction, error)
&i.AccountNumber,
&i.ReferenceNumber,
&i.Verified,
&i.ApprovedBy,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
@ -113,7 +152,9 @@ func (q *Queries) GetAllTransactions(ctx context.Context) ([]Transaction, error)
}
const GetTransactionByBranch = `-- name: GetTransactionByBranch :many
SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at FROM transactions WHERE branch_id = $1
SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, approved_by, created_at, updated_at
FROM transactions
WHERE branch_id = $1
`
func (q *Queries) GetTransactionByBranch(ctx context.Context, branchID int64) ([]Transaction, error) {
@ -142,6 +183,7 @@ func (q *Queries) GetTransactionByBranch(ctx context.Context, branchID int64) ([
&i.AccountNumber,
&i.ReferenceNumber,
&i.Verified,
&i.ApprovedBy,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
@ -156,7 +198,9 @@ func (q *Queries) GetTransactionByBranch(ctx context.Context, branchID int64) ([
}
const GetTransactionByID = `-- name: GetTransactionByID :one
SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at FROM transactions WHERE id = $1
SELECT id, amount, branch_id, cashier_id, bet_id, number_of_outcomes, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, approved_by, created_at, updated_at
FROM transactions
WHERE id = $1
`
func (q *Queries) GetTransactionByID(ctx context.Context, id int64) (Transaction, error) {
@ -179,6 +223,7 @@ func (q *Queries) GetTransactionByID(ctx context.Context, id int64) (Transaction
&i.AccountNumber,
&i.ReferenceNumber,
&i.Verified,
&i.ApprovedBy,
&i.CreatedAt,
&i.UpdatedAt,
)
@ -186,15 +231,20 @@ func (q *Queries) GetTransactionByID(ctx context.Context, id int64) (Transaction
}
const UpdateTransactionVerified = `-- name: UpdateTransactionVerified :exec
UPDATE transactions SET verified = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1
UPDATE transactions
SET verified = $2,
approved_by = $3,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
type UpdateTransactionVerifiedParams struct {
ID int64 `json:"id"`
Verified bool `json:"verified"`
ID int64 `json:"id"`
Verified bool `json:"verified"`
ApprovedBy pgtype.Int8 `json:"approved_by"`
}
func (q *Queries) UpdateTransactionVerified(ctx context.Context, arg UpdateTransactionVerifiedParams) error {
_, err := q.db.Exec(ctx, UpdateTransactionVerified, arg.ID, arg.Verified)
_, err := q.db.Exec(ctx, UpdateTransactionVerified, arg.ID, arg.Verified, arg.ApprovedBy)
return err
}

View File

@ -1,5 +1,7 @@
package domain
import "time"
type TransactionType int
const (
@ -36,6 +38,9 @@ type Transaction struct {
AccountNumber string
ReferenceNumber string
Verified bool
ApprovedBy ValidInt64
UpdatedAt time.Time
CreatedAt time.Time
}
type CreateTransaction struct {

View File

@ -5,10 +5,12 @@ import (
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
)
func convertDBTransaction(transaction dbgen.Transaction) domain.Transaction {
return domain.Transaction{
ID: transaction.ID,
Amount: domain.Currency(transaction.Amount),
BranchID: transaction.BranchID,
CashierID: transaction.CashierID,
@ -23,24 +25,32 @@ func convertDBTransaction(transaction dbgen.Transaction) domain.Transaction {
AccountName: transaction.AccountName,
AccountNumber: transaction.AccountNumber,
ReferenceNumber: transaction.ReferenceNumber,
ApprovedBy: domain.ValidInt64{
Value: transaction.ApprovedBy.Int64,
Valid: transaction.ApprovedBy.Valid,
},
CreatedAt: transaction.CreatedAt.Time,
UpdatedAt: transaction.UpdatedAt.Time,
Verified: transaction.Verified,
}
}
func convertCreateTransaction(transaction domain.CreateTransaction) dbgen.CreateTransactionParams {
return dbgen.CreateTransactionParams{
Amount: int64(transaction.Amount),
BranchID: transaction.BranchID,
CashierID: transaction.CashierID,
BetID: transaction.BetID,
Type: int64(transaction.Type),
PaymentOption: int64(transaction.PaymentOption),
FullName: transaction.FullName,
PhoneNumber: transaction.PhoneNumber,
BankCode: transaction.BankCode,
BeneficiaryName: transaction.BeneficiaryName,
AccountName: transaction.AccountName,
AccountNumber: transaction.AccountNumber,
ReferenceNumber: transaction.ReferenceNumber,
Amount: int64(transaction.Amount),
BranchID: transaction.BranchID,
CashierID: transaction.CashierID,
BetID: transaction.BetID,
Type: int64(transaction.Type),
PaymentOption: int64(transaction.PaymentOption),
FullName: transaction.FullName,
PhoneNumber: transaction.PhoneNumber,
BankCode: transaction.BankCode,
BeneficiaryName: transaction.BeneficiaryName,
AccountName: transaction.AccountName,
AccountNumber: transaction.AccountNumber,
ReferenceNumber: transaction.ReferenceNumber,
NumberOfOutcomes: transaction.NumberOfOutcomes,
}
}
@ -89,9 +99,13 @@ func (s *Store) GetTransactionByBranch(ctx context.Context, id int64) ([]domain.
return result, nil
}
func (s *Store) UpdateTransactionVerified(ctx context.Context, id int64, verified bool) error {
func (s *Store) UpdateTransactionVerified(ctx context.Context, id int64, verified bool, approvedBy int64) error {
err := s.queries.UpdateTransactionVerified(ctx, dbgen.UpdateTransactionVerifiedParams{
ID: id,
ID: id,
ApprovedBy: pgtype.Int8{
Int64: approvedBy,
Valid: true,
},
Verified: verified,
})
return err

View File

@ -11,5 +11,5 @@ type TransactionStore interface {
GetTransactionByID(ctx context.Context, id int64) (domain.Transaction, error)
GetAllTransactions(ctx context.Context) ([]domain.Transaction, error)
GetTransactionByBranch(ctx context.Context, id int64) ([]domain.Transaction, error)
UpdateTransactionVerified(ctx context.Context, id int64, verified bool) error
UpdateTransactionVerified(ctx context.Context, id int64, verified bool, approvedBy int64) error
}

View File

@ -28,7 +28,6 @@ func (s *Service) GetAllTransactions(ctx context.Context) ([]domain.Transaction,
func (s *Service) GetTransactionByBranch(ctx context.Context, id int64) ([]domain.Transaction, error) {
return s.transactionStore.GetTransactionByBranch(ctx, id)
}
func (s *Service) UpdateTransactionVerified(ctx context.Context, id int64, verified bool) error {
return s.transactionStore.UpdateTransactionVerified(ctx, id, verified)
func (s *Service) UpdateTransactionVerified(ctx context.Context, id int64, verified bool, approvedBy int64) error {
return s.transactionStore.UpdateTransactionVerified(ctx, id, verified, approvedBy)
}

View File

@ -5,6 +5,7 @@ import (
"context"
"log"
"time"
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
@ -25,6 +26,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
log.Printf("FetchUpcomingEvents error: %v", err)
}
time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour
},
},
@ -36,18 +38,17 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
// }
// },
// },
// {
// // spec: "0 */15 * * * *", // Every 15 minutes
// spec: "0 0 * * * *", // TODO: Every hour because of the 3600 requests per hour limit
// task: func() {
{
// spec: "0 */15 * * * *", // Every 15 minutes
spec: "0 0 * * * *", // TODO: Every hour because of the 3600 requests per hour limit
task: func() {
if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
log.Printf("FetchNonLiveOdds error: %v", err)
}
time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour
// if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
// log.Printf("FetchNonLiveOdds error: %v", err)
// }
// time.Sleep(2 * time.Second) //This will restrict the fetching to 1800 requests per hour
// },
// },
},
},
// {
// spec: "0 */15 * * * *",
// task: func() {
@ -71,7 +72,6 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
}
for _, job := range schedule {
job.task()
if _, err := c.AddFunc(job.spec, job.task); err != nil {
log.Fatalf("Failed to schedule cron job: %v", err)
}

View File

@ -18,10 +18,10 @@ type CreateBetOutcomeReq struct {
type CreateBetReq struct {
Outcomes []CreateBetOutcomeReq `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,omitempty" example:"1"`
}
type CreateBetRes struct {
@ -99,6 +99,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
// Get user_id from middleware
userID := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role)
var req CreateBetReq
if err := c.BodyParser(&req); err != nil {
@ -111,109 +112,13 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
// Validating user by role
// Differentiating between offline and online bets
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.logger.Error("CreateBetReq failed, user id invalid")
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil)
}
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 user.Role == domain.RoleCashier {
// Get the branch from the branch ID
branch, err := h.branchSvc.GetBranchByCashier(c.Context(), user.ID)
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: req.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 user.Role == domain.RoleSuperAdmin {
// This is just for testing
bet, err = h.betSvc.CreateBet(c.Context(), domain.CreateBet{
Amount: domain.ToCurrency(req.Amount),
TotalOdds: req.TotalOdds,
Status: req.Status,
FullName: req.FullName,
PhoneNumber: req.PhoneNumber,
BranchID: domain.ValidInt64{
Value: 1,
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: req.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)
}
//
// TODO Validate Outcomes Here and make sure they didn't expire
// Validation for creating tickets
if len(req.Outcomes) > 30 {
return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil)
}
var outcomes []domain.CreateBetOutcome = make([]domain.CreateBetOutcome, 0, len(req.Outcomes))
var totalOdds float32 = 1
for _, outcome := range req.Outcomes {
eventIDStr := strconv.FormatInt(outcome.EventID, 10)
marketIDStr := strconv.FormatInt(outcome.MarketID, 10)
@ -262,9 +167,9 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
}
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
totalOdds = totalOdds * float32(parsedOdd)
outcomes = append(outcomes, domain.CreateBetOutcome{
BetID: bet.ID,
EventID: outcome.EventID,
OddID: outcome.OddID,
MarketID: outcome.MarketID,
@ -279,6 +184,108 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
})
}
// 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 {

View File

@ -23,9 +23,8 @@ type CreateTicketOutcomeReq struct {
}
type CreateTicketReq struct {
Outcomes []CreateTicketOutcomeReq `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
Outcomes []CreateTicketOutcomeReq `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
}
type CreateTicketRes struct {
FastCode int64 `json:"fast_code" example:"1234"`
@ -66,6 +65,7 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil)
}
var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes))
var totalOdds float32 = 1
for _, outcome := range req.Outcomes {
eventIDStr := strconv.FormatInt(outcome.EventID, 10)
marketIDStr := strconv.FormatInt(outcome.MarketID, 10)
@ -100,7 +100,7 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error {
rawBytes, err := json.Marshal(raw)
err = json.Unmarshal(rawBytes, &rawOdd)
if err != nil {
h.logger.Error("Failed to unmarshal raw odd:", err)
h.logger.Error("Failed to unmarshal raw odd:", "error", err)
continue
}
if rawOdd.ID == oddIDStr {
@ -114,7 +114,7 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error {
}
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
totalOdds = totalOdds * float32(parsedOdd)
outcomes = append(outcomes, domain.CreateTicketOutcome{
EventID: outcome.EventID,
OddID: outcome.OddID,
@ -129,10 +129,9 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error {
Expires: event.StartTime,
})
}
ticket, err := h.ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{
Amount: domain.ToCurrency(req.Amount),
TotalOdds: req.TotalOdds,
TotalOdds: totalOdds,
})
if err != nil {
h.logger.Error("CreateTicketReq failed", "error", err)

View File

@ -1,7 +1,9 @@
package handlers
import (
"log/slog"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
@ -25,6 +27,9 @@ type TransactionRes struct {
AccountNumber string `json:"account_number"`
ReferenceNumber string `json:"reference_number"`
Verified bool `json:"verified" example:"true"`
ApprovedBy *int64 `json:"approved_by" example:"1"`
UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
}
type CreateTransactionReq struct {
@ -40,26 +45,36 @@ type CreateTransactionReq struct {
AccountName string `json:"account_name"`
AccountNumber string `json:"account_number"`
ReferenceNumber string `json:"reference_number"`
BranchID *int64 `json:"branch_id,omitempty" example:"1"`
}
func convertTransaction(transaction domain.Transaction) TransactionRes {
return TransactionRes{
ID: transaction.ID,
Amount: transaction.Amount.Float32(),
BranchID: transaction.BranchID,
CashierID: transaction.CashierID,
BetID: transaction.BetID,
Type: int64(transaction.Type),
PaymentOption: transaction.PaymentOption,
FullName: transaction.FullName,
PhoneNumber: transaction.PhoneNumber,
BankCode: transaction.BankCode,
BeneficiaryName: transaction.BeneficiaryName,
AccountName: transaction.AccountName,
AccountNumber: transaction.AccountNumber,
ReferenceNumber: transaction.ReferenceNumber,
Verified: transaction.Verified,
newTransaction := TransactionRes{
ID: transaction.ID,
Amount: transaction.Amount.Float32(),
BranchID: transaction.BranchID,
CashierID: transaction.CashierID,
BetID: transaction.BetID,
Type: int64(transaction.Type),
PaymentOption: transaction.PaymentOption,
FullName: transaction.FullName,
PhoneNumber: transaction.PhoneNumber,
BankCode: transaction.BankCode,
BeneficiaryName: transaction.BeneficiaryName,
AccountName: transaction.AccountName,
AccountNumber: transaction.AccountNumber,
ReferenceNumber: transaction.ReferenceNumber,
Verified: transaction.Verified,
NumberOfOutcomes: transaction.NumberOfOutcomes,
CreatedAt: transaction.CreatedAt,
UpdatedAt: transaction.UpdatedAt,
}
if transaction.ApprovedBy.Valid {
newTransaction.ApprovedBy = &transaction.ApprovedBy.Value
}
return newTransaction
}
// CreateTransaction godoc
@ -75,35 +90,17 @@ func convertTransaction(transaction domain.Transaction) TransactionRes {
// @Router /transaction [post]
func (h *Handler) CreateTransaction(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
user, err := h.userSvc.GetUserByID(c.Context(), userID)
role := c.Locals("role").(domain.Role)
// user, err := h.userSvc.GetUserByID(c.Context(), userID)
if user.Role == domain.RoleCustomer {
h.logger.Error("CreateTransactionReq failed")
// TODO: Make a "Only Company" middleware auth and move this into that
if role == domain.RoleCustomer {
h.logger.Error("CreateTransactionReq failed due to unauthorized access")
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "unauthorized access",
})
}
// TODO: Add validation to make sure that the bet hasn't already been cashed out by someone else
var branchID int64
if user.Role == domain.RoleAdmin || user.Role == domain.RoleBranchManager || user.Role == domain.RoleSuperAdmin {
branch, err := h.branchSvc.GetBranchByID(c.Context(), 1)
if err != nil {
h.logger.Error("CreateTransactionReq no branches")
return response.WriteJSON(c, fiber.StatusBadRequest, "This user type doesn't have branches", err, nil)
}
branchID = branch.ID
} else {
branch, err := h.branchSvc.GetBranchByCashier(c.Context(), user.ID)
if err != nil {
h.logger.Error("CreateTransactionReq failed, branch id invalid")
return response.WriteJSON(c, fiber.StatusBadRequest, "Branch ID invalid", err, nil)
}
branchID = branch.ID
}
var req CreateTransactionReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("CreateTransaction failed to parse request", "error", err)
@ -116,13 +113,51 @@ func (h *Handler) CreateTransaction(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
// TODO: Validate the bet id and add the number of outcomes
var branchID int64
if role == domain.RoleAdmin || role == domain.RoleBranchManager || role == domain.RoleSuperAdmin {
if req.BranchID == nil {
h.logger.Error("CreateTransactionReq Branch ID is required for this user role")
return response.WriteJSON(c, fiber.StatusBadRequest, "Branch ID is required for this user role", nil, nil)
}
branch, err := h.branchSvc.GetBranchByID(c.Context(), *req.BranchID)
if err != nil {
h.logger.Error("CreateTransactionReq no branches")
return response.WriteJSON(c, fiber.StatusBadRequest, "cannot find Branch ID", err, nil)
}
branchID = branch.ID
} else {
branch, err := h.branchSvc.GetBranchByCashier(c.Context(), userID)
if err != nil {
h.logger.Error("CreateTransactionReq failed, branch id invalid")
return response.WriteJSON(c, fiber.StatusBadRequest, "Branch ID invalid", err, nil)
}
branchID = branch.ID
}
bet, err := h.betSvc.GetBetByID(c.Context(), req.BetID)
if err != nil {
h.logger.Error("CreateTransactionReq failed", "error", err)
return response.WriteJSON(c, fiber.StatusBadRequest, "Bet ID invalid", err, nil)
}
// if bet.Status != domain.OUTCOME_STATUS_WIN {
// h.logger.Error("CreateTransactionReq failed, bet has not won")
// return response.WriteJSON(c, fiber.StatusBadRequest, "User has not won bet", err, nil)
// }
if bet.CashedOut {
h.logger.Error(("Bet has already been cashed out"))
return response.WriteJSON(c, fiber.StatusBadRequest, "This bet has already been cashed out", err, nil)
}
transaction, err := h.transactionSvc.CreateTransaction(c.Context(), domain.CreateTransaction{
BranchID: branchID,
CashierID: userID,
Amount: domain.ToCurrency(req.Amount),
BetID: req.BetID,
NumberOfOutcomes: 1,
BetID: bet.ID,
NumberOfOutcomes: int64(len(bet.Outcomes)),
Type: domain.TransactionType(req.Type),
PaymentOption: domain.PaymentOption(req.PaymentOption),
FullName: req.FullName,
@ -236,7 +271,7 @@ func (h *Handler) GetTransactionByID(c *fiber.Ctx) error {
}
type UpdateTransactionVerifiedReq struct {
Verified bool `json:"verified" validate:"required" example:"true"`
Verified bool `json:"verified" example:"true"`
}
// UpdateTransactionVerified godoc
@ -250,10 +285,12 @@ type UpdateTransactionVerifiedReq struct {
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /transaction/{id} [patch]
// @Router /transaction/{id} [put]
func (h *Handler) UpdateTransactionVerified(c *fiber.Ctx) error {
transactionID := c.Params("id")
userID := c.Locals("user_id").(int64)
// companyID := c.Locals("company_id").(domain.ValidInt64)
id, err := strconv.ParseInt(transactionID, 10, 64)
if err != nil {
h.logger.Error("Invalid transaction ID", "transactionID", transactionID, "error", err)
@ -266,11 +303,14 @@ func (h *Handler) UpdateTransactionVerified(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil)
}
h.logger.Info("Update Transaction Verified", slog.Bool("verified", req.Verified))
if valErrs, ok := h.validator.Validate(c, req); !ok {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
}
err = h.transactionSvc.UpdateTransactionVerified(c.Context(), id, req.Verified)
// TODO: make it so that only people within the company can verify a transaction
err = h.transactionSvc.UpdateTransactionVerified(c.Context(), id, req.Verified, userID)
if err != nil {
h.logger.Error("Failed to update transaction verification", "transactionID", id, "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transaction verification")

View File

@ -159,7 +159,7 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction)
a.fiber.Get("/transaction", a.authMiddleware, h.GetAllTransactions)
a.fiber.Get("/transaction/:id", a.authMiddleware, h.GetTransactionByID)
a.fiber.Patch("/transaction/:id", a.authMiddleware, h.UpdateTransactionVerified)
a.fiber.Put("/transaction/:id", a.authMiddleware, h.UpdateTransactionVerified)
// Notification Routes
a.fiber.Get("/notifications/ws/connect/:recipientID", h.ConnectSocket)