From 6f30fea12c260aa60e661c36b9fef4dd36627b6f Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 7 Apr 2025 03:45:52 +0300 Subject: [PATCH 01/30] branch management --- .vscode/settings.json | 5 + db/migrations/000001_fortune.up.sql | 41 +- db/query/branch.sql | 39 ++ gen/db/branch.sql.go | 347 +++++++++++++++ gen/db/models.go | 41 ++ gen/db/transfer.sql.go | 12 +- internal/domain/bank.go | 1 + internal/domain/bet.go | 1 + internal/domain/branch.go | 57 ++- internal/domain/chapa.go | 1 + internal/domain/company.go | 10 + internal/domain/event.go | 4 +- internal/domain/transaction.go | 3 +- internal/domain/transfer.go | 17 + internal/repository/branch.go | 163 +++++++ internal/services/branch/port.go | 21 + internal/services/branch/service.go | 51 +++ internal/web_server/handlers/bet_handler.go | 6 +- .../web_server/handlers/branch_handler.go | 420 ++++++++++++++++++ .../web_server/handlers/wallet_handler.go | 6 +- sqlc.yaml | 1 + 21 files changed, 1224 insertions(+), 23 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 db/query/branch.sql create mode 100644 gen/db/branch.sql.go create mode 100644 internal/domain/bank.go create mode 100644 internal/domain/chapa.go create mode 100644 internal/domain/company.go create mode 100644 internal/repository/branch.go create mode 100644 internal/services/branch/port.go create mode 100644 internal/services/branch/service.go create mode 100644 internal/web_server/handlers/branch_handler.go diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f72d738 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "Cashout" + ] +} \ No newline at end of file diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index ed91221..59140c2 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -102,6 +102,7 @@ CREATE TABLE IF NOT EXISTS wallet_transfer ( wallet_transfer VARCHAR(255) NOT NULL, wallet_id BIGINT NOT NULL, verified BOOLEAN NOT NULL DEFAULT false, + payment_method INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); @@ -125,6 +126,38 @@ CREATE TABLE IF NOT EXISTS transactions ( updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); +CREATE TABLE IF NOT EXISTS branches ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + location VARCHAR(255) NOT NULL, + wallet_id BIGINT NOT NULL, + branch_manager_id BIGINT NOT NULL, + company_id BIGINT NOT NULL, + is_self_owned BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE VIEW branch_details AS +SELECT branches.*, + CONCAT(users.first_name, ' ', users.last_name) AS manager_name, + users.phone_number AS manager_phone_number +FROM branches +LEFT JOIN users ON branches.branch_manager_id = users.id; + +CREATE TABLE IF NOT EXISTS supported_operations ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description VARCHAR(255) NOT NULL +); + +CREATE TABLE IF NOT EXISTS branch_operations ( + id BIGSERIAL PRIMARY KEY, + operation_id BIGINT NOT NULL, + branch_id BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); ----------------------------------------------seed data------------------------------------------------------------- -------------------------------------- DO NOT USE IN PRODUCTION------------------------------------------------- @@ -151,4 +184,10 @@ INSERT INTO users ( ); - +INSERT INTO supported_operations ( + name, description +) VALUES +('SportBook', 'Sportbook operations'), +('Virtual', 'Virtual operations'), +('GameZone', 'GameZone operations') +; diff --git a/db/query/branch.sql b/db/query/branch.sql new file mode 100644 index 0000000..736cf08 --- /dev/null +++ b/db/query/branch.sql @@ -0,0 +1,39 @@ + +-- name: CreateBranch :one +INSERT INTO branches (name, location, wallet_id, branch_manager_id, company_id, is_self_owned) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; + +-- name: CreateSupportedOperation :one +INSERT INTO supported_operations (name, description) VALUES ($1, $2) RETURNING *; + +-- name: CreateBranchOperation :one +INSERT INTO branch_operations (operation_id, branch_id) VALUES ($1, $2) RETURNING *; + +-- name: GetAllBranches :many +SELECT * FROM branch_details; + +-- name: GetBranchByID :one +SELECT * FROM branch_details WHERE id = $1; + +-- name: GetBranchByCompanyID :many +SELECT * FROM branch_details WHERE company_id = $1; + +-- name: GetBranchByManagerID :many +SELECT * FROM branch_details WHERE branch_manager_id = $1; + +-- name: GetAllSupportedOperations :many +SELECT * FROM supported_operations; + +-- name: GetBranchOperations :many +SELECT branch_operations.*, supported_operations.name, supported_operations.description +FROM branch_operations +JOIN supported_operations ON branch_operations.operation_id = supported_operations.id +WHERE branch_operations.branch_id = $1; + +-- name: UpdateBranch :one +UPDATE branches SET name = $1, location = $2, branch_manager_id = $3, company_id = $4, is_self_owned = $5 WHERE id = $6 RETURNING *; + +-- name: DeleteBranch :exec +DELETE FROM branches WHERE id = $1; + +-- name: DeleteBranchOperation :exec +DELETE FROM branch_operations WHERE operation_id = $1 AND branch_id = $2; diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go new file mode 100644 index 0000000..5197e58 --- /dev/null +++ b/gen/db/branch.sql.go @@ -0,0 +1,347 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: branch.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreateBranch = `-- name: CreateBranch :one +INSERT INTO branches (name, location, wallet_id, branch_manager_id, company_id, is_self_owned) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at +` + +type CreateBranchParams struct { + Name string + Location string + WalletID int64 + BranchManagerID int64 + CompanyID int64 + IsSelfOwned bool +} + +func (q *Queries) CreateBranch(ctx context.Context, arg CreateBranchParams) (Branch, error) { + row := q.db.QueryRow(ctx, CreateBranch, + arg.Name, + arg.Location, + arg.WalletID, + arg.BranchManagerID, + arg.CompanyID, + arg.IsSelfOwned, + ) + var i Branch + err := row.Scan( + &i.ID, + &i.Name, + &i.Location, + &i.WalletID, + &i.BranchManagerID, + &i.CompanyID, + &i.IsSelfOwned, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const CreateBranchOperation = `-- name: CreateBranchOperation :one +INSERT INTO branch_operations (operation_id, branch_id) VALUES ($1, $2) RETURNING id, operation_id, branch_id, created_at, updated_at +` + +type CreateBranchOperationParams struct { + OperationID int64 + BranchID int64 +} + +func (q *Queries) CreateBranchOperation(ctx context.Context, arg CreateBranchOperationParams) (BranchOperation, error) { + row := q.db.QueryRow(ctx, CreateBranchOperation, arg.OperationID, arg.BranchID) + var i BranchOperation + err := row.Scan( + &i.ID, + &i.OperationID, + &i.BranchID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const CreateSupportedOperation = `-- name: CreateSupportedOperation :one +INSERT INTO supported_operations (name, description) VALUES ($1, $2) RETURNING id, name, description +` + +type CreateSupportedOperationParams struct { + Name string + Description string +} + +func (q *Queries) CreateSupportedOperation(ctx context.Context, arg CreateSupportedOperationParams) (SupportedOperation, error) { + row := q.db.QueryRow(ctx, CreateSupportedOperation, arg.Name, arg.Description) + var i SupportedOperation + err := row.Scan(&i.ID, &i.Name, &i.Description) + return i, err +} + +const DeleteBranch = `-- name: DeleteBranch :exec +DELETE FROM branches WHERE id = $1 +` + +func (q *Queries) DeleteBranch(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteBranch, id) + return err +} + +const DeleteBranchOperation = `-- name: DeleteBranchOperation :exec +DELETE FROM branch_operations WHERE operation_id = $1 AND branch_id = $2 +` + +type DeleteBranchOperationParams struct { + OperationID int64 + BranchID int64 +} + +func (q *Queries) DeleteBranchOperation(ctx context.Context, arg DeleteBranchOperationParams) error { + _, err := q.db.Exec(ctx, DeleteBranchOperation, arg.OperationID, arg.BranchID) + return err +} + +const GetAllBranches = `-- name: GetAllBranches :many +SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number FROM branch_details +` + +func (q *Queries) GetAllBranches(ctx context.Context) ([]BranchDetail, error) { + rows, err := q.db.Query(ctx, GetAllBranches) + if err != nil { + return nil, err + } + defer rows.Close() + var items []BranchDetail + for rows.Next() { + var i BranchDetail + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Location, + &i.WalletID, + &i.BranchManagerID, + &i.CompanyID, + &i.IsSelfOwned, + &i.CreatedAt, + &i.UpdatedAt, + &i.ManagerName, + &i.ManagerPhoneNumber, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetAllSupportedOperations = `-- name: GetAllSupportedOperations :many +SELECT id, name, description FROM supported_operations +` + +func (q *Queries) GetAllSupportedOperations(ctx context.Context) ([]SupportedOperation, error) { + rows, err := q.db.Query(ctx, GetAllSupportedOperations) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SupportedOperation + for rows.Next() { + var i SupportedOperation + if err := rows.Scan(&i.ID, &i.Name, &i.Description); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetBranchByCompanyID = `-- name: GetBranchByCompanyID :many +SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number FROM branch_details WHERE company_id = $1 +` + +func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]BranchDetail, error) { + rows, err := q.db.Query(ctx, GetBranchByCompanyID, companyID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []BranchDetail + for rows.Next() { + var i BranchDetail + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Location, + &i.WalletID, + &i.BranchManagerID, + &i.CompanyID, + &i.IsSelfOwned, + &i.CreatedAt, + &i.UpdatedAt, + &i.ManagerName, + &i.ManagerPhoneNumber, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetBranchByID = `-- name: GetBranchByID :one +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 WHERE id = $1 +` + +func (q *Queries) GetBranchByID(ctx context.Context, id int64) (BranchDetail, error) { + row := q.db.QueryRow(ctx, GetBranchByID, id) + var i BranchDetail + err := row.Scan( + &i.ID, + &i.Name, + &i.Location, + &i.WalletID, + &i.BranchManagerID, + &i.CompanyID, + &i.IsSelfOwned, + &i.CreatedAt, + &i.UpdatedAt, + &i.ManagerName, + &i.ManagerPhoneNumber, + ) + return i, err +} + +const GetBranchByManagerID = `-- name: GetBranchByManagerID :many +SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number FROM branch_details WHERE branch_manager_id = $1 +` + +func (q *Queries) GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]BranchDetail, error) { + rows, err := q.db.Query(ctx, GetBranchByManagerID, branchManagerID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []BranchDetail + for rows.Next() { + var i BranchDetail + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Location, + &i.WalletID, + &i.BranchManagerID, + &i.CompanyID, + &i.IsSelfOwned, + &i.CreatedAt, + &i.UpdatedAt, + &i.ManagerName, + &i.ManagerPhoneNumber, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetBranchOperations = `-- name: GetBranchOperations :many +SELECT branch_operations.id, branch_operations.operation_id, branch_operations.branch_id, branch_operations.created_at, branch_operations.updated_at, supported_operations.name, supported_operations.description +FROM branch_operations +JOIN supported_operations ON branch_operations.operation_id = supported_operations.id +WHERE branch_operations.branch_id = $1 +` + +type GetBranchOperationsRow struct { + ID int64 + OperationID int64 + BranchID int64 + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp + Name string + Description string +} + +func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]GetBranchOperationsRow, error) { + rows, err := q.db.Query(ctx, GetBranchOperations, branchID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetBranchOperationsRow + for rows.Next() { + var i GetBranchOperationsRow + if err := rows.Scan( + &i.ID, + &i.OperationID, + &i.BranchID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Description, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const UpdateBranch = `-- name: UpdateBranch :one +UPDATE branches SET name = $1, location = $2, branch_manager_id = $3, company_id = $4, is_self_owned = $5 WHERE id = $6 RETURNING id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at +` + +type UpdateBranchParams struct { + Name string + Location string + BranchManagerID int64 + CompanyID int64 + IsSelfOwned bool + ID int64 +} + +func (q *Queries) UpdateBranch(ctx context.Context, arg UpdateBranchParams) (Branch, error) { + row := q.db.QueryRow(ctx, UpdateBranch, + arg.Name, + arg.Location, + arg.BranchManagerID, + arg.CompanyID, + arg.IsSelfOwned, + arg.ID, + ) + var i Branch + err := row.Scan( + &i.ID, + &i.Name, + &i.Location, + &i.WalletID, + &i.BranchManagerID, + &i.CompanyID, + &i.IsSelfOwned, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/gen/db/models.go b/gen/db/models.go index c446a64..bb59454 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -23,6 +23,40 @@ type Bet struct { IsShopBet bool } +type Branch struct { + ID int64 + Name string + Location string + WalletID int64 + BranchManagerID int64 + CompanyID int64 + IsSelfOwned bool + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} + +type BranchDetail struct { + ID int64 + Name string + Location string + WalletID int64 + BranchManagerID int64 + CompanyID int64 + IsSelfOwned bool + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp + ManagerName interface{} + ManagerPhoneNumber pgtype.Text +} + +type BranchOperation struct { + ID int64 + OperationID int64 + BranchID int64 + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} + type CustomerWallet struct { ID int64 CustomerID int64 @@ -71,6 +105,12 @@ type RefreshToken struct { Revoked bool } +type SupportedOperation struct { + ID int64 + Name string + Description string +} + type Ticket struct { ID int64 Amount pgtype.Int8 @@ -131,6 +171,7 @@ type WalletTransfer struct { WalletTransfer string WalletID int64 Verified bool + PaymentMethod int32 CreatedAt pgtype.Timestamp UpdatedAt pgtype.Timestamp } diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index c29cec8..770f23c 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -10,7 +10,7 @@ import ( ) const CreateTransfer = `-- name: CreateTransfer :one -INSERT INTO wallet_transfer (amount, wallet_transfer, wallet_id) VALUES ($1, $2, $3) RETURNING id, amount, wallet_transfer, wallet_id, verified, created_at, updated_at +INSERT INTO wallet_transfer (amount, wallet_transfer, wallet_id) VALUES ($1, $2, $3) RETURNING id, amount, wallet_transfer, wallet_id, verified, payment_method, created_at, updated_at ` type CreateTransferParams struct { @@ -28,6 +28,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) &i.WalletTransfer, &i.WalletID, &i.Verified, + &i.PaymentMethod, &i.CreatedAt, &i.UpdatedAt, ) @@ -35,7 +36,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) } const GetAllTransfers = `-- name: GetAllTransfers :many -SELECT id, amount, wallet_transfer, wallet_id, verified, created_at, updated_at FROM wallet_transfer +SELECT id, amount, wallet_transfer, wallet_id, verified, payment_method, created_at, updated_at FROM wallet_transfer ` func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error) { @@ -53,6 +54,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error) &i.WalletTransfer, &i.WalletID, &i.Verified, + &i.PaymentMethod, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -67,7 +69,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error) } const GetTransferByID = `-- name: GetTransferByID :one -SELECT id, amount, wallet_transfer, wallet_id, verified, created_at, updated_at FROM wallet_transfer WHERE id = $1 +SELECT id, amount, wallet_transfer, wallet_id, verified, payment_method, created_at, updated_at FROM wallet_transfer WHERE id = $1 ` func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer, error) { @@ -79,6 +81,7 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer &i.WalletTransfer, &i.WalletID, &i.Verified, + &i.PaymentMethod, &i.CreatedAt, &i.UpdatedAt, ) @@ -86,7 +89,7 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer } const GetTransfersByWallet = `-- name: GetTransfersByWallet :many -SELECT id, amount, wallet_transfer, wallet_id, verified, created_at, updated_at FROM wallet_transfer WHERE wallet_id = $1 +SELECT id, amount, wallet_transfer, wallet_id, verified, payment_method, created_at, updated_at FROM wallet_transfer WHERE wallet_id = $1 ` func (q *Queries) GetTransfersByWallet(ctx context.Context, walletID int64) ([]WalletTransfer, error) { @@ -104,6 +107,7 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, walletID int64) ([]W &i.WalletTransfer, &i.WalletID, &i.Verified, + &i.PaymentMethod, &i.CreatedAt, &i.UpdatedAt, ); err != nil { diff --git a/internal/domain/bank.go b/internal/domain/bank.go new file mode 100644 index 0000000..7d0427a --- /dev/null +++ b/internal/domain/bank.go @@ -0,0 +1 @@ +package domain \ No newline at end of file diff --git a/internal/domain/bet.go b/internal/domain/bet.go index 87bc936..e36ee62 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -23,6 +23,7 @@ type Bet struct { UserID ValidInt64 // Can Be Nullable IsShopBet bool CashedOut bool + CashoutID string } type CreateBet struct { diff --git a/internal/domain/branch.go b/internal/domain/branch.go index dc4a07f..29143c0 100644 --- a/internal/domain/branch.go +++ b/internal/domain/branch.go @@ -1,14 +1,53 @@ package domain type Branch struct { - ID int64 - Name string - Location string - WalletID int64 - BranchManagerID int64 - IsSelfOwned bool - IsSupportingSportBook bool - IsSupportingVirtual bool - IsSupportingGameZone bool + ID int64 + Name string + Location string + WalletID int64 + BranchManagerID int64 + CompanyID int64 + IsSelfOwned bool } +type BranchDetail struct { + ID int64 + Name string + Location string + WalletID int64 + BranchManagerID int64 + CompanyID int64 + IsSelfOwned bool + ManagerName string + ManagerPhoneNumber string +} + +type SupportedOperation struct { + ID int64 + Name string + Description string +} + +type BranchOperation struct { + ID int64 + OperationName string + OperationDescription string +} + +type CreateBranch struct { + Name string + Location string + WalletID int64 + BranchManagerID int64 + CompanyID int64 + IsSelfOwned bool +} + +type CreateSupportedOperation struct { + Name string + Description string +} +type CreateBranchOperation struct { + BranchID int64 + OperationID int64 +} diff --git a/internal/domain/chapa.go b/internal/domain/chapa.go new file mode 100644 index 0000000..7d0427a --- /dev/null +++ b/internal/domain/chapa.go @@ -0,0 +1 @@ +package domain \ No newline at end of file diff --git a/internal/domain/company.go b/internal/domain/company.go new file mode 100644 index 0000000..cf2a807 --- /dev/null +++ b/internal/domain/company.go @@ -0,0 +1,10 @@ +package domain + +// Company represents the client that we will contract the services with +// they are the ones that manage the branches and branch managers +// they will have their own wallet that they will use to distribute to the branch wallets +type Company struct { + ID int64 + Name string + +} \ No newline at end of file diff --git a/internal/domain/event.go b/internal/domain/event.go index e5cc881..43e30fb 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -2,5 +2,7 @@ package domain type Event struct {} -type Outcome struct {} +type Outcome struct { + +} diff --git a/internal/domain/transaction.go b/internal/domain/transaction.go index b4cdaf2..3ff096e 100644 --- a/internal/domain/transaction.go +++ b/internal/domain/transaction.go @@ -8,7 +8,8 @@ const ( ARIFPAY_TRANSACTION BANK ) - +// Transaction only represents when the user cashes out a bet in the shop +// It probably would be better to call it a CashOut or ShopWithdrawal type Transaction struct { ID int64 Amount Currency diff --git a/internal/domain/transfer.go b/internal/domain/transfer.go index 6c6210a..ed31614 100644 --- a/internal/domain/transfer.go +++ b/internal/domain/transfer.go @@ -1,5 +1,7 @@ package domain +import "time" + type TransferType string const ( @@ -7,12 +9,27 @@ const ( WITHDRAW TransferType = "withdraw" ) +type PaymentMethod int + +const ( + TRANSFER_CASH PaymentMethod = iota + 1 + TRANSFER_BANK + TRANSFER_CHAPA + TRANSFER_ARIFPAY + TRANSFER_SANTIM + TRANSFER_ADDISPAY + TRANSFER_OTHER +) + type Transfer struct { ID int64 Amount Currency Verified bool WalletID int64 Type TransferType + PaymentMethod PaymentMethod + CreatedAt time.Time + UpdatedAt time.Time } type CreateTransfer struct { diff --git a/internal/repository/branch.go b/internal/repository/branch.go new file mode 100644 index 0000000..2a8d470 --- /dev/null +++ b/internal/repository/branch.go @@ -0,0 +1,163 @@ +package repository + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +func convertCreateBranch(branch domain.CreateBranch) dbgen.CreateBranchParams { + return dbgen.CreateBranchParams{ + Name: branch.Name, + Location: branch.Location, + WalletID: branch.WalletID, + BranchManagerID: branch.BranchManagerID, + CompanyID: branch.CompanyID, + IsSelfOwned: branch.IsSelfOwned, + } +} + +func convertDBBranchDetail(dbBranch dbgen.BranchDetail) domain.BranchDetail { + return domain.BranchDetail{ + ID: dbBranch.ID, + Name: dbBranch.Name, + Location: dbBranch.Location, + WalletID: dbBranch.WalletID, + BranchManagerID: dbBranch.BranchManagerID, + CompanyID: dbBranch.CompanyID, + IsSelfOwned: dbBranch.IsSelfOwned, + ManagerName: dbBranch.ManagerName.(string), + ManagerPhoneNumber: dbBranch.ManagerPhoneNumber.String, + } +} + +func convertDBBranch(dbBranch dbgen.Branch) domain.Branch { + return domain.Branch{ + ID: dbBranch.ID, + Name: dbBranch.Name, + Location: dbBranch.Location, + WalletID: dbBranch.WalletID, + BranchManagerID: dbBranch.BranchManagerID, + CompanyID: dbBranch.CompanyID, + IsSelfOwned: dbBranch.IsSelfOwned, + } +} + +func (s *Store) CreateBranch(ctx context.Context, branch domain.CreateBranch) (domain.Branch, error) { + + dbBranch, err := s.queries.CreateBranch(ctx, convertCreateBranch(branch)) + + if err != nil { + return domain.Branch{}, err + } + return convertDBBranch(dbBranch), nil +} + +func (s *Store) CreateSupportedOperation(ctx context.Context, supportedOperation domain.CreateSupportedOperation) (domain.SupportedOperation, error) { + dbSupportedOperation, err := s.queries.CreateSupportedOperation(ctx, dbgen.CreateSupportedOperationParams{ + Name: supportedOperation.Name, + Description: supportedOperation.Description, + }) + if err != nil { + return domain.SupportedOperation{}, err + } + return domain.SupportedOperation{ + ID: dbSupportedOperation.ID, + Name: dbSupportedOperation.Name, + Description: dbSupportedOperation.Description, + }, nil +} + +func (s *Store) CreateBranchOperation(ctx context.Context, branchOperation domain.CreateBranchOperation) error { + _, err := s.queries.CreateBranchOperation(ctx, dbgen.CreateBranchOperationParams{ + BranchID: branchOperation.BranchID, + OperationID: branchOperation.OperationID, + }) + return err +} + +func (s *Store) GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error) { + dbBranch, err := s.queries.GetBranchByID(ctx, id) + if err != nil { + return domain.BranchDetail{}, err + } + return convertDBBranchDetail(dbBranch), nil +} + +func (s *Store) GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]domain.BranchDetail, error) { + dbBranches, err := s.queries.GetBranchByManagerID(ctx, branchManagerID) + if err != nil { + return nil, err + } + var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches)) + for _, dbBranch := range dbBranches { + branches = append(branches, convertDBBranchDetail(dbBranch)) + } + return branches, nil +} +func (s *Store) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]domain.BranchDetail, error) { + dbBranches, err := s.queries.GetBranchByCompanyID(ctx, companyID) + if err != nil { + return nil, err + } + var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches)) + for _, dbBranch := range dbBranches { + branches = append(branches, convertDBBranchDetail(dbBranch)) + } + return branches, nil +} + +func (s *Store) GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) { + dbBranchOperations, err := s.queries.GetBranchOperations(ctx, branchID) + if err != nil { + return nil, err + } + var branchOperations []domain.BranchOperation = make([]domain.BranchOperation, 0, len(dbBranchOperations)) + for _, dbBranchOperation := range dbBranchOperations { + branchOperations = append(branchOperations, domain.BranchOperation{ + ID: dbBranchOperation.ID, + OperationName: dbBranchOperation.Name, + OperationDescription: dbBranchOperation.Description, + }) + } + return branchOperations, nil +} + +func (s *Store) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) { + dbBranches, err := s.queries.GetAllBranches(ctx) + if err != nil { + return nil, err + } + var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches)) + for _, dbBranch := range dbBranches { + branches = append(branches, convertDBBranchDetail(dbBranch)) + } + return branches, nil +} + +func (s *Store) UpdateBranch(ctx context.Context, id int64, branch domain.CreateBranch) (domain.Branch, error) { + dbBranch, err := s.queries.UpdateBranch(ctx, dbgen.UpdateBranchParams{ + ID: id, + Name: branch.Name, + Location: branch.Location, + BranchManagerID: branch.BranchManagerID, + IsSelfOwned: branch.IsSelfOwned, + }) + if err != nil { + return domain.Branch{}, err + } + return convertDBBranch(dbBranch), nil +} + +func (s *Store) DeleteBranch(ctx context.Context, id int64) error { + return s.queries.DeleteBranch(ctx, id) +} + +func (s *Store) DeleteBranchOperation(ctx context.Context, branchID int64, operationID int64) error { + err := s.queries.DeleteBranchOperation(ctx, dbgen.DeleteBranchOperationParams{ + BranchID: branchID, + OperationID: operationID, + }) + return err +} diff --git a/internal/services/branch/port.go b/internal/services/branch/port.go new file mode 100644 index 0000000..6cff893 --- /dev/null +++ b/internal/services/branch/port.go @@ -0,0 +1,21 @@ +package branch + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type BranchStore interface { + CreateBranch(ctx context.Context, branch domain.CreateBranch) (domain.Branch, error) + CreateSupportedOperation(ctx context.Context, supportedOperation domain.CreateSupportedOperation) (domain.SupportedOperation, error) + CreateBranchOperation(ctx context.Context, branchOperation domain.CreateBranchOperation) error + GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error) + GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]domain.BranchDetail, error) + GetBranchByCompanyID(ctx context.Context, companyID int64) ([]domain.BranchDetail, error) + GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) + GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) + UpdateBranch(ctx context.Context, id int64, branch domain.CreateBranch) (domain.Branch, error) + DeleteBranch(ctx context.Context, id int64) error + DeleteBranchOperation(ctx context.Context, branchID int64, operationID int64) error +} diff --git a/internal/services/branch/service.go b/internal/services/branch/service.go new file mode 100644 index 0000000..f020252 --- /dev/null +++ b/internal/services/branch/service.go @@ -0,0 +1,51 @@ +package branch + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type Service struct { + branchStore BranchStore +} + +func NewService(branchStore BranchStore) *Service { + return &Service{ + branchStore: branchStore, + } +} + +func (s *Service) CreateBranch(ctx context.Context, branch domain.CreateBranch) (domain.Branch, error) { + return s.branchStore.CreateBranch(ctx, branch) +} +func (s *Service) CreateSupportedOperation(ctx context.Context, supportedOperation domain.CreateSupportedOperation) (domain.SupportedOperation, error) { + return s.branchStore.CreateSupportedOperation(ctx, supportedOperation) +} +func (s *Service) CreateBranchOperation(ctx context.Context, branchOperation domain.CreateBranchOperation) error { + return s.branchStore.CreateBranchOperation(ctx, branchOperation) +} +func (s *Service) GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error) { + return s.branchStore.GetBranchByID(ctx, id) +} +func (s *Service) GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([] domain.BranchDetail, error) { + return s.branchStore.GetBranchByManagerID(ctx, branchManagerID) +} +func (s *Service) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]domain.BranchDetail, error) { + return s.branchStore.GetBranchByCompanyID(ctx, companyID) +} +func (s *Service) GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) { + return s.branchStore.GetBranchOperations(ctx, branchID) +} +func (s *Service) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) { + return s.branchStore.GetAllBranches(ctx) +} +func (s *Service) UpdateBranch(ctx context.Context, id int64, branch domain.CreateBranch) (domain.Branch, error) { + return s.branchStore.UpdateBranch(ctx, id, branch) +} +func (s *Service) DeleteBranch(ctx context.Context, id int64) error { + return s.branchStore.DeleteBranch(ctx, id) +} +func (s *Service) DeleteBranchOperation(ctx context.Context, branchID int64, operationID int64) error { + return s.branchStore.DeleteBranchOperation(ctx, branchID, operationID) +} diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 9d83592..5f47b7c 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -61,10 +61,8 @@ func convertBet(bet domain.Bet) BetRes { // @Router /bet [post] func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { - - // TODO: Check the token, and find the role and get the branch id from there - - // TODO Reduce amount from the branch wallet + // TODO if user is customer, get id from the token then get the wallet id from there + // TODO: If user is a cashier, check the token, and find the role and get the branch id from there. Reduce amount from the branch wallet var isShopBet bool = true var branchID int64 = 1 diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go new file mode 100644 index 0000000..50c4a0a --- /dev/null +++ b/internal/web_server/handlers/branch_handler.go @@ -0,0 +1,420 @@ +package handlers + +import ( + "log/slog" + "strconv" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" + "github.com/gofiber/fiber/v2" +) + +type CreateBranchReq struct { + Name string `json:"name" example:"4-kilo Branch"` + Location string `json:"location" example:"Addis Ababa"` + WalletID int64 `json:"wallet_id" example:"1"` + BranchManagerID int64 `json:"branch_manager_id" example:"1"` + CompanyID int64 `json:"company_id" example:"1"` + IsSelfOwned bool `json:"is_self_owned" example:"false"` +} + +type CreateSupportedOperationReq struct { + Name string `json:"name" example:"SportsBook"` + Description string `json:"description" example:"Betting on sport events"` +} + +type SupportedOperationRes struct { + ID int64 `json:"id" example:"1"` + Name string `json:"name" example:"SportsBook"` + Description string `json:"description" example:"Betting on sport events"` +} + +type CreateBranchOperationReq struct { + BranchID int64 `json:"branch_id" example:"1"` + OperationID int64 `json:"operation_id" example:"1"` +} + +type BranchOperationRes struct { + Name string `json:"name" example:"SportsBook"` + Description string `json:"description" example:"Betting on sport events"` +} + +type BranchRes struct { + Name string `json:"name" example:"4-kilo Branch"` + Location string `json:"location" example:"Addis Ababa"` + WalletID int64 `json:"wallet_id" example:"1"` + BranchManagerID int64 `json:"branch_manager_id" example:"1"` + CompanyID int64 `json:"company_id" example:"1"` + IsSelfOwned bool `json:"is_self_owned" example:"false"` +} + +type BranchDetailRes struct { + Name string `json:"name" example:"4-kilo Branch"` + Location string `json:"location" example:"Addis Ababa"` + WalletID int64 `json:"wallet_id" example:"1"` + BranchManagerID int64 `json:"branch_manager_id" example:"1"` + CompanyID int64 `json:"company_id" example:"1"` + IsSelfOwned bool `json:"is_self_owned" example:"false"` + ManagerName string `json:"manager_name" example:"John Smith"` + ManagerPhoneNumber string `json:"manager_phone_number" example:"0911111111"` +} + +func convertBranch(branch domain.Branch) BranchRes { + return BranchRes{ + Name: branch.Name, + Location: branch.Location, + WalletID: branch.WalletID, + BranchManagerID: branch.BranchManagerID, + CompanyID: branch.CompanyID, + IsSelfOwned: branch.IsSelfOwned, + } +} + +func convertBranchDetail(branch domain.BranchDetail) BranchDetailRes { + return BranchDetailRes{ + Name: branch.Name, + Location: branch.Location, + WalletID: branch.WalletID, + BranchManagerID: branch.BranchManagerID, + CompanyID: branch.CompanyID, + IsSelfOwned: branch.IsSelfOwned, + ManagerName: branch.ManagerName, + ManagerPhoneNumber: branch.ManagerPhoneNumber, + } +} + +func CreateBranch(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + // Check if user is either branch manager / super main + // role := string(c.Locals("role").(domain.Role)) + + // if role != string(domain.RoleCustomer) { + // logger.Error("Unauthorized access", "role", role) + // return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) + // } + + var req CreateBranchReq + + if err := c.BodyParser(&req); err != nil { + logger.Error("CreateBranchReq failed", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request", + }) + } + + valErrs, ok := validator.Validate(c, req) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + + branch, err := branchSvc.CreateBranch(c.Context(), domain.CreateBranch{ + Name: req.Name, + Location: req.Location, + WalletID: req.WalletID, + BranchManagerID: req.BranchManagerID, + CompanyID: req.CompanyID, + IsSelfOwned: req.IsSelfOwned, + }) + + if err != nil { + logger.Error("CreateBranchReq failed", "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Internal server error", + }) + } + + res := convertBranch(branch) + + return response.WriteJSON(c, fiber.StatusOK, "Branch Created", res, nil) + + } + +} + +func CreateSupportedOperation(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + var req CreateSupportedOperationReq + + if err := c.BodyParser(&req); err != nil { + logger.Error("CreateBranchReq failed", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request", + }) + } + valErrs, ok := validator.Validate(c, req) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + operation, err := branchSvc.CreateSupportedOperation(c.Context(), domain.CreateSupportedOperation{ + Name: req.Name, + Description: req.Description, + }) + + if err != nil { + logger.Error("CreateSupportedOperationReq failed", "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Internal server error", + }) + } + + res := SupportedOperationRes{ + Name: operation.Name, + Description: operation.Description, + } + + return response.WriteJSON(c, fiber.StatusOK, "Operation Created", res, nil) + + } +} + +func CreateBranchOperation(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + var req CreateBranchOperationReq + if err := c.BodyParser(&req); err != nil { + logger.Error("CreateBranchOperationReq failed", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request", + }) + } + + valErrs, ok := validator.Validate(c, req) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + + err := branchSvc.CreateBranchOperation(c.Context(), domain.CreateBranchOperation{ + BranchID: req.BranchID, + OperationID: req.OperationID, + }) + + if err != nil { + logger.Error("CreateBranchOperationReq failed", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Branch Operation Created", nil, nil) + + } +} + +func GetBranchByID(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + branchID := c.Params("id") + id, err := strconv.ParseInt(branchID, 10, 64) + if err != nil { + logger.Error("Invalid branch ID", "branchID", branchID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid branch ID", err, nil) + } + + branch, err := branchSvc.GetBranchByID(c.Context(), id) + + if err != nil { + logger.Error("Failed to get branch by ID", "branchID", id, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve branch", err, nil) + } + + res := convertBranchDetail(branch) + + return response.WriteJSON(c, fiber.StatusOK, "Branch retrieved successfully", res, nil) + + } +} + +// /user/:id/branch +func GetBranchByManagerID(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + userID := c.Params("id") + id, err := strconv.ParseInt(userID, 10, 64) + if err != nil { + logger.Error("Invalid user ID", "userID", userID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid user ID", err, nil) + } + + branches, err := branchSvc.GetBranchByManagerID(c.Context(), id) + + if err != nil { + logger.Error("Failed to get branches", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get branches", err, nil) + } + var result []BranchDetailRes = make([]BranchDetailRes, len(branches)) + for _, branch := range branches { + result = append(result, convertBranchDetail(branch)) + } + return response.WriteJSON(c, fiber.StatusOK, "Branches for Branch Manager retrieved", result, nil) + } +} + +// /company/:id/branch +func GetBranchByCompanyID(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + companyID := c.Params("id") + id, err := strconv.ParseInt(companyID, 10, 64) + if err != nil { + logger.Error("Invalid company ID", "companyID", companyID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid company ID", err, nil) + } + + branches, err := branchSvc.GetBranchByCompanyID(c.Context(), id) + if err != nil { + logger.Error("Failed to get branches", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get branches", err, nil) + } + + var result []BranchDetailRes = make([]BranchDetailRes, len(branches)) + for _, branch := range branches { + result = append(result, convertBranchDetail(branch)) + } + return response.WriteJSON(c, fiber.StatusOK, "Branches for Company retrieved", result, nil) + } +} + +func GetAllBranches(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + branches, err := branchSvc.GetAllBranches(c.Context()) + + if err != nil { + logger.Error("Failed to get branches", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get branches", err, nil) + } + + var result []BranchDetailRes = make([]BranchDetailRes, len(branches)) + for _, branch := range branches { + result = append(result, convertBranchDetail(branch)) + } + return response.WriteJSON(c, fiber.StatusOK, "Branches for Company retrieved", result, nil) + + } +} + +func GetBranchOperations(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + + branchID := c.Params("id") + id, err := strconv.ParseInt(branchID, 10, 64) + if err != nil { + logger.Error("Invalid branch ID", "branchID", branchID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid branch ID", err, nil) + } + + operations, err := branchSvc.GetBranchOperations(c.Context(), id) + + if err != nil { + logger.Error("Failed to get operation by ID", "branchID", id, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve operation", err, nil) + } + + var result []BranchOperationRes = make([]BranchOperationRes, len(operations)) + + for _, branch := range operations { + result = append(result, BranchOperationRes{ + Name: branch.OperationName, + Description: branch.OperationDescription, + }) + } + + return response.WriteJSON(c, fiber.StatusOK, "Branch Operations retrieved successfully", result, nil) + } +} + +func UpdateBranch(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + branchID := c.Params("id") + id, err := strconv.ParseInt(branchID, 10, 64) + if err != nil { + logger.Error("Invalid branch ID", "branchID", branchID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid branch ID", err, nil) + } + + var req CreateBranchReq + + if err := c.BodyParser(&req); err != nil { + logger.Error("CreateBetReq failed", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request", + }) + } + valErrs, ok := validator.Validate(c, req) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + + branch, err := branchSvc.UpdateBranch(c.Context(), id, domain.CreateBranch{ + Name: req.Name, + Location: req.Location, + WalletID: req.WalletID, + BranchManagerID: req.BranchManagerID, + CompanyID: req.CompanyID, + IsSelfOwned: req.IsSelfOwned, + }) + + if err != nil { + logger.Error("Failed to update branch", "branchID", id, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update branch", err, nil) + } + + res := convertBranch(branch) + + return response.WriteJSON(c, fiber.StatusOK, "Branch Updated", res, nil) + + } +} + +func DeleteBranch(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + + branchID := c.Params("id") + id, err := strconv.ParseInt(branchID, 10, 64) + + if err != nil { + logger.Error("Invalid Branch ID", "branchID", branchID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid Branch ID", err, nil) + } + + err = branchSvc.DeleteBranch(c.Context(), id) + + if err != nil { + logger.Error("Failed to delete by ID", "Branch ID", id, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to Delete Branch", err, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Branch removed successfully", nil, nil) + + } +} + +func DeleteBranchOperation(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + branchID := c.Params("id") + opID := c.Params("opID") + + id, err := strconv.ParseInt(branchID, 10, 64) + + if err != nil { + logger.Error("Invalid Branch ID", "branchID", branchID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid Branch ID", err, nil) + } + + operationID, err := strconv.ParseInt(opID, 10, 64) + + if err != nil { + logger.Error("Invalid Operation ID", "operationID", opID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid Operation ID", err, nil) + } + + err = branchSvc.DeleteBranchOperation(c.Context(), id, operationID) + + if err != nil { + logger.Error("Failed to delete operation", "Branch ID", id, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to Delete Operation", err, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Branch Operation removed successfully", nil, nil) + + } +} diff --git a/internal/web_server/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index 3f6cb67..49412cc 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -179,17 +179,17 @@ func GetCustomerWallet(logger *slog.Logger, walletSvc *wallet.Service, validator userId := c.Locals("user_id").(int64) role := string(c.Locals("role").(domain.Role)) - companyID, err := strconv.ParseInt(c.Get("company_id"), 10, 64) + vendorID, err := strconv.ParseInt(c.Get("vendor_id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).SendString("Invalid company_id") } - logger.Info("Company ID: " + strconv.FormatInt(companyID, 10)) + logger.Info("Company ID: " + strconv.FormatInt(vendorID, 10)) if role != string(domain.RoleCustomer) { logger.Error("Unauthorized access", "userId", userId, "role", role) return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) } - wallet, err := walletSvc.GetCustomerWallet(c.Context(), userId, companyID) + wallet, err := walletSvc.GetCustomerWallet(c.Context(), userId, vendorID) if err != nil { logger.Error("Failed to get customer wallet", "userId", userId, "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve wallet", err, nil) diff --git a/sqlc.yaml b/sqlc.yaml index 6f394a6..bd998c2 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -16,3 +16,4 @@ sql: - db_type: "uuid" go_type: "github.com/google/uuid.NullUUID" nullable: true + From c9df0b330328e2ef3a76588ac649166cae331f62 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 8 Apr 2025 23:18:53 +0300 Subject: [PATCH 02/30] transfer and branch service implementation --- cmd/main.go | 6 +- db/migrations/000001_fortune.down.sql | 2 + db/migrations/000001_fortune.up.sql | 9 +- db/query/transactions.sql | 5 +- db/query/transfer.sql | 4 +- db/query/wallet.sql | 2 +- docs/docs.go | 748 ++++++++++++++++++ docs/swagger.json | 748 ++++++++++++++++++ docs/swagger.yaml | 505 ++++++++++++ gen/db/models.go | 35 +- gen/db/transactions.sql.go | 41 + gen/db/transfer.sql.go | 58 +- gen/db/wallet.sql.go | 26 +- internal/domain/branch.go | 8 + internal/domain/transfer.go | 48 +- internal/domain/wallet.go | 2 + internal/repository/branch.go | 2 +- internal/repository/transaction.go | 13 + internal/repository/transfer.go | 147 ++-- internal/repository/wallet.go | 24 +- internal/services/branch/port.go | 2 +- internal/services/branch/service.go | 4 +- internal/services/transaction/port.go | 1 + internal/services/transaction/service.go | 3 + internal/services/transfer/chapa.go | 1 - internal/services/transfer/port.go | 15 - internal/services/transfer/service.go | 33 - internal/services/wallet/chapa.go | 1 + internal/services/wallet/port.go | 9 + internal/services/wallet/service.go | 99 +-- internal/services/wallet/transfer.go | 86 ++ internal/services/wallet/wallet.go | 93 +++ internal/web_server/app.go | 12 + .../web_server/handlers/branch_handler.go | 157 +++- .../handlers/transaction_handler.go | 45 +- .../web_server/handlers/transfer_handler.go | 116 +++ .../web_server/handlers/wallet_handler.go | 117 +-- internal/web_server/middleware.go | 6 + internal/web_server/routes.go | 25 +- 39 files changed, 2886 insertions(+), 372 deletions(-) delete mode 100644 internal/services/transfer/chapa.go delete mode 100644 internal/services/transfer/port.go delete mode 100644 internal/services/transfer/service.go create mode 100644 internal/services/wallet/chapa.go create mode 100644 internal/services/wallet/transfer.go create mode 100644 internal/services/wallet/wallet.go create mode 100644 internal/web_server/handlers/transfer_handler.go diff --git a/cmd/main.go b/cmd/main.go index 7ac39e6..250a2b9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -12,6 +12,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" @@ -59,8 +60,9 @@ func main() { userSvc := user.NewService(store, store, mockSms, mockemail) ticketSvc := ticket.NewService(store) betSvc := bet.NewService(store) - walletSvc := wallet.NewService(store) + walletSvc := wallet.NewService(store, store) transactionSvc := transaction.NewService(store) + branchSvc := branch.NewService(store) notificationRepo := repository.NewNotificationRepository(store) notificationSvc := notificationservice.New(notificationRepo, logger, cfg) @@ -68,7 +70,7 @@ func main() { app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, - }, userSvc, ticketSvc, betSvc, walletSvc, transactionSvc, notificationSvc, + }, userSvc, ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, notificationSvc, ) logger.Info("Starting server", "port", cfg.Port) diff --git a/db/migrations/000001_fortune.down.sql b/db/migrations/000001_fortune.down.sql index e4d5b9a..a061f7f 100644 --- a/db/migrations/000001_fortune.down.sql +++ b/db/migrations/000001_fortune.down.sql @@ -80,4 +80,6 @@ DROP TABLE IF EXISTS wallets; DROP TABLE IF EXISTS wallet_transfer; DROP TABLE IF EXISTS transactions; DROP TABLE IF EXISTS customer_wallets; +DROP TABLE IF EXISTS branches; + diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 59140c2..27ae8b1 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -78,6 +78,7 @@ CREATE TABLE IF NOT EXISTS wallets ( balance BIGINT NOT NULL DEFAULT 0, is_withdraw BOOLEAN NOT NULL, is_bettable BOOLEAN NOT NULL, + is_transferable BOOLEAN NOT NULL, user_id BIGINT NOT NULL, is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, @@ -99,10 +100,12 @@ CREATE TABLE IF NOT EXISTS customer_wallets ( CREATE TABLE IF NOT EXISTS wallet_transfer ( id BIGSERIAL PRIMARY KEY, amount BIGINT NOT NULL, - wallet_transfer VARCHAR(255) NOT NULL, - wallet_id BIGINT NOT NULL, + type VARCHAR(255) NOT NULL, + receiver_wallet_id BIGINT NOT NULL, + sender_wallet_id BIGINT, + cashier_id BIGINT, verified BOOLEAN NOT NULL DEFAULT false, - payment_method INT NOT NULL, + payment_method VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); diff --git a/db/query/transactions.sql b/db/query/transactions.sql index 2665eaa..75adba4 100644 --- a/db/query/transactions.sql +++ b/db/query/transactions.sql @@ -4,9 +4,12 @@ INSERT INTO transactions (amount, branch_id, cashier_id, bet_id, payment_option, -- name: GetAllTransactions :many SELECT * FROM transactions; --- name: GetTransactionByID :one +-- name: GetTransactionByID :one SELECT * FROM transactions WHERE id = $1; +-- name: GetTransactionByBranch :many +SELECT * FROM transactions WHERE branch_id = $1; + -- name: UpdateTransactionVerified :exec UPDATE transactions SET verified = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1; diff --git a/db/query/transfer.sql b/db/query/transfer.sql index 895ccb8..62007d6 100644 --- a/db/query/transfer.sql +++ b/db/query/transfer.sql @@ -1,11 +1,11 @@ -- name: CreateTransfer :one -INSERT INTO wallet_transfer (amount, wallet_transfer, wallet_id) VALUES ($1, $2, $3) RETURNING *; +INSERT INTO wallet_transfer (amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; -- name: GetAllTransfers :many SELECT * FROM wallet_transfer; -- name: GetTransfersByWallet :many -SELECT * FROM wallet_transfer WHERE wallet_id = $1; +SELECT * FROM wallet_transfer WHERE receiver_wallet_id = $1 OR sender_wallet_id = $1; -- name: GetTransferByID :one SELECT * FROM wallet_transfer WHERE id = $1; diff --git a/db/query/wallet.sql b/db/query/wallet.sql index 46f3200..dc025e9 100644 --- a/db/query/wallet.sql +++ b/db/query/wallet.sql @@ -1,5 +1,5 @@ -- name: CreateWallet :one -INSERT INTO wallets (is_withdraw, is_bettable, user_id) VALUES ($1, $2, $3) RETURNING *; +INSERT INTO wallets (is_withdraw, is_bettable, is_transferable, user_id) VALUES ($1, $2, $3, $4) RETURNING *; -- name: CreateCustomerWallet :one INSERT INTO customer_wallets (customer_id, company_id, regular_wallet_id, static_wallet_id) VALUES ($1, $2, $3, $4) RETURNING *; diff --git a/docs/docs.go b/docs/docs.go index 36464a6..8cbb6b4 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -399,6 +399,509 @@ const docTemplate = `{ } } }, + "/branch": { + "get": { + "description": "Gets all branches", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets all branches", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BranchDetailRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Creates a branch", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Create a branch", + "parameters": [ + { + "description": "Creates branch", + "name": "createBranch", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateBranchReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BranchRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/branch/{id}": { + "get": { + "description": "Gets a single branch by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets branch by id", + "parameters": [ + { + "type": "integer", + "description": "Branch ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BranchDetailRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "description": "Updates a branch", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Updates a branch", + "parameters": [ + { + "type": "integer", + "description": "Branch ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update Branch", + "name": "updateBranch", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateBranchReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BranchRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "description": "Delete the branch", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Delete the branch", + "parameters": [ + { + "type": "integer", + "description": "Branch 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" + } + } + } + } + }, + "/branch/{id}/operation": { + "get": { + "description": "Gets branch operations", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets branch operations", + "parameters": [ + { + "type": "integer", + "description": "Branch ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BranchOperationRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/branch/{id}/operation/{opID}": { + "delete": { + "description": "Delete the branch operation", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Delete the branch operation", + "parameters": [ + { + "type": "integer", + "description": "Branch ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Branch Operation ID", + "name": "opID", + "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" + } + } + } + } + }, + "/company/{id}/branch": { + "get": { + "description": "Gets branches by company id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets branches by company id", + "parameters": [ + { + "type": "integer", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BranchDetailRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/manager/{id}/branch": { + "get": { + "description": "Gets a branches by manager id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets branches by manager id", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BranchDetailRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/operation": { + "post": { + "description": "Creates a operation", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Create a operation", + "parameters": [ + { + "description": "Creates operation", + "name": "createBranchOperation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateBranchOperationReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BranchOperationRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/supportedOperation": { + "post": { + "description": "Creates a supported operation", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Create a supported operation", + "parameters": [ + { + "description": "Creates supported operation", + "name": "createSupportedOperation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateSupportedOperationReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SupportedOperationRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/ticket": { "get": { "description": "Retrieve all tickets", @@ -702,6 +1205,52 @@ const docTemplate = `{ } } }, + "/transfer/wallet": { + "post": { + "description": "Create a transfer to wallet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transfer" + ], + "summary": "Create a transfer to wallet", + "parameters": [ + { + "description": "Create Transfer", + "name": "transferToWallet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateTransferReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.TransferRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/user/checkPhoneEmailExist": { "post": { "description": "Check if phone number or email exist", @@ -1257,6 +1806,85 @@ const docTemplate = `{ } } }, + "handlers.BranchDetailRes": { + "type": "object", + "properties": { + "branch_manager_id": { + "type": "integer", + "example": 1 + }, + "company_id": { + "type": "integer", + "example": 1 + }, + "is_self_owned": { + "type": "boolean", + "example": false + }, + "location": { + "type": "string", + "example": "Addis Ababa" + }, + "manager_name": { + "type": "string", + "example": "John Smith" + }, + "manager_phone_number": { + "type": "string", + "example": "0911111111" + }, + "name": { + "type": "string", + "example": "4-kilo Branch" + }, + "wallet_id": { + "type": "integer", + "example": 1 + } + } + }, + "handlers.BranchOperationRes": { + "type": "object", + "properties": { + "description": { + "type": "string", + "example": "Betting on sport events" + }, + "name": { + "type": "string", + "example": "SportsBook" + } + } + }, + "handlers.BranchRes": { + "type": "object", + "properties": { + "branch_manager_id": { + "type": "integer", + "example": 1 + }, + "company_id": { + "type": "integer", + "example": 1 + }, + "is_self_owned": { + "type": "boolean", + "example": false + }, + "location": { + "type": "string", + "example": "Addis Ababa" + }, + "name": { + "type": "string", + "example": "4-kilo Branch" + }, + "wallet_id": { + "type": "integer", + "example": 1 + } + } + }, "handlers.CheckPhoneEmailExistReq": { "type": "object", "properties": { @@ -1320,6 +1948,57 @@ const docTemplate = `{ } } }, + "handlers.CreateBranchOperationReq": { + "type": "object", + "properties": { + "branch_id": { + "type": "integer", + "example": 1 + }, + "operation_id": { + "type": "integer", + "example": 1 + } + } + }, + "handlers.CreateBranchReq": { + "type": "object", + "properties": { + "branch_manager_id": { + "type": "integer", + "example": 1 + }, + "company_id": { + "type": "integer", + "example": 1 + }, + "is_self_owned": { + "type": "boolean", + "example": false + }, + "location": { + "type": "string", + "example": "Addis Ababa" + }, + "name": { + "type": "string", + "example": "4-kilo Branch" + } + } + }, + "handlers.CreateSupportedOperationReq": { + "type": "object", + "properties": { + "description": { + "type": "string", + "example": "Betting on sport events" + }, + "name": { + "type": "string", + "example": "SportsBook" + } + } + }, "handlers.CreateTicketReq": { "type": "object", "properties": { @@ -1401,6 +2080,9 @@ const docTemplate = `{ } } }, + "handlers.CreateTransferReq": { + "type": "object" + }, "handlers.CustomerWalletRes": { "type": "object", "properties": { @@ -1520,6 +2202,23 @@ const docTemplate = `{ } } }, + "handlers.SupportedOperationRes": { + "type": "object", + "properties": { + "description": { + "type": "string", + "example": "Betting on sport events" + }, + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "SportsBook" + } + } + }, "handlers.TicketRes": { "type": "object", "properties": { @@ -1603,6 +2302,51 @@ const docTemplate = `{ } } }, + "handlers.TransferRes": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "cashier_id": { + "type": "integer", + "example": 789 + }, + "created_at": { + "type": "string", + "example": "2025-04-08T12:00:00Z" + }, + "id": { + "type": "integer", + "example": 1 + }, + "payment_method": { + "type": "string", + "example": "bank" + }, + "receiver_wallet_id": { + "type": "integer", + "example": 1 + }, + "sender_wallet_id": { + "type": "integer", + "example": 1 + }, + "type": { + "type": "string", + "example": "transfer" + }, + "updated_at": { + "type": "string", + "example": "2025-04-08T12:30:00Z" + }, + "verified": { + "type": "boolean", + "example": true + } + } + }, "handlers.UpdateCashOutReq": { "type": "object", "properties": { @@ -1690,6 +2434,10 @@ const docTemplate = `{ "type": "boolean", "example": true }, + "is_transferable": { + "type": "boolean", + "example": true + }, "is_withdraw": { "type": "boolean", "example": true diff --git a/docs/swagger.json b/docs/swagger.json index cc51adb..21d28dc 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -391,6 +391,509 @@ } } }, + "/branch": { + "get": { + "description": "Gets all branches", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets all branches", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BranchDetailRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Creates a branch", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Create a branch", + "parameters": [ + { + "description": "Creates branch", + "name": "createBranch", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateBranchReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BranchRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/branch/{id}": { + "get": { + "description": "Gets a single branch by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets branch by id", + "parameters": [ + { + "type": "integer", + "description": "Branch ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BranchDetailRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "description": "Updates a branch", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Updates a branch", + "parameters": [ + { + "type": "integer", + "description": "Branch ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update Branch", + "name": "updateBranch", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateBranchReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BranchRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "description": "Delete the branch", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Delete the branch", + "parameters": [ + { + "type": "integer", + "description": "Branch 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" + } + } + } + } + }, + "/branch/{id}/operation": { + "get": { + "description": "Gets branch operations", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets branch operations", + "parameters": [ + { + "type": "integer", + "description": "Branch ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BranchOperationRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/branch/{id}/operation/{opID}": { + "delete": { + "description": "Delete the branch operation", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Delete the branch operation", + "parameters": [ + { + "type": "integer", + "description": "Branch ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Branch Operation ID", + "name": "opID", + "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" + } + } + } + } + }, + "/company/{id}/branch": { + "get": { + "description": "Gets branches by company id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets branches by company id", + "parameters": [ + { + "type": "integer", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BranchDetailRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/manager/{id}/branch": { + "get": { + "description": "Gets a branches by manager id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets branches by manager id", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BranchDetailRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/operation": { + "post": { + "description": "Creates a operation", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Create a operation", + "parameters": [ + { + "description": "Creates operation", + "name": "createBranchOperation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateBranchOperationReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BranchOperationRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/supportedOperation": { + "post": { + "description": "Creates a supported operation", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Create a supported operation", + "parameters": [ + { + "description": "Creates supported operation", + "name": "createSupportedOperation", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateSupportedOperationReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SupportedOperationRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/ticket": { "get": { "description": "Retrieve all tickets", @@ -694,6 +1197,52 @@ } } }, + "/transfer/wallet": { + "post": { + "description": "Create a transfer to wallet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transfer" + ], + "summary": "Create a transfer to wallet", + "parameters": [ + { + "description": "Create Transfer", + "name": "transferToWallet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateTransferReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.TransferRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/user/checkPhoneEmailExist": { "post": { "description": "Check if phone number or email exist", @@ -1249,6 +1798,85 @@ } } }, + "handlers.BranchDetailRes": { + "type": "object", + "properties": { + "branch_manager_id": { + "type": "integer", + "example": 1 + }, + "company_id": { + "type": "integer", + "example": 1 + }, + "is_self_owned": { + "type": "boolean", + "example": false + }, + "location": { + "type": "string", + "example": "Addis Ababa" + }, + "manager_name": { + "type": "string", + "example": "John Smith" + }, + "manager_phone_number": { + "type": "string", + "example": "0911111111" + }, + "name": { + "type": "string", + "example": "4-kilo Branch" + }, + "wallet_id": { + "type": "integer", + "example": 1 + } + } + }, + "handlers.BranchOperationRes": { + "type": "object", + "properties": { + "description": { + "type": "string", + "example": "Betting on sport events" + }, + "name": { + "type": "string", + "example": "SportsBook" + } + } + }, + "handlers.BranchRes": { + "type": "object", + "properties": { + "branch_manager_id": { + "type": "integer", + "example": 1 + }, + "company_id": { + "type": "integer", + "example": 1 + }, + "is_self_owned": { + "type": "boolean", + "example": false + }, + "location": { + "type": "string", + "example": "Addis Ababa" + }, + "name": { + "type": "string", + "example": "4-kilo Branch" + }, + "wallet_id": { + "type": "integer", + "example": 1 + } + } + }, "handlers.CheckPhoneEmailExistReq": { "type": "object", "properties": { @@ -1312,6 +1940,57 @@ } } }, + "handlers.CreateBranchOperationReq": { + "type": "object", + "properties": { + "branch_id": { + "type": "integer", + "example": 1 + }, + "operation_id": { + "type": "integer", + "example": 1 + } + } + }, + "handlers.CreateBranchReq": { + "type": "object", + "properties": { + "branch_manager_id": { + "type": "integer", + "example": 1 + }, + "company_id": { + "type": "integer", + "example": 1 + }, + "is_self_owned": { + "type": "boolean", + "example": false + }, + "location": { + "type": "string", + "example": "Addis Ababa" + }, + "name": { + "type": "string", + "example": "4-kilo Branch" + } + } + }, + "handlers.CreateSupportedOperationReq": { + "type": "object", + "properties": { + "description": { + "type": "string", + "example": "Betting on sport events" + }, + "name": { + "type": "string", + "example": "SportsBook" + } + } + }, "handlers.CreateTicketReq": { "type": "object", "properties": { @@ -1393,6 +2072,9 @@ } } }, + "handlers.CreateTransferReq": { + "type": "object" + }, "handlers.CustomerWalletRes": { "type": "object", "properties": { @@ -1512,6 +2194,23 @@ } } }, + "handlers.SupportedOperationRes": { + "type": "object", + "properties": { + "description": { + "type": "string", + "example": "Betting on sport events" + }, + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "SportsBook" + } + } + }, "handlers.TicketRes": { "type": "object", "properties": { @@ -1595,6 +2294,51 @@ } } }, + "handlers.TransferRes": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "cashier_id": { + "type": "integer", + "example": 789 + }, + "created_at": { + "type": "string", + "example": "2025-04-08T12:00:00Z" + }, + "id": { + "type": "integer", + "example": 1 + }, + "payment_method": { + "type": "string", + "example": "bank" + }, + "receiver_wallet_id": { + "type": "integer", + "example": 1 + }, + "sender_wallet_id": { + "type": "integer", + "example": 1 + }, + "type": { + "type": "string", + "example": "transfer" + }, + "updated_at": { + "type": "string", + "example": "2025-04-08T12:30:00Z" + }, + "verified": { + "type": "boolean", + "example": true + } + } + }, "handlers.UpdateCashOutReq": { "type": "object", "properties": { @@ -1682,6 +2426,10 @@ "type": "boolean", "example": true }, + "is_transferable": { + "type": "boolean", + "example": true + }, "is_withdraw": { "type": "boolean", "example": true diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 9b8b3d4..e6031ac 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -74,6 +74,63 @@ definitions: example: 2 type: integer type: object + handlers.BranchDetailRes: + properties: + branch_manager_id: + example: 1 + type: integer + company_id: + example: 1 + type: integer + is_self_owned: + example: false + type: boolean + location: + example: Addis Ababa + type: string + manager_name: + example: John Smith + type: string + manager_phone_number: + example: "0911111111" + type: string + name: + example: 4-kilo Branch + type: string + wallet_id: + example: 1 + type: integer + type: object + handlers.BranchOperationRes: + properties: + description: + example: Betting on sport events + type: string + name: + example: SportsBook + type: string + type: object + handlers.BranchRes: + properties: + branch_manager_id: + example: 1 + type: integer + company_id: + example: 1 + type: integer + is_self_owned: + example: false + type: boolean + location: + example: Addis Ababa + type: string + name: + example: 4-kilo Branch + type: string + wallet_id: + example: 1 + type: integer + type: object handlers.CheckPhoneEmailExistReq: properties: email: @@ -116,6 +173,42 @@ definitions: example: 4.22 type: number type: object + handlers.CreateBranchOperationReq: + properties: + branch_id: + example: 1 + type: integer + operation_id: + example: 1 + type: integer + type: object + handlers.CreateBranchReq: + properties: + branch_manager_id: + example: 1 + type: integer + company_id: + example: 1 + type: integer + is_self_owned: + example: false + type: boolean + location: + example: Addis Ababa + type: string + name: + example: 4-kilo Branch + type: string + type: object + handlers.CreateSupportedOperationReq: + properties: + description: + example: Betting on sport events + type: string + name: + example: SportsBook + type: string + type: object handlers.CreateTicketReq: properties: amount: @@ -171,6 +264,8 @@ definitions: reference_number: type: string type: object + handlers.CreateTransferReq: + type: object handlers.CustomerWalletRes: properties: company_id: @@ -255,6 +350,18 @@ definitions: phoneNumber: type: string type: object + handlers.SupportedOperationRes: + properties: + description: + example: Betting on sport events + type: string + id: + example: 1 + type: integer + name: + example: SportsBook + type: string + type: object handlers.TicketRes: properties: amount: @@ -312,6 +419,39 @@ definitions: example: true type: boolean type: object + handlers.TransferRes: + properties: + amount: + example: 100 + type: number + cashier_id: + example: 789 + type: integer + created_at: + example: "2025-04-08T12:00:00Z" + type: string + id: + example: 1 + type: integer + payment_method: + example: bank + type: string + receiver_wallet_id: + example: 1 + type: integer + sender_wallet_id: + example: 1 + type: integer + type: + example: transfer + type: string + updated_at: + example: "2025-04-08T12:30:00Z" + type: string + verified: + example: true + type: boolean + type: object handlers.UpdateCashOutReq: properties: cashedOut: @@ -370,6 +510,9 @@ definitions: is_bettable: example: true type: boolean + is_transferable: + example: true + type: boolean is_withdraw: example: true type: boolean @@ -689,6 +832,338 @@ paths: summary: Updates the cashed out field tags: - bet + /branch: + get: + consumes: + - application/json + description: Gets all branches + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.BranchDetailRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets all branches + tags: + - branch + post: + consumes: + - application/json + description: Creates a branch + parameters: + - description: Creates branch + in: body + name: createBranch + required: true + schema: + $ref: '#/definitions/handlers.CreateBranchReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.BranchRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Create a branch + tags: + - branch + /branch/{id}: + delete: + consumes: + - application/json + description: Delete the branch + parameters: + - description: Branch 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 the branch + tags: + - branch + get: + consumes: + - application/json + description: Gets a single branch by id + parameters: + - description: Branch ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.BranchDetailRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets branch by id + tags: + - branch + put: + consumes: + - application/json + description: Updates a branch + parameters: + - description: Branch ID + in: path + name: id + required: true + type: integer + - description: Update Branch + in: body + name: updateBranch + required: true + schema: + $ref: '#/definitions/handlers.CreateBranchReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.BranchRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Updates a branch + tags: + - branch + /branch/{id}/operation: + get: + consumes: + - application/json + description: Gets branch operations + parameters: + - description: Branch ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.BranchOperationRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets branch operations + tags: + - branch + /branch/{id}/operation/{opID}: + delete: + consumes: + - application/json + description: Delete the branch operation + parameters: + - description: Branch ID + in: path + name: id + required: true + type: integer + - description: Branch Operation ID + in: path + name: opID + 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 the branch operation + tags: + - branch + /company/{id}/branch: + get: + consumes: + - application/json + description: Gets branches by company id + parameters: + - description: Company ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.BranchDetailRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets branches by company id + tags: + - branch + /manager/{id}/branch: + get: + consumes: + - application/json + description: Gets a branches by manager id + parameters: + - description: User ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.BranchDetailRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets branches by manager id + tags: + - branch + /operation: + post: + consumes: + - application/json + description: Creates a operation + parameters: + - description: Creates operation + in: body + name: createBranchOperation + required: true + schema: + $ref: '#/definitions/handlers.CreateBranchOperationReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.BranchOperationRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Create a operation + tags: + - branch + /supportedOperation: + post: + consumes: + - application/json + description: Creates a supported operation + parameters: + - description: Creates supported operation + in: body + name: createSupportedOperation + required: true + schema: + $ref: '#/definitions/handlers.CreateSupportedOperationReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SupportedOperationRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Create a supported operation + tags: + - branch /ticket: get: consumes: @@ -889,6 +1364,36 @@ paths: summary: Updates the cashed out field tags: - transaction + /transfer/wallet: + post: + consumes: + - application/json + description: Create a transfer to wallet + parameters: + - description: Create Transfer + in: body + name: transferToWallet + required: true + schema: + $ref: '#/definitions/handlers.CreateTransferReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.TransferRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Create a transfer to wallet + tags: + - transfer /user/checkPhoneEmailExist: post: consumes: diff --git a/gen/db/models.go b/gen/db/models.go index 2f66efc..8e449a6 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -155,23 +155,26 @@ type User struct { } type Wallet struct { - ID int64 - Balance int64 - IsWithdraw bool - IsBettable bool - UserID int64 - IsActive bool - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp -} - -type WalletTransfer struct { ID int64 - Amount int64 - WalletTransfer string - WalletID int64 - Verified bool - PaymentMethod int32 + Balance int64 + IsWithdraw bool + IsBettable bool + IsTransferable bool + UserID int64 + IsActive bool CreatedAt pgtype.Timestamp UpdatedAt pgtype.Timestamp } + +type WalletTransfer struct { + ID int64 + Amount int64 + Type string + ReceiverWalletID int64 + SenderWalletID pgtype.Int8 + CashierID pgtype.Int8 + Verified bool + PaymentMethod string + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} diff --git a/gen/db/transactions.sql.go b/gen/db/transactions.sql.go index 31b535c..4f5fbe1 100644 --- a/gen/db/transactions.sql.go +++ b/gen/db/transactions.sql.go @@ -106,6 +106,47 @@ func (q *Queries) GetAllTransactions(ctx context.Context) ([]Transaction, error) return items, nil } +const GetTransactionByBranch = `-- name: GetTransactionByBranch :many +SELECT id, amount, branch_id, cashier_id, bet_id, 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 +` + +func (q *Queries) GetTransactionByBranch(ctx context.Context, branchID int64) ([]Transaction, error) { + rows, err := q.db.Query(ctx, GetTransactionByBranch, branchID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Transaction + for rows.Next() { + var i Transaction + if err := rows.Scan( + &i.ID, + &i.Amount, + &i.BranchID, + &i.CashierID, + &i.BetID, + &i.PaymentOption, + &i.FullName, + &i.PhoneNumber, + &i.BankCode, + &i.BeneficiaryName, + &i.AccountName, + &i.AccountNumber, + &i.ReferenceNumber, + &i.Verified, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetTransactionByID = `-- name: GetTransactionByID :one SELECT id, amount, branch_id, cashier_id, bet_id, 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 ` diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index 770f23c..9faeaf5 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -7,26 +7,42 @@ package dbgen import ( "context" + + "github.com/jackc/pgx/v5/pgtype" ) const CreateTransfer = `-- name: CreateTransfer :one -INSERT INTO wallet_transfer (amount, wallet_transfer, wallet_id) VALUES ($1, $2, $3) RETURNING id, amount, wallet_transfer, wallet_id, verified, payment_method, created_at, updated_at +INSERT INTO wallet_transfer (amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method, created_at, updated_at ` type CreateTransferParams struct { - Amount int64 - WalletTransfer string - WalletID int64 + Amount int64 + Type string + ReceiverWalletID int64 + SenderWalletID pgtype.Int8 + CashierID pgtype.Int8 + Verified bool + PaymentMethod string } func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) (WalletTransfer, error) { - row := q.db.QueryRow(ctx, CreateTransfer, arg.Amount, arg.WalletTransfer, arg.WalletID) + row := q.db.QueryRow(ctx, CreateTransfer, + arg.Amount, + arg.Type, + arg.ReceiverWalletID, + arg.SenderWalletID, + arg.CashierID, + arg.Verified, + arg.PaymentMethod, + ) var i WalletTransfer err := row.Scan( &i.ID, &i.Amount, - &i.WalletTransfer, - &i.WalletID, + &i.Type, + &i.ReceiverWalletID, + &i.SenderWalletID, + &i.CashierID, &i.Verified, &i.PaymentMethod, &i.CreatedAt, @@ -36,7 +52,7 @@ func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) } const GetAllTransfers = `-- name: GetAllTransfers :many -SELECT id, amount, wallet_transfer, wallet_id, verified, payment_method, created_at, updated_at FROM wallet_transfer +SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method, created_at, updated_at FROM wallet_transfer ` func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error) { @@ -51,8 +67,10 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error) if err := rows.Scan( &i.ID, &i.Amount, - &i.WalletTransfer, - &i.WalletID, + &i.Type, + &i.ReceiverWalletID, + &i.SenderWalletID, + &i.CashierID, &i.Verified, &i.PaymentMethod, &i.CreatedAt, @@ -69,7 +87,7 @@ func (q *Queries) GetAllTransfers(ctx context.Context) ([]WalletTransfer, error) } const GetTransferByID = `-- name: GetTransferByID :one -SELECT id, amount, wallet_transfer, wallet_id, verified, payment_method, created_at, updated_at FROM wallet_transfer WHERE id = $1 +SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method, created_at, updated_at FROM wallet_transfer WHERE id = $1 ` func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer, error) { @@ -78,8 +96,10 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer err := row.Scan( &i.ID, &i.Amount, - &i.WalletTransfer, - &i.WalletID, + &i.Type, + &i.ReceiverWalletID, + &i.SenderWalletID, + &i.CashierID, &i.Verified, &i.PaymentMethod, &i.CreatedAt, @@ -89,11 +109,11 @@ func (q *Queries) GetTransferByID(ctx context.Context, id int64) (WalletTransfer } const GetTransfersByWallet = `-- name: GetTransfersByWallet :many -SELECT id, amount, wallet_transfer, wallet_id, verified, payment_method, created_at, updated_at FROM wallet_transfer WHERE wallet_id = $1 +SELECT id, amount, type, receiver_wallet_id, sender_wallet_id, cashier_id, verified, payment_method, created_at, updated_at FROM wallet_transfer WHERE receiver_wallet_id = $1 OR sender_wallet_id = $1 ` -func (q *Queries) GetTransfersByWallet(ctx context.Context, walletID int64) ([]WalletTransfer, error) { - rows, err := q.db.Query(ctx, GetTransfersByWallet, walletID) +func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID int64) ([]WalletTransfer, error) { + rows, err := q.db.Query(ctx, GetTransfersByWallet, receiverWalletID) if err != nil { return nil, err } @@ -104,8 +124,10 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, walletID int64) ([]W if err := rows.Scan( &i.ID, &i.Amount, - &i.WalletTransfer, - &i.WalletID, + &i.Type, + &i.ReceiverWalletID, + &i.SenderWalletID, + &i.CashierID, &i.Verified, &i.PaymentMethod, &i.CreatedAt, diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index de555d9..0700a07 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -43,23 +43,30 @@ func (q *Queries) CreateCustomerWallet(ctx context.Context, arg CreateCustomerWa } const CreateWallet = `-- name: CreateWallet :one -INSERT INTO wallets (is_withdraw, is_bettable, user_id) VALUES ($1, $2, $3) RETURNING id, balance, is_withdraw, is_bettable, user_id, is_active, created_at, updated_at +INSERT INTO wallets (is_withdraw, is_bettable, is_transferable, user_id) VALUES ($1, $2, $3, $4) RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at ` type CreateWalletParams struct { - IsWithdraw bool - IsBettable bool - UserID int64 + IsWithdraw bool + IsBettable bool + IsTransferable bool + UserID int64 } func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wallet, error) { - row := q.db.QueryRow(ctx, CreateWallet, arg.IsWithdraw, arg.IsBettable, arg.UserID) + row := q.db.QueryRow(ctx, CreateWallet, + arg.IsWithdraw, + arg.IsBettable, + arg.IsTransferable, + arg.UserID, + ) var i Wallet err := row.Scan( &i.ID, &i.Balance, &i.IsWithdraw, &i.IsBettable, + &i.IsTransferable, &i.UserID, &i.IsActive, &i.CreatedAt, @@ -69,7 +76,7 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal } const GetAllWallets = `-- name: GetAllWallets :many -SELECT id, balance, is_withdraw, is_bettable, user_id, is_active, created_at, updated_at FROM wallets +SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at FROM wallets ` func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) { @@ -86,6 +93,7 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) { &i.Balance, &i.IsWithdraw, &i.IsBettable, + &i.IsTransferable, &i.UserID, &i.IsActive, &i.CreatedAt, @@ -156,7 +164,7 @@ func (q *Queries) GetCustomerWallet(ctx context.Context, arg GetCustomerWalletPa } const GetWalletByID = `-- name: GetWalletByID :one -SELECT id, balance, is_withdraw, is_bettable, user_id, is_active, created_at, updated_at FROM wallets WHERE id = $1 +SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at FROM wallets WHERE id = $1 ` func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { @@ -167,6 +175,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { &i.Balance, &i.IsWithdraw, &i.IsBettable, + &i.IsTransferable, &i.UserID, &i.IsActive, &i.CreatedAt, @@ -176,7 +185,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { } const GetWalletByUserID = `-- name: GetWalletByUserID :many -SELECT id, balance, is_withdraw, is_bettable, user_id, is_active, created_at, updated_at FROM wallets WHERE user_id = $1 +SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at FROM wallets WHERE user_id = $1 ` func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet, error) { @@ -193,6 +202,7 @@ func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet &i.Balance, &i.IsWithdraw, &i.IsBettable, + &i.IsTransferable, &i.UserID, &i.IsActive, &i.CreatedAt, diff --git a/internal/domain/branch.go b/internal/domain/branch.go index 29143c0..3295dae 100644 --- a/internal/domain/branch.go +++ b/internal/domain/branch.go @@ -43,6 +43,14 @@ type CreateBranch struct { IsSelfOwned bool } +type UpdateBranch struct { + Name string + Location string + BranchManagerID int64 + CompanyID int64 + IsSelfOwned bool +} + type CreateSupportedOperation struct { Name string Description string diff --git a/internal/domain/transfer.go b/internal/domain/transfer.go index ed31614..845482b 100644 --- a/internal/domain/transfer.go +++ b/internal/domain/transfer.go @@ -7,34 +7,42 @@ type TransferType string const ( DEPOSIT TransferType = "deposit" WITHDRAW TransferType = "withdraw" + WALLET TransferType = "wallet" ) -type PaymentMethod int +type PaymentMethod string const ( - TRANSFER_CASH PaymentMethod = iota + 1 - TRANSFER_BANK - TRANSFER_CHAPA - TRANSFER_ARIFPAY - TRANSFER_SANTIM - TRANSFER_ADDISPAY - TRANSFER_OTHER + TRANSFER_CASH PaymentMethod = "cash" + TRANSFER_BANK PaymentMethod = "bank" + TRANSFER_CHAPA PaymentMethod = "chapa" + TRANSFER_ARIFPAY PaymentMethod = "arifpay" + TRANSFER_SANTIMPAY PaymentMethod = "santimpay" + TRANSFER_ADDISPAY PaymentMethod = "addispay" + TRANSFER_OTHER PaymentMethod = "other" ) +// There is always a receiving wallet id +// There is a sender wallet id only if wallet transfer type type Transfer struct { - ID int64 - Amount Currency - Verified bool - WalletID int64 - Type TransferType - PaymentMethod PaymentMethod - CreatedAt time.Time - UpdatedAt time.Time + ID int64 + Amount Currency + Verified bool + Type TransferType + PaymentMethod PaymentMethod + ReceiverWalletID int64 + SenderWalletID ValidInt64 + CashierID ValidInt64 + CreatedAt time.Time + UpdatedAt time.Time } type CreateTransfer struct { - Amount Currency - Verified bool - WalletID int64 - Type TransferType + Amount Currency + Verified bool + ReceiverWalletID int64 + SenderWalletID ValidInt64 + CashierID ValidInt64 + Type TransferType + PaymentMethod PaymentMethod } diff --git a/internal/domain/wallet.go b/internal/domain/wallet.go index ff92ef6..efb80ab 100644 --- a/internal/domain/wallet.go +++ b/internal/domain/wallet.go @@ -7,6 +7,7 @@ type Wallet struct { Balance Currency IsWithdraw bool IsBettable bool + IsTransferable bool IsActive bool UserID int64 UpdatedAt time.Time @@ -36,6 +37,7 @@ type GetCustomerWallet struct { type CreateWallet struct { IsWithdraw bool IsBettable bool + IsTransferable bool UserID int64 } diff --git a/internal/repository/branch.go b/internal/repository/branch.go index 2a8d470..a37493a 100644 --- a/internal/repository/branch.go +++ b/internal/repository/branch.go @@ -136,7 +136,7 @@ func (s *Store) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, erro return branches, nil } -func (s *Store) UpdateBranch(ctx context.Context, id int64, branch domain.CreateBranch) (domain.Branch, error) { +func (s *Store) UpdateBranch(ctx context.Context, id int64, branch domain.UpdateBranch) (domain.Branch, error) { dbBranch, err := s.queries.UpdateBranch(ctx, dbgen.UpdateBranchParams{ ID: id, Name: branch.Name, diff --git a/internal/repository/transaction.go b/internal/repository/transaction.go index 87f04f7..f0faad3 100644 --- a/internal/repository/transaction.go +++ b/internal/repository/transaction.go @@ -72,6 +72,19 @@ func (s *Store) GetAllTransactions(ctx context.Context) ([]domain.Transaction, e } return result, nil } +func (s *Store) GetTransactionByBranch(ctx context.Context, id int64) ([]domain.Transaction, error) { + transaction, err := s.queries.GetTransactionByBranch(ctx, id) + + if err != nil { + return nil, err + } + + var result []domain.Transaction = make([]domain.Transaction, len(transaction)) + for _, ticket := range transaction { + result = append(result, convertDBTransaction(ticket)) + } + return result, nil +} func (s *Store) UpdateTransactionVerified(ctx context.Context, id int64, verified bool) error { err := s.queries.UpdateTransactionVerified(ctx, dbgen.UpdateTransactionVerifiedParams{ diff --git a/internal/repository/transfer.go b/internal/repository/transfer.go index d39178b..70884f3 100644 --- a/internal/repository/transfer.go +++ b/internal/repository/transfer.go @@ -1,77 +1,96 @@ package repository -// import ( -// "context" +import ( + "context" -// dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" -// "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" -// ) + 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), -// Type: domain.TransactionType(transaction.TransactionType), -// Verified: transaction.Verified, -// WalletID: transaction.WalletID, -// } -// } +func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer { + return domain.Transfer{ + ID: transfer.ID, + Amount: domain.Currency(transfer.Amount), + Type: domain.TransferType(transfer.Type), + Verified: transfer.Verified, + ReceiverWalletID: transfer.ReceiverWalletID, + SenderWalletID: domain.ValidInt64{ + Value: transfer.SenderWalletID.Int64, + Valid: transfer.SenderWalletID.Valid, + }, + CashierID: domain.ValidInt64{ + Value: transfer.CashierID.Int64, + Valid: transfer.CashierID.Valid, + }, + PaymentMethod: domain.PaymentMethod(transfer.PaymentMethod), + } +} -// func convertCreateTransaction(transaction domain.CreateTransaction) dbgen.CreateTransactionParams { -// return dbgen.CreateTransactionParams{ -// Amount: int64(transaction.Amount), -// TransactionType: string(transaction.Type), -// WalletID: transaction.WalletID, -// } -// } +func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferParams { + return dbgen.CreateTransferParams{ + Amount: int64(transfer.Amount), + Type: string(transfer.Type), + ReceiverWalletID: transfer.ReceiverWalletID, + SenderWalletID: pgtype.Int8{ + Int64: transfer.SenderWalletID.Value, + Valid: transfer.SenderWalletID.Valid, + }, + CashierID: pgtype.Int8{ + Int64: transfer.CashierID.Value, + Valid: transfer.CashierID.Valid, + }, + PaymentMethod: string(transfer.PaymentMethod), + } +} -// func (s *Store) CreateTransaction(ctx context.Context, transaction domain.CreateTransaction) (domain.Transaction, error) { -// newTransaction, err := s.queries.CreateTransaction(ctx, convertCreateTransaction(transaction)) -// if err != nil { -// return domain.Transaction{}, err -// } -// return convertDBTransaction(newTransaction), nil -// } +func (s *Store) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { + newTransfer, err := s.queries.CreateTransfer(ctx, convertCreateTransfer(transfer)) + if err != nil { + return domain.Transfer{}, err + } + return convertDBTransfer(newTransfer), nil +} -// func (s *Store) GetAllTransactions(ctx context.Context) ([]domain.Transaction, error) { -// transactions, err := s.queries.GetAllTransactions(ctx) -// if err != nil { -// return nil, err -// } -// var result []domain.Transaction = make([]domain.Transaction, len(transactions)) +func (s *Store) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) { + transfers, err := s.queries.GetAllTransfers(ctx) + if err != nil { + return nil, err + } + var result []domain.Transfer = make([]domain.Transfer, len(transfers)) -// for _, transaction := range transactions { -// result = append(result, convertDBTransaction(transaction)) -// } -// return result, nil -// } -// func (s *Store) GetTransactionsByWallet(ctx context.Context, walletID int64) ([]domain.Transaction, error) { -// transactions, err := s.queries.GetTransactionsByWallet(ctx, walletID) -// if err != nil { -// return nil, err -// } + for _, transfer := range transfers { + result = append(result, convertDBTransfer(transfer)) + } + return result, nil +} +func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) { + transfers, err := s.queries.GetTransfersByWallet(ctx, walletID) + if err != nil { + return nil, err + } -// var result []domain.Transaction = make([]domain.Transaction, len(transactions)) + var result []domain.Transfer = make([]domain.Transfer, len(transfers)) -// for _, transaction := range transactions { -// result = append(result, convertDBTransaction(transaction)) -// } -// return result, nil -// } + for _, transfer := range transfers { + result = append(result, convertDBTransfer(transfer)) + } + return result, nil +} -// func (s *Store) GetTransactionByID(ctx context.Context, id int64) (domain.Transaction, error) { -// transaction, err := s.queries.GetTransactionByID(ctx, id) -// if err != nil { -// return domain.Transaction{}, nil -// } -// return convertDBTransaction(transaction), nil -// } +func (s *Store) GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) { + transfer, err := s.queries.GetTransferByID(ctx, id) + if err != nil { + return domain.Transfer{}, nil + } + return convertDBTransfer(transfer), nil +} -// func (s *Store) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error { -// err := s.queries.UpdateTransferVerification(ctx, dbgen.UpdateTransferVerificationParams{ -// ID: id, -// Verified: verified, -// }) +func (s *Store) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error { + err := s.queries.UpdateTransferVerification(ctx, dbgen.UpdateTransferVerificationParams{ + ID: id, + Verified: verified, + }) -// return err -// } + return err +} diff --git a/internal/repository/wallet.go b/internal/repository/wallet.go index 87f88ac..7126fc4 100644 --- a/internal/repository/wallet.go +++ b/internal/repository/wallet.go @@ -9,22 +9,24 @@ import ( func convertDBWallet(wallet dbgen.Wallet) domain.Wallet { return domain.Wallet{ - ID: wallet.ID, - Balance: domain.Currency(wallet.Balance), - IsWithdraw: wallet.IsWithdraw, - IsBettable: wallet.IsBettable, - IsActive: wallet.IsActive, - UserID: wallet.UserID, - UpdatedAt: wallet.UpdatedAt.Time, - CreatedAt: wallet.CreatedAt.Time, + ID: wallet.ID, + Balance: domain.Currency(wallet.Balance), + IsWithdraw: wallet.IsWithdraw, + IsBettable: wallet.IsBettable, + IsTransferable: wallet.IsTransferable, + IsActive: wallet.IsActive, + UserID: wallet.UserID, + UpdatedAt: wallet.UpdatedAt.Time, + CreatedAt: wallet.CreatedAt.Time, } } func convertCreateWallet(wallet domain.CreateWallet) dbgen.CreateWalletParams { return dbgen.CreateWalletParams{ - IsWithdraw: wallet.IsWithdraw, - IsBettable: wallet.IsBettable, - UserID: wallet.UserID, + IsWithdraw: wallet.IsWithdraw, + IsBettable: wallet.IsBettable, + IsTransferable: wallet.IsTransferable, + UserID: wallet.UserID, } } diff --git a/internal/services/branch/port.go b/internal/services/branch/port.go index 6cff893..633fa31 100644 --- a/internal/services/branch/port.go +++ b/internal/services/branch/port.go @@ -15,7 +15,7 @@ type BranchStore interface { GetBranchByCompanyID(ctx context.Context, companyID int64) ([]domain.BranchDetail, error) GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) - UpdateBranch(ctx context.Context, id int64, branch domain.CreateBranch) (domain.Branch, error) + UpdateBranch(ctx context.Context, id int64, branch domain.UpdateBranch) (domain.Branch, error) DeleteBranch(ctx context.Context, id int64) error DeleteBranchOperation(ctx context.Context, branchID int64, operationID int64) error } diff --git a/internal/services/branch/service.go b/internal/services/branch/service.go index f020252..49a87ff 100644 --- a/internal/services/branch/service.go +++ b/internal/services/branch/service.go @@ -28,7 +28,7 @@ func (s *Service) CreateBranchOperation(ctx context.Context, branchOperation dom func (s *Service) GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error) { return s.branchStore.GetBranchByID(ctx, id) } -func (s *Service) GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([] domain.BranchDetail, error) { +func (s *Service) GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]domain.BranchDetail, error) { return s.branchStore.GetBranchByManagerID(ctx, branchManagerID) } func (s *Service) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]domain.BranchDetail, error) { @@ -40,7 +40,7 @@ func (s *Service) GetBranchOperations(ctx context.Context, branchID int64) ([]do func (s *Service) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) { return s.branchStore.GetAllBranches(ctx) } -func (s *Service) UpdateBranch(ctx context.Context, id int64, branch domain.CreateBranch) (domain.Branch, error) { +func (s *Service) UpdateBranch(ctx context.Context, id int64, branch domain.UpdateBranch) (domain.Branch, error) { return s.branchStore.UpdateBranch(ctx, id, branch) } func (s *Service) DeleteBranch(ctx context.Context, id int64) error { diff --git a/internal/services/transaction/port.go b/internal/services/transaction/port.go index 27b9f5a..cbd9a0f 100644 --- a/internal/services/transaction/port.go +++ b/internal/services/transaction/port.go @@ -10,5 +10,6 @@ type TransactionStore interface { CreateTransaction(ctx context.Context, transaction domain.CreateTransaction) (domain.Transaction, error) 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 } diff --git a/internal/services/transaction/service.go b/internal/services/transaction/service.go index 31ca58e..2c33917 100644 --- a/internal/services/transaction/service.go +++ b/internal/services/transaction/service.go @@ -25,6 +25,9 @@ func (s *Service) GetTransactionByID(ctx context.Context, id int64) (domain.Tran func (s *Service) GetAllTransactions(ctx context.Context) ([]domain.Transaction, error) { return s.transactionStore.GetAllTransactions(ctx) } +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) diff --git a/internal/services/transfer/chapa.go b/internal/services/transfer/chapa.go deleted file mode 100644 index 3e884d3..0000000 --- a/internal/services/transfer/chapa.go +++ /dev/null @@ -1 +0,0 @@ -package transfer diff --git a/internal/services/transfer/port.go b/internal/services/transfer/port.go deleted file mode 100644 index 213d65a..0000000 --- a/internal/services/transfer/port.go +++ /dev/null @@ -1,15 +0,0 @@ -package transfer - -import ( - "context" - - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" -) - -type TransferStore interface { - CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) - GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) - GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) - GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) - UpdateTransferVerification(ctx context.Context, id int64, verified bool) error -} diff --git a/internal/services/transfer/service.go b/internal/services/transfer/service.go deleted file mode 100644 index c628c42..0000000 --- a/internal/services/transfer/service.go +++ /dev/null @@ -1,33 +0,0 @@ -package transfer - -import ( - "context" - - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" -) - -type Service struct { - transferStore TransferStore -} - -func NewService(transferStore TransferStore) *Service { - return &Service{ - transferStore: transferStore, - } -} - -func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { - return s.transferStore.CreateTransfer(ctx, transfer) -} - -func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) { - return s.transferStore.GetAllTransfers(ctx) -} - -func (s *Service) GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) { - return s.transferStore.GetTransferByID(ctx, id) -} - -func (s *Service) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error { - return s.transferStore.UpdateTransferVerification(ctx, id, verified) -} diff --git a/internal/services/wallet/chapa.go b/internal/services/wallet/chapa.go new file mode 100644 index 0000000..23a7507 --- /dev/null +++ b/internal/services/wallet/chapa.go @@ -0,0 +1 @@ +package wallet diff --git a/internal/services/wallet/port.go b/internal/services/wallet/port.go index 0e369bd..b9eb043 100644 --- a/internal/services/wallet/port.go +++ b/internal/services/wallet/port.go @@ -16,3 +16,12 @@ type WalletStore interface { UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error UpdateWalletActive(ctx context.Context, id int64, isActive bool) error } + +type TransferStore interface { + CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) + GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) + GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) + GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) + UpdateTransferVerification(ctx context.Context, id int64, verified bool) error +} + diff --git a/internal/services/wallet/service.go b/internal/services/wallet/service.go index 1cb9ebe..a8913c2 100644 --- a/internal/services/wallet/service.go +++ b/internal/services/wallet/service.go @@ -1,102 +1,13 @@ package wallet -import ( - "context" - "errors" - - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" -) - type Service struct { - walletStore WalletStore + walletStore WalletStore + transferStore TransferStore } -func NewService(walletStore WalletStore) *Service { +func NewService(walletStore WalletStore, transferStore TransferStore) *Service { return &Service{ - walletStore: walletStore, + walletStore: walletStore, + transferStore: transferStore, } } - -var ( - ErrBalanceInsufficient = errors.New("wallet balance is insufficient") -) - -func (s *Service) CreateWallet(ctx context.Context, wallet domain.CreateWallet) (domain.Wallet, error) { - return s.walletStore.CreateWallet(ctx, wallet) -} - -func (s *Service) CreateCustomerWallet(ctx context.Context, customerID int64, companyID int64) (domain.CustomerWallet, error) { - - regularWallet, err := s.CreateWallet(ctx, domain.CreateWallet{ - IsWithdraw: true, - IsBettable: true, - UserID: customerID, - }) - - if err != nil { - return domain.CustomerWallet{}, err - } - - staticWallet, err := s.CreateWallet(ctx, domain.CreateWallet{ - IsWithdraw: false, - IsBettable: true, - UserID: customerID, - }) - - if err != nil { - return domain.CustomerWallet{}, err - } - - return s.walletStore.CreateCustomerWallet(ctx, domain.CreateCustomerWallet{ - CustomerID: customerID, - CompanyID: companyID, - RegularWalletID: regularWallet.ID, - StaticWalletID: staticWallet.ID, - }) -} - -func (s *Service) GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error) { - return s.walletStore.GetWalletByID(ctx, id) -} - -func (s *Service) GetAllWallets(ctx context.Context) ([]domain.Wallet, error) { - return s.walletStore.GetAllWallets(ctx) -} - -func (s *Service) GetWalletsByUser(ctx context.Context, id int64) ([]domain.Wallet, error) { - return s.walletStore.GetWalletsByUser(ctx, id) -} - -func (s *Service) GetCustomerWallet(ctx context.Context, customerID int64, companyID int64) (domain.GetCustomerWallet, error) { - return s.walletStore.GetCustomerWallet(ctx, customerID, companyID) -} - -func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error { - return s.walletStore.UpdateBalance(ctx, id, balance) -} - -func (s *Service) Add(ctx context.Context, id int64, amount domain.Currency) error { - wallet, err := s.GetWalletByID(ctx, id) - if err != nil { - return err - } - - return s.walletStore.UpdateBalance(ctx, id, wallet.Balance+amount) -} - -func (s *Service) Deduct(ctx context.Context, id int64, amount domain.Currency) error { - wallet, err := s.GetWalletByID(ctx, id) - if err != nil { - return err - } - - if wallet.Balance < amount { - return ErrBalanceInsufficient - } - - return s.walletStore.UpdateBalance(ctx, id, wallet.Balance+amount) -} - -func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive bool) error { - return s.walletStore.UpdateWalletActive(ctx, id, isActive) -} diff --git a/internal/services/wallet/transfer.go b/internal/services/wallet/transfer.go new file mode 100644 index 0000000..6f0f0d4 --- /dev/null +++ b/internal/services/wallet/transfer.go @@ -0,0 +1,86 @@ +package wallet + +import ( + "context" + "errors" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +var ( + ErrWalletNotTransferable = errors.New("wallet is not transferable") +) + +func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { + return s.transferStore.CreateTransfer(ctx, transfer) +} + +func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) { + return s.transferStore.GetAllTransfers(ctx) +} + +func (s *Service) GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) { + return s.transferStore.GetTransferByID(ctx, id) +} + +func (s *Service) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error { + return s.transferStore.UpdateTransferVerification(ctx, id, verified) +} + +func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiverID int64, amount domain.Currency, paymentMethod domain.PaymentMethod, cashierID domain.ValidInt64) (domain.Transfer, error) { + + senderWallet, err := s.GetWalletByID(ctx, senderID) + if err != nil { + return domain.Transfer{}, err + } + + if senderWallet.IsTransferable { + return domain.Transfer{}, ErrWalletNotTransferable + } + + receiverWallet, err := s.GetWalletByID(ctx, receiverID) + if err != nil { + return domain.Transfer{}, err + } + + if receiverWallet.IsTransferable { + return domain.Transfer{}, ErrWalletNotTransferable + } + + // Deduct from sender + if senderWallet.Balance < amount { + return domain.Transfer{}, ErrBalanceInsufficient + } + + err = s.walletStore.UpdateBalance(ctx, senderID, senderWallet.Balance-amount) + + if err != nil { + return domain.Transfer{}, err + } + + // Add to receiver + err = s.walletStore.UpdateBalance(ctx, receiverID, receiverWallet.Balance+amount) + + if err != nil { + return domain.Transfer{}, err + } + + // Log the transfer so that if there is a mistake, it can be reverted + transfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ + SenderWalletID: domain.ValidInt64{ + Value: senderID, + Valid: true, + }, + CashierID: cashierID, + ReceiverWalletID: receiverID, + Amount: amount, + Type: domain.WALLET, + PaymentMethod: paymentMethod, + Verified: true, + }) + if err != nil { + return domain.Transfer{}, err + } + + return transfer, nil +} diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go new file mode 100644 index 0000000..c95a660 --- /dev/null +++ b/internal/services/wallet/wallet.go @@ -0,0 +1,93 @@ +package wallet + +import ( + "context" + "errors" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + + +var ( + ErrBalanceInsufficient = errors.New("wallet balance is insufficient") +) + +func (s *Service) CreateWallet(ctx context.Context, wallet domain.CreateWallet) (domain.Wallet, error) { + return s.walletStore.CreateWallet(ctx, wallet) +} + +func (s *Service) CreateCustomerWallet(ctx context.Context, customerID int64, companyID int64) (domain.CustomerWallet, error) { + + regularWallet, err := s.CreateWallet(ctx, domain.CreateWallet{ + IsWithdraw: true, + IsBettable: true, + UserID: customerID, + }) + + if err != nil { + return domain.CustomerWallet{}, err + } + + staticWallet, err := s.CreateWallet(ctx, domain.CreateWallet{ + IsWithdraw: false, + IsBettable: true, + UserID: customerID, + }) + + if err != nil { + return domain.CustomerWallet{}, err + } + + return s.walletStore.CreateCustomerWallet(ctx, domain.CreateCustomerWallet{ + CustomerID: customerID, + CompanyID: companyID, + RegularWalletID: regularWallet.ID, + StaticWalletID: staticWallet.ID, + }) +} + +func (s *Service) GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error) { + return s.walletStore.GetWalletByID(ctx, id) +} + +func (s *Service) GetAllWallets(ctx context.Context) ([]domain.Wallet, error) { + return s.walletStore.GetAllWallets(ctx) +} + +func (s *Service) GetWalletsByUser(ctx context.Context, id int64) ([]domain.Wallet, error) { + return s.walletStore.GetWalletsByUser(ctx, id) +} + +func (s *Service) GetCustomerWallet(ctx context.Context, customerID int64, companyID int64) (domain.GetCustomerWallet, error) { + return s.walletStore.GetCustomerWallet(ctx, customerID, companyID) +} + +func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error { + return s.walletStore.UpdateBalance(ctx, id, balance) +} + +func (s *Service) AddToWallet(ctx context.Context, id int64, amount domain.Currency) error { + wallet, err := s.GetWalletByID(ctx, id) + if err != nil { + return err + } + + return s.walletStore.UpdateBalance(ctx, id, wallet.Balance+amount) +} + +func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.Currency) error { + wallet, err := s.GetWalletByID(ctx, id) + if err != nil { + return err + } + + if wallet.Balance < amount { + return ErrBalanceInsufficient + } + + return s.walletStore.UpdateBalance(ctx, id, wallet.Balance+amount) +} + +func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive bool) error { + return s.walletStore.UpdateWalletActive(ctx, id, isActive) +} diff --git a/internal/web_server/app.go b/internal/web_server/app.go index bdf1b33..53cac25 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -6,6 +6,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -16,6 +17,7 @@ import ( notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" "github.com/bytedance/sonic" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" ) type App struct { @@ -29,6 +31,7 @@ type App struct { betSvc *bet.Service walletSvc *wallet.Service transactionSvc *transaction.Service + branchSvc *branch.Service validator *customvalidator.CustomValidator JwtConfig jwtutil.JwtConfig Logger *slog.Logger @@ -44,6 +47,7 @@ func NewApp( betSvc *bet.Service, walletSvc *wallet.Service, transactionSvc *transaction.Service, + branchSvc *branch.Service, notidicationStore notificationservice.NotificationStore, ) *App { app := fiber.New(fiber.Config{ @@ -52,6 +56,13 @@ func NewApp( JSONEncoder: sonic.Marshal, JSONDecoder: sonic.Unmarshal, }) + + app.Use(cors.New(cors.Config{ + AllowOrigins: "http://localhost:5173", // Specify your frontend's origin + AllowMethods: "GET,POST,PUT,DELETE", // Specify the allowed HTTP methods + AllowHeaders: "Content-Type,Authorization", // Specify the allowed headers + })) + s := &App{ fiber: app, port: port, @@ -64,6 +75,7 @@ func NewApp( betSvc: betSvc, walletSvc: walletSvc, transactionSvc: transactionSvc, + branchSvc: branchSvc, NotidicationStore: notidicationStore, Logger: logger, } diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index 50c4a0a..e1af815 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -6,6 +6,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" "github.com/gofiber/fiber/v2" @@ -14,7 +15,6 @@ import ( type CreateBranchReq struct { Name string `json:"name" example:"4-kilo Branch"` Location string `json:"location" example:"Addis Ababa"` - WalletID int64 `json:"wallet_id" example:"1"` BranchManagerID int64 `json:"branch_manager_id" example:"1"` CompanyID int64 `json:"company_id" example:"1"` IsSelfOwned bool `json:"is_self_owned" example:"false"` @@ -85,12 +85,23 @@ func convertBranchDetail(branch domain.BranchDetail) BranchDetailRes { } } -func CreateBranch(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { +// CreateBranch godoc +// @Summary Create a branch +// @Description Creates a branch +// @Tags branch +// @Accept json +// @Produce json +// @Param createBranch body CreateBranchReq true "Creates branch" +// @Success 200 {object} BranchRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /branch [post] +func CreateBranch(logger *slog.Logger, branchSvc *branch.Service, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { // Check if user is either branch manager / super main // role := string(c.Locals("role").(domain.Role)) - // if role != string(domain.RoleCustomer) { + // if role != string(domain.RoleAdmin) && role != string(domain.RoleSuperAdmin) && role != string(domain.RoleBranchManager) { // logger.Error("Unauthorized access", "role", role) // return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) // } @@ -110,10 +121,23 @@ func CreateBranch(logger *slog.Logger, branchSvc *branch.Service, validator *cus return nil } + // Create Branch Wallet + newWallet, err := walletSvc.CreateWallet(c.Context(), domain.CreateWallet{ + IsWithdraw: false, + IsBettable: true, + IsTransferable: true, + UserID: req.BranchManagerID, + }) + + if err != nil { + logger.Error("Create Branch Wallet failed", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create branch wallet", err, nil) + } + branch, err := branchSvc.CreateBranch(c.Context(), domain.CreateBranch{ Name: req.Name, Location: req.Location, - WalletID: req.WalletID, + WalletID: newWallet.ID, BranchManagerID: req.BranchManagerID, CompanyID: req.CompanyID, IsSelfOwned: req.IsSelfOwned, @@ -134,12 +158,23 @@ func CreateBranch(logger *slog.Logger, branchSvc *branch.Service, validator *cus } +// CreateSupportedOperation godoc +// @Summary Create a supported operation +// @Description Creates a supported operation +// @Tags branch +// @Accept json +// @Produce json +// @Param createSupportedOperation body CreateSupportedOperationReq true "Creates supported operation" +// @Success 200 {object} SupportedOperationRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /supportedOperation [post] func CreateSupportedOperation(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { var req CreateSupportedOperationReq if err := c.BodyParser(&req); err != nil { - logger.Error("CreateBranchReq failed", "error", err) + logger.Error("CreateSupportedOperationReq failed", "error", err) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request", }) @@ -171,6 +206,17 @@ func CreateSupportedOperation(logger *slog.Logger, branchSvc *branch.Service, va } } +// CreateBranchOperation godoc +// @Summary Create a operation +// @Description Creates a operation +// @Tags branch +// @Accept json +// @Produce json +// @Param createBranchOperation body CreateBranchOperationReq true "Creates operation" +// @Success 200 {object} BranchOperationRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /operation [post] func CreateBranchOperation(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { var req CreateBranchOperationReq @@ -202,6 +248,17 @@ func CreateBranchOperation(logger *slog.Logger, branchSvc *branch.Service, valid } } +// GetBranchByID godoc +// @Summary Gets branch by id +// @Description Gets a single branch by id +// @Tags branch +// @Accept json +// @Produce json +// @Param id path int true "Branch ID" +// @Success 200 {object} BranchDetailRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /branch/{id} [get] func GetBranchByID(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { branchID := c.Params("id") @@ -225,9 +282,21 @@ func GetBranchByID(logger *slog.Logger, branchSvc *branch.Service, validator *cu } } -// /user/:id/branch +// GetBranchByManagerID godoc +// @Summary Gets branches by manager id +// @Description Gets a branches by manager id +// @Tags branch +// @Accept json +// @Produce json +// @Param id path int true "User ID" +// @Success 200 {array} BranchDetailRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /manager/{id}/branch [get] func GetBranchByManagerID(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { + + // TODO: Restrict any who isn't branch manager or higher userID := c.Params("id") id, err := strconv.ParseInt(userID, 10, 64) if err != nil { @@ -249,7 +318,17 @@ func GetBranchByManagerID(logger *slog.Logger, branchSvc *branch.Service, valida } } -// /company/:id/branch +// GetBranchByCompanyID godoc +// @Summary Gets branches by company id +// @Description Gets branches by company id +// @Tags branch +// @Accept json +// @Produce json +// @Param id path int true "Company ID" +// @Success 200 {array} BranchDetailRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /company/{id}/branch [get] func GetBranchByCompanyID(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { companyID := c.Params("id") @@ -273,10 +352,19 @@ func GetBranchByCompanyID(logger *slog.Logger, branchSvc *branch.Service, valida } } +// GetAllBranches godoc +// @Summary Gets all branches +// @Description Gets all branches +// @Tags branch +// @Accept json +// @Produce json +// @Success 200 {array} BranchDetailRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /branch [get] func GetAllBranches(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { branches, err := branchSvc.GetAllBranches(c.Context()) - if err != nil { logger.Error("Failed to get branches", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get branches", err, nil) @@ -291,6 +379,17 @@ func GetAllBranches(logger *slog.Logger, branchSvc *branch.Service, validator *c } } +// GetBranchOperations godoc +// @Summary Gets branch operations +// @Description Gets branch operations +// @Tags branch +// @Accept json +// @Produce json +// @Param id path int true "Branch ID" +// @Success 200 {array} BranchOperationRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /branch/{id}/operation [get] func GetBranchOperations(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { @@ -312,7 +411,7 @@ func GetBranchOperations(logger *slog.Logger, branchSvc *branch.Service, validat for _, branch := range operations { result = append(result, BranchOperationRes{ - Name: branch.OperationName, + Name: branch.OperationName, Description: branch.OperationDescription, }) } @@ -321,6 +420,18 @@ func GetBranchOperations(logger *slog.Logger, branchSvc *branch.Service, validat } } +// UpdateBranch godoc +// @Summary Updates a branch +// @Description Updates a branch +// @Tags branch +// @Accept json +// @Produce json +// @Param id path int true "Branch ID" +// @Param updateBranch body CreateBranchReq true "Update Branch" +// @Success 200 {object} BranchRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /branch/{id} [put] func UpdateBranch(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { branchID := c.Params("id") @@ -333,7 +444,7 @@ func UpdateBranch(logger *slog.Logger, branchSvc *branch.Service, validator *cus var req CreateBranchReq if err := c.BodyParser(&req); err != nil { - logger.Error("CreateBetReq failed", "error", err) + logger.Error("CreateBranchReq failed", "error", err) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request", }) @@ -344,10 +455,9 @@ func UpdateBranch(logger *slog.Logger, branchSvc *branch.Service, validator *cus return nil } - branch, err := branchSvc.UpdateBranch(c.Context(), id, domain.CreateBranch{ + branch, err := branchSvc.UpdateBranch(c.Context(), id, domain.UpdateBranch{ Name: req.Name, Location: req.Location, - WalletID: req.WalletID, BranchManagerID: req.BranchManagerID, CompanyID: req.CompanyID, IsSelfOwned: req.IsSelfOwned, @@ -365,6 +475,17 @@ func UpdateBranch(logger *slog.Logger, branchSvc *branch.Service, validator *cus } } +// DeleteBranch godoc +// @Summary Delete the branch +// @Description Delete the branch +// @Tags branch +// @Accept json +// @Produce json +// @Param id path int true "Branch ID"" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /branch/{id} [delete] func DeleteBranch(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { @@ -388,6 +509,18 @@ func DeleteBranch(logger *slog.Logger, branchSvc *branch.Service, validator *cus } } +// DeleteBranchOperation godoc +// @Summary Delete the branch operation +// @Description Delete the branch operation +// @Tags branch +// @Accept json +// @Produce json +// @Param id path int true "Branch ID" +// @Param opID path int true "Branch Operation ID" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /branch/{id}/operation/{opID} [delete] func DeleteBranchOperation(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { branchID := c.Params("id") diff --git a/internal/web_server/handlers/transaction_handler.go b/internal/web_server/handlers/transaction_handler.go index 1d143c7..0a93bf0 100644 --- a/internal/web_server/handlers/transaction_handler.go +++ b/internal/web_server/handlers/transaction_handler.go @@ -6,6 +6,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" "github.com/gofiber/fiber/v2" @@ -127,21 +128,51 @@ func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service, // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /transaction [get] -func GetAllTransactions(logger *slog.Logger, transactionSvc *transaction.Service, validator *customvalidator.CustomValidator) fiber.Handler { +func GetAllTransactions( + logger *slog.Logger, + transactionSvc *transaction.Service, + userSvc *user.Service, + validator *customvalidator.CustomValidator, +) fiber.Handler { return func(c *fiber.Ctx) error { - transactions, err := transactionSvc.GetAllTransactions(c.Context()) + // Get user_id from middleware + userID := c.Locals("user_id").(int64) + + // Fetch user details + user, err := userSvc.GetUserByID(c.Context(), userID) + if err != nil { + logger.Error("Failed to fetch user details", "user_id", userID, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve user details", err, nil) + } + + var transactions []domain.Transaction + + // Check user role and fetch transactions accordingly + switch user.Role { + case domain.RoleAdmin: + // Admin can fetch all transactions + transactions, err = transactionSvc.GetAllTransactions(c.Context()) + case domain.RoleBranchManager, domain.RoleCashier: + // Branch Manager or Cashier can fetch transactions for their branch + // transactions, err = transactionSvc.GetTransactionByBranch(c.Context(), user.BranchID) + transactions, err = transactionSvc.GetAllTransactions(c.Context()) + default: + // Unauthorized role + return response.WriteJSON(c, fiber.StatusForbidden, "Unauthorized", nil, nil) + } if err != nil { - logger.Error("Failed to get transaction", "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve transaction", err, nil) + logger.Error("Failed to get transactions", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve transactions", err, nil) } + // Convert transactions to response format var res []TransactionRes = make([]TransactionRes, len(transactions)) - for _, transaction := range transactions { - res = append(res, convertTransaction(transaction)) + for i, transaction := range transactions { + res[i] = convertTransaction(transaction) } - return response.WriteJSON(c, fiber.StatusOK, "All Transactions Retrieved", res, nil) + return response.WriteJSON(c, fiber.StatusOK, "Transactions retrieved successfully", res, nil) } } diff --git a/internal/web_server/handlers/transfer_handler.go b/internal/web_server/handlers/transfer_handler.go new file mode 100644 index 0000000..c8635ee --- /dev/null +++ b/internal/web_server/handlers/transfer_handler.go @@ -0,0 +1,116 @@ +package handlers + +import ( + "log/slog" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" + "github.com/gofiber/fiber/v2" +) + +type TransferRes struct { + ID int64 `json:"id" example:"1"` + Amount float32 `json:"amount" example:"100.0"` + Verified bool `json:"verified" example:"true"` + Type string `json:"type" example:"transfer"` + PaymentMethod string `json:"payment_method" example:"bank"` + ReceiverWalletID int64 `json:"receiver_wallet_id" example:"1"` + SenderWalletID *int64 `json:"sender_wallet_id" example:"1"` + CashierID *int64 `json:"cashier_id" example:"789"` + CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"` + UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:30:00Z"` +} + + +func convertTransfer(transfer domain.Transfer) TransferRes { + var senderWalletID *int64 + if transfer.SenderWalletID.Valid { + senderWalletID = &transfer.SenderWalletID.Value + } + + var cashierID *int64 + if transfer.CashierID.Valid { + cashierID = &transfer.CashierID.Value + } + + return TransferRes{ + ID: transfer.ID, + Amount: transfer.Amount.Float64(), + Verified: transfer.Verified, + Type: string(transfer.Type), + PaymentMethod: string(transfer.PaymentMethod), + ReceiverWalletID: transfer.ReceiverWalletID, + SenderWalletID: senderWalletID, + CashierID: cashierID, + CreatedAt: transfer.CreatedAt, + UpdatedAt: transfer.UpdatedAt, + } +} + +type CreateTransferReq struct { + receiverID int64 + amount float64 + paymentMethod string +} + +// TransferToWallet godoc +// @Summary Create a transfer to wallet +// @Description Create a transfer to wallet +// @Tags transfer +// @Accept json +// @Produce json +// @Param transferToWallet body CreateTransferReq true "Create Transfer" +// @Success 200 {object} TransferRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /transfer/wallet [post] +func TransferToWallet(logger *slog.Logger, walletSvc *wallet.Service, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + // Get sender ID from the cashier + userID := c.Locals("user_id").(int64) + role := string(c.Locals("role").(domain.Role)) + branchID := c.Locals("branch_id").(int64) + + if role == string(domain.RoleCustomer) { + logger.Error("Unauthorized access", "userID", userID, "role", role) + return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) + } + + branchWallet, err := branchSvc.GetBranchByID(c.Context(), branchID) + if err != nil { + logger.Error("Failed to get branch wallet", "branch ID", branchID, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve branch wallet", err, nil) + } + + var req CreateTransferReq + + if err := c.BodyParser(&req); err != nil { + logger.Error("CreateTransferReq failed", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request", + }) + } + + valErrs, ok := validator.Validate(c, req) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + + transfer, err := walletSvc.TransferToWallet(c.Context(), branchWallet.ID, req.receiverID, domain.Currency(req.amount), domain.PaymentMethod(req.paymentMethod), domain.ValidInt64{Value: userID, Valid: true}) + + if !ok { + response.WriteJSON(c, fiber.StatusInternalServerError, "Transfer Failed", err, nil) + return nil + } + + res := convertTransfer(transfer) + + return response.WriteJSON(c, fiber.StatusOK, "Transfer Successful", res, nil) + + } +} diff --git a/internal/web_server/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index 49412cc..7056c0c 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -13,14 +13,57 @@ import ( ) type WalletRes struct { - ID int64 `json:"id" example:"1"` - Balance float32 `json:"amount" example:"100.0"` - IsWithdraw bool `json:"is_withdraw" example:"true"` - IsBettable bool `json:"is_bettable" example:"true"` - IsActive bool `json:"is_active" example:"true"` - UserID int64 `json:"user_id" example:"1"` - UpdatedAt time.Time `json:"updated_at"` - CreatedAt time.Time `json:"created_at"` + ID int64 `json:"id" example:"1"` + Balance float32 `json:"amount" example:"100.0"` + IsWithdraw bool `json:"is_withdraw" example:"true"` + IsBettable bool `json:"is_bettable" example:"true"` + IsTransferable bool `json:"is_transferable" example:"true"` + IsActive bool `json:"is_active" example:"true"` + UserID int64 `json:"user_id" example:"1"` + UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` +} + +func convertWallet(wallet domain.Wallet) WalletRes { + return WalletRes{ + ID: wallet.ID, + Balance: wallet.Balance.Float64(), + IsWithdraw: wallet.IsWithdraw, + IsBettable: wallet.IsBettable, + IsTransferable: wallet.IsTransferable, + IsActive: wallet.IsActive, + UserID: wallet.UserID, + UpdatedAt: wallet.UpdatedAt, + CreatedAt: wallet.CreatedAt, + } +} + +type CustomerWalletRes struct { + ID int64 `json:"id" example:"1"` + RegularID int64 `json:"regular_id" example:"1"` + RegularBalance float32 `json:"regular_balance" example:"100.0"` + StaticID int64 `json:"static_id" example:"1"` + StaticBalance float32 `json:"static_balance" example:"100.0"` + CustomerID int64 `json:"customer_id" example:"1"` + CompanyID int64 `json:"company_id" example:"1"` + RegularUpdatedAt time.Time `json:"regular_updated_at"` + StaticUpdatedAt time.Time `json:"static_updated_at"` + CreatedAt time.Time `json:"created_at"` +} + +func convertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes { + return CustomerWalletRes{ + ID: wallet.ID, + RegularID: wallet.RegularID, + RegularBalance: wallet.RegularBalance.Float64(), + StaticID: wallet.StaticID, + StaticBalance: wallet.StaticBalance.Float64(), + CustomerID: wallet.CustomerID, + CompanyID: wallet.CompanyID, + RegularUpdatedAt: wallet.RegularUpdatedAt, + StaticUpdatedAt: wallet.StaticUpdatedAt, + CreatedAt: wallet.CreatedAt, + } } // GetWalletByID godoc @@ -51,16 +94,7 @@ func GetWalletByID(logger *slog.Logger, walletSvc *wallet.Service, validator *cu return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve wallet", err, nil) } - res := WalletRes{ - ID: wallet.ID, - Balance: wallet.Balance.Float64(), - IsWithdraw: wallet.IsWithdraw, - IsBettable: wallet.IsBettable, - IsActive: wallet.IsActive, - UserID: wallet.UserID, - UpdatedAt: wallet.UpdatedAt, - CreatedAt: wallet.CreatedAt, - } + res := convertWallet(wallet) return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil) } @@ -87,16 +121,7 @@ func GetAllWallets(logger *slog.Logger, walletSvc *wallet.Service, validator *cu var res []WalletRes = make([]WalletRes, len(wallets)) for _, wallet := range wallets { - res = append(res, WalletRes{ - ID: wallet.ID, - Balance: wallet.Balance.Float64(), - IsWithdraw: wallet.IsWithdraw, - IsBettable: wallet.IsBettable, - IsActive: wallet.IsActive, - UserID: wallet.UserID, - UpdatedAt: wallet.UpdatedAt, - CreatedAt: wallet.CreatedAt, - }) + res = append(res, convertWallet(wallet)) } return response.WriteJSON(c, fiber.StatusOK, "All Wallets retrieved", res, nil) @@ -148,19 +173,6 @@ func UpdateWalletActive(logger *slog.Logger, walletSvc *wallet.Service, validato } } -type CustomerWalletRes struct { - ID int64 `json:"id" example:"1"` - RegularID int64 `json:"regular_id" example:"1"` - RegularBalance float32 `json:"regular_balance" example:"100.0"` - StaticID int64 `json:"static_id" example:"1"` - StaticBalance float32 `json:"static_balance" example:"100.0"` - CustomerID int64 `json:"customer_id" example:"1"` - CompanyID int64 `json:"company_id" example:"1"` - RegularUpdatedAt time.Time `json:"regular_updated_at"` - StaticUpdatedAt time.Time `json:"static_updated_at"` - CreatedAt time.Time `json:"created_at"` -} - // GetCustomerWallet godoc // @Summary Get customer wallet // @Description Retrieve customer wallet details @@ -177,7 +189,7 @@ func GetCustomerWallet(logger *slog.Logger, walletSvc *wallet.Service, validator return func(c *fiber.Ctx) error { userId := c.Locals("user_id").(int64) - role := string(c.Locals("role").(domain.Role)) + // role := string(c.Locals("role").(domain.Role)) vendorID, err := strconv.ParseInt(c.Get("vendor_id"), 10, 64) if err != nil { @@ -185,27 +197,16 @@ func GetCustomerWallet(logger *slog.Logger, walletSvc *wallet.Service, validator } logger.Info("Company ID: " + strconv.FormatInt(vendorID, 10)) - if role != string(domain.RoleCustomer) { - logger.Error("Unauthorized access", "userId", userId, "role", role) - return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) - } + // if role != string(domain.RoleCustomer) { + // logger.Error("Unauthorized access", "userId", userId, "role", role) + // return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) + // } wallet, err := walletSvc.GetCustomerWallet(c.Context(), userId, vendorID) if err != nil { logger.Error("Failed to get customer wallet", "userId", userId, "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve wallet", err, nil) } - res := CustomerWalletRes{ - ID: wallet.ID, - RegularID: wallet.RegularID, - RegularBalance: wallet.RegularBalance.Float64(), - StaticID: wallet.StaticID, - StaticBalance: wallet.StaticBalance.Float64(), - CustomerID: wallet.CustomerID, - CompanyID: wallet.CompanyID, - RegularUpdatedAt: wallet.RegularUpdatedAt, - StaticUpdatedAt: wallet.StaticUpdatedAt, - CreatedAt: wallet.CreatedAt, - } + res := convertCustomerWallet(wallet) return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil) } diff --git a/internal/web_server/middleware.go b/internal/web_server/middleware.go index 4f337fb..46e248b 100644 --- a/internal/web_server/middleware.go +++ b/internal/web_server/middleware.go @@ -4,6 +4,7 @@ import ( "errors" "strings" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" "github.com/gofiber/fiber/v2" ) @@ -39,5 +40,10 @@ func (a *App) authMiddleware(c *fiber.Ctx) error { c.Locals("user_id", claim.UserId) c.Locals("role", claim.Role) c.Locals("refresh_token", refreshToken) + + if claim.Role != domain.RoleCustomer { + // TODO: Add branch id here from the user + // c.Locals("branch_id", claim.) + } return c.Next() } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 204b3c5..5bb2b70 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -42,9 +42,26 @@ func (a *App) initAppRoutes() { a.fiber.Get("/user/wallet", a.authMiddleware, handlers.GetCustomerWallet(a.logger, a.walletSvc, a.validator)) + a.fiber.Get("/manager/:id/branch", handlers.GetBranchByManagerID(a.logger, a.branchSvc, a.validator)) + + a.fiber.Get("/company/:id/branch", handlers.GetBranchByCompanyID(a.logger, a.branchSvc, a.validator)) + // Swagger a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) + // Branch + a.fiber.Post("/branch", handlers.CreateBranch(a.logger, a.branchSvc, a.walletSvc, a.validator)) + a.fiber.Get("/branch", handlers.GetAllBranches(a.logger, a.branchSvc, a.validator)) + a.fiber.Get("/branch/:id", handlers.GetBranchByID(a.logger, a.branchSvc, a.validator)) + a.fiber.Put("/branch/:id", handlers.UpdateBranch(a.logger, a.branchSvc, a.validator)) + a.fiber.Delete("/branch/:id", handlers.DeleteBranch(a.logger, a.branchSvc, a.validator)) + + // Branch Operation + a.fiber.Post("/supportedOperation", handlers.CreateSupportedOperation(a.logger, a.branchSvc, a.validator)) + a.fiber.Post("/operation", handlers.CreateBranchOperation(a.logger, a.branchSvc, a.validator)) + a.fiber.Get("/branch/:id/operation", handlers.GetBranchOperations(a.logger, a.branchSvc, a.validator)) + a.fiber.Delete("/branch/:id/operation/:opID", handlers.DeleteBranchOperation(a.logger, a.branchSvc, a.validator)) + // Ticket a.fiber.Post("/ticket", handlers.CreateTicket(a.logger, a.ticketSvc, a.validator)) a.fiber.Get("/ticket", handlers.GetAllTickets(a.logger, a.ticketSvc, a.validator)) @@ -62,9 +79,13 @@ func (a *App) initAppRoutes() { a.fiber.Get("/wallet/:id", handlers.GetWalletByID(a.logger, a.walletSvc, a.validator)) a.fiber.Put("/wallet/:id", handlers.UpdateWalletActive(a.logger, a.walletSvc, a.validator)) - // Transactions /transactions + // Transfer + // /transfer/wallet - transfer from one wallet to another wallet + a.fiber.Post("/transfer/wallet", handlers.TransferToWallet(a.logger, a.walletSvc, a.branchSvc, a.validator)) + + // Transactions a.fiber.Post("/transaction", handlers.CreateTransaction(a.logger, a.transactionSvc, a.validator)) - a.fiber.Get("/transaction", handlers.GetAllTransactions(a.logger, a.transactionSvc, a.validator)) + a.fiber.Get("/transaction", handlers.GetAllTransactions(a.logger, a.transactionSvc, a.userSvc, a.validator)) a.fiber.Get("/transaction/:id", handlers.GetTransactionByID(a.logger, a.transactionSvc, a.validator)) a.fiber.Patch("/transaction/:id", handlers.UpdateTransactionVerified(a.logger, a.transactionSvc, a.validator)) From 7dd6221f74979136dd04e430878eb54ac693ff99 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 9 Apr 2025 15:33:51 +0300 Subject: [PATCH 03/30] additions and small fixes --- db/migrations/000001_fortune.up.sql | 27 ++++- db/query/branch.sql | 3 + db/query/user.sql | 7 ++ docs/docs.go | 112 +++++++++++++++++- docs/swagger.json | 112 +++++++++++++++++- docs/swagger.yaml | 73 ++++++++++++ gen/db/branch.sql.go | 36 ++++++ gen/db/user.sql.go | 52 ++++++++ internal/domain/user.go | 2 +- internal/repository/bet.go | 2 +- internal/repository/branch.go | 112 ++++++++++++------ internal/repository/ticket.go | 2 +- internal/repository/transaction.go | 4 +- internal/repository/transfer.go | 4 +- internal/repository/user.go | 24 ++++ internal/repository/wallet.go | 4 +- internal/services/branch/port.go | 2 + internal/services/branch/service.go | 8 ++ internal/services/user/port.go | 2 +- internal/services/user/register.go | 2 +- internal/services/user/user.go | 8 ++ internal/web_server/handlers/bet_handler.go | 4 +- .../web_server/handlers/branch_handler.go | 50 ++++++-- .../web_server/handlers/ticket_handler.go | 2 +- .../handlers/transaction_handler.go | 2 +- .../web_server/handlers/transfer_handler.go | 53 ++++----- internal/web_server/handlers/user.go | 58 +++++++++ .../web_server/handlers/wallet_handler.go | 2 +- internal/web_server/routes.go | 13 +- 29 files changed, 682 insertions(+), 100 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 27ae8b1..91e589f 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS users ( -- suspended_at TIMESTAMPTZ NULL, -- this can be NULL if the user is not suspended suspended BOOLEAN NOT NULL DEFAULT FALSE, - CHECK (email IS NOT NULL OR phone_number IS NOT NULL) + CHECK (email IS NOT NULL OR phone_number IS NOT NULL) ); CREATE TABLE refresh_tokens ( id BIGSERIAL PRIMARY KEY, @@ -64,13 +64,15 @@ CREATE TABLE IF NOT EXISTS tickets ( -- CREATE TABLE IF NOT EXISTS bet_outcomes ( -- id BIGSERIAL PRIMARY KEY, -- bet_id BIGINT NOT NULL, --- outcome_id BIGINT NOT NULL, +-- event_id bigint not null, +-- odd_id BIGINT NOT NULL, -- ); -- CREATE TABLE IF NOT EXISTS ticket_outcomes ( -- id BIGSERIAL PRIMARY KEY, -- ticket_id BIGINT NOT NULL, --- outcome_id BIGINT NOT NULL, +-- event_id bigint not null, +-- odd_id BIGINT NOT NULL, -- ); CREATE TABLE IF NOT EXISTS wallets ( @@ -186,6 +188,25 @@ INSERT INTO users ( FALSE ); +INSERT INTO users ( + first_name, last_name, email, phone_number, password, role, + email_verified, phone_verified, created_at, updated_at, + suspended_at, suspended +) VALUES ( + 'Samuel', + 'Tariku', + 'cybersamt@gmail.com', + NULL, + crypt('password@123', gen_salt('bf'))::bytea, + 'cashier', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + NULL, + FALSE +); + INSERT INTO supported_operations ( name, description diff --git a/db/query/branch.sql b/db/query/branch.sql index 736cf08..97b374e 100644 --- a/db/query/branch.sql +++ b/db/query/branch.sql @@ -20,6 +20,9 @@ SELECT * FROM branch_details WHERE company_id = $1; -- name: GetBranchByManagerID :many SELECT * FROM branch_details WHERE branch_manager_id = $1; +-- name: SearchBranchByName :many +SELECT * FROM branch_details WHERE name ILIKE '%' || $1 || '%'; + -- name: GetAllSupportedOperations :many SELECT * FROM supported_operations; diff --git a/db/query/user.sql b/db/query/user.sql index 04cbf84..8e127af 100644 --- a/db/query/user.sql +++ b/db/query/user.sql @@ -13,6 +13,13 @@ WHERE id = $1; SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at FROM users; +-- name: SearchUserByNameOrPhone :many +SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at +FROM users +WHERE first_name ILIKE '%' || $1 || '%' + OR last_name ILIKE '%' || $1 || '%' + OR phone_number LIKE '%' || $1 || '%'; + -- name: UpdateUser :exec UPDATE users SET first_name = $1, last_name = $2, email = $3, phone_number = $4, role = $5, updated_at = $6 diff --git a/docs/docs.go b/docs/docs.go index 8cbb6b4..cd7719d 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -857,6 +857,42 @@ const docTemplate = `{ } }, "/supportedOperation": { + "get": { + "description": "Gets all supported operations", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets all supported operations", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BranchDetailRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, "post": { "description": "Creates a supported operation", "consumes": [ @@ -1429,6 +1465,52 @@ const docTemplate = `{ } } }, + "/user/search": { + "post": { + "description": "Search for user using name or phone", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Search for user using name or phone", + "parameters": [ + { + "description": "Search for using his name or phone", + "name": "searchUserByNameOrPhone", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.SearchUserByNameOrPhoneReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.UserProfileRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/user/sendRegisterCode": { "post": { "description": "Send register code", @@ -1983,6 +2065,12 @@ const docTemplate = `{ "name": { "type": "string", "example": "4-kilo Branch" + }, + "operations": { + "type": "array", + "items": { + "type": "integer" + } } } }, @@ -2081,7 +2169,21 @@ const docTemplate = `{ } }, "handlers.CreateTransferReq": { - "type": "object" + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "payment_method": { + "type": "string", + "example": "cash" + }, + "receiver_id": { + "type": "integer", + "example": 1 + } + } }, "handlers.CustomerWalletRes": { "type": "object", @@ -2202,6 +2304,14 @@ const docTemplate = `{ } } }, + "handlers.SearchUserByNameOrPhoneReq": { + "type": "object", + "properties": { + "searchString": { + "type": "string" + } + } + }, "handlers.SupportedOperationRes": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 21d28dc..7ecbb4f 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -849,6 +849,42 @@ } }, "/supportedOperation": { + "get": { + "description": "Gets all supported operations", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets all supported operations", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BranchDetailRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, "post": { "description": "Creates a supported operation", "consumes": [ @@ -1421,6 +1457,52 @@ } } }, + "/user/search": { + "post": { + "description": "Search for user using name or phone", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Search for user using name or phone", + "parameters": [ + { + "description": "Search for using his name or phone", + "name": "searchUserByNameOrPhone", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.SearchUserByNameOrPhoneReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.UserProfileRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/user/sendRegisterCode": { "post": { "description": "Send register code", @@ -1975,6 +2057,12 @@ "name": { "type": "string", "example": "4-kilo Branch" + }, + "operations": { + "type": "array", + "items": { + "type": "integer" + } } } }, @@ -2073,7 +2161,21 @@ } }, "handlers.CreateTransferReq": { - "type": "object" + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "payment_method": { + "type": "string", + "example": "cash" + }, + "receiver_id": { + "type": "integer", + "example": 1 + } + } }, "handlers.CustomerWalletRes": { "type": "object", @@ -2194,6 +2296,14 @@ } } }, + "handlers.SearchUserByNameOrPhoneReq": { + "type": "object", + "properties": { + "searchString": { + "type": "string" + } + } + }, "handlers.SupportedOperationRes": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e6031ac..69e4307 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -199,6 +199,10 @@ definitions: name: example: 4-kilo Branch type: string + operations: + items: + type: integer + type: array type: object handlers.CreateSupportedOperationReq: properties: @@ -265,6 +269,16 @@ definitions: type: string type: object handlers.CreateTransferReq: + properties: + amount: + example: 100 + type: number + payment_method: + example: cash + type: string + receiver_id: + example: 1 + type: integer type: object handlers.CustomerWalletRes: properties: @@ -350,6 +364,11 @@ definitions: phoneNumber: type: string type: object + handlers.SearchUserByNameOrPhoneReq: + properties: + searchString: + type: string + type: object handlers.SupportedOperationRes: properties: description: @@ -1135,6 +1154,30 @@ paths: tags: - branch /supportedOperation: + get: + consumes: + - application/json + description: Gets all supported operations + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.BranchDetailRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets all supported operations + tags: + - branch post: consumes: - application/json @@ -1509,6 +1552,36 @@ paths: summary: Reset password tags: - user + /user/search: + post: + consumes: + - application/json + description: Search for user using name or phone + parameters: + - description: Search for using his name or phone + in: body + name: searchUserByNameOrPhone + required: true + schema: + $ref: '#/definitions/handlers.SearchUserByNameOrPhoneReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.UserProfileRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Search for user using name or phone + tags: + - user /user/sendRegisterCode: post: consumes: diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index 5197e58..8d34fb3 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -309,6 +309,42 @@ func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]Ge return items, nil } +const SearchBranchByName = `-- name: SearchBranchByName :many +SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number FROM branch_details WHERE name ILIKE '%' || $1 || '%' +` + +func (q *Queries) SearchBranchByName(ctx context.Context, dollar_1 pgtype.Text) ([]BranchDetail, error) { + rows, err := q.db.Query(ctx, SearchBranchByName, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + var items []BranchDetail + for rows.Next() { + var i BranchDetail + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Location, + &i.WalletID, + &i.BranchManagerID, + &i.CompanyID, + &i.IsSelfOwned, + &i.CreatedAt, + &i.UpdatedAt, + &i.ManagerName, + &i.ManagerPhoneNumber, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const UpdateBranch = `-- name: UpdateBranch :one UPDATE branches SET name = $1, location = $2, branch_manager_id = $3, company_id = $4, is_self_owned = $5 WHERE id = $6 RETURNING id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at ` diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 39f0a5c..bce776c 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -256,6 +256,58 @@ func (q *Queries) GetUserByPhone(ctx context.Context, phoneNumber pgtype.Text) ( return i, err } +const SearchUserByNameOrPhone = `-- name: SearchUserByNameOrPhone :many +SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at +FROM users +WHERE first_name ILIKE '%' || $1 || '%' + OR last_name ILIKE '%' || $1 || '%' + OR phone_number LIKE '%' || $1 || '%' +` + +type SearchUserByNameOrPhoneRow struct { + ID int64 + FirstName string + LastName string + Email pgtype.Text + PhoneNumber pgtype.Text + Role string + EmailVerified bool + PhoneVerified bool + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + +func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, dollar_1 pgtype.Text) ([]SearchUserByNameOrPhoneRow, error) { + rows, err := q.db.Query(ctx, SearchUserByNameOrPhone, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SearchUserByNameOrPhoneRow + for rows.Next() { + var i SearchUserByNameOrPhoneRow + if err := rows.Scan( + &i.ID, + &i.FirstName, + &i.LastName, + &i.Email, + &i.PhoneNumber, + &i.Role, + &i.EmailVerified, + &i.PhoneVerified, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const UpdatePassword = `-- name: UpdatePassword :exec UPDATE users SET password = $1, updated_at = $4 diff --git a/internal/domain/user.go b/internal/domain/user.go index ea44cc8..b4d2fa4 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -33,7 +33,7 @@ type RegisterUserReq struct { Email string PhoneNumber string Password string - //Role string + Role string Otp string ReferalCode string // diff --git a/internal/repository/bet.go b/internal/repository/bet.go index c4d0362..2ddef36 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -73,7 +73,7 @@ func (s *Store) GetAllBets(ctx context.Context) ([]domain.Bet, error) { return nil, err } - var result []domain.Bet = make([]domain.Bet, len(bets)) + var result []domain.Bet = make([]domain.Bet, 0, len(bets)) for _, bet := range bets { result = append(result, convertDBBet(bet)) } diff --git a/internal/repository/branch.go b/internal/repository/branch.go index a37493a..6268615 100644 --- a/internal/repository/branch.go +++ b/internal/repository/branch.go @@ -5,6 +5,7 @@ import ( dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" ) func convertCreateBranch(branch domain.CreateBranch) dbgen.CreateBranchParams { @@ -54,29 +55,6 @@ func (s *Store) CreateBranch(ctx context.Context, branch domain.CreateBranch) (d return convertDBBranch(dbBranch), nil } -func (s *Store) CreateSupportedOperation(ctx context.Context, supportedOperation domain.CreateSupportedOperation) (domain.SupportedOperation, error) { - dbSupportedOperation, err := s.queries.CreateSupportedOperation(ctx, dbgen.CreateSupportedOperationParams{ - Name: supportedOperation.Name, - Description: supportedOperation.Description, - }) - if err != nil { - return domain.SupportedOperation{}, err - } - return domain.SupportedOperation{ - ID: dbSupportedOperation.ID, - Name: dbSupportedOperation.Name, - Description: dbSupportedOperation.Description, - }, nil -} - -func (s *Store) CreateBranchOperation(ctx context.Context, branchOperation domain.CreateBranchOperation) error { - _, err := s.queries.CreateBranchOperation(ctx, dbgen.CreateBranchOperationParams{ - BranchID: branchOperation.BranchID, - OperationID: branchOperation.OperationID, - }) - return err -} - func (s *Store) GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error) { dbBranch, err := s.queries.GetBranchByID(ctx, id) if err != nil { @@ -108,22 +86,6 @@ func (s *Store) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]do return branches, nil } -func (s *Store) GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) { - dbBranchOperations, err := s.queries.GetBranchOperations(ctx, branchID) - if err != nil { - return nil, err - } - var branchOperations []domain.BranchOperation = make([]domain.BranchOperation, 0, len(dbBranchOperations)) - for _, dbBranchOperation := range dbBranchOperations { - branchOperations = append(branchOperations, domain.BranchOperation{ - ID: dbBranchOperation.ID, - OperationName: dbBranchOperation.Name, - OperationDescription: dbBranchOperation.Description, - }) - } - return branchOperations, nil -} - func (s *Store) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) { dbBranches, err := s.queries.GetAllBranches(ctx) if err != nil { @@ -136,6 +98,19 @@ func (s *Store) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, erro return branches, nil } +func (s *Store) SearchBranchByName(ctx context.Context, name string) ([]domain.BranchDetail, error) { + dbBranches, err := s.queries.SearchBranchByName(ctx, pgtype.Text{String: name, Valid: true}) + if err != nil { + return nil, err + } + + var branches []domain.BranchDetail = make([]domain.BranchDetail, 0, len(dbBranches)) + for _, dbBranch := range dbBranches { + branches = append(branches, convertDBBranchDetail(dbBranch)) + } + return branches, nil +} + func (s *Store) UpdateBranch(ctx context.Context, id int64, branch domain.UpdateBranch) (domain.Branch, error) { dbBranch, err := s.queries.UpdateBranch(ctx, dbgen.UpdateBranchParams{ ID: id, @@ -154,6 +129,65 @@ func (s *Store) DeleteBranch(ctx context.Context, id int64) error { return s.queries.DeleteBranch(ctx, id) } +// Branch Operations + +func (s *Store) CreateBranchOperation(ctx context.Context, branchOperation domain.CreateBranchOperation) error { + _, err := s.queries.CreateBranchOperation(ctx, dbgen.CreateBranchOperationParams{ + BranchID: branchOperation.BranchID, + OperationID: branchOperation.OperationID, + }) + return err +} + +func (s *Store) CreateSupportedOperation(ctx context.Context, supportedOperation domain.CreateSupportedOperation) (domain.SupportedOperation, error) { + dbSupportedOperation, err := s.queries.CreateSupportedOperation(ctx, dbgen.CreateSupportedOperationParams{ + Name: supportedOperation.Name, + Description: supportedOperation.Description, + }) + if err != nil { + return domain.SupportedOperation{}, err + } + return domain.SupportedOperation{ + ID: dbSupportedOperation.ID, + Name: dbSupportedOperation.Name, + Description: dbSupportedOperation.Description, + }, nil +} + +func (s *Store) GetAllSupportedOperations(ctx context.Context) ([]domain.SupportedOperation, error) { + dbOperations, err := s.queries.GetAllSupportedOperations(ctx) + if err != nil { + return nil, err + } + + var operations []domain.SupportedOperation = make([]domain.SupportedOperation, 0, len(dbOperations)) + for _, dbOperation := range dbOperations { + operations = append(operations, domain.SupportedOperation{ + ID: dbOperation.ID, + Name: dbOperation.Name, + Description: dbOperation.Description, + }) + } + return operations, nil + +} + +func (s *Store) GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) { + dbBranchOperations, err := s.queries.GetBranchOperations(ctx, branchID) + if err != nil { + return nil, err + } + var branchOperations []domain.BranchOperation = make([]domain.BranchOperation, 0, len(dbBranchOperations)) + for _, dbBranchOperation := range dbBranchOperations { + branchOperations = append(branchOperations, domain.BranchOperation{ + ID: dbBranchOperation.ID, + OperationName: dbBranchOperation.Name, + OperationDescription: dbBranchOperation.Description, + }) + } + return branchOperations, nil +} + func (s *Store) DeleteBranchOperation(ctx context.Context, branchID int64, operationID int64) error { err := s.queries.DeleteBranchOperation(ctx, dbgen.DeleteBranchOperationParams{ BranchID: branchID, diff --git a/internal/repository/ticket.go b/internal/repository/ticket.go index d6918d3..b7945c3 100644 --- a/internal/repository/ticket.go +++ b/internal/repository/ticket.go @@ -51,7 +51,7 @@ func (s *Store) GetAllTickets(ctx context.Context) ([]domain.Ticket, error) { return nil, err } - var result []domain.Ticket = make([]domain.Ticket, len(tickets)) + var result []domain.Ticket = make([]domain.Ticket, 0, len(tickets)) for _, ticket := range tickets { result = append(result, convertDBTicket(ticket)) } diff --git a/internal/repository/transaction.go b/internal/repository/transaction.go index f0faad3..e7f3e4f 100644 --- a/internal/repository/transaction.go +++ b/internal/repository/transaction.go @@ -66,7 +66,7 @@ func (s *Store) GetAllTransactions(ctx context.Context) ([]domain.Transaction, e return nil, err } - var result []domain.Transaction = make([]domain.Transaction, len(transaction)) + var result []domain.Transaction = make([]domain.Transaction, 0, len(transaction)) for _, ticket := range transaction { result = append(result, convertDBTransaction(ticket)) } @@ -79,7 +79,7 @@ func (s *Store) GetTransactionByBranch(ctx context.Context, id int64) ([]domain. return nil, err } - var result []domain.Transaction = make([]domain.Transaction, len(transaction)) + var result []domain.Transaction = make([]domain.Transaction, 0, len(transaction)) for _, ticket := range transaction { result = append(result, convertDBTransaction(ticket)) } diff --git a/internal/repository/transfer.go b/internal/repository/transfer.go index 70884f3..7ee876e 100644 --- a/internal/repository/transfer.go +++ b/internal/repository/transfer.go @@ -57,7 +57,7 @@ func (s *Store) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error) if err != nil { return nil, err } - var result []domain.Transfer = make([]domain.Transfer, len(transfers)) + var result []domain.Transfer = make([]domain.Transfer, 0, len(transfers)) for _, transfer := range transfers { result = append(result, convertDBTransfer(transfer)) @@ -70,7 +70,7 @@ func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]dom return nil, err } - var result []domain.Transfer = make([]domain.Transfer, len(transfers)) + var result []domain.Transfer = make([]domain.Transfer, 0, len(transfers)) for _, transfer := range transfers { result = append(result, convertDBTransfer(transfer)) diff --git a/internal/repository/user.go b/internal/repository/user.go index 2177b1e..98814f0 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -100,6 +100,30 @@ func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) { } return userList, nil } + +func (s *Store) SearchUserByNameOrPhone(ctx context.Context, searchString string) ([]domain.User, error) { + users, err := s.queries.SearchUserByNameOrPhone(ctx, pgtype.Text{ + String: searchString, + Valid: true, + }) + if err != nil { + return nil, err + } + + userList := make([]domain.User, 0, len(users)) + for _, user := range users { + userList = append(userList, domain.User{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email.String, + PhoneNumber: user.PhoneNumber.String, + Role: domain.Role(user.Role), + }) + } + return userList, nil +} + func (s *Store) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error { err := s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{ // ID: user.ID, diff --git a/internal/repository/wallet.go b/internal/repository/wallet.go index 7126fc4..97a87e5 100644 --- a/internal/repository/wallet.go +++ b/internal/repository/wallet.go @@ -95,7 +95,7 @@ func (s *Store) GetAllWallets(ctx context.Context) ([]domain.Wallet, error) { return nil, err } - var result []domain.Wallet = make([]domain.Wallet, len(wallets)) + var result []domain.Wallet = make([]domain.Wallet, 0, len(wallets)) for _, wallet := range wallets { result = append(result, convertDBWallet(wallet)) @@ -109,7 +109,7 @@ func (s *Store) GetWalletsByUser(ctx context.Context, userID int64) ([]domain.Wa return nil, err } - var result []domain.Wallet = make([]domain.Wallet, len(wallets)) + var result []domain.Wallet = make([]domain.Wallet, 0, len(wallets)) for _, wallet := range wallets { result = append(result, convertDBWallet(wallet)) diff --git a/internal/services/branch/port.go b/internal/services/branch/port.go index 633fa31..e2f2ec1 100644 --- a/internal/services/branch/port.go +++ b/internal/services/branch/port.go @@ -15,6 +15,8 @@ type BranchStore interface { GetBranchByCompanyID(ctx context.Context, companyID int64) ([]domain.BranchDetail, error) GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) + GetAllSupportedOperations(ctx context.Context) ([]domain.SupportedOperation, error) + SearchBranchByName(ctx context.Context, name string) ([]domain.BranchDetail, error) UpdateBranch(ctx context.Context, id int64, branch domain.UpdateBranch) (domain.Branch, error) DeleteBranch(ctx context.Context, id int64) error DeleteBranchOperation(ctx context.Context, branchID int64, operationID int64) error diff --git a/internal/services/branch/service.go b/internal/services/branch/service.go index 49a87ff..83d97e2 100644 --- a/internal/services/branch/service.go +++ b/internal/services/branch/service.go @@ -40,6 +40,14 @@ func (s *Service) GetBranchOperations(ctx context.Context, branchID int64) ([]do func (s *Service) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) { return s.branchStore.GetAllBranches(ctx) } + +func (s *Service) GetAllSupportedOperations(ctx context.Context) ([]domain.SupportedOperation, error) { + return s.branchStore.GetAllSupportedOperations(ctx) +} + +func (s *Service) SearchBranchByName(ctx context.Context, name string) ([]domain.BranchDetail, error) { + return s.branchStore.SearchBranchByName(ctx, name) +} func (s *Service) UpdateBranch(ctx context.Context, id int64, branch domain.UpdateBranch) (domain.Branch, error) { return s.branchStore.UpdateBranch(ctx, id, branch) } diff --git a/internal/services/user/port.go b/internal/services/user/port.go index aaf502d..bcfdd5a 100644 --- a/internal/services/user/port.go +++ b/internal/services/user/port.go @@ -15,7 +15,7 @@ type UserStore interface { CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) GetUserByEmail(ctx context.Context, email string) (domain.User, error) GetUserByPhone(ctx context.Context, phoneNum string) (domain.User, error) - // + SearchUserByNameOrPhone(ctx context.Context, searchString string) ([]domain.User, error) UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error // identifier verified email or phone } type SmsGateway interface { diff --git a/internal/services/user/register.go b/internal/services/user/register.go index 7966254..dcefa99 100644 --- a/internal/services/user/register.go +++ b/internal/services/user/register.go @@ -65,7 +65,7 @@ func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterU Email: registerReq.Email, PhoneNumber: registerReq.PhoneNumber, Password: hashedPassword, - Role: "user", + Role: domain.RoleCustomer, EmailVerified: registerReq.OtpMedium == domain.OtpMediumEmail, PhoneVerified: registerReq.OtpMedium == domain.OtpMediumSms, } diff --git a/internal/services/user/user.go b/internal/services/user/user.go index 5b65b94..5c141c7 100644 --- a/internal/services/user/user.go +++ b/internal/services/user/user.go @@ -6,6 +6,14 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) + + + +func (s *Service) SearchUserByNameOrPhone(ctx context.Context, searchString string) ([]domain.User, error) { + // Search user + return s.userStore.SearchUserByNameOrPhone(ctx, searchString) + +} func (s *Service) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error { // update user return s.userStore.UpdateUser(ctx, user) diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 5f47b7c..5c9678f 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -62,7 +62,9 @@ func convertBet(bet domain.Bet) BetRes { func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { // TODO if user is customer, get id from the token then get the wallet id from there + // TODO: If user is a cashier, check the token, and find the role and get the branch id from there. Reduce amount from the branch wallet + var isShopBet bool = true var branchID int64 = 1 @@ -134,7 +136,7 @@ func GetAllBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalida return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bets", err, nil) } - var res []BetRes = make([]BetRes, len(bets)) + var res []BetRes = make([]BetRes, 0, len(bets)) for _, bet := range bets { res = append(res, convertBet(bet)) } diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index e1af815..8e0552e 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -13,11 +13,12 @@ import ( ) type CreateBranchReq struct { - Name string `json:"name" example:"4-kilo Branch"` - Location string `json:"location" example:"Addis Ababa"` - BranchManagerID int64 `json:"branch_manager_id" example:"1"` - CompanyID int64 `json:"company_id" example:"1"` - IsSelfOwned bool `json:"is_self_owned" example:"false"` + Name string `json:"name" example:"4-kilo Branch"` + Location string `json:"location" example:"Addis Ababa"` + BranchManagerID int64 `json:"branch_manager_id" example:"1"` + CompanyID int64 `json:"company_id" example:"1"` + IsSelfOwned bool `json:"is_self_owned" example:"false"` + Operations []int64 `json:"operations"` } type CreateSupportedOperationReq struct { @@ -310,7 +311,7 @@ func GetBranchByManagerID(logger *slog.Logger, branchSvc *branch.Service, valida logger.Error("Failed to get branches", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get branches", err, nil) } - var result []BranchDetailRes = make([]BranchDetailRes, len(branches)) + var result []BranchDetailRes = make([]BranchDetailRes, 0, len(branches)) for _, branch := range branches { result = append(result, convertBranchDetail(branch)) } @@ -344,7 +345,7 @@ func GetBranchByCompanyID(logger *slog.Logger, branchSvc *branch.Service, valida return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get branches", err, nil) } - var result []BranchDetailRes = make([]BranchDetailRes, len(branches)) + var result []BranchDetailRes = make([]BranchDetailRes, 0, len(branches)) for _, branch := range branches { result = append(result, convertBranchDetail(branch)) } @@ -370,7 +371,7 @@ func GetAllBranches(logger *slog.Logger, branchSvc *branch.Service, validator *c return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get branches", err, nil) } - var result []BranchDetailRes = make([]BranchDetailRes, len(branches)) + var result []BranchDetailRes = make([]BranchDetailRes, 0, len(branches)) for _, branch := range branches { result = append(result, convertBranchDetail(branch)) } @@ -379,6 +380,37 @@ func GetAllBranches(logger *slog.Logger, branchSvc *branch.Service, validator *c } } +// GetAllSupportedOperations godoc +// @Summary Gets all supported operations +// @Description Gets all supported operations +// @Tags branch +// @Accept json +// @Produce json +// @Success 200 {array} BranchDetailRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /supportedOperation [get] +func GetAllSupportedOperations(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + operations, err := branchSvc.GetAllSupportedOperations(c.Context()) + if err != nil { + logger.Error("Failed to get operations", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get operations", err, nil) + } + + var result []SupportedOperationRes = make([]SupportedOperationRes, 0, len(operations)) + for _, operation := range operations { + result = append(result, SupportedOperationRes{ + ID: operation.ID, + Name: operation.Name, + Description: operation.Description, + }) + } + return response.WriteJSON(c, fiber.StatusOK, "SupportedOperations for Company retrieved", result, nil) + + } +} + // GetBranchOperations godoc // @Summary Gets branch operations // @Description Gets branch operations @@ -407,7 +439,7 @@ func GetBranchOperations(logger *slog.Logger, branchSvc *branch.Service, validat return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve operation", err, nil) } - var result []BranchOperationRes = make([]BranchOperationRes, len(operations)) + var result []BranchOperationRes = make([]BranchOperationRes, 0, len(operations)) for _, branch := range operations { result = append(result, BranchOperationRes{ diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index 68bba4b..05339d4 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -136,7 +136,7 @@ func GetAllTickets(logger *slog.Logger, ticketSvc *ticket.Service, return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve tickets", err, nil) } - var res []TicketRes = make([]TicketRes, len(tickets)) + var res []TicketRes = make([]TicketRes, 0, len(tickets)) for _, ticket := range tickets { res = append(res, TicketRes{ diff --git a/internal/web_server/handlers/transaction_handler.go b/internal/web_server/handlers/transaction_handler.go index 0a93bf0..77f80c9 100644 --- a/internal/web_server/handlers/transaction_handler.go +++ b/internal/web_server/handlers/transaction_handler.go @@ -167,7 +167,7 @@ func GetAllTransactions( } // Convert transactions to response format - var res []TransactionRes = make([]TransactionRes, len(transactions)) + var res []TransactionRes = make([]TransactionRes, 0, len(transactions)) for i, transaction := range transactions { res[i] = convertTransaction(transaction) } diff --git a/internal/web_server/handlers/transfer_handler.go b/internal/web_server/handlers/transfer_handler.go index c8635ee..4ff7810 100644 --- a/internal/web_server/handlers/transfer_handler.go +++ b/internal/web_server/handlers/transfer_handler.go @@ -19,42 +19,41 @@ type TransferRes struct { Type string `json:"type" example:"transfer"` PaymentMethod string `json:"payment_method" example:"bank"` ReceiverWalletID int64 `json:"receiver_wallet_id" example:"1"` - SenderWalletID *int64 `json:"sender_wallet_id" example:"1"` - CashierID *int64 `json:"cashier_id" example:"789"` + SenderWalletID *int64 `json:"sender_wallet_id" example:"1"` + CashierID *int64 `json:"cashier_id" example:"789"` CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"` UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:30:00Z"` } - func convertTransfer(transfer domain.Transfer) TransferRes { - var senderWalletID *int64 - if transfer.SenderWalletID.Valid { - senderWalletID = &transfer.SenderWalletID.Value - } + var senderWalletID *int64 + if transfer.SenderWalletID.Valid { + senderWalletID = &transfer.SenderWalletID.Value + } - var cashierID *int64 - if transfer.CashierID.Valid { - cashierID = &transfer.CashierID.Value - } + var cashierID *int64 + if transfer.CashierID.Valid { + cashierID = &transfer.CashierID.Value + } - return TransferRes{ - ID: transfer.ID, - Amount: transfer.Amount.Float64(), - Verified: transfer.Verified, - Type: string(transfer.Type), - PaymentMethod: string(transfer.PaymentMethod), - ReceiverWalletID: transfer.ReceiverWalletID, - SenderWalletID: senderWalletID, - CashierID: cashierID, - CreatedAt: transfer.CreatedAt, - UpdatedAt: transfer.UpdatedAt, - } + return TransferRes{ + ID: transfer.ID, + Amount: transfer.Amount.Float64(), + Verified: transfer.Verified, + Type: string(transfer.Type), + PaymentMethod: string(transfer.PaymentMethod), + ReceiverWalletID: transfer.ReceiverWalletID, + SenderWalletID: senderWalletID, + CashierID: cashierID, + CreatedAt: transfer.CreatedAt, + UpdatedAt: transfer.UpdatedAt, + } } type CreateTransferReq struct { - receiverID int64 - amount float64 - paymentMethod string + ReceiverID int64 `json:"receiver_id" example:"1"` + Amount float64 `json:"amount" example:"100.0"` + PaymentMethod string `json:"payment_method" example:"cash"` } // TransferToWallet godoc @@ -101,7 +100,7 @@ func TransferToWallet(logger *slog.Logger, walletSvc *wallet.Service, branchSvc return nil } - transfer, err := walletSvc.TransferToWallet(c.Context(), branchWallet.ID, req.receiverID, domain.Currency(req.amount), domain.PaymentMethod(req.paymentMethod), domain.ValidInt64{Value: userID, Valid: true}) + transfer, err := walletSvc.TransferToWallet(c.Context(), branchWallet.ID, req.ReceiverID, domain.Currency(req.Amount), domain.PaymentMethod(req.PaymentMethod), domain.ValidInt64{Value: userID, Valid: true}) if !ok { response.WriteJSON(c, fiber.StatusInternalServerError, "Transfer Failed", err, nil) diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index 665a1ee..a854e78 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -374,3 +374,61 @@ func getMedium(email, phoneNumber string) (domain.OtpMedium, error) { } return "", errors.New("both email and phone number are empty") } + +type SearchUserByNameOrPhoneReq struct { + SearchString string +} + +// SearchUserByNameOrPhone godoc +// @Summary Search for user using name or phone +// @Description Search for user using name or phone +// @Tags user +// @Accept json +// @Produce json +// @Param searchUserByNameOrPhone body SearchUserByNameOrPhoneReq true "Search for using his name or phone" +// @Success 200 {object} UserProfileRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /user/search [post] +func SearchUserByNameOrPhone(logger *slog.Logger, userSvc *user.Service, + validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + var req SearchUserByNameOrPhoneReq + if err := c.BodyParser(&req); err != nil { + logger.Error("SearchUserByNameOrPhone failed", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request", + }) + } + valErrs, ok := validator.Validate(c, req) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + users, err := userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString) + if err != nil { + logger.Error("SearchUserByNameOrPhone failed", "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Internal server error", + }) + } + var res []UserProfileRes = make([]UserProfileRes, 0, len(users)) + for _, user := range users { + res = append(res, UserProfileRes{ + 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, + }) + } + return response.WriteJSON(c, fiber.StatusOK, "Search Successful", res, nil) + } +} diff --git a/internal/web_server/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index 7056c0c..de655f6 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -118,7 +118,7 @@ func GetAllWallets(logger *slog.Logger, walletSvc *wallet.Service, validator *cu logger.Error("Failed to get wallets", "error", err) } - var res []WalletRes = make([]WalletRes, len(wallets)) + var res []WalletRes = make([]WalletRes, 0, len(wallets)) for _, wallet := range wallets { res = append(res, convertWallet(wallet)) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 5bb2b70..b25ed9d 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -41,6 +41,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/user/profile", a.authMiddleware, handlers.UserProfile(a.logger, a.userSvc)) a.fiber.Get("/user/wallet", a.authMiddleware, handlers.GetCustomerWallet(a.logger, a.walletSvc, a.validator)) + a.fiber.Post("/user/search", handlers.SearchUserByNameOrPhone(a.logger, a.userSvc, a.validator)) a.fiber.Get("/manager/:id/branch", handlers.GetBranchByManagerID(a.logger, a.branchSvc, a.validator)) @@ -55,8 +56,10 @@ func (a *App) initAppRoutes() { a.fiber.Get("/branch/:id", handlers.GetBranchByID(a.logger, a.branchSvc, a.validator)) a.fiber.Put("/branch/:id", handlers.UpdateBranch(a.logger, a.branchSvc, a.validator)) a.fiber.Delete("/branch/:id", handlers.DeleteBranch(a.logger, a.branchSvc, a.validator)) + // branch/wallet // Branch Operation + a.fiber.Get("/supportedOperation", handlers.GetAllSupportedOperations(a.logger, a.branchSvc, a.validator)) a.fiber.Post("/supportedOperation", handlers.CreateSupportedOperation(a.logger, a.branchSvc, a.validator)) a.fiber.Post("/operation", handlers.CreateBranchOperation(a.logger, a.branchSvc, a.validator)) a.fiber.Get("/branch/:id/operation", handlers.GetBranchOperations(a.logger, a.branchSvc, a.validator)) @@ -81,13 +84,13 @@ func (a *App) initAppRoutes() { // Transfer // /transfer/wallet - transfer from one wallet to another wallet - a.fiber.Post("/transfer/wallet", handlers.TransferToWallet(a.logger, a.walletSvc, a.branchSvc, a.validator)) + a.fiber.Post("/transfer/wallet", a.authMiddleware, handlers.TransferToWallet(a.logger, a.walletSvc, a.branchSvc, a.validator)) // Transactions - a.fiber.Post("/transaction", handlers.CreateTransaction(a.logger, a.transactionSvc, a.validator)) - a.fiber.Get("/transaction", handlers.GetAllTransactions(a.logger, a.transactionSvc, a.userSvc, a.validator)) - a.fiber.Get("/transaction/:id", handlers.GetTransactionByID(a.logger, a.transactionSvc, a.validator)) - a.fiber.Patch("/transaction/:id", handlers.UpdateTransactionVerified(a.logger, a.transactionSvc, a.validator)) + a.fiber.Post("/transaction", a.authMiddleware, handlers.CreateTransaction(a.logger, a.transactionSvc, a.validator)) + a.fiber.Get("/transaction", a.authMiddleware, handlers.GetAllTransactions(a.logger, a.transactionSvc, a.userSvc, a.validator)) + a.fiber.Get("/transaction/:id", a.authMiddleware, handlers.GetTransactionByID(a.logger, a.transactionSvc, a.validator)) + a.fiber.Patch("/transaction/:id", a.authMiddleware, handlers.UpdateTransactionVerified(a.logger, a.transactionSvc, a.validator)) a.fiber.Get("/notifications/ws/connect/:recipientID", handler.ConnectSocket) a.fiber.Post("/notifications/mark-as-read", handler.MarkNotificationAsRead) From 055d28f8700111eb0d37cbee498358def7d1202c Mon Sep 17 00:00:00 2001 From: lafetz Date: Thu, 10 Apr 2025 04:22:16 +0300 Subject: [PATCH 04/30] start working on admin and cashier --- internal/domain/user.go | 11 +++ internal/repository/user.go | 6 +- internal/services/authentication/impl.go | 14 ++-- internal/services/user/direct.go | 49 ++++++++++++ internal/services/user/port.go | 3 +- internal/web_server/handlers/auth_handler.go | 2 + internal/web_server/handlers/cashier.go | 80 ++++++++++++++++++++ 7 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 internal/services/user/direct.go create mode 100644 internal/web_server/handlers/cashier.go diff --git a/internal/domain/user.go b/internal/domain/user.go index b4d2fa4..7151eb9 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -26,6 +26,8 @@ type User struct { // SuspendedAt time.Time Suspended bool + // + BranchID int64 } type RegisterUserReq struct { FirstName string @@ -39,6 +41,15 @@ type RegisterUserReq struct { // OtpMedium OtpMedium } +type CreateUserReq struct { + BranchID int64 + FirstName string + LastName string + Email string + PhoneNumber string + Password string + Role string +} type ResetPasswordReq struct { Email string PhoneNumber string diff --git a/internal/repository/user.go b/internal/repository/user.go index 98814f0..c3aeacd 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -8,6 +8,7 @@ import ( dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/jackc/pgx/v5/pgtype" ) @@ -82,7 +83,7 @@ func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) Suspended: user.Suspended, }, nil } -func (s *Store) GetAllUsers(ctx context.Context) ([]domain.User, error) { +func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.User, error) { users, err := s.queries.GetAllUsers(ctx) if err != nil { return nil, err @@ -233,3 +234,6 @@ func (s *Store) UpdatePassword(ctx context.Context, identifier string, password } return nil } +func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.CreateUserReq) (domain.User, error) { + return domain.User{}, nil +} diff --git a/internal/services/authentication/impl.go b/internal/services/authentication/impl.go index ea8de4d..58eb044 100644 --- a/internal/services/authentication/impl.go +++ b/internal/services/authentication/impl.go @@ -19,9 +19,10 @@ var ( ) type LoginSuccess struct { - UserId int64 - Role domain.Role - RfToken string + UserId int64 + Role domain.Role + RfToken string + BranchId int64 } func (s *Service) Login(ctx context.Context, email, phone string, password string) (LoginSuccess, error) { @@ -48,9 +49,10 @@ func (s *Service) Login(ctx context.Context, email, phone string, password strin return LoginSuccess{}, err } return LoginSuccess{ - UserId: user.ID, - Role: user.Role, - RfToken: refreshToken, + UserId: user.ID, + Role: user.Role, + RfToken: refreshToken, + BranchId: user.BranchID, }, nil } diff --git a/internal/services/user/direct.go b/internal/services/user/direct.go new file mode 100644 index 0000000..a0f61b9 --- /dev/null +++ b/internal/services/user/direct.go @@ -0,0 +1,49 @@ +package user + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +func (s *Service) CreateUser(ctx context.Context, User domain.CreateUserReq, createrUserId int64, branchId int64) (domain.User, error) { + // Create User + creator, err := s.userStore.GetUserByID(ctx, createrUserId) + if err != nil { + return domain.User{}, err + } + if creator.Role != domain.RoleAdmin { + User.BranchID = creator.BranchID + User.Role = string(domain.RoleCashier) + } else { + User.BranchID = branchId + User.Role = string(domain.RoleBranchManager) + } + + return s.userStore.CreateUserWithoutOtp(ctx, User) +} + +func (s *Service) DeleteUser(ctx context.Context, id int64) error { + // Delete User + return s.userStore.DeleteUser(ctx, id) +} + +type Filter struct { + Role string + BranchId int64 + Page int + PageSize int +} +type ValidRole struct { + Value domain.Role + Valid bool +} +type ValidBranchId struct { + Value int64 + Valid bool +} + +func (s *Service) GetAllUsers(ctx context.Context, filter Filter) ([]domain.User, error) { + // Get all Users + return s.userStore.GetAllUsers(ctx, filter) +} diff --git a/internal/services/user/port.go b/internal/services/user/port.go index bcfdd5a..7d4c5e3 100644 --- a/internal/services/user/port.go +++ b/internal/services/user/port.go @@ -8,8 +8,9 @@ import ( type UserStore interface { CreateUser(ctx context.Context, user domain.User, usedOtpId int64) (domain.User, error) + CreateUserWithoutOtp(ctx context.Context, user domain.CreateUserReq) (domain.User, error) GetUserByID(ctx context.Context, id int64) (domain.User, error) - GetAllUsers(ctx context.Context) ([]domain.User, error) + GetAllUsers(ctx context.Context, filter Filter) ([]domain.User, error) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error DeleteUser(ctx context.Context, id int64) error CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index 0022827..8731159 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -20,6 +20,7 @@ type loginCustomerReq struct { type loginCustomerRes struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` + Role string `json:"role"` } // LoginCustomer godoc @@ -69,6 +70,7 @@ func LoginCustomer( res := loginCustomerRes{ AccessToken: accessToken, RefreshToken: successRes.RfToken, + Role: string(successRes.Role), } return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil) } diff --git a/internal/web_server/handlers/cashier.go b/internal/web_server/handlers/cashier.go new file mode 100644 index 0000000..f63b40e --- /dev/null +++ b/internal/web_server/handlers/cashier.go @@ -0,0 +1,80 @@ +package handlers + +import ( + "log/slog" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" + "github.com/gofiber/fiber/v2" +) + +type CreateCashierReq struct { + FirstName string `json:"first_name" example:"John"` + LastName string `json:"last_name" example:"Doe"` + Email string `json:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" example:"1234567890"` + Password string `json:"password" example:"password123"` +} + +func CreateCashier(logger *slog.Logger, userSvc *user.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + userId := c.Locals("user_id").(int64) + + var req CreateCashierReq + if err := c.BodyParser(&req); err != nil { + logger.Error("RegisterUser failed", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request", + }) + } + valErrs, ok := validator.Validate(c, req) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + user := domain.CreateUserReq{ + FirstName: req.FirstName, + LastName: req.LastName, + Email: req.Email, + PhoneNumber: req.PhoneNumber, + Password: req.Password, + Role: string(domain.RoleCashier), + } + _, err := userSvc.CreateUser(c.Context(), user, userId, 0) + if err != nil { + logger.Error("CreateCashier failed", "error", err) + response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create cashier", nil, nil) + return nil + } + response.WriteJSON(c, fiber.StatusOK, "Cashier created successfully", nil, nil) + return nil + } + +} +func GetAllCashiers(logger *slog.Logger, userSvc *user.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + + filter := user.Filter{ + Role: string(domain.RoleCashier), + BranchId: int64(c.QueryInt("branch_id")), + Page: c.QueryInt("page", 1), + PageSize: c.QueryInt("page_size", 10), + } + valErrs, ok := validator.Validate(c, filter) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + cashiers, err := userSvc.GetAllUsers(c.Context(), filter) + if err != nil { + logger.Error("GetAllCashiers failed", "error", err) + response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil) + return nil + } + response.WriteJSON(c, fiber.StatusOK, "Cashiers retrieved successfully", cashiers, nil) + return nil + } + +} From 857c5c069696aa944b11ada2b21d3f80ad6fc843 Mon Sep 17 00:00:00 2001 From: lafetz Date: Thu, 10 Apr 2025 14:59:31 +0300 Subject: [PATCH 05/30] add manager and cashier --- docs/docs.go | 388 +++++++++++++++++++ docs/swagger.json | 388 +++++++++++++++++++ docs/swagger.yaml | 259 +++++++++++++ internal/domain/user.go | 1 + internal/services/authentication/impl.go | 33 +- internal/services/user/direct.go | 30 +- internal/web_server/handlers/auth_handler.go | 12 +- internal/web_server/handlers/cashier.go | 108 +++++- internal/web_server/handlers/manager.go | 170 ++++++++ internal/web_server/jwt/jwt.go | 18 +- internal/web_server/middleware.go | 1 + internal/web_server/routes.go | 10 + 12 files changed, 1373 insertions(+), 45 deletions(-) create mode 100644 internal/web_server/handlers/manager.go diff --git a/docs/docs.go b/docs/docs.go index cd7719d..28a7e62 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -716,6 +716,163 @@ const docTemplate = `{ } } }, + "/cashiers": { + "get": { + "description": "Get all cashiers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "cashier" + ], + "summary": "Get all cashiers", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + } + ], + "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" + } + } + } + }, + "post": { + "description": "Create cashier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "cashier" + ], + "summary": "Create cashier", + "parameters": [ + { + "description": "Create cashier", + "name": "cashier", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateCashierReq" + } + } + ], + "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" + } + } + } + } + }, + "/cashiers/{id}": { + "put": { + "description": "Update cashier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "cashier" + ], + "summary": "Update cashier", + "parameters": [ + { + "description": "Update cashier", + "name": "cashier", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateUserReq" + } + } + ], + "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" + } + } + } + } + }, "/company/{id}/branch": { "get": { "description": "Gets branches by company id", @@ -810,6 +967,163 @@ const docTemplate = `{ } } }, + "/managers": { + "get": { + "description": "Get all Managers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "manager" + ], + "summary": "Get all Managers", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + } + ], + "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" + } + } + } + }, + "post": { + "description": "Create Managers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "manager" + ], + "summary": "Create Managers", + "parameters": [ + { + "description": "Create manager", + "name": "manger", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateManagerReq" + } + } + ], + "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" + } + } + } + } + }, + "/managers/{id}": { + "put": { + "description": "Update Managers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Managers" + ], + "summary": "Update Managers", + "parameters": [ + { + "description": "Update Managers", + "name": "Managers", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateUserReq" + } + } + ], + "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" + } + } + } + } + }, "/operation": { "post": { "description": "Creates a operation", @@ -2074,6 +2388,60 @@ const docTemplate = `{ } } }, + "handlers.CreateCashierReq": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "john.doe@example.com" + }, + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "password": { + "type": "string", + "example": "password123" + }, + "phone_number": { + "type": "string", + "example": "1234567890" + } + } + }, + "handlers.CreateManagerReq": { + "type": "object", + "properties": { + "branch_id": { + "type": "integer", + "example": 1 + }, + "email": { + "type": "string", + "example": "john.doe@example.com" + }, + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "password": { + "type": "string", + "example": "password123" + }, + "phone_number": { + "type": "string", + "example": "1234567890" + } + } + }, "handlers.CreateSupportedOperationReq": { "type": "object", "properties": { @@ -2586,6 +2954,9 @@ const docTemplate = `{ }, "refresh_token": { "type": "string" + }, + "role": { + "type": "string" } } }, @@ -2608,6 +2979,23 @@ const docTemplate = `{ } } }, + "handlers.updateUserReq": { + "type": "object", + "properties": { + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "suspended": { + "type": "boolean", + "example": false + } + } + }, "response.APIResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 7ecbb4f..cd6c6bd 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -708,6 +708,163 @@ } } }, + "/cashiers": { + "get": { + "description": "Get all cashiers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "cashier" + ], + "summary": "Get all cashiers", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + } + ], + "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" + } + } + } + }, + "post": { + "description": "Create cashier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "cashier" + ], + "summary": "Create cashier", + "parameters": [ + { + "description": "Create cashier", + "name": "cashier", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateCashierReq" + } + } + ], + "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" + } + } + } + } + }, + "/cashiers/{id}": { + "put": { + "description": "Update cashier", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "cashier" + ], + "summary": "Update cashier", + "parameters": [ + { + "description": "Update cashier", + "name": "cashier", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateUserReq" + } + } + ], + "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" + } + } + } + } + }, "/company/{id}/branch": { "get": { "description": "Gets branches by company id", @@ -802,6 +959,163 @@ } } }, + "/managers": { + "get": { + "description": "Get all Managers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "manager" + ], + "summary": "Get all Managers", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + } + ], + "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" + } + } + } + }, + "post": { + "description": "Create Managers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "manager" + ], + "summary": "Create Managers", + "parameters": [ + { + "description": "Create manager", + "name": "manger", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateManagerReq" + } + } + ], + "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" + } + } + } + } + }, + "/managers/{id}": { + "put": { + "description": "Update Managers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Managers" + ], + "summary": "Update Managers", + "parameters": [ + { + "description": "Update Managers", + "name": "Managers", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateUserReq" + } + } + ], + "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" + } + } + } + } + }, "/operation": { "post": { "description": "Creates a operation", @@ -2066,6 +2380,60 @@ } } }, + "handlers.CreateCashierReq": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "john.doe@example.com" + }, + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "password": { + "type": "string", + "example": "password123" + }, + "phone_number": { + "type": "string", + "example": "1234567890" + } + } + }, + "handlers.CreateManagerReq": { + "type": "object", + "properties": { + "branch_id": { + "type": "integer", + "example": 1 + }, + "email": { + "type": "string", + "example": "john.doe@example.com" + }, + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "password": { + "type": "string", + "example": "password123" + }, + "phone_number": { + "type": "string", + "example": "1234567890" + } + } + }, "handlers.CreateSupportedOperationReq": { "type": "object", "properties": { @@ -2578,6 +2946,9 @@ }, "refresh_token": { "type": "string" + }, + "role": { + "type": "string" } } }, @@ -2600,6 +2971,23 @@ } } }, + "handlers.updateUserReq": { + "type": "object", + "properties": { + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "suspended": { + "type": "boolean", + "example": false + } + } + }, "response.APIResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 69e4307..242eb5b 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -204,6 +204,45 @@ definitions: type: integer type: array type: object + handlers.CreateCashierReq: + properties: + email: + example: john.doe@example.com + type: string + first_name: + example: John + type: string + last_name: + example: Doe + type: string + password: + example: password123 + type: string + phone_number: + example: "1234567890" + type: string + type: object + handlers.CreateManagerReq: + properties: + branch_id: + example: 1 + type: integer + email: + example: john.doe@example.com + type: string + first_name: + example: John + type: string + last_name: + example: Doe + type: string + password: + example: password123 + type: string + phone_number: + example: "1234567890" + type: string + type: object handlers.CreateSupportedOperationReq: properties: description: @@ -559,6 +598,8 @@ definitions: type: string refresh_token: type: string + role: + type: string type: object handlers.logoutReq: properties: @@ -572,6 +613,18 @@ definitions: refresh_token: type: string type: object + handlers.updateUserReq: + properties: + first_name: + example: John + type: string + last_name: + example: Doe + type: string + suspended: + example: false + type: boolean + type: object response.APIResponse: properties: data: {} @@ -1061,6 +1114,109 @@ paths: summary: Delete the branch operation tags: - branch + /cashiers: + get: + consumes: + - application/json + description: Get all cashiers + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: page_size + type: integer + 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: Get all cashiers + tags: + - cashier + post: + consumes: + - application/json + description: Create cashier + parameters: + - description: Create cashier + in: body + name: cashier + required: true + schema: + $ref: '#/definitions/handlers.CreateCashierReq' + 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: Create cashier + tags: + - cashier + /cashiers/{id}: + put: + consumes: + - application/json + description: Update cashier + parameters: + - description: Update cashier + in: body + name: cashier + required: true + schema: + $ref: '#/definitions/handlers.updateUserReq' + 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 cashier + tags: + - cashier /company/{id}/branch: get: consumes: @@ -1123,6 +1279,109 @@ paths: summary: Gets branches by manager id tags: - branch + /managers: + get: + consumes: + - application/json + description: Get all Managers + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: page_size + type: integer + 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: Get all Managers + tags: + - manager + post: + consumes: + - application/json + description: Create Managers + parameters: + - description: Create manager + in: body + name: manger + required: true + schema: + $ref: '#/definitions/handlers.CreateManagerReq' + 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: Create Managers + tags: + - manager + /managers/{id}: + put: + consumes: + - application/json + description: Update Managers + parameters: + - description: Update Managers + in: body + name: Managers + required: true + schema: + $ref: '#/definitions/handlers.updateUserReq' + 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 Managers + tags: + - Managers /operation: post: consumes: diff --git a/internal/domain/user.go b/internal/domain/user.go index 7151eb9..23004f4 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -58,6 +58,7 @@ type ResetPasswordReq struct { OtpMedium OtpMedium } type UpdateUserReq struct { + UserId int64 FirstName ValidString LastName ValidString Suspended ValidBool diff --git a/internal/services/authentication/impl.go b/internal/services/authentication/impl.go index 58eb044..2d6bb0b 100644 --- a/internal/services/authentication/impl.go +++ b/internal/services/authentication/impl.go @@ -56,34 +56,31 @@ func (s *Service) Login(ctx context.Context, email, phone string, password strin }, nil } -func (s *Service) RefreshToken(ctx context.Context, refToken string) (string, error) { +func (s *Service) RefreshToken(ctx context.Context, refToken string) error { token, err := s.tokenStore.GetRefreshToken(ctx, refToken) if err != nil { - return "", err + return err } if token.Revoked { - return "", ErrRefreshTokenNotFound + return ErrRefreshTokenNotFound } if token.ExpiresAt.Before(time.Now()) { - return "", ErrExpiredToken + return ErrExpiredToken } - newRefToken, err := generateRefreshToken() - if err != nil { - return "", err - } + // newRefToken, err := generateRefreshToken() + // if err != nil { + // return "", err + // } - err = s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{ - Token: newRefToken, - UserID: token.UserID, - CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second), - }) - if err != nil { - return "", err - } - return newRefToken, nil + // err = s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{ + // Token: newRefToken, + // UserID: token.UserID, + // CreatedAt: time.Now(), + // ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second), + // }) + return nil } func (s *Service) Logout(ctx context.Context, refToken string) error { token, err := s.tokenStore.GetRefreshToken(ctx, refToken) diff --git a/internal/services/user/direct.go b/internal/services/user/direct.go index a0f61b9..b5fca74 100644 --- a/internal/services/user/direct.go +++ b/internal/services/user/direct.go @@ -6,19 +6,19 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) -func (s *Service) CreateUser(ctx context.Context, User domain.CreateUserReq, createrUserId int64, branchId int64) (domain.User, error) { +func (s *Service) CreateUser(ctx context.Context, User domain.CreateUserReq) (domain.User, error) { // Create User - creator, err := s.userStore.GetUserByID(ctx, createrUserId) - if err != nil { - return domain.User{}, err - } - if creator.Role != domain.RoleAdmin { - User.BranchID = creator.BranchID - User.Role = string(domain.RoleCashier) - } else { - User.BranchID = branchId - User.Role = string(domain.RoleBranchManager) - } + // creator, err := s.userStore.GetUserByID(ctx, createrUserId) + // if err != nil { + // return domain.User{}, err + // } + // if creator.Role != domain.RoleAdmin { + // User.BranchID = creator.BranchID + // User.Role = string(domain.RoleCashier) + // } else { + // User.BranchID = branchId + // User.Role = string(domain.RoleBranchManager) + // } return s.userStore.CreateUserWithoutOtp(ctx, User) } @@ -30,7 +30,7 @@ func (s *Service) DeleteUser(ctx context.Context, id int64) error { type Filter struct { Role string - BranchId int64 + BranchId ValidBranchId Page int PageSize int } @@ -47,3 +47,7 @@ func (s *Service) GetAllUsers(ctx context.Context, filter Filter) ([]domain.User // Get all Users return s.userStore.GetAllUsers(ctx, filter) } +func (s *Service) GetUserById(ctx context.Context, id int64) (domain.User, error) { + + return s.userStore.GetUserByID(ctx, id) +} diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index 8731159..3434ea2 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -4,6 +4,7 @@ import ( "errors" "log/slog" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" @@ -66,7 +67,7 @@ func LoginCustomer( return nil } - accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry) + accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.BranchId, JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry) res := loginCustomerRes{ AccessToken: accessToken, RefreshToken: successRes.RfToken, @@ -105,7 +106,10 @@ func RefreshToken(logger *slog.Logger, authSvc *authentication.Service, response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return nil } - rf, err := authSvc.RefreshToken(c.Context(), req.RefreshToken) + userId := c.Locals("user_id").(int64) + role := c.Locals("role").(string) + branchId := c.Locals("branch_id").(int64) + err := authSvc.RefreshToken(c.Context(), req.RefreshToken) if err != nil { logger.Info("Refresh token failed", "error", err) if errors.Is(err, authentication.ErrExpiredToken) { @@ -120,7 +124,7 @@ func RefreshToken(logger *slog.Logger, authSvc *authentication.Service, response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil) return nil } - accessToken, err := jwtutil.CreateJwt(0, "", JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry) + accessToken, err := jwtutil.CreateJwt(userId, domain.Role(role), branchId, JwtConfig.JwtAccessKey, JwtConfig.JwtAccessExpiry) if err != nil { logger.Error("Create jwt failed", "error", err) response.WriteJSON(c, fiber.StatusInternalServerError, "Internal server error", nil, nil) @@ -129,7 +133,7 @@ func RefreshToken(logger *slog.Logger, authSvc *authentication.Service, res := loginCustomerRes{ AccessToken: accessToken, - RefreshToken: rf, + RefreshToken: req.RefreshToken, } return response.WriteJSON(c, fiber.StatusOK, "refresh successful", res, nil) } diff --git a/internal/web_server/handlers/cashier.go b/internal/web_server/handlers/cashier.go index f63b40e..6ad9874 100644 --- a/internal/web_server/handlers/cashier.go +++ b/internal/web_server/handlers/cashier.go @@ -2,6 +2,7 @@ package handlers import ( "log/slog" + "strconv" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -18,10 +19,22 @@ type CreateCashierReq struct { Password string `json:"password" example:"password123"` } +// CreateCashier godoc +// @Summary Create cashier +// @Description Create cashier +// @Tags cashier +// @Accept json +// @Produce json +// @Param cashier body CreateCashierReq true "Create cashier" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /cashiers [post] func CreateCashier(logger *slog.Logger, userSvc *user.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { - userId := c.Locals("user_id").(int64) + creatorBranch := c.Locals("branch_id").(int64) var req CreateCashierReq if err := c.BodyParser(&req); err != nil { logger.Error("RegisterUser failed", "error", err) @@ -41,8 +54,9 @@ func CreateCashier(logger *slog.Logger, userSvc *user.Service, validator *custom PhoneNumber: req.PhoneNumber, Password: req.Password, Role: string(domain.RoleCashier), + BranchID: creatorBranch, } - _, err := userSvc.CreateUser(c.Context(), user, userId, 0) + _, err := userSvc.CreateUser(c.Context(), user) if err != nil { logger.Error("CreateCashier failed", "error", err) response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create cashier", nil, nil) @@ -53,12 +67,29 @@ func CreateCashier(logger *slog.Logger, userSvc *user.Service, validator *custom } } + +// GetAllCashiers godoc +// @Summary Get all cashiers +// @Description Get all cashiers +// @Tags cashier +// @Accept json +// @Produce json +// @Param page query int false "Page number" +// @Param page_size query int false "Page size" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /cashiers [get] func GetAllCashiers(logger *slog.Logger, userSvc *user.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { - + branchId := int64(12) //c.Locals("branch_id").(int64) filter := user.Filter{ - Role: string(domain.RoleCashier), - BranchId: int64(c.QueryInt("branch_id")), + Role: string(domain.RoleCashier), + BranchId: user.ValidBranchId{ + Value: branchId, + Valid: true, + }, Page: c.QueryInt("page", 1), PageSize: c.QueryInt("page_size", 10), } @@ -78,3 +109,70 @@ func GetAllCashiers(logger *slog.Logger, userSvc *user.Service, validator *custo } } + +type updateUserReq struct { + FirstName string `json:"first_name" example:"John"` + LastName string `json:"last_name" example:"Doe"` + Suspended bool `json:"suspended" example:"false"` +} + +// UpdateCashier godoc +// @Summary Update cashier +// @Description Update cashier +// @Tags cashier +// @Accept json +// @Produce json +// @Param cashier body updateUserReq true "Update cashier" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /cashiers/{id} [put] +func UpdateCashier(logger *slog.Logger, userSvc *user.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + var req updateUserReq + if err := c.BodyParser(&req); err != nil { + logger.Error("UpdateCashier failed", "error", err) + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) + return nil + } + + valErrs, ok := validator.Validate(c, req) + + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + cashierIdStr := c.Params("id") + cashierId, err := strconv.ParseInt(cashierIdStr, 10, 64) + if err != nil { + logger.Error("UpdateCashier failed", "error", err) + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashier ID", nil, nil) + return nil + } + err = userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{ + UserId: cashierId, + 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, + }, + }, + ) + if err != nil { + logger.Error("UpdateCashier failed", "error", err) + response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update cashier", nil, nil) + return nil + } + response.WriteJSON(c, fiber.StatusOK, "Cashier updated successfully", nil, nil) + return nil + } + +} diff --git a/internal/web_server/handlers/manager.go b/internal/web_server/handlers/manager.go new file mode 100644 index 0000000..d0ba39a --- /dev/null +++ b/internal/web_server/handlers/manager.go @@ -0,0 +1,170 @@ +package handlers + +import ( + "log/slog" + "strconv" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" + "github.com/gofiber/fiber/v2" +) + +type CreateManagerReq struct { + FirstName string `json:"first_name" example:"John"` + LastName string `json:"last_name" example:"Doe"` + Email string `json:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" example:"1234567890"` + Password string `json:"password" example:"password123"` + BranchId int64 `json:"branch_id" example:"1"` +} + +// CreateManagers godoc +// @Summary Create Managers +// @Description Create Managers +// @Tags manager +// @Accept json +// @Produce json +// @Param manger body CreateManagerReq true "Create manager" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /managers [post] +func CreateManager(logger *slog.Logger, userSvc *user.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + var req CreateManagerReq + if err := c.BodyParser(&req); err != nil { + logger.Error("RegisterUser failed", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request", + }) + } + valErrs, ok := validator.Validate(c, req) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + user := domain.CreateUserReq{ + FirstName: req.FirstName, + LastName: req.LastName, + Email: req.Email, + PhoneNumber: req.PhoneNumber, + Password: req.Password, + Role: string(domain.RoleBranchManager), + BranchID: req.BranchId, + } + _, err := userSvc.CreateUser(c.Context(), user) + if err != nil { + logger.Error("CreateManagers failed", "error", err) + response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create Managers", nil, nil) + return nil + } + response.WriteJSON(c, fiber.StatusOK, "Managers created successfully", nil, nil) + return nil + } + +} + +// GetAllManagers godoc +// @Summary Get all Managers +// @Description Get all Managers +// @Tags manager +// @Accept json +// @Produce json +// @Param page query int false "Page number" +// @Param page_size query int false "Page size" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /managers [get] +func GetAllManagers(logger *slog.Logger, userSvc *user.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + filter := user.Filter{ + Role: string(domain.RoleBranchManager), + BranchId: user.ValidBranchId{ + Value: int64(c.QueryInt("branch_id")), + Valid: true, + }, + Page: c.QueryInt("page", 1), + PageSize: c.QueryInt("page_size", 10), + } + valErrs, ok := validator.Validate(c, filter) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + Managers, err := userSvc.GetAllUsers(c.Context(), filter) + if err != nil { + logger.Error("GetAllManagers failed", "error", err) + response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get Managers", nil, nil) + return nil + } + response.WriteJSON(c, fiber.StatusOK, "Managers retrieved successfully", Managers, nil) + return nil + } + +} + +// UpdateManagers godoc +// @Summary Update Managers +// @Description Update Managers +// @Tags Managers +// @Accept json +// @Produce json +// @Param Managers body updateUserReq true "Update Managers" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /managers/{id} [put] +func UPdateManagers(logger *slog.Logger, userSvc *user.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + var req updateUserReq + if err := c.BodyParser(&req); err != nil { + logger.Error("UpdateManagers failed", "error", err) + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) + return nil + } + + valErrs, ok := validator.Validate(c, req) + + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + ManagersIdStr := c.Params("id") + ManagersId, err := strconv.ParseInt(ManagersIdStr, 10, 64) + if err != nil { + logger.Error("UpdateManagers failed", "error", err) + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid Managers ID", nil, nil) + return nil + } + err = userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{ + UserId: ManagersId, + 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, + }, + }, + ) + if err != nil { + logger.Error("UpdateManagers failed", "error", err) + response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update Managers", nil, nil) + return nil + } + response.WriteJSON(c, fiber.StatusOK, "Managers updated successfully", nil, nil) + return nil + } + +} diff --git a/internal/web_server/jwt/jwt.go b/internal/web_server/jwt/jwt.go index 530eb12..e53a274 100644 --- a/internal/web_server/jwt/jwt.go +++ b/internal/web_server/jwt/jwt.go @@ -18,24 +18,32 @@ var ( ErrRefreshTokenNotFound = errors.New("refresh token not found") // i.e login again ) +type User struct { + UserId int64 + Role domain.Role + BranchId int64 + RefreshToken string +} type UserClaim struct { jwt.RegisteredClaims - UserId int64 - Role domain.Role + UserId int64 + Role domain.Role + BranchId int64 } type JwtConfig struct { JwtAccessKey string JwtAccessExpiry int } -func CreateJwt(userId int64, Role domain.Role, key string, expiry int) (string, error) { +func CreateJwt(userId int64, Role domain.Role, BranchId int64, key string, expiry int) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaim{RegisteredClaims: jwt.RegisteredClaims{Issuer: "github.com/lafetz/snippitstash", IssuedAt: jwt.NewNumericDate(time.Now()), Audience: jwt.ClaimStrings{"fortune.com"}, NotBefore: jwt.NewNumericDate(time.Now()), ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expiry) * time.Second))}, - UserId: userId, - Role: Role, + UserId: userId, + Role: Role, + BranchId: BranchId, }) jwtToken, err := token.SignedString([]byte(key)) // return jwtToken, err diff --git a/internal/web_server/middleware.go b/internal/web_server/middleware.go index 46e248b..dc25a1e 100644 --- a/internal/web_server/middleware.go +++ b/internal/web_server/middleware.go @@ -39,6 +39,7 @@ func (a *App) authMiddleware(c *fiber.Ctx) error { } c.Locals("user_id", claim.UserId) c.Locals("role", claim.Role) + c.Locals("branch_id", claim.BranchId) c.Locals("refresh_token", refreshToken) if claim.Role != domain.RoleCustomer { diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index b25ed9d..7e3fd08 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -39,7 +39,17 @@ func (a *App) initAppRoutes() { a.fiber.Post("/user/sendRegisterCode", handlers.SendRegisterCode(a.logger, a.userSvc, a.validator)) a.fiber.Post("/user/checkPhoneEmailExist", handlers.CheckPhoneEmailExist(a.logger, a.userSvc, a.validator)) a.fiber.Get("/user/profile", a.authMiddleware, handlers.UserProfile(a.logger, a.userSvc)) + // + //, a.authMiddleware + a.fiber.Get("/cashiers", handlers.GetAllCashiers(a.logger, a.userSvc, a.validator)) + a.fiber.Post("/cashiers", handlers.CreateCashier(a.logger, a.userSvc, a.validator)) + a.fiber.Put("/cashiers/:id", handlers.UpdateCashier(a.logger, a.userSvc, a.validator)) + // + a.fiber.Get("/managers", handlers.GetAllManagers(a.logger, a.userSvc, a.validator)) + a.fiber.Post("/managers", handlers.CreateManager(a.logger, a.userSvc, a.validator)) + a.fiber.Put("/managers/:id", handlers.UPdateManagers(a.logger, a.userSvc, a.validator)) + // a.fiber.Get("/user/wallet", a.authMiddleware, handlers.GetCustomerWallet(a.logger, a.walletSvc, a.validator)) a.fiber.Post("/user/search", handlers.SearchUserByNameOrPhone(a.logger, a.userSvc, a.validator)) From 2163053d97ddb6c163544a1dc57819a02a7062ec Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Thu, 10 Apr 2025 16:06:05 +0300 Subject: [PATCH 06/30] some changes --- db/migrations/000001_fortune.up.sql | 6 ++++-- db/query/bet.sql | 4 ++-- db/query/transactions.sql | 2 +- docs/docs.go | 8 ++++++++ docs/swagger.json | 8 ++++++++ docs/swagger.yaml | 6 ++++++ gen/db/bet.sql.go | 17 +++++++++++------ gen/db/models.go | 4 +++- gen/db/transactions.sql.go | 14 ++++++++++---- internal/domain/bet.go | 1 + internal/domain/transaction.go | 10 ++++++++++ internal/repository/bet.go | 9 +++++---- internal/repository/transaction.go | 2 ++ internal/web_server/handlers/bet_handler.go | 5 ++++- internal/web_server/handlers/branch_handler.go | 15 ++++++++++++++- .../web_server/handlers/transaction_handler.go | 16 ++++++++++------ internal/web_server/middleware.go | 5 +++++ 17 files changed, 104 insertions(+), 28 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 91e589f..ddfe56a 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -46,14 +46,15 @@ CREATE TABLE IF NOT EXISTS bets ( phone_number VARCHAR(255) NOT NULL, branch_id BIGINT, user_id BIGINT, - cashed_out BOOLEAN DEFAULT FALSE, + cashed_out BOOLEAN DEFAULT FALSE NOT NULL, + cashout_id VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, is_shop_bet BOOLEAN NOT NULL, CHECK (user_id IS NOT NULL OR branch_id IS NOT NULL) ); -CREATE TABLE IF NOT EXISTS tickets ( +CREATE TABLE IF NOT EXISTS `tickets` ( id BIGSERIAL PRIMARY KEY, amount BIGINT NULL, total_odds REAL NOT NULL, @@ -118,6 +119,7 @@ CREATE TABLE IF NOT EXISTS transactions ( branch_id BIGINT NOT NULL, cashier_id BIGINT NOT NULL, bet_id BIGINT NOT NULL, + type BIGINT NOT NULL, payment_option BIGINT NOT NULL, full_name VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, diff --git a/db/query/bet.sql b/db/query/bet.sql index bf0d466..2d1d098 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -1,6 +1,6 @@ -- name: CreateBet :one -INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id, is_shop_bet) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id, is_shop_bet, cashout_id) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *; -- name: GetAllBets :many diff --git a/db/query/transactions.sql b/db/query/transactions.sql index 75adba4..a5d21b0 100644 --- a/db/query/transactions.sql +++ b/db/query/transactions.sql @@ -1,5 +1,5 @@ -- name: CreateTransaction :one -INSERT INTO transactions (amount, branch_id, cashier_id, bet_id, 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) 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) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *; -- name: GetAllTransactions :many SELECT * FROM transactions; diff --git a/docs/docs.go b/docs/docs.go index cd7719d..f36102b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2165,6 +2165,10 @@ const docTemplate = `{ }, "reference_number": { "type": "string" + }, + "type": { + "type": "integer", + "example": 1 } } }, @@ -2406,6 +2410,10 @@ const docTemplate = `{ "reference_number": { "type": "string" }, + "type": { + "type": "integer", + "example": 1 + }, "verified": { "type": "boolean", "example": true diff --git a/docs/swagger.json b/docs/swagger.json index 7ecbb4f..bad0c85 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2157,6 +2157,10 @@ }, "reference_number": { "type": "string" + }, + "type": { + "type": "integer", + "example": 1 } } }, @@ -2398,6 +2402,10 @@ "reference_number": { "type": "string" }, + "type": { + "type": "integer", + "example": 1 + }, "verified": { "type": "boolean", "example": true diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 69e4307..8b92636 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -267,6 +267,9 @@ definitions: type: string reference_number: type: string + type: + example: 1 + type: integer type: object handlers.CreateTransferReq: properties: @@ -434,6 +437,9 @@ definitions: type: string reference_number: type: string + type: + example: 1 + type: integer verified: example: true type: boolean diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index f3667c6..79b5cf3 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -12,9 +12,9 @@ import ( ) const CreateBet = `-- name: CreateBet :one -INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id, is_shop_bet) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8) -RETURNING id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, created_at, updated_at, is_shop_bet +INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id, is_shop_bet, cashout_id) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) +RETURNING id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet ` type CreateBetParams struct { @@ -26,6 +26,7 @@ type CreateBetParams struct { BranchID pgtype.Int8 UserID pgtype.Int8 IsShopBet bool + CashoutID string } func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, error) { @@ -38,6 +39,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro arg.BranchID, arg.UserID, arg.IsShopBet, + arg.CashoutID, ) var i Bet err := row.Scan( @@ -50,6 +52,7 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro &i.BranchID, &i.UserID, &i.CashedOut, + &i.CashoutID, &i.CreatedAt, &i.UpdatedAt, &i.IsShopBet, @@ -67,7 +70,7 @@ func (q *Queries) DeleteBet(ctx context.Context, id int64) error { } const GetAllBets = `-- name: GetAllBets :many -SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, created_at, updated_at, is_shop_bet FROM bets +SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet FROM bets ` func (q *Queries) GetAllBets(ctx context.Context) ([]Bet, error) { @@ -89,6 +92,7 @@ func (q *Queries) GetAllBets(ctx context.Context) ([]Bet, error) { &i.BranchID, &i.UserID, &i.CashedOut, + &i.CashoutID, &i.CreatedAt, &i.UpdatedAt, &i.IsShopBet, @@ -104,7 +108,7 @@ func (q *Queries) GetAllBets(ctx context.Context) ([]Bet, error) { } const GetBetByID = `-- name: GetBetByID :one -SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, created_at, updated_at, is_shop_bet FROM bets WHERE id = $1 +SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet FROM bets WHERE id = $1 ` func (q *Queries) GetBetByID(ctx context.Context, id int64) (Bet, error) { @@ -120,6 +124,7 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (Bet, error) { &i.BranchID, &i.UserID, &i.CashedOut, + &i.CashoutID, &i.CreatedAt, &i.UpdatedAt, &i.IsShopBet, @@ -133,7 +138,7 @@ UPDATE bets SET cashed_out = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1 type UpdateCashOutParams struct { ID int64 - CashedOut pgtype.Bool + CashedOut bool } func (q *Queries) UpdateCashOut(ctx context.Context, arg UpdateCashOutParams) error { diff --git a/gen/db/models.go b/gen/db/models.go index 8e449a6..e96ee34 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -17,7 +17,8 @@ type Bet struct { PhoneNumber string BranchID pgtype.Int8 UserID pgtype.Int8 - CashedOut pgtype.Bool + CashedOut bool + CashoutID string CreatedAt pgtype.Timestamp UpdatedAt pgtype.Timestamp IsShopBet bool @@ -125,6 +126,7 @@ type Transaction struct { BranchID int64 CashierID int64 BetID int64 + Type int64 PaymentOption int64 FullName string PhoneNumber string diff --git a/gen/db/transactions.sql.go b/gen/db/transactions.sql.go index 4f5fbe1..8f15071 100644 --- a/gen/db/transactions.sql.go +++ b/gen/db/transactions.sql.go @@ -10,7 +10,7 @@ import ( ) const CreateTransaction = `-- name: CreateTransaction :one -INSERT INTO transactions (amount, branch_id, cashier_id, bet_id, 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) RETURNING id, amount, branch_id, cashier_id, bet_id, 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) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, amount, branch_id, cashier_id, bet_id, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at ` type CreateTransactionParams struct { @@ -18,6 +18,7 @@ type CreateTransactionParams struct { BranchID int64 CashierID int64 BetID int64 + Type int64 PaymentOption int64 FullName string PhoneNumber string @@ -34,6 +35,7 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa arg.BranchID, arg.CashierID, arg.BetID, + arg.Type, arg.PaymentOption, arg.FullName, arg.PhoneNumber, @@ -50,6 +52,7 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa &i.BranchID, &i.CashierID, &i.BetID, + &i.Type, &i.PaymentOption, &i.FullName, &i.PhoneNumber, @@ -66,7 +69,7 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa } const GetAllTransactions = `-- name: GetAllTransactions :many -SELECT id, amount, branch_id, cashier_id, bet_id, 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, type, payment_option, full_name, phone_number, bank_code, beneficiary_name, account_name, account_number, reference_number, verified, created_at, updated_at FROM transactions ` func (q *Queries) GetAllTransactions(ctx context.Context) ([]Transaction, error) { @@ -84,6 +87,7 @@ func (q *Queries) GetAllTransactions(ctx context.Context) ([]Transaction, error) &i.BranchID, &i.CashierID, &i.BetID, + &i.Type, &i.PaymentOption, &i.FullName, &i.PhoneNumber, @@ -107,7 +111,7 @@ func (q *Queries) GetAllTransactions(ctx context.Context) ([]Transaction, error) } const GetTransactionByBranch = `-- name: GetTransactionByBranch :many -SELECT id, amount, branch_id, cashier_id, bet_id, 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, 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 ` func (q *Queries) GetTransactionByBranch(ctx context.Context, branchID int64) ([]Transaction, error) { @@ -125,6 +129,7 @@ func (q *Queries) GetTransactionByBranch(ctx context.Context, branchID int64) ([ &i.BranchID, &i.CashierID, &i.BetID, + &i.Type, &i.PaymentOption, &i.FullName, &i.PhoneNumber, @@ -148,7 +153,7 @@ func (q *Queries) GetTransactionByBranch(ctx context.Context, branchID int64) ([ } const GetTransactionByID = `-- name: GetTransactionByID :one -SELECT id, amount, branch_id, cashier_id, bet_id, 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, 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 ` func (q *Queries) GetTransactionByID(ctx context.Context, id int64) (Transaction, error) { @@ -160,6 +165,7 @@ func (q *Queries) GetTransactionByID(ctx context.Context, id int64) (Transaction &i.BranchID, &i.CashierID, &i.BetID, + &i.Type, &i.PaymentOption, &i.FullName, &i.PhoneNumber, diff --git a/internal/domain/bet.go b/internal/domain/bet.go index e36ee62..cc8175d 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -36,6 +36,7 @@ type CreateBet struct { BranchID ValidInt64 // Can Be Nullable UserID ValidInt64 // Can Be Nullable IsShopBet bool + CashoutID string } func (b BetStatus) String() string { diff --git a/internal/domain/transaction.go b/internal/domain/transaction.go index 3ff096e..f47c34a 100644 --- a/internal/domain/transaction.go +++ b/internal/domain/transaction.go @@ -1,5 +1,12 @@ package domain +type TransactionType int + +const ( + TRANSACTION_CASHOUT TransactionType = iota + TRANSACTION_DEPOSIT +) + type PaymentOption int64 const ( @@ -8,6 +15,7 @@ const ( ARIFPAY_TRANSACTION BANK ) + // Transaction only represents when the user cashes out a bet in the shop // It probably would be better to call it a CashOut or ShopWithdrawal type Transaction struct { @@ -16,6 +24,7 @@ type Transaction struct { BranchID int64 CashierID int64 BetID int64 + Type TransactionType PaymentOption PaymentOption FullName string PhoneNumber string @@ -33,6 +42,7 @@ type CreateTransaction struct { BranchID int64 CashierID int64 BetID int64 + Type TransactionType PaymentOption PaymentOption FullName string PhoneNumber string diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 2ddef36..210fcbd 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -25,6 +25,8 @@ func convertDBBet(bet dbgen.Bet) domain.Bet { Valid: bet.UserID.Valid, }, IsShopBet: bet.IsShopBet, + CashedOut: bet.CashedOut, + CashoutID: bet.CashoutID, } } @@ -44,6 +46,7 @@ func convertCreateBet(bet domain.CreateBet) dbgen.CreateBetParams { Valid: bet.UserID.Valid, }, IsShopBet: bet.IsShopBet, + CashoutID: bet.CashoutID, } } @@ -83,10 +86,8 @@ func (s *Store) GetAllBets(ctx context.Context) ([]domain.Bet, error) { func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error { err := s.queries.UpdateCashOut(ctx, dbgen.UpdateCashOutParams{ - ID: id, - CashedOut: pgtype.Bool{ - Bool: cashedOut, - }, + ID: id, + CashedOut: cashedOut, }) return err } diff --git a/internal/repository/transaction.go b/internal/repository/transaction.go index e7f3e4f..24cf9e0 100644 --- a/internal/repository/transaction.go +++ b/internal/repository/transaction.go @@ -13,6 +13,7 @@ func convertDBTransaction(transaction dbgen.Transaction) domain.Transaction { BranchID: transaction.BranchID, CashierID: transaction.CashierID, BetID: transaction.BetID, + Type: domain.TransactionType(transaction.Type), PaymentOption: domain.PaymentOption(transaction.PaymentOption), FullName: transaction.FullName, PhoneNumber: transaction.PhoneNumber, @@ -30,6 +31,7 @@ func convertCreateTransaction(transaction domain.CreateTransaction) dbgen.Create BranchID: transaction.BranchID, CashierID: transaction.CashierID, BetID: transaction.BetID, + Type: int64(transaction.Type), PaymentOption: int64(transaction.PaymentOption), FullName: transaction.FullName, PhoneNumber: transaction.PhoneNumber, diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 5c9678f..dab2e1d 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -9,6 +9,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" "github.com/gofiber/fiber/v2" + "github.com/google/uuid" ) type CreateBetReq struct { @@ -64,7 +65,6 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalida // TODO if user is customer, get id from the token then get the wallet id from there // TODO: If user is a cashier, check the token, and find the role and get the branch id from there. Reduce amount from the branch wallet - var isShopBet bool = true var branchID int64 = 1 @@ -87,6 +87,8 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalida // TODO Validate Outcomes Here and make sure they didn't expire + cashoutUUID := uuid.New() + bet, err := betSvc.CreateBet(c.Context(), domain.CreateBet{ Outcomes: req.Outcomes, Amount: domain.Currency(req.Amount), @@ -104,6 +106,7 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalida Valid: !isShopBet, }, IsShopBet: req.IsShopBet, + CashoutID: cashoutUUID.String(), }) if err != nil { diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index 8e0552e..6a2bf16 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -151,9 +151,22 @@ func CreateBranch(logger *slog.Logger, branchSvc *branch.Service, walletSvc *wal }) } + for _, operation := range req.Operations { + err := branchSvc.CreateBranchOperation(c.Context(), domain.CreateBranchOperation{ + BranchID: branch.ID, + OperationID: operation, + }) + if err != nil { + logger.Error("Failed to create branch operations", "BranchID", branch.ID, "operation", operation, "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Internal server error", + }) + } + } + res := convertBranch(branch) - return response.WriteJSON(c, fiber.StatusOK, "Branch Created", res, nil) + return response.WriteJSON(c, fiber.StatusCreated, "Branch Created", res, nil) } diff --git a/internal/web_server/handlers/transaction_handler.go b/internal/web_server/handlers/transaction_handler.go index 77f80c9..cd441fb 100644 --- a/internal/web_server/handlers/transaction_handler.go +++ b/internal/web_server/handlers/transaction_handler.go @@ -1,6 +1,7 @@ package handlers import ( + // "fmt" "log/slog" "strconv" @@ -13,12 +14,12 @@ import ( ) type TransactionRes struct { - ID int64 `json:"id" example:"1"` - Amount float32 `json:"amount" example:"100.0"` - BranchID int64 `json:"branch_id" example:"1"` - CashierID int64 `json:"cashier_id" example:"1"` - BetID int64 `json:"bet_id" example:"1"` - + ID int64 `json:"id" example:"1"` + Amount float32 `json:"amount" example:"100.0"` + BranchID int64 `json:"branch_id" example:"1"` + CashierID int64 `json:"cashier_id" example:"1"` + BetID int64 `json:"bet_id" example:"1"` + Type int64 `json:"type" example:"1"` PaymentOption domain.PaymentOption `json:"payment_option" example:"1"` FullName string `json:"full_name" example:"John Smith"` PhoneNumber string `json:"phone_number" example:"0911111111"` @@ -35,6 +36,7 @@ type CreateTransactionReq struct { BranchID int64 `json:"branch_id" example:"1"` CashierID int64 `json:"cashier_id" example:"1"` BetID int64 `json:"bet_id" example:"1"` + Type int64 `json:"type" example:"1"` PaymentOption domain.PaymentOption `json:"payment_option" example:"1"` FullName string `json:"full_name" example:"John Smith"` PhoneNumber string `json:"phone_number" example:"0911111111"` @@ -53,6 +55,7 @@ func convertTransaction(transaction domain.Transaction) TransactionRes { BranchID: transaction.BranchID, CashierID: transaction.CashierID, BetID: transaction.BetID, + Type: int64(transaction.Type), PaymentOption: transaction.PaymentOption, FullName: transaction.FullName, PhoneNumber: transaction.PhoneNumber, @@ -97,6 +100,7 @@ func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service, BranchID: req.BranchID, CashierID: req.CashierID, BetID: req.BetID, + Type: domain.TransactionType(req.Type), PaymentOption: domain.PaymentOption(req.PaymentOption), FullName: req.FullName, PhoneNumber: req.PhoneNumber, diff --git a/internal/web_server/middleware.go b/internal/web_server/middleware.go index 46e248b..217a2bf 100644 --- a/internal/web_server/middleware.go +++ b/internal/web_server/middleware.go @@ -2,6 +2,7 @@ package httpserver import ( "errors" + "fmt" "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -13,10 +14,12 @@ func (a *App) authMiddleware(c *fiber.Ctx) error { authHeader := c.Get("Authorization") if authHeader == "" { + fmt.Println("Auth Header Missing") return fiber.NewError(fiber.StatusUnauthorized, "Authorization header missing") } if !strings.HasPrefix(authHeader, "Bearer ") { + fmt.Println("Invalid authorization header format") return fiber.NewError(fiber.StatusUnauthorized, "Invalid authorization header format") } @@ -25,8 +28,10 @@ func (a *App) authMiddleware(c *fiber.Ctx) error { claim, err := jwtutil.ParseJwt(accessToken, a.JwtConfig.JwtAccessKey) if err != nil { if errors.Is(err, jwtutil.ErrExpiredToken) { + fmt.Println("Token Expired") return fiber.NewError(fiber.StatusUnauthorized, "Access token expired") } + fmt.Println("Invalid Token") return fiber.NewError(fiber.StatusUnauthorized, "Invalid access token") } From c65ab77386e22d995ab7ffe198b75ef54bfb75ff Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Thu, 10 Apr 2025 16:34:00 +0300 Subject: [PATCH 07/30] small fix --- db/migrations/000001_fortune.down.sql | 7 +- db/migrations/000001_fortune.up.sql | 208 ++++++++++++++------------ 2 files changed, 115 insertions(+), 100 deletions(-) diff --git a/db/migrations/000001_fortune.down.sql b/db/migrations/000001_fortune.down.sql index a061f7f..d077f0b 100644 --- a/db/migrations/000001_fortune.down.sql +++ b/db/migrations/000001_fortune.down.sql @@ -75,11 +75,16 @@ DROP TYPE IF EXISTS ua_registaration_type; -- Drop FortuneBet DROP TABLE IF EXISTS tickets; +DROP TABLE IF EXISTS ticket_outcomes; DROP TABLE IF EXISTS bets; +DROP TABLE IF EXISTS bet_outcomes; DROP TABLE IF EXISTS wallets; +DROP TABLE IF EXISTS customer_wallets; DROP TABLE IF EXISTS wallet_transfer; DROP TABLE IF EXISTS transactions; -DROP TABLE IF EXISTS customer_wallets; DROP TABLE IF EXISTS branches; +DROP TABLE IF EXISTS supported_operations; +DROP TABLE IF EXISTS refresh_tokens; +DROP TABLE IF EXISTS otps; diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index ddfe56a..982681a 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -2,45 +2,47 @@ CREATE TABLE IF NOT EXISTS users ( id BIGSERIAL PRIMARY KEY, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, - email VARCHAR(255) UNIQUE , + email VARCHAR(255) UNIQUE, phone_number VARCHAR(20) UNIQUE, role VARCHAR(50) NOT NULL, password BYTEA NOT NULL, email_verified BOOLEAN NOT NULL DEFAULT FALSE, phone_verified BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ , + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ, -- suspended_at TIMESTAMPTZ NULL, -- this can be NULL if the user is not suspended suspended BOOLEAN NOT NULL DEFAULT FALSE, - CHECK (email IS NOT NULL OR phone_number IS NOT NULL) + CHECK ( + email IS NOT NULL + OR phone_number IS NOT NULL + ) ); CREATE TABLE refresh_tokens ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, token TEXT NOT NULL UNIQUE, - expires_at TIMESTAMPTZ NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, revoked BOOLEAN DEFAULT FALSE NOT NULL, CONSTRAINT unique_token UNIQUE (token) ); ----- - CREATE TABLE otps ( - id BIGSERIAL PRIMARY KEY, +CREATE TABLE otps ( + id BIGSERIAL PRIMARY KEY, sent_to VARCHAR(255) NOT NULL, - medium VARCHAR(50) NOT NULL, - otp_for VARCHAR(50) NOT NULL, - otp VARCHAR(10) NOT NULL, + medium VARCHAR(50) NOT NULL, + otp_for VARCHAR(50) NOT NULL, + otp VARCHAR(10) NOT NULL, used BOOLEAN NOT NULL DEFAULT FALSE, - used_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMPTZ NOT NULL + used_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMPTZ NOT NULL ); - CREATE TABLE IF NOT EXISTS bets ( - id BIGSERIAL PRIMARY KEY, - amount BIGINT NOT NULL, - total_odds REAL NOT NULL, + id BIGSERIAL PRIMARY KEY, + amount BIGINT NOT NULL, + total_odds REAL NOT NULL, status INT NOT NULL, full_name VARCHAR(255) NOT NULL, phone_number VARCHAR(255) NOT NULL, @@ -48,34 +50,39 @@ CREATE TABLE IF NOT EXISTS bets ( user_id BIGINT, cashed_out BOOLEAN DEFAULT FALSE NOT NULL, cashout_id VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, is_shop_bet BOOLEAN NOT NULL, - CHECK (user_id IS NOT NULL OR branch_id IS NOT NULL) + CHECK ( + user_id IS NOT NULL + OR branch_id IS NOT NULL + ) ); - -CREATE TABLE IF NOT EXISTS `tickets` ( - id BIGSERIAL PRIMARY KEY, - amount BIGINT NULL, - total_odds REAL NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, +CREATE TABLE IF NOT EXISTS tickets ( + id BIGSERIAL PRIMARY KEY, + amount BIGINT NULL, + total_odds REAL NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); - --- CREATE TABLE IF NOT EXISTS bet_outcomes ( --- id BIGSERIAL PRIMARY KEY, --- bet_id BIGINT NOT NULL, --- event_id bigint not null, --- odd_id BIGINT NOT NULL, --- ); - --- CREATE TABLE IF NOT EXISTS ticket_outcomes ( --- id BIGSERIAL PRIMARY KEY, --- ticket_id BIGINT NOT NULL, --- event_id bigint not null, --- odd_id BIGINT NOT NULL, --- ); - +CREATE TABLE IF NOT EXISTS bet_outcomes ( + id BIGSERIAL PRIMARY KEY, + bet_id BIGINT NOT NULL, + event_id bigint not null, + odd_id BIGINT NOT NULL, +); +CREATE TABLE IF NOT EXISTS ticket_outcomes ( + id BIGSERIAL PRIMARY KEY, + ticket_id BIGINT NOT NULL, + event_id bigint not null, + odd_id BIGINT NOT NULL, +); +ALTER TABLE bets +ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id); +ALTER TABLE bets +ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id); +ALTER TABLE bet_outcomes +ADD CONSTRAINT fk_bet_outcomes_bet FOREIGN KEY (bet_id) REFERENCES bets(id); CREATE TABLE IF NOT EXISTS wallets ( id BIGSERIAL PRIMARY KEY, balance BIGINT NOT NULL DEFAULT 0, @@ -87,8 +94,6 @@ CREATE TABLE IF NOT EXISTS wallets ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); - - CREATE TABLE IF NOT EXISTS customer_wallets ( id BIGSERIAL PRIMARY KEY, customer_id BIGINT NOT NULL, @@ -99,7 +104,6 @@ CREATE TABLE IF NOT EXISTS customer_wallets ( updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE (customer_id, company_id) ); - CREATE TABLE IF NOT EXISTS wallet_transfer ( id BIGSERIAL PRIMARY KEY, amount BIGINT NOT NULL, @@ -112,7 +116,6 @@ CREATE TABLE IF NOT EXISTS wallet_transfer ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); - CREATE TABLE IF NOT EXISTS transactions ( id BIGSERIAL PRIMARY KEY, amount BIGINT NOT NULL, @@ -132,7 +135,6 @@ CREATE TABLE IF NOT EXISTS transactions ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); - CREATE TABLE IF NOT EXISTS branches ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, @@ -144,20 +146,17 @@ CREATE TABLE IF NOT EXISTS branches ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); - CREATE VIEW branch_details AS -SELECT branches.*, - CONCAT(users.first_name, ' ', users.last_name) AS manager_name, +SELECT branches.*, + CONCAT(users.first_name, ' ', users.last_name) AS manager_name, users.phone_number AS manager_phone_number FROM branches -LEFT JOIN users ON branches.branch_manager_id = users.id; - + LEFT JOIN users ON branches.branch_manager_id = users.id; CREATE TABLE IF NOT EXISTS supported_operations ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, description VARCHAR(255) NOT NULL ); - CREATE TABLE IF NOT EXISTS branch_operations ( id BIGSERIAL PRIMARY KEY, operation_id BIGINT NOT NULL, @@ -165,55 +164,66 @@ CREATE TABLE IF NOT EXISTS branch_operations ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); - ----------------------------------------------seed data------------------------------------------------------------- -------------------------------------- DO NOT USE IN PRODUCTION------------------------------------------------- - CREATE EXTENSION IF NOT EXISTS pgcrypto; - INSERT INTO users ( - first_name, last_name, email, phone_number, password, role, - email_verified, phone_verified, created_at, updated_at, - suspended_at, suspended -) VALUES ( - 'John', - 'Doe', - 'john.doe@example.com', - NULL, - crypt('password123', gen_salt('bf'))::bytea, - 'customer', - TRUE, - FALSE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - NULL, - FALSE -); - + first_name, + last_name, + email, + phone_number, + password, + role, + email_verified, + phone_verified, + created_at, + updated_at, + suspended_at, + suspended + ) +VALUES ( + 'John', + 'Doe', + 'john.doe@example.com', + NULL, + crypt('password123', gen_salt('bf'))::bytea, + 'customer', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + NULL, + FALSE + ); INSERT INTO users ( - first_name, last_name, email, phone_number, password, role, - email_verified, phone_verified, created_at, updated_at, - suspended_at, suspended -) VALUES ( - 'Samuel', - 'Tariku', - 'cybersamt@gmail.com', - NULL, - crypt('password@123', gen_salt('bf'))::bytea, - 'cashier', - TRUE, - FALSE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - NULL, - FALSE -); - - -INSERT INTO supported_operations ( - name, description -) VALUES -('SportBook', 'Sportbook operations'), -('Virtual', 'Virtual operations'), -('GameZone', 'GameZone operations') -; + first_name, + last_name, + email, + phone_number, + password, + role, + email_verified, + phone_verified, + created_at, + updated_at, + suspended_at, + suspended + ) +VALUES ( + 'Samuel', + 'Tariku', + 'cybersamt@gmail.com', + NULL, + crypt('password@123', gen_salt('bf'))::bytea, + 'cashier', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + NULL, + FALSE + ); +INSERT INTO supported_operations (name, description) +VALUES ('SportBook', 'Sportbook operations'), + ('Virtual', 'Virtual operations'), + ('GameZone', 'GameZone operations'); \ No newline at end of file From 92250d61a825375d39d5093e80db87355d54718f Mon Sep 17 00:00:00 2001 From: OneTap Technologies Date: Thu, 10 Apr 2025 16:42:26 +0300 Subject: [PATCH 08/30] event and odd data --- cmd/main.go | 34 +++-- db/migrations/000001_fortune.down.sql | 5 + db/migrations/000001_fortune.up.sql | 45 ++++++- db/query/events.sql | 38 ++++++ db/query/odds.sql | 14 +++ gen/db/auth.sql.go | 2 +- gen/db/db.go | 2 +- gen/db/events.sql.go | 122 ++++++++++++++++++ gen/db/models.go | 43 ++++++- gen/db/odds.sql.go | 76 +++++++++++ gen/db/otp.sql.go | 2 +- gen/db/user.sql.go | 2 +- go.mod | 1 + go.sum | 2 + internal/config/config.go | 7 ++ internal/domain/event.go | 23 ++++ internal/domain/odds.go | 14 +++ internal/repository/event.go | 44 +++++++ internal/repository/odds.go | 64 ++++++++++ internal/services/event/port.go | 8 ++ internal/services/event/service.go | 173 ++++++++++++++++++++++++++ internal/services/odds/port.go | 8 ++ internal/services/odds/service.go | 152 ++++++++++++++++++++++ internal/web_server/cron.go | 54 ++++++++ 24 files changed, 921 insertions(+), 14 deletions(-) create mode 100644 db/query/events.sql create mode 100644 db/query/odds.sql create mode 100644 gen/db/events.sql.go create mode 100644 gen/db/odds.sql.go create mode 100644 internal/domain/event.go create mode 100644 internal/domain/odds.go create mode 100644 internal/repository/event.go create mode 100644 internal/repository/odds.go create mode 100644 internal/services/event/port.go create mode 100644 internal/services/event/service.go create mode 100644 internal/services/odds/port.go create mode 100644 internal/services/odds/service.go create mode 100644 internal/web_server/cron.go diff --git a/cmd/main.go b/cmd/main.go index b57aacc..ac6ca85 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,17 +5,20 @@ import ( "log/slog" "os" + "github.com/go-playground/validator/v10" + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger" mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email" mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" - "github.com/go-playground/validator/v10" ) // @title FortuneBet API @@ -32,32 +35,47 @@ import ( // @name Authorization // @BasePath / func main() { + // Load config cfg, err := config.NewConfig() if err != nil { - slog.Error(err.Error()) + slog.Error("❌ Config error:", "err", err) os.Exit(1) } + + // Connect to database db, _, err := repository.OpenDB(cfg.DbUrl) if err != nil { - fmt.Print("db", err) + fmt.Println("❌ Database error:", err) os.Exit(1) } + + // Init core services logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel) store := repository.NewStore(db) v := customvalidator.NewCustomValidator(validator.New()) + + // Auth and user services authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) mockSms := mocksms.NewMockSMS() - mockemail := mockemail.NewMockEmail() - userSvc := user.NewService(store, store, mockSms, mockemail) + mockEmail := mockemail.NewMockEmail() + userSvc := user.NewService(store, store, mockSms, mockEmail) + + // 🎯 Event & Odds fetching services + eventSvc := event.New(cfg.Bet365Token, store) + oddsSvc := odds.New(cfg.Bet365Token, store) + + // 🕒 Start scheduled cron jobs + httpserver.StartDataFetchingCrons(eventSvc, oddsSvc) + + // 🚀 Start HTTP server app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, - }, userSvc, - ) + }, userSvc) + logger.Info("Starting server", "port", cfg.Port) if err := app.Run(); err != nil { logger.Error("Failed to start server", "error", err) os.Exit(1) } - } diff --git a/db/migrations/000001_fortune.down.sql b/db/migrations/000001_fortune.down.sql index 489466f..180201e 100644 --- a/db/migrations/000001_fortune.down.sql +++ b/db/migrations/000001_fortune.down.sql @@ -72,3 +72,8 @@ DROP TABLE IF EXISTS ussd_account; DROP TYPE IF EXISTS ua_pin_status; DROP TYPE IF EXISTS ua_status; DROP TYPE IF EXISTS ua_registaration_type; + + +DROP TABLE IF EXISTS odds; +DROP TABLE IF EXISTS events; + diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index a4e3bd0..6d965b4 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -58,4 +58,47 @@ INSERT INTO users ( CURRENT_TIMESTAMP, NULL, FALSE -); \ No newline at end of file +); + +--------------------------------------------------Bet365 Data Fetching + Event Managment------------------------------------------------ +CREATE TABLE events ( + id TEXT PRIMARY KEY, + sport_id TEXT, + match_name TEXT, + home_team TEXT, + away_team TEXT, + home_team_id TEXT, + away_team_id TEXT, + home_kit_image TEXT, + away_kit_image TEXT, + league_id TEXT, + league_name TEXT, + league_cc TEXT, + start_time TIMESTAMP, + score TEXT, + match_minute INT, + timer_status TEXT, + added_time INT, + match_period INT, + is_live BOOLEAN, + status TEXT, + fetched_at TIMESTAMP DEFAULT now() +); + + +CREATE TABLE odds ( + id SERIAL PRIMARY KEY, + event_id TEXT, -- Parsed from "FI" (Bet365 Event ID). Nullable in case of failures. + market_type TEXT NOT NULL, -- E.g., 'asian_handicap', 'goal_line', 'both_teams_to_score' + header TEXT, -- E.g., '1', '2', 'Over', 'Under', 'Draw', 'Yes', 'No' + name TEXT, -- Bet name like "2.5", "Over 2.5 & Yes", etc. + odds_value DOUBLE PRECISION, -- The numeric odds (e.g., 1.920) + handicap TEXT, -- Handicap value like "-0.5", "0.0, +0.5" + section TEXT NOT NULL, -- Odds section: 'asian_lines', 'goals', etc. + category TEXT, -- Market category (e.g., 'sp') + market_id TEXT, -- Market ID from the API (e.g., "938", "50138") + fetched_at TIMESTAMP DEFAULT now(), -- When this record was fetched + source TEXT DEFAULT 'b365api', -- Source identifier + is_active BOOLEAN DEFAULT true, -- Optional deactivation flag + raw_event_id TEXT -- Original/failed event ID if event_id is nil or invalid +); diff --git a/db/query/events.sql b/db/query/events.sql new file mode 100644 index 0000000..dddec63 --- /dev/null +++ b/db/query/events.sql @@ -0,0 +1,38 @@ +-- name: InsertEvent :exec +INSERT INTO events ( + id, sport_id, match_name, home_team, away_team, + home_team_id, away_team_id, home_kit_image, away_kit_image, + league_id, league_name, league_cc, start_time, score, + match_minute, timer_status, added_time, match_period, + is_live, status +) VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, $9, + $10, $11, $12, $13, $14, + $15, $16, $17, $18, + $19, $20 +) +ON CONFLICT (id) DO UPDATE SET + sport_id = EXCLUDED.sport_id, + match_name = EXCLUDED.match_name, + home_team = EXCLUDED.home_team, + away_team = EXCLUDED.away_team, + home_team_id = EXCLUDED.home_team_id, + away_team_id = EXCLUDED.away_team_id, + home_kit_image = EXCLUDED.home_kit_image, + away_kit_image = EXCLUDED.away_kit_image, + league_id = EXCLUDED.league_id, + league_name = EXCLUDED.league_name, + league_cc = EXCLUDED.league_cc, + start_time = EXCLUDED.start_time, + score = EXCLUDED.score, + match_minute = EXCLUDED.match_minute, + timer_status = EXCLUDED.timer_status, + added_time = EXCLUDED.added_time, + match_period = EXCLUDED.match_period, + is_live = EXCLUDED.is_live, + status = EXCLUDED.status, + fetched_at = now(); + +-- name: ListLiveEvents :many +SELECT id FROM events WHERE is_live = true; \ No newline at end of file diff --git a/db/query/odds.sql b/db/query/odds.sql new file mode 100644 index 0000000..b041a85 --- /dev/null +++ b/db/query/odds.sql @@ -0,0 +1,14 @@ + +-- name: InsertNonLiveOdd :exec +INSERT INTO odds ( + event_id, market_type, header, name, odds_value, handicap, + section, category, market_id, is_active, source, fetched_at, raw_event_id +) VALUES ( + $1, $2, $3, $4, $5, $6, + $7, $8, $9, true, 'b365api', now(), $10 +); + + +-- name: GetUpcomingEventIDs :many +SELECT id FROM events +WHERE is_live = false; diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 27fb891..c826c36 100644 --- a/gen/db/auth.sql.go +++ b/gen/db/auth.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.28.0 // source: auth.sql package dbgen diff --git a/gen/db/db.go b/gen/db/db.go index fe4ae38..136f20a 100644 --- a/gen/db/db.go +++ b/gen/db/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.28.0 package dbgen diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go new file mode 100644 index 0000000..e833d0e --- /dev/null +++ b/gen/db/events.sql.go @@ -0,0 +1,122 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: events.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const InsertEvent = `-- name: InsertEvent :exec +INSERT INTO events ( + id, sport_id, match_name, home_team, away_team, + home_team_id, away_team_id, home_kit_image, away_kit_image, + league_id, league_name, league_cc, start_time, score, + match_minute, timer_status, added_time, match_period, + is_live, status +) VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, $9, + $10, $11, $12, $13, $14, + $15, $16, $17, $18, + $19, $20 +) +ON CONFLICT (id) DO UPDATE SET + sport_id = EXCLUDED.sport_id, + match_name = EXCLUDED.match_name, + home_team = EXCLUDED.home_team, + away_team = EXCLUDED.away_team, + home_team_id = EXCLUDED.home_team_id, + away_team_id = EXCLUDED.away_team_id, + home_kit_image = EXCLUDED.home_kit_image, + away_kit_image = EXCLUDED.away_kit_image, + league_id = EXCLUDED.league_id, + league_name = EXCLUDED.league_name, + league_cc = EXCLUDED.league_cc, + start_time = EXCLUDED.start_time, + score = EXCLUDED.score, + match_minute = EXCLUDED.match_minute, + timer_status = EXCLUDED.timer_status, + added_time = EXCLUDED.added_time, + match_period = EXCLUDED.match_period, + is_live = EXCLUDED.is_live, + status = EXCLUDED.status, + fetched_at = now() +` + +type InsertEventParams struct { + ID string + SportID pgtype.Text + MatchName pgtype.Text + HomeTeam pgtype.Text + AwayTeam pgtype.Text + HomeTeamID pgtype.Text + AwayTeamID pgtype.Text + HomeKitImage pgtype.Text + AwayKitImage pgtype.Text + LeagueID pgtype.Text + LeagueName pgtype.Text + LeagueCc pgtype.Text + StartTime pgtype.Timestamp + Score pgtype.Text + MatchMinute pgtype.Int4 + TimerStatus pgtype.Text + AddedTime pgtype.Int4 + MatchPeriod pgtype.Int4 + IsLive pgtype.Bool + Status pgtype.Text +} + +func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error { + _, err := q.db.Exec(ctx, InsertEvent, + arg.ID, + arg.SportID, + arg.MatchName, + arg.HomeTeam, + arg.AwayTeam, + arg.HomeTeamID, + arg.AwayTeamID, + arg.HomeKitImage, + arg.AwayKitImage, + arg.LeagueID, + arg.LeagueName, + arg.LeagueCc, + arg.StartTime, + arg.Score, + arg.MatchMinute, + arg.TimerStatus, + arg.AddedTime, + arg.MatchPeriod, + arg.IsLive, + arg.Status, + ) + return err +} + +const ListLiveEvents = `-- name: ListLiveEvents :many +SELECT id FROM events WHERE is_live = true +` + +func (q *Queries) ListLiveEvents(ctx context.Context) ([]string, error) { + rows, err := q.db.Query(ctx, ListLiveEvents) + if err != nil { + return nil, err + } + defer rows.Close() + var items []string + for rows.Next() { + var id string + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/gen/db/models.go b/gen/db/models.go index a1465c2..24be0bd 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.28.0 package dbgen @@ -8,6 +8,47 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +type Event struct { + ID string + SportID pgtype.Text + MatchName pgtype.Text + HomeTeam pgtype.Text + AwayTeam pgtype.Text + HomeTeamID pgtype.Text + AwayTeamID pgtype.Text + HomeKitImage pgtype.Text + AwayKitImage pgtype.Text + LeagueID pgtype.Text + LeagueName pgtype.Text + LeagueCc pgtype.Text + StartTime pgtype.Timestamp + Score pgtype.Text + MatchMinute pgtype.Int4 + TimerStatus pgtype.Text + AddedTime pgtype.Int4 + MatchPeriod pgtype.Int4 + IsLive pgtype.Bool + Status pgtype.Text + FetchedAt pgtype.Timestamp +} + +type Odd struct { + ID int32 + EventID pgtype.Text + MarketType string + Header pgtype.Text + Name pgtype.Text + OddsValue pgtype.Float8 + Handicap pgtype.Text + Section string + Category pgtype.Text + MarketID pgtype.Text + FetchedAt pgtype.Timestamp + Source pgtype.Text + IsActive pgtype.Bool + RawEventID pgtype.Text +} + type Otp struct { ID int64 SentTo string diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go new file mode 100644 index 0000000..fe33a5e --- /dev/null +++ b/gen/db/odds.sql.go @@ -0,0 +1,76 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: odds.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const GetUpcomingEventIDs = `-- name: GetUpcomingEventIDs :many +SELECT id FROM events +WHERE is_live = false +` + +func (q *Queries) GetUpcomingEventIDs(ctx context.Context) ([]string, error) { + rows, err := q.db.Query(ctx, GetUpcomingEventIDs) + if err != nil { + return nil, err + } + defer rows.Close() + var items []string + for rows.Next() { + var id string + if err := rows.Scan(&id); err != nil { + return nil, err + } + items = append(items, id) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec +INSERT INTO odds ( + event_id, market_type, header, name, odds_value, handicap, + section, category, market_id, is_active, source, fetched_at, raw_event_id +) VALUES ( + $1, $2, $3, $4, $5, $6, + $7, $8, $9, true, 'b365api', now(), $10 +) +` + +type InsertNonLiveOddParams struct { + EventID pgtype.Text + MarketType string + Header pgtype.Text + Name pgtype.Text + OddsValue pgtype.Float8 + Handicap pgtype.Text + Section string + Category pgtype.Text + MarketID pgtype.Text + RawEventID pgtype.Text +} + +func (q *Queries) InsertNonLiveOdd(ctx context.Context, arg InsertNonLiveOddParams) error { + _, err := q.db.Exec(ctx, InsertNonLiveOdd, + arg.EventID, + arg.MarketType, + arg.Header, + arg.Name, + arg.OddsValue, + arg.Handicap, + arg.Section, + arg.Category, + arg.MarketID, + arg.RawEventID, + ) + return err +} diff --git a/gen/db/otp.sql.go b/gen/db/otp.sql.go index 619bf92..e0b9806 100644 --- a/gen/db/otp.sql.go +++ b/gen/db/otp.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.28.0 // source: otp.sql package dbgen diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 88ee397..39f0a5c 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.26.0 +// sqlc v1.28.0 // source: user.sql package dbgen diff --git a/go.mod b/go.mod index 2fb3275..02a28b2 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect diff --git a/go.sum b/go.sum index c86e5af..1cd6ef9 100644 --- a/go.sum +++ b/go.sum @@ -103,6 +103,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/internal/config/config.go b/internal/config/config.go index 229bd47..98a6a12 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,6 +19,7 @@ var ( ErrLogLevel = errors.New("log level not set") ErrInvalidLevel = errors.New("invalid log level") ErrInvalidEnv = errors.New("env not set or invalid") + ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env") ) type Config struct { @@ -29,6 +30,7 @@ type Config struct { JwtKey string LogLevel slog.Level Env string + Bet365Token string } func NewConfig() (*Config, error) { @@ -89,5 +91,10 @@ func (c *Config) loadEnv() error { return ErrInvalidLevel } c.LogLevel = lvl + betToken := os.Getenv("BET365_TOKEN") + if betToken == "" { + return ErrMissingBetToken + } + c.Bet365Token = betToken return nil } diff --git a/internal/domain/event.go b/internal/domain/event.go new file mode 100644 index 0000000..fb7edf4 --- /dev/null +++ b/internal/domain/event.go @@ -0,0 +1,23 @@ +package domain +type Event struct { + ID string + SportID string + MatchName string + HomeTeam string + AwayTeam string + HomeTeamID string + AwayTeamID string + HomeKitImage string + AwayKitImage string + LeagueID string + LeagueName string + LeagueCC string + StartTime string + Score string + MatchMinute int + TimerStatus string + AddedTime int + MatchPeriod int + IsLive bool + Status string +} \ No newline at end of file diff --git a/internal/domain/odds.go b/internal/domain/odds.go new file mode 100644 index 0000000..0af5dbd --- /dev/null +++ b/internal/domain/odds.go @@ -0,0 +1,14 @@ +package domain + +type OddsRecord struct { + EventID string + MarketType string + Header string + Name string + OddsValue float64 + Handicap string + Section string + Category string + MarketID string + RawEventID string +} diff --git a/internal/repository/event.go b/internal/repository/event.go new file mode 100644 index 0000000..1949a44 --- /dev/null +++ b/internal/repository/event.go @@ -0,0 +1,44 @@ +package repository + +import ( + "context" + "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Store) SaveEvent(ctx context.Context, e domain.Event) error { + parsedTime, err := time.Parse(time.RFC3339, e.StartTime) + if err != nil { + return err + } + + return s.queries.InsertEvent(ctx, dbgen.InsertEventParams{ + ID: e.ID, + SportID: pgtype.Text{String: e.SportID, Valid: true}, + MatchName: pgtype.Text{String: e.MatchName, Valid: true}, + HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true}, + AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true}, + HomeTeamID: pgtype.Text{String: e.HomeTeamID, Valid: true}, + AwayTeamID: pgtype.Text{String: e.AwayTeamID, Valid: true}, + HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true}, + AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true}, + LeagueID: pgtype.Text{String: e.LeagueID, Valid: true}, + LeagueName: pgtype.Text{String: e.LeagueName, Valid: true}, + LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true}, + StartTime: pgtype.Timestamp{Time: parsedTime, Valid: true}, + Score: pgtype.Text{String: e.Score, Valid: true}, + MatchMinute: pgtype.Int4{Int32: int32(e.MatchMinute), Valid: true}, + TimerStatus: pgtype.Text{String: e.TimerStatus, Valid: true}, + AddedTime: pgtype.Int4{Int32: int32(e.AddedTime), Valid: true}, + MatchPeriod: pgtype.Int4{Int32: int32(e.MatchPeriod), Valid: true}, + IsLive: pgtype.Bool{Bool: e.IsLive, Valid: true}, + Status: pgtype.Text{String: e.Status, Valid: true}, + }) +} + +func (s *Store) GetLiveEventIDs(ctx context.Context) ([]string, error) { + return s.queries.ListLiveEvents(ctx) +} diff --git a/internal/repository/odds.go b/internal/repository/odds.go new file mode 100644 index 0000000..c2ea64f --- /dev/null +++ b/internal/repository/odds.go @@ -0,0 +1,64 @@ +package repository + +import ( + "context" + "fmt" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Store) SaveNonLiveOdd(ctx context.Context, o domain.OddsRecord) error { + params := dbgen.InsertNonLiveOddParams{ + EventID: pgtype.Text{ + String: o.EventID, + Valid: o.EventID != "", + }, + MarketType: o.MarketType, + Header: pgtype.Text{ + String: o.Header, + Valid: o.Header != "", + }, + Name: pgtype.Text{ + String: o.Name, + Valid: o.Name != "", + }, + OddsValue: pgtype.Float8{ + Float64: o.OddsValue, + Valid: true, + }, + Handicap: pgtype.Text{ + String: o.Handicap, + Valid: o.Handicap != "", + }, + Section: o.Section, + Category: pgtype.Text{ + String: o.Category, + Valid: o.Category != "", + }, + MarketID: pgtype.Text{ + String: o.MarketID, + Valid: o.MarketID != "", + }, + RawEventID: pgtype.Text{ + String: o.RawEventID, + Valid: o.RawEventID != "", + }, + } + + err := s.queries.InsertNonLiveOdd(ctx, params) + + if err != nil { + fmt.Printf("❌ Failed to insert odd: event_id=%s | market=%s | odds=%.3f | error=%v\n", + o.EventID, o.MarketType, o.OddsValue, err) + } else { + fmt.Printf("✅ Stored: event_id=%s | market=%s | odds=%.3f\n", o.EventID, o.MarketType, o.OddsValue) + } + + return err +} + +func (s *Store) GetUpcomingEventIDs(ctx context.Context) ([]string, error) { + return s.queries.GetUpcomingEventIDs(ctx) +} diff --git a/internal/services/event/port.go b/internal/services/event/port.go new file mode 100644 index 0000000..b500ca4 --- /dev/null +++ b/internal/services/event/port.go @@ -0,0 +1,8 @@ +package event + +import "context" + +type Service interface { + FetchLiveEvents(ctx context.Context) error + FetchUpcomingEvents(ctx context.Context) error +} diff --git a/internal/services/event/service.go b/internal/services/event/service.go new file mode 100644 index 0000000..3d54d7a --- /dev/null +++ b/internal/services/event/service.go @@ -0,0 +1,173 @@ +package event + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "sync" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" +) + +type service struct { + token string + store *repository.Store +} + +func New(token string, store *repository.Store) Service { + return &service{ + token: token, + store: store, + } +} + +func (s *service) FetchLiveEvents(ctx context.Context) error { + sportIDs := []int{1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, 95, 110, 107, 151, 162, 148} + + var wg sync.WaitGroup + + for _, sportID := range sportIDs { + wg.Add(1) + go func(sportID int) { + defer wg.Done() + + url := fmt.Sprintf("https://api.b365api.com/v1/bet365/inplay?sport_id=%d&token=%s", sportID, s.token) + resp, err := http.Get(url) + if err != nil { + fmt.Printf(" Failed request for sport_id=%d: %v\n", sportID, err) + return + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + + var data struct { + Success int `json:"success"` + Results [][]map[string]interface{} `json:"results"` + } + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { + fmt.Printf(" Decode failed for sport_id=%d\nRaw: %s\n", sportID, string(body)) + return + } + + for _, group := range data.Results { + for _, ev := range group { + if getString(ev["type"]) != "EV" { + continue + } + + event := domain.Event{ + ID: getString(ev["ID"]), + SportID: fmt.Sprintf("%d", sportID), + MatchName: getString(ev["NA"]), + Score: getString(ev["SS"]), + MatchMinute: getInt(ev["TM"]), + TimerStatus: getString(ev["TT"]), + HomeTeamID: getString(ev["HT"]), + AwayTeamID: getString(ev["AT"]), + HomeKitImage: getString(ev["K1"]), + AwayKitImage: getString(ev["K2"]), + LeagueName: getString(ev["CT"]), + LeagueID: getString(ev["C2"]), + LeagueCC: getString(ev["CB"]), + StartTime: time.Now().UTC().Format(time.RFC3339), + IsLive: true, + Status: "live", + MatchPeriod: getInt(ev["MD"]), + AddedTime: getInt(ev["TA"]), + } + + if err := s.store.SaveEvent(ctx, event); err != nil { + fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) + } + } + } + }(sportID) + } + + wg.Wait() + fmt.Println("All live events fetched and stored.") + return nil +} + +func (s *service) FetchUpcomingEvents(ctx context.Context) error { + sportIDs := []int{1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, 95, 110, 107, 151, 162, 148} + + var wg sync.WaitGroup + + for _, sportID := range sportIDs { + wg.Add(1) + go func(sportID int) { + defer wg.Done() + + url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token) + resp, err := http.Get(url) + if err != nil { + fmt.Printf(" Failed request for upcoming sport_id=%d: %v\n", sportID, err) + return + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + + var data struct { + Success int `json:"success"` + Results [][]map[string]interface{} `json:"results"` + } + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { + fmt.Printf(" Decode failed for upcoming sport_id=%d\nRaw: %s\n", sportID, string(body)) + return + } + + for _, group := range data.Results { + for _, ev := range group { + if getString(ev["type"]) != "EV" { + continue + } + + event := domain.Event{ + ID: getString(ev["ID"]), + SportID: fmt.Sprintf("%d", sportID), + MatchName: getString(ev["NA"]), + HomeTeamID: getString(ev["HT"]), + AwayTeamID: getString(ev["AT"]), + HomeKitImage: getString(ev["K1"]), + AwayKitImage: getString(ev["K2"]), + LeagueID: getString(ev["C2"]), + LeagueName: getString(ev["CT"]), + LeagueCC: getString(ev["CB"]), + StartTime: time.Now().UTC().Format(time.RFC3339), + IsLive: false, + Status: "upcoming", + } + + if err := s.store.SaveEvent(ctx, event); err != nil { + fmt.Printf(" Could not store upcoming event [id=%s]: %v\n", event.ID, err) + } + } + } + }(sportID) + } + + wg.Wait() + fmt.Println(" All upcoming events fetched and stored.") + return nil +} + +func getString(v interface{}) string { + if str, ok := v.(string); ok { + return str + } + return "" +} + +func getInt(v interface{}) int { + if f, ok := v.(float64); ok { + return int(f) + } + return 0 +} diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go new file mode 100644 index 0000000..d95367e --- /dev/null +++ b/internal/services/odds/port.go @@ -0,0 +1,8 @@ +package odds + +import "context" + +type Service interface { + FetchNonLiveOdds(ctx context.Context) error + +} diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go new file mode 100644 index 0000000..090add4 --- /dev/null +++ b/internal/services/odds/service.go @@ -0,0 +1,152 @@ +package odds + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "sync" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" +) + +type ServiceImpl struct { + token string + store *repository.Store +} + +func New(token string, store *repository.Store) *ServiceImpl { + return &ServiceImpl{token: token, store: store} +} +func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { + eventIDs, err := s.store.GetUpcomingEventIDs(ctx) + if err != nil { + return fmt.Errorf("fetch upcoming event IDs: %w", err) + } + + type OddsMarket struct { + ID string `json:"id"` + Name string `json:"name"` + Odds []struct { + ID string `json:"id"` + Odds string `json:"odds"` + Header string `json:"header,omitempty"` + Name string `json:"name,omitempty"` + Handicap string `json:"handicap,omitempty"` + } `json:"odds"` + } + + type OddsSection struct { + UpdatedAt string `json:"updated_at"` + Sp map[string]OddsMarket `json:"sp"` + } + + type StructuredOddsResponse struct { + Success int `json:"success"` + Results []struct { + EventID string `json:"event_id"` + FI string `json:"FI"` + AsianLines OddsSection `json:"asian_lines"` + Goals OddsSection `json:"goals"` + } `json:"results"` + } + + var wg sync.WaitGroup + sem := make(chan struct{}, 5) + + for _, eventID := range eventIDs { + if eventID == "" || len(eventID) < 5 { + continue + } + + wg.Add(1) + go func(originalID string) { + defer wg.Done() + sem <- struct{}{} + defer func() { <-sem }() + + url := fmt.Sprintf("https://api.b365api.com/v1/bet365/odds?token=%s&event_id=%s", s.token, originalID) + resp, err := http.Get(url) + if err != nil { + fmt.Printf(" Failed HTTP request for event_id=%s: %v\n", originalID, err) + return + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf(" Failed to read response body for event_id=%s: %v\n", originalID, err) + return + } + + var data StructuredOddsResponse + if err := json.Unmarshal(body, &data); err != nil { + fmt.Printf(" JSON unmarshal failed for event_id=%s. Response: %s\n", originalID, string(body)) + return + } + + if data.Success != 1 || len(data.Results) == 0 { + fmt.Printf(" API response error or no results for event_id=%s\nBody: %s\n", originalID, string(body)) + return + } + + result := data.Results[0] + finalEventID := result.EventID + if finalEventID == "" { + finalEventID = result.FI + } + if finalEventID == "" { + fmt.Printf(" Skipping event_id=%s due to missing both event_id and FI\n", originalID) + return + } + + saveOdds := func(sectionName string, section OddsSection) { + for marketType, market := range section.Sp { + for _, odd := range market.Odds { + val, err := strconv.ParseFloat(odd.Odds, 64) + if err != nil { + fmt.Printf(" Skipping invalid odds for market=%s, event_id=%s\n", marketType, finalEventID) + continue + } + + record := domain.OddsRecord{ + EventID: finalEventID, + MarketType: marketType, + Header: odd.Header, + Name: odd.Name, + Handicap: odd.Handicap, + OddsValue: val, + Section: sectionName, + Category: market.ID, + MarketID: odd.ID, + RawEventID: originalID, + } + + fmt.Printf("🟡 Preparing to store: event_id=%s | market=%s | header=%s | name=%s | odds=%.3f\n", + finalEventID, marketType, odd.Header, odd.Name, val) + + err = s.store.SaveNonLiveOdd(ctx, record) + if err != nil { + fmt.Printf("❌ DB save error for market=%s, event_id=%s: %v\nRecord: %+v\n", marketType, finalEventID, err, record) + } else { + fmt.Printf("✅ Stored odd: event_id=%s | market=%s | odds=%.3f\n", finalEventID, marketType, val) + } + } + } + } + + + saveOdds("asian_lines", result.AsianLines) + saveOdds("goals", result.Goals) + + }(eventID) + } + + wg.Wait() + fmt.Println("✅ All non-live odds fetched and stored.") + return nil +} + diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go new file mode 100644 index 0000000..e895b6e --- /dev/null +++ b/internal/web_server/cron.go @@ -0,0 +1,54 @@ +package httpserver + +import ( + "context" + "log" + + eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/robfig/cron/v3" +) + +func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service) { + c := cron.New(cron.WithSeconds()) + + schedule := []struct { + spec string + task func() + }{ + { + spec: "0 0 * * * *", // Every hour + task: func() { + if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + log.Printf(" FetchUpcomingEvents error: %v", err) + } + }, + }, + // { + // spec: "*/5 * * * * *", // Every 5 seconds + // task: func() { + // if err := eventService.FetchLiveEvents(context.Background()); err != nil { + // log.Printf(" FetchLiveEvents error: %v", err) + // } + // }, + // }, + { + spec: "*/5 * * * * *", // Every 5 seconds + task: func() { + if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + log.Printf(" FetchNonLiveOdds error: %v", err) + } + }, + }, + + } + + 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 event and odds services") +} From 1d6a533f7ee83610308ec53530982b760ce9d9bc Mon Sep 17 00:00:00 2001 From: OneTap Technologies Date: Fri, 11 Apr 2025 13:57:32 +0300 Subject: [PATCH 09/30] addign odd --- cmd/main.go | 10 +- db/migrations/000001_fortune.up.sql | 42 ++++-- db/query/odds.sql | 39 ++++- gen/db/models.go | 32 ++-- gen/db/odds.sql.go | 69 ++++++--- go.mod | 2 +- internal/domain/odds.go | 27 ++-- internal/repository/odds.go | 94 +++++++----- internal/services/odds/service.go | 221 ++++++++++++++-------------- 9 files changed, 318 insertions(+), 218 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index ac6ca85..6b945d5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,6 +1,7 @@ package main import ( + // "context" "fmt" "log/slog" "os" @@ -35,39 +36,34 @@ import ( // @name Authorization // @BasePath / func main() { - // Load config cfg, err := config.NewConfig() if err != nil { slog.Error("❌ Config error:", "err", err) os.Exit(1) } - // Connect to database db, _, err := repository.OpenDB(cfg.DbUrl) if err != nil { fmt.Println("❌ Database error:", err) os.Exit(1) } - // Init core services logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel) store := repository.NewStore(db) v := customvalidator.NewCustomValidator(validator.New()) - // Auth and user services authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) mockSms := mocksms.NewMockSMS() mockEmail := mockemail.NewMockEmail() userSvc := user.NewService(store, store, mockSms, mockEmail) - // 🎯 Event & Odds fetching services eventSvc := event.New(cfg.Bet365Token, store) oddsSvc := odds.New(cfg.Bet365Token, store) - // 🕒 Start scheduled cron jobs + + httpserver.StartDataFetchingCrons(eventSvc, oddsSvc) - // 🚀 Start HTTP server app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 6d965b4..ba45316 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -88,17 +88,33 @@ CREATE TABLE events ( CREATE TABLE odds ( id SERIAL PRIMARY KEY, - event_id TEXT, -- Parsed from "FI" (Bet365 Event ID). Nullable in case of failures. - market_type TEXT NOT NULL, -- E.g., 'asian_handicap', 'goal_line', 'both_teams_to_score' - header TEXT, -- E.g., '1', '2', 'Over', 'Under', 'Draw', 'Yes', 'No' - name TEXT, -- Bet name like "2.5", "Over 2.5 & Yes", etc. - odds_value DOUBLE PRECISION, -- The numeric odds (e.g., 1.920) - handicap TEXT, -- Handicap value like "-0.5", "0.0, +0.5" - section TEXT NOT NULL, -- Odds section: 'asian_lines', 'goals', etc. - category TEXT, -- Market category (e.g., 'sp') - market_id TEXT, -- Market ID from the API (e.g., "938", "50138") - fetched_at TIMESTAMP DEFAULT now(), -- When this record was fetched - source TEXT DEFAULT 'b365api', -- Source identifier - is_active BOOLEAN DEFAULT true, -- Optional deactivation flag - raw_event_id TEXT -- Original/failed event ID if event_id is nil or invalid + + -- Core IDs + event_id TEXT, + fi TEXT, -- ✅ from Market.FI + raw_event_id TEXT, -- Original event ID if different + + -- Market info + market_type TEXT NOT NULL, -- e.g., "asian_handicap" + market_name TEXT, -- ✅ from Market.MarketName + market_category TEXT, -- ✅ from Market.marketcatagory (like "asian_lines") + market_id TEXT, -- e.g., "938" + + -- Odds detail + header TEXT, + name TEXT, + handicap TEXT, + odds_value DOUBLE PRECISION, + + -- Meta + section TEXT NOT NULL, + category TEXT, + raw_odds JSONB, -- ✅ store full odds array here + fetched_at TIMESTAMP DEFAULT now(), + source TEXT DEFAULT 'b365api', + is_active BOOLEAN DEFAULT true, + + -- Conflict resolution key + UNIQUE (event_id, market_id, header, name, handicap) ); + diff --git a/db/query/odds.sql b/db/query/odds.sql index b041a85..c1ca21c 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -1,13 +1,38 @@ - -- name: InsertNonLiveOdd :exec INSERT INTO odds ( - event_id, market_type, header, name, odds_value, handicap, - section, category, market_id, is_active, source, fetched_at, raw_event_id + event_id, + fi, + raw_event_id, + market_type, + market_name, + market_category, + market_id, + header, + name, + handicap, + odds_value, + section, + category, + raw_odds, + is_active, + source, + fetched_at ) VALUES ( - $1, $2, $3, $4, $5, $6, - $7, $8, $9, true, 'b365api', now(), $10 -); - + $1, $2, $3, $4, $5, $6, $7, + $8, $9, $10, $11, $12, $13, $14, + true, 'b365api', now() +) +ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET + odds_value = EXCLUDED.odds_value, + raw_odds = EXCLUDED.raw_odds, + market_type = EXCLUDED.market_type, + market_name = EXCLUDED.market_name, + market_category = EXCLUDED.market_category, + fetched_at = now(), + is_active = true, + source = 'b365api', + fi = EXCLUDED.fi, + raw_event_id = EXCLUDED.raw_event_id; -- name: GetUpcomingEventIDs :many SELECT id FROM events diff --git a/gen/db/models.go b/gen/db/models.go index 24be0bd..071fe68 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -33,20 +33,24 @@ type Event struct { } type Odd struct { - ID int32 - EventID pgtype.Text - MarketType string - Header pgtype.Text - Name pgtype.Text - OddsValue pgtype.Float8 - Handicap pgtype.Text - Section string - Category pgtype.Text - MarketID pgtype.Text - FetchedAt pgtype.Timestamp - Source pgtype.Text - IsActive pgtype.Bool - RawEventID pgtype.Text + ID int32 + EventID pgtype.Text + Fi pgtype.Text + RawEventID pgtype.Text + MarketType string + MarketName pgtype.Text + MarketCategory pgtype.Text + MarketID pgtype.Text + Header pgtype.Text + Name pgtype.Text + Handicap pgtype.Text + OddsValue pgtype.Float8 + Section string + Category pgtype.Text + RawOdds []byte + FetchedAt pgtype.Timestamp + Source pgtype.Text + IsActive pgtype.Bool } type Otp struct { diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index fe33a5e..f3e0029 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -38,39 +38,74 @@ func (q *Queries) GetUpcomingEventIDs(ctx context.Context) ([]string, error) { const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec INSERT INTO odds ( - event_id, market_type, header, name, odds_value, handicap, - section, category, market_id, is_active, source, fetched_at, raw_event_id + event_id, + fi, + raw_event_id, + market_type, + market_name, + market_category, + market_id, + header, + name, + handicap, + odds_value, + section, + category, + raw_odds, + is_active, + source, + fetched_at ) VALUES ( - $1, $2, $3, $4, $5, $6, - $7, $8, $9, true, 'b365api', now(), $10 + $1, $2, $3, $4, $5, $6, $7, + $8, $9, $10, $11, $12, $13, $14, + true, 'b365api', now() ) +ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET + odds_value = EXCLUDED.odds_value, + raw_odds = EXCLUDED.raw_odds, + market_type = EXCLUDED.market_type, + market_name = EXCLUDED.market_name, + market_category = EXCLUDED.market_category, + fetched_at = now(), + is_active = true, + source = 'b365api', + fi = EXCLUDED.fi, + raw_event_id = EXCLUDED.raw_event_id ` type InsertNonLiveOddParams struct { - EventID pgtype.Text - MarketType string - Header pgtype.Text - Name pgtype.Text - OddsValue pgtype.Float8 - Handicap pgtype.Text - Section string - Category pgtype.Text - MarketID pgtype.Text - RawEventID pgtype.Text + EventID pgtype.Text + Fi pgtype.Text + RawEventID pgtype.Text + MarketType string + MarketName pgtype.Text + MarketCategory pgtype.Text + MarketID pgtype.Text + Header pgtype.Text + Name pgtype.Text + Handicap pgtype.Text + OddsValue pgtype.Float8 + Section string + Category pgtype.Text + RawOdds []byte } func (q *Queries) InsertNonLiveOdd(ctx context.Context, arg InsertNonLiveOddParams) error { _, err := q.db.Exec(ctx, InsertNonLiveOdd, arg.EventID, + arg.Fi, + arg.RawEventID, arg.MarketType, + arg.MarketName, + arg.MarketCategory, + arg.MarketID, arg.Header, arg.Name, - arg.OddsValue, arg.Handicap, + arg.OddsValue, arg.Section, arg.Category, - arg.MarketID, - arg.RawEventID, + arg.RawOdds, ) return err } diff --git a/go.mod b/go.mod index 02a28b2..7988b31 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.2 github.com/jackc/pgx/v5 v5.7.4 github.com/joho/godotenv v1.5.1 + github.com/robfig/cron/v3 v3.0.1 github.com/swaggo/fiber-swagger v1.3.0 github.com/swaggo/swag v1.16.4 golang.org/x/crypto v0.36.0 @@ -39,7 +40,6 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect diff --git a/internal/domain/odds.go b/internal/domain/odds.go index 0af5dbd..18652f5 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -1,14 +1,17 @@ package domain -type OddsRecord struct { - EventID string - MarketType string - Header string - Name string - OddsValue float64 - Handicap string - Section string - Category string - MarketID string - RawEventID string -} +import ( + "encoding/json" + "time" +) + +type Market struct { + EventID string // 7549892 + FI string // 147543881 + MarketCategory string // Corrected spelling and casing + MarketType string // e.g., "asian_handicap", "goal_line" + MarketName string // e.g., "Asian Handicap" + MarketID string // e.g., "938" + UpdatedAt time.Time // parsed from "updated_at" + Odds []json.RawMessage // oddd is sometimes null +} \ No newline at end of file diff --git a/internal/repository/odds.go b/internal/repository/odds.go index c2ea64f..af20bbb 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -2,63 +2,79 @@ package repository import ( "context" + "encoding/json" "fmt" + "os" + "time" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/jackc/pgx/v5/pgtype" ) -func (s *Store) SaveNonLiveOdd(ctx context.Context, o domain.OddsRecord) error { +func (s *Store) SaveNonLiveOdd(ctx context.Context, m domain.Market) error { + rawOddsBytes, _ := json.Marshal(m.Odds) + params := dbgen.InsertNonLiveOddParams{ - EventID: pgtype.Text{ - String: o.EventID, - Valid: o.EventID != "", - }, - MarketType: o.MarketType, - Header: pgtype.Text{ - String: o.Header, - Valid: o.Header != "", - }, - Name: pgtype.Text{ - String: o.Name, - Valid: o.Name != "", - }, - OddsValue: pgtype.Float8{ - Float64: o.OddsValue, - Valid: true, - }, - Handicap: pgtype.Text{ - String: o.Handicap, - Valid: o.Handicap != "", - }, - Section: o.Section, - Category: pgtype.Text{ - String: o.Category, - Valid: o.Category != "", - }, - MarketID: pgtype.Text{ - String: o.MarketID, - Valid: o.MarketID != "", - }, - RawEventID: pgtype.Text{ - String: o.RawEventID, - Valid: o.RawEventID != "", - }, + EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, + Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""}, + RawEventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, + MarketType: m.MarketType, + MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""}, + MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""}, + MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""}, + Header: pgtype.Text{String: "", Valid: false}, + Name: pgtype.Text{String: "", Valid: false}, + Handicap: pgtype.Text{String: "", Valid: false}, + OddsValue: pgtype.Float8{Float64: 0, Valid: false}, + Section: m.MarketCategory, + Category: pgtype.Text{String: "", Valid: false}, + RawOdds: rawOddsBytes, } err := s.queries.InsertNonLiveOdd(ctx, params) - if err != nil { - fmt.Printf("❌ Failed to insert odd: event_id=%s | market=%s | odds=%.3f | error=%v\n", - o.EventID, o.MarketType, o.OddsValue, err) + fmt.Printf("❌ Failed to insert/upsert market: event_id=%s | market_type=%s | err=%v\n", + m.EventID, m.MarketType, err) + _ = writeFailedMarketLog(m, err) } else { - fmt.Printf("✅ Stored: event_id=%s | market=%s | odds=%.3f\n", o.EventID, o.MarketType, o.OddsValue) + fmt.Printf("✅ Upserted market: event_id=%s | market_type=%s\n", m.EventID, m.MarketType) } return err } +func writeFailedMarketLog(m domain.Market, err error) error { + logDir := "logs" + logFile := logDir + "/failed_markets.log" + + if mkErr := os.MkdirAll(logDir, 0755); mkErr != nil { + return mkErr + } + + f, fileErr := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if fileErr != nil { + return fileErr + } + defer f.Close() + + entry := struct { + Time string `json:"time"` + Error string `json:"error"` + Record domain.Market `json:"record"` + }{ + Time: time.Now().Format(time.RFC3339), + Error: err.Error(), + Record: m, + } + + jsonData, _ := json.MarshalIndent(entry, "", " ") + _, writeErr := f.WriteString(string(jsonData) + "\n\n") + return writeErr +} + + + func (s *Store) GetUpcomingEventIDs(ctx context.Context) ([]string, error) { return s.queries.GetUpcomingEventIDs(ctx) } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 090add4..7562ff0 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -7,7 +7,7 @@ import ( "io" "net/http" "strconv" - "sync" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" @@ -21,132 +21,137 @@ type ServiceImpl struct { func New(token string, store *repository.Store) *ServiceImpl { return &ServiceImpl{token: token, store: store} } + func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { - eventIDs, err := s.store.GetUpcomingEventIDs(ctx) - if err != nil { - return fmt.Errorf("fetch upcoming event IDs: %w", err) - } + sportIDs := []int{1, 13, 78, 18, 91, 16, 17} - type OddsMarket struct { - ID string `json:"id"` - Name string `json:"name"` - Odds []struct { - ID string `json:"id"` - Odds string `json:"odds"` - Header string `json:"header,omitempty"` - Name string `json:"name,omitempty"` - Handicap string `json:"handicap,omitempty"` - } `json:"odds"` - } + for _, sportID := range sportIDs { + upcomingURL := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token) + resp, err := http.Get(upcomingURL) + if err != nil { + fmt.Printf("❌ Failed to fetch upcoming for sport_id=%d: %v\n", sportID, err) + continue + } + defer resp.Body.Close() - type OddsSection struct { - UpdatedAt string `json:"updated_at"` - Sp map[string]OddsMarket `json:"sp"` - } - - type StructuredOddsResponse struct { - Success int `json:"success"` - Results []struct { - EventID string `json:"event_id"` - FI string `json:"FI"` - AsianLines OddsSection `json:"asian_lines"` - Goals OddsSection `json:"goals"` - } `json:"results"` - } - - var wg sync.WaitGroup - sem := make(chan struct{}, 5) - - for _, eventID := range eventIDs { - if eventID == "" || len(eventID) < 5 { + body, _ := io.ReadAll(resp.Body) + var data struct { + Success int `json:"success"` + Results []map[string]interface{} `json:"results"` + } + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { + fmt.Printf("❌ Failed to decode upcoming for sport_id=%d\nRaw: %s\n", sportID, string(body)) continue } - wg.Add(1) - go func(originalID string) { - defer wg.Done() - sem <- struct{}{} - defer func() { <-sem }() + for _, ev := range data.Results { + if getString(ev["type"]) != "EV" { + continue + } + eventID := getString(ev["ID"]) + if eventID == "" { + continue + } - url := fmt.Sprintf("https://api.b365api.com/v1/bet365/odds?token=%s&event_id=%s", s.token, originalID) - resp, err := http.Get(url) + prematchURL := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%s", s.token, eventID) + oddsResp, err := http.Get(prematchURL) if err != nil { - fmt.Printf(" Failed HTTP request for event_id=%s: %v\n", originalID, err) - return + fmt.Printf("❌ Odds fetch failed for event_id=%s: %v\n", eventID, err) + continue } - defer resp.Body.Close() + defer oddsResp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - fmt.Printf(" Failed to read response body for event_id=%s: %v\n", originalID, err) - return + oddsBody, _ := io.ReadAll(oddsResp.Body) + var oddsData struct { + Success int `json:"success"` + Results []struct { + EventID string `json:"event_id"` + FI string `json:"FI"` + AsianLines OddsSection `json:"asian_lines"` + Goals OddsSection `json:"goals"` + } `json:"results"` + } + if err := json.Unmarshal(oddsBody, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { + fmt.Printf("❌ Failed odds decode for event_id=%s\nRaw: %s\n", eventID, string(oddsBody)) + continue } - var data StructuredOddsResponse - if err := json.Unmarshal(body, &data); err != nil { - fmt.Printf(" JSON unmarshal failed for event_id=%s. Response: %s\n", originalID, string(body)) - return + result := oddsData.Results[0] + finalID := result.EventID + if finalID == "" { + finalID = result.FI + } + if finalID == "" { + fmt.Println("⚠️ Skipping event with missing final ID.") + continue } - if data.Success != 1 || len(data.Results) == 0 { - fmt.Printf(" API response error or no results for event_id=%s\nBody: %s\n", originalID, string(body)) - return - } - - result := data.Results[0] - finalEventID := result.EventID - if finalEventID == "" { - finalEventID = result.FI - } - if finalEventID == "" { - fmt.Printf(" Skipping event_id=%s due to missing both event_id and FI\n", originalID) - return - } - - saveOdds := func(sectionName string, section OddsSection) { - for marketType, market := range section.Sp { - for _, odd := range market.Odds { - val, err := strconv.ParseFloat(odd.Odds, 64) - if err != nil { - fmt.Printf(" Skipping invalid odds for market=%s, event_id=%s\n", marketType, finalEventID) - continue - } - - record := domain.OddsRecord{ - EventID: finalEventID, - MarketType: marketType, - Header: odd.Header, - Name: odd.Name, - Handicap: odd.Handicap, - OddsValue: val, - Section: sectionName, - Category: market.ID, - MarketID: odd.ID, - RawEventID: originalID, - } - - fmt.Printf("🟡 Preparing to store: event_id=%s | market=%s | header=%s | name=%s | odds=%.3f\n", - finalEventID, marketType, odd.Header, odd.Name, val) - - err = s.store.SaveNonLiveOdd(ctx, record) - if err != nil { - fmt.Printf("❌ DB save error for market=%s, event_id=%s: %v\nRecord: %+v\n", marketType, finalEventID, err, record) - } else { - fmt.Printf("✅ Stored odd: event_id=%s | market=%s | odds=%.3f\n", finalEventID, marketType, val) - } - } + saveOdds := func(sectionName string, section OddsSection) error { + if len(section.Sp) == 0 { + fmt.Printf("⚠️ No odds in section '%s' for event_id=%s\n", sectionName, finalID) + return nil } - } - - - saveOdds("asian_lines", result.AsianLines) - saveOdds("goals", result.Goals) - }(eventID) + updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64) + updatedAt := time.Unix(updatedAtUnix, 0) + + for marketType, market := range section.Sp { + if len(market.Odds) == 0 { + fmt.Printf("⚠️ No odds for marketType=%s in section=%s\n", marketType, sectionName) + continue + } + + marketRecord := domain.Market{ + EventID: finalID, + FI: result.FI, + MarketCategory: sectionName, + MarketType: marketType, + MarketName: market.Name, + MarketID: market.ID, + UpdatedAt: updatedAt, + Odds: market.Odds, + } + + s.store.SaveNonLiveOdd(ctx, marketRecord) + fmt.Printf("✅ STORED MARKET: event_id=%s | type=%s | name=%s\n", finalID, marketType, market.Name) + } + return nil + } + + if err := saveOdds("asian_lines", result.AsianLines); err != nil { + fmt.Printf("⚠️ Skipping event %s due to asian_lines error: %v\n", finalID, err) + continue + } + if err := saveOdds("goals", result.Goals); err != nil { + fmt.Printf("⚠️ Skipping event %s due to goals error: %v\n", finalID, err) + continue + } + + fmt.Printf("✅ Done storing all odds for event_id=%s\n", finalID) + } } - wg.Wait() fmt.Println("✅ All non-live odds fetched and stored.") return nil } +// Odds structures + +type OddsMarket struct { + ID string `json:"id"` + Name string `json:"name"` + Odds []json.RawMessage `json:"odds"` +} + +type OddsSection struct { + UpdatedAt string `json:"updated_at"` + Sp map[string]OddsMarket `json:"sp"` +} + +// Helper +func getString(v interface{}) string { + if str, ok := v.(string); ok { + return str + } + return "" +} From a2820801331363ebcb907de7edd16ac406d3369f Mon Sep 17 00:00:00 2001 From: OneTap Technologies Date: Fri, 11 Apr 2025 15:12:55 +0300 Subject: [PATCH 10/30] addign odd data --- db/migrations/000001_fortune.up.sql | 25 +--- db/query/odds.sql | 1 + internal/domain/odds.go | 24 +-- internal/repository/odds.go | 89 ++++++++---- internal/services/odds/service.go | 217 ++++++++++++++-------------- 5 files changed, 193 insertions(+), 163 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index ba45316..602a6ae 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -88,33 +88,24 @@ CREATE TABLE events ( CREATE TABLE odds ( id SERIAL PRIMARY KEY, - - -- Core IDs event_id TEXT, - fi TEXT, -- ✅ from Market.FI - raw_event_id TEXT, -- Original event ID if different - - -- Market info - market_type TEXT NOT NULL, -- e.g., "asian_handicap" - market_name TEXT, -- ✅ from Market.MarketName - market_category TEXT, -- ✅ from Market.marketcatagory (like "asian_lines") - market_id TEXT, -- e.g., "938" - - -- Odds detail + fi TEXT, + raw_event_id TEXT, + market_type TEXT NOT NULL, + market_name TEXT, + market_category TEXT, + market_id TEXT, header TEXT, name TEXT, handicap TEXT, odds_value DOUBLE PRECISION, - - -- Meta section TEXT NOT NULL, category TEXT, - raw_odds JSONB, -- ✅ store full odds array here + raw_odds JSONB, fetched_at TIMESTAMP DEFAULT now(), source TEXT DEFAULT 'b365api', is_active BOOLEAN DEFAULT true, - - -- Conflict resolution key UNIQUE (event_id, market_id, header, name, handicap) ); + diff --git a/db/query/odds.sql b/db/query/odds.sql index c1ca21c..acb69d6 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -34,6 +34,7 @@ ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET fi = EXCLUDED.fi, raw_event_id = EXCLUDED.raw_event_id; + -- name: GetUpcomingEventIDs :many SELECT id FROM events WHERE is_live = false; diff --git a/internal/domain/odds.go b/internal/domain/odds.go index 18652f5..521fdb5 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -6,12 +6,18 @@ import ( ) type Market struct { - EventID string // 7549892 - FI string // 147543881 - MarketCategory string // Corrected spelling and casing - MarketType string // e.g., "asian_handicap", "goal_line" - MarketName string // e.g., "Asian Handicap" - MarketID string // e.g., "938" - UpdatedAt time.Time // parsed from "updated_at" - Odds []json.RawMessage // oddd is sometimes null -} \ No newline at end of file + EventID string + FI string + MarketCategory string + MarketType string + MarketName string + MarketID string + UpdatedAt time.Time + Odds []json.RawMessage + + // Optional breakdown (extracted from odds) + Header string // only if processing one odd at a time + Name string + Handicap string + OddsVal float64 +} diff --git a/internal/repository/odds.go b/internal/repository/odds.go index af20bbb..399c13b 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "strconv" "time" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" @@ -12,38 +13,57 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -func (s *Store) SaveNonLiveOdd(ctx context.Context, m domain.Market) error { - rawOddsBytes, _ := json.Marshal(m.Odds) - - params := dbgen.InsertNonLiveOddParams{ - EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, - Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""}, - RawEventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, - MarketType: m.MarketType, - MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""}, - MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""}, - MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""}, - Header: pgtype.Text{String: "", Valid: false}, - Name: pgtype.Text{String: "", Valid: false}, - Handicap: pgtype.Text{String: "", Valid: false}, - OddsValue: pgtype.Float8{Float64: 0, Valid: false}, - Section: m.MarketCategory, - Category: pgtype.Text{String: "", Valid: false}, - RawOdds: rawOddsBytes, +func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { + if len(m.Odds) == 0 { + fmt.Printf("⚠️ Market has no odds: %s (%s)\n", m.MarketType, m.EventID) + return nil } - err := s.queries.InsertNonLiveOdd(ctx, params) - if err != nil { - fmt.Printf("❌ Failed to insert/upsert market: event_id=%s | market_type=%s | err=%v\n", - m.EventID, m.MarketType, err) - _ = writeFailedMarketLog(m, err) - } else { - fmt.Printf("✅ Upserted market: event_id=%s | market_type=%s\n", m.EventID, m.MarketType) - } + for _, raw := range m.Odds { + var item map[string]interface{} + if err := json.Unmarshal(raw, &item); err != nil { + fmt.Printf("❌ Invalid odd JSON for %s (%s): %v\n", m.MarketType, m.EventID, err) + continue + } - return err + header := getString(item["header"]) + name := getString(item["name"]) + handicap := getString(item["handicap"]) + oddsVal := getFloat(item["odds"]) + + // Marshal the full list of odds for reference (if needed) + rawOddsBytes, _ := json.Marshal(m.Odds) + + params := dbgen.InsertNonLiveOddParams{ + EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, + Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""}, + RawEventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, + MarketType: m.MarketType, + MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""}, + MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""}, + MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""}, + Header: pgtype.Text{String: header, Valid: header != ""}, + Name: pgtype.Text{String: name, Valid: name != ""}, + Handicap: pgtype.Text{String: handicap, Valid: handicap != ""}, + OddsValue: pgtype.Float8{Float64: oddsVal, Valid: oddsVal != 0}, + Section: m.MarketCategory, + Category: pgtype.Text{Valid: false}, + RawOdds: rawOddsBytes, + } + + err := s.queries.InsertNonLiveOdd(ctx, params) + if err != nil { + fmt.Printf("❌ Failed to insert odd for market %s (%s): %v\n", m.MarketType, m.EventID, err) + _ = writeFailedMarketLog(m, err) + continue + } + + fmt.Printf("✅ Inserted odd: %s | type=%s | header=%s | name=%s\n", m.EventID, m.MarketType, header, name) + } + return nil } + func writeFailedMarketLog(m domain.Market, err error) error { logDir := "logs" logFile := logDir + "/failed_markets.log" @@ -73,7 +93,22 @@ func writeFailedMarketLog(m domain.Market, err error) error { return writeErr } +func getString(v interface{}) string { + if s, ok := v.(string); ok { + return s + } + return "" +} +func getFloat(v interface{}) float64 { + if s, ok := v.(string); ok { + f, err := strconv.ParseFloat(s, 64) + if err == nil { + return f + } + } + return 0 +} func (s *Store) GetUpcomingEventIDs(ctx context.Context) ([]string, error) { return s.queries.GetUpcomingEventIDs(ctx) diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 7562ff0..b45152c 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -23,124 +23,121 @@ func New(token string, store *repository.Store) *ServiceImpl { } func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { - sportIDs := []int{1, 13, 78, 18, 91, 16, 17} + fmt.Println("🔄 Starting FetchNonLiveOdds...") - for _, sportID := range sportIDs { - upcomingURL := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token) - resp, err := http.Get(upcomingURL) - if err != nil { - fmt.Printf("❌ Failed to fetch upcoming for sport_id=%d: %v\n", sportID, err) - continue - } - defer resp.Body.Close() + sportID := 1 + upcomingURL := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token) + resp, err := http.Get(upcomingURL) + if err != nil { + fmt.Printf("❌ Failed to fetch upcoming: %v\n", err) + return err + } + defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - var data struct { - Success int `json:"success"` - Results []map[string]interface{} `json:"results"` - } - if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - fmt.Printf("❌ Failed to decode upcoming for sport_id=%d\nRaw: %s\n", sportID, string(body)) - continue - } - - for _, ev := range data.Results { - if getString(ev["type"]) != "EV" { - continue - } - eventID := getString(ev["ID"]) - if eventID == "" { - continue - } - - prematchURL := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%s", s.token, eventID) - oddsResp, err := http.Get(prematchURL) - if err != nil { - fmt.Printf("❌ Odds fetch failed for event_id=%s: %v\n", eventID, err) - continue - } - defer oddsResp.Body.Close() - - oddsBody, _ := io.ReadAll(oddsResp.Body) - var oddsData struct { - Success int `json:"success"` - Results []struct { - EventID string `json:"event_id"` - FI string `json:"FI"` - AsianLines OddsSection `json:"asian_lines"` - Goals OddsSection `json:"goals"` - } `json:"results"` - } - if err := json.Unmarshal(oddsBody, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { - fmt.Printf("❌ Failed odds decode for event_id=%s\nRaw: %s\n", eventID, string(oddsBody)) - continue - } - - result := oddsData.Results[0] - finalID := result.EventID - if finalID == "" { - finalID = result.FI - } - if finalID == "" { - fmt.Println("⚠️ Skipping event with missing final ID.") - continue - } - - saveOdds := func(sectionName string, section OddsSection) error { - if len(section.Sp) == 0 { - fmt.Printf("⚠️ No odds in section '%s' for event_id=%s\n", sectionName, finalID) - return nil - } - - updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64) - updatedAt := time.Unix(updatedAtUnix, 0) - - for marketType, market := range section.Sp { - if len(market.Odds) == 0 { - fmt.Printf("⚠️ No odds for marketType=%s in section=%s\n", marketType, sectionName) - continue - } - - marketRecord := domain.Market{ - EventID: finalID, - FI: result.FI, - MarketCategory: sectionName, - MarketType: marketType, - MarketName: market.Name, - MarketID: market.ID, - UpdatedAt: updatedAt, - Odds: market.Odds, - } - - s.store.SaveNonLiveOdd(ctx, marketRecord) - fmt.Printf("✅ STORED MARKET: event_id=%s | type=%s | name=%s\n", finalID, marketType, market.Name) - } - return nil - } - - if err := saveOdds("asian_lines", result.AsianLines); err != nil { - fmt.Printf("⚠️ Skipping event %s due to asian_lines error: %v\n", finalID, err) - continue - } - if err := saveOdds("goals", result.Goals); err != nil { - fmt.Printf("⚠️ Skipping event %s due to goals error: %v\n", finalID, err) - continue - } - - fmt.Printf("✅ Done storing all odds for event_id=%s\n", finalID) - } + body, _ := io.ReadAll(resp.Body) + var upcomingData struct { + Success int `json:"success"` + Results []struct { + ID string `json:"id"` + } `json:"results"` + } + if err := json.Unmarshal(body, &upcomingData); err != nil || upcomingData.Success != 1 { + fmt.Printf("❌ Failed to decode upcoming response\nRaw: %s\n", string(body)) + return err } - fmt.Println("✅ All non-live odds fetched and stored.") + for _, ev := range upcomingData.Results { + eventID := ev.ID + fmt.Printf("📦 Fetching prematch odds for event_id=%s\n", eventID) + prematchURL := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%s", s.token, eventID) + oddsResp, err := http.Get(prematchURL) + if err != nil { + fmt.Printf("❌ Odds fetch failed for event_id=%s: %v\n", eventID, err) + continue + } + defer oddsResp.Body.Close() + + oddsBody, _ := io.ReadAll(oddsResp.Body) + fmt.Printf("📩 Raw odds response for event_id=%s: %.300s...\n", eventID, string(oddsBody)) + + var oddsData struct { + Success int `json:"success"` + Results []struct { + EventID string `json:"event_id"` + FI string `json:"FI"` + Main OddsSection `json:"main"` + } `json:"results"` + } + if err := json.Unmarshal(oddsBody, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { + fmt.Printf("❌ Failed odds decode for event_id=%s\nRaw: %s\n", eventID, string(oddsBody)) + continue + } + + result := oddsData.Results[0] + finalID := result.EventID + if finalID == "" { + finalID = result.FI + } + if finalID == "" { + fmt.Println("⚠️ Skipping event with missing final ID.") + continue + } + + fmt.Printf("🗂 Saving prematch odds for event_id=%s\n", finalID) + s.storeSection(ctx, finalID, result.FI, "main", result.Main) + fmt.Printf("✅ Finished storing prematch odds for event_id=%s\n", finalID) + } + + fmt.Println("✅ All prematch odds fetched and stored.") return nil } +func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section OddsSection) { + fmt.Printf("📂 Processing section '%s' for event_id=%s\n", sectionName, eventID) + if len(section.Sp) == 0 { + fmt.Printf("⚠️ No odds in section '%s' for event_id=%s\n", sectionName, eventID) + return + } + + updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64) + updatedAt := time.Unix(updatedAtUnix, 0) + + for marketType, market := range section.Sp { + fmt.Printf("🔍 Processing market: %s (%s)\n", marketType, market.ID) + if len(market.Odds) == 0 { + fmt.Printf("⚠️ Empty odds for marketType=%s in section=%s\n", marketType, sectionName) + continue + } + + marketRecord := domain.Market{ + EventID: eventID, + FI: fi, + MarketCategory: sectionName, + MarketType: marketType, + MarketName: market.Name, + MarketID: market.ID, + UpdatedAt: updatedAt, + Odds: market.Odds, + } + + fmt.Printf("📦 Saving market to DB: %s (%s)\n", marketType, market.ID) + err := s.store.SaveNonLiveMarket(ctx, marketRecord) + if err != nil { + fmt.Printf("❌ Save failed for market %s (%s): %v\n", marketType, eventID, err) + } else { + fmt.Printf("✅ Successfully stored market: %s (%s)\n", marketType, eventID) + } + } +} + // Odds structures type OddsMarket struct { - ID string `json:"id"` - Name string `json:"name"` - Odds []json.RawMessage `json:"odds"` + ID string `json:"id"` + Name string `json:"name"` + Odds []json.RawMessage `json:"odds"` + Header string `json:"header,omitempty"` + Handicap string `json:"handicap,omitempty"` } type OddsSection struct { @@ -148,10 +145,10 @@ type OddsSection struct { Sp map[string]OddsMarket `json:"sp"` } -// Helper +// Utility func getString(v interface{}) string { if str, ok := v.(string); ok { return str } return "" -} +} \ No newline at end of file From b90fd84aba3765e3cc2fa88aba1559637fb4437a Mon Sep 17 00:00:00 2001 From: OneTap Technologies Date: Fri, 11 Apr 2025 17:04:25 +0300 Subject: [PATCH 11/30] adding prematchodd --- cmd/main.go | 2 +- db/query/odds.sql | 25 ++++- docs/docs.go | 115 ++++++++++++++++++++++- docs/swagger.json | 115 ++++++++++++++++++++++- docs/swagger.yaml | 79 +++++++++++++++- gen/db/odds.sql.go | 56 +++++++++-- internal/domain/odds.go | 26 ++++- internal/repository/odds.go | 52 ++++++++-- internal/services/odds/port.go | 8 +- internal/services/odds/service.go | 51 ++++++---- internal/services/user/service.go | 1 + internal/web_server/app.go | 83 ++++++++-------- internal/web_server/cron.go | 16 ++-- internal/web_server/handlers/prematch.go | 37 ++++++++ internal/web_server/routes.go | 3 + 15 files changed, 566 insertions(+), 103 deletions(-) create mode 100644 internal/web_server/handlers/prematch.go diff --git a/cmd/main.go b/cmd/main.go index 6b945d5..584e821 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -67,7 +67,7 @@ func main() { app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, - }, userSvc) + }, userSvc, oddsSvc) logger.Info("Starting server", "port", cfg.Port) if err := app.Run(); err != nil { diff --git a/db/query/odds.sql b/db/query/odds.sql index acb69d6..07e1c99 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -35,6 +35,25 @@ ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET raw_event_id = EXCLUDED.raw_event_id; --- name: GetUpcomingEventIDs :many -SELECT id FROM events -WHERE is_live = false; +-- name: GetPrematchOdds :many +SELECT + id, + event_id, + fi, + raw_event_id, + market_type, + market_name, + market_category, + market_id, + header, + name, + handicap, + odds_value, + section, + category, + raw_odds, + fetched_at, + source, + is_active +FROM odds +WHERE event_id = $1 AND is_active = true AND source = 'b365api'; \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 6625028..6448575 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -180,6 +180,53 @@ const docTemplate = `{ } } }, + "/prematch/odds/{event_id}": { + "get": { + "description": "Retrieve prematch odds for a specific event by event ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve prematch odds for an event", + "parameters": [ + { + "type": "string", + "description": "Event ID", + "name": "event_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Odd" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/user/checkPhoneEmailExist": { "post": { "description": "Check if phone number or email exist", @@ -452,20 +499,80 @@ const docTemplate = `{ } }, "definitions": { + "domain.Odd": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "event_id": { + "type": "string" + }, + "fetched_at": { + "type": "string" + }, + "fi": { + "type": "string" + }, + "handicap": { + "type": "string" + }, + "header": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "market_category": { + "type": "string" + }, + "market_id": { + "type": "string" + }, + "market_name": { + "type": "string" + }, + "market_type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "odds_value": { + "type": "number" + }, + "raw_event_id": { + "type": "string" + }, + "raw_odds": { + "type": "array", + "items": {} + }, + "section": { + "type": "string" + }, + "source": { + "type": "string" + } + } + }, "domain.Role": { "type": "string", "enum": [ - "admin", - "customer", "super_admin", + "admin", "branch_manager", + "customer", "cashier" ], "x-enum-varnames": [ - "RoleAdmin", - "RoleCustomer", "RoleSuperAdmin", + "RoleAdmin", "RoleBranchManager", + "RoleCustomer", "RoleCashier" ] }, diff --git a/docs/swagger.json b/docs/swagger.json index 76ae6c5..414256f 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -172,6 +172,53 @@ } } }, + "/prematch/odds/{event_id}": { + "get": { + "description": "Retrieve prematch odds for a specific event by event ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve prematch odds for an event", + "parameters": [ + { + "type": "string", + "description": "Event ID", + "name": "event_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Odd" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/user/checkPhoneEmailExist": { "post": { "description": "Check if phone number or email exist", @@ -444,20 +491,80 @@ } }, "definitions": { + "domain.Odd": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "event_id": { + "type": "string" + }, + "fetched_at": { + "type": "string" + }, + "fi": { + "type": "string" + }, + "handicap": { + "type": "string" + }, + "header": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "market_category": { + "type": "string" + }, + "market_id": { + "type": "string" + }, + "market_name": { + "type": "string" + }, + "market_type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "odds_value": { + "type": "number" + }, + "raw_event_id": { + "type": "string" + }, + "raw_odds": { + "type": "array", + "items": {} + }, + "section": { + "type": "string" + }, + "source": { + "type": "string" + } + } + }, "domain.Role": { "type": "string", "enum": [ - "admin", - "customer", "super_admin", + "admin", "branch_manager", + "customer", "cashier" ], "x-enum-varnames": [ - "RoleAdmin", - "RoleCustomer", "RoleSuperAdmin", + "RoleAdmin", "RoleBranchManager", + "RoleCustomer", "RoleCashier" ] }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 166d41d..00333d0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,17 +1,57 @@ definitions: + domain.Odd: + properties: + category: + type: string + event_id: + type: string + fetched_at: + type: string + fi: + type: string + handicap: + type: string + header: + type: string + id: + type: integer + is_active: + type: boolean + market_category: + type: string + market_id: + type: string + market_name: + type: string + market_type: + type: string + name: + type: string + odds_value: + type: number + raw_event_id: + type: string + raw_odds: + items: {} + type: array + section: + type: string + source: + type: string + type: object domain.Role: enum: - - admin - - customer - super_admin + - admin - branch_manager + - customer - cashier type: string x-enum-varnames: - - RoleAdmin - - RoleCustomer - RoleSuperAdmin + - RoleAdmin - RoleBranchManager + - RoleCustomer - RoleCashier handlers.CheckPhoneEmailExistReq: properties: @@ -275,6 +315,37 @@ paths: summary: Refresh token tags: - auth + /prematch/odds/{event_id}: + get: + consumes: + - application/json + description: Retrieve prematch odds for a specific event by event ID + parameters: + - description: Event ID + in: path + name: event_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.Odd' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve prematch odds for an event + tags: + - prematch /user/checkPhoneEmailExist: post: consumes: diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index f3e0029..003be80 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -11,24 +11,62 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -const GetUpcomingEventIDs = `-- name: GetUpcomingEventIDs :many -SELECT id FROM events -WHERE is_live = false +const GetPrematchOdds = `-- name: GetPrematchOdds :many +SELECT + id, + event_id, + fi, + raw_event_id, + market_type, + market_name, + market_category, + market_id, + header, + name, + handicap, + odds_value, + section, + category, + raw_odds, + fetched_at, + source, + is_active +FROM odds +WHERE event_id = $1 AND is_active = true AND source = 'b365api' ` -func (q *Queries) GetUpcomingEventIDs(ctx context.Context) ([]string, error) { - rows, err := q.db.Query(ctx, GetUpcomingEventIDs) +func (q *Queries) GetPrematchOdds(ctx context.Context, eventID pgtype.Text) ([]Odd, error) { + rows, err := q.db.Query(ctx, GetPrematchOdds, eventID) if err != nil { return nil, err } defer rows.Close() - var items []string + var items []Odd for rows.Next() { - var id string - if err := rows.Scan(&id); err != nil { + var i Odd + if err := rows.Scan( + &i.ID, + &i.EventID, + &i.Fi, + &i.RawEventID, + &i.MarketType, + &i.MarketName, + &i.MarketCategory, + &i.MarketID, + &i.Header, + &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, id) + items = append(items, i) } if err := rows.Err(); err != nil { return nil, err diff --git a/internal/domain/odds.go b/internal/domain/odds.go index 521fdb5..418b33d 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -3,7 +3,9 @@ package domain import ( "encoding/json" "time" + ) +type RawMessage interface{} // Change from json.RawMessage to interface{} type Market struct { EventID string @@ -15,9 +17,29 @@ type Market struct { UpdatedAt time.Time Odds []json.RawMessage - // Optional breakdown (extracted from odds) - Header string // only if processing one odd at a time + Header string Name string Handicap string OddsVal float64 } + +type Odd struct { + ID int64 `json:"id"` + EventID string `json:"event_id"` + Fi string `json:"fi"` + RawEventID string `json:"raw_event_id"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID string `json:"market_id"` + Header string `json:"header"` + Name string `json:"name"` + Handicap string `json:"handicap"` + OddsValue float64 `json:"odds_value"` + Section string `json:"section"` + Category string `json:"category"` + RawOdds []RawMessage `json:"raw_odds"` + FetchedAt time.Time `json:"fetched_at"` + Source string `json:"source"` + IsActive bool `json:"is_active"` +} \ No newline at end of file diff --git a/internal/repository/odds.go b/internal/repository/odds.go index 399c13b..a21c729 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -15,14 +15,14 @@ import ( func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { if len(m.Odds) == 0 { - fmt.Printf("⚠️ Market has no odds: %s (%s)\n", m.MarketType, m.EventID) + fmt.Printf(" Market has no odds: %s (%s)\n", m.MarketType, m.EventID) return nil } for _, raw := range m.Odds { var item map[string]interface{} if err := json.Unmarshal(raw, &item); err != nil { - fmt.Printf("❌ Invalid odd JSON for %s (%s): %v\n", m.MarketType, m.EventID, err) + fmt.Printf(" Invalid odd JSON for %s (%s): %v\n", m.MarketType, m.EventID, err) continue } @@ -31,7 +31,6 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { handicap := getString(item["handicap"]) oddsVal := getFloat(item["odds"]) - // Marshal the full list of odds for reference (if needed) rawOddsBytes, _ := json.Marshal(m.Odds) params := dbgen.InsertNonLiveOddParams{ @@ -53,12 +52,12 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { err := s.queries.InsertNonLiveOdd(ctx, params) if err != nil { - fmt.Printf("❌ Failed to insert odd for market %s (%s): %v\n", m.MarketType, m.EventID, err) + fmt.Printf(" Failed to insert odd for market %s (%s): %v\n", m.MarketType, m.EventID, err) _ = writeFailedMarketLog(m, err) continue } - fmt.Printf("✅ Inserted odd: %s | type=%s | header=%s | name=%s\n", m.EventID, m.MarketType, header, name) + fmt.Printf("Inserted odd: %s | type=%s | header=%s | name=%s\n", m.EventID, m.MarketType, header, name) } return nil } @@ -110,6 +109,43 @@ func getFloat(v interface{}) float64 { return 0 } -func (s *Store) GetUpcomingEventIDs(ctx context.Context) ([]string, error) { - return s.queries.GetUpcomingEventIDs(ctx) -} +func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { + eventIDParam := pgtype.Text{String: eventID, Valid: eventID != ""} + + odds, err := s.queries.GetPrematchOdds(ctx, eventIDParam) + if err != nil { + return nil, err + } + + domainOdds := make([]domain.Odd, len(odds)) + for i, odd := range odds { + domainOdds[i] = domain.Odd{ + ID: int64(odd.ID), // Cast int32 to int64 + EventID: odd.EventID.String, // Extract the String value + Fi: odd.Fi.String, // Extract the String value + RawEventID: odd.RawEventID.String, // Extract the String value + MarketType: odd.MarketType, // Direct assignment + MarketName: odd.MarketName.String, // Extract the String value + MarketCategory: odd.MarketCategory.String, // Extract the String value + MarketID: odd.MarketID.String, // Extract the String value + Header: odd.Header.String, // Extract the String value + Name: odd.Name.String, // Extract the String value + Handicap: odd.Handicap.String, // Extract the String value + OddsValue: odd.OddsValue.Float64, // Extract the Float64 value + Section: odd.Section, // Direct assignment + Category: odd.Category.String, // Extract the String value + RawOdds: func() []domain.RawMessage { + var rawOdds []domain.RawMessage + if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { + rawOdds = nil + } + return rawOdds + }(), + FetchedAt: odd.FetchedAt.Time, // Extract the Time value + Source: odd.Source.String, // Extract the String value + IsActive: odd.IsActive.Bool, // Extract the Bool value + } + } + + return domainOdds, nil +} \ No newline at end of file diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index d95367e..d50a8af 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -1,8 +1,14 @@ package odds -import "context" +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) type Service interface { FetchNonLiveOdds(ctx context.Context) error + GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) + } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index b45152c..2f3245d 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -23,13 +23,13 @@ func New(token string, store *repository.Store) *ServiceImpl { } func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { - fmt.Println("🔄 Starting FetchNonLiveOdds...") + fmt.Println("Starting FetchNonLiveOdds...") sportID := 1 upcomingURL := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token) resp, err := http.Get(upcomingURL) if err != nil { - fmt.Printf("❌ Failed to fetch upcoming: %v\n", err) + fmt.Printf("Failed to fetch upcoming: %v\n", err) return err } defer resp.Body.Close() @@ -42,23 +42,23 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { } `json:"results"` } if err := json.Unmarshal(body, &upcomingData); err != nil || upcomingData.Success != 1 { - fmt.Printf("❌ Failed to decode upcoming response\nRaw: %s\n", string(body)) + fmt.Printf("Failed to decode upcoming response\nRaw: %s\n", string(body)) return err } for _, ev := range upcomingData.Results { eventID := ev.ID - fmt.Printf("📦 Fetching prematch odds for event_id=%s\n", eventID) + fmt.Printf("Fetching prematch odds for event_id=%s\n", eventID) prematchURL := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%s", s.token, eventID) oddsResp, err := http.Get(prematchURL) if err != nil { - fmt.Printf("❌ Odds fetch failed for event_id=%s: %v\n", eventID, err) + fmt.Printf(" Odds fetch failed for event_id=%s: %v\n", eventID, err) continue } defer oddsResp.Body.Close() oddsBody, _ := io.ReadAll(oddsResp.Body) - fmt.Printf("📩 Raw odds response for event_id=%s: %.300s...\n", eventID, string(oddsBody)) + fmt.Printf(" Raw odds response for event_id=%s: %.300s...\n", eventID, string(oddsBody)) var oddsData struct { Success int `json:"success"` @@ -69,7 +69,7 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { } `json:"results"` } if err := json.Unmarshal(oddsBody, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { - fmt.Printf("❌ Failed odds decode for event_id=%s\nRaw: %s\n", eventID, string(oddsBody)) + fmt.Printf(" Failed odds decode for event_id=%s\nRaw: %s\n", eventID, string(oddsBody)) continue } @@ -79,23 +79,23 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { finalID = result.FI } if finalID == "" { - fmt.Println("⚠️ Skipping event with missing final ID.") + fmt.Println(" Skipping event with missing final ID.") continue } fmt.Printf("🗂 Saving prematch odds for event_id=%s\n", finalID) s.storeSection(ctx, finalID, result.FI, "main", result.Main) - fmt.Printf("✅ Finished storing prematch odds for event_id=%s\n", finalID) + fmt.Printf(" Finished storing prematch odds for event_id=%s\n", finalID) } - fmt.Println("✅ All prematch odds fetched and stored.") + fmt.Println(" All prematch odds fetched and stored.") return nil } func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section OddsSection) { - fmt.Printf("📂 Processing section '%s' for event_id=%s\n", sectionName, eventID) + fmt.Printf(" Processing section '%s' for event_id=%s\n", sectionName, eventID) if len(section.Sp) == 0 { - fmt.Printf("⚠️ No odds in section '%s' for event_id=%s\n", sectionName, eventID) + fmt.Printf(" No odds in section '%s' for event_id=%s\n", sectionName, eventID) return } @@ -103,9 +103,9 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName updatedAt := time.Unix(updatedAtUnix, 0) for marketType, market := range section.Sp { - fmt.Printf("🔍 Processing market: %s (%s)\n", marketType, market.ID) + fmt.Printf(" Processing market: %s (%s)\n", marketType, market.ID) if len(market.Odds) == 0 { - fmt.Printf("⚠️ Empty odds for marketType=%s in section=%s\n", marketType, sectionName) + fmt.Printf(" Empty odds for marketType=%s in section=%s\n", marketType, sectionName) continue } @@ -120,17 +120,16 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName Odds: market.Odds, } - fmt.Printf("📦 Saving market to DB: %s (%s)\n", marketType, market.ID) + fmt.Printf(" Saving market to DB: %s (%s)\n", marketType, market.ID) err := s.store.SaveNonLiveMarket(ctx, marketRecord) if err != nil { - fmt.Printf("❌ Save failed for market %s (%s): %v\n", marketType, eventID, err) + fmt.Printf(" Save failed for market %s (%s): %v\n", marketType, eventID, err) } else { - fmt.Printf("✅ Successfully stored market: %s (%s)\n", marketType, eventID) + fmt.Printf(" Successfully stored market: %s (%s)\n", marketType, eventID) } } } -// Odds structures type OddsMarket struct { ID string `json:"id"` @@ -145,10 +144,22 @@ type OddsSection struct { Sp map[string]OddsMarket `json:"sp"` } -// Utility func getString(v interface{}) string { if str, ok := v.(string); ok { return str } return "" -} \ No newline at end of file +} + +func (s *ServiceImpl) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { + fmt.Printf("Retrieving prematch odds for event_id=%s\n", eventID) + + odds, err := s.store.GetPrematchOdds(ctx, eventID) + if err != nil { + fmt.Printf(" Failed to retrieve odds for event_id=%s: %v\n", eventID, err) + return nil, err + } + + fmt.Printf(" Retrieved %d odds entries for event_id=%s\n", len(odds), eventID) + return odds, nil +} diff --git a/internal/services/user/service.go b/internal/services/user/service.go index cfa93fd..17a7820 100644 --- a/internal/services/user/service.go +++ b/internal/services/user/service.go @@ -13,6 +13,7 @@ type Service struct { otpStore OtpStore smsGateway SmsGateway emailGateway EmailGateway + } func NewService( diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 2ebd22e..4149f6b 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -1,55 +1,60 @@ package httpserver import ( - "fmt" - "log/slog" + "fmt" + "log/slog" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" - jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" - customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" - "github.com/bytedance/sonic" - "github.com/gofiber/fiber/v2" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" + jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" + customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" + + "github.com/bytedance/sonic" + "github.com/gofiber/fiber/v2" ) type App struct { - fiber *fiber.App - logger *slog.Logger - port int - authSvc *authentication.Service - userSvc *user.Service - validator *customvalidator.CustomValidator - JwtConfig jwtutil.JwtConfig + fiber *fiber.App + logger *slog.Logger + port int + authSvc *authentication.Service + userSvc *user.Service + validator *customvalidator.CustomValidator + JwtConfig jwtutil.JwtConfig + prematchSvc *odds.ServiceImpl } func NewApp( - port int, validator *customvalidator.CustomValidator, - authSvc *authentication.Service, - logger *slog.Logger, - JwtConfig jwtutil.JwtConfig, - userSvc *user.Service, + port int, validator *customvalidator.CustomValidator, + authSvc *authentication.Service, + logger *slog.Logger, + JwtConfig jwtutil.JwtConfig, + userSvc *user.Service, + prematchSvc *odds.ServiceImpl, ) *App { - app := fiber.New(fiber.Config{ - CaseSensitive: true, - DisableHeaderNormalizing: true, - JSONEncoder: sonic.Marshal, - JSONDecoder: sonic.Unmarshal, - }) - s := &App{ - fiber: app, - port: port, - authSvc: authSvc, - validator: validator, - logger: logger, - JwtConfig: JwtConfig, - userSvc: userSvc, - } + app := fiber.New(fiber.Config{ + CaseSensitive: true, + DisableHeaderNormalizing: true, + JSONEncoder: sonic.Marshal, + JSONDecoder: sonic.Unmarshal, + }) + s := &App{ + fiber: app, + port: port, + authSvc: authSvc, + validator: validator, + logger: logger, + JwtConfig: JwtConfig, + userSvc: userSvc, + prematchSvc: prematchSvc, + } - s.initAppRoutes() + s.initAppRoutes() - return s + return s } func (a *App) Run() error { - return a.fiber.Listen(fmt.Sprintf(":%d", a.port)) -} + return a.fiber.Listen(fmt.Sprintf(":%d", a.port)) +} \ No newline at end of file diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index e895b6e..68ddcf2 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -24,14 +24,14 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } }, }, - // { - // spec: "*/5 * * * * *", // Every 5 seconds - // task: func() { - // if err := eventService.FetchLiveEvents(context.Background()); err != nil { - // log.Printf(" FetchLiveEvents error: %v", err) - // } - // }, - // }, + { + spec: "*/5 * * * * *", // Every 5 seconds + task: func() { + if err := eventService.FetchLiveEvents(context.Background()); err != nil { + log.Printf(" FetchLiveEvents error: %v", err) + } + }, + }, { spec: "*/5 * * * * *", // Every 5 seconds task: func() { diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go new file mode 100644 index 0000000..cc48b10 --- /dev/null +++ b/internal/web_server/handlers/prematch.go @@ -0,0 +1,37 @@ +package handlers + +import ( + "github.com/gofiber/fiber/v2" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + "log/slog" +) + +// GetPrematchOdds godoc +// @Summary Retrieve prematch odds for an event +// @Description Retrieve prematch odds for a specific event by event ID +// @Tags prematch +// @Accept json +// @Produce json +// @Param event_id path string true "Event ID" +// @Success 200 {array} domain.Odd +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /prematch/odds/{event_id} [get] +func GetPrematchOdds(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { + return func(c *fiber.Ctx) error { + eventID := c.Params("event_id") + if eventID == "" { + logger.Error("GetPrematchOdds failed: missing event_id") + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing event_id", nil, nil) + } + + odds, err := prematchSvc.GetPrematchOdds(c.Context(), eventID) + if err != nil { + logger.Error("GetPrematchOdds failed", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve odds", nil, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) + } +} \ No newline at end of file diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index c30622d..22ebb93 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -26,6 +26,9 @@ func (a *App) initAppRoutes() { a.fiber.Post("/user/sendRegisterCode", handlers.SendRegisterCode(a.logger, a.userSvc, a.validator)) a.fiber.Post("/user/checkPhoneEmailExist", handlers.CheckPhoneEmailExist(a.logger, a.userSvc, a.validator)) a.fiber.Get("/user/profile", a.authMiddleware, handlers.UserProfile(a.logger, a.userSvc)) + + a.fiber.Get("/prematch/odds/:event_id", handlers.GetPrematchOdds(a.logger, a.prematchSvc)) + // Swagger a.fiber.Get("/swagger/*", fiberSwagger.WrapHandler) } From 1b0a068a02d436517154b06eb4ab7b42899b3f67 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Fri, 11 Apr 2025 21:46:48 +0300 Subject: [PATCH 12/30] major fixes while integrating --- db/migrations/000001_fortune.up.sql | 88 ++++- db/query/bet.sql | 45 ++- db/query/branch.sql | 98 +++-- db/query/ticket.sql | 27 +- db/query/user.sql | 111 ++++-- db/query/wallet.sql | 72 ++-- docs/docs.go | 363 +++++++++++++++--- docs/swagger.json | 363 +++++++++++++++--- docs/swagger.yaml | 244 ++++++++++-- gen/db/bet.sql.go | 129 ++++++- gen/db/branch.sql.go | 198 +++++++++- gen/db/copyfrom.go | 78 ++++ gen/db/db.go | 1 + gen/db/models.go | 46 +++ gen/db/ticket.sql.go | 74 +++- gen/db/user.sql.go | 103 ++++- gen/db/wallet.sql.go | 108 +++++- internal/domain/bet.go | 30 +- internal/domain/common.go | 2 + internal/domain/ticket.go | 22 +- internal/domain/user.go | 1 - internal/domain/wallet.go | 45 ++- internal/repository/bet.go | 85 +++- internal/repository/branch.go | 27 ++ internal/repository/ticket.go | 57 ++- internal/repository/user.go | 76 +++- internal/repository/wallet.go | 25 ++ internal/services/bet/port.go | 6 +- internal/services/bet/service.go | 13 +- internal/services/branch/port.go | 11 +- internal/services/branch/service.go | 13 + internal/services/ticket/port.go | 5 +- internal/services/ticket/service.go | 9 +- internal/services/user/direct.go | 23 +- internal/services/user/port.go | 4 +- internal/services/wallet/port.go | 2 +- internal/services/wallet/transfer.go | 33 ++ internal/services/wallet/wallet.go | 5 +- internal/web_server/handlers/bet_handler.go | 156 ++++++-- .../web_server/handlers/branch_handler.go | 76 ++++ internal/web_server/handlers/cashier.go | 88 +++-- internal/web_server/handlers/manager.go | 2 - .../web_server/handlers/ticket_handler.go | 44 ++- .../handlers/transaction_handler.go | 3 + .../web_server/handlers/transfer_handler.go | 162 +++++++- .../web_server/handlers/wallet_handler.go | 56 +++ internal/web_server/routes.go | 12 +- sqlc.yaml | 8 + 48 files changed, 2811 insertions(+), 438 deletions(-) create mode 100644 gen/db/copyfrom.go diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 982681a..830ad51 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -69,20 +69,26 @@ CREATE TABLE IF NOT EXISTS bet_outcomes ( id BIGSERIAL PRIMARY KEY, bet_id BIGINT NOT NULL, event_id bigint not null, - odd_id BIGINT NOT NULL, + odd_id BIGINT NOT NULL ); CREATE TABLE IF NOT EXISTS ticket_outcomes ( id BIGSERIAL PRIMARY KEY, ticket_id BIGINT NOT NULL, event_id bigint not null, - odd_id BIGINT NOT NULL, + odd_id BIGINT NOT NULL ); -ALTER TABLE bets -ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id); -ALTER TABLE bets -ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id); -ALTER TABLE bet_outcomes -ADD CONSTRAINT fk_bet_outcomes_bet FOREIGN KEY (bet_id) REFERENCES bets(id); +CREATE VIEW bet_with_outcomes AS +SELECT bets.*, + JSON_AGG(bet_outcomes.*) AS outcomes +FROM bets + LEFT JOIN bet_outcomes ON bets.id = bet_outcomes.bet_id +GROUP BY bets.id; +CREATE VIEW ticket_with_outcomes AS +SELECT tickets.*, + JSON_AGG(ticket_outcomes.*) AS outcomes +FROM tickets + LEFT JOIN ticket_outcomes ON tickets.id = ticket_outcomes.ticket_id +GROUP BY tickets.id; CREATE TABLE IF NOT EXISTS wallets ( id BIGSERIAL PRIMARY KEY, balance BIGINT NOT NULL DEFAULT 0, @@ -164,6 +170,48 @@ CREATE TABLE IF NOT EXISTS branch_operations ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); +CREATE TABLE IF NOT EXISTS branch_cashiers ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + branch_id BIGINT NOT NULL, + UNIQUE(user_id, branch_id) +); +ALTER TABLE refresh_tokens +ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id); +ALTER TABLE bets +ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id), + ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id); +ALTER TABLE bet_outcomes +ADD CONSTRAINT fk_bet_outcomes_bets FOREIGN KEY (bet_id) REFERENCES bets(id), + ADD CONSTRAINT fk_bet_outcomes_events FOREIGN KEY (event_id) REFERENCES supported_operations(id), + ADD CONSTRAINT fk_bet_outcomes_odds FOREIGN KEY (odd_id) REFERENCES supported_operations(id); +ALTER TABLE ticket_outcomes +ADD CONSTRAINT fk_ticket_outcomes_tickets FOREIGN KEY (ticket_id) REFERENCES tickets(id), + ADD CONSTRAINT fk_ticket_outcomes_events FOREIGN KEY (event_id) REFERENCES supported_operations(id), + ADD CONSTRAINT fk_ticket_outcomes_odds FOREIGN KEY (odd_id) REFERENCES supported_operations(id); +ALTER TABLE wallets +ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id); +ALTER TABLE customer_wallets +ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id), + ADD CONSTRAINT fk_customer_wallets_regular_wallet FOREIGN KEY (regular_wallet_id) REFERENCES wallets(id), + ADD CONSTRAINT fk_customer_wallets_static_wallet FOREIGN KEY (static_wallet_id) REFERENCES wallets(id); +ALTER TABLE wallet_transfer +ADD CONSTRAINT fk_wallet_transfer_receiver_wallet FOREIGN KEY (receiver_wallet_id) REFERENCES wallets(id), + ADD CONSTRAINT fk_wallet_transfer_sender_wallet FOREIGN KEY (sender_wallet_id) REFERENCES wallets(id), + ADD CONSTRAINT fk_wallet_transfer_cashier FOREIGN KEY (cashier_id) REFERENCES users(id); +ALTER TABLE transactions +ADD CONSTRAINT fk_transactions_branches FOREIGN KEY (branch_id) REFERENCES branches(id), + ADD CONSTRAINT fk_transactions_cashiers FOREIGN KEY (cashier_id) REFERENCES users(id), + ADD CONSTRAINT fk_transactions_bets FOREIGN KEY (bet_id) REFERENCES bets(id); +ALTER TABLE branches +ADD CONSTRAINT fk_branches_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id), + ADD CONSTRAINT fk_branches_manager FOREIGN KEY (branch_manager_id) REFERENCES users(id); +ALTER TABLE branch_operations +ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERENCES supported_operations(id), + ADD CONSTRAINT fk_branch_operations_branches FOREIGN KEY (branch_id) REFERENCES branches(id); +ALTER TABLE branch_cashiers +ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id), + ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id); ----------------------------------------------seed data------------------------------------------------------------- -------------------------------------- DO NOT USE IN PRODUCTION------------------------------------------------- CREATE EXTENSION IF NOT EXISTS pgcrypto; @@ -215,7 +263,7 @@ VALUES ( 'cybersamt@gmail.com', NULL, crypt('password@123', gen_salt('bf'))::bytea, - 'cashier', + 'super_admin', TRUE, FALSE, CURRENT_TIMESTAMP, @@ -226,4 +274,24 @@ VALUES ( INSERT INTO supported_operations (name, description) VALUES ('SportBook', 'Sportbook operations'), ('Virtual', 'Virtual operations'), - ('GameZone', 'GameZone operations'); \ No newline at end of file + ('GameZone', 'GameZone operations'); +INSERT INTO wallets ( + balance, + is_withdraw, + is_bettable, + is_transferable, + user_id, + is_active, + created_at, + updated_at + ) +VALUES ( + 10000, + TRUE, + TRUE, + TRUE, + 1, + TRUE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ); \ No newline at end of file diff --git a/db/query/bet.sql b/db/query/bet.sql index 2d1d098..01230b2 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -1,16 +1,43 @@ -- name: CreateBet :one -INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id, is_shop_bet, cashout_id) +INSERT INTO bets ( + amount, + total_odds, + status, + full_name, + phone_number, + branch_id, + user_id, + is_shop_bet, + cashout_id + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *; - +-- name: CreateBetOutcome :copyfrom +INSERT INTO bet_outcomes (bet_id, event_id, odd_id) +VALUES ($1, $2, $3); -- name: GetAllBets :many -SELECT * FROM bets; - +SELECT * +FROM bet_with_outcomes; -- name: GetBetByID :one -SELECT * FROM bets WHERE id = $1; - +SELECT * +FROM bet_with_outcomes +WHERE id = $1; +-- name: GetBetByCashoutID :many +SELECT +FROM bet_with_outcomes +WHERE cashout_id = $1; +-- name: GetBetByBranchID :many +SELECT * +FROM bet_with_outcomes +WHERE branch_id = $1; -- name: UpdateCashOut :exec -UPDATE bets SET cashed_out = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1; - +UPDATE bets +SET cashed_out = $2, + updated_at = CURRENT_TIMESTAMP +WHERE id = $1; -- name: DeleteBet :exec -DELETE FROM bets WHERE id = $1; +DELETE FROM bets +WHERE id = $1; +-- name: DeleteBetOutcome :exec +DELETE FROM bet_outcomes +WHERE bet_id = $1; \ No newline at end of file diff --git a/db/query/branch.sql b/db/query/branch.sql index 97b374e..041d0ef 100644 --- a/db/query/branch.sql +++ b/db/query/branch.sql @@ -1,42 +1,86 @@ - -- name: CreateBranch :one -INSERT INTO branches (name, location, wallet_id, branch_manager_id, company_id, is_self_owned) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; - +INSERT INTO branches ( + name, + location, + wallet_id, + branch_manager_id, + company_id, + is_self_owned + ) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING *; -- name: CreateSupportedOperation :one -INSERT INTO supported_operations (name, description) VALUES ($1, $2) RETURNING *; - +INSERT INTO supported_operations (name, description) +VALUES ($1, $2) +RETURNING *; -- name: CreateBranchOperation :one -INSERT INTO branch_operations (operation_id, branch_id) VALUES ($1, $2) RETURNING *; - +INSERT INTO branch_operations (operation_id, branch_id) +VALUES ($1, $2) +RETURNING *; +-- name: CreateBranchCashier :one +INSERT INTO branch_cashiers (user_id, branch_id) +VALUES ($1, $2) +RETURNING *; -- name: GetAllBranches :many -SELECT * FROM branch_details; +SELECT * +FROM branch_details; -- name: GetBranchByID :one -SELECT * FROM branch_details WHERE id = $1; - +SELECT * +FROM branch_details +WHERE id = $1; -- name: GetBranchByCompanyID :many -SELECT * FROM branch_details WHERE company_id = $1; - +SELECT * +FROM branch_details +WHERE company_id = $1; -- name: GetBranchByManagerID :many -SELECT * FROM branch_details WHERE branch_manager_id = $1; - +SELECT * +FROM branch_details +WHERE branch_manager_id = $1; -- name: SearchBranchByName :many -SELECT * FROM branch_details WHERE name ILIKE '%' || $1 || '%'; - +SELECT * +FROM branch_details +WHERE name ILIKE '%' || $1 || '%'; -- name: GetAllSupportedOperations :many -SELECT * FROM supported_operations; - +SELECT * +FROM supported_operations; -- name: GetBranchOperations :many -SELECT branch_operations.*, supported_operations.name, supported_operations.description -FROM branch_operations -JOIN supported_operations ON branch_operations.operation_id = supported_operations.id +SELECT branch_operations.*, + supported_operations.name, + supported_operations.description +FROM branch_operations + JOIN supported_operations ON branch_operations.operation_id = supported_operations.id WHERE branch_operations.branch_id = $1; - +-- name: GetBranchByCashier :one +SELECT branches.* +FROM branch_cashiers + JOIN branches ON branch_cashiers.branch_id = branches.id +WHERE branch_cashiers.user_id = $1; +-- name: GetCashiersByBranch :many +SELECT users.* +FROM branch_cashiers + JOIN users ON branch_cashiers.user_id = users.id +WHERE branch_cashiers.branch_id = $1; +-- name: GetAllCashiers :many +SELECT users.* +FROM branch_cashiers + JOIN users ON branch_cashiers.user_id = users.id; -- name: UpdateBranch :one -UPDATE branches SET name = $1, location = $2, branch_manager_id = $3, company_id = $4, is_self_owned = $5 WHERE id = $6 RETURNING *; - +UPDATE branches +SET name = $1, + location = $2, + branch_manager_id = $3, + company_id = $4, + is_self_owned = $5 +WHERE id = $6 +RETURNING *; -- name: DeleteBranch :exec -DELETE FROM branches WHERE id = $1; - +DELETE FROM branches +WHERE id = $1; -- name: DeleteBranchOperation :exec -DELETE FROM branch_operations WHERE operation_id = $1 AND branch_id = $2; +DELETE FROM branch_operations +WHERE operation_id = $1 + AND branch_id = $2; +-- name: DeleteBranchCashier :exec +DELETE FROM branch_cashiers +WHERE user_id = $1; \ No newline at end of file diff --git a/db/query/ticket.sql b/db/query/ticket.sql index 04be763..debcb48 100644 --- a/db/query/ticket.sql +++ b/db/query/ticket.sql @@ -2,15 +2,26 @@ INSERT INTO tickets (amount, total_odds) VALUES ($1, $2) RETURNING *; - +-- name: CreateTicketOutcome :copyfrom +INSERT INTO ticket_outcomes (ticket_id, event_id, odd_id) +VALUES ($1, $2, $3); -- name: GetAllTickets :many -SELECT * FROM tickets; - +SELECT * +FROM ticket_with_outcomes; -- name: GetTicketByID :one -SELECT * FROM tickets WHERE id = $1; - +SELECT * +FROM ticket_with_outcomes +WHERE id = $1; +-- name: GetTicketOutcome :many +SELECT * +FROM ticket_outcomes +WHERE ticket_id = $1; -- name: DeleteTicket :exec -DELETE FROM tickets WHERE id = $1; - +DELETE FROM tickets +WHERE id = $1; -- name: DeleteOldTickets :exec -Delete from tickets where created_at < now() - interval '1 day'; +Delete from tickets +where created_at < now() - interval '1 day'; +-- name: DeleteTicketOutcome :exec +Delete from ticket_outcomes +where ticket_id = $1; \ No newline at end of file diff --git a/db/query/user.sql b/db/query/user.sql index 8e127af..bee4713 100644 --- a/db/query/user.sql +++ b/db/query/user.sql @@ -1,49 +1,114 @@ -- name: CreateUser :one - -INSERT INTO users (first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at) +INSERT INTO users ( + first_name, + last_name, + email, + phone_number, + role, + password, + email_verified, + phone_verified, + created_at, + updated_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) -RETURNING id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at; - +RETURNING id, + first_name, + last_name, + email, + phone_number, + role, + email_verified, + phone_verified, + created_at, + updated_at; -- name: GetUserByID :one SELECT * FROM users WHERE id = $1; - -- name: GetAllUsers :many -SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at +SELECT id, + first_name, + last_name, + email, + phone_number, + role, + email_verified, + phone_verified, + created_at, + updated_at FROM users; - -- name: SearchUserByNameOrPhone :many -SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at +SELECT id, + first_name, + last_name, + email, + phone_number, + role, + email_verified, + phone_verified, + created_at, + updated_at FROM users -WHERE first_name ILIKE '%' || $1 || '%' - OR last_name ILIKE '%' || $1 || '%' +WHERE first_name ILIKE '%' || $1 || '%' + OR last_name ILIKE '%' || $1 || '%' OR phone_number LIKE '%' || $1 || '%'; - -- name: UpdateUser :exec UPDATE users -SET first_name = $1, last_name = $2, email = $3, phone_number = $4, role = $5, updated_at = $6 +SET first_name = $1, + last_name = $2, + email = $3, + phone_number = $4, + role = $5, + updated_at = $6 WHERE id = $7; - -- name: DeleteUser :exec DELETE FROM users WHERE id = $1; - -- name: CheckPhoneEmailExist :one -SELECT - EXISTS (SELECT 1 FROM users WHERE users.phone_number = $1 AND users.phone_number IS NOT NULL) AS phone_exists, - EXISTS (SELECT 1 FROM users WHERE users.email = $2 AND users.email IS NOT NULL) AS email_exists; +SELECT EXISTS ( + SELECT 1 + FROM users + WHERE users.phone_number = $1 + AND users.phone_number IS NOT NULL + ) AS phone_exists, + EXISTS ( + SELECT 1 + FROM users + WHERE users.email = $2 + AND users.email IS NOT NULL + ) AS email_exists; -- name: GetUserByEmail :one -SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at +SELECT id, + first_name, + last_name, + email, + phone_number, + role, + email_verified, + phone_verified, + created_at, + updated_at FROM users WHERE email = $1; - -- name: GetUserByPhone :one -SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at +SELECT id, + first_name, + last_name, + email, + phone_number, + role, + email_verified, + phone_verified, + created_at, + updated_at FROM users WHERE phone_number = $1; - -- name: UpdatePassword :exec UPDATE users -SET password = $1, updated_at = $4 -WHERE (email = $2 OR phone_number = $3); \ No newline at end of file +SET password = $1, + updated_at = $4 +WHERE ( + email = $2 + OR phone_number = $3 + ); \ No newline at end of file diff --git a/db/query/wallet.sql b/db/query/wallet.sql index dc025e9..0f4d27d 100644 --- a/db/query/wallet.sql +++ b/db/query/wallet.sql @@ -1,21 +1,34 @@ -- name: CreateWallet :one -INSERT INTO wallets (is_withdraw, is_bettable, is_transferable, user_id) VALUES ($1, $2, $3, $4) RETURNING *; - +INSERT INTO wallets ( + is_withdraw, + is_bettable, + is_transferable, + user_id + ) +VALUES ($1, $2, $3, $4) +RETURNING *; -- name: CreateCustomerWallet :one -INSERT INTO customer_wallets (customer_id, company_id, regular_wallet_id, static_wallet_id) VALUES ($1, $2, $3, $4) RETURNING *; - +INSERT INTO customer_wallets ( + customer_id, + company_id, + regular_wallet_id, + static_wallet_id + ) +VALUES ($1, $2, $3, $4) +RETURNING *; -- name: GetAllWallets :many -SELECT * FROM wallets; - +SELECT * +FROM wallets; -- name: GetWalletByID :one -SELECT * FROM wallets WHERE id = $1; - +SELECT * +FROM wallets +WHERE id = $1; -- name: GetWalletByUserID :many -SELECT * FROM wallets WHERE user_id = $1; - +SELECT * +FROM wallets +WHERE user_id = $1; -- name: GetCustomerWallet :one -SELECT - cw.id, +SELECT cw.id, cw.customer_id, cw.company_id, rw.id AS regular_id, @@ -26,15 +39,30 @@ SELECT sw.updated_at as static_updated_at, cw.created_at FROM customer_wallets cw -JOIN wallets rw ON cw.regular_wallet_id = rw.id -JOIN wallets sw ON cw.static_wallet_id = sw.id -WHERE cw.customer_id = $1 AND cw.company_id = $2; - + JOIN wallets rw ON cw.regular_wallet_id = rw.id + JOIN wallets sw ON cw.static_wallet_id = sw.id +WHERE cw.customer_id = $1 + AND cw.company_id = $2; +-- name: GetAllBranchWallets :many +SELECT wallets.id, + wallets.balance, + wallets.is_active, + wallets.updated_at, + wallets.created_at, + branches.name, + branches.location, + branches.branch_manager_id, + branches.company_id, + branches.is_self_owned +FROM branches + JOIN wallets ON branches.wallet_id = wallets.id; -- name: UpdateBalance :exec -UPDATE wallets SET balance = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2; - +UPDATE wallets +SET balance = $1, + updated_at = CURRENT_TIMESTAMP +WHERE id = $2; -- name: UpdateWalletActive :exec -UPDATE wallets SET is_active = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2; - - - +UPDATE wallets +SET is_active = $1, + updated_at = CURRENT_TIMESTAMP +WHERE id = $2; \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index bebc6db..71ba3a4 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -618,6 +618,44 @@ const docTemplate = `{ } } }, + "/branch/{id}/bets": { + "get": { + "description": "Gets bets by its branch id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets bets by its branch id", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BetRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/branch/{id}/operation": { "get": { "description": "Gets branch operations", @@ -716,6 +754,44 @@ const docTemplate = `{ } } }, + "/branchWallet": { + "get": { + "description": "Retrieve all branch wallets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wallet" + ], + "summary": "Get all branch wallets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.WalletRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/cashiers": { "get": { "description": "Get all cashiers", @@ -1170,6 +1246,53 @@ const docTemplate = `{ } } }, + "/search/branch": { + "get": { + "description": "Search branches by name or location", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Search branches", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "q", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BranchDetailRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/supportedOperation": { "get": { "description": "Gets all supported operations", @@ -1555,7 +1678,53 @@ const docTemplate = `{ } } }, - "/transfer/wallet": { + "/transfer/refill/:id": { + "post": { + "description": "Super Admin route to refill a wallet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transfer" + ], + "summary": "Refill wallet", + "parameters": [ + { + "description": "Create Transfer", + "name": "refillWallet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateTransferReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.TransferWalletRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/transfer/wallet/:id": { "post": { "description": "Create a transfer to wallet", "consumes": [ @@ -1583,7 +1752,53 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.TransferRes" + "$ref": "#/definitions/handlers.TransferWalletRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/transfer/wallet/{id}": { + "get": { + "description": "Get transfer by wallet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transfer" + ], + "summary": "Get transfer by wallet", + "parameters": [ + { + "description": "Create Transfer", + "name": "transferToWallet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateTransferReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.TransferWalletRes" } }, "400": { @@ -2101,6 +2316,23 @@ const docTemplate = `{ } }, "definitions": { + "domain.BetOutcome": { + "type": "object", + "properties": { + "betID": { + "type": "integer" + }, + "eventID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "oddID": { + "type": "integer" + } + } + }, "domain.BetStatus": { "type": "integer", "enum": [ @@ -2116,9 +2348,6 @@ const docTemplate = `{ "BET_STATUS_ERROR" ] }, - "domain.Outcome": { - "type": "object" - }, "domain.PaymentOption": { "type": "integer", "enum": [ @@ -2151,6 +2380,36 @@ const docTemplate = `{ "RoleCashier" ] }, + "domain.TicketOutcome": { + "type": "object", + "properties": { + "eventID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "oddID": { + "type": "integer" + }, + "ticketID": { + "type": "integer" + } + } + }, + "handlers.BetOutcome": { + "type": "object", + "properties": { + "event_id": { + "type": "integer", + "example": 1 + }, + "odd_id": { + "type": "integer", + "example": 1 + } + } + }, "handlers.BetRes": { "type": "object", "properties": { @@ -2177,7 +2436,7 @@ const docTemplate = `{ "outcomes": { "type": "array", "items": { - "$ref": "#/definitions/domain.Outcome" + "$ref": "#/definitions/domain.BetOutcome" } }, "phone_number": { @@ -2213,6 +2472,10 @@ const docTemplate = `{ "type": "integer", "example": 1 }, + "id": { + "type": "integer", + "example": 1 + }, "is_self_owned": { "type": "boolean", "example": false @@ -2263,6 +2526,10 @@ const docTemplate = `{ "type": "integer", "example": 1 }, + "id": { + "type": "integer", + "example": 1 + }, "is_self_owned": { "type": "boolean", "example": false @@ -2306,43 +2573,7 @@ const docTemplate = `{ } }, "handlers.CreateBetReq": { - "type": "object", - "properties": { - "amount": { - "type": "number", - "example": 100 - }, - "full_name": { - "type": "string", - "example": "John" - }, - "is_shop_bet": { - "type": "boolean", - "example": false - }, - "outcomes": { - "type": "array", - "items": { - "type": "integer" - } - }, - "phone_number": { - "type": "string", - "example": "1234567890" - }, - "status": { - "allOf": [ - { - "$ref": "#/definitions/domain.BetStatus" - } - ], - "example": 1 - }, - "total_odds": { - "type": "number", - "example": 4.22 - } - } + "type": "object" }, "handlers.CreateBranchOperationReq": { "type": "object", @@ -2391,6 +2622,10 @@ const docTemplate = `{ "handlers.CreateCashierReq": { "type": "object", "properties": { + "branch_id": { + "type": "integer", + "example": 1 + }, "email": { "type": "string", "example": "john.doe@example.com" @@ -2416,10 +2651,6 @@ const docTemplate = `{ "handlers.CreateManagerReq": { "type": "object", "properties": { - "branch_id": { - "type": "integer", - "example": 1 - }, "email": { "type": "string", "example": "john.doe@example.com" @@ -2465,7 +2696,7 @@ const docTemplate = `{ "outcomes": { "type": "array", "items": { - "type": "integer" + "$ref": "#/definitions/handlers.TicketOutcome" } }, "total_odds": { @@ -2477,6 +2708,10 @@ const docTemplate = `{ "handlers.CreateTicketRes": { "type": "object", "properties": { + "created_number": { + "type": "integer", + "example": 3 + }, "fast_code": { "type": "integer", "example": 1234 @@ -2550,10 +2785,6 @@ const docTemplate = `{ "payment_method": { "type": "string", "example": "cash" - }, - "receiver_id": { - "type": "integer", - "example": 1 } } }, @@ -2599,6 +2830,17 @@ const docTemplate = `{ } } }, + "handlers.NullableInt64": { + "type": "object", + "properties": { + "valid": { + "type": "boolean" + }, + "value": { + "type": "integer" + } + } + }, "handlers.RegisterCodeReq": { "type": "object", "properties": { @@ -2701,6 +2943,19 @@ const docTemplate = `{ } } }, + "handlers.TicketOutcome": { + "type": "object", + "properties": { + "event_id": { + "type": "integer", + "example": 1 + }, + "odd_id": { + "type": "integer", + "example": 1 + } + } + }, "handlers.TicketRes": { "type": "object", "properties": { @@ -2715,7 +2970,7 @@ const docTemplate = `{ "outcomes": { "type": "array", "items": { - "$ref": "#/definitions/domain.Outcome" + "$ref": "#/definitions/domain.TicketOutcome" } }, "total_odds": { @@ -2788,7 +3043,7 @@ const docTemplate = `{ } } }, - "handlers.TransferRes": { + "handlers.TransferWalletRes": { "type": "object", "properties": { "amount": { diff --git a/docs/swagger.json b/docs/swagger.json index d6e6201..5e4b713 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -610,6 +610,44 @@ } } }, + "/branch/{id}/bets": { + "get": { + "description": "Gets bets by its branch id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Gets bets by its branch id", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BetRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/branch/{id}/operation": { "get": { "description": "Gets branch operations", @@ -708,6 +746,44 @@ } } }, + "/branchWallet": { + "get": { + "description": "Retrieve all branch wallets", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wallet" + ], + "summary": "Get all branch wallets", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.WalletRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/cashiers": { "get": { "description": "Get all cashiers", @@ -1162,6 +1238,53 @@ } } }, + "/search/branch": { + "get": { + "description": "Search branches by name or location", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch" + ], + "summary": "Search branches", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "q", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BranchDetailRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/supportedOperation": { "get": { "description": "Gets all supported operations", @@ -1547,7 +1670,53 @@ } } }, - "/transfer/wallet": { + "/transfer/refill/:id": { + "post": { + "description": "Super Admin route to refill a wallet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transfer" + ], + "summary": "Refill wallet", + "parameters": [ + { + "description": "Create Transfer", + "name": "refillWallet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateTransferReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.TransferWalletRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/transfer/wallet/:id": { "post": { "description": "Create a transfer to wallet", "consumes": [ @@ -1575,7 +1744,53 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handlers.TransferRes" + "$ref": "#/definitions/handlers.TransferWalletRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/transfer/wallet/{id}": { + "get": { + "description": "Get transfer by wallet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "transfer" + ], + "summary": "Get transfer by wallet", + "parameters": [ + { + "description": "Create Transfer", + "name": "transferToWallet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateTransferReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.TransferWalletRes" } }, "400": { @@ -2093,6 +2308,23 @@ } }, "definitions": { + "domain.BetOutcome": { + "type": "object", + "properties": { + "betID": { + "type": "integer" + }, + "eventID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "oddID": { + "type": "integer" + } + } + }, "domain.BetStatus": { "type": "integer", "enum": [ @@ -2108,9 +2340,6 @@ "BET_STATUS_ERROR" ] }, - "domain.Outcome": { - "type": "object" - }, "domain.PaymentOption": { "type": "integer", "enum": [ @@ -2143,6 +2372,36 @@ "RoleCashier" ] }, + "domain.TicketOutcome": { + "type": "object", + "properties": { + "eventID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "oddID": { + "type": "integer" + }, + "ticketID": { + "type": "integer" + } + } + }, + "handlers.BetOutcome": { + "type": "object", + "properties": { + "event_id": { + "type": "integer", + "example": 1 + }, + "odd_id": { + "type": "integer", + "example": 1 + } + } + }, "handlers.BetRes": { "type": "object", "properties": { @@ -2169,7 +2428,7 @@ "outcomes": { "type": "array", "items": { - "$ref": "#/definitions/domain.Outcome" + "$ref": "#/definitions/domain.BetOutcome" } }, "phone_number": { @@ -2205,6 +2464,10 @@ "type": "integer", "example": 1 }, + "id": { + "type": "integer", + "example": 1 + }, "is_self_owned": { "type": "boolean", "example": false @@ -2255,6 +2518,10 @@ "type": "integer", "example": 1 }, + "id": { + "type": "integer", + "example": 1 + }, "is_self_owned": { "type": "boolean", "example": false @@ -2298,43 +2565,7 @@ } }, "handlers.CreateBetReq": { - "type": "object", - "properties": { - "amount": { - "type": "number", - "example": 100 - }, - "full_name": { - "type": "string", - "example": "John" - }, - "is_shop_bet": { - "type": "boolean", - "example": false - }, - "outcomes": { - "type": "array", - "items": { - "type": "integer" - } - }, - "phone_number": { - "type": "string", - "example": "1234567890" - }, - "status": { - "allOf": [ - { - "$ref": "#/definitions/domain.BetStatus" - } - ], - "example": 1 - }, - "total_odds": { - "type": "number", - "example": 4.22 - } - } + "type": "object" }, "handlers.CreateBranchOperationReq": { "type": "object", @@ -2383,6 +2614,10 @@ "handlers.CreateCashierReq": { "type": "object", "properties": { + "branch_id": { + "type": "integer", + "example": 1 + }, "email": { "type": "string", "example": "john.doe@example.com" @@ -2408,10 +2643,6 @@ "handlers.CreateManagerReq": { "type": "object", "properties": { - "branch_id": { - "type": "integer", - "example": 1 - }, "email": { "type": "string", "example": "john.doe@example.com" @@ -2457,7 +2688,7 @@ "outcomes": { "type": "array", "items": { - "type": "integer" + "$ref": "#/definitions/handlers.TicketOutcome" } }, "total_odds": { @@ -2469,6 +2700,10 @@ "handlers.CreateTicketRes": { "type": "object", "properties": { + "created_number": { + "type": "integer", + "example": 3 + }, "fast_code": { "type": "integer", "example": 1234 @@ -2542,10 +2777,6 @@ "payment_method": { "type": "string", "example": "cash" - }, - "receiver_id": { - "type": "integer", - "example": 1 } } }, @@ -2591,6 +2822,17 @@ } } }, + "handlers.NullableInt64": { + "type": "object", + "properties": { + "valid": { + "type": "boolean" + }, + "value": { + "type": "integer" + } + } + }, "handlers.RegisterCodeReq": { "type": "object", "properties": { @@ -2693,6 +2935,19 @@ } } }, + "handlers.TicketOutcome": { + "type": "object", + "properties": { + "event_id": { + "type": "integer", + "example": 1 + }, + "odd_id": { + "type": "integer", + "example": 1 + } + } + }, "handlers.TicketRes": { "type": "object", "properties": { @@ -2707,7 +2962,7 @@ "outcomes": { "type": "array", "items": { - "$ref": "#/definitions/domain.Outcome" + "$ref": "#/definitions/domain.TicketOutcome" } }, "total_odds": { @@ -2780,7 +3035,7 @@ } } }, - "handlers.TransferRes": { + "handlers.TransferWalletRes": { "type": "object", "properties": { "amount": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7345a67..604daf7 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,4 +1,15 @@ definitions: + domain.BetOutcome: + properties: + betID: + type: integer + eventID: + type: integer + id: + type: integer + oddID: + type: integer + type: object domain.BetStatus: enum: - 0 @@ -11,8 +22,6 @@ definitions: - BET_STATUS_WIN - BET_STATUS_LOSS - BET_STATUS_ERROR - domain.Outcome: - type: object domain.PaymentOption: enum: - 0 @@ -39,6 +48,26 @@ definitions: - RoleBranchManager - RoleCustomer - RoleCashier + domain.TicketOutcome: + properties: + eventID: + type: integer + id: + type: integer + oddID: + type: integer + ticketID: + type: integer + type: object + handlers.BetOutcome: + properties: + event_id: + example: 1 + type: integer + odd_id: + example: 1 + type: integer + type: object handlers.BetRes: properties: amount: @@ -58,7 +87,7 @@ definitions: type: boolean outcomes: items: - $ref: '#/definitions/domain.Outcome' + $ref: '#/definitions/domain.BetOutcome' type: array phone_number: example: "1234567890" @@ -82,6 +111,9 @@ definitions: company_id: example: 1 type: integer + id: + example: 1 + type: integer is_self_owned: example: false type: boolean @@ -118,6 +150,9 @@ definitions: company_id: example: 1 type: integer + id: + example: 1 + type: integer is_self_owned: example: false type: boolean @@ -148,30 +183,6 @@ definitions: type: boolean type: object handlers.CreateBetReq: - properties: - amount: - example: 100 - type: number - full_name: - example: John - type: string - is_shop_bet: - example: false - type: boolean - outcomes: - items: - type: integer - type: array - phone_number: - example: "1234567890" - type: string - status: - allOf: - - $ref: '#/definitions/domain.BetStatus' - example: 1 - total_odds: - example: 4.22 - type: number type: object handlers.CreateBranchOperationReq: properties: @@ -206,6 +217,9 @@ definitions: type: object handlers.CreateCashierReq: properties: + branch_id: + example: 1 + type: integer email: example: john.doe@example.com type: string @@ -224,9 +238,6 @@ definitions: type: object handlers.CreateManagerReq: properties: - branch_id: - example: 1 - type: integer email: example: john.doe@example.com type: string @@ -259,7 +270,7 @@ definitions: type: number outcomes: items: - type: integer + $ref: '#/definitions/handlers.TicketOutcome' type: array total_odds: example: 4.22 @@ -267,6 +278,9 @@ definitions: type: object handlers.CreateTicketRes: properties: + created_number: + example: 3 + type: integer fast_code: example: 1234 type: integer @@ -318,9 +332,6 @@ definitions: payment_method: example: cash type: string - receiver_id: - example: 1 - type: integer type: object handlers.CustomerWalletRes: properties: @@ -352,6 +363,13 @@ definitions: static_updated_at: type: string type: object + handlers.NullableInt64: + properties: + valid: + type: boolean + value: + type: integer + type: object handlers.RegisterCodeReq: properties: email: @@ -423,6 +441,15 @@ definitions: example: SportsBook type: string type: object + handlers.TicketOutcome: + properties: + event_id: + example: 1 + type: integer + odd_id: + example: 1 + type: integer + type: object handlers.TicketRes: properties: amount: @@ -433,7 +460,7 @@ definitions: type: integer outcomes: items: - $ref: '#/definitions/domain.Outcome' + $ref: '#/definitions/domain.TicketOutcome' type: array total_odds: example: 4.22 @@ -483,7 +510,7 @@ definitions: example: true type: boolean type: object - handlers.TransferRes: + handlers.TransferWalletRes: properties: amount: example: 100 @@ -1055,6 +1082,31 @@ paths: summary: Updates a branch tags: - branch + /branch/{id}/bets: + get: + consumes: + - application/json + description: Gets bets by its branch id + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.BetRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets bets by its branch id + tags: + - branch /branch/{id}/operation: get: consumes: @@ -1120,6 +1172,31 @@ paths: summary: Delete the branch operation tags: - branch + /branchWallet: + get: + consumes: + - application/json + description: Retrieve all branch wallets + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.WalletRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get all branch wallets + tags: + - wallet /cashiers: get: consumes: @@ -1418,6 +1495,37 @@ paths: summary: Create a operation tags: - branch + /search/branch: + get: + consumes: + - application/json + description: Search branches by name or location + parameters: + - description: Search query + in: query + name: q + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.BranchDetailRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Search branches + tags: + - branch /supportedOperation: get: consumes: @@ -1672,7 +1780,37 @@ paths: summary: Updates the cashed out field tags: - transaction - /transfer/wallet: + /transfer/refill/:id: + post: + consumes: + - application/json + description: Super Admin route to refill a wallet + parameters: + - description: Create Transfer + in: body + name: refillWallet + required: true + schema: + $ref: '#/definitions/handlers.CreateTransferReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.TransferWalletRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Refill wallet + tags: + - transfer + /transfer/wallet/:id: post: consumes: - application/json @@ -1690,7 +1828,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/handlers.TransferRes' + $ref: '#/definitions/handlers.TransferWalletRes' "400": description: Bad Request schema: @@ -1702,6 +1840,36 @@ paths: summary: Create a transfer to wallet tags: - transfer + /transfer/wallet/{id}: + get: + consumes: + - application/json + description: Get transfer by wallet + parameters: + - description: Create Transfer + in: body + name: transferToWallet + required: true + schema: + $ref: '#/definitions/handlers.CreateTransferReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.TransferWalletRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get transfer by wallet + tags: + - transfer /user/checkPhoneEmailExist: post: consumes: diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index 79b5cf3..89a636a 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -12,7 +12,17 @@ import ( ) const CreateBet = `-- name: CreateBet :one -INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id, is_shop_bet, cashout_id) +INSERT INTO bets ( + amount, + total_odds, + status, + full_name, + phone_number, + branch_id, + user_id, + is_shop_bet, + cashout_id + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet ` @@ -60,8 +70,15 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro return i, err } +type CreateBetOutcomeParams struct { + BetID int64 + EventID int64 + OddID int64 +} + const DeleteBet = `-- name: DeleteBet :exec -DELETE FROM bets WHERE id = $1 +DELETE FROM bets +WHERE id = $1 ` func (q *Queries) DeleteBet(ctx context.Context, id int64) error { @@ -69,19 +86,30 @@ func (q *Queries) DeleteBet(ctx context.Context, id int64) error { return err } -const GetAllBets = `-- name: GetAllBets :many -SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet FROM bets +const DeleteBetOutcome = `-- name: DeleteBetOutcome :exec +DELETE FROM bet_outcomes +WHERE bet_id = $1 ` -func (q *Queries) GetAllBets(ctx context.Context) ([]Bet, error) { +func (q *Queries) DeleteBetOutcome(ctx context.Context, betID int64) error { + _, err := q.db.Exec(ctx, DeleteBetOutcome, betID) + return err +} + +const GetAllBets = `-- name: GetAllBets :many +SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes +FROM bet_with_outcomes +` + +func (q *Queries) GetAllBets(ctx context.Context) ([]BetWithOutcome, error) { rows, err := q.db.Query(ctx, GetAllBets) if err != nil { return nil, err } defer rows.Close() - var items []Bet + var items []BetWithOutcome for rows.Next() { - var i Bet + var i BetWithOutcome if err := rows.Scan( &i.ID, &i.Amount, @@ -96,6 +124,7 @@ func (q *Queries) GetAllBets(ctx context.Context) ([]Bet, error) { &i.CreatedAt, &i.UpdatedAt, &i.IsShopBet, + &i.Outcomes, ); err != nil { return nil, err } @@ -107,13 +136,85 @@ func (q *Queries) GetAllBets(ctx context.Context) ([]Bet, error) { return items, nil } -const GetBetByID = `-- name: GetBetByID :one -SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet FROM bets WHERE id = $1 +const GetBetByBranchID = `-- name: GetBetByBranchID :many +SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes +FROM bet_with_outcomes +WHERE branch_id = $1 ` -func (q *Queries) GetBetByID(ctx context.Context, id int64) (Bet, error) { +func (q *Queries) GetBetByBranchID(ctx context.Context, branchID pgtype.Int8) ([]BetWithOutcome, error) { + rows, err := q.db.Query(ctx, GetBetByBranchID, branchID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []BetWithOutcome + for rows.Next() { + var i BetWithOutcome + if err := rows.Scan( + &i.ID, + &i.Amount, + &i.TotalOdds, + &i.Status, + &i.FullName, + &i.PhoneNumber, + &i.BranchID, + &i.UserID, + &i.CashedOut, + &i.CashoutID, + &i.CreatedAt, + &i.UpdatedAt, + &i.IsShopBet, + &i.Outcomes, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetBetByCashoutID = `-- name: GetBetByCashoutID :many +SELECT +FROM bet_with_outcomes +WHERE cashout_id = $1 +` + +type GetBetByCashoutIDRow struct { +} + +func (q *Queries) GetBetByCashoutID(ctx context.Context, cashoutID string) ([]GetBetByCashoutIDRow, error) { + rows, err := q.db.Query(ctx, GetBetByCashoutID, cashoutID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetBetByCashoutIDRow + for rows.Next() { + var i GetBetByCashoutIDRow + if err := rows.Scan(); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetBetByID = `-- name: GetBetByID :one +SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes +FROM bet_with_outcomes +WHERE id = $1 +` + +func (q *Queries) GetBetByID(ctx context.Context, id int64) (BetWithOutcome, error) { row := q.db.QueryRow(ctx, GetBetByID, id) - var i Bet + var i BetWithOutcome err := row.Scan( &i.ID, &i.Amount, @@ -128,12 +229,16 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (Bet, error) { &i.CreatedAt, &i.UpdatedAt, &i.IsShopBet, + &i.Outcomes, ) return i, err } const UpdateCashOut = `-- name: UpdateCashOut :exec -UPDATE bets SET cashed_out = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1 +UPDATE bets +SET cashed_out = $2, + updated_at = CURRENT_TIMESTAMP +WHERE id = $1 ` type UpdateCashOutParams struct { diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index 8d34fb3..d1d8e99 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -12,7 +12,16 @@ import ( ) const CreateBranch = `-- name: CreateBranch :one -INSERT INTO branches (name, location, wallet_id, branch_manager_id, company_id, is_self_owned) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at +INSERT INTO branches ( + name, + location, + wallet_id, + branch_manager_id, + company_id, + is_self_owned + ) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at ` type CreateBranchParams struct { @@ -48,8 +57,28 @@ func (q *Queries) CreateBranch(ctx context.Context, arg CreateBranchParams) (Bra return i, err } +const CreateBranchCashier = `-- name: CreateBranchCashier :one +INSERT INTO branch_cashiers (user_id, branch_id) +VALUES ($1, $2) +RETURNING id, user_id, branch_id +` + +type CreateBranchCashierParams struct { + UserID int64 + BranchID int64 +} + +func (q *Queries) CreateBranchCashier(ctx context.Context, arg CreateBranchCashierParams) (BranchCashier, error) { + row := q.db.QueryRow(ctx, CreateBranchCashier, arg.UserID, arg.BranchID) + var i BranchCashier + err := row.Scan(&i.ID, &i.UserID, &i.BranchID) + return i, err +} + const CreateBranchOperation = `-- name: CreateBranchOperation :one -INSERT INTO branch_operations (operation_id, branch_id) VALUES ($1, $2) RETURNING id, operation_id, branch_id, created_at, updated_at +INSERT INTO branch_operations (operation_id, branch_id) +VALUES ($1, $2) +RETURNING id, operation_id, branch_id, created_at, updated_at ` type CreateBranchOperationParams struct { @@ -71,7 +100,9 @@ func (q *Queries) CreateBranchOperation(ctx context.Context, arg CreateBranchOpe } const CreateSupportedOperation = `-- name: CreateSupportedOperation :one -INSERT INTO supported_operations (name, description) VALUES ($1, $2) RETURNING id, name, description +INSERT INTO supported_operations (name, description) +VALUES ($1, $2) +RETURNING id, name, description ` type CreateSupportedOperationParams struct { @@ -87,7 +118,8 @@ func (q *Queries) CreateSupportedOperation(ctx context.Context, arg CreateSuppor } const DeleteBranch = `-- name: DeleteBranch :exec -DELETE FROM branches WHERE id = $1 +DELETE FROM branches +WHERE id = $1 ` func (q *Queries) DeleteBranch(ctx context.Context, id int64) error { @@ -95,8 +127,20 @@ func (q *Queries) DeleteBranch(ctx context.Context, id int64) error { return err } +const DeleteBranchCashier = `-- name: DeleteBranchCashier :exec +DELETE FROM branch_cashiers +WHERE user_id = $1 +` + +func (q *Queries) DeleteBranchCashier(ctx context.Context, userID int64) error { + _, err := q.db.Exec(ctx, DeleteBranchCashier, userID) + return err +} + const DeleteBranchOperation = `-- name: DeleteBranchOperation :exec -DELETE FROM branch_operations WHERE operation_id = $1 AND branch_id = $2 +DELETE FROM branch_operations +WHERE operation_id = $1 + AND branch_id = $2 ` type DeleteBranchOperationParams struct { @@ -110,7 +154,8 @@ func (q *Queries) DeleteBranchOperation(ctx context.Context, arg DeleteBranchOpe } const GetAllBranches = `-- name: GetAllBranches :many -SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number FROM branch_details +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 ` func (q *Queries) GetAllBranches(ctx context.Context) ([]BranchDetail, error) { @@ -145,8 +190,49 @@ func (q *Queries) GetAllBranches(ctx context.Context) ([]BranchDetail, error) { return items, nil } +const GetAllCashiers = `-- name: GetAllCashiers :many +SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.suspended_at, users.suspended +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.SuspendedAt, + &i.Suspended, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetAllSupportedOperations = `-- name: GetAllSupportedOperations :many -SELECT id, name, description FROM supported_operations +SELECT id, name, description +FROM supported_operations ` func (q *Queries) GetAllSupportedOperations(ctx context.Context) ([]SupportedOperation, error) { @@ -169,8 +255,34 @@ func (q *Queries) GetAllSupportedOperations(ctx context.Context) ([]SupportedOpe return items, nil } +const GetBranchByCashier = `-- name: GetBranchByCashier :one +SELECT branches.id, branches.name, branches.location, branches.wallet_id, branches.branch_manager_id, branches.company_id, branches.is_self_owned, branches.created_at, branches.updated_at +FROM branch_cashiers + JOIN branches ON branch_cashiers.branch_id = branches.id +WHERE branch_cashiers.user_id = $1 +` + +func (q *Queries) GetBranchByCashier(ctx context.Context, userID int64) (Branch, error) { + row := q.db.QueryRow(ctx, GetBranchByCashier, userID) + var i Branch + err := row.Scan( + &i.ID, + &i.Name, + &i.Location, + &i.WalletID, + &i.BranchManagerID, + &i.CompanyID, + &i.IsSelfOwned, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const GetBranchByCompanyID = `-- name: GetBranchByCompanyID :many -SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number FROM branch_details WHERE company_id = $1 +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 +WHERE company_id = $1 ` func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]BranchDetail, error) { @@ -206,7 +318,9 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([] } const GetBranchByID = `-- name: GetBranchByID :one -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 WHERE id = $1 +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 +WHERE id = $1 ` func (q *Queries) GetBranchByID(ctx context.Context, id int64) (BranchDetail, error) { @@ -229,7 +343,9 @@ func (q *Queries) GetBranchByID(ctx context.Context, id int64) (BranchDetail, er } const GetBranchByManagerID = `-- name: GetBranchByManagerID :many -SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number FROM branch_details WHERE branch_manager_id = $1 +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 +WHERE branch_manager_id = $1 ` func (q *Queries) GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]BranchDetail, error) { @@ -265,9 +381,11 @@ func (q *Queries) GetBranchByManagerID(ctx context.Context, branchManagerID int6 } const GetBranchOperations = `-- name: GetBranchOperations :many -SELECT branch_operations.id, branch_operations.operation_id, branch_operations.branch_id, branch_operations.created_at, branch_operations.updated_at, supported_operations.name, supported_operations.description -FROM branch_operations -JOIN supported_operations ON branch_operations.operation_id = supported_operations.id +SELECT branch_operations.id, branch_operations.operation_id, branch_operations.branch_id, branch_operations.created_at, branch_operations.updated_at, + supported_operations.name, + supported_operations.description +FROM branch_operations + JOIN supported_operations ON branch_operations.operation_id = supported_operations.id WHERE branch_operations.branch_id = $1 ` @@ -309,8 +427,51 @@ func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]Ge return items, nil } +const GetCashiersByBranch = `-- name: GetCashiersByBranch :many +SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.suspended_at, users.suspended +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.SuspendedAt, + &i.Suspended, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const SearchBranchByName = `-- name: SearchBranchByName :many -SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number FROM branch_details WHERE name ILIKE '%' || $1 || '%' +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 +WHERE name ILIKE '%' || $1 || '%' ` func (q *Queries) SearchBranchByName(ctx context.Context, dollar_1 pgtype.Text) ([]BranchDetail, error) { @@ -346,7 +507,14 @@ func (q *Queries) SearchBranchByName(ctx context.Context, dollar_1 pgtype.Text) } const UpdateBranch = `-- name: UpdateBranch :one -UPDATE branches SET name = $1, location = $2, branch_manager_id = $3, company_id = $4, is_self_owned = $5 WHERE id = $6 RETURNING id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at +UPDATE branches +SET name = $1, + location = $2, + branch_manager_id = $3, + company_id = $4, + is_self_owned = $5 +WHERE id = $6 +RETURNING id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at ` type UpdateBranchParams struct { diff --git a/gen/db/copyfrom.go b/gen/db/copyfrom.go new file mode 100644 index 0000000..f3dffed --- /dev/null +++ b/gen/db/copyfrom.go @@ -0,0 +1,78 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: copyfrom.go + +package dbgen + +import ( + "context" +) + +// iteratorForCreateBetOutcome implements pgx.CopyFromSource. +type iteratorForCreateBetOutcome struct { + rows []CreateBetOutcomeParams + skippedFirstNextCall bool +} + +func (r *iteratorForCreateBetOutcome) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForCreateBetOutcome) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].BetID, + r.rows[0].EventID, + r.rows[0].OddID, + }, nil +} + +func (r iteratorForCreateBetOutcome) Err() error { + return nil +} + +func (q *Queries) CreateBetOutcome(ctx context.Context, arg []CreateBetOutcomeParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"bet_outcomes"}, []string{"bet_id", "event_id", "odd_id"}, &iteratorForCreateBetOutcome{rows: arg}) +} + +// iteratorForCreateTicketOutcome implements pgx.CopyFromSource. +type iteratorForCreateTicketOutcome struct { + rows []CreateTicketOutcomeParams + skippedFirstNextCall bool +} + +func (r *iteratorForCreateTicketOutcome) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForCreateTicketOutcome) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].TicketID, + r.rows[0].EventID, + r.rows[0].OddID, + }, nil +} + +func (r iteratorForCreateTicketOutcome) Err() error { + return nil +} + +func (q *Queries) CreateTicketOutcome(ctx context.Context, arg []CreateTicketOutcomeParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"ticket_outcomes"}, []string{"ticket_id", "event_id", "odd_id"}, &iteratorForCreateTicketOutcome{rows: arg}) +} diff --git a/gen/db/db.go b/gen/db/db.go index 136f20a..d892683 100644 --- a/gen/db/db.go +++ b/gen/db/db.go @@ -15,6 +15,7 @@ type DBTX interface { Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) Query(context.Context, string, ...interface{}) (pgx.Rows, error) QueryRow(context.Context, string, ...interface{}) pgx.Row + CopyFrom(ctx context.Context, tableName pgx.Identifier, columnNames []string, rowSrc pgx.CopyFromSource) (int64, error) } func New(db DBTX) *Queries { diff --git a/gen/db/models.go b/gen/db/models.go index e96ee34..d41d796 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -24,6 +24,30 @@ type Bet struct { IsShopBet bool } +type BetOutcome struct { + ID int64 + BetID int64 + EventID int64 + OddID int64 +} + +type BetWithOutcome struct { + ID int64 + Amount int64 + TotalOdds float32 + Status int32 + FullName string + PhoneNumber string + BranchID pgtype.Int8 + UserID pgtype.Int8 + CashedOut bool + CashoutID string + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp + IsShopBet bool + Outcomes []BetOutcome +} + type Branch struct { ID int64 Name string @@ -36,6 +60,12 @@ type Branch struct { UpdatedAt pgtype.Timestamp } +type BranchCashier struct { + ID int64 + UserID int64 + BranchID int64 +} + type BranchDetail struct { ID int64 Name string @@ -120,6 +150,22 @@ type Ticket struct { UpdatedAt pgtype.Timestamp } +type TicketOutcome struct { + ID int64 + TicketID int64 + EventID int64 + OddID int64 +} + +type TicketWithOutcome struct { + ID int64 + Amount pgtype.Int8 + TotalOdds float32 + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp + Outcomes []TicketOutcome +} + type Transaction struct { ID int64 Amount int64 diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index d7e5ff3..150d386 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -35,8 +35,15 @@ func (q *Queries) CreateTicket(ctx context.Context, arg CreateTicketParams) (Tic return i, err } +type CreateTicketOutcomeParams struct { + TicketID int64 + EventID int64 + OddID int64 +} + const DeleteOldTickets = `-- name: DeleteOldTickets :exec -Delete from tickets where created_at < now() - interval '1 day' +Delete from tickets +where created_at < now() - interval '1 day' ` func (q *Queries) DeleteOldTickets(ctx context.Context) error { @@ -45,7 +52,8 @@ func (q *Queries) DeleteOldTickets(ctx context.Context) error { } const DeleteTicket = `-- name: DeleteTicket :exec -DELETE FROM tickets WHERE id = $1 +DELETE FROM tickets +WHERE id = $1 ` func (q *Queries) DeleteTicket(ctx context.Context, id int64) error { @@ -53,25 +61,37 @@ func (q *Queries) DeleteTicket(ctx context.Context, id int64) error { return err } -const GetAllTickets = `-- name: GetAllTickets :many -SELECT id, amount, total_odds, created_at, updated_at FROM tickets +const DeleteTicketOutcome = `-- name: DeleteTicketOutcome :exec +Delete from ticket_outcomes +where ticket_id = $1 ` -func (q *Queries) GetAllTickets(ctx context.Context) ([]Ticket, error) { +func (q *Queries) DeleteTicketOutcome(ctx context.Context, ticketID int64) error { + _, err := q.db.Exec(ctx, DeleteTicketOutcome, ticketID) + return err +} + +const GetAllTickets = `-- name: GetAllTickets :many +SELECT id, amount, total_odds, created_at, updated_at, outcomes +FROM ticket_with_outcomes +` + +func (q *Queries) GetAllTickets(ctx context.Context) ([]TicketWithOutcome, error) { rows, err := q.db.Query(ctx, GetAllTickets) if err != nil { return nil, err } defer rows.Close() - var items []Ticket + var items []TicketWithOutcome for rows.Next() { - var i Ticket + var i TicketWithOutcome if err := rows.Scan( &i.ID, &i.Amount, &i.TotalOdds, &i.CreatedAt, &i.UpdatedAt, + &i.Outcomes, ); err != nil { return nil, err } @@ -84,18 +104,52 @@ func (q *Queries) GetAllTickets(ctx context.Context) ([]Ticket, error) { } const GetTicketByID = `-- name: GetTicketByID :one -SELECT id, amount, total_odds, created_at, updated_at FROM tickets WHERE id = $1 +SELECT id, amount, total_odds, created_at, updated_at, outcomes +FROM ticket_with_outcomes +WHERE id = $1 ` -func (q *Queries) GetTicketByID(ctx context.Context, id int64) (Ticket, error) { +func (q *Queries) GetTicketByID(ctx context.Context, id int64) (TicketWithOutcome, error) { row := q.db.QueryRow(ctx, GetTicketByID, id) - var i Ticket + var i TicketWithOutcome err := row.Scan( &i.ID, &i.Amount, &i.TotalOdds, &i.CreatedAt, &i.UpdatedAt, + &i.Outcomes, ) return i, err } + +const GetTicketOutcome = `-- name: GetTicketOutcome :many +SELECT id, ticket_id, event_id, odd_id +FROM ticket_outcomes +WHERE ticket_id = $1 +` + +func (q *Queries) GetTicketOutcome(ctx context.Context, ticketID int64) ([]TicketOutcome, error) { + rows, err := q.db.Query(ctx, GetTicketOutcome, ticketID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TicketOutcome + for rows.Next() { + var i TicketOutcome + if err := rows.Scan( + &i.ID, + &i.TicketID, + &i.EventID, + &i.OddID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index bce776c..e259cb9 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -12,9 +12,18 @@ import ( ) const CheckPhoneEmailExist = `-- name: CheckPhoneEmailExist :one -SELECT - EXISTS (SELECT 1 FROM users WHERE users.phone_number = $1 AND users.phone_number IS NOT NULL) AS phone_exists, - EXISTS (SELECT 1 FROM users WHERE users.email = $2 AND users.email IS NOT NULL) AS email_exists +SELECT EXISTS ( + SELECT 1 + FROM users + WHERE users.phone_number = $1 + AND users.phone_number IS NOT NULL + ) AS phone_exists, + EXISTS ( + SELECT 1 + FROM users + WHERE users.email = $2 + AND users.email IS NOT NULL + ) AS email_exists ` type CheckPhoneEmailExistParams struct { @@ -35,10 +44,29 @@ func (q *Queries) CheckPhoneEmailExist(ctx context.Context, arg CheckPhoneEmailE } const CreateUser = `-- name: CreateUser :one - -INSERT INTO users (first_name, last_name, email, phone_number, role, password, email_verified, phone_verified, created_at, updated_at) +INSERT INTO users ( + first_name, + last_name, + email, + phone_number, + role, + password, + email_verified, + phone_verified, + created_at, + updated_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) -RETURNING id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at +RETURNING id, + first_name, + last_name, + email, + phone_number, + role, + email_verified, + phone_verified, + created_at, + updated_at ` type CreateUserParams struct { @@ -107,7 +135,16 @@ func (q *Queries) DeleteUser(ctx context.Context, id int64) error { } const GetAllUsers = `-- name: GetAllUsers :many -SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at +SELECT id, + first_name, + last_name, + email, + phone_number, + role, + email_verified, + phone_verified, + created_at, + updated_at FROM users ` @@ -156,7 +193,16 @@ func (q *Queries) GetAllUsers(ctx context.Context) ([]GetAllUsersRow, error) { } const GetUserByEmail = `-- name: GetUserByEmail :one -SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at +SELECT id, + first_name, + last_name, + email, + phone_number, + role, + email_verified, + phone_verified, + created_at, + updated_at FROM users WHERE email = $1 ` @@ -220,7 +266,16 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) { } const GetUserByPhone = `-- name: GetUserByPhone :one -SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at +SELECT id, + first_name, + last_name, + email, + phone_number, + role, + email_verified, + phone_verified, + created_at, + updated_at FROM users WHERE phone_number = $1 ` @@ -257,10 +312,19 @@ func (q *Queries) GetUserByPhone(ctx context.Context, phoneNumber pgtype.Text) ( } const SearchUserByNameOrPhone = `-- name: SearchUserByNameOrPhone :many -SELECT id, first_name, last_name, email, phone_number, role, email_verified, phone_verified, created_at, updated_at +SELECT id, + first_name, + last_name, + email, + phone_number, + role, + email_verified, + phone_verified, + created_at, + updated_at FROM users -WHERE first_name ILIKE '%' || $1 || '%' - OR last_name ILIKE '%' || $1 || '%' +WHERE first_name ILIKE '%' || $1 || '%' + OR last_name ILIKE '%' || $1 || '%' OR phone_number LIKE '%' || $1 || '%' ` @@ -310,8 +374,12 @@ func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, dollar_1 pgtype.T const UpdatePassword = `-- name: UpdatePassword :exec UPDATE users -SET password = $1, updated_at = $4 -WHERE (email = $2 OR phone_number = $3) +SET password = $1, + updated_at = $4 +WHERE ( + email = $2 + OR phone_number = $3 + ) ` type UpdatePasswordParams struct { @@ -333,7 +401,12 @@ func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) const UpdateUser = `-- name: UpdateUser :exec UPDATE users -SET first_name = $1, last_name = $2, email = $3, phone_number = $4, role = $5, updated_at = $6 +SET first_name = $1, + last_name = $2, + email = $3, + phone_number = $4, + role = $5, + updated_at = $6 WHERE id = $7 ` diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index 0700a07..5c3410a 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -12,7 +12,14 @@ import ( ) const CreateCustomerWallet = `-- name: CreateCustomerWallet :one -INSERT INTO customer_wallets (customer_id, company_id, regular_wallet_id, static_wallet_id) VALUES ($1, $2, $3, $4) RETURNING id, customer_id, company_id, regular_wallet_id, static_wallet_id, created_at, updated_at +INSERT INTO customer_wallets ( + customer_id, + company_id, + regular_wallet_id, + static_wallet_id + ) +VALUES ($1, $2, $3, $4) +RETURNING id, customer_id, company_id, regular_wallet_id, static_wallet_id, created_at, updated_at ` type CreateCustomerWalletParams struct { @@ -43,7 +50,14 @@ func (q *Queries) CreateCustomerWallet(ctx context.Context, arg CreateCustomerWa } const CreateWallet = `-- name: CreateWallet :one -INSERT INTO wallets (is_withdraw, is_bettable, is_transferable, user_id) VALUES ($1, $2, $3, $4) RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at +INSERT INTO wallets ( + is_withdraw, + is_bettable, + is_transferable, + user_id + ) +VALUES ($1, $2, $3, $4) +RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at ` type CreateWalletParams struct { @@ -75,8 +89,68 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal return i, err } +const GetAllBranchWallets = `-- name: GetAllBranchWallets :many +SELECT wallets.id, + wallets.balance, + wallets.is_active, + wallets.updated_at, + wallets.created_at, + branches.name, + branches.location, + branches.branch_manager_id, + branches.company_id, + branches.is_self_owned +FROM branches + JOIN wallets ON branches.wallet_id = wallets.id +` + +type GetAllBranchWalletsRow struct { + ID int64 + Balance int64 + IsActive bool + UpdatedAt pgtype.Timestamp + CreatedAt pgtype.Timestamp + Name string + Location string + BranchManagerID int64 + CompanyID int64 + IsSelfOwned bool +} + +func (q *Queries) GetAllBranchWallets(ctx context.Context) ([]GetAllBranchWalletsRow, error) { + rows, err := q.db.Query(ctx, GetAllBranchWallets) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllBranchWalletsRow + for rows.Next() { + var i GetAllBranchWalletsRow + if err := rows.Scan( + &i.ID, + &i.Balance, + &i.IsActive, + &i.UpdatedAt, + &i.CreatedAt, + &i.Name, + &i.Location, + &i.BranchManagerID, + &i.CompanyID, + &i.IsSelfOwned, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetAllWallets = `-- name: GetAllWallets :many -SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at FROM wallets +SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at +FROM wallets ` func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) { @@ -110,8 +184,7 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) { } const GetCustomerWallet = `-- name: GetCustomerWallet :one -SELECT - cw.id, +SELECT cw.id, cw.customer_id, cw.company_id, rw.id AS regular_id, @@ -122,9 +195,10 @@ SELECT sw.updated_at as static_updated_at, cw.created_at FROM customer_wallets cw -JOIN wallets rw ON cw.regular_wallet_id = rw.id -JOIN wallets sw ON cw.static_wallet_id = sw.id -WHERE cw.customer_id = $1 AND cw.company_id = $2 + JOIN wallets rw ON cw.regular_wallet_id = rw.id + JOIN wallets sw ON cw.static_wallet_id = sw.id +WHERE cw.customer_id = $1 + AND cw.company_id = $2 ` type GetCustomerWalletParams struct { @@ -164,7 +238,9 @@ func (q *Queries) GetCustomerWallet(ctx context.Context, arg GetCustomerWalletPa } const GetWalletByID = `-- name: GetWalletByID :one -SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at FROM wallets WHERE id = $1 +SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at +FROM wallets +WHERE id = $1 ` func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { @@ -185,7 +261,9 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { } const GetWalletByUserID = `-- name: GetWalletByUserID :many -SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at FROM wallets WHERE user_id = $1 +SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at +FROM wallets +WHERE user_id = $1 ` func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet, error) { @@ -219,7 +297,10 @@ func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet } const UpdateBalance = `-- name: UpdateBalance :exec -UPDATE wallets SET balance = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 +UPDATE wallets +SET balance = $1, + updated_at = CURRENT_TIMESTAMP +WHERE id = $2 ` type UpdateBalanceParams struct { @@ -233,7 +314,10 @@ func (q *Queries) UpdateBalance(ctx context.Context, arg UpdateBalanceParams) er } const UpdateWalletActive = `-- name: UpdateWalletActive :exec -UPDATE wallets SET is_active = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 +UPDATE wallets +SET is_active = $1, + updated_at = CURRENT_TIMESTAMP +WHERE id = $2 ` type UpdateWalletActiveParams struct { diff --git a/internal/domain/bet.go b/internal/domain/bet.go index cc8175d..23b3ee8 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -1,5 +1,18 @@ package domain +type BetOutcome struct { + ID int64 + BetID int64 + EventID int64 + OddID int64 +} + +type CreateBetOutcome struct { + BetID int64 + EventID int64 + OddID int64 +} + type BetStatus int const ( @@ -13,7 +26,6 @@ const ( // If it is a DigitalBet then UserID will be the user and the branchID will be 0 or nil type Bet struct { ID int64 - Outcomes []Outcome Amount Currency TotalOdds float32 Status BetStatus @@ -26,8 +38,22 @@ type Bet struct { CashoutID string } +type GetBet struct { + ID int64 + Amount Currency + TotalOdds float32 + Status BetStatus + FullName string + PhoneNumber string + BranchID ValidInt64 // Can Be Nullable + UserID ValidInt64 // Can Be Nullable + IsShopBet bool + CashedOut bool + CashoutID string + Outcomes []BetOutcome +} + type CreateBet struct { - Outcomes []int64 Amount Currency TotalOdds float32 Status BetStatus diff --git a/internal/domain/common.go b/internal/domain/common.go index 985e97e..88273f3 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -37,3 +37,5 @@ func (m Currency) String() string { return fmt.Sprintf("$%.2f", x) } + + diff --git a/internal/domain/ticket.go b/internal/domain/ticket.go index b1c000f..18cbf68 100644 --- a/internal/domain/ticket.go +++ b/internal/domain/ticket.go @@ -1,15 +1,33 @@ package domain +type TicketOutcome struct { + ID int64 + TicketID int64 + EventID int64 + OddID int64 +} + +type CreateTicketOutcome struct { + TicketID int64 + EventID int64 + OddID int64 +} + // ID will serve as the fast code since this doesn't need to be secure type Ticket struct { ID int64 - Outcomes []Outcome Amount Currency TotalOdds float32 } +type GetTicket struct { + ID int64 + Amount Currency + TotalOdds float32 + Outcomes []TicketOutcome +} + type CreateTicket struct { - Outcomes []int64 Amount Currency TotalOdds float32 } diff --git a/internal/domain/user.go b/internal/domain/user.go index 23004f4..df9aaba 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -42,7 +42,6 @@ type RegisterUserReq struct { OtpMedium OtpMedium } type CreateUserReq struct { - BranchID int64 FirstName string LastName string Email string diff --git a/internal/domain/wallet.go b/internal/domain/wallet.go index efb80ab..33e9466 100644 --- a/internal/domain/wallet.go +++ b/internal/domain/wallet.go @@ -3,23 +3,23 @@ package domain import "time" type Wallet struct { - ID int64 - Balance Currency - IsWithdraw bool - IsBettable bool + ID int64 + Balance Currency + IsWithdraw bool + IsBettable bool IsTransferable bool - IsActive bool - UserID int64 - UpdatedAt time.Time - CreatedAt time.Time + IsActive bool + UserID int64 + UpdatedAt time.Time + CreatedAt time.Time } type CustomerWallet struct { - ID int64 - RegularID int64 - StaticID int64 - CustomerID int64 - CompanyID int64 + ID int64 + RegularID int64 + StaticID int64 + CustomerID int64 + CompanyID int64 } type GetCustomerWallet struct { ID int64 @@ -34,11 +34,24 @@ type GetCustomerWallet struct { CreatedAt time.Time } +type BranchWallet struct { + ID int64 + Balance Currency + IsActive bool + Name string + Location string + BranchManagerID int64 + CompanyID int64 + IsSelfOwned bool + UpdatedAt time.Time + CreatedAt time.Time +} + type CreateWallet struct { - IsWithdraw bool - IsBettable bool + IsWithdraw bool + IsBettable bool IsTransferable bool - UserID int64 + UserID int64 } type CreateCustomerWallet struct { diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 210fcbd..5467d0f 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -30,6 +30,39 @@ func convertDBBet(bet dbgen.Bet) domain.Bet { } } +func convertDBBetOutcomes(bet dbgen.BetWithOutcome) domain.GetBet { + var outcomes []domain.BetOutcome = make([]domain.BetOutcome, len(bet.Outcomes)) + + for _, outcome := range bet.Outcomes { + outcomes = append(outcomes, domain.BetOutcome{ + ID: outcome.ID, + BetID: outcome.BetID, + EventID: outcome.EventID, + OddID: outcome.OddID, + }) + } + return domain.GetBet{ + ID: bet.ID, + Amount: domain.Currency(bet.Amount), + TotalOdds: bet.TotalOdds, + Status: domain.BetStatus(bet.Status), + FullName: bet.FullName, + PhoneNumber: bet.PhoneNumber, + BranchID: domain.ValidInt64{ + Value: bet.BranchID.Int64, + Valid: bet.BranchID.Valid, + }, + UserID: domain.ValidInt64{ + Value: bet.UserID.Int64, + Valid: bet.UserID.Valid, + }, + IsShopBet: bet.IsShopBet, + CashedOut: bet.CashedOut, + CashoutID: bet.CashoutID, + Outcomes: outcomes, + } +} + func convertCreateBet(bet domain.CreateBet) dbgen.CreateBetParams { return dbgen.CreateBetParams{ Amount: int64(bet.Amount), @@ -51,7 +84,6 @@ func convertCreateBet(bet domain.CreateBet) dbgen.CreateBetParams { } func (s *Store) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) { - newBet, err := s.queries.CreateBet(ctx, convertCreateBet(bet)) if err != nil { return domain.Bet{}, err @@ -60,25 +92,62 @@ func (s *Store) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet } -func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.Bet, error) { - bet, err := s.queries.GetBetByID(ctx, id) +func (s *Store) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBetOutcome) (int64, error) { + var dbParams []dbgen.CreateBetOutcomeParams = make([]dbgen.CreateBetOutcomeParams, 0, len(outcomes)) + + for _, outcome := range outcomes { + dbParams = append(dbParams, dbgen.CreateBetOutcomeParams{ + BetID: outcome.BetID, + EventID: outcome.EventID, + OddID: outcome.OddID, + }) + } + rows, err := s.queries.CreateBetOutcome(ctx, dbParams) + if err != nil { - return domain.Bet{}, err + return rows, err } - return convertDBBet(bet), nil + return rows, nil } -func (s *Store) GetAllBets(ctx context.Context) ([]domain.Bet, error) { +func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) { + bet, err := s.queries.GetBetByID(ctx, id) + if err != nil { + return domain.GetBet{}, err + } + + return convertDBBetOutcomes(bet), nil +} + +func (s *Store) GetAllBets(ctx context.Context) ([]domain.GetBet, error) { bets, err := s.queries.GetAllBets(ctx) if err != nil { return nil, err } - var result []domain.Bet = make([]domain.Bet, 0, len(bets)) + var result []domain.GetBet = make([]domain.GetBet, 0, len(bets)) for _, bet := range bets { - result = append(result, convertDBBet(bet)) + result = append(result, convertDBBetOutcomes(bet)) + } + + return result, nil +} + +func (s *Store) GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.GetBet, error) { + bets, err := s.queries.GetBetByBranchID(ctx, pgtype.Int8{ + Int64: BranchID, + Valid: true, + }) + + if err != nil { + return nil, err + } + + var result []domain.GetBet = make([]domain.GetBet, 0, len(bets)) + for _, bet := range bets { + result = append(result, convertDBBetOutcomes(bet)) } return result, nil diff --git a/internal/repository/branch.go b/internal/repository/branch.go index 6268615..6287380 100644 --- a/internal/repository/branch.go +++ b/internal/repository/branch.go @@ -154,6 +154,18 @@ func (s *Store) CreateSupportedOperation(ctx context.Context, supportedOperation }, nil } +func (s *Store) CreateBranchCashier(ctx context.Context, branchID int64, userID int64) error { + _, err := s.queries.CreateBranchCashier(ctx, dbgen.CreateBranchCashierParams{ + UserID: userID, + BranchID: branchID, + }) + + if err != nil { + return err + } + return nil +} + func (s *Store) GetAllSupportedOperations(ctx context.Context) ([]domain.SupportedOperation, error) { dbOperations, err := s.queries.GetAllSupportedOperations(ctx) if err != nil { @@ -188,6 +200,16 @@ func (s *Store) GetBranchOperations(ctx context.Context, branchID int64) ([]doma return branchOperations, nil } +func (s *Store) GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error) { + branch, err := s.queries.GetBranchByCashier(ctx, userID) + if err != nil { + return domain.Branch{}, err + } + + return convertDBBranch(branch), err +} + + func (s *Store) DeleteBranchOperation(ctx context.Context, branchID int64, operationID int64) error { err := s.queries.DeleteBranchOperation(ctx, dbgen.DeleteBranchOperationParams{ BranchID: branchID, @@ -195,3 +217,8 @@ func (s *Store) DeleteBranchOperation(ctx context.Context, branchID int64, opera }) return err } + +func (s *Store) DeleteBranchCashier(ctx context.Context, userID int64) error { + return s.queries.DeleteBranchCashier(ctx, userID) + +} diff --git a/internal/repository/ticket.go b/internal/repository/ticket.go index b7945c3..a2d5c1a 100644 --- a/internal/repository/ticket.go +++ b/internal/repository/ticket.go @@ -14,6 +14,27 @@ func convertDBTicket(ticket dbgen.Ticket) domain.Ticket { Amount: domain.Currency(ticket.Amount.Int64), TotalOdds: ticket.TotalOdds, } + +} + +func convertDBTicketOutcomes(ticket dbgen.TicketWithOutcome) domain.GetTicket { + + var outcomes []domain.TicketOutcome = make([]domain.TicketOutcome, 0, len(ticket.Outcomes)) + + for _, outcome := range ticket.Outcomes { + outcomes = append(outcomes, domain.TicketOutcome{ + ID: outcome.ID, + TicketID: outcome.TicketID, + EventID: outcome.EventID, + OddID: outcome.OddID, + }) + } + return domain.GetTicket{ + ID: ticket.ID, + Amount: domain.Currency(ticket.Amount.Int64), + TotalOdds: ticket.TotalOdds, + Outcomes: outcomes, + } } func convertCreateTicket(ticket domain.CreateTicket) dbgen.CreateTicketParams { @@ -35,25 +56,45 @@ func (s *Store) CreateTicket(ctx context.Context, ticket domain.CreateTicket) (d } -func (s *Store) GetTicketByID(ctx context.Context, id int64) (domain.Ticket, error) { - ticket, err := s.queries.GetTicketByID(ctx, id) - if err != nil { - return domain.Ticket{}, err +func (s *Store) CreateTicketOutcome(ctx context.Context, outcomes []domain.CreateTicketOutcome) (int64, error) { + + var dbParams []dbgen.CreateTicketOutcomeParams = make([]dbgen.CreateTicketOutcomeParams, 0, len(outcomes)) + for _, outcome := range outcomes { + dbParams = append(dbParams, dbgen.CreateTicketOutcomeParams{ + TicketID: outcome.TicketID, + EventID: outcome.EventID, + OddID: outcome.OddID, + }) } - return convertDBTicket(ticket), nil + rows, err := s.queries.CreateTicketOutcome(ctx, dbParams) + + if err != nil { + return rows, err + } + + return rows, nil } -func (s *Store) GetAllTickets(ctx context.Context) ([]domain.Ticket, error) { +func (s *Store) GetTicketByID(ctx context.Context, id int64) (domain.GetTicket, error) { + ticket, err := s.queries.GetTicketByID(ctx, id) + if err != nil { + return domain.GetTicket{}, err + } + + return convertDBTicketOutcomes(ticket), nil +} + +func (s *Store) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) { tickets, err := s.queries.GetAllTickets(ctx) if err != nil { return nil, err } - var result []domain.Ticket = make([]domain.Ticket, 0, len(tickets)) + var result []domain.GetTicket = make([]domain.GetTicket, 0, len(tickets)) for _, ticket := range tickets { - result = append(result, convertDBTicket(ticket)) + result = append(result, convertDBTicketOutcomes(ticket)) } return result, nil diff --git a/internal/repository/user.go b/internal/repository/user.go index c3aeacd..1b20834 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -102,6 +102,44 @@ func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.U return userList, nil } +func (s *Store) GetAllCashiers(ctx context.Context) ([]domain.User, error) { + users, err := s.queries.GetAllCashiers(ctx) + if err != nil { + return nil, err + } + userList := make([]domain.User, len(users)) + for i, user := range users { + userList[i] = domain.User{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email.String, + PhoneNumber: user.PhoneNumber.String, + Role: domain.Role(user.Role), + } + } + return userList, nil +} + +func (s *Store) GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error) { + users, err := s.queries.GetCashiersByBranch(ctx, branchID) + if err != nil { + return nil, err + } + userList := make([]domain.User, len(users)) + for i, user := range users { + userList[i] = domain.User{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email.String, + PhoneNumber: user.PhoneNumber.String, + Role: domain.Role(user.Role), + } + } + return userList, nil +} + func (s *Store) SearchUserByNameOrPhone(ctx context.Context, searchString string) ([]domain.User, error) { users, err := s.queries.SearchUserByNameOrPhone(ctx, pgtype.Text{ String: searchString, @@ -234,6 +272,40 @@ func (s *Store) UpdatePassword(ctx context.Context, identifier string, password } return nil } -func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.CreateUserReq) (domain.User, error) { - return domain.User{}, nil +func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.User) (domain.User, error) { + userRes, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{ + FirstName: user.FirstName, + LastName: user.LastName, + Email: pgtype.Text{ + String: user.Email, + Valid: user.Email != "", + }, + PhoneNumber: pgtype.Text{ + String: user.PhoneNumber, + Valid: user.PhoneNumber != "", + }, + Password: user.Password, + Role: string(user.Role), + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: pgtype.Timestamptz{ + Time: time.Now(), + Valid: true, + }, + UpdatedAt: pgtype.Timestamptz{ + Time: time.Now(), + Valid: true, + }, + }) + if err != nil { + return domain.User{}, err + } + return domain.User{ + ID: userRes.ID, + FirstName: userRes.FirstName, + LastName: userRes.LastName, + Email: userRes.Email.String, + PhoneNumber: userRes.PhoneNumber.String, + Role: domain.Role(userRes.Role), + }, nil } diff --git a/internal/repository/wallet.go b/internal/repository/wallet.go index 97a87e5..86bf670 100644 --- a/internal/repository/wallet.go +++ b/internal/repository/wallet.go @@ -129,6 +129,31 @@ func (s *Store) GetCustomerWallet(ctx context.Context, customerID int64, company return convertDBGetCustomerWallet(customerWallet), nil } +func (s *Store) GetAllBranchWallets(ctx context.Context) ([]domain.BranchWallet, error) { + wallets, err := s.queries.GetAllBranchWallets(ctx) + if err != nil { + return nil, err + } + + var result []domain.BranchWallet = make([]domain.BranchWallet, 0, len(wallets)) + + for _, wallet := range wallets { + result = append(result, domain.BranchWallet{ + ID: wallet.ID, + Balance: domain.Currency(wallet.Balance), + IsActive: wallet.IsActive, + UpdatedAt: wallet.UpdatedAt.Time, + CreatedAt: wallet.CreatedAt.Time, + Name: wallet.Name, + Location: wallet.Location, + BranchManagerID: wallet.BranchManagerID, + CompanyID: wallet.CompanyID, + IsSelfOwned: wallet.IsSelfOwned, + }) + } + return result, nil +} + func (s *Store) UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error { err := s.queries.UpdateBalance(ctx, dbgen.UpdateBalanceParams{ ID: id, diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index 1061b45..2c7c133 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -8,8 +8,10 @@ import ( type BetStore interface { CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) - GetBetByID(ctx context.Context, id int64) (domain.Bet, error) - GetAllBets(ctx context.Context) ([]domain.Bet, error) + CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBetOutcome) (int64, error) + GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) + GetAllBets(ctx context.Context) ([]domain.GetBet, error) + GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.GetBet, error) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error DeleteBet(ctx context.Context, id int64) error } diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 58b9cc5..83cbb27 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -19,13 +19,22 @@ func NewService(betStore BetStore) *Service { func (s *Service) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) { return s.betStore.CreateBet(ctx, bet) } -func (s *Service) GetBetByID(ctx context.Context, id int64) (domain.Bet, error) { + +func (s *Service) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBetOutcome) (int64, error) { + return s.betStore.CreateBetOutcome(ctx, outcomes) +} + +func (s *Service) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) { return s.betStore.GetBetByID(ctx, id) } -func (s *Service) GetAllBets(ctx context.Context) ([]domain.Bet, error) { +func (s *Service) GetAllBets(ctx context.Context) ([]domain.GetBet, error) { return s.betStore.GetAllBets(ctx) } +func (s *Service) GetBetByBranchID(ctx context.Context, branchID int64) ([]domain.GetBet, error) { + return s.betStore.GetBetByBranchID(ctx, branchID) +} + func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error { return s.betStore.UpdateCashOut(ctx, id, cashedOut) } diff --git a/internal/services/branch/port.go b/internal/services/branch/port.go index e2f2ec1..53e6b0d 100644 --- a/internal/services/branch/port.go +++ b/internal/services/branch/port.go @@ -8,16 +8,19 @@ import ( type BranchStore interface { CreateBranch(ctx context.Context, branch domain.CreateBranch) (domain.Branch, error) - CreateSupportedOperation(ctx context.Context, supportedOperation domain.CreateSupportedOperation) (domain.SupportedOperation, error) - CreateBranchOperation(ctx context.Context, branchOperation domain.CreateBranchOperation) error GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error) GetBranchByManagerID(ctx context.Context, branchManagerID int64) ([]domain.BranchDetail, error) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]domain.BranchDetail, error) - GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, error) - GetAllSupportedOperations(ctx context.Context) ([]domain.SupportedOperation, error) SearchBranchByName(ctx context.Context, name string) ([]domain.BranchDetail, error) UpdateBranch(ctx context.Context, id int64, branch domain.UpdateBranch) (domain.Branch, error) DeleteBranch(ctx context.Context, id int64) error + CreateBranchOperation(ctx context.Context, branchOperation domain.CreateBranchOperation) error + CreateSupportedOperation(ctx context.Context, supportedOperation domain.CreateSupportedOperation) (domain.SupportedOperation, error) + GetAllSupportedOperations(ctx context.Context) ([]domain.SupportedOperation, error) + GetBranchOperations(ctx context.Context, branchID int64) ([]domain.BranchOperation, error) DeleteBranchOperation(ctx context.Context, branchID int64, operationID int64) error + CreateBranchCashier(ctx context.Context, branchID int64, userID int64) error + GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error) + DeleteBranchCashier(ctx context.Context, userID int64) error } diff --git a/internal/services/branch/service.go b/internal/services/branch/service.go index 83d97e2..eddd1e9 100644 --- a/internal/services/branch/service.go +++ b/internal/services/branch/service.go @@ -25,6 +25,11 @@ func (s *Service) CreateSupportedOperation(ctx context.Context, supportedOperati func (s *Service) CreateBranchOperation(ctx context.Context, branchOperation domain.CreateBranchOperation) error { return s.branchStore.CreateBranchOperation(ctx, branchOperation) } + +func (s *Service) CreateBranchCashier(ctx context.Context, branchID int64, userID int64) error { + return s.branchStore.CreateBranchCashier(ctx, branchID, userID) +} + func (s *Service) GetBranchByID(ctx context.Context, id int64) (domain.BranchDetail, error) { return s.branchStore.GetBranchByID(ctx, id) } @@ -41,6 +46,10 @@ func (s *Service) GetAllBranches(ctx context.Context) ([]domain.BranchDetail, er return s.branchStore.GetAllBranches(ctx) } +func (s *Service) GetBranchByCashier(ctx context.Context, userID int64) (domain.Branch, error) { + return s.branchStore.GetBranchByCashier(ctx, userID) +} + func (s *Service) GetAllSupportedOperations(ctx context.Context) ([]domain.SupportedOperation, error) { return s.branchStore.GetAllSupportedOperations(ctx) } @@ -57,3 +66,7 @@ func (s *Service) DeleteBranch(ctx context.Context, id int64) error { func (s *Service) DeleteBranchOperation(ctx context.Context, branchID int64, operationID int64) error { return s.branchStore.DeleteBranchOperation(ctx, branchID, operationID) } + +func (s *Service) DeleteBranchCashier(ctx context.Context, userID int64) error { + return s.branchStore.DeleteBranchCashier(ctx, userID) +} diff --git a/internal/services/ticket/port.go b/internal/services/ticket/port.go index 042d27a..ae531c6 100644 --- a/internal/services/ticket/port.go +++ b/internal/services/ticket/port.go @@ -8,8 +8,9 @@ import ( type TicketStore interface { CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) - GetTicketByID(ctx context.Context, id int64) (domain.Ticket, error) - GetAllTickets(ctx context.Context) ([]domain.Ticket, error) + CreateTicketOutcome(ctx context.Context, outcomes []domain.CreateTicketOutcome) (int64, error) + GetTicketByID(ctx context.Context, id int64) (domain.GetTicket, error) + GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) DeleteOldTickets(ctx context.Context) error DeleteTicket(ctx context.Context, id int64) error } diff --git a/internal/services/ticket/service.go b/internal/services/ticket/service.go index 5779ce4..46036f4 100644 --- a/internal/services/ticket/service.go +++ b/internal/services/ticket/service.go @@ -19,10 +19,15 @@ func NewService(ticketStore TicketStore) *Service { func (s *Service) CreateTicket(ctx context.Context, ticket domain.CreateTicket) (domain.Ticket, error) { return s.ticketStore.CreateTicket(ctx, ticket) } -func (s *Service) GetTicketByID(ctx context.Context, id int64) (domain.Ticket, error) { + +func (s *Service) CreateTicketOutcome(ctx context.Context, outcomes []domain.CreateTicketOutcome) (int64, error) { + return s.ticketStore.CreateTicketOutcome(ctx, outcomes) +} + +func (s *Service) GetTicketByID(ctx context.Context, id int64) (domain.GetTicket, error) { return s.ticketStore.GetTicketByID(ctx, id) } -func (s *Service) GetAllTickets(ctx context.Context) ([]domain.Ticket, error) { +func (s *Service) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) { return s.ticketStore.GetAllTickets(ctx) } func (s *Service) DeleteTicket(ctx context.Context, id int64) error { diff --git a/internal/services/user/direct.go b/internal/services/user/direct.go index b5fca74..f848792 100644 --- a/internal/services/user/direct.go +++ b/internal/services/user/direct.go @@ -19,8 +19,21 @@ func (s *Service) CreateUser(ctx context.Context, User domain.CreateUserReq) (do // User.BranchID = branchId // User.Role = string(domain.RoleBranchManager) // } + hashedPassword, err := hashPassword(User.Password) + if err != nil { + return domain.User{}, err + } - return s.userStore.CreateUserWithoutOtp(ctx, User) + return s.userStore.CreateUserWithoutOtp(ctx, domain.User{ + FirstName: User.FirstName, + LastName: User.LastName, + Email: User.Email, + PhoneNumber: User.PhoneNumber, + Password: hashedPassword, + Role: domain.Role(User.Role), + EmailVerified: true, + PhoneVerified: true, + }) } func (s *Service) DeleteUser(ctx context.Context, id int64) error { @@ -51,3 +64,11 @@ func (s *Service) GetUserById(ctx context.Context, id int64) (domain.User, error return s.userStore.GetUserByID(ctx, id) } + +func (s *Service) GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error) { + return s.userStore.GetCashiersByBranch(ctx, branchID) +} + +func (s *Service) GetAllCashiers(ctx context.Context) ([]domain.User, error) { + return s.userStore.GetAllCashiers(ctx) +} diff --git a/internal/services/user/port.go b/internal/services/user/port.go index 7d4c5e3..9b8d06e 100644 --- a/internal/services/user/port.go +++ b/internal/services/user/port.go @@ -8,9 +8,11 @@ import ( type UserStore interface { CreateUser(ctx context.Context, user domain.User, usedOtpId int64) (domain.User, error) - CreateUserWithoutOtp(ctx context.Context, user domain.CreateUserReq) (domain.User, error) + CreateUserWithoutOtp(ctx context.Context, user domain.User) (domain.User, error) GetUserByID(ctx context.Context, id int64) (domain.User, error) GetAllUsers(ctx context.Context, filter Filter) ([]domain.User, error) + GetAllCashiers(ctx context.Context) ([]domain.User, error) + GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error DeleteUser(ctx context.Context, id int64) error CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) diff --git a/internal/services/wallet/port.go b/internal/services/wallet/port.go index b9eb043..9271039 100644 --- a/internal/services/wallet/port.go +++ b/internal/services/wallet/port.go @@ -13,6 +13,7 @@ type WalletStore interface { GetAllWallets(ctx context.Context) ([]domain.Wallet, error) GetWalletsByUser(ctx context.Context, id int64) ([]domain.Wallet, error) GetCustomerWallet(ctx context.Context, customerID int64, companyID int64) (domain.GetCustomerWallet, error) + GetAllBranchWallets(ctx context.Context) ([]domain.BranchWallet, error) UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error UpdateWalletActive(ctx context.Context, id int64, isActive bool) error } @@ -24,4 +25,3 @@ type TransferStore interface { GetTransferByID(ctx context.Context, id int64) (domain.Transfer, error) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error } - diff --git a/internal/services/wallet/transfer.go b/internal/services/wallet/transfer.go index 6f0f0d4..387f255 100644 --- a/internal/services/wallet/transfer.go +++ b/internal/services/wallet/transfer.go @@ -23,10 +23,43 @@ func (s *Service) GetTransferByID(ctx context.Context, id int64) (domain.Transfe return s.transferStore.GetTransferByID(ctx, id) } +func (s *Service) GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) { + return s.transferStore.GetTransfersByWallet(ctx, walletID) +} + func (s *Service) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error { return s.transferStore.UpdateTransferVerification(ctx, id, verified) } +func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { + receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID) + if err != nil { + return domain.Transfer{}, err + } + + // Add to receiver + err = s.walletStore.UpdateBalance(ctx, receiverWallet.ID, receiverWallet.Balance+transfer.Amount) + if err != nil { + return domain.Transfer{}, err + } + + // Log the transfer so that if there is a mistake, it can be reverted + newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ + CashierID: transfer.CashierID, + ReceiverWalletID: receiverWallet.ID, + Amount: transfer.Amount, + Type: domain.DEPOSIT, + PaymentMethod: transfer.PaymentMethod, + Verified: true, + }) + if err != nil { + return domain.Transfer{}, err + } + + return newTransfer, nil + +} + func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiverID int64, amount domain.Currency, paymentMethod domain.PaymentMethod, cashierID domain.ValidInt64) (domain.Transfer, error) { senderWallet, err := s.GetWalletByID(ctx, senderID) diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go index c95a660..2644a39 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -7,7 +7,6 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) - var ( ErrBalanceInsufficient = errors.New("wallet balance is insufficient") ) @@ -62,6 +61,10 @@ func (s *Service) GetCustomerWallet(ctx context.Context, customerID int64, compa return s.walletStore.GetCustomerWallet(ctx, customerID, companyID) } +func (s *Service) GetAllBranchWallets(ctx context.Context) ([]domain.BranchWallet, error) { + return s.walletStore.GetAllBranchWallets(ctx) +} + func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error { return s.walletStore.UpdateBalance(ctx, id, balance) } diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index dab2e1d..efd3884 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -1,44 +1,106 @@ package handlers import ( + "encoding/json" "log/slog" "strconv" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" "github.com/gofiber/fiber/v2" "github.com/google/uuid" ) +type BetOutcome struct { + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` +} +type NullableInt64 struct { + Value int64 + Valid bool +} + +func (n *NullableInt64) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + n.Valid = false + return nil + } + + var value int64 + if err := json.Unmarshal(data, &value); err != nil { + return err + } + + n.Value = value + n.Valid = true + return nil +} + +func (n NullableInt64) MarshalJSON() ([]byte, error) { + if !n.Valid { + return []byte("null"), nil + } + return json.Marshal(n.Value) +} + type CreateBetReq struct { - Outcomes []int64 `json:"outcomes"` + Outcomes []BetOutcome `json:"outcomes"` Amount float32 `json:"amount" example:"100.0"` TotalOdds float32 `json:"total_odds" example:"4.22"` Status domain.BetStatus `json:"status" example:"1"` FullName string `json:"full_name" example:"John"` PhoneNumber string `json:"phone_number" example:"1234567890"` IsShopBet bool `json:"is_shop_bet" example:"false"` + BranchID NullableInt64 `json:"branch_id" 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.BetStatus `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"` +} type BetRes struct { - ID int64 `json:"id" example:"1"` - Outcomes []domain.Outcome `json:"outcomes"` - Amount float32 `json:"amount" example:"100.0"` - TotalOdds float32 `json:"total_odds" example:"4.22"` - Status domain.BetStatus `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"` + 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.BetStatus `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"` } -func convertBet(bet domain.Bet) BetRes { +func convertCreateBet(bet domain.Bet, createdNumber int64) CreateBetRes { + return CreateBetRes{ + ID: bet.ID, + Amount: bet.Amount.Float64(), + TotalOdds: bet.TotalOdds, + Status: bet.Status, + FullName: bet.FullName, + PhoneNumber: bet.PhoneNumber, + BranchID: bet.BranchID.Value, + UserID: bet.UserID.Value, + CreatedNumber: createdNumber, + } +} + +func convertBet(bet domain.GetBet) BetRes { return BetRes{ ID: bet.ID, - Outcomes: bet.Outcomes, Amount: bet.Amount.Float64(), TotalOdds: bet.TotalOdds, Status: bet.Status, @@ -60,15 +122,12 @@ func convertBet(bet domain.Bet) BetRes { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /bet [post] -func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { +func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, branchSvc *branch.Service, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { - // TODO if user is customer, get id from the token then get the wallet id from there - // TODO: If user is a cashier, check the token, and find the role and get the branch id from there. Reduce amount from the branch wallet - - var isShopBet bool = true - var branchID int64 = 1 - var userID int64 + // Get user_id from middleware + userID := c.Locals("user_id").(int64) + var isShopBet bool var req CreateBetReq @@ -85,12 +144,46 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalida return nil } + user, err := userSvc.GetUserByID(c.Context(), userID) + + if user.Role != domain.RoleCustomer { + isShopBet = true + if !req.BranchID.Valid { + logger.Error("CreateBetReq failed, branch id necessary") + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Branch ID necessary", + }) + } + + // Get the branch from the branch ID + branch, err := branchSvc.GetBranchByID(c.Context(), req.BranchID.Value) + if err != nil { + logger.Error("CreateBetReq failed, branch id invalid") + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Branch ID invalid", + }) + } + + // Deduct a percentage of the amount + var deductedAmount = req.Amount / 10 + err = walletSvc.DeductFromWallet(c.Context(), branch.WalletID, domain.Currency(deductedAmount)) + + if err != nil { + logger.Error("CreateBetReq failed, unable to deduct from WalletID", branch.WalletID) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Unable to deduct from branch wallet", + }) + } + } else { + isShopBet = false + // TODO if user is customer, get id from the token then get the wallet id from there and reduce the amount + } + // TODO Validate Outcomes Here and make sure they didn't expire cashoutUUID := uuid.New() bet, err := betSvc.CreateBet(c.Context(), domain.CreateBet{ - Outcomes: req.Outcomes, Amount: domain.Currency(req.Amount), TotalOdds: req.TotalOdds, Status: req.Status, @@ -98,7 +191,7 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalida PhoneNumber: req.PhoneNumber, BranchID: domain.ValidInt64{ - Value: branchID, + Value: req.BranchID.Value, Valid: isShopBet, }, UserID: domain.ValidInt64{ @@ -113,8 +206,25 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalida logger.Error("CreateBetReq failed", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) } + var outcomes []domain.CreateBetOutcome = make([]domain.CreateBetOutcome, 0, len(req.Outcomes)) - res := convertBet(bet) + for _, outcome := range req.Outcomes { + outcomes = append(outcomes, domain.CreateBetOutcome{ + BetID: bet.ID, + EventID: outcome.EventID, + OddID: outcome.OddID, + }) + } + rows, err := betSvc.CreateBetOutcome(c.Context(), outcomes) + + if err != nil { + 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) } diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index 6a2bf16..ab97316 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -5,6 +5,7 @@ import ( "strconv" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" @@ -43,6 +44,7 @@ type BranchOperationRes struct { } type BranchRes struct { + ID int64 `json:"id" example:"1"` Name string `json:"name" example:"4-kilo Branch"` Location string `json:"location" example:"Addis Ababa"` WalletID int64 `json:"wallet_id" example:"1"` @@ -52,6 +54,7 @@ type BranchRes struct { } type BranchDetailRes struct { + ID int64 `json:"id" example:"1"` Name string `json:"name" example:"4-kilo Branch"` Location string `json:"location" example:"Addis Ababa"` WalletID int64 `json:"wallet_id" example:"1"` @@ -64,6 +67,7 @@ type BranchDetailRes struct { func convertBranch(branch domain.Branch) BranchRes { return BranchRes{ + ID: branch.ID, Name: branch.Name, Location: branch.Location, WalletID: branch.WalletID, @@ -75,6 +79,7 @@ func convertBranch(branch domain.Branch) BranchRes { func convertBranchDetail(branch domain.BranchDetail) BranchDetailRes { return BranchDetailRes{ + ID: branch.ID, Name: branch.Name, Location: branch.Location, WalletID: branch.WalletID, @@ -393,6 +398,42 @@ func GetAllBranches(logger *slog.Logger, branchSvc *branch.Service, validator *c } } +// SearchBranch godoc +// @Summary Search branches +// @Description Search branches by name or location +// @Tags branch +// @Accept json +// @Produce json +// @Param q query string true "Search query" +// @Success 200 {array} BranchDetailRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /search/branch [get] +func SearchBranch(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + // Get search query from request + searchQuery := c.Query("q") + if searchQuery == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Search query is required", nil, nil) + } + + // Call the service to search for branches + branches, err := branchSvc.SearchBranchByName(c.Context(), searchQuery) + if err != nil { + logger.Error("Failed to search branches", "query", searchQuery, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to search branches", err, nil) + } + + // Convert branches to response format + var result []BranchDetailRes + for _, branch := range branches { + result = append(result, convertBranchDetail(branch)) + } + + return response.WriteJSON(c, fiber.StatusOK, "Branches retrieved successfully", result, nil) + } +} + // GetAllSupportedOperations godoc // @Summary Gets all supported operations // @Description Gets all supported operations @@ -465,6 +506,41 @@ func GetBranchOperations(logger *slog.Logger, branchSvc *branch.Service, validat } } +// GetBetByBranchID godoc +// @Summary Gets bets by its branch id +// @Description Gets bets by its branch id +// @Tags branch +// @Accept json +// @Produce json +// @Success 200 {array} BetRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /branch/{id}/bets [get] +func GetBetByBranchID(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + branchID := c.Params("id") + id, err := strconv.ParseInt(branchID, 10, 64) + if err != nil { + logger.Error("Invalid branch ID", "branchID", branchID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid branch ID", err, nil) + } + + bets, err := betSvc.GetBetByBranchID(c.Context(), id) + + if err != nil { + logger.Error("Failed to get bets", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bets", err, nil) + } + + var res []BetRes = make([]BetRes, 0, len(bets)) + for _, bet := range bets { + res = append(res, convertBet(bet)) + } + + return response.WriteJSON(c, fiber.StatusOK, "Branch Bets Retrieved", res, nil) + } +} + // UpdateBranch godoc // @Summary Updates a branch // @Description Updates a branch diff --git a/internal/web_server/handlers/cashier.go b/internal/web_server/handlers/cashier.go index 6ad9874..b2b6418 100644 --- a/internal/web_server/handlers/cashier.go +++ b/internal/web_server/handlers/cashier.go @@ -3,8 +3,10 @@ package handlers import ( "log/slog" "strconv" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" @@ -17,6 +19,7 @@ type CreateCashierReq struct { Email string `json:"email" example:"john.doe@example.com"` PhoneNumber string `json:"phone_number" example:"1234567890"` Password string `json:"password" example:"password123"` + BranchID int64 `json:"branch_id" example:"1"` } // CreateCashier godoc @@ -31,10 +34,9 @@ type CreateCashierReq struct { // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /cashiers [post] -func CreateCashier(logger *slog.Logger, userSvc *user.Service, validator *customvalidator.CustomValidator) fiber.Handler { +func CreateCashier(logger *slog.Logger, userSvc *user.Service, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { - creatorBranch := c.Locals("branch_id").(int64) var req CreateCashierReq if err := c.BodyParser(&req); err != nil { logger.Error("RegisterUser failed", "error", err) @@ -47,27 +49,49 @@ func CreateCashier(logger *slog.Logger, userSvc *user.Service, validator *custom response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return nil } - user := domain.CreateUserReq{ + userRequest := domain.CreateUserReq{ FirstName: req.FirstName, LastName: req.LastName, Email: req.Email, PhoneNumber: req.PhoneNumber, Password: req.Password, Role: string(domain.RoleCashier), - BranchID: creatorBranch, } - _, err := userSvc.CreateUser(c.Context(), user) + newUser, err := userSvc.CreateUser(c.Context(), userRequest) if err != nil { logger.Error("CreateCashier failed", "error", err) response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create cashier", nil, nil) return nil } + + err = branchSvc.CreateBranchCashier(c.Context(), req.BranchID, newUser.ID) + if err != nil { + logger.Error("CreateCashier failed", "error", err) + response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create cashier", nil, nil) + return nil + } + response.WriteJSON(c, fiber.StatusOK, "Cashier created successfully", nil, nil) return nil } } +type GetCashierRes 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 domain.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"` +} + // GetAllCashiers godoc // @Summary Get all cashiers // @Description Get all cashiers @@ -83,28 +107,48 @@ func CreateCashier(logger *slog.Logger, userSvc *user.Service, validator *custom // @Router /cashiers [get] func GetAllCashiers(logger *slog.Logger, userSvc *user.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { - branchId := int64(12) //c.Locals("branch_id").(int64) - filter := user.Filter{ - Role: string(domain.RoleCashier), - BranchId: user.ValidBranchId{ - Value: branchId, - Valid: true, - }, - Page: c.QueryInt("page", 1), - PageSize: c.QueryInt("page_size", 10), - } - valErrs, ok := validator.Validate(c, filter) - if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil - } - cashiers, err := userSvc.GetAllUsers(c.Context(), filter) + // branchId := int64(12) //c.Locals("branch_id").(int64) + // filter := user.Filter{ + // Role: string(domain.RoleCashier), + // BranchId: user.ValidBranchId{ + // Value: branchId, + // Valid: true, + // }, + // Page: c.QueryInt("page", 1), + // PageSize: c.QueryInt("page_size", 10), + // } + // valErrs, ok := validator.Validate(c, filter) + // if !ok { + // response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + // return nil + // } + + cashiers, err := userSvc.GetAllCashiers(c.Context()) if err != nil { logger.Error("GetAllCashiers failed", "error", err) response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil) return nil } - response.WriteJSON(c, fiber.StatusOK, "Cashiers retrieved successfully", cashiers, nil) + + var result []GetCashierRes + + for _, cashier := range cashiers { + 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, + }) + } + response.WriteJSON(c, fiber.StatusOK, "Cashiers retrieved successfully", result, nil) return nil } diff --git a/internal/web_server/handlers/manager.go b/internal/web_server/handlers/manager.go index d0ba39a..be428c9 100644 --- a/internal/web_server/handlers/manager.go +++ b/internal/web_server/handlers/manager.go @@ -17,7 +17,6 @@ type CreateManagerReq struct { Email string `json:"email" example:"john.doe@example.com"` PhoneNumber string `json:"phone_number" example:"1234567890"` Password string `json:"password" example:"password123"` - BranchId int64 `json:"branch_id" example:"1"` } // CreateManagers godoc @@ -53,7 +52,6 @@ func CreateManager(logger *slog.Logger, userSvc *user.Service, validator *custom PhoneNumber: req.PhoneNumber, Password: req.Password, Role: string(domain.RoleBranchManager), - BranchID: req.BranchId, } _, err := userSvc.CreateUser(c.Context(), user) if err != nil { diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index 05339d4..a5bf943 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -11,13 +11,19 @@ import ( "github.com/gofiber/fiber/v2" ) +type TicketOutcome struct { + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` +} + type CreateTicketReq struct { - Outcomes []int64 `json:"outcomes"` - Amount float32 `json:"amount" example:"100.0"` - TotalOdds float32 `json:"total_odds" example:"4.22"` + Outcomes []TicketOutcome `json:"outcomes"` + Amount float32 `json:"amount" example:"100.0"` + TotalOdds float32 `json:"total_odds" example:"4.22"` } type CreateTicketRes struct { - FastCode int64 `json:"fast_code" example:"1234"` + FastCode int64 `json:"fast_code" example:"1234"` + CreatedNumber int64 `json:"created_number" example:"3"` } // CreateTicket godoc @@ -51,7 +57,6 @@ func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, // TODO Validate Outcomes Here and make sure they didn't expire ticket, err := ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{ - Outcomes: req.Outcomes, Amount: domain.Currency(req.Amount), TotalOdds: req.TotalOdds, }) @@ -61,18 +66,37 @@ func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, "error": "Internal server error", }) } + + var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes)) + + for _, outcome := range req.Outcomes { + outcomes = append(outcomes, domain.CreateTicketOutcome{ + TicketID: ticket.ID, + EventID: outcome.EventID, + OddID: outcome.OddID, + }) + } + rows, err := ticketSvc.CreateTicketOutcome(c.Context(), outcomes) + + if err != nil { + logger.Error("CreateTicketReq failed to create outcomes", "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Internal server error", + }) + } res := CreateTicketRes{ - FastCode: ticket.ID, + FastCode: ticket.ID, + CreatedNumber: rows, } return response.WriteJSON(c, fiber.StatusOK, "Ticket Created", res, nil) } } type TicketRes struct { - ID int64 `json:"id" example:"1"` - Outcomes []domain.Outcome `json:"outcomes"` - Amount float32 `json:"amount" example:"100.0"` - TotalOdds float32 `json:"total_odds" example:"4.22"` + ID int64 `json:"id" example:"1"` + Outcomes []domain.TicketOutcome `json:"outcomes"` + Amount float32 `json:"amount" example:"100.0"` + TotalOdds float32 `json:"total_odds" example:"4.22"` } // GetTicketByID godoc diff --git a/internal/web_server/handlers/transaction_handler.go b/internal/web_server/handlers/transaction_handler.go index cd441fb..1b5e9fa 100644 --- a/internal/web_server/handlers/transaction_handler.go +++ b/internal/web_server/handlers/transaction_handler.go @@ -153,6 +153,9 @@ func GetAllTransactions( // Check user role and fetch transactions accordingly switch user.Role { + case domain.RoleSuperAdmin: + // Admin can fetch all transactions + transactions, err = transactionSvc.GetAllTransactions(c.Context()) case domain.RoleAdmin: // Admin can fetch all transactions transactions, err = transactionSvc.GetAllTransactions(c.Context()) diff --git a/internal/web_server/handlers/transfer_handler.go b/internal/web_server/handlers/transfer_handler.go index 4ff7810..c3d71af 100644 --- a/internal/web_server/handlers/transfer_handler.go +++ b/internal/web_server/handlers/transfer_handler.go @@ -2,6 +2,7 @@ package handlers import ( "log/slog" + "strconv" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -12,7 +13,19 @@ import ( "github.com/gofiber/fiber/v2" ) -type TransferRes struct { +type TransferWalletRes struct { + ID int64 `json:"id" example:"1"` + Amount float32 `json:"amount" example:"100.0"` + Verified bool `json:"verified" example:"true"` + Type string `json:"type" example:"transfer"` + PaymentMethod string `json:"payment_method" example:"bank"` + ReceiverWalletID int64 `json:"receiver_wallet_id" example:"1"` + SenderWalletID *int64 `json:"sender_wallet_id" example:"1"` + CashierID *int64 `json:"cashier_id" example:"789"` + CreatedAt time.Time `json:"created_at" example:"2025-04-08T12:00:00Z"` + UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:30:00Z"` +} +type RefillRes struct { ID int64 `json:"id" example:"1"` Amount float32 `json:"amount" example:"100.0"` Verified bool `json:"verified" example:"true"` @@ -25,7 +38,7 @@ type TransferRes struct { UpdatedAt time.Time `json:"updated_at" example:"2025-04-08T12:30:00Z"` } -func convertTransfer(transfer domain.Transfer) TransferRes { +func convertTransfer(transfer domain.Transfer) TransferWalletRes { var senderWalletID *int64 if transfer.SenderWalletID.Valid { senderWalletID = &transfer.SenderWalletID.Value @@ -36,7 +49,7 @@ func convertTransfer(transfer domain.Transfer) TransferRes { cashierID = &transfer.CashierID.Value } - return TransferRes{ + return TransferWalletRes{ ID: transfer.ID, Amount: transfer.Amount.Float64(), Verified: transfer.Verified, @@ -51,11 +64,47 @@ func convertTransfer(transfer domain.Transfer) TransferRes { } type CreateTransferReq struct { - ReceiverID int64 `json:"receiver_id" example:"1"` Amount float64 `json:"amount" example:"100.0"` PaymentMethod string `json:"payment_method" example:"cash"` } +// GetTransfersByWallet godoc +// @Summary Get transfer by wallet +// @Description Get transfer by wallet +// @Tags transfer +// @Accept json +// @Produce json +// @Param transferToWallet body CreateTransferReq true "Create Transfer" +// @Success 200 {object} TransferWalletRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /transfer/wallet/{id} [get] +func GetTransfersByWallet(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + walletID := c.Params("id") + + id, err := strconv.ParseInt(walletID, 10, 64) + + if err != nil { + logger.Error("Invalid wallet ID", "walletID", walletID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid wallet ID", err, nil) + } + + transfers, err := walletSvc.GetTransfersByWallet(c.Context(), int64(id)) + if err != nil { + logger.Error("Failed to get transfers by wallet", "walletID", walletID, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve transfers", err, nil) + } + + var transferResponses []TransferWalletRes + for _, transfer := range transfers { + transferResponses = append(transferResponses, convertTransfer(transfer)) + } + + return response.WriteJSON(c, fiber.StatusOK, "Transfers retrieved successfully", transferResponses, nil) + } +} + // TransferToWallet godoc // @Summary Create a transfer to wallet // @Description Create a transfer to wallet @@ -63,26 +112,42 @@ type CreateTransferReq struct { // @Accept json // @Produce json // @Param transferToWallet body CreateTransferReq true "Create Transfer" -// @Success 200 {object} TransferRes +// @Success 200 {object} TransferWalletRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /transfer/wallet [post] +// @Router /transfer/wallet/:id [post] func TransferToWallet(logger *slog.Logger, walletSvc *wallet.Service, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { + + receiverIDString := c.Params("id") + + receiverID, err := strconv.ParseInt(receiverIDString, 10, 64) + + if err != nil { + logger.Error("Invalid wallet ID", "walletID", receiverID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid wallet ID", err, nil) + } // Get sender ID from the cashier userID := c.Locals("user_id").(int64) role := string(c.Locals("role").(domain.Role)) - branchID := c.Locals("branch_id").(int64) + + var senderID int64 if role == string(domain.RoleCustomer) { logger.Error("Unauthorized access", "userID", userID, "role", role) return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) - } - - branchWallet, err := branchSvc.GetBranchByID(c.Context(), branchID) - if err != nil { - logger.Error("Failed to get branch wallet", "branch ID", branchID, "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve branch wallet", err, nil) + } else if role == string(domain.RoleBranchManager) || role == string(domain.RoleAdmin) || role == string(domain.RoleSuperAdmin) { + // TODO Add a way for admins to reference branch wallet + senderID = 0 + logger.Error("Will", "userID", userID, "role", role) + return response.WriteJSON(c, fiber.StatusBadRequest, "Unauthorized access", nil, nil) + } else { + cashierBranch, err := branchSvc.GetBranchByCashier(c.Context(), userID) + if err != nil { + logger.Error("Failed to get branch", "user ID", userID, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve cashier branch", err, nil) + } + senderID = cashierBranch.WalletID } var req CreateTransferReq @@ -100,7 +165,7 @@ func TransferToWallet(logger *slog.Logger, walletSvc *wallet.Service, branchSvc return nil } - transfer, err := walletSvc.TransferToWallet(c.Context(), branchWallet.ID, req.ReceiverID, domain.Currency(req.Amount), domain.PaymentMethod(req.PaymentMethod), domain.ValidInt64{Value: userID, Valid: true}) + transfer, err := walletSvc.TransferToWallet(c.Context(), senderID, receiverID, domain.Currency(req.Amount), domain.PaymentMethod(req.PaymentMethod), domain.ValidInt64{Value: userID, Valid: true}) if !ok { response.WriteJSON(c, fiber.StatusInternalServerError, "Transfer Failed", err, nil) @@ -113,3 +178,72 @@ func TransferToWallet(logger *slog.Logger, walletSvc *wallet.Service, branchSvc } } + +// RefillWallet godoc +// @Summary Refill wallet +// @Description Super Admin route to refill a wallet +// @Tags transfer +// @Accept json +// @Produce json +// @Param refillWallet body CreateTransferReq true "Create Transfer" +// @Success 200 {object} TransferWalletRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /transfer/refill/:id [post] +func RefillWallet(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + + receiverIDString := c.Params("id") + + receiverID, err := strconv.ParseInt(receiverIDString, 10, 64) + + if err != nil { + logger.Error("Invalid wallet ID", "walletID", receiverID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid wallet ID", err, nil) + } + // Get sender ID from the cashier + userID := c.Locals("user_id").(int64) + role := string(c.Locals("role").(domain.Role)) + + if role != string(domain.RoleSuperAdmin) { + logger.Error("Unauthorized access", "userID", userID, "role", role) + return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) + } + + var req CreateTransferReq + + if err := c.BodyParser(&req); err != nil { + logger.Error("CreateTransferReq failed", "error", err) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Invalid request", + }) + } + + valErrs, ok := validator.Validate(c, req) + if !ok { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + return nil + } + + transfer, err := walletSvc.RefillWallet(c.Context(), domain.CreateTransfer{ + Amount: domain.Currency(req.Amount), + PaymentMethod: domain.PaymentMethod(req.PaymentMethod), + ReceiverWalletID: receiverID, + CashierID: domain.ValidInt64{ + Value: userID, + Valid: true, + }, + Type: domain.TransferType("deposit"), + }) + + if !ok { + response.WriteJSON(c, fiber.StatusInternalServerError, "Creating Transfer Failed", err, nil) + return nil + } + + res := convertTransfer(transfer) + + return response.WriteJSON(c, fiber.StatusOK, "Transfer Successful", res, nil) + + } +} diff --git a/internal/web_server/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index de655f6..37b72f4 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -66,6 +66,19 @@ func convertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes { } } +type BranchWalletRes struct { + ID int64 `json:"id" example:"1"` + Balance float32 `json:"balance" example:"100.0"` + IsActive bool `json:"is_active" example:"true"` + Name string `json:"name" example:"true"` + Location string `json:"location" example:"somewhere"` + BranchManagerID int64 `json:"branch_manager_id" example:"1"` + CompanyID int64 `json:"company_id" example:"1"` + IsSelfOwned bool `json:"is_self_owned" example:"false"` + UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` +} + // GetWalletByID godoc // @Summary Get wallet by ID // @Description Retrieve wallet details by wallet ID @@ -112,10 +125,12 @@ func GetWalletByID(logger *slog.Logger, walletSvc *wallet.Service, validator *cu // @Router /wallet [get] func GetAllWallets(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { + wallets, err := walletSvc.GetAllWallets(c.Context()) if err != nil { logger.Error("Failed to get wallets", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve wallets", err, nil) } var res []WalletRes = make([]WalletRes, 0, len(wallets)) @@ -128,6 +143,47 @@ func GetAllWallets(logger *slog.Logger, walletSvc *wallet.Service, validator *cu } } +// GetAllBranchWallets godoc +// @Summary Get all branch wallets +// @Description Retrieve all branch wallets +// @Tags wallet +// @Accept json +// @Produce json +// @Success 200 {array} WalletRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /branchWallet [get] +func GetAllBranchWallets(logger *slog.Logger, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + + wallets, err := walletSvc.GetAllBranchWallets(c.Context()) + + if err != nil { + logger.Error("Failed to get wallets", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve wallets", err, nil) + } + + var res []BranchWalletRes = make([]BranchWalletRes, 0, len(wallets)) + + for _, wallet := range wallets { + res = append(res, BranchWalletRes{ + ID: wallet.ID, + Balance: wallet.Balance.Float64(), + IsActive: wallet.IsActive, + Name: wallet.Name, + Location: wallet.Location, + BranchManagerID: wallet.BranchManagerID, + CompanyID: wallet.CompanyID, + IsSelfOwned: wallet.IsSelfOwned, + UpdatedAt: wallet.UpdatedAt, + CreatedAt: wallet.CreatedAt, + }) + } + + return response.WriteJSON(c, fiber.StatusOK, "All Wallets retrieved", res, nil) + } +} + type UpdateWalletActiveReq struct { IsActive bool } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 7e3fd08..2e5fc08 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -42,7 +42,7 @@ func (a *App) initAppRoutes() { // //, a.authMiddleware a.fiber.Get("/cashiers", handlers.GetAllCashiers(a.logger, a.userSvc, a.validator)) - a.fiber.Post("/cashiers", handlers.CreateCashier(a.logger, a.userSvc, a.validator)) + a.fiber.Post("/cashiers", handlers.CreateCashier(a.logger, a.userSvc, a.branchSvc, a.validator)) a.fiber.Put("/cashiers/:id", handlers.UpdateCashier(a.logger, a.userSvc, a.validator)) // @@ -64,8 +64,11 @@ func (a *App) initAppRoutes() { a.fiber.Post("/branch", handlers.CreateBranch(a.logger, a.branchSvc, a.walletSvc, a.validator)) a.fiber.Get("/branch", handlers.GetAllBranches(a.logger, a.branchSvc, a.validator)) a.fiber.Get("/branch/:id", handlers.GetBranchByID(a.logger, a.branchSvc, a.validator)) + a.fiber.Get("/branch/:id/bets", handlers.GetBetByBranchID(a.logger, a.betSvc, a.validator)) a.fiber.Put("/branch/:id", handlers.UpdateBranch(a.logger, a.branchSvc, a.validator)) a.fiber.Delete("/branch/:id", handlers.DeleteBranch(a.logger, a.branchSvc, a.validator)) + a.fiber.Get("/search/branch", a.authMiddleware, handlers.SearchBranch(a.logger, a.branchSvc, a.validator)) + // /branch/search // branch/wallet // Branch Operation @@ -81,7 +84,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/ticket/:id", handlers.GetTicketByID(a.logger, a.ticketSvc, a.validator)) // Bet - a.fiber.Post("/bet", handlers.CreateBet(a.logger, a.betSvc, a.validator)) + a.fiber.Post("/bet", handlers.CreateBet(a.logger, a.betSvc, a.userSvc, a.branchSvc, a.walletSvc, a.validator)) a.fiber.Get("/bet", handlers.GetAllBet(a.logger, a.betSvc, a.validator)) a.fiber.Get("/bet/:id", handlers.GetBetByID(a.logger, a.betSvc, a.validator)) a.fiber.Patch("/bet/:id", handlers.UpdateCashOut(a.logger, a.betSvc, a.validator)) @@ -91,10 +94,13 @@ func (a *App) initAppRoutes() { a.fiber.Get("/wallet", handlers.GetAllWallets(a.logger, a.walletSvc, a.validator)) a.fiber.Get("/wallet/:id", handlers.GetWalletByID(a.logger, a.walletSvc, a.validator)) a.fiber.Put("/wallet/:id", handlers.UpdateWalletActive(a.logger, a.walletSvc, a.validator)) + a.fiber.Get("/branchWallet", handlers.GetAllBranchWallets(a.logger, a.walletSvc, a.validator)) // Transfer // /transfer/wallet - transfer from one wallet to another wallet - a.fiber.Post("/transfer/wallet", a.authMiddleware, handlers.TransferToWallet(a.logger, a.walletSvc, a.branchSvc, a.validator)) + a.fiber.Post("/transfer/wallet/:id", a.authMiddleware, handlers.TransferToWallet(a.logger, a.walletSvc, a.branchSvc, a.validator)) + a.fiber.Get("/transfer/wallet/:id", a.authMiddleware, handlers.GetTransfersByWallet(a.logger, a.walletSvc, a.validator)) + a.fiber.Get("/transfer/refill/:id", a.authMiddleware, handlers.RefillWallet(a.logger, a.walletSvc, a.validator)) // Transactions a.fiber.Post("/transaction", a.authMiddleware, handlers.CreateTransaction(a.logger, a.transactionSvc, a.validator)) diff --git a/sqlc.yaml b/sqlc.yaml index bd998c2..5e2ff3a 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -16,4 +16,12 @@ sql: - db_type: "uuid" go_type: "github.com/google/uuid.NullUUID" nullable: true + - column: "bet_with_outcomes.outcomes" + go_type: + type: "BetOutcome" + slice: true + - column: "ticket_with_outcomes.outcomes" + go_type: + type: "TicketOutcome" + slice: true From aba4b89bb06515d25deecd358dcba86d6061420c Mon Sep 17 00:00:00 2001 From: OneTap Technologies Date: Sat, 12 Apr 2025 09:27:41 +0300 Subject: [PATCH 13/30] adding getby odd id --- cmd/main.go | 4 +- db/query/odds.sql | 35 ++- docs/docs.go | 102 ++++++++- docs/swagger.json | 102 ++++++++- docs/swagger.yaml | 68 +++++- gen/db/odds.sql.go | 111 +++++++++ internal/domain/odds.go | 12 +- internal/repository/odds.go | 273 ++++++++++++++--------- internal/services/event/service.go | 2 +- internal/services/odds/port.go | 2 + internal/services/odds/service.go | 143 +++++------- internal/web_server/cron.go | 4 +- internal/web_server/handlers/prematch.go | 47 +++- internal/web_server/routes.go | 3 +- 14 files changed, 691 insertions(+), 217 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 584e821..31a728e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -38,13 +38,13 @@ import ( func main() { cfg, err := config.NewConfig() if err != nil { - slog.Error("❌ Config error:", "err", err) + slog.Error(" Config error:", "err", err) os.Exit(1) } db, _, err := repository.OpenDB(cfg.DbUrl) if err != nil { - fmt.Println("❌ Database error:", err) + fmt.Println(" Database error:", err) os.Exit(1) } diff --git a/db/query/odds.sql b/db/query/odds.sql index 07e1c99..5158ee0 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -56,4 +56,37 @@ SELECT source, is_active FROM odds -WHERE event_id = $1 AND is_active = true AND source = 'b365api'; \ No newline at end of file +WHERE event_id = $1 AND is_active = true AND source = 'b365api'; + +-- name: GetALLPrematchOdds :many +SELECT + id, + event_id, + fi, + market_type, + market_name, + market_category, + market_id, + name, + handicap, + odds_value, + section, + category, + raw_odds, + fetched_at, + source, + is_active +FROM odds +WHERE is_active = true AND source = 'b365api'; +-- name: GetRawOddsByID :one +SELECT + id, + event_id, + raw_odds, + fetched_at +FROM odds +WHERE + raw_odds @> $1::jsonb AND + is_active = true AND + source = 'b365api' +LIMIT 1; \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 6448575..6143af4 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -180,6 +180,82 @@ const docTemplate = `{ } } }, + "/prematch/odds": { + "get": { + "description": "Retrieve all prematch odds from the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve all prematch odds", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Odd" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/prematch/odds/raw/{raw_odds_id}": { + "get": { + "description": "Retrieve raw odds by raw odds ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve raw odds by ID", + "parameters": [ + { + "type": "string", + "description": "Raw Odds ID", + "name": "raw_odds_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.RawOddsByID" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/prematch/odds/{event_id}": { "get": { "description": "Retrieve prematch odds for a specific event by event ID", @@ -517,9 +593,6 @@ const docTemplate = `{ "handicap": { "type": "string" }, - "header": { - "type": "string" - }, "id": { "type": "integer" }, @@ -536,17 +609,16 @@ const docTemplate = `{ "type": "string" }, "market_type": { + "description": "RawEventID string ` + "`" + `json:\"raw_event_id\"` + "`" + `", "type": "string" }, "name": { + "description": "Header string ` + "`" + `json:\"header\"` + "`" + `", "type": "string" }, "odds_value": { "type": "number" }, - "raw_event_id": { - "type": "string" - }, "raw_odds": { "type": "array", "items": {} @@ -559,6 +631,24 @@ const docTemplate = `{ } } }, + "domain.RawOddsByID": { + "type": "object", + "properties": { + "event_id": { + "type": "string" + }, + "fetched_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "raw_odds": { + "type": "array", + "items": {} + } + } + }, "domain.Role": { "type": "string", "enum": [ diff --git a/docs/swagger.json b/docs/swagger.json index 414256f..8692c31 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -172,6 +172,82 @@ } } }, + "/prematch/odds": { + "get": { + "description": "Retrieve all prematch odds from the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve all prematch odds", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Odd" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/prematch/odds/raw/{raw_odds_id}": { + "get": { + "description": "Retrieve raw odds by raw odds ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve raw odds by ID", + "parameters": [ + { + "type": "string", + "description": "Raw Odds ID", + "name": "raw_odds_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.RawOddsByID" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/prematch/odds/{event_id}": { "get": { "description": "Retrieve prematch odds for a specific event by event ID", @@ -509,9 +585,6 @@ "handicap": { "type": "string" }, - "header": { - "type": "string" - }, "id": { "type": "integer" }, @@ -528,17 +601,16 @@ "type": "string" }, "market_type": { + "description": "RawEventID string `json:\"raw_event_id\"`", "type": "string" }, "name": { + "description": "Header string `json:\"header\"`", "type": "string" }, "odds_value": { "type": "number" }, - "raw_event_id": { - "type": "string" - }, "raw_odds": { "type": "array", "items": {} @@ -551,6 +623,24 @@ } } }, + "domain.RawOddsByID": { + "type": "object", + "properties": { + "event_id": { + "type": "string" + }, + "fetched_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "raw_odds": { + "type": "array", + "items": {} + } + } + }, "domain.Role": { "type": "string", "enum": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 00333d0..7bfdfa4 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -11,8 +11,6 @@ definitions: type: string handicap: type: string - header: - type: string id: type: integer is_active: @@ -24,13 +22,13 @@ definitions: market_name: type: string market_type: + description: RawEventID string `json:"raw_event_id"` type: string name: + description: Header string `json:"header"` type: string odds_value: type: number - raw_event_id: - type: string raw_odds: items: {} type: array @@ -39,6 +37,18 @@ definitions: source: type: string type: object + domain.RawOddsByID: + properties: + event_id: + type: string + fetched_at: + type: string + id: + type: integer + raw_odds: + items: {} + type: array + type: object domain.Role: enum: - super_admin @@ -315,6 +325,27 @@ paths: summary: Refresh token tags: - auth + /prematch/odds: + get: + consumes: + - application/json + description: Retrieve all prematch odds from the database + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.Odd' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve all prematch odds + tags: + - prematch /prematch/odds/{event_id}: get: consumes: @@ -346,6 +377,35 @@ paths: summary: Retrieve prematch odds for an event tags: - prematch + /prematch/odds/raw/{raw_odds_id}: + get: + consumes: + - application/json + description: Retrieve raw odds by raw odds ID + parameters: + - description: Raw Odds ID + in: path + name: raw_odds_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.RawOddsByID' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve raw odds by ID + tags: + - prematch /user/checkPhoneEmailExist: post: consumes: diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 003be80..2cc76c6 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -11,6 +11,84 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const GetALLPrematchOdds = `-- name: GetALLPrematchOdds :many +SELECT + id, + event_id, + fi, + market_type, + market_name, + market_category, + market_id, + name, + handicap, + odds_value, + section, + category, + raw_odds, + fetched_at, + source, + is_active +FROM odds +WHERE is_active = true AND source = 'b365api' +` + +type GetALLPrematchOddsRow struct { + ID int32 + EventID pgtype.Text + Fi pgtype.Text + MarketType string + MarketName pgtype.Text + MarketCategory pgtype.Text + MarketID pgtype.Text + Name pgtype.Text + Handicap pgtype.Text + OddsValue pgtype.Float8 + Section string + Category pgtype.Text + RawOdds []byte + FetchedAt pgtype.Timestamp + Source pgtype.Text + IsActive pgtype.Bool +} + +func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]GetALLPrematchOddsRow, error) { + rows, err := q.db.Query(ctx, GetALLPrematchOdds) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetALLPrematchOddsRow + for rows.Next() { + var i GetALLPrematchOddsRow + if err := rows.Scan( + &i.ID, + &i.EventID, + &i.Fi, + &i.MarketType, + &i.MarketName, + &i.MarketCategory, + &i.MarketID, + &i.Name, + &i.Handicap, + &i.OddsValue, + &i.Section, + &i.Category, + &i.RawOdds, + &i.FetchedAt, + &i.Source, + &i.IsActive, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetPrematchOdds = `-- name: GetPrematchOdds :many SELECT id, @@ -74,6 +152,39 @@ func (q *Queries) GetPrematchOdds(ctx context.Context, eventID pgtype.Text) ([]O return items, nil } +const GetRawOddsByID = `-- name: GetRawOddsByID :one +SELECT + id, + event_id, + raw_odds, + fetched_at +FROM odds +WHERE + raw_odds @> $1::jsonb AND + is_active = true AND + source = 'b365api' +LIMIT 1 +` + +type GetRawOddsByIDRow struct { + ID int32 + EventID pgtype.Text + RawOdds []byte + FetchedAt pgtype.Timestamp +} + +func (q *Queries) GetRawOddsByID(ctx context.Context, dollar_1 []byte) (GetRawOddsByIDRow, error) { + row := q.db.QueryRow(ctx, GetRawOddsByID, dollar_1) + var i GetRawOddsByIDRow + err := row.Scan( + &i.ID, + &i.EventID, + &i.RawOdds, + &i.FetchedAt, + ) + return i, err +} + const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec INSERT INTO odds ( event_id, diff --git a/internal/domain/odds.go b/internal/domain/odds.go index 418b33d..1d47b85 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -5,7 +5,7 @@ import ( "time" ) -type RawMessage interface{} // Change from json.RawMessage to interface{} +type RawMessage interface{} type Market struct { EventID string @@ -27,12 +27,12 @@ type Odd struct { ID int64 `json:"id"` EventID string `json:"event_id"` Fi string `json:"fi"` - RawEventID string `json:"raw_event_id"` + // RawEventID string `json:"raw_event_id"` MarketType string `json:"market_type"` MarketName string `json:"market_name"` MarketCategory string `json:"market_category"` MarketID string `json:"market_id"` - Header string `json:"header"` + // Header string `json:"header"` Name string `json:"name"` Handicap string `json:"handicap"` OddsValue float64 `json:"odds_value"` @@ -42,4 +42,10 @@ type Odd struct { FetchedAt time.Time `json:"fetched_at"` Source string `json:"source"` IsActive bool `json:"is_active"` +} +type RawOddsByID struct { + ID int64 `json:"id"` + EventID string `json:"event_id"` + RawOdds []RawMessage `json:"raw_odds"` + FetchedAt time.Time `json:"fetched_at"` } \ No newline at end of file diff --git a/internal/repository/odds.go b/internal/repository/odds.go index a21c729..ae510e7 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -1,112 +1,105 @@ package repository import ( - "context" - "encoding/json" - "fmt" - "os" - "strconv" - "time" + "context" + "encoding/json" + "os" + "strconv" + "time" - dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/jackc/pgx/v5/pgtype" + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" ) func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { - if len(m.Odds) == 0 { - fmt.Printf(" Market has no odds: %s (%s)\n", m.MarketType, m.EventID) - return nil - } + if len(m.Odds) == 0 { + return nil + } - for _, raw := range m.Odds { - var item map[string]interface{} - if err := json.Unmarshal(raw, &item); err != nil { - fmt.Printf(" Invalid odd JSON for %s (%s): %v\n", m.MarketType, m.EventID, err) - continue - } + for _, raw := range m.Odds { + var item map[string]interface{} + if err := json.Unmarshal(raw, &item); err != nil { + continue + } - header := getString(item["header"]) - name := getString(item["name"]) - handicap := getString(item["handicap"]) - oddsVal := getFloat(item["odds"]) + header := getString(item["header"]) + name := getString(item["name"]) + handicap := getString(item["handicap"]) + oddsVal := getFloat(item["odds"]) - rawOddsBytes, _ := json.Marshal(m.Odds) + rawOddsBytes, _ := json.Marshal(m.Odds) - params := dbgen.InsertNonLiveOddParams{ - EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, - Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""}, - RawEventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, - MarketType: m.MarketType, - MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""}, - MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""}, - MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""}, - Header: pgtype.Text{String: header, Valid: header != ""}, - Name: pgtype.Text{String: name, Valid: name != ""}, - Handicap: pgtype.Text{String: handicap, Valid: handicap != ""}, - OddsValue: pgtype.Float8{Float64: oddsVal, Valid: oddsVal != 0}, - Section: m.MarketCategory, - Category: pgtype.Text{Valid: false}, - RawOdds: rawOddsBytes, - } + params := dbgen.InsertNonLiveOddParams{ + EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, + Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""}, + RawEventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, + MarketType: m.MarketType, + MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""}, + MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""}, + MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""}, + Header: pgtype.Text{String: header, Valid: header != ""}, + Name: pgtype.Text{String: name, Valid: name != ""}, + Handicap: pgtype.Text{String: handicap, Valid: handicap != ""}, + OddsValue: pgtype.Float8{Float64: oddsVal, Valid: oddsVal != 0}, + Section: m.MarketCategory, + Category: pgtype.Text{Valid: false}, + RawOdds: rawOddsBytes, + } - err := s.queries.InsertNonLiveOdd(ctx, params) - if err != nil { - fmt.Printf(" Failed to insert odd for market %s (%s): %v\n", m.MarketType, m.EventID, err) - _ = writeFailedMarketLog(m, err) - continue - } - - fmt.Printf("Inserted odd: %s | type=%s | header=%s | name=%s\n", m.EventID, m.MarketType, header, name) - } - return nil + err := s.queries.InsertNonLiveOdd(ctx, params) + if err != nil { + _ = writeFailedMarketLog(m, err) + continue + } + } + return nil } - func writeFailedMarketLog(m domain.Market, err error) error { - logDir := "logs" - logFile := logDir + "/failed_markets.log" + logDir := "logs" + logFile := logDir + "/failed_markets.log" - if mkErr := os.MkdirAll(logDir, 0755); mkErr != nil { - return mkErr - } + if mkErr := os.MkdirAll(logDir, 0755); mkErr != nil { + return mkErr + } - f, fileErr := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if fileErr != nil { - return fileErr - } - defer f.Close() + f, fileErr := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if fileErr != nil { + return fileErr + } + defer f.Close() - entry := struct { - Time string `json:"time"` - Error string `json:"error"` - Record domain.Market `json:"record"` - }{ - Time: time.Now().Format(time.RFC3339), - Error: err.Error(), - Record: m, - } + entry := struct { + Time string `json:"time"` + Error string `json:"error"` + Record domain.Market `json:"record"` + }{ + Time: time.Now().Format(time.RFC3339), + Error: err.Error(), + Record: m, + } - jsonData, _ := json.MarshalIndent(entry, "", " ") - _, writeErr := f.WriteString(string(jsonData) + "\n\n") - return writeErr + jsonData, _ := json.MarshalIndent(entry, "", " ") + _, writeErr := f.WriteString(string(jsonData) + "\n\n") + return writeErr } func getString(v interface{}) string { - if s, ok := v.(string); ok { - return s - } - return "" + if s, ok := v.(string); ok { + return s + } + return "" } func getFloat(v interface{}) float64 { - if s, ok := v.(string); ok { - f, err := strconv.ParseFloat(s, 64) - if err == nil { - return f - } - } - return 0 + if s, ok := v.(string); ok { + f, err := strconv.ParseFloat(s, 64) + if err == nil { + return f + } + } + return 0 } func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { @@ -120,32 +113,96 @@ func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.O domainOdds := make([]domain.Odd, len(odds)) for i, odd := range odds { domainOdds[i] = domain.Odd{ - ID: int64(odd.ID), // Cast int32 to int64 - EventID: odd.EventID.String, // Extract the String value - Fi: odd.Fi.String, // Extract the String value - RawEventID: odd.RawEventID.String, // Extract the String value - MarketType: odd.MarketType, // Direct assignment - MarketName: odd.MarketName.String, // Extract the String value - MarketCategory: odd.MarketCategory.String, // Extract the String value - MarketID: odd.MarketID.String, // Extract the String value - Header: odd.Header.String, // Extract the String value - Name: odd.Name.String, // Extract the String value - Handicap: odd.Handicap.String, // Extract the String value - OddsValue: odd.OddsValue.Float64, // Extract the Float64 value - Section: odd.Section, // Direct assignment - Category: odd.Category.String, // Extract the String value - RawOdds: func() []domain.RawMessage { - var rawOdds []domain.RawMessage - if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { - rawOdds = nil - } - return rawOdds - }(), - FetchedAt: odd.FetchedAt.Time, // Extract the Time value - Source: odd.Source.String, // Extract the String value - IsActive: odd.IsActive.Bool, // Extract the Bool value + ID: int64(odd.ID), + EventID: odd.EventID.String, + Fi: odd.Fi.String, + // RawEventID: odd.RawEventID.String, + MarketType: odd.MarketType, + MarketName: odd.MarketName.String, + MarketCategory: odd.MarketCategory.String, + MarketID: odd.MarketID.String, + // Header: odd.Header.String, + Name: odd.Name.String, + Handicap: odd.Handicap.String, + OddsValue: odd.OddsValue.Float64, + Section: odd.Section, + Category: odd.Category.String, + RawOdds: func() []domain.RawMessage { + var rawOdds []domain.RawMessage + if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { + rawOdds = nil + } + return rawOdds + }(), + FetchedAt: odd.FetchedAt.Time, + Source: odd.Source.String, + IsActive: odd.IsActive.Bool, } } return domainOdds, nil +} + +func (s *Store) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) { + rows, err := s.queries.GetALLPrematchOdds(ctx) + if err != nil { + return nil, err + } + + domainOdds := make([]domain.Odd, len(rows)) + for i, row := range rows { + domainOdds[i] = domain.Odd{ + ID: int64(row.ID), + EventID: row.EventID.String, + Fi: row.Fi.String, + MarketType: row.MarketType, + MarketName: row.MarketName.String, + MarketCategory: row.MarketCategory.String, + MarketID: row.MarketID.String, + Name: row.Name.String, + Handicap: row.Handicap.String, + OddsValue: row.OddsValue.Float64, + Section: row.Section, + Category: row.Category.String, + RawOdds: func() []domain.RawMessage { + var rawOdds []domain.RawMessage + if err := json.Unmarshal(row.RawOdds, &rawOdds); err != nil { + rawOdds = nil + } + return rawOdds + }(), + FetchedAt: row.FetchedAt.Time, + Source: row.Source.String, + IsActive: row.IsActive.Bool, + } + } + + return domainOdds, nil +} + +func (s *Store) GetRawOddsByID(ctx context.Context, rawOddsID string) (domain.RawOddsByID, error) { + jsonFilter := `[{"id":"` + rawOddsID + `"}]` + + odd, err := s.queries.GetRawOddsByID(ctx, []byte(jsonFilter)) + if err != nil { + return domain.RawOddsByID{}, err + } + + var rawOdds []json.RawMessage + if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { + return domain.RawOddsByID{}, err + } + + return domain.RawOddsByID{ + ID: int64(odd.ID), + EventID: odd.EventID.String, + RawOdds: func() []domain.RawMessage { + converted := make([]domain.RawMessage, len(rawOdds)) + for i, r := range rawOdds { + converted[i] = domain.RawMessage(r) + } + return converted + }(), + FetchedAt: odd.FetchedAt.Time, + }, nil } \ No newline at end of file diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 3d54d7a..24207ca 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -170,4 +170,4 @@ func getInt(v interface{}) int { return int(f) } return 0 -} +} \ No newline at end of file diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index d50a8af..8805a66 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -9,6 +9,8 @@ import ( type Service interface { FetchNonLiveOdds(ctx context.Context) error GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) + GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) + GetRawOddsByID(ctx context.Context, rawOddsID string) ([]domain.RawOddsByID, error) } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 2f3245d..09a32fa 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -3,7 +3,6 @@ package odds import ( "context" "encoding/json" - "fmt" "io" "net/http" "strconv" @@ -23,79 +22,70 @@ func New(token string, store *repository.Store) *ServiceImpl { } func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { - fmt.Println("Starting FetchNonLiveOdds...") - - sportID := 1 - upcomingURL := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token) - resp, err := http.Get(upcomingURL) - if err != nil { - fmt.Printf("Failed to fetch upcoming: %v\n", err) - return err + sportIDs := []int{ + 1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, + 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, + 95, 110, 107, 151, 162, 148, } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - var upcomingData struct { - Success int `json:"success"` - Results []struct { - ID string `json:"id"` - } `json:"results"` - } - if err := json.Unmarshal(body, &upcomingData); err != nil || upcomingData.Success != 1 { - fmt.Printf("Failed to decode upcoming response\nRaw: %s\n", string(body)) - return err - } - - for _, ev := range upcomingData.Results { - eventID := ev.ID - fmt.Printf("Fetching prematch odds for event_id=%s\n", eventID) - prematchURL := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%s", s.token, eventID) - oddsResp, err := http.Get(prematchURL) + for _, sportID := range sportIDs { + upcomingURL := "https://api.b365api.com/v1/bet365/upcoming?sport_id=" + strconv.Itoa(sportID) + "&token=" + s.token + resp, err := http.Get(upcomingURL) if err != nil { - fmt.Printf(" Odds fetch failed for event_id=%s: %v\n", eventID, err) continue } - defer oddsResp.Body.Close() + defer resp.Body.Close() - oddsBody, _ := io.ReadAll(oddsResp.Body) - fmt.Printf(" Raw odds response for event_id=%s: %.300s...\n", eventID, string(oddsBody)) - - var oddsData struct { + body, _ := io.ReadAll(resp.Body) + var upcomingData struct { Success int `json:"success"` Results []struct { - EventID string `json:"event_id"` - FI string `json:"FI"` - Main OddsSection `json:"main"` + ID string `json:"id"` } `json:"results"` } - if err := json.Unmarshal(oddsBody, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { - fmt.Printf(" Failed odds decode for event_id=%s\nRaw: %s\n", eventID, string(oddsBody)) + if err := json.Unmarshal(body, &upcomingData); err != nil || upcomingData.Success != 1 { continue } - result := oddsData.Results[0] - finalID := result.EventID - if finalID == "" { - finalID = result.FI - } - if finalID == "" { - fmt.Println(" Skipping event with missing final ID.") - continue - } + for _, ev := range upcomingData.Results { + eventID := ev.ID + prematchURL := "https://api.b365api.com/v3/bet365/prematch?token=" + s.token + "&FI=" + eventID + oddsResp, err := http.Get(prematchURL) + if err != nil { + continue + } + defer oddsResp.Body.Close() - fmt.Printf("🗂 Saving prematch odds for event_id=%s\n", finalID) - s.storeSection(ctx, finalID, result.FI, "main", result.Main) - fmt.Printf(" Finished storing prematch odds for event_id=%s\n", finalID) + oddsBody, _ := io.ReadAll(oddsResp.Body) + var oddsData struct { + Success int `json:"success"` + Results []struct { + EventID string `json:"event_id"` + FI string `json:"FI"` + Main OddsSection `json:"main"` + } `json:"results"` + } + if err := json.Unmarshal(oddsBody, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { + continue + } + + result := oddsData.Results[0] + finalID := result.EventID + if finalID == "" { + finalID = result.FI + } + if finalID == "" { + continue + } + + s.storeSection(ctx, finalID, result.FI, "main", result.Main) + } } - fmt.Println(" All prematch odds fetched and stored.") return nil } func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section OddsSection) { - fmt.Printf(" Processing section '%s' for event_id=%s\n", sectionName, eventID) if len(section.Sp) == 0 { - fmt.Printf(" No odds in section '%s' for event_id=%s\n", sectionName, eventID) return } @@ -103,9 +93,7 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName updatedAt := time.Unix(updatedAtUnix, 0) for marketType, market := range section.Sp { - fmt.Printf(" Processing market: %s (%s)\n", marketType, market.ID) if len(market.Odds) == 0 { - fmt.Printf(" Empty odds for marketType=%s in section=%s\n", marketType, sectionName) continue } @@ -120,23 +108,16 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName Odds: market.Odds, } - fmt.Printf(" Saving market to DB: %s (%s)\n", marketType, market.ID) - err := s.store.SaveNonLiveMarket(ctx, marketRecord) - if err != nil { - fmt.Printf(" Save failed for market %s (%s): %v\n", marketType, eventID, err) - } else { - fmt.Printf(" Successfully stored market: %s (%s)\n", marketType, eventID) - } + _ = s.store.SaveNonLiveMarket(ctx, marketRecord) } } - type OddsMarket struct { - ID string `json:"id"` - Name string `json:"name"` - Odds []json.RawMessage `json:"odds"` - Header string `json:"header,omitempty"` - Handicap string `json:"handicap,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Odds []json.RawMessage `json:"odds"` + Header string `json:"header,omitempty"` + Handicap string `json:"handicap,omitempty"` } type OddsSection struct { @@ -144,22 +125,20 @@ type OddsSection struct { Sp map[string]OddsMarket `json:"sp"` } -func getString(v interface{}) string { - if str, ok := v.(string); ok { - return str - } - return "" -} + func (s *ServiceImpl) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { - fmt.Printf("Retrieving prematch odds for event_id=%s\n", eventID) + return s.store.GetPrematchOdds(ctx, eventID) +} - odds, err := s.store.GetPrematchOdds(ctx, eventID) +func (s *ServiceImpl) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) { + return s.store.GetALLPrematchOdds(ctx) +} + +func (s *ServiceImpl) GetRawOddsByID(ctx context.Context, rawOddsID string) ([]domain.RawOddsByID, error) { + rawOdds, err := s.store.GetRawOddsByID(ctx, rawOddsID) if err != nil { - fmt.Printf(" Failed to retrieve odds for event_id=%s: %v\n", eventID, err) return nil, err } - - fmt.Printf(" Retrieved %d odds entries for event_id=%s\n", len(odds), eventID) - return odds, nil -} + return []domain.RawOddsByID{rawOdds}, nil +} \ No newline at end of file diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 68ddcf2..71cb9b4 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -33,7 +33,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }, }, { - spec: "*/5 * * * * *", // Every 5 seconds + spec: "0 */5 * * * *", // Every 5 minutes task: func() { if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { log.Printf(" FetchNonLiveOdds error: %v", err) @@ -41,6 +41,8 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }, }, + + } for _, job := range schedule { diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index cc48b10..5e08966 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -22,16 +22,59 @@ func GetPrematchOdds(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.H return func(c *fiber.Ctx) error { eventID := c.Params("event_id") if eventID == "" { - logger.Error("GetPrematchOdds failed: missing event_id") return response.WriteJSON(c, fiber.StatusBadRequest, "Missing event_id", nil, nil) } odds, err := prematchSvc.GetPrematchOdds(c.Context(), eventID) if err != nil { - logger.Error("GetPrematchOdds failed", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve odds", nil, nil) } return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) } +} +//GetALLPrematchOdds +// @Summary Retrieve all prematch odds +// @Description Retrieve all prematch odds from the database +// @Tags prematch +// @Accept json +// @Produce json +// @Success 200 {array} domain.Odd +// @Failure 500 {object} response.APIResponse +// @Router /prematch/odds [get] +func GetALLPrematchOdds(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { + return func(c *fiber.Ctx) error { + odds, err := prematchSvc.GetALLPrematchOdds(c.Context()) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve all prematch odds", nil, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "All prematch odds retrieved successfully", odds, nil) + } +} +// GetRawOddsByID +// @Summary Retrieve raw odds by ID +// @Description Retrieve raw odds by raw odds ID +// @Tags prematch +// @Accept json +// @Produce json +// @Param raw_odds_id path string true "Raw Odds ID" +// @Success 200 {object} domain.RawOddsByID +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /prematch/odds/raw/{raw_odds_id} [get] +func GetRawOddsByID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { + return func(c *fiber.Ctx) error { + rawOddsID := c.Params("raw_odds_id") + if rawOddsID == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing raw_odds_id", nil, nil) + } + + rawOdds, err := prematchSvc.GetRawOddsByID(c.Context(), rawOddsID) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve raw odds", nil, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Raw odds retrieved successfully", rawOdds, nil) + } } \ No newline at end of file diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 22ebb93..6bd82f5 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -28,7 +28,8 @@ func (a *App) initAppRoutes() { a.fiber.Get("/user/profile", a.authMiddleware, handlers.UserProfile(a.logger, a.userSvc)) a.fiber.Get("/prematch/odds/:event_id", handlers.GetPrematchOdds(a.logger, a.prematchSvc)) - + a.fiber.Get("/prematch/odds", handlers.GetALLPrematchOdds(a.logger, a.prematchSvc)) + a.fiber.Get("/prematch/odds/raw/:raw_odds_id", handlers.GetRawOddsByID(a.logger, a.prematchSvc)) // Swagger a.fiber.Get("/swagger/*", fiberSwagger.WrapHandler) } From 6f28299a2ed9b0e93efe1e43c52092e133706de1 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sat, 12 Apr 2025 12:57:19 +0300 Subject: [PATCH 14/30] fix ticket issue --- docs/docs.go | 49 ++- docs/swagger.json | 49 ++- docs/swagger.yaml | 31 +- gen/db/auth.sql.go | 14 +- gen/db/bet.sql.go | 28 +- gen/db/branch.sql.go | 54 +-- gen/db/events.sql.go | 40 +- gen/db/models.go | 402 +++++++++--------- gen/db/notification.sql.go | 40 +- gen/db/odds.sql.go | 28 +- gen/db/otp.sql.go | 22 +- gen/db/ticket.sql.go | 10 +- gen/db/transactions.sql.go | 30 +- gen/db/transfer.sql.go | 18 +- gen/db/user.sql.go | 150 +++---- gen/db/wallet.sql.go | 68 +-- internal/domain/ticket.go | 8 +- internal/repository/ticket.go | 5 +- internal/web_server/cron.go | 58 +-- internal/web_server/handlers/bet_handler.go | 79 ++-- .../web_server/handlers/ticket_handler.go | 2 +- .../handlers/transaction_handler.go | 2 +- .../web_server/handlers/transfer_handler.go | 16 +- internal/web_server/routes.go | 3 +- sqlc.yaml | 2 +- 25 files changed, 647 insertions(+), 561 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index d569ff8..66d8837 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2680,7 +2680,43 @@ const docTemplate = `{ } }, "handlers.CreateBetReq": { - "type": "object" + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "full_name": { + "type": "string", + "example": "John" + }, + "is_shop_bet": { + "type": "boolean", + "example": false + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BetOutcome" + } + }, + "phone_number": { + "type": "string", + "example": "1234567890" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.BetStatus" + } + ], + "example": 1 + }, + "total_odds": { + "type": "number", + "example": 4.22 + } + } }, "handlers.CreateBranchOperationReq": { "type": "object", @@ -2937,17 +2973,6 @@ const docTemplate = `{ } } }, - "handlers.NullableInt64": { - "type": "object", - "properties": { - "valid": { - "type": "boolean" - }, - "value": { - "type": "integer" - } - } - }, "handlers.RegisterCodeReq": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 31f5a1d..9412cfa 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2672,7 +2672,43 @@ } }, "handlers.CreateBetReq": { - "type": "object" + "type": "object", + "properties": { + "amount": { + "type": "number", + "example": 100 + }, + "full_name": { + "type": "string", + "example": "John" + }, + "is_shop_bet": { + "type": "boolean", + "example": false + }, + "outcomes": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BetOutcome" + } + }, + "phone_number": { + "type": "string", + "example": "1234567890" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.BetStatus" + } + ], + "example": 1 + }, + "total_odds": { + "type": "number", + "example": 4.22 + } + } }, "handlers.CreateBranchOperationReq": { "type": "object", @@ -2929,17 +2965,6 @@ } } }, - "handlers.NullableInt64": { - "type": "object", - "properties": { - "valid": { - "type": "boolean" - }, - "value": { - "type": "integer" - } - } - }, "handlers.RegisterCodeReq": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a6726d2..041c38a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -223,6 +223,30 @@ definitions: type: boolean type: object handlers.CreateBetReq: + properties: + amount: + example: 100 + type: number + full_name: + example: John + type: string + is_shop_bet: + example: false + type: boolean + outcomes: + items: + $ref: '#/definitions/handlers.BetOutcome' + type: array + phone_number: + example: "1234567890" + type: string + status: + allOf: + - $ref: '#/definitions/domain.BetStatus' + example: 1 + total_odds: + example: 4.22 + type: number type: object handlers.CreateBranchOperationReq: properties: @@ -403,13 +427,6 @@ definitions: static_updated_at: type: string type: object - handlers.NullableInt64: - properties: - valid: - type: boolean - value: - type: integer - type: object handlers.RegisterCodeReq: properties: email: diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index c826c36..bbc0e60 100644 --- a/gen/db/auth.sql.go +++ b/gen/db/auth.sql.go @@ -17,11 +17,11 @@ VALUES ($1, $2, $3, $4, $5) ` type CreateRefreshTokenParams struct { - UserID int64 - Token string - ExpiresAt pgtype.Timestamptz - CreatedAt pgtype.Timestamptz - Revoked bool + UserID int64 `json:"user_id"` + Token string `json:"token"` + ExpiresAt pgtype.Timestamptz `json:"expires_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + Revoked bool `json:"revoked"` } func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) error { @@ -60,8 +60,8 @@ WHERE email = $1 OR phone_number = $2 ` type GetUserByEmailPhoneParams struct { - Email pgtype.Text - PhoneNumber pgtype.Text + Email pgtype.Text `json:"email"` + PhoneNumber pgtype.Text `json:"phone_number"` } func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPhoneParams) (User, error) { diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index 89a636a..c429b91 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -28,15 +28,15 @@ RETURNING id, amount, total_odds, status, full_name, phone_number, branch_id, us ` type CreateBetParams struct { - Amount int64 - TotalOdds float32 - Status int32 - FullName string - PhoneNumber string - BranchID pgtype.Int8 - UserID pgtype.Int8 - IsShopBet bool - CashoutID string + Amount int64 `json:"amount"` + TotalOdds float32 `json:"total_odds"` + Status int32 `json:"status"` + FullName string `json:"full_name"` + PhoneNumber string `json:"phone_number"` + BranchID pgtype.Int8 `json:"branch_id"` + UserID pgtype.Int8 `json:"user_id"` + IsShopBet bool `json:"is_shop_bet"` + CashoutID string `json:"cashout_id"` } func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, error) { @@ -71,9 +71,9 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro } type CreateBetOutcomeParams struct { - BetID int64 - EventID int64 - OddID int64 + BetID int64 `json:"bet_id"` + EventID int64 `json:"event_id"` + OddID int64 `json:"odd_id"` } const DeleteBet = `-- name: DeleteBet :exec @@ -242,8 +242,8 @@ WHERE id = $1 ` type UpdateCashOutParams struct { - ID int64 - CashedOut bool + ID int64 `json:"id"` + CashedOut bool `json:"cashed_out"` } func (q *Queries) UpdateCashOut(ctx context.Context, arg UpdateCashOutParams) error { diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index d1d8e99..a04d4fd 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -25,12 +25,12 @@ RETURNING id, name, location, wallet_id, branch_manager_id, company_id, is_self_ ` type CreateBranchParams struct { - Name string - Location string - WalletID int64 - BranchManagerID int64 - CompanyID int64 - IsSelfOwned bool + Name string `json:"name"` + Location string `json:"location"` + WalletID int64 `json:"wallet_id"` + BranchManagerID int64 `json:"branch_manager_id"` + CompanyID int64 `json:"company_id"` + IsSelfOwned bool `json:"is_self_owned"` } func (q *Queries) CreateBranch(ctx context.Context, arg CreateBranchParams) (Branch, error) { @@ -64,8 +64,8 @@ RETURNING id, user_id, branch_id ` type CreateBranchCashierParams struct { - UserID int64 - BranchID int64 + UserID int64 `json:"user_id"` + BranchID int64 `json:"branch_id"` } func (q *Queries) CreateBranchCashier(ctx context.Context, arg CreateBranchCashierParams) (BranchCashier, error) { @@ -82,8 +82,8 @@ RETURNING id, operation_id, branch_id, created_at, updated_at ` type CreateBranchOperationParams struct { - OperationID int64 - BranchID int64 + OperationID int64 `json:"operation_id"` + BranchID int64 `json:"branch_id"` } func (q *Queries) CreateBranchOperation(ctx context.Context, arg CreateBranchOperationParams) (BranchOperation, error) { @@ -106,8 +106,8 @@ RETURNING id, name, description ` type CreateSupportedOperationParams struct { - Name string - Description string + Name string `json:"name"` + Description string `json:"description"` } func (q *Queries) CreateSupportedOperation(ctx context.Context, arg CreateSupportedOperationParams) (SupportedOperation, error) { @@ -144,8 +144,8 @@ WHERE operation_id = $1 ` type DeleteBranchOperationParams struct { - OperationID int64 - BranchID int64 + OperationID int64 `json:"operation_id"` + BranchID int64 `json:"branch_id"` } func (q *Queries) DeleteBranchOperation(ctx context.Context, arg DeleteBranchOperationParams) error { @@ -390,13 +390,13 @@ WHERE branch_operations.branch_id = $1 ` type GetBranchOperationsRow struct { - ID int64 - OperationID int64 - BranchID int64 - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp - Name string - Description string + ID int64 `json:"id"` + OperationID int64 `json:"operation_id"` + BranchID int64 `json:"branch_id"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + Name string `json:"name"` + Description string `json:"description"` } func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]GetBranchOperationsRow, error) { @@ -518,12 +518,12 @@ RETURNING id, name, location, wallet_id, branch_manager_id, company_id, is_self_ ` type UpdateBranchParams struct { - Name string - Location string - BranchManagerID int64 - CompanyID int64 - IsSelfOwned bool - ID int64 + Name string `json:"name"` + Location string `json:"location"` + BranchManagerID int64 `json:"branch_manager_id"` + CompanyID int64 `json:"company_id"` + IsSelfOwned bool `json:"is_self_owned"` + ID int64 `json:"id"` } func (q *Queries) UpdateBranch(ctx context.Context, arg UpdateBranchParams) (Branch, error) { diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index e833d0e..6cb26e1 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -49,26 +49,26 @@ ON CONFLICT (id) DO UPDATE SET ` type InsertEventParams struct { - ID string - SportID pgtype.Text - MatchName pgtype.Text - HomeTeam pgtype.Text - AwayTeam pgtype.Text - HomeTeamID pgtype.Text - AwayTeamID pgtype.Text - HomeKitImage pgtype.Text - AwayKitImage pgtype.Text - LeagueID pgtype.Text - LeagueName pgtype.Text - LeagueCc pgtype.Text - StartTime pgtype.Timestamp - Score pgtype.Text - MatchMinute pgtype.Int4 - TimerStatus pgtype.Text - AddedTime pgtype.Int4 - MatchPeriod pgtype.Int4 - IsLive pgtype.Bool - Status pgtype.Text + ID string `json:"id"` + SportID pgtype.Text `json:"sport_id"` + MatchName pgtype.Text `json:"match_name"` + HomeTeam pgtype.Text `json:"home_team"` + AwayTeam pgtype.Text `json:"away_team"` + HomeTeamID pgtype.Text `json:"home_team_id"` + AwayTeamID pgtype.Text `json:"away_team_id"` + HomeKitImage pgtype.Text `json:"home_kit_image"` + AwayKitImage pgtype.Text `json:"away_kit_image"` + LeagueID pgtype.Text `json:"league_id"` + LeagueName pgtype.Text `json:"league_name"` + LeagueCc pgtype.Text `json:"league_cc"` + StartTime pgtype.Timestamp `json:"start_time"` + Score pgtype.Text `json:"score"` + MatchMinute pgtype.Int4 `json:"match_minute"` + TimerStatus pgtype.Text `json:"timer_status"` + AddedTime pgtype.Int4 `json:"added_time"` + MatchPeriod pgtype.Int4 `json:"match_period"` + IsLive pgtype.Bool `json:"is_live"` + Status pgtype.Text `json:"status"` } func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error { diff --git a/gen/db/models.go b/gen/db/models.go index 31b7b3b..8fe75fd 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -9,265 +9,265 @@ import ( ) type Bet struct { - ID int64 - Amount int64 - TotalOdds float32 - Status int32 - FullName string - PhoneNumber string - BranchID pgtype.Int8 - UserID pgtype.Int8 - CashedOut bool - CashoutID string - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp - IsShopBet bool + ID int64 `json:"id"` + Amount int64 `json:"amount"` + TotalOdds float32 `json:"total_odds"` + Status int32 `json:"status"` + FullName string `json:"full_name"` + PhoneNumber string `json:"phone_number"` + BranchID pgtype.Int8 `json:"branch_id"` + UserID pgtype.Int8 `json:"user_id"` + CashedOut bool `json:"cashed_out"` + CashoutID string `json:"cashout_id"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + IsShopBet bool `json:"is_shop_bet"` } type BetOutcome struct { - ID int64 - BetID int64 - EventID int64 - OddID int64 + ID int64 `json:"id"` + BetID int64 `json:"bet_id"` + EventID int64 `json:"event_id"` + OddID int64 `json:"odd_id"` } type BetWithOutcome struct { - ID int64 - Amount int64 - TotalOdds float32 - Status int32 - FullName string - PhoneNumber string - BranchID pgtype.Int8 - UserID pgtype.Int8 - CashedOut bool - CashoutID string - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp - IsShopBet bool - Outcomes []BetOutcome + ID int64 `json:"id"` + Amount int64 `json:"amount"` + TotalOdds float32 `json:"total_odds"` + Status int32 `json:"status"` + FullName string `json:"full_name"` + PhoneNumber string `json:"phone_number"` + BranchID pgtype.Int8 `json:"branch_id"` + UserID pgtype.Int8 `json:"user_id"` + CashedOut bool `json:"cashed_out"` + CashoutID string `json:"cashout_id"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + IsShopBet bool `json:"is_shop_bet"` + Outcomes []BetOutcome `json:"outcomes"` } type Branch struct { - ID int64 - Name string - Location string - WalletID int64 - BranchManagerID int64 - CompanyID int64 - IsSelfOwned bool - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp + ID int64 `json:"id"` + Name string `json:"name"` + Location string `json:"location"` + WalletID int64 `json:"wallet_id"` + BranchManagerID int64 `json:"branch_manager_id"` + CompanyID int64 `json:"company_id"` + IsSelfOwned bool `json:"is_self_owned"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type BranchCashier struct { - ID int64 - UserID int64 - BranchID int64 + ID int64 `json:"id"` + UserID int64 `json:"user_id"` + BranchID int64 `json:"branch_id"` } type BranchDetail struct { - ID int64 - Name string - Location string - WalletID int64 - BranchManagerID int64 - CompanyID int64 - IsSelfOwned bool - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp - ManagerName interface{} - ManagerPhoneNumber pgtype.Text + ID int64 `json:"id"` + Name string `json:"name"` + Location string `json:"location"` + WalletID int64 `json:"wallet_id"` + BranchManagerID int64 `json:"branch_manager_id"` + CompanyID int64 `json:"company_id"` + IsSelfOwned bool `json:"is_self_owned"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + ManagerName interface{} `json:"manager_name"` + ManagerPhoneNumber pgtype.Text `json:"manager_phone_number"` } type BranchOperation struct { - ID int64 - OperationID int64 - BranchID int64 - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp + ID int64 `json:"id"` + OperationID int64 `json:"operation_id"` + BranchID int64 `json:"branch_id"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type CustomerWallet struct { - ID int64 - CustomerID int64 - CompanyID int64 - RegularWalletID int64 - StaticWalletID int64 - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp + ID int64 `json:"id"` + CustomerID int64 `json:"customer_id"` + CompanyID int64 `json:"company_id"` + RegularWalletID int64 `json:"regular_wallet_id"` + StaticWalletID int64 `json:"static_wallet_id"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type Event struct { - ID string - SportID pgtype.Text - MatchName pgtype.Text - HomeTeam pgtype.Text - AwayTeam pgtype.Text - HomeTeamID pgtype.Text - AwayTeamID pgtype.Text - HomeKitImage pgtype.Text - AwayKitImage pgtype.Text - LeagueID pgtype.Text - LeagueName pgtype.Text - LeagueCc pgtype.Text - StartTime pgtype.Timestamp - Score pgtype.Text - MatchMinute pgtype.Int4 - TimerStatus pgtype.Text - AddedTime pgtype.Int4 - MatchPeriod pgtype.Int4 - IsLive pgtype.Bool - Status pgtype.Text - FetchedAt pgtype.Timestamp + ID string `json:"id"` + SportID pgtype.Text `json:"sport_id"` + MatchName pgtype.Text `json:"match_name"` + HomeTeam pgtype.Text `json:"home_team"` + AwayTeam pgtype.Text `json:"away_team"` + HomeTeamID pgtype.Text `json:"home_team_id"` + AwayTeamID pgtype.Text `json:"away_team_id"` + HomeKitImage pgtype.Text `json:"home_kit_image"` + AwayKitImage pgtype.Text `json:"away_kit_image"` + LeagueID pgtype.Text `json:"league_id"` + LeagueName pgtype.Text `json:"league_name"` + LeagueCc pgtype.Text `json:"league_cc"` + StartTime pgtype.Timestamp `json:"start_time"` + Score pgtype.Text `json:"score"` + MatchMinute pgtype.Int4 `json:"match_minute"` + TimerStatus pgtype.Text `json:"timer_status"` + AddedTime pgtype.Int4 `json:"added_time"` + MatchPeriod pgtype.Int4 `json:"match_period"` + IsLive pgtype.Bool `json:"is_live"` + Status pgtype.Text `json:"status"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` } type Notification struct { - ID string - RecipientID int64 - Type string - Level string - ErrorSeverity pgtype.Text - Reciever string - IsRead bool - DeliveryStatus string - DeliveryChannel pgtype.Text - Payload []byte - Priority pgtype.Int4 - Version int32 - Timestamp pgtype.Timestamptz - Metadata []byte + ID string `json:"id"` + RecipientID int64 `json:"recipient_id"` + Type string `json:"type"` + Level string `json:"level"` + ErrorSeverity pgtype.Text `json:"error_severity"` + Reciever string `json:"reciever"` + IsRead bool `json:"is_read"` + DeliveryStatus string `json:"delivery_status"` + DeliveryChannel pgtype.Text `json:"delivery_channel"` + Payload []byte `json:"payload"` + Priority pgtype.Int4 `json:"priority"` + Version int32 `json:"version"` + Timestamp pgtype.Timestamptz `json:"timestamp"` + Metadata []byte `json:"metadata"` } type Odd struct { - ID int32 - EventID pgtype.Text - Fi pgtype.Text - RawEventID pgtype.Text - MarketType string - MarketName pgtype.Text - MarketCategory pgtype.Text - MarketID pgtype.Text - Header pgtype.Text - Name pgtype.Text - Handicap pgtype.Text - OddsValue pgtype.Float8 - Section string - Category pgtype.Text - RawOdds []byte - FetchedAt pgtype.Timestamp - Source pgtype.Text - IsActive pgtype.Bool + ID int32 `json:"id"` + EventID pgtype.Text `json:"event_id"` + Fi pgtype.Text `json:"fi"` + RawEventID pgtype.Text `json:"raw_event_id"` + MarketType string `json:"market_type"` + MarketName pgtype.Text `json:"market_name"` + MarketCategory pgtype.Text `json:"market_category"` + MarketID pgtype.Text `json:"market_id"` + Header pgtype.Text `json:"header"` + 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"` } type Otp struct { - ID int64 - SentTo string - Medium string - OtpFor string - Otp string - Used bool - UsedAt pgtype.Timestamptz - CreatedAt pgtype.Timestamptz - ExpiresAt pgtype.Timestamptz + ID int64 `json:"id"` + SentTo string `json:"sent_to"` + Medium string `json:"medium"` + OtpFor string `json:"otp_for"` + Otp string `json:"otp"` + Used bool `json:"used"` + UsedAt pgtype.Timestamptz `json:"used_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + ExpiresAt pgtype.Timestamptz `json:"expires_at"` } type RefreshToken struct { - ID int64 - UserID int64 - Token string - ExpiresAt pgtype.Timestamptz - CreatedAt pgtype.Timestamptz - Revoked bool + ID int64 `json:"id"` + UserID int64 `json:"user_id"` + Token string `json:"token"` + ExpiresAt pgtype.Timestamptz `json:"expires_at"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + Revoked bool `json:"revoked"` } type SupportedOperation struct { - ID int64 - Name string - Description string + ID int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` } type Ticket struct { - ID int64 - Amount pgtype.Int8 - TotalOdds float32 - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp + ID int64 `json:"id"` + Amount pgtype.Int8 `json:"amount"` + TotalOdds float32 `json:"total_odds"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type TicketOutcome struct { - ID int64 - TicketID int64 - EventID int64 - OddID int64 + ID int64 `json:"id"` + TicketID int64 `json:"ticket_id"` + EventID int64 `json:"event_id"` + OddID int64 `json:"odd_id"` } type TicketWithOutcome struct { - ID int64 - Amount pgtype.Int8 - TotalOdds float32 - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp - Outcomes []TicketOutcome + ID int64 `json:"id"` + Amount pgtype.Int8 `json:"amount"` + TotalOdds float32 `json:"total_odds"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + Outcomes []TicketOutcome `json:"outcomes"` } type Transaction struct { - ID int64 - Amount int64 - BranchID int64 - CashierID int64 - BetID int64 - Type int64 - PaymentOption int64 - FullName string - PhoneNumber string - BankCode string - BeneficiaryName string - AccountName string - AccountNumber string - ReferenceNumber string - Verified bool - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp + ID int64 `json:"id"` + 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"` + Verified bool `json:"verified"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type User struct { - ID int64 - FirstName string - LastName string - Email pgtype.Text - PhoneNumber pgtype.Text - Role string - Password []byte - EmailVerified bool - PhoneVerified bool - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz - SuspendedAt pgtype.Timestamptz - Suspended bool + 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"` + SuspendedAt pgtype.Timestamptz `json:"suspended_at"` + Suspended bool `json:"suspended"` } type Wallet struct { - ID int64 - Balance int64 - IsWithdraw bool - IsBettable bool - IsTransferable bool - UserID int64 - IsActive bool - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp + ID int64 `json:"id"` + Balance int64 `json:"balance"` + IsWithdraw bool `json:"is_withdraw"` + IsBettable bool `json:"is_bettable"` + IsTransferable bool `json:"is_transferable"` + UserID int64 `json:"user_id"` + IsActive bool `json:"is_active"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type WalletTransfer struct { - ID int64 - Amount int64 - Type string - ReceiverWalletID int64 - SenderWalletID pgtype.Int8 - CashierID pgtype.Int8 - Verified bool - PaymentMethod string - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp + ID int64 `json:"id"` + Amount int64 `json:"amount"` + Type string `json:"type"` + ReceiverWalletID int64 `json:"receiver_wallet_id"` + SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` + CashierID pgtype.Int8 `json:"cashier_id"` + Verified bool `json:"verified"` + PaymentMethod string `json:"payment_method"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } diff --git a/gen/db/notification.sql.go b/gen/db/notification.sql.go index 3735d72..5bfedd6 100644 --- a/gen/db/notification.sql.go +++ b/gen/db/notification.sql.go @@ -20,19 +20,19 @@ INSERT INTO notifications ( ` type CreateNotificationParams struct { - ID string - RecipientID int64 - Type string - Level string - ErrorSeverity pgtype.Text - Reciever string - IsRead bool - DeliveryStatus string - DeliveryChannel pgtype.Text - Payload []byte - Priority pgtype.Int4 - Timestamp pgtype.Timestamptz - Metadata []byte + ID string `json:"id"` + RecipientID int64 `json:"recipient_id"` + Type string `json:"type"` + Level string `json:"level"` + ErrorSeverity pgtype.Text `json:"error_severity"` + Reciever string `json:"reciever"` + IsRead bool `json:"is_read"` + DeliveryStatus string `json:"delivery_status"` + DeliveryChannel pgtype.Text `json:"delivery_channel"` + Payload []byte `json:"payload"` + Priority pgtype.Int4 `json:"priority"` + Timestamp pgtype.Timestamptz `json:"timestamp"` + Metadata []byte `json:"metadata"` } func (q *Queries) CreateNotification(ctx context.Context, arg CreateNotificationParams) (Notification, error) { @@ -141,9 +141,9 @@ SELECT id, recipient_id, type, level, error_severity, reciever, is_read, deliver ` type ListNotificationsParams struct { - RecipientID int64 - Limit int32 - Offset int32 + RecipientID int64 `json:"recipient_id"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` } func (q *Queries) ListNotifications(ctx context.Context, arg ListNotificationsParams) ([]Notification, error) { @@ -210,10 +210,10 @@ UPDATE notifications SET delivery_status = $2, is_read = $3, metadata = $4 WHERE ` type UpdateNotificationStatusParams struct { - ID string - DeliveryStatus string - IsRead bool - Metadata []byte + ID string `json:"id"` + DeliveryStatus string `json:"delivery_status"` + IsRead bool `json:"is_read"` + Metadata []byte `json:"metadata"` } func (q *Queries) UpdateNotificationStatus(ctx context.Context, arg UpdateNotificationStatusParams) (Notification, error) { diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 003be80..3b8441c 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -112,20 +112,20 @@ ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET ` type InsertNonLiveOddParams struct { - EventID pgtype.Text - Fi pgtype.Text - RawEventID pgtype.Text - MarketType string - MarketName pgtype.Text - MarketCategory pgtype.Text - MarketID pgtype.Text - Header pgtype.Text - Name pgtype.Text - Handicap pgtype.Text - OddsValue pgtype.Float8 - Section string - Category pgtype.Text - RawOdds []byte + EventID pgtype.Text `json:"event_id"` + Fi pgtype.Text `json:"fi"` + RawEventID pgtype.Text `json:"raw_event_id"` + MarketType string `json:"market_type"` + MarketName pgtype.Text `json:"market_name"` + MarketCategory pgtype.Text `json:"market_category"` + MarketID pgtype.Text `json:"market_id"` + Header pgtype.Text `json:"header"` + 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"` } func (q *Queries) InsertNonLiveOdd(ctx context.Context, arg InsertNonLiveOddParams) error { diff --git a/gen/db/otp.sql.go b/gen/db/otp.sql.go index e0b9806..99cdd4c 100644 --- a/gen/db/otp.sql.go +++ b/gen/db/otp.sql.go @@ -17,12 +17,12 @@ VALUES ($1, $2, $3, $4, FALSE, $5, $6) ` type CreateOtpParams struct { - SentTo string - Medium string - OtpFor string - Otp string - CreatedAt pgtype.Timestamptz - ExpiresAt pgtype.Timestamptz + SentTo string `json:"sent_to"` + Medium string `json:"medium"` + OtpFor string `json:"otp_for"` + Otp string `json:"otp"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + ExpiresAt pgtype.Timestamptz `json:"expires_at"` } func (q *Queries) CreateOtp(ctx context.Context, arg CreateOtpParams) error { @@ -45,9 +45,9 @@ ORDER BY created_at DESC LIMIT 1 ` type GetOtpParams struct { - SentTo string - OtpFor string - Medium string + SentTo string `json:"sent_to"` + OtpFor string `json:"otp_for"` + Medium string `json:"medium"` } func (q *Queries) GetOtp(ctx context.Context, arg GetOtpParams) (Otp, error) { @@ -74,8 +74,8 @@ WHERE id = $1 ` type MarkOtpAsUsedParams struct { - ID int64 - UsedAt pgtype.Timestamptz + ID int64 `json:"id"` + UsedAt pgtype.Timestamptz `json:"used_at"` } func (q *Queries) MarkOtpAsUsed(ctx context.Context, arg MarkOtpAsUsedParams) error { diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index 150d386..59ebd69 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -18,8 +18,8 @@ RETURNING id, amount, total_odds, created_at, updated_at ` type CreateTicketParams struct { - Amount pgtype.Int8 - TotalOdds float32 + Amount pgtype.Int8 `json:"amount"` + TotalOdds float32 `json:"total_odds"` } func (q *Queries) CreateTicket(ctx context.Context, arg CreateTicketParams) (Ticket, error) { @@ -36,9 +36,9 @@ func (q *Queries) CreateTicket(ctx context.Context, arg CreateTicketParams) (Tic } type CreateTicketOutcomeParams struct { - TicketID int64 - EventID int64 - OddID int64 + TicketID int64 `json:"ticket_id"` + EventID int64 `json:"event_id"` + OddID int64 `json:"odd_id"` } const DeleteOldTickets = `-- name: DeleteOldTickets :exec diff --git a/gen/db/transactions.sql.go b/gen/db/transactions.sql.go index 8f15071..2865972 100644 --- a/gen/db/transactions.sql.go +++ b/gen/db/transactions.sql.go @@ -14,19 +14,19 @@ INSERT INTO transactions (amount, branch_id, cashier_id, bet_id, type, payment_o ` type CreateTransactionParams struct { - Amount int64 - BranchID int64 - CashierID int64 - BetID int64 - Type int64 - PaymentOption int64 - FullName string - PhoneNumber string - BankCode string - BeneficiaryName string - AccountName string - AccountNumber string - ReferenceNumber string + 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"` } func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (Transaction, error) { @@ -186,8 +186,8 @@ UPDATE transactions SET verified = $2, updated_at = CURRENT_TIMESTAMP WHERE id = ` type UpdateTransactionVerifiedParams struct { - ID int64 - Verified bool + ID int64 `json:"id"` + Verified bool `json:"verified"` } func (q *Queries) UpdateTransactionVerified(ctx context.Context, arg UpdateTransactionVerifiedParams) error { diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index 9faeaf5..f4d8cc2 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -16,13 +16,13 @@ INSERT INTO wallet_transfer (amount, type, receiver_wallet_id, sender_wallet_id, ` type CreateTransferParams struct { - Amount int64 - Type string - ReceiverWalletID int64 - SenderWalletID pgtype.Int8 - CashierID pgtype.Int8 - Verified bool - PaymentMethod string + Amount int64 `json:"amount"` + Type string `json:"type"` + ReceiverWalletID int64 `json:"receiver_wallet_id"` + SenderWalletID pgtype.Int8 `json:"sender_wallet_id"` + CashierID pgtype.Int8 `json:"cashier_id"` + Verified bool `json:"verified"` + PaymentMethod string `json:"payment_method"` } func (q *Queries) CreateTransfer(ctx context.Context, arg CreateTransferParams) (WalletTransfer, error) { @@ -148,8 +148,8 @@ UPDATE wallet_transfer SET verified = $1, updated_at = CURRENT_TIMESTAMP WHERE i ` type UpdateTransferVerificationParams struct { - Verified bool - ID int64 + Verified bool `json:"verified"` + ID int64 `json:"id"` } func (q *Queries) UpdateTransferVerification(ctx context.Context, arg UpdateTransferVerificationParams) error { diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index e259cb9..d4c5b34 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -27,13 +27,13 @@ SELECT EXISTS ( ` type CheckPhoneEmailExistParams struct { - PhoneNumber pgtype.Text - Email pgtype.Text + PhoneNumber pgtype.Text `json:"phone_number"` + Email pgtype.Text `json:"email"` } type CheckPhoneEmailExistRow struct { - PhoneExists bool - EmailExists bool + PhoneExists bool `json:"phone_exists"` + EmailExists bool `json:"email_exists"` } func (q *Queries) CheckPhoneEmailExist(ctx context.Context, arg CheckPhoneEmailExistParams) (CheckPhoneEmailExistRow, error) { @@ -70,29 +70,29 @@ RETURNING id, ` type CreateUserParams struct { - FirstName string - LastName string - Email pgtype.Text - PhoneNumber pgtype.Text - Role string - Password []byte - EmailVerified bool - PhoneVerified bool - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz + 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"` } type CreateUserRow struct { - ID int64 - FirstName string - LastName string - Email pgtype.Text - PhoneNumber pgtype.Text - Role string - EmailVerified bool - PhoneVerified bool - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz + 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"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` } func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) { @@ -149,16 +149,16 @@ FROM users ` type GetAllUsersRow struct { - ID int64 - FirstName string - LastName string - Email pgtype.Text - PhoneNumber pgtype.Text - Role string - EmailVerified bool - PhoneVerified bool - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz + 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"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` } func (q *Queries) GetAllUsers(ctx context.Context) ([]GetAllUsersRow, error) { @@ -208,16 +208,16 @@ WHERE email = $1 ` type GetUserByEmailRow struct { - ID int64 - FirstName string - LastName string - Email pgtype.Text - PhoneNumber pgtype.Text - Role string - EmailVerified bool - PhoneVerified bool - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz + 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"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` } func (q *Queries) GetUserByEmail(ctx context.Context, email pgtype.Text) (GetUserByEmailRow, error) { @@ -281,16 +281,16 @@ WHERE phone_number = $1 ` type GetUserByPhoneRow struct { - ID int64 - FirstName string - LastName string - Email pgtype.Text - PhoneNumber pgtype.Text - Role string - EmailVerified bool - PhoneVerified bool - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz + 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"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` } func (q *Queries) GetUserByPhone(ctx context.Context, phoneNumber pgtype.Text) (GetUserByPhoneRow, error) { @@ -329,16 +329,16 @@ WHERE first_name ILIKE '%' || $1 || '%' ` type SearchUserByNameOrPhoneRow struct { - ID int64 - FirstName string - LastName string - Email pgtype.Text - PhoneNumber pgtype.Text - Role string - EmailVerified bool - PhoneVerified bool - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz + 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"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` } func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, dollar_1 pgtype.Text) ([]SearchUserByNameOrPhoneRow, error) { @@ -383,10 +383,10 @@ WHERE ( ` type UpdatePasswordParams struct { - Password []byte - Email pgtype.Text - PhoneNumber pgtype.Text - UpdatedAt pgtype.Timestamptz + Password []byte `json:"password"` + Email pgtype.Text `json:"email"` + PhoneNumber pgtype.Text `json:"phone_number"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` } func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error { @@ -411,13 +411,13 @@ WHERE id = $7 ` type UpdateUserParams struct { - FirstName string - LastName string - Email pgtype.Text - PhoneNumber pgtype.Text - Role string - UpdatedAt pgtype.Timestamptz - ID int64 + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email pgtype.Text `json:"email"` + PhoneNumber pgtype.Text `json:"phone_number"` + Role string `json:"role"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + ID int64 `json:"id"` } func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index 5c3410a..6684284 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -23,10 +23,10 @@ RETURNING id, customer_id, company_id, regular_wallet_id, static_wallet_id, crea ` type CreateCustomerWalletParams struct { - CustomerID int64 - CompanyID int64 - RegularWalletID int64 - StaticWalletID int64 + CustomerID int64 `json:"customer_id"` + CompanyID int64 `json:"company_id"` + RegularWalletID int64 `json:"regular_wallet_id"` + StaticWalletID int64 `json:"static_wallet_id"` } func (q *Queries) CreateCustomerWallet(ctx context.Context, arg CreateCustomerWalletParams) (CustomerWallet, error) { @@ -61,10 +61,10 @@ RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_ac ` type CreateWalletParams struct { - IsWithdraw bool - IsBettable bool - IsTransferable bool - UserID int64 + IsWithdraw bool `json:"is_withdraw"` + IsBettable bool `json:"is_bettable"` + IsTransferable bool `json:"is_transferable"` + UserID int64 `json:"user_id"` } func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wallet, error) { @@ -105,16 +105,16 @@ FROM branches ` type GetAllBranchWalletsRow struct { - ID int64 - Balance int64 - IsActive bool - UpdatedAt pgtype.Timestamp - CreatedAt pgtype.Timestamp - Name string - Location string - BranchManagerID int64 - CompanyID int64 - IsSelfOwned bool + ID int64 `json:"id"` + Balance int64 `json:"balance"` + IsActive bool `json:"is_active"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + CreatedAt pgtype.Timestamp `json:"created_at"` + Name string `json:"name"` + Location string `json:"location"` + BranchManagerID int64 `json:"branch_manager_id"` + CompanyID int64 `json:"company_id"` + IsSelfOwned bool `json:"is_self_owned"` } func (q *Queries) GetAllBranchWallets(ctx context.Context) ([]GetAllBranchWalletsRow, error) { @@ -202,21 +202,21 @@ WHERE cw.customer_id = $1 ` type GetCustomerWalletParams struct { - CustomerID int64 - CompanyID int64 + CustomerID int64 `json:"customer_id"` + CompanyID int64 `json:"company_id"` } type GetCustomerWalletRow struct { - ID int64 - CustomerID int64 - CompanyID int64 - RegularID int64 - RegularBalance int64 - StaticID int64 - StaticBalance int64 - RegularUpdatedAt pgtype.Timestamp - StaticUpdatedAt pgtype.Timestamp - CreatedAt pgtype.Timestamp + ID int64 `json:"id"` + CustomerID int64 `json:"customer_id"` + CompanyID int64 `json:"company_id"` + RegularID int64 `json:"regular_id"` + RegularBalance int64 `json:"regular_balance"` + StaticID int64 `json:"static_id"` + StaticBalance int64 `json:"static_balance"` + RegularUpdatedAt pgtype.Timestamp `json:"regular_updated_at"` + StaticUpdatedAt pgtype.Timestamp `json:"static_updated_at"` + CreatedAt pgtype.Timestamp `json:"created_at"` } func (q *Queries) GetCustomerWallet(ctx context.Context, arg GetCustomerWalletParams) (GetCustomerWalletRow, error) { @@ -304,8 +304,8 @@ WHERE id = $2 ` type UpdateBalanceParams struct { - Balance int64 - ID int64 + Balance int64 `json:"balance"` + ID int64 `json:"id"` } func (q *Queries) UpdateBalance(ctx context.Context, arg UpdateBalanceParams) error { @@ -321,8 +321,8 @@ WHERE id = $2 ` type UpdateWalletActiveParams struct { - IsActive bool - ID int64 + IsActive bool `json:"is_active"` + ID int64 `json:"id"` } func (q *Queries) UpdateWalletActive(ctx context.Context, arg UpdateWalletActiveParams) error { diff --git a/internal/domain/ticket.go b/internal/domain/ticket.go index 18cbf68..50e23f3 100644 --- a/internal/domain/ticket.go +++ b/internal/domain/ticket.go @@ -1,10 +1,10 @@ package domain type TicketOutcome struct { - ID int64 - TicketID int64 - EventID int64 - OddID int64 + ID int64 `json:"id" example:"1"` + TicketID int64 `json:"ticket_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` } type CreateTicketOutcome struct { diff --git a/internal/repository/ticket.go b/internal/repository/ticket.go index a2d5c1a..cefdbf8 100644 --- a/internal/repository/ticket.go +++ b/internal/repository/ticket.go @@ -2,6 +2,7 @@ package repository import ( "context" + "fmt" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -78,6 +79,7 @@ func (s *Store) CreateTicketOutcome(ctx context.Context, outcomes []domain.Creat func (s *Store) GetTicketByID(ctx context.Context, id int64) (domain.GetTicket, error) { ticket, err := s.queries.GetTicketByID(ctx, id) + if err != nil { return domain.GetTicket{}, err } @@ -87,7 +89,7 @@ func (s *Store) GetTicketByID(ctx context.Context, id int64) (domain.GetTicket, func (s *Store) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) { tickets, err := s.queries.GetAllTickets(ctx) - + fmt.Printf("%v", tickets) if err != nil { return nil, err } @@ -95,6 +97,7 @@ func (s *Store) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) { var result []domain.GetTicket = make([]domain.GetTicket, 0, len(tickets)) for _, ticket := range tickets { result = append(result, convertDBTicketOutcomes(ticket)) + // fmt.Printf("%v", convertDBTicketOutcomes(ticket)) } return result, nil diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 68ddcf2..4b4b5f4 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -1,7 +1,6 @@ package httpserver import ( - "context" "log" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" @@ -16,31 +15,38 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S spec string task func() }{ - { - spec: "0 0 * * * *", // Every hour - task: func() { - if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - log.Printf(" FetchUpcomingEvents error: %v", err) - } - }, - }, - { - spec: "*/5 * * * * *", // Every 5 seconds - task: func() { - if err := eventService.FetchLiveEvents(context.Background()); err != nil { - log.Printf(" FetchLiveEvents error: %v", err) - } - }, - }, - { - spec: "*/5 * * * * *", // Every 5 seconds - task: func() { - if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - log.Printf(" FetchNonLiveOdds error: %v", err) - } - }, - }, - + // { + // spec: "0 0 * * * *", // Every hour + // task: func() { + // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + // log.Printf(" FetchUpcomingEvents error: %v", err) + // } + // }, + // }, + // { + // spec: "*/5 * * * * *", // Every 5 seconds + // task: func() { + // if err := eventService.FetchLiveEvents(context.Background()); err != nil { + // log.Printf(" FetchLiveEvents error: %v", err) + // } + // }, + // }, + // { + // spec: "0 0 * * * *", // Every 5 seconds + // task: func() { + // if err := eventService.FetchLiveEvents(context.Background()); err != nil { + // log.Printf(" FetchLiveEvents error: %v", err) + // } + // }, + // }, + // { + // spec: "0 0 * * * *", // Every 5 seconds + // task: func() { + // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + // log.Printf(" FetchNonLiveOdds error: %v", err) + // } + // }, + // }, } for _, job := range schedule { diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index efd3884..dca3acc 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -56,7 +56,6 @@ type CreateBetReq struct { FullName string `json:"full_name" example:"John"` PhoneNumber string `json:"phone_number" example:"1234567890"` IsShopBet bool `json:"is_shop_bet" example:"false"` - BranchID NullableInt64 `json:"branch_id" example:"1"` } type CreateBetRes struct { @@ -127,7 +126,6 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, // Get user_id from middleware userID := c.Locals("user_id").(int64) - var isShopBet bool var req CreateBetReq @@ -145,18 +143,12 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, } user, err := userSvc.GetUserByID(c.Context(), userID) - - if user.Role != domain.RoleCustomer { - isShopBet = true - if !req.BranchID.Valid { - logger.Error("CreateBetReq failed, branch id necessary") - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Branch ID necessary", - }) - } + cashoutUUID := uuid.New() + var bet domain.Bet + if user.Role != domain.RoleCashier { // Get the branch from the branch ID - branch, err := branchSvc.GetBranchByID(c.Context(), req.BranchID.Value) + branch, err := branchSvc.GetBranchByCashier(c.Context(), user.ID) if err != nil { logger.Error("CreateBetReq failed, branch id invalid") return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ @@ -165,43 +157,58 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, } // Deduct a percentage of the amount + // TODO move to service layer var deductedAmount = req.Amount / 10 - err = walletSvc.DeductFromWallet(c.Context(), branch.WalletID, domain.Currency(deductedAmount)) + err = walletSvc.DeductFromWallet(c.Context(), branch.WalletID, domain.ToCurrency(deductedAmount)) if err != nil { - logger.Error("CreateBetReq failed, unable to deduct from WalletID", branch.WalletID) + logger.Error("CreateBetReq failed, unable to deduct from WalletID") return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Unable to deduct from branch wallet", }) } + bet, err = 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: req.IsShopBet, + CashoutID: cashoutUUID.String(), + }) } else { - isShopBet = false // TODO if user is customer, get id from the token then get the wallet id from there and reduce the amount + bet, err = 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: req.IsShopBet, + CashoutID: cashoutUUID.String(), + }) } // TODO Validate Outcomes Here and make sure they didn't expire - cashoutUUID := uuid.New() - - bet, err := betSvc.CreateBet(c.Context(), domain.CreateBet{ - Amount: domain.Currency(req.Amount), - TotalOdds: req.TotalOdds, - Status: req.Status, - FullName: req.FullName, - PhoneNumber: req.PhoneNumber, - - BranchID: domain.ValidInt64{ - Value: req.BranchID.Value, - Valid: isShopBet, - }, - UserID: domain.ValidInt64{ - Value: userID, - Valid: !isShopBet, - }, - IsShopBet: req.IsShopBet, - CashoutID: cashoutUUID.String(), - }) - if err != nil { logger.Error("CreateBetReq failed", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index a5bf943..013eca6 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -57,7 +57,7 @@ func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, // TODO Validate Outcomes Here and make sure they didn't expire ticket, err := ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{ - Amount: domain.Currency(req.Amount), + Amount: domain.ToCurrency(req.Amount), TotalOdds: req.TotalOdds, }) if err != nil { diff --git a/internal/web_server/handlers/transaction_handler.go b/internal/web_server/handlers/transaction_handler.go index 1b5e9fa..c5b5ea4 100644 --- a/internal/web_server/handlers/transaction_handler.go +++ b/internal/web_server/handlers/transaction_handler.go @@ -96,7 +96,7 @@ func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service, } transaction, err := transactionSvc.CreateTransaction(c.Context(), domain.CreateTransaction{ - Amount: domain.Currency(req.Amount), + Amount: domain.ToCurrency(req.Amount), BranchID: req.BranchID, CashierID: req.CashierID, BetID: req.BetID, diff --git a/internal/web_server/handlers/transfer_handler.go b/internal/web_server/handlers/transfer_handler.go index c3d71af..983164e 100644 --- a/internal/web_server/handlers/transfer_handler.go +++ b/internal/web_server/handlers/transfer_handler.go @@ -64,10 +64,14 @@ func convertTransfer(transfer domain.Transfer) TransferWalletRes { } type CreateTransferReq struct { - Amount float64 `json:"amount" example:"100.0"` + Amount float32 `json:"amount" example:"100.0"` PaymentMethod string `json:"payment_method" example:"cash"` } +type CreateRefillReq struct { + Amount float32 `json:"amount" example:"100.0"` +} + // GetTransfersByWallet godoc // @Summary Get transfer by wallet // @Description Get transfer by wallet @@ -165,7 +169,7 @@ func TransferToWallet(logger *slog.Logger, walletSvc *wallet.Service, branchSvc return nil } - transfer, err := walletSvc.TransferToWallet(c.Context(), senderID, receiverID, domain.Currency(req.Amount), domain.PaymentMethod(req.PaymentMethod), domain.ValidInt64{Value: userID, Valid: true}) + transfer, err := walletSvc.TransferToWallet(c.Context(), senderID, receiverID, domain.ToCurrency(req.Amount), domain.PaymentMethod(req.PaymentMethod), domain.ValidInt64{Value: userID, Valid: true}) if !ok { response.WriteJSON(c, fiber.StatusInternalServerError, "Transfer Failed", err, nil) @@ -210,10 +214,10 @@ func RefillWallet(logger *slog.Logger, walletSvc *wallet.Service, validator *cus return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) } - var req CreateTransferReq + var req CreateRefillReq if err := c.BodyParser(&req); err != nil { - logger.Error("CreateTransferReq failed", "error", err) + logger.Error("CreateRefillReq failed", "error", err) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request", }) @@ -226,8 +230,8 @@ func RefillWallet(logger *slog.Logger, walletSvc *wallet.Service, validator *cus } transfer, err := walletSvc.RefillWallet(c.Context(), domain.CreateTransfer{ - Amount: domain.Currency(req.Amount), - PaymentMethod: domain.PaymentMethod(req.PaymentMethod), + Amount: domain.ToCurrency(req.Amount), + PaymentMethod: domain.TRANSFER_BANK, ReceiverWalletID: receiverID, CashierID: domain.ValidInt64{ Value: userID, diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index b2b6063..f199a7d 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -57,7 +57,6 @@ func (a *App) initAppRoutes() { a.fiber.Get("/company/:id/branch", handlers.GetBranchByCompanyID(a.logger, a.branchSvc, a.validator)) - a.fiber.Get("/prematch/odds/:event_id", handlers.GetPrematchOdds(a.logger, a.prematchSvc)) // Swagger @@ -103,7 +102,7 @@ func (a *App) initAppRoutes() { // /transfer/wallet - transfer from one wallet to another wallet a.fiber.Post("/transfer/wallet/:id", a.authMiddleware, handlers.TransferToWallet(a.logger, a.walletSvc, a.branchSvc, a.validator)) a.fiber.Get("/transfer/wallet/:id", a.authMiddleware, handlers.GetTransfersByWallet(a.logger, a.walletSvc, a.validator)) - a.fiber.Get("/transfer/refill/:id", a.authMiddleware, handlers.RefillWallet(a.logger, a.walletSvc, a.validator)) + a.fiber.Post("/transfer/refill/:id", a.authMiddleware, handlers.RefillWallet(a.logger, a.walletSvc, a.validator)) // Transactions a.fiber.Post("/transaction", a.authMiddleware, handlers.CreateTransaction(a.logger, a.transactionSvc, a.validator)) diff --git a/sqlc.yaml b/sqlc.yaml index 5e2ff3a..dca5591 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -9,7 +9,7 @@ sql: sql_package: "pgx/v5" out: "./gen/db" emit_exported_queries: true - emit_json_tags: false + emit_json_tags: true overrides: - db_type: "uuid" go_type: "github.com/google/uuid.UUID" From ed0d107f1a8335c4976d0f17fbde894141451650 Mon Sep 17 00:00:00 2001 From: OneTap Technologies Date: Sat, 12 Apr 2025 13:55:45 +0300 Subject: [PATCH 15/30] adding prematch --- internal/services/odds/service.go | 120 ++++++++++++++++-------------- internal/web_server/cron.go | 84 +++++++++++---------- 2 files changed, 110 insertions(+), 94 deletions(-) diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 09a32fa..467117b 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "io" + "log" "net/http" "strconv" "time" @@ -21,67 +22,78 @@ func New(token string, store *repository.Store) *ServiceImpl { return &ServiceImpl{token: token, store: store} } + func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { - sportIDs := []int{ - 1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, - 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, - 95, 110, 107, 151, 162, 148, - } - for _, sportID := range sportIDs { - upcomingURL := "https://api.b365api.com/v1/bet365/upcoming?sport_id=" + strconv.Itoa(sportID) + "&token=" + s.token - resp, err := http.Get(upcomingURL) - if err != nil { - continue - } - defer resp.Body.Close() + sportIDs := []int{ + 1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, + 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, + 95, 110, 107, 151, 162, 148, + } + for _, sportID := range sportIDs { + upcomingURL := "https://api.b365api.com/v1/bet365/upcoming?sport_id=" + strconv.Itoa(sportID) + "&token=" + s.token + log.Printf("Fetching upcoming odds for sport ID: %d from URL: %s", sportID, upcomingURL) + resp, err := http.Get(upcomingURL) + if err != nil { + log.Printf("Error fetching upcoming odds for sport ID: %d, error: %v", sportID, err) + continue + } + defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - var upcomingData struct { - Success int `json:"success"` - Results []struct { - ID string `json:"id"` - } `json:"results"` - } - if err := json.Unmarshal(body, &upcomingData); err != nil || upcomingData.Success != 1 { - continue - } + body, _ := io.ReadAll(resp.Body) + var upcomingData struct { + Success int `json:"success"` + Results []struct { + ID string `json:"id"` + } `json:"results"` + } + if err := json.Unmarshal(body, &upcomingData); err != nil || upcomingData.Success != 1 { + log.Printf("Failed to parse upcoming odds for sport ID: %d, error: %v", sportID, err) + continue + } - for _, ev := range upcomingData.Results { - eventID := ev.ID - prematchURL := "https://api.b365api.com/v3/bet365/prematch?token=" + s.token + "&FI=" + eventID - oddsResp, err := http.Get(prematchURL) - if err != nil { - continue - } - defer oddsResp.Body.Close() + log.Printf("Successfully fetched upcoming odds for sport ID: %d", sportID) - oddsBody, _ := io.ReadAll(oddsResp.Body) - var oddsData struct { - Success int `json:"success"` - Results []struct { - EventID string `json:"event_id"` - FI string `json:"FI"` - Main OddsSection `json:"main"` - } `json:"results"` - } - if err := json.Unmarshal(oddsBody, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { - continue - } + for _, ev := range upcomingData.Results { + eventID := ev.ID + prematchURL := "https://api.b365api.com/v3/bet365/prematch?token=" + s.token + "&FI=" + eventID + log.Printf("Fetching prematch odds for event ID: %s from URL: %s", eventID, prematchURL) + oddsResp, err := http.Get(prematchURL) + if err != nil { + log.Printf("Error fetching prematch odds for event ID: %s, error: %v", eventID, err) + continue + } + defer oddsResp.Body.Close() - result := oddsData.Results[0] - finalID := result.EventID - if finalID == "" { - finalID = result.FI - } - if finalID == "" { - continue - } + oddsBody, _ := io.ReadAll(oddsResp.Body) + var oddsData struct { + Success int `json:"success"` + Results []struct { + EventID string `json:"event_id"` + FI string `json:"FI"` + Main OddsSection `json:"main"` + } `json:"results"` + } + if err := json.Unmarshal(oddsBody, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { + log.Printf("Failed to parse prematch odds for event ID: %s, error: %v", eventID, err) + continue + } - s.storeSection(ctx, finalID, result.FI, "main", result.Main) - } - } + result := oddsData.Results[0] + finalID := result.EventID + if finalID == "" { + finalID = result.FI + } + if finalID == "" { + log.Printf("Skipping event with missing final ID for event ID: %s", eventID) + continue + } - return nil + log.Printf("Storing odds for event ID: %s, final ID: %s", eventID, finalID) + s.storeSection(ctx, finalID, result.FI, "main", result.Main) + } + } + + return nil } func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section OddsSection) { diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 71cb9b4..6bae5b1 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -1,56 +1,60 @@ package httpserver import ( - "context" - "log" + "context" + "log" - eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" - oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" - "github.com/robfig/cron/v3" + eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/robfig/cron/v3" ) func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service) { - c := cron.New(cron.WithSeconds()) + c := cron.New(cron.WithSeconds()) - schedule := []struct { - spec string - task func() - }{ - { - spec: "0 0 * * * *", // Every hour - task: func() { - if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - log.Printf(" FetchUpcomingEvents error: %v", err) - } - }, - }, - { - spec: "*/5 * * * * *", // Every 5 seconds - task: func() { - if err := eventService.FetchLiveEvents(context.Background()); err != nil { - log.Printf(" FetchLiveEvents error: %v", err) - } - }, - }, - { - spec: "0 */5 * * * *", // Every 5 minutes + schedule := []struct { + spec string + task func() + }{ + + { + spec: "0 0 * * * *", // Every hour + task: func() { + if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + log.Printf("FetchUpcomingEvents error: %v", err) + } + }, + }, + + + { + spec: "*/5 * * * * *", // Every 5 seconds + task: func() { + if err := eventService.FetchLiveEvents(context.Background()); err != nil { + log.Printf("FetchLiveEvents error: %v", err) + } + }, + }, + + + { + spec: "0 * * * * *", // Every 1 minute task: func() { if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - log.Printf(" FetchNonLiveOdds error: %v", err) + log.Printf("FetchNonLiveOdds error: %v", err) } }, }, - - - } + } - for _, job := range schedule { - if _, err := c.AddFunc(job.spec, job.task); err != nil { - log.Fatalf(" Failed to schedule cron job: %v", err) - } - } + + 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 event and odds services") -} + c.Start() + log.Println("Cron jobs started for event and odds services") +} \ No newline at end of file From 1726cfd63bb23e4b0d06d1857fc86685d5aa31e6 Mon Sep 17 00:00:00 2001 From: OneTap Technologies Date: Sat, 12 Apr 2025 14:43:09 +0300 Subject: [PATCH 16/30] adding upcoming --- db/query/events.sql | 29 ++++++++ gen/db/events.sql.go | 65 ++++++++++++++++++ internal/domain/event.go | 58 ++++++++++------ internal/repository/event.go | 17 +++++ internal/services/event/service.go | 104 +++++++++++++++-------------- internal/web_server/cron.go | 15 +++-- 6 files changed, 210 insertions(+), 78 deletions(-) diff --git a/db/query/events.sql b/db/query/events.sql index dddec63..8c96d9e 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -33,6 +33,35 @@ ON CONFLICT (id) DO UPDATE SET is_live = EXCLUDED.is_live, status = EXCLUDED.status, fetched_at = now(); +-- name: InsertUpcomingEvent :exec +INSERT INTO events ( + id, sport_id, match_name, home_team, away_team, + home_team_id, away_team_id, home_kit_image, away_kit_image, + league_id, league_name, league_cc, start_time, + is_live, status +) VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, $9, + $10, $11, $12, $13, + false, 'upcoming' +) +ON CONFLICT (id) DO UPDATE SET + sport_id = EXCLUDED.sport_id, + match_name = EXCLUDED.match_name, + home_team = EXCLUDED.home_team, + away_team = EXCLUDED.away_team, + home_team_id = EXCLUDED.home_team_id, + away_team_id = EXCLUDED.away_team_id, + home_kit_image = EXCLUDED.home_kit_image, + away_kit_image = EXCLUDED.away_kit_image, + league_id = EXCLUDED.league_id, + league_name = EXCLUDED.league_name, + league_cc = EXCLUDED.league_cc, + start_time = EXCLUDED.start_time, + is_live = false, + status = 'upcoming', + fetched_at = now(); + -- name: ListLiveEvents :many SELECT id FROM events WHERE is_live = true; \ No newline at end of file diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index e833d0e..8f1b423 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -97,6 +97,71 @@ func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error return err } +const InsertUpcomingEvent = `-- name: InsertUpcomingEvent :exec +INSERT INTO events ( + id, sport_id, match_name, home_team, away_team, + home_team_id, away_team_id, home_kit_image, away_kit_image, + league_id, league_name, league_cc, start_time, + is_live, status +) VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, $9, + $10, $11, $12, $13, + false, 'upcoming' +) +ON CONFLICT (id) DO UPDATE SET + sport_id = EXCLUDED.sport_id, + match_name = EXCLUDED.match_name, + home_team = EXCLUDED.home_team, + away_team = EXCLUDED.away_team, + home_team_id = EXCLUDED.home_team_id, + away_team_id = EXCLUDED.away_team_id, + home_kit_image = EXCLUDED.home_kit_image, + away_kit_image = EXCLUDED.away_kit_image, + league_id = EXCLUDED.league_id, + league_name = EXCLUDED.league_name, + league_cc = EXCLUDED.league_cc, + start_time = EXCLUDED.start_time, + is_live = false, + status = 'upcoming', + fetched_at = now() +` + +type InsertUpcomingEventParams struct { + ID string + SportID pgtype.Text + MatchName pgtype.Text + HomeTeam pgtype.Text + AwayTeam pgtype.Text + HomeTeamID pgtype.Text + AwayTeamID pgtype.Text + HomeKitImage pgtype.Text + AwayKitImage pgtype.Text + LeagueID pgtype.Text + LeagueName pgtype.Text + LeagueCc pgtype.Text + StartTime pgtype.Timestamp +} + +func (q *Queries) InsertUpcomingEvent(ctx context.Context, arg InsertUpcomingEventParams) error { + _, err := q.db.Exec(ctx, InsertUpcomingEvent, + arg.ID, + arg.SportID, + arg.MatchName, + arg.HomeTeam, + arg.AwayTeam, + arg.HomeTeamID, + arg.AwayTeamID, + arg.HomeKitImage, + arg.AwayKitImage, + arg.LeagueID, + arg.LeagueName, + arg.LeagueCc, + arg.StartTime, + ) + return err +} + const ListLiveEvents = `-- name: ListLiveEvents :many SELECT id FROM events WHERE is_live = true ` diff --git a/internal/domain/event.go b/internal/domain/event.go index fb7edf4..0a69607 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -1,23 +1,41 @@ package domain + +import "time" + type Event struct { - ID string - SportID string - MatchName string - HomeTeam string - AwayTeam string - HomeTeamID string - AwayTeamID string - HomeKitImage string - AwayKitImage string - LeagueID string - LeagueName string - LeagueCC string - StartTime string - Score string - MatchMinute int - TimerStatus string - AddedTime int - MatchPeriod int - IsLive bool - Status string + ID string + SportID string + MatchName string + HomeTeam string + AwayTeam string + HomeTeamID string + AwayTeamID string + HomeKitImage string + AwayKitImage string + LeagueID string + LeagueName string + LeagueCC string + StartTime string + Score string + MatchMinute int + TimerStatus string + AddedTime int + MatchPeriod int + IsLive bool + Status string +} +type UpcomingEvent struct { + ID string // Event ID + SportID string // Sport ID + MatchName string // Match or event name + HomeTeam string // Home team name (if available) + AwayTeam string // Away team name (can be empty/null) + HomeTeamID string // Home team ID + AwayTeamID string // Away team ID (can be empty/null) + HomeKitImage string // Kit or image for home team (optional) + AwayKitImage string // Kit or image for away team (optional) + LeagueID string // League ID + LeagueName string // League name + LeagueCC string // League country code + StartTime time.Time // Converted from "time" field in UNIX format } \ No newline at end of file diff --git a/internal/repository/event.go b/internal/repository/event.go index 1949a44..282b89c 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -38,6 +38,23 @@ func (s *Store) SaveEvent(ctx context.Context, e domain.Event) error { Status: pgtype.Text{String: e.Status, Valid: true}, }) } +func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) error { + return s.queries.InsertUpcomingEvent(ctx, dbgen.InsertUpcomingEventParams{ + ID: e.ID, + SportID: pgtype.Text{String: e.SportID, Valid: true}, + MatchName: pgtype.Text{String: e.MatchName, Valid: true}, + HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true}, + AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true}, + HomeTeamID: pgtype.Text{String: e.HomeTeamID, Valid: true}, + AwayTeamID: pgtype.Text{String: e.AwayTeamID, Valid: true}, + HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true}, + AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true}, + LeagueID: pgtype.Text{String: e.LeagueID, Valid: true}, + LeagueName: pgtype.Text{String: e.LeagueName, Valid: true}, + LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true}, + StartTime: pgtype.Timestamp{Time: e.StartTime, Valid: true}, + }) +} func (s *Store) GetLiveEventIDs(ctx context.Context) ([]string, error) { return s.queries.ListLiveEvents(ctx) diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 24207ca..f047cd2 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "strconv" "sync" "time" @@ -96,65 +97,66 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { func (s *service) FetchUpcomingEvents(ctx context.Context) error { sportIDs := []int{1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, 95, 110, 107, 151, 162, 148} - - var wg sync.WaitGroup - for _, sportID := range sportIDs { - wg.Add(1) - go func(sportID int) { - defer wg.Done() + url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token) + resp, err := http.Get(url) + if err != nil { + continue + } + defer resp.Body.Close() - url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token) - resp, err := http.Get(url) - if err != nil { - fmt.Printf(" Failed request for upcoming sport_id=%d: %v\n", sportID, err) - return - } - defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + var data struct { + Success int `json:"success"` + Results []struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + League struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"league"` + Home struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"home"` + Away *struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"away"` + } `json:"results"` + } + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { + continue + } - body, _ := io.ReadAll(resp.Body) - - var data struct { - Success int `json:"success"` - Results [][]map[string]interface{} `json:"results"` - } - if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - fmt.Printf(" Decode failed for upcoming sport_id=%d\nRaw: %s\n", sportID, string(body)) - return + for _, ev := range data.Results { + startUnix, _ := strconv.ParseInt(ev.Time, 10, 64) + event := domain.UpcomingEvent{ + ID: ev.ID, + SportID: ev.SportID, + MatchName: ev.Home.Name, + HomeTeam: ev.Home.Name, + AwayTeam: "", // handle nil safely + HomeTeamID: ev.Home.ID, + AwayTeamID: "", + HomeKitImage: "", + AwayKitImage: "", + LeagueID: ev.League.ID, + LeagueName: ev.League.Name, + LeagueCC: "", + StartTime: time.Unix(startUnix, 0).UTC(), } - for _, group := range data.Results { - for _, ev := range group { - if getString(ev["type"]) != "EV" { - continue - } - - event := domain.Event{ - ID: getString(ev["ID"]), - SportID: fmt.Sprintf("%d", sportID), - MatchName: getString(ev["NA"]), - HomeTeamID: getString(ev["HT"]), - AwayTeamID: getString(ev["AT"]), - HomeKitImage: getString(ev["K1"]), - AwayKitImage: getString(ev["K2"]), - LeagueID: getString(ev["C2"]), - LeagueName: getString(ev["CT"]), - LeagueCC: getString(ev["CB"]), - StartTime: time.Now().UTC().Format(time.RFC3339), - IsLive: false, - Status: "upcoming", - } - - if err := s.store.SaveEvent(ctx, event); err != nil { - fmt.Printf(" Could not store upcoming event [id=%s]: %v\n", event.ID, err) - } - } + if ev.Away != nil { + event.AwayTeam = ev.Away.Name + event.AwayTeamID = ev.Away.ID } - }(sportID) + + _ = s.store.SaveUpcomingEvent(ctx, event) + } } - wg.Wait() - fmt.Println(" All upcoming events fetched and stored.") return nil } diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 6bae5b1..dad7d61 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -18,13 +18,14 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }{ { - spec: "0 0 * * * *", // Every hour - task: func() { - if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - log.Printf("FetchUpcomingEvents error: %v", err) - } - }, - }, + spec: "*/5 * * * * *", // Every 5 seconds + task: func() { + if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + log.Printf("FetchUpcomingEvents error: %v", err) + } + }, + }, + { From 6c478df2b4f56612ba82de569aeded5ef901b2bb Mon Sep 17 00:00:00 2001 From: OneTap Technologies Date: Sat, 12 Apr 2025 15:55:42 +0300 Subject: [PATCH 17/30] adding upcoming data --- cmd/main.go | 2 +- db/query/events.sql | 48 +++++++- docs/docs.go | 133 ++++++++++++++++++++ docs/swagger.json | 133 ++++++++++++++++++++ docs/swagger.yaml | 92 ++++++++++++++ gen/db/events.sql.go | 148 +++++++++++++++++++++++ internal/repository/event.go | 50 ++++++++ internal/services/event/port.go | 8 +- internal/services/event/service.go | 10 +- internal/web_server/app.go | 10 +- internal/web_server/cron.go | 8 +- internal/web_server/handlers/prematch.go | 47 ++++++- internal/web_server/routes.go | 3 + 13 files changed, 679 insertions(+), 13 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 31a728e..c27c482 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -67,7 +67,7 @@ func main() { app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, - }, userSvc, oddsSvc) + }, userSvc, oddsSvc, eventSvc) logger.Info("Starting server", "port", cfg.Port) if err := app.Run(); err != nil { diff --git a/db/query/events.sql b/db/query/events.sql index 8c96d9e..66a28cc 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -64,4 +64,50 @@ ON CONFLICT (id) DO UPDATE SET -- name: ListLiveEvents :many -SELECT id FROM events WHERE is_live = true; \ No newline at end of file +SELECT id FROM events WHERE is_live = true; + +-- name: GetAllUpcomingEvents :many +SELECT + id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + is_live, + status, + fetched_at +FROM events +WHERE is_live = false + AND status = 'upcoming' +ORDER BY start_time ASC; +-- name: GetUpcomingByID :one +SELECT + id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + is_live, + status, + fetched_at +FROM events +WHERE id = $1 + AND is_live = false + AND status = 'upcoming' +LIMIT 1; diff --git a/docs/docs.go b/docs/docs.go index 6143af4..b815188 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -180,6 +180,82 @@ const docTemplate = `{ } } }, + "/prematch/events": { + "get": { + "description": "Retrieve all upcoming events from the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve all upcoming events", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/prematch/events/{id}": { + "get": { + "description": "Retrieve an upcoming event by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve an upcoming by ID", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/prematch/odds": { "get": { "description": "Retrieve all prematch odds from the database", @@ -666,6 +742,63 @@ const docTemplate = `{ "RoleCashier" ] }, + "domain.UpcomingEvent": { + "type": "object", + "properties": { + "awayKitImage": { + "description": "Kit or image for away team (optional)", + "type": "string" + }, + "awayTeam": { + "description": "Away team name (can be empty/null)", + "type": "string" + }, + "awayTeamID": { + "description": "Away team ID (can be empty/null)", + "type": "string" + }, + "homeKitImage": { + "description": "Kit or image for home team (optional)", + "type": "string" + }, + "homeTeam": { + "description": "Home team name (if available)", + "type": "string" + }, + "homeTeamID": { + "description": "Home team ID", + "type": "string" + }, + "id": { + "description": "Event ID", + "type": "string" + }, + "leagueCC": { + "description": "League country code", + "type": "string" + }, + "leagueID": { + "description": "League ID", + "type": "string" + }, + "leagueName": { + "description": "League name", + "type": "string" + }, + "matchName": { + "description": "Match or event name", + "type": "string" + }, + "sportID": { + "description": "Sport ID", + "type": "string" + }, + "startTime": { + "description": "Converted from \"time\" field in UNIX format", + "type": "string" + } + } + }, "handlers.CheckPhoneEmailExistReq": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 8692c31..061eb47 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -172,6 +172,82 @@ } } }, + "/prematch/events": { + "get": { + "description": "Retrieve all upcoming events from the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve all upcoming events", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/prematch/events/{id}": { + "get": { + "description": "Retrieve an upcoming event by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve an upcoming by ID", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/prematch/odds": { "get": { "description": "Retrieve all prematch odds from the database", @@ -658,6 +734,63 @@ "RoleCashier" ] }, + "domain.UpcomingEvent": { + "type": "object", + "properties": { + "awayKitImage": { + "description": "Kit or image for away team (optional)", + "type": "string" + }, + "awayTeam": { + "description": "Away team name (can be empty/null)", + "type": "string" + }, + "awayTeamID": { + "description": "Away team ID (can be empty/null)", + "type": "string" + }, + "homeKitImage": { + "description": "Kit or image for home team (optional)", + "type": "string" + }, + "homeTeam": { + "description": "Home team name (if available)", + "type": "string" + }, + "homeTeamID": { + "description": "Home team ID", + "type": "string" + }, + "id": { + "description": "Event ID", + "type": "string" + }, + "leagueCC": { + "description": "League country code", + "type": "string" + }, + "leagueID": { + "description": "League ID", + "type": "string" + }, + "leagueName": { + "description": "League name", + "type": "string" + }, + "matchName": { + "description": "Match or event name", + "type": "string" + }, + "sportID": { + "description": "Sport ID", + "type": "string" + }, + "startTime": { + "description": "Converted from \"time\" field in UNIX format", + "type": "string" + } + } + }, "handlers.CheckPhoneEmailExistReq": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7bfdfa4..55be753 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -63,6 +63,48 @@ definitions: - RoleBranchManager - RoleCustomer - RoleCashier + domain.UpcomingEvent: + properties: + awayKitImage: + description: Kit or image for away team (optional) + type: string + awayTeam: + description: Away team name (can be empty/null) + type: string + awayTeamID: + description: Away team ID (can be empty/null) + type: string + homeKitImage: + description: Kit or image for home team (optional) + type: string + homeTeam: + description: Home team name (if available) + type: string + homeTeamID: + description: Home team ID + type: string + id: + description: Event ID + type: string + leagueCC: + description: League country code + type: string + leagueID: + description: League ID + type: string + leagueName: + description: League name + type: string + matchName: + description: Match or event name + type: string + sportID: + description: Sport ID + type: string + startTime: + description: Converted from "time" field in UNIX format + type: string + type: object handlers.CheckPhoneEmailExistReq: properties: email: @@ -325,6 +367,56 @@ paths: summary: Refresh token tags: - auth + /prematch/events: + get: + consumes: + - application/json + description: Retrieve all upcoming events from the database + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.UpcomingEvent' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve all upcoming events + tags: + - prematch + /prematch/events/{id}: + get: + consumes: + - application/json + description: Retrieve an upcoming event by ID + parameters: + - description: ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.UpcomingEvent' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve an upcoming by ID + tags: + - prematch /prematch/odds: get: consumes: diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 8f1b423..0c597be 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -11,6 +11,154 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const GetAllUpcomingEvents = `-- name: GetAllUpcomingEvents :many +SELECT + id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + is_live, + status, + fetched_at +FROM events +WHERE is_live = false + AND status = 'upcoming' +ORDER BY start_time ASC +` + +type GetAllUpcomingEventsRow struct { + ID string + SportID pgtype.Text + MatchName pgtype.Text + HomeTeam pgtype.Text + AwayTeam pgtype.Text + HomeTeamID pgtype.Text + AwayTeamID pgtype.Text + HomeKitImage pgtype.Text + AwayKitImage pgtype.Text + LeagueID pgtype.Text + LeagueName pgtype.Text + LeagueCc pgtype.Text + StartTime pgtype.Timestamp + IsLive pgtype.Bool + Status pgtype.Text + FetchedAt pgtype.Timestamp +} + +func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]GetAllUpcomingEventsRow, error) { + rows, err := q.db.Query(ctx, GetAllUpcomingEvents) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllUpcomingEventsRow + for rows.Next() { + var i GetAllUpcomingEventsRow + if err := rows.Scan( + &i.ID, + &i.SportID, + &i.MatchName, + &i.HomeTeam, + &i.AwayTeam, + &i.HomeTeamID, + &i.AwayTeamID, + &i.HomeKitImage, + &i.AwayKitImage, + &i.LeagueID, + &i.LeagueName, + &i.LeagueCc, + &i.StartTime, + &i.IsLive, + &i.Status, + &i.FetchedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetUpcomingByID = `-- name: GetUpcomingByID :one +SELECT + id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + is_live, + status, + fetched_at +FROM events +WHERE id = $1 + AND is_live = false + AND status = 'upcoming' +LIMIT 1 +` + +type GetUpcomingByIDRow struct { + ID string + SportID pgtype.Text + MatchName pgtype.Text + HomeTeam pgtype.Text + AwayTeam pgtype.Text + HomeTeamID pgtype.Text + AwayTeamID pgtype.Text + HomeKitImage pgtype.Text + AwayKitImage pgtype.Text + LeagueID pgtype.Text + LeagueName pgtype.Text + LeagueCc pgtype.Text + StartTime pgtype.Timestamp + IsLive pgtype.Bool + Status pgtype.Text + FetchedAt pgtype.Timestamp +} + +func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (GetUpcomingByIDRow, error) { + row := q.db.QueryRow(ctx, GetUpcomingByID, id) + var i GetUpcomingByIDRow + err := row.Scan( + &i.ID, + &i.SportID, + &i.MatchName, + &i.HomeTeam, + &i.AwayTeam, + &i.HomeTeamID, + &i.AwayTeamID, + &i.HomeKitImage, + &i.AwayKitImage, + &i.LeagueID, + &i.LeagueName, + &i.LeagueCc, + &i.StartTime, + &i.IsLive, + &i.Status, + &i.FetchedAt, + ) + return i, err +} + const InsertEvent = `-- name: InsertEvent :exec INSERT INTO events ( id, sport_id, match_name, home_team, away_team, diff --git a/internal/repository/event.go b/internal/repository/event.go index 282b89c..9493087 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -6,6 +6,7 @@ import ( dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/jackc/pgx/v5/pgtype" ) @@ -59,3 +60,52 @@ func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) e func (s *Store) GetLiveEventIDs(ctx context.Context) ([]string, error) { return s.queries.ListLiveEvents(ctx) } +func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { + events, err := s.queries.GetAllUpcomingEvents(ctx) + if err != nil { + return nil, err + } + + upcomingEvents := make([]domain.UpcomingEvent, len(events)) + for i, e := range events { + upcomingEvents[i] = domain.UpcomingEvent{ + ID: e.ID, + SportID: e.SportID.String, + MatchName: e.MatchName.String, + HomeTeam: e.HomeTeam.String, + AwayTeam: e.AwayTeam.String, + HomeTeamID: e.HomeTeamID.String, + AwayTeamID: e.AwayTeamID.String, + HomeKitImage: e.HomeKitImage.String, + AwayKitImage: e.AwayKitImage.String, + LeagueID: e.LeagueID.String, + LeagueName: e.LeagueName.String, + LeagueCC: e.LeagueCc.String, + StartTime: e.StartTime.Time.UTC(), + } + } + return upcomingEvents, nil +} +func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { + event, err := s.queries.GetUpcomingByID(ctx, ID) + if err != nil { + return domain.UpcomingEvent{}, err + } + + return domain.UpcomingEvent{ + ID: event.ID, + SportID: event.SportID.String, + MatchName: event.MatchName.String, + HomeTeam: event.HomeTeam.String, + AwayTeam: event.AwayTeam.String, + HomeTeamID: event.HomeTeamID.String, + AwayTeamID: event.AwayTeamID.String, + HomeKitImage: event.HomeKitImage.String, + AwayKitImage: event.AwayKitImage.String, + LeagueID: event.LeagueID.String, + LeagueName: event.LeagueName.String, + LeagueCC: event.LeagueCc.String, + StartTime: event.StartTime.Time.UTC(), + }, nil +} + diff --git a/internal/services/event/port.go b/internal/services/event/port.go index b500ca4..2a81a1a 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -1,8 +1,14 @@ package event -import "context" +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) type Service interface { FetchLiveEvents(ctx context.Context) error FetchUpcomingEvents(ctx context.Context) error + GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) + GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index f047cd2..350c8cf 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -12,6 +12,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" ) type service struct { @@ -172,4 +173,11 @@ func getInt(v interface{}) int { return int(f) } return 0 -} \ No newline at end of file +} +func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { + return s.store.GetAllUpcomingEvents(ctx) +} + +func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { + return s.store.GetUpcomingEventByID(ctx, ID) +} diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 4149f6b..6f29cc8 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -5,7 +5,8 @@ import ( "log/slog" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" @@ -22,7 +23,8 @@ type App struct { userSvc *user.Service validator *customvalidator.CustomValidator JwtConfig jwtutil.JwtConfig - prematchSvc *odds.ServiceImpl + prematchSvc *odds.ServiceImpl + eventSvc event.Service } func NewApp( @@ -32,6 +34,7 @@ func NewApp( JwtConfig jwtutil.JwtConfig, userSvc *user.Service, prematchSvc *odds.ServiceImpl, + eventSvc event.Service, ) *App { app := fiber.New(fiber.Config{ CaseSensitive: true, @@ -47,7 +50,8 @@ func NewApp( logger: logger, JwtConfig: JwtConfig, userSvc: userSvc, - prematchSvc: prematchSvc, + prematchSvc: prematchSvc, + eventSvc: eventSvc, } s.initAppRoutes() diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index dad7d61..afbccc9 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -18,7 +18,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }{ { - spec: "*/5 * * * * *", // Every 5 seconds + spec: "0 0 * * * *", // Every hour at minute 0 and second 0 task: func() { if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { log.Printf("FetchUpcomingEvents error: %v", err) @@ -26,8 +26,6 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }, }, - - { spec: "*/5 * * * * *", // Every 5 seconds task: func() { @@ -38,8 +36,8 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }, - { - spec: "0 * * * * *", // Every 1 minute + { + spec: "0 0 * * * *", // Every hour at minute 0 and second 0 task: func() { if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { log.Printf("FetchNonLiveOdds error: %v", err) diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index 5e08966..4fea8a7 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -3,6 +3,7 @@ package handlers import ( "github.com/gofiber/fiber/v2" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "log/slog" ) @@ -77,4 +78,48 @@ func GetRawOddsByID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Ha return response.WriteJSON(c, fiber.StatusOK, "Raw odds retrieved successfully", rawOdds, nil) } -} \ No newline at end of file +} + +// @Summary Retrieve all upcoming events +// @Description Retrieve all upcoming events from the database +// @Tags prematch +// @Accept json +// @Produce json +// @Success 200 {array} domain.UpcomingEvent +// @Failure 500 {object} response.APIResponse +// @Router /prematch/events [get] +func GetAllUpcomingEvents(logger *slog.Logger, eventSvc event.Service) fiber.Handler { + return func(c *fiber.Ctx) error { + events, err := eventSvc.GetAllUpcomingEvents(c.Context()) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve all upcoming events", nil, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", events, nil) + } +} +// @Summary Retrieve an upcoming by ID +// @Description Retrieve an upcoming event by ID +// @Tags prematch +// @Accept json +// @Produce json +// @Param id path string true "ID" +// @Success 200 {object} domain.UpcomingEvent +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /prematch/events/{id} [get] +func GetUpcomingEventByID(logger *slog.Logger, eventSvc event.Service) fiber.Handler { + return func(c *fiber.Ctx) error { + id := c.Params("id") + if id == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing id", nil, nil) + } + + event, err := eventSvc.GetUpcomingEventByID(c.Context(), id) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve upcoming event", nil, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Upcoming event retrieved successfully", event, nil) + } +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 6bd82f5..5841fbb 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -30,6 +30,9 @@ func (a *App) initAppRoutes() { a.fiber.Get("/prematch/odds/:event_id", handlers.GetPrematchOdds(a.logger, a.prematchSvc)) a.fiber.Get("/prematch/odds", handlers.GetALLPrematchOdds(a.logger, a.prematchSvc)) a.fiber.Get("/prematch/odds/raw/:raw_odds_id", handlers.GetRawOddsByID(a.logger, a.prematchSvc)) + + a.fiber.Get("/prematch/events/:id", handlers.GetUpcomingEventByID(a.logger, a.eventSvc)) + a.fiber.Get("/prematch/events", handlers.GetAllUpcomingEvents(a.logger, a.eventSvc)) // Swagger a.fiber.Get("/swagger/*", fiberSwagger.WrapHandler) } From b8d15695a4498c5b93d0cd1d1363cc4a77555af3 Mon Sep 17 00:00:00 2001 From: OneTap Technologies Date: Sun, 13 Apr 2025 10:55:16 +0300 Subject: [PATCH 18/30] adding fi --- db/migrations/000001_fortune.up.sql | 4 +- db/query/odds.sql | 91 +++++++------ docs/docs.go | 64 ++++++++- docs/swagger.json | 64 ++++++++- docs/swagger.yaml | 44 ++++++- gen/db/models.go | 2 - gen/db/odds.sql.go | 159 ++++++++++++++++++----- internal/domain/odds.go | 5 - internal/repository/odds.go | 64 +++++++-- internal/services/event/service.go | 2 +- internal/services/odds/service.go | 92 +++++-------- internal/web_server/cron.go | 52 ++++---- internal/web_server/handlers/prematch.go | 50 ++++++- internal/web_server/routes.go | 1 + 14 files changed, 496 insertions(+), 198 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 602a6ae..5d3ee99 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -90,12 +90,10 @@ CREATE TABLE odds ( id SERIAL PRIMARY KEY, event_id TEXT, fi TEXT, - raw_event_id TEXT, market_type TEXT NOT NULL, market_name TEXT, market_category TEXT, market_id TEXT, - header TEXT, name TEXT, handicap TEXT, odds_value DOUBLE PRECISION, @@ -105,7 +103,7 @@ CREATE TABLE odds ( fetched_at TIMESTAMP DEFAULT now(), source TEXT DEFAULT 'b365api', is_active BOOLEAN DEFAULT true, - UNIQUE (event_id, market_id, header, name, handicap) + UNIQUE (event_id, market_id, name, handicap) ); diff --git a/db/query/odds.sql b/db/query/odds.sql index 5158ee0..912b1f9 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -2,12 +2,10 @@ INSERT INTO odds ( event_id, fi, - raw_event_id, market_type, market_name, market_category, market_id, - header, name, handicap, odds_value, @@ -19,48 +17,21 @@ INSERT INTO odds ( fetched_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, - $8, $9, $10, $11, $12, $13, $14, - true, 'b365api', now() + $8, $9, $10, $11, $12, $13, $14, $15 ) -ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET +ON CONFLICT (market_id, name, handicap) DO UPDATE SET odds_value = EXCLUDED.odds_value, raw_odds = EXCLUDED.raw_odds, market_type = EXCLUDED.market_type, market_name = EXCLUDED.market_name, market_category = EXCLUDED.market_category, - fetched_at = now(), - is_active = true, - source = 'b365api', - fi = EXCLUDED.fi, - raw_event_id = EXCLUDED.raw_event_id; - + fetched_at = EXCLUDED.fetched_at, + is_active = EXCLUDED.is_active, + source = EXCLUDED.source, + fi = EXCLUDED.fi; -- name: GetPrematchOdds :many SELECT - id, - event_id, - fi, - raw_event_id, - market_type, - market_name, - market_category, - market_id, - header, - name, - handicap, - odds_value, - section, - category, - raw_odds, - fetched_at, - source, - is_active -FROM odds -WHERE event_id = $1 AND is_active = true AND source = 'b365api'; - --- name: GetALLPrematchOdds :many -SELECT - id, event_id, fi, market_type, @@ -78,10 +49,30 @@ SELECT is_active FROM odds WHERE is_active = true AND source = 'b365api'; + +-- name: GetALLPrematchOdds :many +SELECT + event_id, + fi, + market_type, + market_name, + market_category, + market_id, + name, + handicap, + odds_value, + section, + category, + raw_odds, + fetched_at, + source, + is_active +FROM odds +WHERE is_active = true AND source = 'b365api'; + -- name: GetRawOddsByID :one SELECT id, - event_id, raw_odds, fetched_at FROM odds @@ -89,4 +80,30 @@ WHERE raw_odds @> $1::jsonb AND is_active = true AND source = 'b365api' -LIMIT 1; \ No newline at end of file +LIMIT 1; + +-- name: GetPrematchOddsByUpcomingID :many +SELECT + o.event_id, + o.fi, + o.market_type, + o.market_name, + o.market_category, + o.market_id, + o.name, + o.handicap, + o.odds_value, + o.section, + o.category, + o.raw_odds, + o.fetched_at, + o.source, + o.is_active +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 $2 OFFSET $3; \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index b815188..1c7970b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -332,6 +332,65 @@ const docTemplate = `{ } } }, + "/prematch/odds/upcoming/{upcoming_id}": { + "get": { + "description": "Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve prematch odds by upcoming ID (FI)", + "parameters": [ + { + "type": "string", + "description": "Upcoming Event ID (FI)", + "name": "upcoming_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Number of results to return (default: 10)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of results to skip (default: 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Odd" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/prematch/odds/{event_id}": { "get": { "description": "Retrieve prematch odds for a specific event by event ID", @@ -669,9 +728,6 @@ const docTemplate = `{ "handicap": { "type": "string" }, - "id": { - "type": "integer" - }, "is_active": { "type": "boolean" }, @@ -685,11 +741,9 @@ const docTemplate = `{ "type": "string" }, "market_type": { - "description": "RawEventID string ` + "`" + `json:\"raw_event_id\"` + "`" + `", "type": "string" }, "name": { - "description": "Header string ` + "`" + `json:\"header\"` + "`" + `", "type": "string" }, "odds_value": { diff --git a/docs/swagger.json b/docs/swagger.json index 061eb47..118ca95 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -324,6 +324,65 @@ } } }, + "/prematch/odds/upcoming/{upcoming_id}": { + "get": { + "description": "Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve prematch odds by upcoming ID (FI)", + "parameters": [ + { + "type": "string", + "description": "Upcoming Event ID (FI)", + "name": "upcoming_id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Number of results to return (default: 10)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of results to skip (default: 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Odd" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/prematch/odds/{event_id}": { "get": { "description": "Retrieve prematch odds for a specific event by event ID", @@ -661,9 +720,6 @@ "handicap": { "type": "string" }, - "id": { - "type": "integer" - }, "is_active": { "type": "boolean" }, @@ -677,11 +733,9 @@ "type": "string" }, "market_type": { - "description": "RawEventID string `json:\"raw_event_id\"`", "type": "string" }, "name": { - "description": "Header string `json:\"header\"`", "type": "string" }, "odds_value": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 55be753..b458113 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -11,8 +11,6 @@ definitions: type: string handicap: type: string - id: - type: integer is_active: type: boolean market_category: @@ -22,10 +20,8 @@ definitions: market_name: type: string market_type: - description: RawEventID string `json:"raw_event_id"` type: string name: - description: Header string `json:"header"` type: string odds_value: type: number @@ -498,6 +494,46 @@ paths: summary: Retrieve raw odds by ID tags: - prematch + /prematch/odds/upcoming/{upcoming_id}: + get: + consumes: + - application/json + description: Retrieve prematch odds by upcoming event ID (FI from Bet365) with + optional pagination + parameters: + - description: Upcoming Event ID (FI) + in: path + name: upcoming_id + required: true + type: string + - description: 'Number of results to return (default: 10)' + in: query + name: limit + type: integer + - description: 'Number of results to skip (default: 0)' + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.Odd' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve prematch odds by upcoming ID (FI) + tags: + - prematch /user/checkPhoneEmailExist: post: consumes: diff --git a/gen/db/models.go b/gen/db/models.go index 071fe68..66ca59c 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -36,12 +36,10 @@ type Odd struct { ID int32 EventID pgtype.Text Fi pgtype.Text - RawEventID pgtype.Text MarketType string MarketName pgtype.Text MarketCategory pgtype.Text MarketID pgtype.Text - Header pgtype.Text Name pgtype.Text Handicap pgtype.Text OddsValue pgtype.Float8 diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 2cc76c6..66c4078 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -13,7 +13,6 @@ import ( const GetALLPrematchOdds = `-- name: GetALLPrematchOdds :many SELECT - id, event_id, fi, market_type, @@ -34,7 +33,6 @@ WHERE is_active = true AND source = 'b365api' ` type GetALLPrematchOddsRow struct { - ID int32 EventID pgtype.Text Fi pgtype.Text MarketType string @@ -62,7 +60,6 @@ func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]GetALLPrematchOddsR for rows.Next() { var i GetALLPrematchOddsRow if err := rows.Scan( - &i.ID, &i.EventID, &i.Fi, &i.MarketType, @@ -91,15 +88,12 @@ func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]GetALLPrematchOddsR const GetPrematchOdds = `-- name: GetPrematchOdds :many SELECT - id, event_id, fi, - raw_event_id, market_type, market_name, market_category, market_id, - header, name, handicap, odds_value, @@ -110,28 +104,130 @@ SELECT source, is_active FROM odds -WHERE event_id = $1 AND is_active = true AND source = 'b365api' +WHERE is_active = true AND source = 'b365api' ` -func (q *Queries) GetPrematchOdds(ctx context.Context, eventID pgtype.Text) ([]Odd, error) { - rows, err := q.db.Query(ctx, GetPrematchOdds, eventID) +type GetPrematchOddsRow struct { + EventID pgtype.Text + Fi pgtype.Text + MarketType string + MarketName pgtype.Text + MarketCategory pgtype.Text + MarketID pgtype.Text + Name pgtype.Text + Handicap pgtype.Text + OddsValue pgtype.Float8 + Section string + Category pgtype.Text + RawOdds []byte + FetchedAt pgtype.Timestamp + Source pgtype.Text + IsActive pgtype.Bool +} + +func (q *Queries) GetPrematchOdds(ctx context.Context) ([]GetPrematchOddsRow, error) { + rows, err := q.db.Query(ctx, GetPrematchOdds) if err != nil { return nil, err } defer rows.Close() - var items []Odd + var items []GetPrematchOddsRow for rows.Next() { - var i Odd + var i GetPrematchOddsRow + if err := rows.Scan( + &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 GetPrematchOddsByUpcomingID = `-- name: GetPrematchOddsByUpcomingID :many +SELECT + o.event_id, + o.fi, + o.market_type, + o.market_name, + o.market_category, + o.market_id, + o.name, + o.handicap, + o.odds_value, + o.section, + o.category, + o.raw_odds, + o.fetched_at, + o.source, + o.is_active +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 $2 OFFSET $3 +` + +type GetPrematchOddsByUpcomingIDParams struct { + ID string + Limit int32 + Offset int32 +} + +type GetPrematchOddsByUpcomingIDRow struct { + EventID pgtype.Text + Fi pgtype.Text + MarketType string + MarketName pgtype.Text + MarketCategory pgtype.Text + MarketID pgtype.Text + Name pgtype.Text + Handicap pgtype.Text + OddsValue pgtype.Float8 + Section string + Category pgtype.Text + RawOdds []byte + FetchedAt pgtype.Timestamp + Source pgtype.Text + IsActive pgtype.Bool +} + +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 { + return nil, err + } + defer rows.Close() + var items []GetPrematchOddsByUpcomingIDRow + for rows.Next() { + var i GetPrematchOddsByUpcomingIDRow if err := rows.Scan( - &i.ID, &i.EventID, &i.Fi, - &i.RawEventID, &i.MarketType, &i.MarketName, &i.MarketCategory, &i.MarketID, - &i.Header, &i.Name, &i.Handicap, &i.OddsValue, @@ -155,7 +251,6 @@ func (q *Queries) GetPrematchOdds(ctx context.Context, eventID pgtype.Text) ([]O const GetRawOddsByID = `-- name: GetRawOddsByID :one SELECT id, - event_id, raw_odds, fetched_at FROM odds @@ -168,7 +263,6 @@ LIMIT 1 type GetRawOddsByIDRow struct { ID int32 - EventID pgtype.Text RawOdds []byte FetchedAt pgtype.Timestamp } @@ -176,12 +270,7 @@ type GetRawOddsByIDRow struct { func (q *Queries) GetRawOddsByID(ctx context.Context, dollar_1 []byte) (GetRawOddsByIDRow, error) { row := q.db.QueryRow(ctx, GetRawOddsByID, dollar_1) var i GetRawOddsByIDRow - err := row.Scan( - &i.ID, - &i.EventID, - &i.RawOdds, - &i.FetchedAt, - ) + err := row.Scan(&i.ID, &i.RawOdds, &i.FetchedAt) return i, err } @@ -189,12 +278,10 @@ const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec INSERT INTO odds ( event_id, fi, - raw_event_id, market_type, market_name, market_category, market_id, - header, name, handicap, odds_value, @@ -206,55 +293,55 @@ INSERT INTO odds ( fetched_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, - $8, $9, $10, $11, $12, $13, $14, - true, 'b365api', now() + $8, $9, $10, $11, $12, $13, $14, $15 ) -ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET +ON CONFLICT (market_id, name, handicap) DO UPDATE SET odds_value = EXCLUDED.odds_value, raw_odds = EXCLUDED.raw_odds, market_type = EXCLUDED.market_type, market_name = EXCLUDED.market_name, market_category = EXCLUDED.market_category, - fetched_at = now(), - is_active = true, - source = 'b365api', - fi = EXCLUDED.fi, - raw_event_id = EXCLUDED.raw_event_id + fetched_at = EXCLUDED.fetched_at, + is_active = EXCLUDED.is_active, + source = EXCLUDED.source, + fi = EXCLUDED.fi ` type InsertNonLiveOddParams struct { EventID pgtype.Text Fi pgtype.Text - RawEventID pgtype.Text MarketType string MarketName pgtype.Text MarketCategory pgtype.Text MarketID pgtype.Text - Header pgtype.Text Name pgtype.Text Handicap pgtype.Text OddsValue pgtype.Float8 Section string Category pgtype.Text RawOdds []byte + IsActive pgtype.Bool + Source pgtype.Text + FetchedAt pgtype.Timestamp } func (q *Queries) InsertNonLiveOdd(ctx context.Context, arg InsertNonLiveOddParams) error { _, err := q.db.Exec(ctx, InsertNonLiveOdd, arg.EventID, arg.Fi, - arg.RawEventID, arg.MarketType, arg.MarketName, arg.MarketCategory, arg.MarketID, - arg.Header, arg.Name, arg.Handicap, arg.OddsValue, arg.Section, arg.Category, arg.RawOdds, + arg.IsActive, + arg.Source, + arg.FetchedAt, ) return err } diff --git a/internal/domain/odds.go b/internal/domain/odds.go index 1d47b85..df2de7e 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -16,23 +16,18 @@ type Market struct { MarketID string UpdatedAt time.Time Odds []json.RawMessage - - Header string Name string Handicap string OddsVal float64 } type Odd struct { - ID int64 `json:"id"` EventID string `json:"event_id"` Fi string `json:"fi"` - // RawEventID string `json:"raw_event_id"` MarketType string `json:"market_type"` MarketName string `json:"market_name"` MarketCategory string `json:"market_category"` MarketID string `json:"market_id"` - // Header string `json:"header"` Name string `json:"name"` Handicap string `json:"handicap"` OddsValue float64 `json:"odds_value"` diff --git a/internal/repository/odds.go b/internal/repository/odds.go index ae510e7..a8573a3 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -23,7 +23,6 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { continue } - header := getString(item["header"]) name := getString(item["name"]) handicap := getString(item["handicap"]) oddsVal := getFloat(item["odds"]) @@ -33,23 +32,24 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { params := dbgen.InsertNonLiveOddParams{ EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""}, - RawEventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, MarketType: m.MarketType, MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""}, MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""}, MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""}, - Header: pgtype.Text{String: header, Valid: header != ""}, Name: pgtype.Text{String: name, Valid: name != ""}, Handicap: pgtype.Text{String: handicap, Valid: handicap != ""}, OddsValue: pgtype.Float8{Float64: oddsVal, Valid: oddsVal != 0}, Section: m.MarketCategory, - Category: pgtype.Text{Valid: false}, + Category: pgtype.Text{Valid: false}, RawOdds: rawOddsBytes, + IsActive: pgtype.Bool{Bool: true, Valid: true}, + Source: pgtype.Text{String: "b365api", Valid: true}, + FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, } err := s.queries.InsertNonLiveOdd(ctx, params) if err != nil { - _ = writeFailedMarketLog(m, err) + _ = writeFailedMarketLog(m, err) continue } } @@ -103,9 +103,7 @@ func getFloat(v interface{}) float64 { } func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { - eventIDParam := pgtype.Text{String: eventID, Valid: eventID != ""} - - odds, err := s.queries.GetPrematchOdds(ctx, eventIDParam) + odds, err := s.queries.GetPrematchOdds(ctx) if err != nil { return nil, err } @@ -113,15 +111,12 @@ func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.O domainOdds := make([]domain.Odd, len(odds)) for i, odd := range odds { domainOdds[i] = domain.Odd{ - ID: int64(odd.ID), EventID: odd.EventID.String, Fi: odd.Fi.String, - // RawEventID: odd.RawEventID.String, MarketType: odd.MarketType, MarketName: odd.MarketName.String, MarketCategory: odd.MarketCategory.String, MarketID: odd.MarketID.String, - // Header: odd.Header.String, Name: odd.Name.String, Handicap: odd.Handicap.String, OddsValue: odd.OddsValue.Float64, @@ -152,7 +147,7 @@ func (s *Store) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) { domainOdds := make([]domain.Odd, len(rows)) for i, row := range rows { domainOdds[i] = domain.Odd{ - ID: int64(row.ID), + // ID: int64(row.ID), EventID: row.EventID.String, Fi: row.Fi.String, MarketType: row.MarketType, @@ -195,7 +190,6 @@ func (s *Store) GetRawOddsByID(ctx context.Context, rawOddsID string) (domain.Ra return domain.RawOddsByID{ ID: int64(odd.ID), - EventID: odd.EventID.String, RawOdds: func() []domain.RawMessage { converted := make([]domain.RawMessage, len(rawOdds)) for i, r := range rawOdds { @@ -205,4 +199,48 @@ func (s *Store) GetRawOddsByID(ctx context.Context, rawOddsID string) (domain.Ra }(), FetchedAt: odd.FetchedAt.Time, }, nil +} + +func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) { + // Prepare query parameters + params := dbgen.GetPrematchOddsByUpcomingIDParams{ + ID: upcomingID, + Limit: limit, + Offset: offset, + } + + // Execute the query + odds, err := s.queries.GetPrematchOddsByUpcomingID(ctx, params) + 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 + } + + domainOdds[i] = domain.Odd{ + EventID: odd.EventID.String, + Fi: odd.Fi.String, + MarketType: odd.MarketType, + MarketName: odd.MarketName.String, + MarketCategory: odd.MarketCategory.String, + MarketID: odd.MarketID.String, + Name: odd.Name.String, + Handicap: odd.Handicap.String, + OddsValue: odd.OddsValue.Float64, + Section: odd.Section, + Category: odd.Category.String, + RawOdds: rawOdds, + FetchedAt: odd.FetchedAt.Time, + Source: odd.Source.String, + IsActive: odd.IsActive.Bool, + } + } + + return domainOdds, nil } \ No newline at end of file diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 350c8cf..41a2d1d 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -97,7 +97,7 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { } func (s *service) FetchUpcomingEvents(ctx context.Context) error { - sportIDs := []int{1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, 95, 110, 107, 151, 162, 148} + sportIDs := []int{1} for _, sportID := range sportIDs { url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token) resp, err := http.Get(url) diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 467117b..9b31a94 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -24,76 +24,51 @@ func New(token string, store *repository.Store) *ServiceImpl { func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { - sportIDs := []int{ - 1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, - 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, - 95, 110, 107, 151, 162, 148, - } - for _, sportID := range sportIDs { - upcomingURL := "https://api.b365api.com/v1/bet365/upcoming?sport_id=" + strconv.Itoa(sportID) + "&token=" + s.token - log.Printf("Fetching upcoming odds for sport ID: %d from URL: %s", sportID, upcomingURL) - resp, err := http.Get(upcomingURL) + eventIDs, err := s.store.GetAllUpcomingEvents(ctx) + if err != nil { + log.Printf("❌ Failed to fetch upcoming event IDs: %v", err) + return err + } + + for _, event := range eventIDs { + eventID := event.ID + prematchURL := "https://api.b365api.com/v3/bet365/prematch?token=" + s.token + "&FI=" + eventID + log.Printf("📡 Fetching prematch odds for event ID: %s", eventID) + resp, err := http.Get(prematchURL) if err != nil { - log.Printf("Error fetching upcoming odds for sport ID: %d, error: %v", sportID, err) + log.Printf("❌ Failed to fetch prematch odds for event %s: %v", eventID, err) continue } defer resp.Body.Close() - + body, _ := io.ReadAll(resp.Body) - var upcomingData struct { + var oddsData struct { Success int `json:"success"` Results []struct { - ID string `json:"id"` + EventID string `json:"event_id"` + FI string `json:"FI"` + Main OddsSection `json:"main"` } `json:"results"` } - if err := json.Unmarshal(body, &upcomingData); err != nil || upcomingData.Success != 1 { - log.Printf("Failed to parse upcoming odds for sport ID: %d, error: %v", sportID, err) + if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { + log.Printf("❌ Invalid prematch data for event %s", eventID) continue } - - log.Printf("Successfully fetched upcoming odds for sport ID: %d", sportID) - - for _, ev := range upcomingData.Results { - eventID := ev.ID - prematchURL := "https://api.b365api.com/v3/bet365/prematch?token=" + s.token + "&FI=" + eventID - log.Printf("Fetching prematch odds for event ID: %s from URL: %s", eventID, prematchURL) - oddsResp, err := http.Get(prematchURL) - if err != nil { - log.Printf("Error fetching prematch odds for event ID: %s, error: %v", eventID, err) - continue - } - defer oddsResp.Body.Close() - - oddsBody, _ := io.ReadAll(oddsResp.Body) - var oddsData struct { - Success int `json:"success"` - Results []struct { - EventID string `json:"event_id"` - FI string `json:"FI"` - Main OddsSection `json:"main"` - } `json:"results"` - } - if err := json.Unmarshal(oddsBody, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { - log.Printf("Failed to parse prematch odds for event ID: %s, error: %v", eventID, err) - continue - } - - result := oddsData.Results[0] - finalID := result.EventID - if finalID == "" { - finalID = result.FI - } - if finalID == "" { - log.Printf("Skipping event with missing final ID for event ID: %s", eventID) - continue - } - - log.Printf("Storing odds for event ID: %s, final ID: %s", eventID, finalID) - s.storeSection(ctx, finalID, result.FI, "main", result.Main) + + result := oddsData.Results[0] + finalID := result.EventID + if finalID == "" { + finalID = result.FI } + if finalID == "" { + log.Printf("⚠️ Skipping event %s with no valid ID", eventID) + 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) { @@ -153,4 +128,7 @@ func (s *ServiceImpl) GetRawOddsByID(ctx context.Context, rawOddsID string) ([]d return nil, err } return []domain.RawOddsByID{rawOdds}, nil -} \ No newline at end of file +} +func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) { + return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) +} diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index afbccc9..ddf9677 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -1,7 +1,7 @@ package httpserver import ( - "context" + // "context" "log" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" @@ -17,33 +17,35 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S task func() }{ - { - spec: "0 0 * * * *", // Every hour at minute 0 and second 0 - task: func() { - if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - log.Printf("FetchUpcomingEvents error: %v", err) - } - }, - }, + // { + // spec: "*/30 * * * * *", // Every 30 seconds + // task: func() { + // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + // log.Printf("FetchUpcomingEvents error: %v", err) + // } + // }, + // }, + - { - spec: "*/5 * * * * *", // Every 5 seconds - task: func() { - if err := eventService.FetchLiveEvents(context.Background()); err != nil { - log.Printf("FetchLiveEvents error: %v", err) - } - }, - }, + // { + // spec: "*/5 * * * * *", // Every 5 seconds + // task: func() { + // if err := eventService.FetchLiveEvents(context.Background()); err != nil { + // log.Printf("FetchLiveEvents error: %v", err) + // } + // }, + // }, - { - spec: "0 0 * * * *", // Every hour at minute 0 and second 0 - task: func() { - if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - log.Printf("FetchNonLiveOdds error: %v", err) - } - }, - }, + // { + // spec: "*/30 * * * * *", // Every 30 seconds + // task: func() { + // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + // log.Printf("FetchNonLiveOdds error: %v", err) + // } + // }, + // }, + } diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index 4fea8a7..17e4de4 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -1,11 +1,13 @@ package handlers import ( - "github.com/gofiber/fiber/v2" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" - "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" - "log/slog" + "log/slog" + "strconv" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + "github.com/gofiber/fiber/v2" ) // GetPrematchOdds godoc @@ -123,3 +125,41 @@ func GetUpcomingEventByID(logger *slog.Logger, eventSvc event.Service) fiber.Han return response.WriteJSON(c, fiber.StatusOK, "Upcoming event retrieved successfully", event, nil) } } +// @Summary Retrieve prematch odds by upcoming ID (FI) +// @Description Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination +// @Tags prematch +// @Accept json +// @Produce json +// @Param upcoming_id path string true "Upcoming Event ID (FI)" +// @Param limit query int false "Number of results to return (default: 10)" +// @Param offset query int false "Number of results to skip (default: 0)" +// @Success 200 {array} domain.Odd +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /prematch/odds/upcoming/{upcoming_id} [get] +func GetPrematchOddsByUpcomingID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { + return func(c *fiber.Ctx) error { + upcomingID := c.Params("upcoming_id") + if upcomingID == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing upcoming_id", nil, nil) + } + + limit, err := strconv.Atoi(c.Query("limit", "10")) // Default limit is 10 + if err != nil || limit <= 0 { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid limit value", nil, nil) + } + + offset, err := strconv.Atoi(c.Query("offset", "0")) // Default offset is 0 + if err != nil || offset < 0 { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid offset value", nil, nil) + } + + odds, err := prematchSvc.GetPrematchOddsByUpcomingID(c.Context(), upcomingID, int32(limit), int32(offset)) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve prematch odds", nil, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) + } +} + diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 5841fbb..f18aa50 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -33,6 +33,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/prematch/events/:id", handlers.GetUpcomingEventByID(a.logger, a.eventSvc)) a.fiber.Get("/prematch/events", handlers.GetAllUpcomingEvents(a.logger, a.eventSvc)) + a.fiber.Get("/prematch/odds/upcoming/:upcoming_id", handlers.GetPrematchOddsByUpcomingID(a.logger, a.prematchSvc)) // Swagger a.fiber.Get("/swagger/*", fiberSwagger.WrapHandler) } From 788119f71803777130ec1a4815c18091d1ec1014 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 14 Apr 2025 00:09:04 +0300 Subject: [PATCH 19/30] small fix --- .gitignore | 1 + db/migrations/000001_fortune.up.sql | 16 +---- gen/db/models.go | 1 + gen/db/odds.sql.go | 107 +++++++++++++++------------- internal/web_server/app.go | 88 +++++++++++------------ 5 files changed, 107 insertions(+), 106 deletions(-) diff --git a/.gitignore b/.gitignore index c29bfa6..bdc8b3e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ coverage .env tmp build +*.log diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 9d3709e..610b920 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -176,7 +176,6 @@ CREATE TABLE IF NOT EXISTS branch_cashiers ( branch_id BIGINT NOT NULL, UNIQUE(user_id, branch_id) ); - CREATE TABLE events ( id TEXT PRIMARY KEY, sport_id TEXT, @@ -217,24 +216,14 @@ CREATE TABLE odds ( fetched_at TIMESTAMP DEFAULT now(), source TEXT DEFAULT 'b365api', is_active BOOLEAN DEFAULT true, + UNIQUE (market_id, name, handicap), UNIQUE (event_id, market_id, name, handicap) ); - - - ALTER TABLE refresh_tokens ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id); ALTER TABLE bets ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id), ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id); -ALTER TABLE bet_outcomes -ADD CONSTRAINT fk_bet_outcomes_bets FOREIGN KEY (bet_id) REFERENCES bets(id), - ADD CONSTRAINT fk_bet_outcomes_events FOREIGN KEY (event_id) REFERENCES supported_operations(id), - ADD CONSTRAINT fk_bet_outcomes_odds FOREIGN KEY (odd_id) REFERENCES supported_operations(id); -ALTER TABLE ticket_outcomes -ADD CONSTRAINT fk_ticket_outcomes_tickets FOREIGN KEY (ticket_id) REFERENCES tickets(id), - ADD CONSTRAINT fk_ticket_outcomes_events FOREIGN KEY (event_id) REFERENCES supported_operations(id), - ADD CONSTRAINT fk_ticket_outcomes_odds FOREIGN KEY (odd_id) REFERENCES supported_operations(id); ALTER TABLE wallets ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id); ALTER TABLE customer_wallets @@ -341,5 +330,4 @@ VALUES ( CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ); - ---------------------------------------------------Bet365 Data Fetching + Event Managment------------------------------------------------ +--------------------------------------------------Bet365 Data Fetching + Event Managment------------------------------------------------ \ No newline at end of file diff --git a/gen/db/models.go b/gen/db/models.go index 72f7dfc..9a9993c 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -148,6 +148,7 @@ type Odd struct { MarketCategory pgtype.Text `json:"market_category"` MarketID pgtype.Text `json:"market_id"` Name pgtype.Text `json:"name"` + Header pgtype.Text `json:"header"` Handicap pgtype.Text `json:"handicap"` OddsValue pgtype.Float8 `json:"odds_value"` Section string `json:"section"` diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index f7e88ab..5f966bf 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -12,8 +12,7 @@ import ( ) const GetALLPrematchOdds = `-- name: GetALLPrematchOdds :many -SELECT - event_id, +SELECT event_id, fi, market_type, market_name, @@ -29,7 +28,8 @@ SELECT source, is_active FROM odds -WHERE is_active = true AND source = 'b365api' +WHERE is_active = true + AND source = 'b365api' ` type GetALLPrematchOddsRow struct { @@ -87,8 +87,7 @@ func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]GetALLPrematchOddsR } const GetPrematchOdds = `-- name: GetPrematchOdds :many -SELECT - event_id, +SELECT event_id, fi, market_type, market_name, @@ -104,7 +103,8 @@ SELECT source, is_active FROM odds -WHERE is_active = true AND source = 'b365api' +WHERE is_active = true + AND source = 'b365api' ` type GetPrematchOddsRow struct { @@ -162,8 +162,7 @@ func (q *Queries) GetPrematchOdds(ctx context.Context) ([]GetPrematchOddsRow, er } const GetPrematchOddsByUpcomingID = `-- name: GetPrematchOddsByUpcomingID :many -SELECT - o.event_id, +SELECT o.event_id, o.fi, o.market_type, o.market_name, @@ -179,12 +178,12 @@ SELECT o.source, o.is_active FROM odds o -JOIN events e ON o.fi = e.id + 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' + AND e.is_live = false + AND e.status = 'upcoming' + AND o.is_active = true + AND o.source = 'b365api' LIMIT $2 OFFSET $3 ` @@ -249,15 +248,13 @@ func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, arg GetPremat } const GetRawOddsByID = `-- name: GetRawOddsByID :one -SELECT - id, - raw_odds, +SELECT id, + raw_odds, fetched_at FROM odds -WHERE - raw_odds @> $1::jsonb AND - is_active = true AND - source = 'b365api' +WHERE raw_odds @> $1::jsonb + AND is_active = true + AND source = 'b365api' LIMIT 1 ` @@ -276,35 +273,49 @@ func (q *Queries) GetRawOddsByID(ctx context.Context, dollar_1 []byte) (GetRawOd const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec INSERT INTO odds ( - event_id, - fi, - market_type, - market_name, - market_category, - market_id, - name, - handicap, - odds_value, - section, - category, - raw_odds, - is_active, - source, - fetched_at -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, - $8, $9, $10, $11, $12, $13, $14, $15 -) -ON CONFLICT (market_id, name, handicap) DO UPDATE SET - odds_value = EXCLUDED.odds_value, - raw_odds = EXCLUDED.raw_odds, - market_type = EXCLUDED.market_type, - market_name = EXCLUDED.market_name, + event_id, + fi, + market_type, + market_name, + market_category, + market_id, + name, + handicap, + odds_value, + section, + category, + raw_odds, + is_active, + source, + fetched_at + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15 + ) ON CONFLICT (market_id, name, handicap) DO +UPDATE +SET odds_value = EXCLUDED.odds_value, + raw_odds = EXCLUDED.raw_odds, + market_type = EXCLUDED.market_type, + market_name = EXCLUDED.market_name, market_category = EXCLUDED.market_category, - fetched_at = EXCLUDED.fetched_at, - is_active = EXCLUDED.is_active, - source = EXCLUDED.source, - fi = EXCLUDED.fi + fetched_at = EXCLUDED.fetched_at, + is_active = EXCLUDED.is_active, + source = EXCLUDED.source, + fi = EXCLUDED.fi ` type InsertNonLiveOddParams struct { diff --git a/internal/web_server/app.go b/internal/web_server/app.go index a6ed559..c81a930 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -1,25 +1,24 @@ package httpserver import ( - "fmt" - "log/slog" + "fmt" + "log/slog" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" - jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" - customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" - + jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" + customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" - "github.com/bytedance/sonic" - "github.com/gofiber/fiber/v2" + "github.com/bytedance/sonic" + "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" ) @@ -38,46 +37,47 @@ type App struct { validator *customvalidator.CustomValidator JwtConfig jwtutil.JwtConfig Logger *slog.Logger - prematchSvc *odds.ServiceImpl - eventSvc event.Service + prematchSvc *odds.ServiceImpl + eventSvc event.Service } func NewApp( - port int, validator *customvalidator.CustomValidator, - authSvc *authentication.Service, - logger *slog.Logger, - JwtConfig jwtutil.JwtConfig, - userSvc *user.Service, + port int, validator *customvalidator.CustomValidator, + authSvc *authentication.Service, + logger *slog.Logger, + JwtConfig jwtutil.JwtConfig, + userSvc *user.Service, ticketSvc *ticket.Service, betSvc *bet.Service, walletSvc *wallet.Service, transactionSvc *transaction.Service, branchSvc *branch.Service, notidicationStore notificationservice.NotificationStore, - prematchSvc *odds.ServiceImpl, - eventSvc event.Service, + prematchSvc *odds.ServiceImpl, + eventSvc event.Service, ) *App { - app := fiber.New(fiber.Config{ - CaseSensitive: true, - DisableHeaderNormalizing: true, - JSONEncoder: sonic.Marshal, - JSONDecoder: sonic.Unmarshal, - }) + app := fiber.New(fiber.Config{ + CaseSensitive: true, + DisableHeaderNormalizing: true, + JSONEncoder: sonic.Marshal, + JSONDecoder: sonic.Unmarshal, + }) app.Use(cors.New(cors.Config{ - AllowOrigins: "http://localhost:5173", // Specify your frontend's origin - AllowMethods: "GET,POST,PUT,DELETE", // Specify the allowed HTTP methods - AllowHeaders: "Content-Type,Authorization", // Specify the allowed headers + AllowOrigins: "http://localhost:8000", // Specify your frontend's origin + AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", // Specify the allowed HTTP methods + AllowHeaders: "Content-Type,Authorization,platform", // Specify the allowed headers + AllowCredentials: true, })) - s := &App{ - fiber: app, - port: port, - authSvc: authSvc, - validator: validator, - logger: logger, - JwtConfig: JwtConfig, - userSvc: userSvc, + s := &App{ + fiber: app, + port: port, + authSvc: authSvc, + validator: validator, + logger: logger, + JwtConfig: JwtConfig, + userSvc: userSvc, ticketSvc: ticketSvc, betSvc: betSvc, walletSvc: walletSvc, @@ -85,15 +85,15 @@ func NewApp( branchSvc: branchSvc, NotidicationStore: notidicationStore, Logger: logger, - prematchSvc: prematchSvc, - eventSvc: eventSvc, - } + prematchSvc: prematchSvc, + eventSvc: eventSvc, + } - s.initAppRoutes() + s.initAppRoutes() - return s + return s } func (a *App) Run() error { - return a.fiber.Listen(fmt.Sprintf(":%d", a.port)) -} \ No newline at end of file + return a.fiber.Listen(fmt.Sprintf(":%d", a.port)) +} From b4a21a5ddb834ddd536b9cdb7cfbba2046c0feb3 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 14 Apr 2025 07:59:23 +0300 Subject: [PATCH 20/30] - --- db/migrations/000001_fortune.up.sql | 27 +- db/query/bet.sql | 19 +- db/query/ticket.sql | 15 +- docs/docs.go | 254 +++++++++++++++--- docs/swagger.json | 254 +++++++++++++++--- docs/swagger.yaml | 181 +++++++++++-- gen/db/bet.sql.go | 58 ++-- gen/db/copyfrom.go | 18 +- gen/db/models.go | 35 ++- gen/db/odds.sql.go | 107 ++++---- gen/db/ticket.sql.go | 26 +- internal/domain/bet.go | 30 ++- internal/domain/ticket.go | 30 ++- internal/repository/bet.go | 54 +++- internal/repository/ticket.go | 51 ++-- internal/services/bet/port.go | 1 + internal/services/bet/service.go | 3 + internal/web_server/app.go | 8 +- internal/web_server/cron.go | 93 +++---- internal/web_server/handlers/bet_handler.go | 89 ++++-- .../web_server/handlers/ticket_handler.go | 34 ++- .../handlers/transaction_handler.go | 80 ++++-- internal/web_server/routes.go | 5 +- 23 files changed, 1100 insertions(+), 372 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 610b920..ef85495 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -56,11 +56,12 @@ CREATE TABLE IF NOT EXISTS bets ( CHECK ( user_id IS NOT NULL OR branch_id IS NOT NULL - ) + ), + UNIQUE(cashier_id) ); CREATE TABLE IF NOT EXISTS tickets ( id BIGSERIAL PRIMARY KEY, - amount BIGINT NULL, + amount BIGINT NOT NULL, total_odds REAL NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP @@ -68,14 +69,28 @@ CREATE TABLE IF NOT EXISTS tickets ( CREATE TABLE IF NOT EXISTS bet_outcomes ( id BIGSERIAL PRIMARY KEY, bet_id BIGINT NOT NULL, - event_id bigint not null, - odd_id BIGINT NOT NULL + event_id BIGINT NOT null, + odd_id BIGINT NOT NULL, + home_team_name VARCHAR(255) NOT NULL, + away_team_name VARCHAR(255) NOT NULL, + market_id BIGINT NOT NULL, + market_name VARCHAR(255) NOT NULL, + odd REAL NOT NULL, + odd_name VARCHAR(255) NOT NULL, + expires TIMESTAMP NOT NULL ); CREATE TABLE IF NOT EXISTS ticket_outcomes ( id BIGSERIAL PRIMARY KEY, ticket_id BIGINT NOT NULL, - event_id bigint not null, - odd_id BIGINT NOT NULL + event_id BIGINT NOT null, + odd_id BIGINT NOT NULL, + home_team_name VARCHAR(255) NOT NULL, + away_team_name VARCHAR(255) NOT NULL, + market_id BIGINT NOT NULL, + market_name VARCHAR(255) NOT NULL, + odd REAL NOT NULL, + odd_name VARCHAR(255) NOT NULL, + expires TIMESTAMP NOT NULL ); CREATE VIEW bet_with_outcomes AS SELECT bets.*, diff --git a/db/query/bet.sql b/db/query/bet.sql index 01230b2..bc37717 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -13,8 +13,19 @@ INSERT INTO bets ( VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *; -- name: CreateBetOutcome :copyfrom -INSERT INTO bet_outcomes (bet_id, event_id, odd_id) -VALUES ($1, $2, $3); +INSERT INTO bet_outcomes ( + bet_id, + event_id, + odd_id, + home_team_name, + away_team_name, + market_id, + market_name, + odd, + odd_name, + expires + ) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10); -- name: GetAllBets :many SELECT * FROM bet_with_outcomes; @@ -22,8 +33,8 @@ FROM bet_with_outcomes; SELECT * FROM bet_with_outcomes WHERE id = $1; --- name: GetBetByCashoutID :many -SELECT +-- name: GetBetByCashoutID :one +SELECT * FROM bet_with_outcomes WHERE cashout_id = $1; -- name: GetBetByBranchID :many diff --git a/db/query/ticket.sql b/db/query/ticket.sql index debcb48..86d82f5 100644 --- a/db/query/ticket.sql +++ b/db/query/ticket.sql @@ -3,8 +3,19 @@ INSERT INTO tickets (amount, total_odds) VALUES ($1, $2) RETURNING *; -- name: CreateTicketOutcome :copyfrom -INSERT INTO ticket_outcomes (ticket_id, event_id, odd_id) -VALUES ($1, $2, $3); +INSERT INTO ticket_outcomes ( + ticket_id, + event_id, + odd_id, + home_team_name, + away_team_name, + market_id, + market_name, + odd, + odd_name, + expires + ) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10); -- name: GetAllTickets :many SELECT * FROM ticket_with_outcomes; diff --git a/docs/docs.go b/docs/docs.go index 67e9ac0..4e4fd35 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -262,6 +262,50 @@ const docTemplate = `{ } } }, + "/bet/cashout/{id}": { + "get": { + "description": "Gets a single bet by cashout id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by cashout id", + "parameters": [ + { + "type": "string", + "description": "cashout ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/bet/{id}": { "get": { "description": "Gets a single bet by id", @@ -2577,17 +2621,49 @@ const docTemplate = `{ "domain.BetOutcome": { "type": "object", "properties": { - "betID": { - "type": "integer" + "away_team_name": { + "type": "string", + "example": "Liverpool" }, - "eventID": { - "type": "integer" + "bet_id": { + "type": "integer", + "example": 1 + }, + "event_id": { + "type": "integer", + "example": 1 + }, + "expires": { + "type": "string", + "example": "2025-04-08T12:00:00Z" + }, + "home_team_name": { + "type": "string", + "example": "Manchester" }, "id": { - "type": "integer" + "type": "integer", + "example": 1 }, - "oddID": { - "type": "integer" + "market_id": { + "type": "integer", + "example": 1 + }, + "market_name": { + "type": "string", + "example": "Fulltime Result" + }, + "odd": { + "type": "number", + "example": 1.5 + }, + "odd_id": { + "type": "integer", + "example": 1 + }, + "odd_name": { + "type": "string", + "example": "1" } } }, @@ -2710,18 +2786,46 @@ const docTemplate = `{ "domain.TicketOutcome": { "type": "object", "properties": { + "away_team_name": { + "type": "string", + "example": "Liverpool" + }, "event_id": { "type": "integer", "example": 1 }, + "expires": { + "type": "string", + "example": "2025-04-08T12:00:00Z" + }, + "home_team_name": { + "type": "string", + "example": "Manchester" + }, "id": { "type": "integer", "example": 1 }, + "market_id": { + "type": "integer", + "example": 1 + }, + "market_name": { + "type": "string", + "example": "Fulltime Result" + }, + "odd": { + "type": "number", + "example": 1.5 + }, "odd_id": { "type": "integer", "example": 1 }, + "odd_name": { + "type": "string", + "example": "1" + }, "ticket_id": { "type": "integer", "example": 1 @@ -2785,19 +2889,6 @@ const docTemplate = `{ } } }, - "handlers.BetOutcome": { - "type": "object", - "properties": { - "event_id": { - "type": "integer", - "example": 1 - }, - "odd_id": { - "type": "integer", - "example": 1 - } - } - }, "handlers.BetRes": { "type": "object", "properties": { @@ -2809,6 +2900,14 @@ const docTemplate = `{ "type": "integer", "example": 2 }, + "cashed_id": { + "type": "string", + "example": "21234" + }, + "cashed_out": { + "type": "boolean", + "example": false + }, "full_name": { "type": "string", "example": "John" @@ -2960,6 +3059,51 @@ const docTemplate = `{ } } }, + "handlers.CreateBetOutcomeReq": { + "type": "object", + "properties": { + "away_team_name": { + "type": "string", + "example": "Liverpool" + }, + "bet_id": { + "type": "integer", + "example": 1 + }, + "event_id": { + "type": "integer", + "example": 1 + }, + "expires": { + "type": "string", + "example": "2025-04-08T12:00:00Z" + }, + "home_team_name": { + "type": "string", + "example": "Manchester" + }, + "market_id": { + "type": "integer", + "example": 1 + }, + "market_name": { + "type": "string", + "example": "Fulltime Result" + }, + "odd": { + "type": "number", + "example": 1.5 + }, + "odd_id": { + "type": "integer", + "example": 1 + }, + "odd_name": { + "type": "string", + "example": "1" + } + } + }, "handlers.CreateBetReq": { "type": "object", "properties": { @@ -2978,7 +3122,7 @@ const docTemplate = `{ "outcomes": { "type": "array", "items": { - "$ref": "#/definitions/handlers.BetOutcome" + "$ref": "#/definitions/handlers.CreateBetOutcomeReq" } }, "phone_number": { @@ -3110,6 +3254,51 @@ const docTemplate = `{ } } }, + "handlers.CreateTicketOutcomeReq": { + "type": "object", + "properties": { + "away_team_name": { + "type": "string", + "example": "Liverpool" + }, + "event_id": { + "type": "integer", + "example": 1 + }, + "expires": { + "type": "string", + "example": "2025-04-08T12:00:00Z" + }, + "home_team_name": { + "type": "string", + "example": "Manchester" + }, + "market_id": { + "type": "integer", + "example": 1 + }, + "market_name": { + "type": "string", + "example": "Fulltime Result" + }, + "odd": { + "type": "number", + "example": 1.5 + }, + "odd_id": { + "type": "integer", + "example": 1 + }, + "odd_name": { + "type": "string", + "example": "1" + }, + "ticket_id": { + "type": "integer", + "example": 1 + } + } + }, "handlers.CreateTicketReq": { "type": "object", "properties": { @@ -3120,7 +3309,7 @@ const docTemplate = `{ "outcomes": { "type": "array", "items": { - "$ref": "#/definitions/handlers.TicketOutcome" + "$ref": "#/definitions/handlers.CreateTicketOutcomeReq" } }, "total_odds": { @@ -3166,14 +3355,6 @@ const docTemplate = `{ "type": "integer", "example": 1 }, - "branch_id": { - "type": "integer", - "example": 1 - }, - "cashier_id": { - "type": "integer", - "example": 1 - }, "full_name": { "type": "string", "example": "John Smith" @@ -3356,19 +3537,6 @@ const docTemplate = `{ } } }, - "handlers.TicketOutcome": { - "type": "object", - "properties": { - "event_id": { - "type": "integer", - "example": 1 - }, - "odd_id": { - "type": "integer", - "example": 1 - } - } - }, "handlers.TicketRes": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index fc559e3..e20677f 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -254,6 +254,50 @@ } } }, + "/bet/cashout/{id}": { + "get": { + "description": "Gets a single bet by cashout id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bet" + ], + "summary": "Gets bet by cashout id", + "parameters": [ + { + "type": "string", + "description": "cashout ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BetRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/bet/{id}": { "get": { "description": "Gets a single bet by id", @@ -2569,17 +2613,49 @@ "domain.BetOutcome": { "type": "object", "properties": { - "betID": { - "type": "integer" + "away_team_name": { + "type": "string", + "example": "Liverpool" }, - "eventID": { - "type": "integer" + "bet_id": { + "type": "integer", + "example": 1 + }, + "event_id": { + "type": "integer", + "example": 1 + }, + "expires": { + "type": "string", + "example": "2025-04-08T12:00:00Z" + }, + "home_team_name": { + "type": "string", + "example": "Manchester" }, "id": { - "type": "integer" + "type": "integer", + "example": 1 }, - "oddID": { - "type": "integer" + "market_id": { + "type": "integer", + "example": 1 + }, + "market_name": { + "type": "string", + "example": "Fulltime Result" + }, + "odd": { + "type": "number", + "example": 1.5 + }, + "odd_id": { + "type": "integer", + "example": 1 + }, + "odd_name": { + "type": "string", + "example": "1" } } }, @@ -2702,18 +2778,46 @@ "domain.TicketOutcome": { "type": "object", "properties": { + "away_team_name": { + "type": "string", + "example": "Liverpool" + }, "event_id": { "type": "integer", "example": 1 }, + "expires": { + "type": "string", + "example": "2025-04-08T12:00:00Z" + }, + "home_team_name": { + "type": "string", + "example": "Manchester" + }, "id": { "type": "integer", "example": 1 }, + "market_id": { + "type": "integer", + "example": 1 + }, + "market_name": { + "type": "string", + "example": "Fulltime Result" + }, + "odd": { + "type": "number", + "example": 1.5 + }, "odd_id": { "type": "integer", "example": 1 }, + "odd_name": { + "type": "string", + "example": "1" + }, "ticket_id": { "type": "integer", "example": 1 @@ -2777,19 +2881,6 @@ } } }, - "handlers.BetOutcome": { - "type": "object", - "properties": { - "event_id": { - "type": "integer", - "example": 1 - }, - "odd_id": { - "type": "integer", - "example": 1 - } - } - }, "handlers.BetRes": { "type": "object", "properties": { @@ -2801,6 +2892,14 @@ "type": "integer", "example": 2 }, + "cashed_id": { + "type": "string", + "example": "21234" + }, + "cashed_out": { + "type": "boolean", + "example": false + }, "full_name": { "type": "string", "example": "John" @@ -2952,6 +3051,51 @@ } } }, + "handlers.CreateBetOutcomeReq": { + "type": "object", + "properties": { + "away_team_name": { + "type": "string", + "example": "Liverpool" + }, + "bet_id": { + "type": "integer", + "example": 1 + }, + "event_id": { + "type": "integer", + "example": 1 + }, + "expires": { + "type": "string", + "example": "2025-04-08T12:00:00Z" + }, + "home_team_name": { + "type": "string", + "example": "Manchester" + }, + "market_id": { + "type": "integer", + "example": 1 + }, + "market_name": { + "type": "string", + "example": "Fulltime Result" + }, + "odd": { + "type": "number", + "example": 1.5 + }, + "odd_id": { + "type": "integer", + "example": 1 + }, + "odd_name": { + "type": "string", + "example": "1" + } + } + }, "handlers.CreateBetReq": { "type": "object", "properties": { @@ -2970,7 +3114,7 @@ "outcomes": { "type": "array", "items": { - "$ref": "#/definitions/handlers.BetOutcome" + "$ref": "#/definitions/handlers.CreateBetOutcomeReq" } }, "phone_number": { @@ -3102,6 +3246,51 @@ } } }, + "handlers.CreateTicketOutcomeReq": { + "type": "object", + "properties": { + "away_team_name": { + "type": "string", + "example": "Liverpool" + }, + "event_id": { + "type": "integer", + "example": 1 + }, + "expires": { + "type": "string", + "example": "2025-04-08T12:00:00Z" + }, + "home_team_name": { + "type": "string", + "example": "Manchester" + }, + "market_id": { + "type": "integer", + "example": 1 + }, + "market_name": { + "type": "string", + "example": "Fulltime Result" + }, + "odd": { + "type": "number", + "example": 1.5 + }, + "odd_id": { + "type": "integer", + "example": 1 + }, + "odd_name": { + "type": "string", + "example": "1" + }, + "ticket_id": { + "type": "integer", + "example": 1 + } + } + }, "handlers.CreateTicketReq": { "type": "object", "properties": { @@ -3112,7 +3301,7 @@ "outcomes": { "type": "array", "items": { - "$ref": "#/definitions/handlers.TicketOutcome" + "$ref": "#/definitions/handlers.CreateTicketOutcomeReq" } }, "total_odds": { @@ -3158,14 +3347,6 @@ "type": "integer", "example": 1 }, - "branch_id": { - "type": "integer", - "example": 1 - }, - "cashier_id": { - "type": "integer", - "example": 1 - }, "full_name": { "type": "string", "example": "John Smith" @@ -3348,19 +3529,6 @@ } } }, - "handlers.TicketOutcome": { - "type": "object", - "properties": { - "event_id": { - "type": "integer", - "example": 1 - }, - "odd_id": { - "type": "integer", - "example": 1 - } - } - }, "handlers.TicketRes": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ad5e9cd..306272a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,14 +1,39 @@ definitions: domain.BetOutcome: properties: - betID: + away_team_name: + example: Liverpool + type: string + bet_id: + example: 1 type: integer - eventID: + event_id: + example: 1 type: integer + expires: + example: "2025-04-08T12:00:00Z" + type: string + home_team_name: + example: Manchester + type: string id: + example: 1 type: integer - oddID: + market_id: + example: 1 type: integer + market_name: + example: Fulltime Result + type: string + odd: + example: 1.5 + type: number + odd_id: + example: 1 + type: integer + odd_name: + example: "1" + type: string type: object domain.BetStatus: enum: @@ -96,15 +121,36 @@ definitions: - RoleCashier domain.TicketOutcome: properties: + away_team_name: + example: Liverpool + type: string event_id: example: 1 type: integer + expires: + example: "2025-04-08T12:00:00Z" + type: string + home_team_name: + example: Manchester + type: string id: example: 1 type: integer + market_id: + example: 1 + type: integer + market_name: + example: Fulltime Result + type: string + odd: + example: 1.5 + type: number odd_id: example: 1 type: integer + odd_name: + example: "1" + type: string ticket_id: example: 1 type: integer @@ -151,15 +197,6 @@ definitions: description: Converted from "time" field in UNIX format type: string type: object - handlers.BetOutcome: - properties: - event_id: - example: 1 - type: integer - odd_id: - example: 1 - type: integer - type: object handlers.BetRes: properties: amount: @@ -168,6 +205,12 @@ definitions: branch_id: example: 2 type: integer + cashed_id: + example: "21234" + type: string + cashed_out: + example: false + type: boolean full_name: example: John type: string @@ -274,6 +317,39 @@ definitions: phone_number_exist: type: boolean type: object + handlers.CreateBetOutcomeReq: + properties: + away_team_name: + example: Liverpool + type: string + bet_id: + example: 1 + type: integer + event_id: + example: 1 + type: integer + expires: + example: "2025-04-08T12:00:00Z" + type: string + home_team_name: + example: Manchester + type: string + market_id: + example: 1 + type: integer + market_name: + example: Fulltime Result + type: string + odd: + example: 1.5 + type: number + odd_id: + example: 1 + type: integer + odd_name: + example: "1" + type: string + type: object handlers.CreateBetReq: properties: amount: @@ -287,7 +363,7 @@ definitions: type: boolean outcomes: items: - $ref: '#/definitions/handlers.BetOutcome' + $ref: '#/definitions/handlers.CreateBetOutcomeReq' type: array phone_number: example: "1234567890" @@ -379,6 +455,39 @@ definitions: example: SportsBook type: string type: object + handlers.CreateTicketOutcomeReq: + properties: + away_team_name: + example: Liverpool + type: string + event_id: + example: 1 + type: integer + expires: + example: "2025-04-08T12:00:00Z" + type: string + home_team_name: + example: Manchester + type: string + market_id: + example: 1 + type: integer + market_name: + example: Fulltime Result + type: string + odd: + example: 1.5 + type: number + odd_id: + example: 1 + type: integer + odd_name: + example: "1" + type: string + ticket_id: + example: 1 + type: integer + type: object handlers.CreateTicketReq: properties: amount: @@ -386,7 +495,7 @@ definitions: type: number outcomes: items: - $ref: '#/definitions/handlers.TicketOutcome' + $ref: '#/definitions/handlers.CreateTicketOutcomeReq' type: array total_odds: example: 4.22 @@ -418,12 +527,6 @@ definitions: bet_id: example: 1 type: integer - branch_id: - example: 1 - type: integer - cashier_id: - example: 1 - type: integer full_name: example: John Smith type: string @@ -550,15 +653,6 @@ definitions: example: SportsBook type: string type: object - handlers.TicketOutcome: - properties: - event_id: - example: 1 - type: integer - odd_id: - example: 1 - type: integer - type: object handlers.TicketRes: properties: amount: @@ -1046,6 +1140,35 @@ paths: summary: Updates the cashed out field tags: - bet + /bet/cashout/{id}: + get: + consumes: + - application/json + description: Gets a single bet by cashout id + parameters: + - description: cashout ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.BetRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets bet by cashout id + tags: + - bet /branch: get: consumes: diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index c429b91..d8914d7 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -71,9 +71,16 @@ func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, erro } type CreateBetOutcomeParams struct { - BetID int64 `json:"bet_id"` - EventID int64 `json:"event_id"` - OddID int64 `json:"odd_id"` + BetID int64 `json:"bet_id"` + EventID int64 `json:"event_id"` + OddID int64 `json:"odd_id"` + HomeTeamName string `json:"home_team_name"` + AwayTeamName string `json:"away_team_name"` + MarketID int64 `json:"market_id"` + MarketName string `json:"market_name"` + Odd float32 `json:"odd"` + OddName string `json:"odd_name"` + Expires pgtype.Timestamp `json:"expires"` } const DeleteBet = `-- name: DeleteBet :exec @@ -177,33 +184,32 @@ func (q *Queries) GetBetByBranchID(ctx context.Context, branchID pgtype.Int8) ([ return items, nil } -const GetBetByCashoutID = `-- name: GetBetByCashoutID :many -SELECT +const GetBetByCashoutID = `-- name: GetBetByCashoutID :one +SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, cashout_id, created_at, updated_at, is_shop_bet, outcomes FROM bet_with_outcomes WHERE cashout_id = $1 ` -type GetBetByCashoutIDRow struct { -} - -func (q *Queries) GetBetByCashoutID(ctx context.Context, cashoutID string) ([]GetBetByCashoutIDRow, error) { - rows, err := q.db.Query(ctx, GetBetByCashoutID, cashoutID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetBetByCashoutIDRow - for rows.Next() { - var i GetBetByCashoutIDRow - if err := rows.Scan(); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil +func (q *Queries) GetBetByCashoutID(ctx context.Context, cashoutID string) (BetWithOutcome, error) { + row := q.db.QueryRow(ctx, GetBetByCashoutID, cashoutID) + var i BetWithOutcome + err := row.Scan( + &i.ID, + &i.Amount, + &i.TotalOdds, + &i.Status, + &i.FullName, + &i.PhoneNumber, + &i.BranchID, + &i.UserID, + &i.CashedOut, + &i.CashoutID, + &i.CreatedAt, + &i.UpdatedAt, + &i.IsShopBet, + &i.Outcomes, + ) + return i, err } const GetBetByID = `-- name: GetBetByID :one diff --git a/gen/db/copyfrom.go b/gen/db/copyfrom.go index f3dffed..5428a01 100644 --- a/gen/db/copyfrom.go +++ b/gen/db/copyfrom.go @@ -32,6 +32,13 @@ func (r iteratorForCreateBetOutcome) Values() ([]interface{}, error) { r.rows[0].BetID, r.rows[0].EventID, r.rows[0].OddID, + r.rows[0].HomeTeamName, + r.rows[0].AwayTeamName, + r.rows[0].MarketID, + r.rows[0].MarketName, + r.rows[0].Odd, + r.rows[0].OddName, + r.rows[0].Expires, }, nil } @@ -40,7 +47,7 @@ func (r iteratorForCreateBetOutcome) Err() error { } func (q *Queries) CreateBetOutcome(ctx context.Context, arg []CreateBetOutcomeParams) (int64, error) { - return q.db.CopyFrom(ctx, []string{"bet_outcomes"}, []string{"bet_id", "event_id", "odd_id"}, &iteratorForCreateBetOutcome{rows: arg}) + return q.db.CopyFrom(ctx, []string{"bet_outcomes"}, []string{"bet_id", "event_id", "odd_id", "home_team_name", "away_team_name", "market_id", "market_name", "odd", "odd_name", "expires"}, &iteratorForCreateBetOutcome{rows: arg}) } // iteratorForCreateTicketOutcome implements pgx.CopyFromSource. @@ -66,6 +73,13 @@ func (r iteratorForCreateTicketOutcome) Values() ([]interface{}, error) { r.rows[0].TicketID, r.rows[0].EventID, r.rows[0].OddID, + r.rows[0].HomeTeamName, + r.rows[0].AwayTeamName, + r.rows[0].MarketID, + r.rows[0].MarketName, + r.rows[0].Odd, + r.rows[0].OddName, + r.rows[0].Expires, }, nil } @@ -74,5 +88,5 @@ func (r iteratorForCreateTicketOutcome) Err() error { } func (q *Queries) CreateTicketOutcome(ctx context.Context, arg []CreateTicketOutcomeParams) (int64, error) { - return q.db.CopyFrom(ctx, []string{"ticket_outcomes"}, []string{"ticket_id", "event_id", "odd_id"}, &iteratorForCreateTicketOutcome{rows: arg}) + return q.db.CopyFrom(ctx, []string{"ticket_outcomes"}, []string{"ticket_id", "event_id", "odd_id", "home_team_name", "away_team_name", "market_id", "market_name", "odd", "odd_name", "expires"}, &iteratorForCreateTicketOutcome{rows: arg}) } diff --git a/gen/db/models.go b/gen/db/models.go index 9a9993c..8d02167 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -25,10 +25,17 @@ type Bet struct { } type BetOutcome struct { - ID int64 `json:"id"` - BetID int64 `json:"bet_id"` - EventID int64 `json:"event_id"` - OddID int64 `json:"odd_id"` + ID int64 `json:"id"` + BetID int64 `json:"bet_id"` + EventID int64 `json:"event_id"` + OddID int64 `json:"odd_id"` + HomeTeamName string `json:"home_team_name"` + AwayTeamName string `json:"away_team_name"` + MarketID int64 `json:"market_id"` + MarketName string `json:"market_name"` + Odd float32 `json:"odd"` + OddName string `json:"odd_name"` + Expires pgtype.Timestamp `json:"expires"` } type BetWithOutcome struct { @@ -148,7 +155,6 @@ type Odd struct { MarketCategory pgtype.Text `json:"market_category"` MarketID pgtype.Text `json:"market_id"` Name pgtype.Text `json:"name"` - Header pgtype.Text `json:"header"` Handicap pgtype.Text `json:"handicap"` OddsValue pgtype.Float8 `json:"odds_value"` Section string `json:"section"` @@ -188,22 +194,29 @@ type SupportedOperation struct { type Ticket struct { ID int64 `json:"id"` - Amount pgtype.Int8 `json:"amount"` + Amount int64 `json:"amount"` TotalOdds float32 `json:"total_odds"` CreatedAt pgtype.Timestamp `json:"created_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"` } type TicketOutcome struct { - ID int64 `json:"id"` - TicketID int64 `json:"ticket_id"` - EventID int64 `json:"event_id"` - OddID int64 `json:"odd_id"` + ID int64 `json:"id"` + TicketID int64 `json:"ticket_id"` + EventID int64 `json:"event_id"` + OddID int64 `json:"odd_id"` + HomeTeamName string `json:"home_team_name"` + AwayTeamName string `json:"away_team_name"` + MarketID int64 `json:"market_id"` + MarketName string `json:"market_name"` + Odd float32 `json:"odd"` + OddName string `json:"odd_name"` + Expires pgtype.Timestamp `json:"expires"` } type TicketWithOutcome struct { ID int64 `json:"id"` - Amount pgtype.Int8 `json:"amount"` + Amount int64 `json:"amount"` TotalOdds float32 `json:"total_odds"` CreatedAt pgtype.Timestamp `json:"created_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"` diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 5f966bf..f7e88ab 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -12,7 +12,8 @@ import ( ) const GetALLPrematchOdds = `-- name: GetALLPrematchOdds :many -SELECT event_id, +SELECT + event_id, fi, market_type, market_name, @@ -28,8 +29,7 @@ SELECT event_id, source, is_active FROM odds -WHERE is_active = true - AND source = 'b365api' +WHERE is_active = true AND source = 'b365api' ` type GetALLPrematchOddsRow struct { @@ -87,7 +87,8 @@ func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]GetALLPrematchOddsR } const GetPrematchOdds = `-- name: GetPrematchOdds :many -SELECT event_id, +SELECT + event_id, fi, market_type, market_name, @@ -103,8 +104,7 @@ SELECT event_id, source, is_active FROM odds -WHERE is_active = true - AND source = 'b365api' +WHERE is_active = true AND source = 'b365api' ` type GetPrematchOddsRow struct { @@ -162,7 +162,8 @@ func (q *Queries) GetPrematchOdds(ctx context.Context) ([]GetPrematchOddsRow, er } const GetPrematchOddsByUpcomingID = `-- name: GetPrematchOddsByUpcomingID :many -SELECT o.event_id, +SELECT + o.event_id, o.fi, o.market_type, o.market_name, @@ -178,12 +179,12 @@ SELECT o.event_id, o.source, o.is_active FROM odds o - JOIN events e ON o.fi = e.id +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' + AND e.is_live = false + AND e.status = 'upcoming' + AND o.is_active = true + AND o.source = 'b365api' LIMIT $2 OFFSET $3 ` @@ -248,13 +249,15 @@ func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, arg GetPremat } const GetRawOddsByID = `-- name: GetRawOddsByID :one -SELECT id, - raw_odds, +SELECT + id, + raw_odds, fetched_at FROM odds -WHERE raw_odds @> $1::jsonb - AND is_active = true - AND source = 'b365api' +WHERE + raw_odds @> $1::jsonb AND + is_active = true AND + source = 'b365api' LIMIT 1 ` @@ -273,49 +276,35 @@ func (q *Queries) GetRawOddsByID(ctx context.Context, dollar_1 []byte) (GetRawOd const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec INSERT INTO odds ( - event_id, - fi, - market_type, - market_name, - market_category, - market_id, - name, - handicap, - odds_value, - section, - category, - raw_odds, - is_active, - source, - fetched_at - ) -VALUES ( - $1, - $2, - $3, - $4, - $5, - $6, - $7, - $8, - $9, - $10, - $11, - $12, - $13, - $14, - $15 - ) ON CONFLICT (market_id, name, handicap) DO -UPDATE -SET odds_value = EXCLUDED.odds_value, - raw_odds = EXCLUDED.raw_odds, - market_type = EXCLUDED.market_type, - market_name = EXCLUDED.market_name, + event_id, + fi, + market_type, + market_name, + market_category, + market_id, + name, + handicap, + odds_value, + section, + category, + raw_odds, + is_active, + source, + fetched_at +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, + $8, $9, $10, $11, $12, $13, $14, $15 +) +ON CONFLICT (market_id, name, handicap) DO UPDATE SET + odds_value = EXCLUDED.odds_value, + raw_odds = EXCLUDED.raw_odds, + market_type = EXCLUDED.market_type, + market_name = EXCLUDED.market_name, market_category = EXCLUDED.market_category, - fetched_at = EXCLUDED.fetched_at, - is_active = EXCLUDED.is_active, - source = EXCLUDED.source, - fi = EXCLUDED.fi + fetched_at = EXCLUDED.fetched_at, + is_active = EXCLUDED.is_active, + source = EXCLUDED.source, + fi = EXCLUDED.fi ` type InsertNonLiveOddParams struct { diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index 59ebd69..d49ca8c 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -18,8 +18,8 @@ RETURNING id, amount, total_odds, created_at, updated_at ` type CreateTicketParams struct { - Amount pgtype.Int8 `json:"amount"` - TotalOdds float32 `json:"total_odds"` + Amount int64 `json:"amount"` + TotalOdds float32 `json:"total_odds"` } func (q *Queries) CreateTicket(ctx context.Context, arg CreateTicketParams) (Ticket, error) { @@ -36,9 +36,16 @@ func (q *Queries) CreateTicket(ctx context.Context, arg CreateTicketParams) (Tic } type CreateTicketOutcomeParams struct { - TicketID int64 `json:"ticket_id"` - EventID int64 `json:"event_id"` - OddID int64 `json:"odd_id"` + TicketID int64 `json:"ticket_id"` + EventID int64 `json:"event_id"` + OddID int64 `json:"odd_id"` + HomeTeamName string `json:"home_team_name"` + AwayTeamName string `json:"away_team_name"` + MarketID int64 `json:"market_id"` + MarketName string `json:"market_name"` + Odd float32 `json:"odd"` + OddName string `json:"odd_name"` + Expires pgtype.Timestamp `json:"expires"` } const DeleteOldTickets = `-- name: DeleteOldTickets :exec @@ -124,7 +131,7 @@ func (q *Queries) GetTicketByID(ctx context.Context, id int64) (TicketWithOutcom } const GetTicketOutcome = `-- name: GetTicketOutcome :many -SELECT id, ticket_id, event_id, odd_id +SELECT id, ticket_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, expires FROM ticket_outcomes WHERE ticket_id = $1 ` @@ -143,6 +150,13 @@ func (q *Queries) GetTicketOutcome(ctx context.Context, ticketID int64) ([]Ticke &i.TicketID, &i.EventID, &i.OddID, + &i.HomeTeamName, + &i.AwayTeamName, + &i.MarketID, + &i.MarketName, + &i.Odd, + &i.OddName, + &i.Expires, ); err != nil { return nil, err } diff --git a/internal/domain/bet.go b/internal/domain/bet.go index 23b3ee8..300b65e 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -1,16 +1,32 @@ package domain +import "time" + type BetOutcome struct { - ID int64 - BetID int64 - EventID int64 - OddID int64 + ID int64 `json:"id" example:"1"` + BetID int64 `json:"bet_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` + HomeTeamName string `json:"home_team_name" example:"Manchester"` + AwayTeamName string `json:"away_team_name" example:"Liverpool"` + MarketID int64 `json:"market_id" example:"1"` + MarketName string `json:"market_name" example:"Fulltime Result"` + Odd float32 `json:"odd" example:"1.5"` + OddName string `json:"odd_name" example:"1"` + Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } type CreateBetOutcome struct { - BetID int64 - EventID int64 - OddID int64 + BetID int64 `json:"bet_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` + HomeTeamName string `json:"home_team_name" example:"Manchester"` + AwayTeamName string `json:"away_team_name" example:"Liverpool"` + MarketID int64 `json:"market_id" example:"1"` + MarketName string `json:"market_name" example:"Fulltime Result"` + Odd float32 `json:"odd" example:"1.5"` + OddName string `json:"odd_name" example:"1"` + Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } type BetStatus int diff --git a/internal/domain/ticket.go b/internal/domain/ticket.go index 50e23f3..6cdf400 100644 --- a/internal/domain/ticket.go +++ b/internal/domain/ticket.go @@ -1,16 +1,32 @@ package domain +import "time" + type TicketOutcome struct { - ID int64 `json:"id" example:"1"` - TicketID int64 `json:"ticket_id" example:"1"` - EventID int64 `json:"event_id" example:"1"` - OddID int64 `json:"odd_id" example:"1"` + ID int64 `json:"id" example:"1"` + TicketID int64 `json:"ticket_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` + HomeTeamName string `json:"home_team_name" example:"Manchester"` + AwayTeamName string `json:"away_team_name" example:"Liverpool"` + MarketID int64 `json:"market_id" example:"1"` + MarketName string `json:"market_name" example:"Fulltime Result"` + Odd float32 `json:"odd" example:"1.5"` + OddName string `json:"odd_name" example:"1"` + Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } type CreateTicketOutcome struct { - TicketID int64 - EventID int64 - OddID int64 + TicketID int64 `json:"ticket_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` + HomeTeamName string `json:"home_team_name" example:"Manchester"` + AwayTeamName string `json:"away_team_name" example:"Liverpool"` + MarketID int64 `json:"market_id" example:"1"` + MarketName string `json:"market_name" example:"Fulltime Result"` + Odd float32 `json:"odd" example:"1.5"` + OddName string `json:"odd_name" example:"1"` + Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } // ID will serve as the fast code since this doesn't need to be secure diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 5467d0f..23b81d1 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -2,6 +2,7 @@ package repository import ( "context" + // "fmt" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -31,14 +32,21 @@ func convertDBBet(bet dbgen.Bet) domain.Bet { } func convertDBBetOutcomes(bet dbgen.BetWithOutcome) domain.GetBet { - var outcomes []domain.BetOutcome = make([]domain.BetOutcome, len(bet.Outcomes)) + var outcomes []domain.BetOutcome = make([]domain.BetOutcome, 0, len(bet.Outcomes)) for _, outcome := range bet.Outcomes { outcomes = append(outcomes, domain.BetOutcome{ - ID: outcome.ID, - BetID: outcome.BetID, - EventID: outcome.EventID, - OddID: outcome.OddID, + ID: outcome.ID, + BetID: outcome.BetID, + EventID: outcome.EventID, + OddID: outcome.OddID, + HomeTeamName: outcome.HomeTeamName, + AwayTeamName: outcome.AwayTeamName, + MarketID: outcome.MarketID, + MarketName: outcome.MarketName, + Odd: outcome.Odd, + OddName: outcome.OddName, + Expires: outcome.Expires.Time, }) } return domain.GetBet{ @@ -63,6 +71,24 @@ func convertDBBetOutcomes(bet dbgen.BetWithOutcome) domain.GetBet { } } +func convertDBCreateBetOutcome(betOutcome domain.CreateBetOutcome) dbgen.CreateBetOutcomeParams { + return dbgen.CreateBetOutcomeParams{ + BetID: betOutcome.BetID, + EventID: betOutcome.EventID, + OddID: betOutcome.OddID, + HomeTeamName: betOutcome.HomeTeamName, + AwayTeamName: betOutcome.AwayTeamName, + MarketID: betOutcome.MarketID, + MarketName: betOutcome.MarketName, + Odd: betOutcome.Odd, + OddName: betOutcome.OddName, + Expires: pgtype.Timestamp{ + Time: betOutcome.Expires, + Valid: true, + }, + } +} + func convertCreateBet(bet domain.CreateBet) dbgen.CreateBetParams { return dbgen.CreateBetParams{ Amount: int64(bet.Amount), @@ -96,11 +122,7 @@ func (s *Store) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBe var dbParams []dbgen.CreateBetOutcomeParams = make([]dbgen.CreateBetOutcomeParams, 0, len(outcomes)) for _, outcome := range outcomes { - dbParams = append(dbParams, dbgen.CreateBetOutcomeParams{ - BetID: outcome.BetID, - EventID: outcome.EventID, - OddID: outcome.OddID, - }) + dbParams = append(dbParams, convertDBCreateBetOutcome(outcome)) } rows, err := s.queries.CreateBetOutcome(ctx, dbParams) @@ -113,6 +135,17 @@ func (s *Store) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBe func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) { bet, err := s.queries.GetBetByID(ctx, id) + + if err != nil { + return domain.GetBet{}, err + } + + return convertDBBetOutcomes(bet), nil +} + +func (s *Store) GetBetByCashoutID(ctx context.Context, id string) (domain.GetBet, error) { + bet, err := s.queries.GetBetByCashoutID(ctx, id) + if err != nil { return domain.GetBet{}, err } @@ -122,7 +155,6 @@ func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) func (s *Store) GetAllBets(ctx context.Context) ([]domain.GetBet, error) { bets, err := s.queries.GetAllBets(ctx) - if err != nil { return nil, err } diff --git a/internal/repository/ticket.go b/internal/repository/ticket.go index cefdbf8..50eff64 100644 --- a/internal/repository/ticket.go +++ b/internal/repository/ticket.go @@ -2,7 +2,6 @@ package repository import ( "context" - "fmt" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -12,10 +11,9 @@ import ( func convertDBTicket(ticket dbgen.Ticket) domain.Ticket { return domain.Ticket{ ID: ticket.ID, - Amount: domain.Currency(ticket.Amount.Int64), + Amount: domain.Currency(ticket.Amount), TotalOdds: ticket.TotalOdds, } - } func convertDBTicketOutcomes(ticket dbgen.TicketWithOutcome) domain.GetTicket { @@ -24,25 +22,48 @@ func convertDBTicketOutcomes(ticket dbgen.TicketWithOutcome) domain.GetTicket { for _, outcome := range ticket.Outcomes { outcomes = append(outcomes, domain.TicketOutcome{ - ID: outcome.ID, - TicketID: outcome.TicketID, - EventID: outcome.EventID, - OddID: outcome.OddID, + ID: outcome.ID, + TicketID: outcome.TicketID, + EventID: outcome.EventID, + OddID: outcome.OddID, + HomeTeamName: outcome.HomeTeamName, + AwayTeamName: outcome.AwayTeamName, + MarketID: outcome.MarketID, + MarketName: outcome.MarketName, + Odd: outcome.Odd, + OddName: outcome.OddName, + Expires: outcome.Expires.Time, }) } return domain.GetTicket{ ID: ticket.ID, - Amount: domain.Currency(ticket.Amount.Int64), + Amount: domain.Currency(ticket.Amount), TotalOdds: ticket.TotalOdds, Outcomes: outcomes, } } +func convertDBCreateTicketOutcome(ticketOutcome domain.CreateTicketOutcome) dbgen.CreateTicketOutcomeParams { + return dbgen.CreateTicketOutcomeParams{ + TicketID: ticketOutcome.TicketID, + EventID: ticketOutcome.EventID, + OddID: ticketOutcome.OddID, + HomeTeamName: ticketOutcome.HomeTeamName, + AwayTeamName: ticketOutcome.AwayTeamName, + MarketID: ticketOutcome.MarketID, + MarketName: ticketOutcome.MarketName, + Odd: ticketOutcome.Odd, + OddName: ticketOutcome.OddName, + Expires: pgtype.Timestamp{ + Time: ticketOutcome.Expires, + Valid: true, + }, + } +} + func convertCreateTicket(ticket domain.CreateTicket) dbgen.CreateTicketParams { return dbgen.CreateTicketParams{ - Amount: pgtype.Int8{ - Int64: int64(ticket.Amount), - }, + Amount: int64(ticket.Amount), TotalOdds: ticket.TotalOdds, } } @@ -61,11 +82,7 @@ func (s *Store) CreateTicketOutcome(ctx context.Context, outcomes []domain.Creat var dbParams []dbgen.CreateTicketOutcomeParams = make([]dbgen.CreateTicketOutcomeParams, 0, len(outcomes)) for _, outcome := range outcomes { - dbParams = append(dbParams, dbgen.CreateTicketOutcomeParams{ - TicketID: outcome.TicketID, - EventID: outcome.EventID, - OddID: outcome.OddID, - }) + dbParams = append(dbParams, convertDBCreateTicketOutcome(outcome)) } rows, err := s.queries.CreateTicketOutcome(ctx, dbParams) @@ -89,7 +106,6 @@ func (s *Store) GetTicketByID(ctx context.Context, id int64) (domain.GetTicket, func (s *Store) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) { tickets, err := s.queries.GetAllTickets(ctx) - fmt.Printf("%v", tickets) if err != nil { return nil, err } @@ -97,7 +113,6 @@ func (s *Store) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) { var result []domain.GetTicket = make([]domain.GetTicket, 0, len(tickets)) for _, ticket := range tickets { result = append(result, convertDBTicketOutcomes(ticket)) - // fmt.Printf("%v", convertDBTicketOutcomes(ticket)) } return result, nil diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index 2c7c133..3b10393 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -9,6 +9,7 @@ import ( type BetStore interface { CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBetOutcome) (int64, error) + GetBetByCashoutID(ctx context.Context, id string) (domain.GetBet, error) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) GetAllBets(ctx context.Context) ([]domain.GetBet, error) GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.GetBet, error) diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 83cbb27..b5f61ef 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -27,6 +27,9 @@ func (s *Service) CreateBetOutcome(ctx context.Context, outcomes []domain.Create func (s *Service) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) { return s.betStore.GetBetByID(ctx, id) } +func (s *Service) GetBetByCashoutID(ctx context.Context, id string) (domain.GetBet, error) { + return s.betStore.GetBetByCashoutID(ctx, id) +} func (s *Service) GetAllBets(ctx context.Context) ([]domain.GetBet, error) { return s.betStore.GetAllBets(ctx) } diff --git a/internal/web_server/app.go b/internal/web_server/app.go index c81a930..b41dcf0 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -64,10 +64,10 @@ func NewApp( }) app.Use(cors.New(cors.Config{ - AllowOrigins: "http://localhost:8000", // Specify your frontend's origin - AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", // Specify the allowed HTTP methods - AllowHeaders: "Content-Type,Authorization,platform", // Specify the allowed headers - AllowCredentials: true, + AllowOrigins: "*", // Specify your frontend's origin + AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", // Specify the allowed HTTP methods + AllowHeaders: "Content-Type,Authorization,platform", // Specify the allowed headers + // AllowCredentials: true, })) s := &App{ diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index ddf9677..461c0cb 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -1,61 +1,56 @@ package httpserver import ( - // "context" - "log" + "log" - eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" - oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" - "github.com/robfig/cron/v3" + eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/robfig/cron/v3" ) func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service) { - c := cron.New(cron.WithSeconds()) - - schedule := []struct { - spec string - task func() - }{ - - // { - // spec: "*/30 * * * * *", // Every 30 seconds - // task: func() { - // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - // log.Printf("FetchUpcomingEvents error: %v", err) - // } - // }, - // }, - - - // { - // spec: "*/5 * * * * *", // Every 5 seconds - // task: func() { - // if err := eventService.FetchLiveEvents(context.Background()); err != nil { - // log.Printf("FetchLiveEvents error: %v", err) - // } - // }, - // }, + c := cron.New(cron.WithSeconds()) + schedule := []struct { + spec string + task func() + }{ // { - // spec: "*/30 * * * * *", // Every 30 seconds - // task: func() { - // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - // log.Printf("FetchNonLiveOdds error: %v", err) - // } - // }, - // }, - - - } + // spec: "*/30 * * * * *", // Every 30 seconds + // task: func() { + // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + // log.Printf("FetchUpcomingEvents error: %v", err) + // } + // }, + // }, - - for _, job := range schedule { - if _, err := c.AddFunc(job.spec, job.task); err != nil { - log.Fatalf("Failed to schedule cron job: %v", err) - } - } + // { + // spec: "*/5 * * * * *", // Every 5 seconds + // task: func() { + // if err := eventService.FetchLiveEvents(context.Background()); err != nil { + // log.Printf("FetchLiveEvents error: %v", err) + // } + // }, + // }, - c.Start() - log.Println("Cron jobs started for event and odds services") -} \ No newline at end of file + // { + // spec: "*/30 * * * * *", // Every 30 seconds + // task: func() { + // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + // log.Printf("FetchNonLiveOdds error: %v", err) + // } + // }, + // }, + } + + 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) + } + } + + c.Start() + log.Println("Cron jobs started for event and odds services") +} diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index dca3acc..8baae19 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -4,6 +4,7 @@ import ( "encoding/json" "log/slog" "strconv" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" @@ -16,10 +17,19 @@ import ( "github.com/google/uuid" ) -type BetOutcome struct { - EventID int64 `json:"event_id" example:"1"` - OddID int64 `json:"odd_id" example:"1"` +type CreateBetOutcomeReq struct { + BetID int64 `json:"bet_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` + HomeTeamName string `json:"home_team_name" example:"Manchester"` + AwayTeamName string `json:"away_team_name" example:"Liverpool"` + MarketID int64 `json:"market_id" example:"1"` + MarketName string `json:"market_name" example:"Fulltime Result"` + Odd float32 `json:"odd" example:"1.5"` + OddName string `json:"odd_name" example:"1"` + Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } + type NullableInt64 struct { Value int64 Valid bool @@ -49,13 +59,13 @@ func (n NullableInt64) MarshalJSON() ([]byte, error) { } type CreateBetReq struct { - Outcomes []BetOutcome `json:"outcomes"` - Amount float32 `json:"amount" example:"100.0"` - TotalOdds float32 `json:"total_odds" example:"4.22"` - Status domain.BetStatus `json:"status" example:"1"` - FullName string `json:"full_name" example:"John"` - PhoneNumber string `json:"phone_number" example:"1234567890"` - IsShopBet bool `json:"is_shop_bet" example:"false"` + Outcomes []CreateBetOutcomeReq `json:"outcomes"` + Amount float32 `json:"amount" example:"100.0"` + TotalOdds float32 `json:"total_odds" example:"4.22"` + Status domain.BetStatus `json:"status" example:"1"` + FullName string `json:"full_name" example:"John"` + PhoneNumber string `json:"phone_number" example:"1234567890"` + IsShopBet bool `json:"is_shop_bet" example:"false"` } type CreateBetRes struct { @@ -69,6 +79,7 @@ type CreateBetRes struct { 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"` @@ -81,6 +92,8 @@ type BetRes struct { 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 { @@ -94,6 +107,7 @@ func convertCreateBet(bet domain.Bet, createdNumber int64) CreateBetRes { BranchID: bet.BranchID.Value, UserID: bet.UserID.Value, CreatedNumber: createdNumber, + CashedID: bet.CashoutID, } } @@ -107,6 +121,10 @@ func convertBet(bet domain.GetBet) BetRes { PhoneNumber: bet.PhoneNumber, BranchID: bet.BranchID.Value, UserID: bet.UserID.Value, + Outcomes: bet.Outcomes, + IsShopBet: bet.IsShopBet, + CashedOut: bet.CashedOut, + CashedID: bet.CashoutID, } } @@ -145,7 +163,7 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, user, err := userSvc.GetUserByID(c.Context(), userID) cashoutUUID := uuid.New() var bet domain.Bet - if user.Role != domain.RoleCashier { + if user.Role == domain.RoleCashier { // Get the branch from the branch ID branch, err := branchSvc.GetBranchByCashier(c.Context(), user.ID) @@ -167,6 +185,7 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, "error": "Unable to deduct from branch wallet", }) } + bet, err = betSvc.CreateBet(c.Context(), domain.CreateBet{ Amount: domain.ToCurrency(req.Amount), TotalOdds: req.TotalOdds, @@ -217,9 +236,16 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, for _, outcome := range req.Outcomes { outcomes = append(outcomes, domain.CreateBetOutcome{ - BetID: bet.ID, - EventID: outcome.EventID, - OddID: outcome.OddID, + BetID: bet.ID, + EventID: outcome.EventID, + OddID: outcome.OddID, + HomeTeamName: outcome.HomeTeamName, + AwayTeamName: outcome.AwayTeamName, + MarketID: outcome.MarketID, + MarketName: outcome.MarketName, + Odd: outcome.Odd, + OddName: outcome.OddName, + Expires: outcome.Expires, }) } rows, err := betSvc.CreateBetOutcome(c.Context(), outcomes) @@ -287,7 +313,6 @@ func GetBetByID(logger *slog.Logger, betSvc *bet.Service, validator *customvalid } bet, err := betSvc.GetBetByID(c.Context(), id) - if err != nil { logger.Error("Failed to get bet by ID", "betID", id, "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bet", err, nil) @@ -300,6 +325,40 @@ func GetBetByID(logger *slog.Logger, betSvc *bet.Service, validator *customvalid } } +// GetBetByCashoutID godoc +// @Summary Gets bet by cashout id +// @Description Gets a single bet by cashout id +// @Tags bet +// @Accept json +// @Produce json +// @Param id path string true "cashout ID" +// @Success 200 {object} BetRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /bet/cashout/{id} [get] +func GetBetByCashoutID(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + cashoutID := c.Params("id") + // id, err := strconv.ParseInt(cashoutID, 10, 64) + + // if err != nil { + // logger.Error("Invalid cashout ID", "cashoutID", cashoutID, "error", err) + // return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashout ID", err, nil) + // } + + bet, err := betSvc.GetBetByCashoutID(c.Context(), cashoutID) + if err != nil { + logger.Error("Failed to get bet by ID", "cashoutID", cashoutID, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bet", err, nil) + } + + res := convertBet(bet) + + return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil) + + } +} + type UpdateCashOutReq struct { CashedOut bool } diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index 013eca6..b9e398b 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -3,6 +3,7 @@ package handlers import ( "log/slog" "strconv" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" @@ -11,15 +12,23 @@ import ( "github.com/gofiber/fiber/v2" ) -type TicketOutcome struct { - EventID int64 `json:"event_id" example:"1"` - OddID int64 `json:"odd_id" example:"1"` +type CreateTicketOutcomeReq struct { + TicketID int64 `json:"ticket_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` + HomeTeamName string `json:"home_team_name" example:"Manchester"` + AwayTeamName string `json:"away_team_name" example:"Liverpool"` + MarketID int64 `json:"market_id" example:"1"` + MarketName string `json:"market_name" example:"Fulltime Result"` + Odd float32 `json:"odd" example:"1.5"` + OddName string `json:"odd_name" example:"1"` + Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } type CreateTicketReq struct { - Outcomes []TicketOutcome `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"` + TotalOdds float32 `json:"total_odds" example:"4.22"` } type CreateTicketRes struct { FastCode int64 `json:"fast_code" example:"1234"` @@ -71,9 +80,16 @@ func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, for _, outcome := range req.Outcomes { outcomes = append(outcomes, domain.CreateTicketOutcome{ - TicketID: ticket.ID, - EventID: outcome.EventID, - OddID: outcome.OddID, + TicketID: ticket.ID, + EventID: outcome.EventID, + OddID: outcome.OddID, + HomeTeamName: outcome.HomeTeamName, + AwayTeamName: outcome.AwayTeamName, + MarketID: outcome.MarketID, + MarketName: outcome.MarketName, + Odd: outcome.Odd, + OddName: outcome.OddName, + Expires: outcome.Expires, }) } rows, err := ticketSvc.CreateTicketOutcome(c.Context(), outcomes) diff --git a/internal/web_server/handlers/transaction_handler.go b/internal/web_server/handlers/transaction_handler.go index c5b5ea4..7cf85e1 100644 --- a/internal/web_server/handlers/transaction_handler.go +++ b/internal/web_server/handlers/transaction_handler.go @@ -6,6 +6,8 @@ import ( "strconv" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" @@ -32,20 +34,18 @@ type TransactionRes struct { } type CreateTransactionReq struct { - Amount float32 `json:"amount" example:"100.0"` - BranchID int64 `json:"branch_id" example:"1"` - CashierID int64 `json:"cashier_id" example:"1"` - BetID int64 `json:"bet_id" example:"1"` - Type int64 `json:"type" example:"1"` - PaymentOption domain.PaymentOption `json:"payment_option" example:"1"` - FullName string `json:"full_name" example:"John Smith"` - PhoneNumber string `json:"phone_number" example:"0911111111"` - // Payment Details for bank - 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"` + CashoutID string `json:"cashout_id" example:"191212"` + Amount float32 `json:"amount" example:"100.0"` + BetID int64 `json:"bet_id" example:"1"` + Type int64 `json:"type" example:"1"` + PaymentOption domain.PaymentOption `json:"payment_option" example:"1"` + FullName string `json:"full_name" example:"John Smith"` + PhoneNumber string `json:"phone_number" example:"0911111111"` + 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"` } func convertTransaction(transaction domain.Transaction) TransactionRes { @@ -79,8 +79,42 @@ func convertTransaction(transaction domain.Transaction) TransactionRes { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /transaction [post] -func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service, validator *customvalidator.CustomValidator) fiber.Handler { +func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service, userSvc *user.Service, branchSvc *branch.Service, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { + userID := c.Locals("user_id").(int64) + user, err := userSvc.GetUserByID(c.Context(), userID) + + if user.Role == domain.RoleCustomer { + logger.Error("CreateTransactionReq failed") + 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 := branchSvc.GetBranchByID(c.Context(), 1) + if err != nil { + logger.Error("CreateTransactionReq no branches") + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "This user type doesn't have branches", + }) + } + + branchID = branch.ID + + } else { + branch, err := branchSvc.GetBranchByCashier(c.Context(), user.ID) + if err != nil { + logger.Error("CreateTransactionReq failed, branch id invalid") + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Branch ID invalid", + }) + } + branchID = branch.ID + } + var req CreateTransactionReq if err := c.BodyParser(&req); err != nil { logger.Error("CreateTransactionReq failed", "error", err) @@ -91,14 +125,15 @@ func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service, valErrs, ok := validator.Validate(c, req) if !ok { + logger.Error("CreateTransactionReq failed v", "error", valErrs) response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return nil } transaction, err := transactionSvc.CreateTransaction(c.Context(), domain.CreateTransaction{ + BranchID: branchID, + CashierID: userID, Amount: domain.ToCurrency(req.Amount), - BranchID: req.BranchID, - CashierID: req.CashierID, BetID: req.BetID, Type: domain.TransactionType(req.Type), PaymentOption: domain.PaymentOption(req.PaymentOption), @@ -116,6 +151,13 @@ func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service, return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) } + err = betSvc.UpdateCashOut(c.Context(), req.BetID, true) + + if err != nil { + logger.Error("CreateTransactionReq failed", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) + } + res := convertTransaction(transaction) return response.WriteJSON(c, fiber.StatusOK, "Transaction Created", res, nil) @@ -175,8 +217,8 @@ func GetAllTransactions( // Convert transactions to response format var res []TransactionRes = make([]TransactionRes, 0, len(transactions)) - for i, transaction := range transactions { - res[i] = convertTransaction(transaction) + for _, transaction := range transactions { + res = append(res, convertTransaction(transaction)) } return response.WriteJSON(c, fiber.StatusOK, "Transactions retrieved successfully", res, nil) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index b968e15..5de11e4 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -91,9 +91,10 @@ func (a *App) initAppRoutes() { a.fiber.Get("/ticket/:id", handlers.GetTicketByID(a.logger, a.ticketSvc, a.validator)) // Bet - a.fiber.Post("/bet", handlers.CreateBet(a.logger, a.betSvc, a.userSvc, a.branchSvc, a.walletSvc, a.validator)) + a.fiber.Post("/bet", a.authMiddleware, handlers.CreateBet(a.logger, a.betSvc, a.userSvc, a.branchSvc, a.walletSvc, a.validator)) a.fiber.Get("/bet", handlers.GetAllBet(a.logger, a.betSvc, a.validator)) a.fiber.Get("/bet/:id", handlers.GetBetByID(a.logger, a.betSvc, a.validator)) + a.fiber.Get("/bet/cashout/:id", handlers.GetBetByCashoutID(a.logger, a.betSvc, a.validator)) a.fiber.Patch("/bet/:id", handlers.UpdateCashOut(a.logger, a.betSvc, a.validator)) a.fiber.Delete("/bet/:id", handlers.DeleteBet(a.logger, a.betSvc, a.validator)) @@ -110,7 +111,7 @@ func (a *App) initAppRoutes() { a.fiber.Post("/transfer/refill/:id", a.authMiddleware, handlers.RefillWallet(a.logger, a.walletSvc, a.validator)) // Transactions - a.fiber.Post("/transaction", a.authMiddleware, handlers.CreateTransaction(a.logger, a.transactionSvc, a.validator)) + a.fiber.Post("/transaction", a.authMiddleware, handlers.CreateTransaction(a.logger, a.transactionSvc, a.userSvc, a.branchSvc, a.betSvc, a.validator)) a.fiber.Get("/transaction", a.authMiddleware, handlers.GetAllTransactions(a.logger, a.transactionSvc, a.userSvc, a.validator)) a.fiber.Get("/transaction/:id", a.authMiddleware, handlers.GetTransactionByID(a.logger, a.transactionSvc, a.validator)) a.fiber.Patch("/transaction/:id", a.authMiddleware, handlers.UpdateTransactionVerified(a.logger, a.transactionSvc, a.validator)) From d416a40caefa3268c7a97950d2ed6e4ec6792881 Mon Sep 17 00:00:00 2001 From: OneTap Technologies Date: Mon, 14 Apr 2025 16:52:42 +0300 Subject: [PATCH 21/30] fix odd --- db/query/odds.sql | 25 +++++++++++++++---------- gen/db/odds.sql.go | 22 ++++++++++++---------- internal/web_server/cron.go | 4 ++-- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/db/query/odds.sql b/db/query/odds.sql index 912b1f9..84e6382 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -16,19 +16,24 @@ INSERT INTO odds ( source, fetched_at ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15 ) -ON CONFLICT (market_id, name, handicap) DO UPDATE SET - odds_value = EXCLUDED.odds_value, - raw_odds = EXCLUDED.raw_odds, - market_type = EXCLUDED.market_type, - market_name = EXCLUDED.market_name, +ON CONFLICT (event_id, market_id) DO UPDATE SET + odds_value = EXCLUDED.odds_value, + raw_odds = EXCLUDED.raw_odds, + market_type = EXCLUDED.market_type, + market_name = EXCLUDED.market_name, market_category = EXCLUDED.market_category, - fetched_at = EXCLUDED.fetched_at, - is_active = EXCLUDED.is_active, - source = EXCLUDED.source, - fi = EXCLUDED.fi; + name = EXCLUDED.name, + handicap = EXCLUDED.handicap, + fetched_at = EXCLUDED.fetched_at, + is_active = EXCLUDED.is_active, + source = EXCLUDED.source, + fi = EXCLUDED.fi; + + + -- name: GetPrematchOdds :many SELECT diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 66c4078..57865e6 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -292,19 +292,21 @@ INSERT INTO odds ( source, fetched_at ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15 ) -ON CONFLICT (market_id, name, handicap) DO UPDATE SET - odds_value = EXCLUDED.odds_value, - raw_odds = EXCLUDED.raw_odds, - market_type = EXCLUDED.market_type, - market_name = EXCLUDED.market_name, +ON CONFLICT (event_id, market_id) DO UPDATE SET + odds_value = EXCLUDED.odds_value, + raw_odds = EXCLUDED.raw_odds, + market_type = EXCLUDED.market_type, + market_name = EXCLUDED.market_name, market_category = EXCLUDED.market_category, - fetched_at = EXCLUDED.fetched_at, - is_active = EXCLUDED.is_active, - source = EXCLUDED.source, - fi = EXCLUDED.fi + name = EXCLUDED.name, + handicap = EXCLUDED.handicap, + fetched_at = EXCLUDED.fetched_at, + is_active = EXCLUDED.is_active, + source = EXCLUDED.source, + fi = EXCLUDED.fi ` type InsertNonLiveOddParams struct { diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index ddf9677..2ee4e69 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -37,8 +37,8 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S // }, - // { - // spec: "*/30 * * * * *", // Every 30 seconds + // { + // spec: "*/5 * * * * *", // Every 5 seconds // task: func() { // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { // log.Printf("FetchNonLiveOdds error: %v", err) From 1f154e9fafed1051d65aab3cb96d2eabdddf5a8c Mon Sep 17 00:00:00 2001 From: OneTap Technologies Date: Mon, 14 Apr 2025 17:44:08 +0300 Subject: [PATCH 22/30] add filter by market id --- db/query/odds.sql | 6 +-- docs/docs.go | 20 +++++----- docs/swagger.json | 20 +++++----- docs/swagger.yaml | 18 ++++----- gen/db/odds.sql.go | 37 ++++++++++++----- internal/domain/odds.go | 9 ++--- internal/repository/odds.go | 51 ++++++++++++++---------- internal/services/odds/port.go | 2 +- internal/services/odds/service.go | 8 ++-- internal/web_server/handlers/prematch.go | 23 ++++++----- internal/web_server/routes.go | 2 +- 11 files changed, 113 insertions(+), 83 deletions(-) diff --git a/db/query/odds.sql b/db/query/odds.sql index 84e6382..a508511 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -75,17 +75,17 @@ SELECT FROM odds WHERE is_active = true AND source = 'b365api'; --- name: GetRawOddsByID :one +-- name: GetRawOddsByMarketID :many SELECT id, raw_odds, fetched_at FROM odds WHERE - raw_odds @> $1::jsonb AND + market_id = $1 AND is_active = true AND source = 'b365api' -LIMIT 1; +LIMIT $2 OFFSET $3; -- name: GetPrematchOddsByUpcomingID :many SELECT diff --git a/docs/docs.go b/docs/docs.go index 1c7970b..a9242b9 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -288,9 +288,9 @@ const docTemplate = `{ } } }, - "/prematch/odds/raw/{raw_odds_id}": { + "/prematch/odds/raw/{market_id}": { "get": { - "description": "Retrieve raw odds by raw odds ID", + "description": "Retrieve raw odds records using a Market ID", "consumes": [ "application/json" ], @@ -300,12 +300,12 @@ const docTemplate = `{ "tags": [ "prematch" ], - "summary": "Retrieve raw odds by ID", + "summary": "Retrieve raw odds by Market ID", "parameters": [ { "type": "string", - "description": "Raw Odds ID", - "name": "raw_odds_id", + "description": "Market ID", + "name": "market_id", "in": "path", "required": true } @@ -314,7 +314,10 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/domain.RawOddsByID" + "type": "array", + "items": { + "$ref": "#/definitions/domain.RawOddsByMarketID" + } } }, "400": { @@ -761,12 +764,9 @@ const docTemplate = `{ } } }, - "domain.RawOddsByID": { + "domain.RawOddsByMarketID": { "type": "object", "properties": { - "event_id": { - "type": "string" - }, "fetched_at": { "type": "string" }, diff --git a/docs/swagger.json b/docs/swagger.json index 118ca95..a9eb780 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -280,9 +280,9 @@ } } }, - "/prematch/odds/raw/{raw_odds_id}": { + "/prematch/odds/raw/{market_id}": { "get": { - "description": "Retrieve raw odds by raw odds ID", + "description": "Retrieve raw odds records using a Market ID", "consumes": [ "application/json" ], @@ -292,12 +292,12 @@ "tags": [ "prematch" ], - "summary": "Retrieve raw odds by ID", + "summary": "Retrieve raw odds by Market ID", "parameters": [ { "type": "string", - "description": "Raw Odds ID", - "name": "raw_odds_id", + "description": "Market ID", + "name": "market_id", "in": "path", "required": true } @@ -306,7 +306,10 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/domain.RawOddsByID" + "type": "array", + "items": { + "$ref": "#/definitions/domain.RawOddsByMarketID" + } } }, "400": { @@ -753,12 +756,9 @@ } } }, - "domain.RawOddsByID": { + "domain.RawOddsByMarketID": { "type": "object", "properties": { - "event_id": { - "type": "string" - }, "fetched_at": { "type": "string" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b458113..7587a4d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -33,10 +33,8 @@ definitions: source: type: string type: object - domain.RawOddsByID: + domain.RawOddsByMarketID: properties: - event_id: - type: string fetched_at: type: string id: @@ -465,15 +463,15 @@ paths: summary: Retrieve prematch odds for an event tags: - prematch - /prematch/odds/raw/{raw_odds_id}: + /prematch/odds/raw/{market_id}: get: consumes: - application/json - description: Retrieve raw odds by raw odds ID + description: Retrieve raw odds records using a Market ID parameters: - - description: Raw Odds ID + - description: Market ID in: path - name: raw_odds_id + name: market_id required: true type: string produces: @@ -482,7 +480,9 @@ paths: "200": description: OK schema: - $ref: '#/definitions/domain.RawOddsByID' + items: + $ref: '#/definitions/domain.RawOddsByMarketID' + type: array "400": description: Bad Request schema: @@ -491,7 +491,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/response.APIResponse' - summary: Retrieve raw odds by ID + summary: Retrieve raw odds by Market ID tags: - prematch /prematch/odds/upcoming/{upcoming_id}: diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 57865e6..2ced1d4 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -248,30 +248,49 @@ func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, arg GetPremat return items, nil } -const GetRawOddsByID = `-- name: GetRawOddsByID :one +const GetRawOddsByMarketID = `-- name: GetRawOddsByMarketID :many SELECT id, raw_odds, fetched_at FROM odds WHERE - raw_odds @> $1::jsonb AND + market_id = $1 AND is_active = true AND source = 'b365api' -LIMIT 1 +LIMIT $2 OFFSET $3 ` -type GetRawOddsByIDRow struct { +type GetRawOddsByMarketIDParams struct { + MarketID pgtype.Text + Limit int32 + Offset int32 +} + +type GetRawOddsByMarketIDRow struct { ID int32 RawOdds []byte FetchedAt pgtype.Timestamp } -func (q *Queries) GetRawOddsByID(ctx context.Context, dollar_1 []byte) (GetRawOddsByIDRow, error) { - row := q.db.QueryRow(ctx, GetRawOddsByID, dollar_1) - var i GetRawOddsByIDRow - err := row.Scan(&i.ID, &i.RawOdds, &i.FetchedAt) - return i, err +func (q *Queries) GetRawOddsByMarketID(ctx context.Context, arg GetRawOddsByMarketIDParams) ([]GetRawOddsByMarketIDRow, error) { + rows, err := q.db.Query(ctx, GetRawOddsByMarketID, arg.MarketID, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetRawOddsByMarketIDRow + for rows.Next() { + var i GetRawOddsByMarketIDRow + if err := rows.Scan(&i.ID, &i.RawOdds, &i.FetchedAt); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil } const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec diff --git a/internal/domain/odds.go b/internal/domain/odds.go index df2de7e..9992490 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -38,9 +38,8 @@ type Odd struct { Source string `json:"source"` IsActive bool `json:"is_active"` } -type RawOddsByID struct { - ID int64 `json:"id"` - EventID string `json:"event_id"` - RawOdds []RawMessage `json:"raw_odds"` - FetchedAt time.Time `json:"fetched_at"` +type RawOddsByMarketID struct { + ID int64 `json:"id"` + RawOdds []RawMessage `json:"raw_odds"` + FetchedAt time.Time `json:"fetched_at"` } \ No newline at end of file diff --git a/internal/repository/odds.go b/internal/repository/odds.go index a8573a3..bb41387 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -1,15 +1,16 @@ package repository import ( - "context" - "encoding/json" - "os" - "strconv" - "time" + "context" + "encoding/json" + "fmt" + "os" + "strconv" + "time" - dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/jackc/pgx/v5/pgtype" + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" ) func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { @@ -175,21 +176,31 @@ func (s *Store) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) { return domainOdds, nil } -func (s *Store) GetRawOddsByID(ctx context.Context, rawOddsID string) (domain.RawOddsByID, error) { - jsonFilter := `[{"id":"` + rawOddsID + `"}]` - - odd, err := s.queries.GetRawOddsByID(ctx, []byte(jsonFilter)) - if err != nil { - return domain.RawOddsByID{}, err +func (s *Store) GetRawOddsByMarketID(ctx context.Context, rawOddsID string) (domain.RawOddsByMarketID, error) { + params := dbgen.GetRawOddsByMarketIDParams{ + MarketID: pgtype.Text{String: rawOddsID, Valid: true}, + Limit: 1, + Offset: 0, } + rows, err := s.queries.GetRawOddsByMarketID(ctx, params) + if err != nil { + return domain.RawOddsByMarketID{}, err + } + + if len(rows) == 0 { + return domain.RawOddsByMarketID{}, fmt.Errorf("no raw odds found for market_id: %s", rawOddsID) + } + + row := rows[0] + var rawOdds []json.RawMessage - if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { - return domain.RawOddsByID{}, err + if err := json.Unmarshal(row.RawOdds, &rawOdds); err != nil { + return domain.RawOddsByMarketID{}, err } - return domain.RawOddsByID{ - ID: int64(odd.ID), + return domain.RawOddsByMarketID{ + ID: int64(row.ID), RawOdds: func() []domain.RawMessage { converted := make([]domain.RawMessage, len(rawOdds)) for i, r := range rawOdds { @@ -197,19 +208,17 @@ func (s *Store) GetRawOddsByID(ctx context.Context, rawOddsID string) (domain.Ra } return converted }(), - FetchedAt: odd.FetchedAt.Time, + FetchedAt: row.FetchedAt.Time, }, nil } func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) { - // Prepare query parameters params := dbgen.GetPrematchOddsByUpcomingIDParams{ ID: upcomingID, Limit: limit, Offset: offset, } - // Execute the query odds, err := s.queries.GetPrematchOddsByUpcomingID(ctx, params) if err != nil { return nil, err diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index 8805a66..2472e99 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -10,7 +10,7 @@ type Service interface { FetchNonLiveOdds(ctx context.Context) error GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) - GetRawOddsByID(ctx context.Context, rawOddsID string) ([]domain.RawOddsByID, error) + GetRawOddsByMarketID(ctx context.Context, marketID string) ([]domain.RawOddsByMarketID, error) } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 9b31a94..d8bce34 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -122,13 +122,15 @@ func (s *ServiceImpl) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, err return s.store.GetALLPrematchOdds(ctx) } -func (s *ServiceImpl) GetRawOddsByID(ctx context.Context, rawOddsID string) ([]domain.RawOddsByID, error) { - rawOdds, err := s.store.GetRawOddsByID(ctx, rawOddsID) +func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string) ([]domain.RawOddsByMarketID, error) { + rows, err := s.store.GetRawOddsByMarketID(ctx, marketID) if err != nil { return nil, err } - return []domain.RawOddsByID{rawOdds}, nil + + return []domain.RawOddsByMarketID{rows}, nil } + func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) { return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) } diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index 17e4de4..ea1ac59 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -55,26 +55,27 @@ func GetALLPrematchOdds(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fibe return response.WriteJSON(c, fiber.StatusOK, "All prematch odds retrieved successfully", odds, nil) } } -// GetRawOddsByID -// @Summary Retrieve raw odds by ID -// @Description Retrieve raw odds by raw odds ID +// GetRawOddsByMarketID +// @Summary Retrieve raw odds by Market ID +// @Description Retrieve raw odds records using a Market ID // @Tags prematch // @Accept json // @Produce json -// @Param raw_odds_id path string true "Raw Odds ID" -// @Success 200 {object} domain.RawOddsByID +// @Param market_id path string true "Market ID" +// @Success 200 {array} domain.RawOddsByMarketID // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /prematch/odds/raw/{raw_odds_id} [get] -func GetRawOddsByID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { +// @Router /prematch/odds/raw/{market_id} [get] +func GetRawOddsByMarketID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { return func(c *fiber.Ctx) error { - rawOddsID := c.Params("raw_odds_id") - if rawOddsID == "" { - return response.WriteJSON(c, fiber.StatusBadRequest, "Missing raw_odds_id", nil, nil) + marketID := c.Params("market_id") + if marketID == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing market_id", nil, nil) } - rawOdds, err := prematchSvc.GetRawOddsByID(c.Context(), rawOddsID) + rawOdds, err := prematchSvc.GetRawOddsByMarketID(c.Context(), marketID) if err != nil { + logger.Error("failed to fetch raw odds", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve raw odds", nil, nil) } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index f18aa50..1e8a6e5 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -29,7 +29,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/prematch/odds/:event_id", handlers.GetPrematchOdds(a.logger, a.prematchSvc)) a.fiber.Get("/prematch/odds", handlers.GetALLPrematchOdds(a.logger, a.prematchSvc)) - a.fiber.Get("/prematch/odds/raw/:raw_odds_id", handlers.GetRawOddsByID(a.logger, a.prematchSvc)) + a.fiber.Get("/prematch/odds/raw/:market_id", handlers.GetRawOddsByMarketID(a.logger, a.prematchSvc)) a.fiber.Get("/prematch/events/:id", handlers.GetUpcomingEventByID(a.logger, a.eventSvc)) a.fiber.Get("/prematch/events", handlers.GetAllUpcomingEvents(a.logger, a.eventSvc)) From d5be4803ae1bb852cd3fadd25065588365b95b35 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 14 Apr 2025 19:48:22 +0300 Subject: [PATCH 23/30] fix market fetching --- db/query/odds.sql | 121 +++---- docs/docs.go | 101 +++--- docs/swagger.json | 101 +++--- docs/swagger.yaml | 67 ++-- gen/db/models.go | 1 - gen/db/odds.sql.go | 45 +-- internal/repository/odds.go | 395 ++++++++++++----------- internal/services/odds/port.go | 4 +- internal/services/odds/service.go | 85 +++-- internal/web_server/cron.go | 93 +++--- internal/web_server/handlers/prematch.go | 149 +++++---- internal/web_server/routes.go | 2 +- 12 files changed, 587 insertions(+), 577 deletions(-) diff --git a/db/query/odds.sql b/db/query/odds.sql index a508511..c44a691 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -1,43 +1,52 @@ -- name: InsertNonLiveOdd :exec INSERT INTO odds ( - event_id, - fi, - market_type, - market_name, - market_category, - market_id, - name, - handicap, - odds_value, - section, - category, - raw_odds, - is_active, - source, - fetched_at -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, - $8, $9, $10, $11, $12, $13, $14, $15 -) -ON CONFLICT (event_id, market_id) DO UPDATE SET - odds_value = EXCLUDED.odds_value, - raw_odds = EXCLUDED.raw_odds, - market_type = EXCLUDED.market_type, - market_name = EXCLUDED.market_name, + event_id, + fi, + market_type, + market_name, + market_category, + market_id, + name, + handicap, + odds_value, + section, + category, + raw_odds, + is_active, + source, + fetched_at + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15 + ) ON CONFLICT (event_id, market_id) DO +UPDATE +SET odds_value = EXCLUDED.odds_value, + raw_odds = EXCLUDED.raw_odds, + market_type = EXCLUDED.market_type, + market_name = EXCLUDED.market_name, market_category = EXCLUDED.market_category, - name = EXCLUDED.name, - handicap = EXCLUDED.handicap, - fetched_at = EXCLUDED.fetched_at, - is_active = EXCLUDED.is_active, - source = EXCLUDED.source, - fi = EXCLUDED.fi; - - - - + name = EXCLUDED.name, + handicap = EXCLUDED.handicap, + fetched_at = EXCLUDED.fetched_at, + is_active = EXCLUDED.is_active, + source = EXCLUDED.source, + fi = EXCLUDED.fi; -- name: GetPrematchOdds :many -SELECT - event_id, +SELECT event_id, fi, market_type, market_name, @@ -53,11 +62,10 @@ SELECT source, is_active FROM odds -WHERE is_active = true AND source = 'b365api'; - +WHERE is_active = true + AND source = 'b365api'; -- name: GetALLPrematchOdds :many -SELECT - event_id, +SELECT event_id, fi, market_type, market_name, @@ -73,23 +81,20 @@ SELECT source, is_active FROM odds -WHERE is_active = true AND source = 'b365api'; - +WHERE is_active = true + AND source = 'b365api'; -- name: GetRawOddsByMarketID :many -SELECT - id, - raw_odds, +SELECT id, + raw_odds, fetched_at FROM odds -WHERE - market_id = $1 AND - is_active = true AND - source = 'b365api' -LIMIT $2 OFFSET $3; - +WHERE market_id = $1 + AND fi = $2 + AND is_active = true + AND source = 'b365api' +LIMIT $3 OFFSET $4; -- name: GetPrematchOddsByUpcomingID :many -SELECT - o.event_id, +SELECT o.event_id, o.fi, o.market_type, o.market_name, @@ -105,10 +110,10 @@ SELECT o.source, o.is_active FROM odds o -JOIN events e ON o.fi = e.id + 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' + AND e.is_live = false + AND e.status = 'upcoming' + AND o.is_active = true + AND o.source = 'b365api' LIMIT $2 OFFSET $3; \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 57af447..0c5cb37 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1354,53 +1354,6 @@ const docTemplate = `{ } } }, - "/prematch/odds/raw/{market_id}": { - "get": { - "description": "Retrieve raw odds records using a Market ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "prematch" - ], - "summary": "Retrieve raw odds by Market ID", - "parameters": [ - { - "type": "string", - "description": "Market ID", - "name": "market_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.RawOddsByMarketID" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/prematch/odds/upcoming/{upcoming_id}": { "get": { "description": "Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination", @@ -1460,6 +1413,60 @@ const docTemplate = `{ } } }, + "/prematch/odds/upcoming/{upcoming_id}/market/{market_id}": { + "get": { + "description": "Retrieve raw odds records using a Market ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve raw odds by Market ID", + "parameters": [ + { + "type": "string", + "description": "Upcoming ID", + "name": "upcoming_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Market ID", + "name": "market_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.RawOddsByMarketID" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/prematch/odds/{event_id}": { "get": { "description": "Retrieve prematch odds for a specific event by event ID", diff --git a/docs/swagger.json b/docs/swagger.json index 55f1352..60327a4 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1346,53 +1346,6 @@ } } }, - "/prematch/odds/raw/{market_id}": { - "get": { - "description": "Retrieve raw odds records using a Market ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "prematch" - ], - "summary": "Retrieve raw odds by Market ID", - "parameters": [ - { - "type": "string", - "description": "Market ID", - "name": "market_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.RawOddsByMarketID" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/prematch/odds/upcoming/{upcoming_id}": { "get": { "description": "Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination", @@ -1452,6 +1405,60 @@ } } }, + "/prematch/odds/upcoming/{upcoming_id}/market/{market_id}": { + "get": { + "description": "Retrieve raw odds records using a Market ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve raw odds by Market ID", + "parameters": [ + { + "type": "string", + "description": "Upcoming ID", + "name": "upcoming_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Market ID", + "name": "market_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.RawOddsByMarketID" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/prematch/odds/{event_id}": { "get": { "description": "Retrieve prematch odds for a specific event by event ID", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b27f68c..7256661 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1704,37 +1704,6 @@ paths: summary: Retrieve prematch odds for an event tags: - prematch - /prematch/odds/raw/{market_id}: - get: - consumes: - - application/json - description: Retrieve raw odds records using a Market ID - parameters: - - description: Market ID - in: path - name: market_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/domain.RawOddsByMarketID' - type: array - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Retrieve raw odds by Market ID - tags: - - prematch /prematch/odds/upcoming/{upcoming_id}: get: consumes: @@ -1775,6 +1744,42 @@ paths: summary: Retrieve prematch odds by upcoming ID (FI) tags: - prematch + /prematch/odds/upcoming/{upcoming_id}/market/{market_id}: + get: + consumes: + - application/json + description: Retrieve raw odds records using a Market ID + parameters: + - description: Upcoming ID + in: path + name: upcoming_id + required: true + type: string + - description: Market ID + in: path + name: market_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.RawOddsByMarketID' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve raw odds by Market ID + tags: + - prematch /search/branch: get: consumes: diff --git a/gen/db/models.go b/gen/db/models.go index 9a9993c..72f7dfc 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -148,7 +148,6 @@ type Odd struct { MarketCategory pgtype.Text `json:"market_category"` MarketID pgtype.Text `json:"market_id"` Name pgtype.Text `json:"name"` - Header pgtype.Text `json:"header"` Handicap pgtype.Text `json:"handicap"` OddsValue pgtype.Float8 `json:"odds_value"` Section string `json:"section"` diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index d4c31c2..846494f 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -253,15 +253,17 @@ SELECT id, fetched_at FROM odds WHERE market_id = $1 + AND fi = $2 AND is_active = true AND source = 'b365api' -LIMIT $2 OFFSET $3 +LIMIT $3 OFFSET $4 ` type GetRawOddsByMarketIDParams struct { - MarketID pgtype.Text - Limit int32 - Offset int32 + MarketID pgtype.Text `json:"market_id"` + Fi pgtype.Text `json:"fi"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` } type GetRawOddsByMarketIDRow struct { @@ -271,7 +273,12 @@ type GetRawOddsByMarketIDRow struct { } func (q *Queries) GetRawOddsByMarketID(ctx context.Context, arg GetRawOddsByMarketIDParams) ([]GetRawOddsByMarketIDRow, error) { - rows, err := q.db.Query(ctx, GetRawOddsByMarketID, arg.MarketID, arg.Limit, arg.Offset) + rows, err := q.db.Query(ctx, GetRawOddsByMarketID, + arg.MarketID, + arg.Fi, + arg.Limit, + arg.Offset, + ) if err != nil { return nil, err } @@ -324,37 +331,15 @@ VALUES ( $13, $14, $15 - ) ON CONFLICT (market_id, name, handicap) DO + ) ON CONFLICT (event_id, market_id) DO UPDATE SET odds_value = EXCLUDED.odds_value, raw_odds = EXCLUDED.raw_odds, market_type = EXCLUDED.market_type, market_name = EXCLUDED.market_name, - event_id, - fi, - market_type, - market_name, - market_category, - market_id, - name, - handicap, - odds_value, - section, - category, - raw_odds, - is_active, - source, - fetched_at -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, - $8, $9, $10, $11, $12, $13, $14, $15 -) -ON CONFLICT (event_id, market_id) DO UPDATE SET - odds_value = EXCLUDED.odds_value, - raw_odds = EXCLUDED.raw_odds, - market_type = EXCLUDED.market_type, - market_name = EXCLUDED.market_name, market_category = EXCLUDED.market_category, + name = EXCLUDED.name, + handicap = EXCLUDED.handicap, fetched_at = EXCLUDED.fetched_at, is_active = EXCLUDED.is_active, source = EXCLUDED.source, diff --git a/internal/repository/odds.go b/internal/repository/odds.go index bb41387..72f2c93 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -14,242 +14,243 @@ import ( ) func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { - if len(m.Odds) == 0 { - return nil - } + if len(m.Odds) == 0 { + return nil + } - for _, raw := range m.Odds { - var item map[string]interface{} - if err := json.Unmarshal(raw, &item); err != nil { - continue - } + for _, raw := range m.Odds { + var item map[string]interface{} + if err := json.Unmarshal(raw, &item); err != nil { + continue + } - name := getString(item["name"]) - handicap := getString(item["handicap"]) - oddsVal := getFloat(item["odds"]) + name := getString(item["name"]) + handicap := getString(item["handicap"]) + oddsVal := getFloat(item["odds"]) - rawOddsBytes, _ := json.Marshal(m.Odds) + rawOddsBytes, _ := json.Marshal(m.Odds) - params := dbgen.InsertNonLiveOddParams{ - EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, - Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""}, - MarketType: m.MarketType, - MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""}, - MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""}, - MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""}, - Name: pgtype.Text{String: name, Valid: name != ""}, - Handicap: pgtype.Text{String: handicap, Valid: handicap != ""}, - OddsValue: pgtype.Float8{Float64: oddsVal, Valid: oddsVal != 0}, - Section: m.MarketCategory, - Category: pgtype.Text{Valid: false}, - RawOdds: rawOddsBytes, - IsActive: pgtype.Bool{Bool: true, Valid: true}, - Source: pgtype.Text{String: "b365api", Valid: true}, - FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, - } + params := dbgen.InsertNonLiveOddParams{ + EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""}, + Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""}, + MarketType: m.MarketType, + MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""}, + MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""}, + MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""}, + Name: pgtype.Text{String: name, Valid: name != ""}, + Handicap: pgtype.Text{String: handicap, Valid: handicap != ""}, + OddsValue: pgtype.Float8{Float64: oddsVal, Valid: oddsVal != 0}, + Section: m.MarketCategory, + Category: pgtype.Text{Valid: false}, + RawOdds: rawOddsBytes, + IsActive: pgtype.Bool{Bool: true, Valid: true}, + Source: pgtype.Text{String: "b365api", Valid: true}, + FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, + } - err := s.queries.InsertNonLiveOdd(ctx, params) - if err != nil { - _ = writeFailedMarketLog(m, err) - continue - } - } - return nil + err := s.queries.InsertNonLiveOdd(ctx, params) + if err != nil { + _ = writeFailedMarketLog(m, err) + continue + } + } + return nil } func writeFailedMarketLog(m domain.Market, err error) error { - logDir := "logs" - logFile := logDir + "/failed_markets.log" + logDir := "logs" + logFile := logDir + "/failed_markets.log" - if mkErr := os.MkdirAll(logDir, 0755); mkErr != nil { - return mkErr - } + if mkErr := os.MkdirAll(logDir, 0755); mkErr != nil { + return mkErr + } - f, fileErr := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if fileErr != nil { - return fileErr - } - defer f.Close() + f, fileErr := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if fileErr != nil { + return fileErr + } + defer f.Close() - entry := struct { - Time string `json:"time"` - Error string `json:"error"` - Record domain.Market `json:"record"` - }{ - Time: time.Now().Format(time.RFC3339), - Error: err.Error(), - Record: m, - } + entry := struct { + Time string `json:"time"` + Error string `json:"error"` + Record domain.Market `json:"record"` + }{ + Time: time.Now().Format(time.RFC3339), + Error: err.Error(), + Record: m, + } - jsonData, _ := json.MarshalIndent(entry, "", " ") - _, writeErr := f.WriteString(string(jsonData) + "\n\n") - return writeErr + jsonData, _ := json.MarshalIndent(entry, "", " ") + _, writeErr := f.WriteString(string(jsonData) + "\n\n") + return writeErr } func getString(v interface{}) string { - if s, ok := v.(string); ok { - return s - } - return "" + if s, ok := v.(string); ok { + return s + } + return "" } func getFloat(v interface{}) float64 { - if s, ok := v.(string); ok { - f, err := strconv.ParseFloat(s, 64) - if err == nil { - return f - } - } - return 0 + if s, ok := v.(string); ok { + f, err := strconv.ParseFloat(s, 64) + if err == nil { + return f + } + } + return 0 } func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { - odds, err := s.queries.GetPrematchOdds(ctx) - if err != nil { - return nil, err - } + odds, err := s.queries.GetPrematchOdds(ctx) + if err != nil { + return nil, err + } - domainOdds := make([]domain.Odd, len(odds)) - for i, odd := range odds { - domainOdds[i] = domain.Odd{ - EventID: odd.EventID.String, - Fi: odd.Fi.String, - MarketType: odd.MarketType, - MarketName: odd.MarketName.String, - MarketCategory: odd.MarketCategory.String, - MarketID: odd.MarketID.String, - Name: odd.Name.String, - Handicap: odd.Handicap.String, - OddsValue: odd.OddsValue.Float64, - Section: odd.Section, - Category: odd.Category.String, - RawOdds: func() []domain.RawMessage { - var rawOdds []domain.RawMessage - if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { - rawOdds = nil - } - return rawOdds - }(), - FetchedAt: odd.FetchedAt.Time, - Source: odd.Source.String, - IsActive: odd.IsActive.Bool, - } - } + domainOdds := make([]domain.Odd, len(odds)) + for i, odd := range odds { + domainOdds[i] = domain.Odd{ + EventID: odd.EventID.String, + Fi: odd.Fi.String, + MarketType: odd.MarketType, + MarketName: odd.MarketName.String, + MarketCategory: odd.MarketCategory.String, + MarketID: odd.MarketID.String, + Name: odd.Name.String, + Handicap: odd.Handicap.String, + OddsValue: odd.OddsValue.Float64, + Section: odd.Section, + Category: odd.Category.String, + RawOdds: func() []domain.RawMessage { + var rawOdds []domain.RawMessage + if err := json.Unmarshal(odd.RawOdds, &rawOdds); err != nil { + rawOdds = nil + } + return rawOdds + }(), + FetchedAt: odd.FetchedAt.Time, + Source: odd.Source.String, + IsActive: odd.IsActive.Bool, + } + } - return domainOdds, nil + return domainOdds, nil } func (s *Store) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) { - rows, err := s.queries.GetALLPrematchOdds(ctx) - if err != nil { - return nil, err - } + rows, err := s.queries.GetALLPrematchOdds(ctx) + if err != nil { + return nil, err + } - domainOdds := make([]domain.Odd, len(rows)) - for i, row := range rows { - domainOdds[i] = domain.Odd{ - // ID: int64(row.ID), - EventID: row.EventID.String, - Fi: row.Fi.String, - MarketType: row.MarketType, - MarketName: row.MarketName.String, - MarketCategory: row.MarketCategory.String, - MarketID: row.MarketID.String, - Name: row.Name.String, - Handicap: row.Handicap.String, - OddsValue: row.OddsValue.Float64, - Section: row.Section, - Category: row.Category.String, - RawOdds: func() []domain.RawMessage { - var rawOdds []domain.RawMessage - if err := json.Unmarshal(row.RawOdds, &rawOdds); err != nil { - rawOdds = nil - } - return rawOdds - }(), - FetchedAt: row.FetchedAt.Time, - Source: row.Source.String, - IsActive: row.IsActive.Bool, - } - } + domainOdds := make([]domain.Odd, len(rows)) + for i, row := range rows { + domainOdds[i] = domain.Odd{ + // ID: int64(row.ID), + EventID: row.EventID.String, + Fi: row.Fi.String, + MarketType: row.MarketType, + MarketName: row.MarketName.String, + MarketCategory: row.MarketCategory.String, + MarketID: row.MarketID.String, + Name: row.Name.String, + Handicap: row.Handicap.String, + OddsValue: row.OddsValue.Float64, + Section: row.Section, + Category: row.Category.String, + RawOdds: func() []domain.RawMessage { + var rawOdds []domain.RawMessage + if err := json.Unmarshal(row.RawOdds, &rawOdds); err != nil { + rawOdds = nil + } + return rawOdds + }(), + FetchedAt: row.FetchedAt.Time, + Source: row.Source.String, + IsActive: row.IsActive.Bool, + } + } - return domainOdds, nil + return domainOdds, nil } -func (s *Store) GetRawOddsByMarketID(ctx context.Context, rawOddsID string) (domain.RawOddsByMarketID, error) { - params := dbgen.GetRawOddsByMarketIDParams{ - MarketID: pgtype.Text{String: rawOddsID, Valid: true}, - Limit: 1, - Offset: 0, - } +func (s *Store) GetRawOddsByMarketID(ctx context.Context, rawOddsID string, upcomingID string) (domain.RawOddsByMarketID, error) { + params := dbgen.GetRawOddsByMarketIDParams{ + MarketID: pgtype.Text{String: rawOddsID, Valid: true}, + Fi: pgtype.Text{String: upcomingID, Valid: true}, + Limit: 1, + Offset: 0, + } - rows, err := s.queries.GetRawOddsByMarketID(ctx, params) - if err != nil { - return domain.RawOddsByMarketID{}, err - } + rows, err := s.queries.GetRawOddsByMarketID(ctx, params) + if err != nil { + return domain.RawOddsByMarketID{}, err + } - if len(rows) == 0 { - return domain.RawOddsByMarketID{}, fmt.Errorf("no raw odds found for market_id: %s", rawOddsID) - } + if len(rows) == 0 { + return domain.RawOddsByMarketID{}, fmt.Errorf("no raw odds found for market_id: %s", rawOddsID) + } - row := rows[0] + row := rows[0] - var rawOdds []json.RawMessage - if err := json.Unmarshal(row.RawOdds, &rawOdds); err != nil { - return domain.RawOddsByMarketID{}, err - } + var rawOdds []json.RawMessage + if err := json.Unmarshal(row.RawOdds, &rawOdds); err != nil { + return domain.RawOddsByMarketID{}, err + } - return domain.RawOddsByMarketID{ - ID: int64(row.ID), - RawOdds: func() []domain.RawMessage { - converted := make([]domain.RawMessage, len(rawOdds)) - for i, r := range rawOdds { - converted[i] = domain.RawMessage(r) - } - return converted - }(), - FetchedAt: row.FetchedAt.Time, - }, nil + return domain.RawOddsByMarketID{ + ID: int64(row.ID), + RawOdds: func() []domain.RawMessage { + converted := make([]domain.RawMessage, len(rawOdds)) + for i, r := range rawOdds { + converted[i] = domain.RawMessage(r) + } + return converted + }(), + FetchedAt: row.FetchedAt.Time, + }, nil } func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) { - params := dbgen.GetPrematchOddsByUpcomingIDParams{ - ID: upcomingID, - Limit: limit, - Offset: offset, - } + params := dbgen.GetPrematchOddsByUpcomingIDParams{ + ID: upcomingID, + Limit: limit, + Offset: offset, + } - odds, err := s.queries.GetPrematchOddsByUpcomingID(ctx, params) - if err != nil { - return nil, err - } + odds, err := s.queries.GetPrematchOddsByUpcomingID(ctx, params) + 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 - } + // 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 + } - domainOdds[i] = domain.Odd{ - EventID: odd.EventID.String, - Fi: odd.Fi.String, - MarketType: odd.MarketType, - MarketName: odd.MarketName.String, - MarketCategory: odd.MarketCategory.String, - MarketID: odd.MarketID.String, - Name: odd.Name.String, - Handicap: odd.Handicap.String, - OddsValue: odd.OddsValue.Float64, - Section: odd.Section, - Category: odd.Category.String, - RawOdds: rawOdds, - FetchedAt: odd.FetchedAt.Time, - Source: odd.Source.String, - IsActive: odd.IsActive.Bool, - } - } + domainOdds[i] = domain.Odd{ + EventID: odd.EventID.String, + Fi: odd.Fi.String, + MarketType: odd.MarketType, + MarketName: odd.MarketName.String, + MarketCategory: odd.MarketCategory.String, + MarketID: odd.MarketID.String, + Name: odd.Name.String, + Handicap: odd.Handicap.String, + OddsValue: odd.OddsValue.Float64, + Section: odd.Section, + Category: odd.Category.String, + RawOdds: rawOdds, + FetchedAt: odd.FetchedAt.Time, + Source: odd.Source.String, + IsActive: odd.IsActive.Bool, + } + } - return domainOdds, nil -} \ No newline at end of file + return domainOdds, nil +} diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index 2472e99..eb3d3e6 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -10,7 +10,5 @@ type Service interface { FetchNonLiveOdds(ctx context.Context) error GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) - GetRawOddsByMarketID(ctx context.Context, marketID string) ([]domain.RawOddsByMarketID, error) - - + GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) ([]domain.RawOddsByMarketID, error) } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index d8bce34..2ae8e4d 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -22,7 +22,6 @@ func New(token string, store *repository.Store) *ServiceImpl { return &ServiceImpl{token: token, store: store} } - func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { eventIDs, err := s.store.GetAllUpcomingEvents(ctx) if err != nil { @@ -30,43 +29,43 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { return err } - for _, event := range eventIDs { - eventID := event.ID - prematchURL := "https://api.b365api.com/v3/bet365/prematch?token=" + s.token + "&FI=" + eventID - log.Printf("📡 Fetching prematch odds for event ID: %s", eventID) - resp, err := http.Get(prematchURL) - if err != nil { - log.Printf("❌ Failed to fetch prematch odds for event %s: %v", eventID, err) - continue - } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - var oddsData struct { - Success int `json:"success"` - Results []struct { - EventID string `json:"event_id"` - FI string `json:"FI"` - Main OddsSection `json:"main"` - } `json:"results"` - } - if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { - log.Printf("❌ Invalid prematch data for event %s", eventID) - continue - } - - result := oddsData.Results[0] - finalID := result.EventID - if finalID == "" { - finalID = result.FI - } - if finalID == "" { - log.Printf("⚠️ Skipping event %s with no valid ID", eventID) - continue - } - - s.storeSection(ctx, finalID, result.FI, "main", result.Main) - } + for _, event := range eventIDs { + eventID := event.ID + prematchURL := "https://api.b365api.com/v3/bet365/prematch?token=" + s.token + "&FI=" + eventID + log.Printf("📡 Fetching prematch odds for event ID: %s", eventID) + resp, err := http.Get(prematchURL) + if err != nil { + log.Printf("❌ Failed to fetch prematch odds for event %s: %v", eventID, err) + continue + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + var oddsData struct { + Success int `json:"success"` + Results []struct { + EventID string `json:"event_id"` + FI string `json:"FI"` + Main OddsSection `json:"main"` + } `json:"results"` + } + if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { + log.Printf("❌ Invalid prematch data for event %s", eventID) + continue + } + + result := oddsData.Results[0] + finalID := result.EventID + if finalID == "" { + finalID = result.FI + } + if finalID == "" { + log.Printf("⚠️ Skipping event %s with no valid ID", eventID) + continue + } + + s.storeSection(ctx, finalID, result.FI, "main", result.Main) + } return nil } @@ -108,12 +107,10 @@ type OddsMarket struct { } type OddsSection struct { - UpdatedAt string `json:"updated_at"` + UpdatedAt string `json:"updated_at"` Sp map[string]OddsMarket `json:"sp"` } - - func (s *ServiceImpl) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { return s.store.GetPrematchOdds(ctx, eventID) } @@ -122,8 +119,8 @@ func (s *ServiceImpl) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, err return s.store.GetALLPrematchOdds(ctx) } -func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string) ([]domain.RawOddsByMarketID, error) { - rows, err := s.store.GetRawOddsByMarketID(ctx, marketID) +func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) ([]domain.RawOddsByMarketID, error) { + rows, err := s.store.GetRawOddsByMarketID(ctx, marketID, upcomingID) if err != nil { return nil, err } @@ -132,5 +129,5 @@ func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string) } func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) { - return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) + return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) } diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 2ee4e69..0eeb4ac 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -1,61 +1,58 @@ package httpserver import ( - // "context" - "log" + // "context" - eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" - oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" - "github.com/robfig/cron/v3" + "log" + + eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/robfig/cron/v3" ) func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service) { - c := cron.New(cron.WithSeconds()) + c := cron.New(cron.WithSeconds()) - schedule := []struct { - spec string - task func() - }{ + schedule := []struct { + spec string + task func() + }{ - // { - // spec: "*/30 * * * * *", // Every 30 seconds - // task: func() { - // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - // log.Printf("FetchUpcomingEvents error: %v", err) - // } - // }, - // }, - - - // { - // spec: "*/5 * * * * *", // Every 5 seconds - // task: func() { - // if err := eventService.FetchLiveEvents(context.Background()); err != nil { - // log.Printf("FetchLiveEvents error: %v", err) - // } - // }, - // }, + // { + // spec: "*/30 * * * * *", // Every 30 seconds + // task: func() { + // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + // log.Printf("FetchUpcomingEvents error: %v", err) + // } + // }, + // }, + // { + // spec: "*/5 * * * * *", // Every 5 seconds + // task: func() { + // if err := eventService.FetchLiveEvents(context.Background()); err != nil { + // log.Printf("FetchLiveEvents error: %v", err) + // } + // }, + // }, - // { - // spec: "*/5 * * * * *", // Every 5 seconds - // task: func() { - // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - // log.Printf("FetchNonLiveOdds error: %v", err) - // } - // }, - // }, - - - } + // { + // spec: "*/5 * * * * *", // Every 5 seconds + // task: func() { + // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + // log.Printf("FetchNonLiveOdds error: %v", err) + // } + // }, + // }, + } - - for _, job := range schedule { - if _, err := c.AddFunc(job.spec, job.task); err != nil { - log.Fatalf("Failed to schedule cron job: %v", err) - } - } + 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) + } + } - c.Start() - log.Println("Cron jobs started for event and odds services") -} \ No newline at end of file + c.Start() + log.Println("Cron jobs started for event and odds services") +} diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index ea1ac59..efa3b00 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -22,21 +22,22 @@ import ( // @Failure 500 {object} response.APIResponse // @Router /prematch/odds/{event_id} [get] func GetPrematchOdds(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { - return func(c *fiber.Ctx) error { - eventID := c.Params("event_id") - if eventID == "" { - return response.WriteJSON(c, fiber.StatusBadRequest, "Missing event_id", nil, nil) - } + return func(c *fiber.Ctx) error { + eventID := c.Params("event_id") + if eventID == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing event_id", nil, nil) + } - odds, err := prematchSvc.GetPrematchOdds(c.Context(), eventID) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve odds", nil, nil) - } + odds, err := prematchSvc.GetPrematchOdds(c.Context(), eventID) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve odds", nil, nil) + } - return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) - } + return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) + } } -//GetALLPrematchOdds + +// GetALLPrematchOdds // @Summary Retrieve all prematch odds // @Description Retrieve all prematch odds from the database // @Tags prematch @@ -46,41 +47,48 @@ func GetPrematchOdds(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.H // @Failure 500 {object} response.APIResponse // @Router /prematch/odds [get] func GetALLPrematchOdds(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { - return func(c *fiber.Ctx) error { - odds, err := prematchSvc.GetALLPrematchOdds(c.Context()) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve all prematch odds", nil, nil) - } + return func(c *fiber.Ctx) error { + odds, err := prematchSvc.GetALLPrematchOdds(c.Context()) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve all prematch odds", nil, nil) + } - return response.WriteJSON(c, fiber.StatusOK, "All prematch odds retrieved successfully", odds, nil) - } + return response.WriteJSON(c, fiber.StatusOK, "All prematch odds retrieved successfully", odds, nil) + } } + // GetRawOddsByMarketID // @Summary Retrieve raw odds by Market ID // @Description Retrieve raw odds records using a Market ID // @Tags prematch // @Accept json // @Produce json +// @Param upcoming_id path string true "Upcoming ID" // @Param market_id path string true "Market ID" // @Success 200 {array} domain.RawOddsByMarketID // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /prematch/odds/raw/{market_id} [get] +// @Router /prematch/odds/upcoming/{upcoming_id}/market/{market_id} [get] func GetRawOddsByMarketID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { - return func(c *fiber.Ctx) error { - marketID := c.Params("market_id") - if marketID == "" { - return response.WriteJSON(c, fiber.StatusBadRequest, "Missing market_id", nil, nil) - } + return func(c *fiber.Ctx) error { + marketID := c.Params("market_id") + upcomingID := c.Params("upcoming_id") + if marketID == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing market_id", nil, nil) + } - rawOdds, err := prematchSvc.GetRawOddsByMarketID(c.Context(), marketID) - if err != nil { - logger.Error("failed to fetch raw odds", "error", err) - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve raw odds", nil, nil) - } + if upcomingID == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing upcoming_id", nil, nil) + } - return response.WriteJSON(c, fiber.StatusOK, "Raw odds retrieved successfully", rawOdds, nil) - } + rawOdds, err := prematchSvc.GetRawOddsByMarketID(c.Context(), marketID, upcomingID) + if err != nil { + logger.Error("failed to fetch raw odds", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve raw odds", err, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Raw odds retrieved successfully", rawOdds, nil) + } } // @Summary Retrieve all upcoming events @@ -92,15 +100,16 @@ func GetRawOddsByMarketID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fi // @Failure 500 {object} response.APIResponse // @Router /prematch/events [get] func GetAllUpcomingEvents(logger *slog.Logger, eventSvc event.Service) fiber.Handler { - return func(c *fiber.Ctx) error { - events, err := eventSvc.GetAllUpcomingEvents(c.Context()) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve all upcoming events", nil, nil) - } + return func(c *fiber.Ctx) error { + events, err := eventSvc.GetAllUpcomingEvents(c.Context()) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve all upcoming events", nil, nil) + } - return response.WriteJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", events, nil) - } + return response.WriteJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", events, nil) + } } + // @Summary Retrieve an upcoming by ID // @Description Retrieve an upcoming event by ID // @Tags prematch @@ -112,20 +121,21 @@ func GetAllUpcomingEvents(logger *slog.Logger, eventSvc event.Service) fiber.Han // @Failure 500 {object} response.APIResponse // @Router /prematch/events/{id} [get] func GetUpcomingEventByID(logger *slog.Logger, eventSvc event.Service) fiber.Handler { - return func(c *fiber.Ctx) error { - id := c.Params("id") - if id == "" { - return response.WriteJSON(c, fiber.StatusBadRequest, "Missing id", nil, nil) - } + return func(c *fiber.Ctx) error { + id := c.Params("id") + if id == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing id", nil, nil) + } - event, err := eventSvc.GetUpcomingEventByID(c.Context(), id) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve upcoming event", nil, nil) - } + event, err := eventSvc.GetUpcomingEventByID(c.Context(), id) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve upcoming event", nil, nil) + } - return response.WriteJSON(c, fiber.StatusOK, "Upcoming event retrieved successfully", event, nil) - } + return response.WriteJSON(c, fiber.StatusOK, "Upcoming event retrieved successfully", event, nil) + } } + // @Summary Retrieve prematch odds by upcoming ID (FI) // @Description Retrieve prematch odds by upcoming event ID (FI from Bet365) with optional pagination // @Tags prematch @@ -139,28 +149,27 @@ func GetUpcomingEventByID(logger *slog.Logger, eventSvc event.Service) fiber.Han // @Failure 500 {object} response.APIResponse // @Router /prematch/odds/upcoming/{upcoming_id} [get] func GetPrematchOddsByUpcomingID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Handler { - return func(c *fiber.Ctx) error { - upcomingID := c.Params("upcoming_id") - if upcomingID == "" { - return response.WriteJSON(c, fiber.StatusBadRequest, "Missing upcoming_id", nil, nil) - } + return func(c *fiber.Ctx) error { + upcomingID := c.Params("upcoming_id") + if upcomingID == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing upcoming_id", nil, nil) + } - limit, err := strconv.Atoi(c.Query("limit", "10")) // Default limit is 10 - if err != nil || limit <= 0 { - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid limit value", nil, nil) - } + limit, err := strconv.Atoi(c.Query("limit", "10")) // Default limit is 10 + if err != nil || limit <= 0 { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid limit value", nil, nil) + } - offset, err := strconv.Atoi(c.Query("offset", "0")) // Default offset is 0 - if err != nil || offset < 0 { - return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid offset value", nil, nil) - } + offset, err := strconv.Atoi(c.Query("offset", "0")) // Default offset is 0 + if err != nil || offset < 0 { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid offset value", nil, nil) + } - odds, err := prematchSvc.GetPrematchOddsByUpcomingID(c.Context(), upcomingID, int32(limit), int32(offset)) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve prematch odds", nil, nil) - } + odds, err := prematchSvc.GetPrematchOddsByUpcomingID(c.Context(), upcomingID, int32(limit), int32(offset)) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve prematch odds", nil, nil) + } - return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) - } + return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) + } } - diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index c8d75a5..7e00e4c 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -59,7 +59,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/prematch/odds/:event_id", handlers.GetPrematchOdds(a.logger, a.prematchSvc)) a.fiber.Get("/prematch/odds", handlers.GetALLPrematchOdds(a.logger, a.prematchSvc)) - a.fiber.Get("/prematch/odds/raw/:market_id", handlers.GetRawOddsByMarketID(a.logger, a.prematchSvc)) + a.fiber.Get("/prematch/odds/upcoming/:upcoming_id/market/:market_id", handlers.GetRawOddsByMarketID(a.logger, a.prematchSvc)) a.fiber.Get("/prematch/events/:id", handlers.GetUpcomingEventByID(a.logger, a.eventSvc)) a.fiber.Get("/prematch/events", handlers.GetAllUpcomingEvents(a.logger, a.eventSvc)) From 44b23855656e214fbe128c232eaceebfcd17c9af Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 14 Apr 2025 19:50:08 +0300 Subject: [PATCH 24/30] - --- docs/docs.go | 5 ++++- docs/swagger.json | 5 ++++- docs/swagger.yaml | 4 +++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 919bb51..3295bd1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3352,7 +3352,6 @@ const docTemplate = `{ "example": 100 }, "bank_code": { - "description": "Payment Details for bank", "type": "string" }, "beneficiary_name": { @@ -3362,6 +3361,10 @@ const docTemplate = `{ "type": "integer", "example": 1 }, + "cashout_id": { + "type": "string", + "example": "191212" + }, "full_name": { "type": "string", "example": "John Smith" diff --git a/docs/swagger.json b/docs/swagger.json index 5ea7099..83760ec 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3344,7 +3344,6 @@ "example": 100 }, "bank_code": { - "description": "Payment Details for bank", "type": "string" }, "beneficiary_name": { @@ -3354,6 +3353,10 @@ "type": "integer", "example": 1 }, + "cashout_id": { + "type": "string", + "example": "191212" + }, "full_name": { "type": "string", "example": "John Smith" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f03a301..d52dc80 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -518,13 +518,15 @@ definitions: example: 100 type: number bank_code: - description: Payment Details for bank type: string beneficiary_name: type: string bet_id: example: 1 type: integer + cashout_id: + example: "191212" + type: string full_name: example: John Smith type: string From 4f7302cb643a6182755249f022f9b3b2b9c72099 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 14 Apr 2025 20:20:31 +0300 Subject: [PATCH 25/30] - --- db/migrations/000001_fortune.up.sql | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index ef85495..4fb809e 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -56,8 +56,7 @@ CREATE TABLE IF NOT EXISTS bets ( CHECK ( user_id IS NOT NULL OR branch_id IS NOT NULL - ), - UNIQUE(cashier_id) + ) ); CREATE TABLE IF NOT EXISTS tickets ( id BIGSERIAL PRIMARY KEY, From 3f42d7e5c77338a19da73b052e44baafdecbbe1a Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 14 Apr 2025 20:25:59 +0300 Subject: [PATCH 26/30] cron uncommented --- internal/web_server/cron.go | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 0eeb4ac..5aaa06d 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -3,6 +3,7 @@ package httpserver import ( // "context" + "context" "log" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" @@ -18,14 +19,14 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S task func() }{ - // { - // spec: "*/30 * * * * *", // Every 30 seconds - // task: func() { - // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - // log.Printf("FetchUpcomingEvents error: %v", err) - // } - // }, - // }, + { + spec: "*/30 * * * * *", // Every 30 seconds + task: func() { + if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + log.Printf("FetchUpcomingEvents error: %v", err) + } + }, + }, // { // spec: "*/5 * * * * *", // Every 5 seconds @@ -36,14 +37,14 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S // }, // }, - // { - // spec: "*/5 * * * * *", // Every 5 seconds - // task: func() { - // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - // log.Printf("FetchNonLiveOdds error: %v", err) - // } - // }, - // }, + { + spec: "*/5 * * * * *", // Every 5 seconds + task: func() { + if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + log.Printf("FetchNonLiveOdds error: %v", err) + } + }, + }, } for _, job := range schedule { From 5d2acd4645cae68b4487cfafc7b335fbf0275ff6 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 14 Apr 2025 21:28:52 +0300 Subject: [PATCH 27/30] small fixes --- .gitignore | 1 + db/migrations/000001_fortune.up.sql | 3 ++- internal/web_server/cron.go | 5 +++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index bdc8b3e..660eeeb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ coverage tmp build *.log +db.sql \ No newline at end of file diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 4fb809e..7f5896f 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -231,7 +231,8 @@ CREATE TABLE odds ( source TEXT DEFAULT 'b365api', is_active BOOLEAN DEFAULT true, UNIQUE (market_id, name, handicap), - UNIQUE (event_id, market_id, name, handicap) + UNIQUE (event_id, market_id, name, handicap), + UNIQUE (event_id, market_id) ); ALTER TABLE refresh_tokens ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id); diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 5aaa06d..7cf442f 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -20,7 +20,8 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }{ { - spec: "*/30 * * * * *", // Every 30 seconds + + spec: "0 0 * * * *", // Every hour task: func() { if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { log.Printf("FetchUpcomingEvents error: %v", err) @@ -38,7 +39,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S // }, { - spec: "*/5 * * * * *", // Every 5 seconds + spec: "0 */15 * * * *", // Every 15 minutes task: func() { if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { log.Printf("FetchNonLiveOdds error: %v", err) From cab7dbe2fac76647750eb538418a259085ea3174 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sat, 19 Apr 2025 21:34:06 +0300 Subject: [PATCH 28/30] - --- internal/web_server/cron.go | 36 ++++++++++------------ internal/web_server/routes.go | 58 +++++++++++++++++------------------ 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 7cf442f..ea15153 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -3,7 +3,6 @@ package httpserver import ( // "context" - "context" "log" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" @@ -19,15 +18,15 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S task func() }{ - { - - spec: "0 0 * * * *", // Every hour - task: func() { - if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - log.Printf("FetchUpcomingEvents error: %v", err) - } - }, - }, + // { + + // spec: "0 0 * * * *", // Every hour + // task: func() { + // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + // log.Printf("FetchUpcomingEvents error: %v", err) + // } + // }, + // }, // { // spec: "*/5 * * * * *", // Every 5 seconds @@ -38,18 +37,17 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S // }, // }, - { - 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 * * * *", // Every 15 minutes + // task: func() { + // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + // log.Printf("FetchNonLiveOdds error: %v", err) + // } + // }, + // }, } 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) } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 7547aa9..c59a79a 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -41,21 +41,21 @@ func (a *App) initAppRoutes() { a.fiber.Get("/user/profile", a.authMiddleware, handlers.UserProfile(a.logger, a.userSvc)) // //, a.authMiddleware - a.fiber.Get("/cashiers", handlers.GetAllCashiers(a.logger, a.userSvc, a.validator)) - a.fiber.Post("/cashiers", handlers.CreateCashier(a.logger, a.userSvc, a.branchSvc, a.validator)) - a.fiber.Put("/cashiers/:id", handlers.UpdateCashier(a.logger, a.userSvc, a.validator)) + a.fiber.Get("/cashiers", a.authMiddleware, handlers.GetAllCashiers(a.logger, a.userSvc, a.validator)) + a.fiber.Post("/cashiers", a.authMiddleware, handlers.CreateCashier(a.logger, a.userSvc, a.branchSvc, a.validator)) + a.fiber.Put("/cashiers/:id", a.authMiddleware, handlers.UpdateCashier(a.logger, a.userSvc, a.validator)) // - a.fiber.Get("/managers", handlers.GetAllManagers(a.logger, a.userSvc, a.validator)) - a.fiber.Post("/managers", handlers.CreateManager(a.logger, a.userSvc, a.validator)) - a.fiber.Put("/managers/:id", handlers.UPdateManagers(a.logger, a.userSvc, a.validator)) + a.fiber.Get("/managers", a.authMiddleware, handlers.GetAllManagers(a.logger, a.userSvc, a.validator)) + a.fiber.Post("/managers", a.authMiddleware, handlers.CreateManager(a.logger, a.userSvc, a.validator)) + a.fiber.Put("/managers/:id", a.authMiddleware, handlers.UPdateManagers(a.logger, a.userSvc, a.validator)) // a.fiber.Get("/user/wallet", a.authMiddleware, handlers.GetCustomerWallet(a.logger, a.walletSvc, a.validator)) - a.fiber.Post("/user/search", handlers.SearchUserByNameOrPhone(a.logger, a.userSvc, a.validator)) + a.fiber.Post("/user/search", a.authMiddleware, handlers.SearchUserByNameOrPhone(a.logger, a.userSvc, a.validator)) - a.fiber.Get("/manager/:id/branch", handlers.GetBranchByManagerID(a.logger, a.branchSvc, a.validator)) + a.fiber.Get("/manager/:id/branch", a.authMiddleware, handlers.GetBranchByManagerID(a.logger, a.branchSvc, a.validator)) - a.fiber.Get("/company/:id/branch", handlers.GetBranchByCompanyID(a.logger, a.branchSvc, a.validator)) + a.fiber.Get("/company/:id/branch", a.authMiddleware, handlers.GetBranchByCompanyID(a.logger, a.branchSvc, a.validator)) a.fiber.Get("/prematch/odds/:event_id", handlers.GetPrematchOdds(a.logger, a.prematchSvc)) a.fiber.Get("/prematch/odds", handlers.GetALLPrematchOdds(a.logger, a.prematchSvc)) @@ -68,22 +68,22 @@ func (a *App) initAppRoutes() { a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) // Branch - a.fiber.Post("/branch", handlers.CreateBranch(a.logger, a.branchSvc, a.walletSvc, a.validator)) - a.fiber.Get("/branch", handlers.GetAllBranches(a.logger, a.branchSvc, a.validator)) - a.fiber.Get("/branch/:id", handlers.GetBranchByID(a.logger, a.branchSvc, a.validator)) - a.fiber.Get("/branch/:id/bets", handlers.GetBetByBranchID(a.logger, a.betSvc, a.validator)) - a.fiber.Put("/branch/:id", handlers.UpdateBranch(a.logger, a.branchSvc, a.validator)) - a.fiber.Delete("/branch/:id", handlers.DeleteBranch(a.logger, a.branchSvc, a.validator)) + a.fiber.Post("/branch", a.authMiddleware, handlers.CreateBranch(a.logger, a.branchSvc, a.walletSvc, a.validator)) + a.fiber.Get("/branch", a.authMiddleware, handlers.GetAllBranches(a.logger, a.branchSvc, a.validator)) + a.fiber.Get("/branch/:id", a.authMiddleware, handlers.GetBranchByID(a.logger, a.branchSvc, a.validator)) + a.fiber.Get("/branch/:id/bets", a.authMiddleware, handlers.GetBetByBranchID(a.logger, a.betSvc, a.validator)) + a.fiber.Put("/branch/:id", a.authMiddleware, handlers.UpdateBranch(a.logger, a.branchSvc, a.validator)) + a.fiber.Delete("/branch/:id", a.authMiddleware, handlers.DeleteBranch(a.logger, a.branchSvc, a.validator)) a.fiber.Get("/search/branch", a.authMiddleware, handlers.SearchBranch(a.logger, a.branchSvc, a.validator)) // /branch/search // branch/wallet // Branch Operation - a.fiber.Get("/supportedOperation", handlers.GetAllSupportedOperations(a.logger, a.branchSvc, a.validator)) - a.fiber.Post("/supportedOperation", handlers.CreateSupportedOperation(a.logger, a.branchSvc, a.validator)) - a.fiber.Post("/operation", handlers.CreateBranchOperation(a.logger, a.branchSvc, a.validator)) - a.fiber.Get("/branch/:id/operation", handlers.GetBranchOperations(a.logger, a.branchSvc, a.validator)) - a.fiber.Delete("/branch/:id/operation/:opID", handlers.DeleteBranchOperation(a.logger, a.branchSvc, a.validator)) + a.fiber.Get("/supportedOperation", a.authMiddleware, handlers.GetAllSupportedOperations(a.logger, a.branchSvc, a.validator)) + a.fiber.Post("/supportedOperation", a.authMiddleware, handlers.CreateSupportedOperation(a.logger, a.branchSvc, a.validator)) + a.fiber.Post("/operation", a.authMiddleware, handlers.CreateBranchOperation(a.logger, a.branchSvc, a.validator)) + a.fiber.Get("/branch/:id/operation", a.authMiddleware, handlers.GetBranchOperations(a.logger, a.branchSvc, a.validator)) + a.fiber.Delete("/branch/:id/operation/:opID", a.authMiddleware, handlers.DeleteBranchOperation(a.logger, a.branchSvc, a.validator)) // Ticket a.fiber.Post("/ticket", handlers.CreateTicket(a.logger, a.ticketSvc, a.validator)) @@ -92,17 +92,17 @@ func (a *App) initAppRoutes() { // Bet a.fiber.Post("/bet", a.authMiddleware, handlers.CreateBet(a.logger, a.betSvc, a.userSvc, a.branchSvc, a.walletSvc, a.validator)) - a.fiber.Get("/bet", handlers.GetAllBet(a.logger, a.betSvc, a.validator)) - a.fiber.Get("/bet/:id", handlers.GetBetByID(a.logger, a.betSvc, a.validator)) - a.fiber.Get("/bet/cashout/:id", handlers.GetBetByCashoutID(a.logger, a.betSvc, a.validator)) - a.fiber.Patch("/bet/:id", handlers.UpdateCashOut(a.logger, a.betSvc, a.validator)) - a.fiber.Delete("/bet/:id", handlers.DeleteBet(a.logger, a.betSvc, a.validator)) + a.fiber.Get("/bet", a.authMiddleware, handlers.GetAllBet(a.logger, a.betSvc, a.validator)) + a.fiber.Get("/bet/:id", a.authMiddleware, handlers.GetBetByID(a.logger, a.betSvc, a.validator)) + a.fiber.Get("/bet/cashout/:id", a.authMiddleware, handlers.GetBetByCashoutID(a.logger, a.betSvc, a.validator)) + a.fiber.Patch("/bet/:id", a.authMiddleware, handlers.UpdateCashOut(a.logger, a.betSvc, a.validator)) + a.fiber.Delete("/bet/:id", a.authMiddleware, handlers.DeleteBet(a.logger, a.betSvc, a.validator)) // Wallet - a.fiber.Get("/wallet", handlers.GetAllWallets(a.logger, a.walletSvc, a.validator)) - a.fiber.Get("/wallet/:id", handlers.GetWalletByID(a.logger, a.walletSvc, a.validator)) - a.fiber.Put("/wallet/:id", handlers.UpdateWalletActive(a.logger, a.walletSvc, a.validator)) - a.fiber.Get("/branchWallet", handlers.GetAllBranchWallets(a.logger, a.walletSvc, a.validator)) + a.fiber.Get("/wallet", a.authMiddleware, handlers.GetAllWallets(a.logger, a.walletSvc, a.validator)) + a.fiber.Get("/wallet/:id", a.authMiddleware, handlers.GetWalletByID(a.logger, a.walletSvc, a.validator)) + a.fiber.Put("/wallet/:id", a.authMiddleware, handlers.UpdateWalletActive(a.logger, a.walletSvc, a.validator)) + a.fiber.Get("/branchWallet", a.authMiddleware, handlers.GetAllBranchWallets(a.logger, a.walletSvc, a.validator)) // Transfer // /transfer/wallet - transfer from one wallet to another wallet From 991199c3dcf0c703f65071e316bbe60372f29d9b Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 22 Apr 2025 03:20:52 +0300 Subject: [PATCH 29/30] events pagination + ticket and bet validation --- db/migrations/000001_fortune.up.sql | 32 +++ db/query/bet.sql | 9 +- db/query/events.sql | 154 +++++++++--- db/query/odds.sql | 8 +- db/query/ticket.sql | 17 +- docs/docs.go | 100 ++++---- docs/swagger.json | 100 ++++---- docs/swagger.yaml | 73 +++--- gen/db/bet.sql.go | 19 ++ gen/db/copyfrom.go | 8 +- gen/db/events.sql.go | 221 +++++++++++++++--- gen/db/models.go | 4 + gen/db/odds.sql.go | 47 ++-- gen/db/ticket.sql.go | 6 +- internal/domain/bet.go | 27 ++- internal/domain/odds.go | 66 +++--- internal/domain/ticket.go | 6 +- internal/repository/bet.go | 12 + internal/repository/event.go | 38 ++- internal/repository/odds.go | 19 +- internal/repository/ticket.go | 4 + internal/services/bet/port.go | 1 + internal/services/bet/service.go | 4 + internal/services/event/port.go | 1 + internal/services/event/service.go | 12 +- internal/services/odds/port.go | 2 +- internal/services/odds/service.go | 6 +- internal/web_server/cron.go | 6 +- internal/web_server/handlers/bet_handler.go | 105 +++++++-- internal/web_server/handlers/prematch.go | 9 +- .../web_server/handlers/ticket_handler.go | 119 +++++++--- internal/web_server/response/res.go | 18 +- internal/web_server/routes.go | 4 +- 33 files changed, 873 insertions(+), 384 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 7f5896f..e24ba3f 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -76,6 +76,8 @@ CREATE TABLE IF NOT EXISTS bet_outcomes ( market_name VARCHAR(255) NOT NULL, odd REAL NOT NULL, odd_name VARCHAR(255) NOT NULL, + odd_header VARCHAR(255) NOT NULL, + odd_handicap VARCHAR(255) NOT NULL, expires TIMESTAMP NOT NULL ); CREATE TABLE IF NOT EXISTS ticket_outcomes ( @@ -89,6 +91,8 @@ CREATE TABLE IF NOT EXISTS ticket_outcomes ( market_name VARCHAR(255) NOT NULL, odd REAL NOT NULL, odd_name VARCHAR(255) NOT NULL, + odd_header VARCHAR(255) NOT NULL, + odd_handicap VARCHAR(255) NOT NULL, expires TIMESTAMP NOT NULL ); CREATE VIEW bet_with_outcomes AS @@ -321,6 +325,34 @@ VALUES ( 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 ( + 'Kirubel', + 'Kibru', + 'kirubeljkl679 @gmail.com', + NULL, + crypt('password@123', gen_salt('bf'))::bytea, + 'super_admin', + TRUE, + FALSE, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP, + NULL, + FALSE + ); INSERT INTO supported_operations (name, description) VALUES ('SportBook', 'Sportbook operations'), ('Virtual', 'Virtual operations'), diff --git a/db/query/bet.sql b/db/query/bet.sql index bc37717..c4eb124 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -23,9 +23,11 @@ INSERT INTO bet_outcomes ( market_name, odd, odd_name, + odd_header, + odd_handicap, expires ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10); +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12); -- name: GetAllBets :many SELECT * FROM bet_with_outcomes; @@ -46,6 +48,11 @@ UPDATE bets SET cashed_out = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1; +-- name: UpdateStatus :exec +UPDATE bets +SET status = $2, + updated_at = CURRENT_TIMESTAMP +WHERE id = $1; -- name: DeleteBet :exec DELETE FROM bets WHERE id = $1; diff --git a/db/query/events.sql b/db/query/events.sql index 66a28cc..61dbdbb 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -1,19 +1,50 @@ -- name: InsertEvent :exec INSERT INTO events ( - id, sport_id, match_name, home_team, away_team, - home_team_id, away_team_id, home_kit_image, away_kit_image, - league_id, league_name, league_cc, start_time, score, - match_minute, timer_status, added_time, match_period, - is_live, status -) VALUES ( - $1, $2, $3, $4, $5, - $6, $7, $8, $9, - $10, $11, $12, $13, $14, - $15, $16, $17, $18, - $19, $20 -) -ON CONFLICT (id) DO UPDATE SET - sport_id = EXCLUDED.sport_id, + id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + score, + match_minute, + timer_status, + added_time, + match_period, + is_live, + status + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15, + $16, + $17, + $18, + $19, + $20 + ) ON CONFLICT (id) DO +UPDATE +SET sport_id = EXCLUDED.sport_id, match_name = EXCLUDED.match_name, home_team = EXCLUDED.home_team, away_team = EXCLUDED.away_team, @@ -35,18 +66,41 @@ ON CONFLICT (id) DO UPDATE SET fetched_at = now(); -- name: InsertUpcomingEvent :exec INSERT INTO events ( - id, sport_id, match_name, home_team, away_team, - home_team_id, away_team_id, home_kit_image, away_kit_image, - league_id, league_name, league_cc, start_time, - is_live, status -) VALUES ( - $1, $2, $3, $4, $5, - $6, $7, $8, $9, - $10, $11, $12, $13, - false, 'upcoming' -) -ON CONFLICT (id) DO UPDATE SET - sport_id = EXCLUDED.sport_id, + id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + is_live, + status + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + false, + 'upcoming' + ) ON CONFLICT (id) DO +UPDATE +SET sport_id = EXCLUDED.sport_id, match_name = EXCLUDED.match_name, home_team = EXCLUDED.home_team, away_team = EXCLUDED.away_team, @@ -61,14 +115,12 @@ ON CONFLICT (id) DO UPDATE SET is_live = false, status = 'upcoming', fetched_at = now(); - - -- name: ListLiveEvents :many -SELECT id FROM events WHERE is_live = true; - +SELECT id +FROM events +WHERE is_live = true; -- name: GetAllUpcomingEvents :many -SELECT - id, +SELECT id, sport_id, match_name, home_team, @@ -86,11 +138,37 @@ SELECT fetched_at FROM events WHERE is_live = false - AND status = 'upcoming' + AND status = 'upcoming' ORDER BY start_time ASC; +-- name: GetTotalEvents :one +SELECT COUNT(*) +FROM events +WHERE is_live = false + AND status = 'upcoming'; +-- name: GetPaginatedUpcomingEvents :many +SELECT id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + is_live, + status, + fetched_at +FROM events +WHERE is_live = false + AND status = 'upcoming' +ORDER BY start_time ASC +LIMIT $1 OFFSET $2; -- name: GetUpcomingByID :one -SELECT - id, +SELECT id, sport_id, match_name, home_team, @@ -108,6 +186,6 @@ SELECT fetched_at FROM events WHERE id = $1 - AND is_live = false - AND status = 'upcoming' -LIMIT 1; + AND is_live = false + AND status = 'upcoming' +LIMIT 1; \ No newline at end of file diff --git a/db/query/odds.sql b/db/query/odds.sql index c44a691..908a445 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -83,16 +83,18 @@ SELECT event_id, FROM odds WHERE is_active = true AND source = 'b365api'; --- name: GetRawOddsByMarketID :many +-- name: GetRawOddsByMarketID :one SELECT id, + market_name, + handicap, raw_odds, fetched_at FROM odds WHERE market_id = $1 AND fi = $2 AND is_active = true - AND source = 'b365api' -LIMIT $3 OFFSET $4; + AND source = 'b365api'; + -- name: GetPrematchOddsByUpcomingID :many SELECT o.event_id, o.fi, diff --git a/db/query/ticket.sql b/db/query/ticket.sql index 86d82f5..d8db732 100644 --- a/db/query/ticket.sql +++ b/db/query/ticket.sql @@ -13,9 +13,24 @@ INSERT INTO ticket_outcomes ( market_name, odd, odd_name, + odd_header, + odd_handicap, expires ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10); +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12 + ); -- name: GetAllTickets :many SELECT * FROM ticket_with_outcomes; diff --git a/docs/docs.go b/docs/docs.go index 3295bd1..abbb190 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1303,6 +1303,20 @@ const docTemplate = `{ "prematch" ], "summary": "Retrieve all upcoming events", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + } + ], "responses": { "200": { "description": "OK", @@ -2667,6 +2681,14 @@ const docTemplate = `{ "type": "number", "example": 1.5 }, + "odd_handicap": { + "type": "string", + "example": "1" + }, + "odd_header": { + "type": "string", + "example": "1" + }, "odd_id": { "type": "integer", "example": 1 @@ -2764,9 +2786,15 @@ const docTemplate = `{ "fetched_at": { "type": "string" }, + "handicap": { + "type": "string" + }, "id": { "type": "integer" }, + "market_name": { + "type": "string" + }, "raw_odds": { "type": "array", "items": {} @@ -2825,6 +2853,14 @@ const docTemplate = `{ "type": "number", "example": 1.5 }, + "odd_handicap": { + "type": "string", + "example": "1" + }, + "odd_header": { + "type": "string", + "example": "1" + }, "odd_id": { "type": "integer", "example": 1 @@ -3069,45 +3105,18 @@ const docTemplate = `{ "handlers.CreateBetOutcomeReq": { "type": "object", "properties": { - "away_team_name": { - "type": "string", - "example": "Liverpool" - }, - "bet_id": { - "type": "integer", - "example": 1 - }, "event_id": { + "description": "BetID int64 ` + "`" + `json:\"bet_id\" example:\"1\"` + "`" + `", "type": "integer", "example": 1 }, - "expires": { - "type": "string", - "example": "2025-04-08T12:00:00Z" - }, - "home_team_name": { - "type": "string", - "example": "Manchester" - }, "market_id": { "type": "integer", "example": 1 }, - "market_name": { - "type": "string", - "example": "Fulltime Result" - }, - "odd": { - "type": "number", - "example": 1.5 - }, "odd_id": { "type": "integer", "example": 1 - }, - "odd_name": { - "type": "string", - "example": "1" } } }, @@ -3264,45 +3273,18 @@ const docTemplate = `{ "handlers.CreateTicketOutcomeReq": { "type": "object", "properties": { - "away_team_name": { - "type": "string", - "example": "Liverpool" - }, "event_id": { + "description": "TicketID int64 ` + "`" + `json:\"ticket_id\" example:\"1\"` + "`" + `", "type": "integer", "example": 1 }, - "expires": { - "type": "string", - "example": "2025-04-08T12:00:00Z" - }, - "home_team_name": { - "type": "string", - "example": "Manchester" - }, "market_id": { "type": "integer", "example": 1 }, - "market_name": { - "type": "string", - "example": "Fulltime Result" - }, - "odd": { - "type": "number", - "example": 1.5 - }, "odd_id": { "type": "integer", "example": 1 - }, - "odd_name": { - "type": "string", - "example": "1" - }, - "ticket_id": { - "type": "integer", - "example": 1 } } }, @@ -3858,11 +3840,17 @@ const docTemplate = `{ "type": "string" }, "metadata": {}, + "page": { + "type": "integer" + }, "status": { "$ref": "#/definitions/response.Status" }, "timestamp": { "type": "string" + }, + "total": { + "type": "integer" } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 83760ec..536186b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1295,6 +1295,20 @@ "prematch" ], "summary": "Retrieve all upcoming events", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "page_size", + "in": "query" + } + ], "responses": { "200": { "description": "OK", @@ -2659,6 +2673,14 @@ "type": "number", "example": 1.5 }, + "odd_handicap": { + "type": "string", + "example": "1" + }, + "odd_header": { + "type": "string", + "example": "1" + }, "odd_id": { "type": "integer", "example": 1 @@ -2756,9 +2778,15 @@ "fetched_at": { "type": "string" }, + "handicap": { + "type": "string" + }, "id": { "type": "integer" }, + "market_name": { + "type": "string" + }, "raw_odds": { "type": "array", "items": {} @@ -2817,6 +2845,14 @@ "type": "number", "example": 1.5 }, + "odd_handicap": { + "type": "string", + "example": "1" + }, + "odd_header": { + "type": "string", + "example": "1" + }, "odd_id": { "type": "integer", "example": 1 @@ -3061,45 +3097,18 @@ "handlers.CreateBetOutcomeReq": { "type": "object", "properties": { - "away_team_name": { - "type": "string", - "example": "Liverpool" - }, - "bet_id": { - "type": "integer", - "example": 1 - }, "event_id": { + "description": "BetID int64 `json:\"bet_id\" example:\"1\"`", "type": "integer", "example": 1 }, - "expires": { - "type": "string", - "example": "2025-04-08T12:00:00Z" - }, - "home_team_name": { - "type": "string", - "example": "Manchester" - }, "market_id": { "type": "integer", "example": 1 }, - "market_name": { - "type": "string", - "example": "Fulltime Result" - }, - "odd": { - "type": "number", - "example": 1.5 - }, "odd_id": { "type": "integer", "example": 1 - }, - "odd_name": { - "type": "string", - "example": "1" } } }, @@ -3256,45 +3265,18 @@ "handlers.CreateTicketOutcomeReq": { "type": "object", "properties": { - "away_team_name": { - "type": "string", - "example": "Liverpool" - }, "event_id": { + "description": "TicketID int64 `json:\"ticket_id\" example:\"1\"`", "type": "integer", "example": 1 }, - "expires": { - "type": "string", - "example": "2025-04-08T12:00:00Z" - }, - "home_team_name": { - "type": "string", - "example": "Manchester" - }, "market_id": { "type": "integer", "example": 1 }, - "market_name": { - "type": "string", - "example": "Fulltime Result" - }, - "odd": { - "type": "number", - "example": 1.5 - }, "odd_id": { "type": "integer", "example": 1 - }, - "odd_name": { - "type": "string", - "example": "1" - }, - "ticket_id": { - "type": "integer", - "example": 1 } } }, @@ -3850,11 +3832,17 @@ "type": "string" }, "metadata": {}, + "page": { + "type": "integer" + }, "status": { "$ref": "#/definitions/response.Status" }, "timestamp": { "type": "string" + }, + "total": { + "type": "integer" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d52dc80..7a5d23b 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -28,6 +28,12 @@ definitions: odd: example: 1.5 type: number + odd_handicap: + example: "1" + type: string + odd_header: + example: "1" + type: string odd_id: example: 1 type: integer @@ -97,8 +103,12 @@ definitions: properties: fetched_at: type: string + handicap: + type: string id: type: integer + market_name: + type: string raw_odds: items: {} type: array @@ -143,6 +153,12 @@ definitions: odd: example: 1.5 type: number + odd_handicap: + example: "1" + type: string + odd_header: + example: "1" + type: string odd_id: example: 1 type: integer @@ -317,36 +333,16 @@ definitions: type: object handlers.CreateBetOutcomeReq: properties: - away_team_name: - example: Liverpool - type: string - bet_id: - example: 1 - type: integer event_id: + description: BetID int64 `json:"bet_id" example:"1"` example: 1 type: integer - expires: - example: "2025-04-08T12:00:00Z" - type: string - home_team_name: - example: Manchester - type: string market_id: example: 1 type: integer - market_name: - example: Fulltime Result - type: string - odd: - example: 1.5 - type: number odd_id: example: 1 type: integer - odd_name: - example: "1" - type: string type: object handlers.CreateBetReq: properties: @@ -455,36 +451,16 @@ definitions: type: object handlers.CreateTicketOutcomeReq: properties: - away_team_name: - example: Liverpool - type: string event_id: + description: TicketID int64 `json:"ticket_id" example:"1"` example: 1 type: integer - expires: - example: "2025-04-08T12:00:00Z" - type: string - home_team_name: - example: Manchester - type: string market_id: example: 1 type: integer - market_name: - example: Fulltime Result - type: string - odd: - example: 1.5 - type: number odd_id: example: 1 type: integer - odd_name: - example: "1" - type: string - ticket_id: - example: 1 - type: integer type: object handlers.CreateTicketReq: properties: @@ -867,10 +843,14 @@ definitions: message: type: string metadata: {} + page: + type: integer status: $ref: '#/definitions/response.Status' timestamp: type: string + total: + type: integer type: object response.Status: enum: @@ -1732,6 +1712,15 @@ paths: consumes: - application/json description: Retrieve all upcoming events from the database + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: page_size + type: integer produces: - application/json responses: diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index d8914d7..34f74a2 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -80,6 +80,8 @@ type CreateBetOutcomeParams struct { MarketName string `json:"market_name"` Odd float32 `json:"odd"` OddName string `json:"odd_name"` + OddHeader string `json:"odd_header"` + OddHandicap string `json:"odd_handicap"` Expires pgtype.Timestamp `json:"expires"` } @@ -256,3 +258,20 @@ func (q *Queries) UpdateCashOut(ctx context.Context, arg UpdateCashOutParams) er _, err := q.db.Exec(ctx, UpdateCashOut, arg.ID, arg.CashedOut) return err } + +const UpdateStatus = `-- name: UpdateStatus :exec +UPDATE bets +SET status = $2, + updated_at = CURRENT_TIMESTAMP +WHERE id = $1 +` + +type UpdateStatusParams struct { + ID int64 `json:"id"` + Status int32 `json:"status"` +} + +func (q *Queries) UpdateStatus(ctx context.Context, arg UpdateStatusParams) error { + _, err := q.db.Exec(ctx, UpdateStatus, arg.ID, arg.Status) + return err +} diff --git a/gen/db/copyfrom.go b/gen/db/copyfrom.go index 5428a01..54dbb8b 100644 --- a/gen/db/copyfrom.go +++ b/gen/db/copyfrom.go @@ -38,6 +38,8 @@ func (r iteratorForCreateBetOutcome) Values() ([]interface{}, error) { r.rows[0].MarketName, r.rows[0].Odd, r.rows[0].OddName, + r.rows[0].OddHeader, + r.rows[0].OddHandicap, r.rows[0].Expires, }, nil } @@ -47,7 +49,7 @@ func (r iteratorForCreateBetOutcome) Err() error { } func (q *Queries) CreateBetOutcome(ctx context.Context, arg []CreateBetOutcomeParams) (int64, error) { - return q.db.CopyFrom(ctx, []string{"bet_outcomes"}, []string{"bet_id", "event_id", "odd_id", "home_team_name", "away_team_name", "market_id", "market_name", "odd", "odd_name", "expires"}, &iteratorForCreateBetOutcome{rows: arg}) + return q.db.CopyFrom(ctx, []string{"bet_outcomes"}, []string{"bet_id", "event_id", "odd_id", "home_team_name", "away_team_name", "market_id", "market_name", "odd", "odd_name", "odd_header", "odd_handicap", "expires"}, &iteratorForCreateBetOutcome{rows: arg}) } // iteratorForCreateTicketOutcome implements pgx.CopyFromSource. @@ -79,6 +81,8 @@ func (r iteratorForCreateTicketOutcome) Values() ([]interface{}, error) { r.rows[0].MarketName, r.rows[0].Odd, r.rows[0].OddName, + r.rows[0].OddHeader, + r.rows[0].OddHandicap, r.rows[0].Expires, }, nil } @@ -88,5 +92,5 @@ func (r iteratorForCreateTicketOutcome) Err() error { } func (q *Queries) CreateTicketOutcome(ctx context.Context, arg []CreateTicketOutcomeParams) (int64, error) { - return q.db.CopyFrom(ctx, []string{"ticket_outcomes"}, []string{"ticket_id", "event_id", "odd_id", "home_team_name", "away_team_name", "market_id", "market_name", "odd", "odd_name", "expires"}, &iteratorForCreateTicketOutcome{rows: arg}) + return q.db.CopyFrom(ctx, []string{"ticket_outcomes"}, []string{"ticket_id", "event_id", "odd_id", "home_team_name", "away_team_name", "market_id", "market_name", "odd", "odd_name", "odd_header", "odd_handicap", "expires"}, &iteratorForCreateTicketOutcome{rows: arg}) } diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 7cc1c36..4654a3e 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -12,8 +12,7 @@ import ( ) const GetAllUpcomingEvents = `-- name: GetAllUpcomingEvents :many -SELECT - id, +SELECT id, sport_id, match_name, home_team, @@ -31,7 +30,7 @@ SELECT fetched_at FROM events WHERE is_live = false - AND status = 'upcoming' + AND status = 'upcoming' ORDER BY start_time ASC ` @@ -91,9 +90,107 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]GetAllUpcomingEve return items, nil } +const GetPaginatedUpcomingEvents = `-- name: GetPaginatedUpcomingEvents :many +SELECT id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + is_live, + status, + fetched_at +FROM events +WHERE is_live = false + AND status = 'upcoming' +ORDER BY start_time ASC +LIMIT $1 OFFSET $2 +` + +type GetPaginatedUpcomingEventsParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +type GetPaginatedUpcomingEventsRow struct { + ID string `json:"id"` + SportID pgtype.Text `json:"sport_id"` + MatchName pgtype.Text `json:"match_name"` + HomeTeam pgtype.Text `json:"home_team"` + AwayTeam pgtype.Text `json:"away_team"` + HomeTeamID pgtype.Text `json:"home_team_id"` + AwayTeamID pgtype.Text `json:"away_team_id"` + HomeKitImage pgtype.Text `json:"home_kit_image"` + AwayKitImage pgtype.Text `json:"away_kit_image"` + LeagueID pgtype.Text `json:"league_id"` + LeagueName pgtype.Text `json:"league_name"` + LeagueCc pgtype.Text `json:"league_cc"` + StartTime pgtype.Timestamp `json:"start_time"` + IsLive pgtype.Bool `json:"is_live"` + Status pgtype.Text `json:"status"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` +} + +func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginatedUpcomingEventsParams) ([]GetPaginatedUpcomingEventsRow, error) { + rows, err := q.db.Query(ctx, GetPaginatedUpcomingEvents, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetPaginatedUpcomingEventsRow + for rows.Next() { + var i GetPaginatedUpcomingEventsRow + if err := rows.Scan( + &i.ID, + &i.SportID, + &i.MatchName, + &i.HomeTeam, + &i.AwayTeam, + &i.HomeTeamID, + &i.AwayTeamID, + &i.HomeKitImage, + &i.AwayKitImage, + &i.LeagueID, + &i.LeagueName, + &i.LeagueCc, + &i.StartTime, + &i.IsLive, + &i.Status, + &i.FetchedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetTotalEvents = `-- name: GetTotalEvents :one +SELECT COUNT(*) +FROM events +WHERE is_live = false + AND status = 'upcoming' +` + +func (q *Queries) GetTotalEvents(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, GetTotalEvents) + var count int64 + err := row.Scan(&count) + return count, err +} + const GetUpcomingByID = `-- name: GetUpcomingByID :one -SELECT - id, +SELECT id, sport_id, match_name, home_team, @@ -111,8 +208,8 @@ SELECT fetched_at FROM events WHERE id = $1 - AND is_live = false - AND status = 'upcoming' + AND is_live = false + AND status = 'upcoming' LIMIT 1 ` @@ -161,20 +258,51 @@ func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (GetUpcomingBy const InsertEvent = `-- name: InsertEvent :exec INSERT INTO events ( - id, sport_id, match_name, home_team, away_team, - home_team_id, away_team_id, home_kit_image, away_kit_image, - league_id, league_name, league_cc, start_time, score, - match_minute, timer_status, added_time, match_period, - is_live, status -) VALUES ( - $1, $2, $3, $4, $5, - $6, $7, $8, $9, - $10, $11, $12, $13, $14, - $15, $16, $17, $18, - $19, $20 -) -ON CONFLICT (id) DO UPDATE SET - sport_id = EXCLUDED.sport_id, + id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + score, + match_minute, + timer_status, + added_time, + match_period, + is_live, + status + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15, + $16, + $17, + $18, + $19, + $20 + ) ON CONFLICT (id) DO +UPDATE +SET sport_id = EXCLUDED.sport_id, match_name = EXCLUDED.match_name, home_team = EXCLUDED.home_team, away_team = EXCLUDED.away_team, @@ -247,18 +375,41 @@ func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error const InsertUpcomingEvent = `-- name: InsertUpcomingEvent :exec INSERT INTO events ( - id, sport_id, match_name, home_team, away_team, - home_team_id, away_team_id, home_kit_image, away_kit_image, - league_id, league_name, league_cc, start_time, - is_live, status -) VALUES ( - $1, $2, $3, $4, $5, - $6, $7, $8, $9, - $10, $11, $12, $13, - false, 'upcoming' -) -ON CONFLICT (id) DO UPDATE SET - sport_id = EXCLUDED.sport_id, + id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + is_live, + status + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + false, + 'upcoming' + ) ON CONFLICT (id) DO +UPDATE +SET sport_id = EXCLUDED.sport_id, match_name = EXCLUDED.match_name, home_team = EXCLUDED.home_team, away_team = EXCLUDED.away_team, @@ -311,7 +462,9 @@ func (q *Queries) InsertUpcomingEvent(ctx context.Context, arg InsertUpcomingEve } const ListLiveEvents = `-- name: ListLiveEvents :many -SELECT id FROM events WHERE is_live = true +SELECT id +FROM events +WHERE is_live = true ` func (q *Queries) ListLiveEvents(ctx context.Context) ([]string, error) { diff --git a/gen/db/models.go b/gen/db/models.go index 8d02167..4b297d6 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -35,6 +35,8 @@ type BetOutcome struct { MarketName string `json:"market_name"` Odd float32 `json:"odd"` OddName string `json:"odd_name"` + OddHeader string `json:"odd_header"` + OddHandicap string `json:"odd_handicap"` Expires pgtype.Timestamp `json:"expires"` } @@ -211,6 +213,8 @@ type TicketOutcome struct { MarketName string `json:"market_name"` Odd float32 `json:"odd"` OddName string `json:"odd_name"` + OddHeader string `json:"odd_header"` + OddHandicap string `json:"odd_handicap"` Expires pgtype.Timestamp `json:"expires"` } diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 846494f..3f920f4 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -247,8 +247,10 @@ func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, arg GetPremat return items, nil } -const GetRawOddsByMarketID = `-- name: GetRawOddsByMarketID :many +const GetRawOddsByMarketID = `-- name: GetRawOddsByMarketID :one SELECT id, + market_name, + handicap, raw_odds, fetched_at FROM odds @@ -256,45 +258,32 @@ WHERE market_id = $1 AND fi = $2 AND is_active = true AND source = 'b365api' -LIMIT $3 OFFSET $4 ` type GetRawOddsByMarketIDParams struct { MarketID pgtype.Text `json:"market_id"` Fi pgtype.Text `json:"fi"` - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` } type GetRawOddsByMarketIDRow struct { - ID int32 `json:"id"` - RawOdds []byte `json:"raw_odds"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` + ID int32 `json:"id"` + MarketName pgtype.Text `json:"market_name"` + Handicap pgtype.Text `json:"handicap"` + RawOdds []byte `json:"raw_odds"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` } -func (q *Queries) GetRawOddsByMarketID(ctx context.Context, arg GetRawOddsByMarketIDParams) ([]GetRawOddsByMarketIDRow, error) { - rows, err := q.db.Query(ctx, GetRawOddsByMarketID, - arg.MarketID, - arg.Fi, - arg.Limit, - arg.Offset, +func (q *Queries) GetRawOddsByMarketID(ctx context.Context, arg GetRawOddsByMarketIDParams) (GetRawOddsByMarketIDRow, error) { + row := q.db.QueryRow(ctx, GetRawOddsByMarketID, arg.MarketID, arg.Fi) + var i GetRawOddsByMarketIDRow + err := row.Scan( + &i.ID, + &i.MarketName, + &i.Handicap, + &i.RawOdds, + &i.FetchedAt, ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetRawOddsByMarketIDRow - for rows.Next() { - var i GetRawOddsByMarketIDRow - if err := rows.Scan(&i.ID, &i.RawOdds, &i.FetchedAt); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil + return i, err } const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index d49ca8c..2dc219c 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -45,6 +45,8 @@ type CreateTicketOutcomeParams struct { MarketName string `json:"market_name"` Odd float32 `json:"odd"` OddName string `json:"odd_name"` + OddHeader string `json:"odd_header"` + OddHandicap string `json:"odd_handicap"` Expires pgtype.Timestamp `json:"expires"` } @@ -131,7 +133,7 @@ func (q *Queries) GetTicketByID(ctx context.Context, id int64) (TicketWithOutcom } const GetTicketOutcome = `-- name: GetTicketOutcome :many -SELECT id, ticket_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, expires +SELECT id, ticket_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, expires FROM ticket_outcomes WHERE ticket_id = $1 ` @@ -156,6 +158,8 @@ func (q *Queries) GetTicketOutcome(ctx context.Context, ticketID int64) ([]Ticke &i.MarketName, &i.Odd, &i.OddName, + &i.OddHeader, + &i.OddHandicap, &i.Expires, ); err != nil { return nil, err diff --git a/internal/domain/bet.go b/internal/domain/bet.go index 300b65e..1fe05f2 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -3,17 +3,20 @@ package domain import "time" type BetOutcome struct { - ID int64 `json:"id" example:"1"` - BetID int64 `json:"bet_id" example:"1"` - EventID int64 `json:"event_id" example:"1"` - OddID int64 `json:"odd_id" example:"1"` - HomeTeamName string `json:"home_team_name" example:"Manchester"` - AwayTeamName string `json:"away_team_name" example:"Liverpool"` - MarketID int64 `json:"market_id" example:"1"` - MarketName string `json:"market_name" example:"Fulltime Result"` - Odd float32 `json:"odd" example:"1.5"` - OddName string `json:"odd_name" example:"1"` - Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` + ID int64 `json:"id" example:"1"` + BetID int64 `json:"bet_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` + HomeTeamName string `json:"home_team_name" example:"Manchester"` + AwayTeamName string `json:"away_team_name" example:"Liverpool"` + MarketID int64 `json:"market_id" example:"1"` + MarketName string `json:"market_name" example:"Fulltime Result"` + Odd float32 `json:"odd" example:"1.5"` + OddName string `json:"odd_name" example:"1"` + OddHeader string `json:"odd_header" example:"1"` + OddHandicap string `json:"odd_handicap" example:"1"` + + Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } type CreateBetOutcome struct { @@ -26,6 +29,8 @@ type CreateBetOutcome struct { MarketName string `json:"market_name" example:"Fulltime Result"` Odd float32 `json:"odd" example:"1.5"` OddName string `json:"odd_name" example:"1"` + OddHeader string `json:"odd_header" example:"1"` + OddHandicap string `json:"odd_handicap" example:"1"` Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } diff --git a/internal/domain/odds.go b/internal/domain/odds.go index 9992490..990c6a0 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -3,43 +3,45 @@ package domain import ( "encoding/json" "time" - ) -type RawMessage interface{} + +type RawMessage interface{} type Market struct { - EventID string - FI string - MarketCategory string - MarketType string - MarketName string - MarketID string - UpdatedAt time.Time - Odds []json.RawMessage - Name string - Handicap string - OddsVal float64 + EventID string + FI string + MarketCategory string + MarketType string + MarketName string + MarketID string + UpdatedAt time.Time + Odds []json.RawMessage + Name string + Handicap string + OddsVal float64 } type Odd struct { - EventID string `json:"event_id"` - Fi string `json:"fi"` - MarketType string `json:"market_type"` - MarketName string `json:"market_name"` - MarketCategory string `json:"market_category"` - MarketID string `json:"market_id"` - Name string `json:"name"` - Handicap string `json:"handicap"` - OddsValue float64 `json:"odds_value"` - Section string `json:"section"` - Category string `json:"category"` - RawOdds []RawMessage `json:"raw_odds"` - FetchedAt time.Time `json:"fetched_at"` - Source string `json:"source"` - IsActive bool `json:"is_active"` + EventID string `json:"event_id"` + Fi string `json:"fi"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID string `json:"market_id"` + Name string `json:"name"` + Handicap string `json:"handicap"` + OddsValue float64 `json:"odds_value"` + Section string `json:"section"` + Category string `json:"category"` + RawOdds []RawMessage `json:"raw_odds"` + FetchedAt time.Time `json:"fetched_at"` + Source string `json:"source"` + IsActive bool `json:"is_active"` } type RawOddsByMarketID struct { - ID int64 `json:"id"` - RawOdds []RawMessage `json:"raw_odds"` - FetchedAt time.Time `json:"fetched_at"` -} \ No newline at end of file + ID int64 `json:"id"` + MarketName string `json:"market_name"` + Handicap string `json:"handicap"` + RawOdds []RawMessage `json:"raw_odds"` + FetchedAt time.Time `json:"fetched_at"` +} diff --git a/internal/domain/ticket.go b/internal/domain/ticket.go index 6cdf400..8fedd64 100644 --- a/internal/domain/ticket.go +++ b/internal/domain/ticket.go @@ -6,13 +6,15 @@ type TicketOutcome struct { ID int64 `json:"id" example:"1"` TicketID int64 `json:"ticket_id" example:"1"` EventID int64 `json:"event_id" example:"1"` - OddID int64 `json:"odd_id" example:"1"` HomeTeamName string `json:"home_team_name" example:"Manchester"` AwayTeamName string `json:"away_team_name" example:"Liverpool"` MarketID int64 `json:"market_id" example:"1"` MarketName string `json:"market_name" example:"Fulltime Result"` + OddID int64 `json:"odd_id" example:"1"` Odd float32 `json:"odd" example:"1.5"` OddName string `json:"odd_name" example:"1"` + OddHeader string `json:"odd_header" example:"1"` + OddHandicap string `json:"odd_handicap" example:"1"` Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } @@ -26,6 +28,8 @@ type CreateTicketOutcome struct { MarketName string `json:"market_name" example:"Fulltime Result"` Odd float32 `json:"odd" example:"1.5"` OddName string `json:"odd_name" example:"1"` + OddHeader string `json:"odd_header" example:"1"` + OddHandicap string `json:"odd_handicap" example:"1"` Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 23b81d1..b486756 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -46,6 +46,8 @@ func convertDBBetOutcomes(bet dbgen.BetWithOutcome) domain.GetBet { MarketName: outcome.MarketName, Odd: outcome.Odd, OddName: outcome.OddName, + OddHeader: outcome.OddHeader, + OddHandicap: outcome.OddHandicap, Expires: outcome.Expires.Time, }) } @@ -82,6 +84,8 @@ func convertDBCreateBetOutcome(betOutcome domain.CreateBetOutcome) dbgen.CreateB MarketName: betOutcome.MarketName, Odd: betOutcome.Odd, OddName: betOutcome.OddName, + OddHeader: betOutcome.OddHeader, + OddHandicap: betOutcome.OddHandicap, Expires: pgtype.Timestamp{ Time: betOutcome.Expires, Valid: true, @@ -193,6 +197,14 @@ func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) err return err } +func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.BetStatus) error { + err := s.queries.UpdateStatus(ctx, dbgen.UpdateStatusParams{ + ID: id, + Status: int32(status), + }) + return err +} + func (s *Store) DeleteBet(ctx context.Context, id int64) error { return s.queries.DeleteBet(ctx, id) } diff --git a/internal/repository/event.go b/internal/repository/event.go index 9493087..d0ff7d6 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -6,6 +6,7 @@ import ( dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/jackc/pgx/v5/pgtype" ) @@ -86,6 +87,42 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven } return upcomingEvents, nil } +func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32) ([]domain.UpcomingEvent, int64, error) { + events, err := s.queries.GetPaginatedUpcomingEvents(ctx, dbgen.GetPaginatedUpcomingEventsParams{ + Limit: limit, + Offset: offset * limit, + }) + + if err != nil { + return nil, 0, err + } + + upcomingEvents := make([]domain.UpcomingEvent, len(events)) + for i, e := range events { + upcomingEvents[i] = domain.UpcomingEvent{ + ID: e.ID, + SportID: e.SportID.String, + MatchName: e.MatchName.String, + HomeTeam: e.HomeTeam.String, + AwayTeam: e.AwayTeam.String, + HomeTeamID: e.HomeTeamID.String, + AwayTeamID: e.AwayTeamID.String, + HomeKitImage: e.HomeKitImage.String, + AwayKitImage: e.AwayKitImage.String, + LeagueID: e.LeagueID.String, + LeagueName: e.LeagueName.String, + LeagueCC: e.LeagueCc.String, + StartTime: e.StartTime.Time.UTC(), + } + } + totalCount, err := s.queries.GetTotalEvents(ctx) + if err != nil { + return nil, 0, err + } + + numberOfPages := (totalCount) / int64(limit) + return upcomingEvents, numberOfPages, nil +} func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { event, err := s.queries.GetUpcomingByID(ctx, ID) if err != nil { @@ -108,4 +145,3 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc StartTime: event.StartTime.Time.UTC(), }, nil } - diff --git a/internal/repository/odds.go b/internal/repository/odds.go index 72f2c93..31810f5 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -3,7 +3,6 @@ package repository import ( "context" "encoding/json" - "fmt" "os" "strconv" "time" @@ -180,28 +179,22 @@ func (s *Store) GetRawOddsByMarketID(ctx context.Context, rawOddsID string, upco params := dbgen.GetRawOddsByMarketIDParams{ MarketID: pgtype.Text{String: rawOddsID, Valid: true}, Fi: pgtype.Text{String: upcomingID, Valid: true}, - Limit: 1, - Offset: 0, } - rows, err := s.queries.GetRawOddsByMarketID(ctx, params) + odds, err := s.queries.GetRawOddsByMarketID(ctx, params) if err != nil { return domain.RawOddsByMarketID{}, err } - if len(rows) == 0 { - return domain.RawOddsByMarketID{}, fmt.Errorf("no raw odds found for market_id: %s", rawOddsID) - } - - row := rows[0] - var rawOdds []json.RawMessage - if err := json.Unmarshal(row.RawOdds, &rawOdds); err != nil { + if err := json.Unmarshal(odds.RawOdds, &rawOdds); err != nil { return domain.RawOddsByMarketID{}, err } return domain.RawOddsByMarketID{ - ID: int64(row.ID), + ID: int64(odds.ID), + MarketName: odds.MarketName.String, + Handicap: odds.Handicap.String, RawOdds: func() []domain.RawMessage { converted := make([]domain.RawMessage, len(rawOdds)) for i, r := range rawOdds { @@ -209,7 +202,7 @@ func (s *Store) GetRawOddsByMarketID(ctx context.Context, rawOddsID string, upco } return converted }(), - FetchedAt: row.FetchedAt.Time, + FetchedAt: odds.FetchedAt.Time, }, nil } diff --git a/internal/repository/ticket.go b/internal/repository/ticket.go index 50eff64..911ad9e 100644 --- a/internal/repository/ticket.go +++ b/internal/repository/ticket.go @@ -32,6 +32,8 @@ func convertDBTicketOutcomes(ticket dbgen.TicketWithOutcome) domain.GetTicket { MarketName: outcome.MarketName, Odd: outcome.Odd, OddName: outcome.OddName, + OddHeader: outcome.OddHeader, + OddHandicap: outcome.OddHandicap, Expires: outcome.Expires.Time, }) } @@ -54,6 +56,8 @@ func convertDBCreateTicketOutcome(ticketOutcome domain.CreateTicketOutcome) dbge MarketName: ticketOutcome.MarketName, Odd: ticketOutcome.Odd, OddName: ticketOutcome.OddName, + OddHeader: ticketOutcome.OddHeader, + OddHandicap: ticketOutcome.OddHandicap, Expires: pgtype.Timestamp{ Time: ticketOutcome.Expires, Valid: true, diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index 3b10393..8066c50 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -14,5 +14,6 @@ type BetStore interface { GetAllBets(ctx context.Context) ([]domain.GetBet, error) GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.GetBet, error) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error + UpdateStatus(ctx context.Context, id int64, status domain.BetStatus) error DeleteBet(ctx context.Context, id int64) error } diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index b5f61ef..a464094 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -42,6 +42,10 @@ func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) e return s.betStore.UpdateCashOut(ctx, id, cashedOut) } +func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.BetStatus) error { + return s.betStore.UpdateStatus(ctx, id, status) +} + func (s *Service) DeleteBet(ctx context.Context, id int64) error { return s.betStore.DeleteBet(ctx, id) } diff --git a/internal/services/event/port.go b/internal/services/event/port.go index 2a81a1a..e1c3c64 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -10,5 +10,6 @@ type Service interface { FetchLiveEvents(ctx context.Context) error FetchUpcomingEvents(ctx context.Context) error GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) + GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32) ([]domain.UpcomingEvent, int64, error) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 41a2d1d..5ab1ceb 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -110,10 +110,10 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { var data struct { Success int `json:"success"` Results []struct { - ID string `json:"id"` - SportID string `json:"sport_id"` - Time string `json:"time"` - League struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + League struct { ID string `json:"id"` Name string `json:"name"` } `json:"league"` @@ -178,6 +178,10 @@ func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEv return s.store.GetAllUpcomingEvents(ctx) } +func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32) ([]domain.UpcomingEvent, int64, error) { + return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset) +} + func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { return s.store.GetUpcomingEventByID(ctx, ID) } diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index eb3d3e6..69fd5ee 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -10,5 +10,5 @@ type Service interface { FetchNonLiveOdds(ctx context.Context) error GetPrematchOdds(ctx context.Context, eventID string) ([]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) } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 2ae8e4d..aa59a5a 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -119,13 +119,13 @@ func (s *ServiceImpl) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, err return s.store.GetALLPrematchOdds(ctx) } -func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) ([]domain.RawOddsByMarketID, error) { +func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) { rows, err := s.store.GetRawOddsByMarketID(ctx, marketID, upcomingID) if err != nil { - return nil, err + return domain.RawOddsByMarketID{}, err } - return []domain.RawOddsByMarketID{rows}, nil + return rows, nil } func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) { diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index ea15153..3106cb8 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -1,8 +1,7 @@ package httpserver import ( - // "context" - + "fmt" "log" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" @@ -19,7 +18,6 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }{ // { - // spec: "0 0 * * * *", // Every hour // task: func() { // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { @@ -48,6 +46,8 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } for _, job := range schedule { + job.task() + fmt.Printf("here at") if _, err := c.AddFunc(job.spec, job.task); err != nil { log.Fatalf("Failed to schedule cron job: %v", err) } diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 8baae19..b04a9f3 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -2,6 +2,7 @@ package handlers import ( "encoding/json" + "fmt" "log/slog" "strconv" "time" @@ -9,6 +10,8 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "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/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" @@ -18,16 +21,16 @@ import ( ) type CreateBetOutcomeReq struct { - BetID int64 `json:"bet_id" example:"1"` - EventID int64 `json:"event_id" example:"1"` - OddID int64 `json:"odd_id" example:"1"` - HomeTeamName string `json:"home_team_name" example:"Manchester"` - AwayTeamName string `json:"away_team_name" example:"Liverpool"` - MarketID int64 `json:"market_id" example:"1"` - MarketName string `json:"market_name" example:"Fulltime Result"` - Odd float32 `json:"odd" example:"1.5"` - OddName string `json:"odd_name" example:"1"` - Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` + // BetID int64 `json:"bet_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` + MarketID int64 `json:"market_id" example:"1"` + // HomeTeamName string `json:"home_team_name" example:"Manchester"` + // AwayTeamName string `json:"away_team_name" example:"Liverpool"` + // MarketName string `json:"market_name" example:"Fulltime Result"` + // Odd float32 `json:"odd" example:"1.5"` + // OddName string `json:"odd_name" example:"1"` + // Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } type NullableInt64 struct { @@ -139,7 +142,7 @@ func convertBet(bet domain.GetBet) BetRes { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /bet [post] -func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, branchSvc *branch.Service, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { +func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, branchSvc *branch.Service, walletSvc *wallet.Service, eventSvc event.Service, oddSvc odds.ServiceImpl, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { // Get user_id from middleware @@ -160,6 +163,8 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, return nil } + // Validating user by role + // Differentiating between offline and online bets user, err := userSvc.GetUserByID(c.Context(), userID) cashoutUUID := uuid.New() var bet domain.Bet @@ -226,28 +231,88 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, }) } - // TODO Validate Outcomes Here and make sure they didn't expire - if err != nil { logger.Error("CreateBetReq failed", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) } - var outcomes []domain.CreateBetOutcome = make([]domain.CreateBetOutcome, 0, len(req.Outcomes)) + // + // TODO Validate Outcomes Here and make sure they didn't expire + // Validation for creating tickets + if len(req.Outcomes) > 30 { + response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil) + return nil + } + var outcomes []domain.CreateBetOutcome = make([]domain.CreateBetOutcome, 0, len(req.Outcomes)) for _, outcome := range req.Outcomes { + eventIDStr := strconv.FormatInt(outcome.EventID, 10) + marketIDStr := strconv.FormatInt(outcome.MarketID, 10) + oddIDStr := strconv.FormatInt(outcome.OddID, 10) + event, err := eventSvc.GetUpcomingEventByID(c.Context(), eventIDStr) + if err != nil { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid event id", err, nil) + return nil + } + + // Checking to make sure the event hasn't already started + currentTime := time.Now() + if event.StartTime.Before(currentTime) { + response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) + return nil + } + + odds, err := oddSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr) + + if err != nil { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil) + return nil + } + type rawOddType struct { + ID string + Name string + Odds string + Header string + Handicap string + } + var selectedOdd rawOddType + var isOddFound bool = false + for _, raw := range odds.RawOdds { + var rawOdd rawOddType + rawBytes, err := json.Marshal(raw) + err = json.Unmarshal(rawBytes, &rawOdd) + if err != nil { + fmt.Println("Failed to unmarshal raw odd:", err) + continue + } + if rawOdd.ID == oddIDStr { + selectedOdd = rawOdd + isOddFound = true + } + } + + if !isOddFound { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil) + return nil + } + + parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32) + outcomes = append(outcomes, domain.CreateBetOutcome{ BetID: bet.ID, EventID: outcome.EventID, OddID: outcome.OddID, - HomeTeamName: outcome.HomeTeamName, - AwayTeamName: outcome.AwayTeamName, MarketID: outcome.MarketID, - MarketName: outcome.MarketName, - Odd: outcome.Odd, - OddName: outcome.OddName, - Expires: outcome.Expires, + HomeTeamName: event.HomeTeam, + AwayTeamName: event.AwayTeam, + MarketName: odds.MarketName, + Odd: float32(parsedOdd), + OddName: selectedOdd.Name, + OddHeader: selectedOdd.Header, + OddHandicap: selectedOdd.Handicap, + Expires: event.StartTime, }) } + rows, err := betSvc.CreateBetOutcome(c.Context(), outcomes) if err != nil { diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index efa3b00..493bf94 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -96,17 +96,22 @@ func GetRawOddsByMarketID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fi // @Tags prematch // @Accept json // @Produce json +// @Param page query int false "Page number" +// @Param page_size query int false "Page size" // @Success 200 {array} domain.UpcomingEvent // @Failure 500 {object} response.APIResponse // @Router /prematch/events [get] func GetAllUpcomingEvents(logger *slog.Logger, eventSvc event.Service) fiber.Handler { return func(c *fiber.Ctx) error { - events, err := eventSvc.GetAllUpcomingEvents(c.Context()) + page := c.QueryInt("page", 1) + pageSize := c.QueryInt("page_size", 10) + + events, total, err := eventSvc.GetPaginatedUpcomingEvents(c.Context(), int32(pageSize), int32(page) - 1) if err != nil { return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve all upcoming events", nil, nil) } - return response.WriteJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", events, nil) + return response.WritePaginatedJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", events, nil, page, int(total)) } } diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index b9e398b..bf1bb87 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -1,11 +1,15 @@ package handlers import ( + "encoding/json" + "fmt" "log/slog" "strconv" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" @@ -13,16 +17,16 @@ import ( ) type CreateTicketOutcomeReq struct { - TicketID int64 `json:"ticket_id" example:"1"` - EventID int64 `json:"event_id" example:"1"` - OddID int64 `json:"odd_id" example:"1"` - HomeTeamName string `json:"home_team_name" example:"Manchester"` - AwayTeamName string `json:"away_team_name" example:"Liverpool"` - MarketID int64 `json:"market_id" example:"1"` - MarketName string `json:"market_name" example:"Fulltime Result"` - Odd float32 `json:"odd" example:"1.5"` - OddName string `json:"odd_name" example:"1"` - Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` + // TicketID int64 `json:"ticket_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` + MarketID int64 `json:"market_id" example:"1"` + // HomeTeamName string `json:"home_team_name" example:"Manchester"` + // AwayTeamName string `json:"away_team_name" example:"Liverpool"` + // MarketName string `json:"market_name" example:"Fulltime Result"` + // Odd float32 `json:"odd" example:"1.5"` + // OddName string `json:"odd_name" example:"1"` + // Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } type CreateTicketReq struct { @@ -46,8 +50,7 @@ type CreateTicketRes struct { // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /ticket [post] -func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, - validator *customvalidator.CustomValidator) fiber.Handler { +func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, eventSvc event.Service, oddSvc odds.ServiceImpl, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { var req CreateTicketReq if err := c.BodyParser(&req); err != nil { @@ -64,6 +67,79 @@ func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, } // TODO Validate Outcomes Here and make sure they didn't expire + // Validation for creating tickets + if len(req.Outcomes) > 30 { + response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil) + return nil + } + var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes)) + for _, outcome := range req.Outcomes { + eventIDStr := strconv.FormatInt(outcome.EventID, 10) + marketIDStr := strconv.FormatInt(outcome.MarketID, 10) + oddIDStr := strconv.FormatInt(outcome.OddID, 10) + event, err := eventSvc.GetUpcomingEventByID(c.Context(), eventIDStr) + if err != nil { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid event id", err, nil) + return nil + } + + // Checking to make sure the event hasn't already started + currentTime := time.Now() + if event.StartTime.Before(currentTime) { + response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) + return nil + } + + odds, err := oddSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr) + + if err != nil { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil) + return nil + } + type rawOddType struct { + ID string + Name string + Odds string + Header string + Handicap string + } + var selectedOdd rawOddType + var isOddFound bool = false + for _, raw := range odds.RawOdds { + var rawOdd rawOddType + rawBytes, err := json.Marshal(raw) + err = json.Unmarshal(rawBytes, &rawOdd) + if err != nil { + fmt.Println("Failed to unmarshal raw odd:", err) + continue + } + if rawOdd.ID == oddIDStr { + selectedOdd = rawOdd + isOddFound = true + } + } + + if !isOddFound { + response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil) + return nil + } + + parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32) + + outcomes = append(outcomes, domain.CreateTicketOutcome{ + EventID: outcome.EventID, + OddID: outcome.OddID, + MarketID: outcome.MarketID, + HomeTeamName: event.HomeTeam, + AwayTeamName: event.AwayTeam, + MarketName: odds.MarketName, + Odd: float32(parsedOdd), + OddName: selectedOdd.Name, + OddHeader: selectedOdd.Header, + OddHandicap: selectedOdd.Handicap, + Expires: event.StartTime, + }) + } ticket, err := ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{ Amount: domain.ToCurrency(req.Amount), @@ -76,22 +152,11 @@ func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, }) } - var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes)) - - for _, outcome := range req.Outcomes { - outcomes = append(outcomes, domain.CreateTicketOutcome{ - TicketID: ticket.ID, - EventID: outcome.EventID, - OddID: outcome.OddID, - HomeTeamName: outcome.HomeTeamName, - AwayTeamName: outcome.AwayTeamName, - MarketID: outcome.MarketID, - MarketName: outcome.MarketName, - Odd: outcome.Odd, - OddName: outcome.OddName, - Expires: outcome.Expires, - }) + // Add the ticket id now that it has fetched from the database + for index := range outcomes { + outcomes[index].TicketID = ticket.ID } + rows, err := ticketSvc.CreateTicketOutcome(c.Context(), outcomes) if err != nil { diff --git a/internal/web_server/response/res.go b/internal/web_server/response/res.go index 593758d..496a14f 100644 --- a/internal/web_server/response/res.go +++ b/internal/web_server/response/res.go @@ -18,12 +18,15 @@ type APIResponse struct { Message string `json:"message"` Data interface{} `json:"data,omitempty"` Metadata interface{} `json:"metadata,omitempty"` + Page *int `json:"page,omitempty"` + Total *int `json:"total,omitempty"` Timestamp time.Time `json:"timestamp"` } func NewAPIResponse( status Status, message string, data interface{}, metadata interface{}, + page *int, total *int, ) APIResponse { return APIResponse{ @@ -32,6 +35,8 @@ func NewAPIResponse( Data: data, Metadata: metadata, Timestamp: time.Now(), + Page: page, + Total: total, } } func WriteJSON(c *fiber.Ctx, status int, message string, data, metadata interface{}) error { @@ -41,7 +46,18 @@ func WriteJSON(c *fiber.Ctx, status int, message string, data, metadata interfac } else { apiStatus = Error } - apiRes := NewAPIResponse(apiStatus, message, data, metadata) + apiRes := NewAPIResponse(apiStatus, message, data, metadata, nil, nil) + + return c.Status(status).JSON(apiRes) +} +func WritePaginatedJSON(c *fiber.Ctx, status int, message string, data, metadata interface{}, page int, total int) error { + var apiStatus Status + if status >= 200 && status <= 299 { + apiStatus = Success + } else { + apiStatus = Error + } + apiRes := NewAPIResponse(apiStatus, message, data, metadata, &page, &total) return c.Status(status).JSON(apiRes) } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index c59a79a..e730bc0 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -86,12 +86,12 @@ func (a *App) initAppRoutes() { a.fiber.Delete("/branch/:id/operation/:opID", a.authMiddleware, handlers.DeleteBranchOperation(a.logger, a.branchSvc, a.validator)) // Ticket - a.fiber.Post("/ticket", handlers.CreateTicket(a.logger, a.ticketSvc, a.validator)) + a.fiber.Post("/ticket", handlers.CreateTicket(a.logger, a.ticketSvc, a.eventSvc, *a.prematchSvc, a.validator)) a.fiber.Get("/ticket", handlers.GetAllTickets(a.logger, a.ticketSvc, a.validator)) a.fiber.Get("/ticket/:id", handlers.GetTicketByID(a.logger, a.ticketSvc, a.validator)) // Bet - a.fiber.Post("/bet", a.authMiddleware, handlers.CreateBet(a.logger, a.betSvc, a.userSvc, a.branchSvc, a.walletSvc, a.validator)) + a.fiber.Post("/bet", a.authMiddleware, handlers.CreateBet(a.logger, a.betSvc, a.userSvc, a.branchSvc, a.walletSvc, a.eventSvc, *a.prematchSvc, a.validator)) a.fiber.Get("/bet", a.authMiddleware, handlers.GetAllBet(a.logger, a.betSvc, a.validator)) a.fiber.Get("/bet/:id", a.authMiddleware, handlers.GetBetByID(a.logger, a.betSvc, a.validator)) a.fiber.Get("/bet/cashout/:id", a.authMiddleware, handlers.GetBetByCashoutID(a.logger, a.betSvc, a.validator)) From 8c536a6d2f6b5d8b244e70e690f160dbefabd7e3 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 23 Apr 2025 03:44:17 +0300 Subject: [PATCH 30/30] company management --- cmd/main.go | 14 +- db/migrations/000001_fortune.down.sql | 21 +- db/migrations/000001_fortune.up.sql | 8 + db/query/bet.sql | 19 +- db/query/company.sql | 24 ++ db/query/events.sql | 18 +- db/query/ticket.sql | 4 + docs/docs.go | 315 +++++++++++++++++- docs/swagger.json | 315 +++++++++++++++++- docs/swagger.yaml | 213 +++++++++++- gen/db/bet.sql.go | 16 + gen/db/company.sql.go | 122 +++++++ gen/db/events.sql.go | 38 ++- gen/db/models.go | 9 + gen/db/ticket.sql.go | 19 +- internal/domain/bet.go | 52 ++- internal/domain/common.go | 12 +- internal/domain/company.go | 19 +- internal/domain/ticket.go | 27 +- internal/repository/bet.go | 15 +- internal/repository/company.go | 74 ++++ internal/repository/event.go | 26 +- internal/repository/ticket.go | 9 + internal/services/bet/port.go | 3 +- internal/services/bet/service.go | 7 +- internal/services/company/port.go | 15 + internal/services/company/service.go | 35 ++ internal/services/event/port.go | 2 +- internal/services/event/service.go | 4 +- internal/services/ticket/port.go | 1 + internal/services/ticket/service.go | 4 + internal/services/wallet/wallet.go | 2 + internal/web_server/app.go | 4 + internal/web_server/handlers/bet_handler.go | 120 ++----- .../web_server/handlers/branch_handler.go | 30 +- internal/web_server/handlers/cashier.go | 42 +-- .../web_server/handlers/company_handler.go | 229 +++++++++++++ internal/web_server/handlers/manager.go | 38 +-- internal/web_server/handlers/prematch.go | 17 +- .../web_server/handlers/ticket_handler.go | 35 +- .../handlers/transaction_handler.go | 22 +- .../web_server/handlers/transfer_handler.go | 20 +- .../web_server/handlers/wallet_handler.go | 6 +- internal/web_server/middleware.go | 10 +- internal/web_server/routes.go | 8 +- 45 files changed, 1681 insertions(+), 362 deletions(-) create mode 100644 db/query/company.sql create mode 100644 gen/db/company.sql.go create mode 100644 internal/repository/company.go create mode 100644 internal/services/company/port.go create mode 100644 internal/services/company/service.go create mode 100644 internal/web_server/handlers/company_handler.go diff --git a/cmd/main.go b/cmd/main.go index a3d8d1c..dfedef6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -16,11 +16,12 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server" @@ -67,22 +68,23 @@ func main() { eventSvc := event.New(cfg.Bet365Token, store) oddsSvc := odds.New(cfg.Bet365Token, store) - ticketSvc := ticket.NewService(store) betSvc := bet.NewService(store) walletSvc := wallet.NewService(store, store) transactionSvc := transaction.NewService(store) branchSvc := branch.NewService(store) - + companySvc := company.NewService(store) + notificationRepo := repository.NewNotificationRepository(store) notificationSvc := notificationservice.New(notificationRepo, logger, cfg) - + httpserver.StartDataFetchingCrons(eventSvc, oddsSvc) app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, - }, userSvc, ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, notificationSvc, oddsSvc, eventSvc) + }, userSvc, + ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc) logger.Info("Starting server", "port", cfg.Port) diff --git a/db/migrations/000001_fortune.down.sql b/db/migrations/000001_fortune.down.sql index 35659ea..82d488d 100644 --- a/db/migrations/000001_fortune.down.sql +++ b/db/migrations/000001_fortune.down.sql @@ -1,22 +1,18 @@ -- Drop tables that depend on service_type_setting DROP TABLE IF EXISTS service_type_setting; - -- Drop product-related tables and types DROP TABLE IF EXISTS product; DROP TYPE IF EXISTS tier_group; - -- Drop onboarding-related tables and types DROP TABLE IF EXISTS verification_key; DROP TABLE IF EXISTS onboarding_user; DROP TYPE IF EXISTS verification_status; DROP TYPE IF EXISTS onboarding_status; - -- Drop staff-related tables and types DROP TABLE IF EXISTS staff_session; DROP TABLE IF EXISTS user_agent; DROP TABLE IF EXISTS staff; DROP TYPE IF EXISTS password_status; - -- Drop mobile app-related tables and types DROP TABLE IF EXISTS user_devices; DROP TABLE IF EXISTS user_session; @@ -25,17 +21,14 @@ DROP TABLE IF EXISTS users; DROP TYPE IF EXISTS device_type; DROP TYPE IF EXISTS registeration_type; DROP TYPE IF EXISTS customer_group; - -- Drop linked accounts and beneficiary tables and types DROP TABLE IF EXISTS beneficiary; DROP TYPE IF EXISTS fund_destination; - -- Drop maker checker-related tables and types DROP TABLE IF EXISTS workflow; DROP TYPE IF EXISTS approval_status; DROP TYPE IF EXISTS action_type; DROP TYPE IF EXISTS context_type; - -- Drop authorization-related tables and types DROP TRIGGER IF EXISTS enforce_unique_array ON policy; DROP FUNCTION IF EXISTS check_unique_array; @@ -43,11 +36,9 @@ DROP TABLE IF EXISTS policy; DROP TABLE IF EXISTS roles; DROP TYPE IF EXISTS policy_action; DROP TYPE IF EXISTS policy_object; - -- Drop bank-related tables and types DROP TABLE IF EXISTS bank; DROP TABLE IF EXISTS flagged_users; - -- Drop transaction-related tables and types DROP TABLE IF EXISTS transaction_daily; DROP TABLE IF EXISTS system_limits; @@ -57,22 +48,18 @@ DROP TYPE IF EXISTS service_type; DROP TYPE IF EXISTS channel; DROP TYPE IF EXISTS transaction_category; DROP TYPE IF EXISTS registration_type; - -- Drop branches and related tables DROP TABLE IF EXISTS branches; DROP TABLE IF EXISTS cities; DROP TABLE IF EXISTS districts; DROP TABLE IF EXISTS regions; - -- Drop activity logs DROP TABLE IF EXISTS activity; - -- Drop ussd account and related enums DROP TABLE IF EXISTS ussd_account; DROP TYPE IF EXISTS ua_pin_status; DROP TYPE IF EXISTS ua_status; DROP TYPE IF EXISTS ua_registaration_type; - -- Drop FortuneBet DROP TABLE IF EXISTS tickets; DROP TABLE IF EXISTS ticket_outcomes; @@ -83,13 +70,9 @@ DROP TABLE IF EXISTS customer_wallets; DROP TABLE IF EXISTS wallet_transfer; DROP TABLE IF EXISTS transactions; DROP TABLE IF EXISTS branches; +DROP TABLE IF EXISTS companies; DROP TABLE IF EXISTS supported_operations; DROP TABLE IF EXISTS refresh_tokens; DROP TABLE IF EXISTS otps; - - - - DROP TABLE IF EXISTS odds; -DROP TABLE IF EXISTS events; - +DROP TABLE IF EXISTS events; \ No newline at end of file diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index e24ba3f..111aa73 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -78,6 +78,7 @@ CREATE TABLE IF NOT EXISTS bet_outcomes ( odd_name VARCHAR(255) NOT NULL, odd_header VARCHAR(255) NOT NULL, odd_handicap VARCHAR(255) NOT NULL, + status INT NOT NULL DEFAULT 0, expires TIMESTAMP NOT NULL ); CREATE TABLE IF NOT EXISTS ticket_outcomes ( @@ -93,6 +94,7 @@ CREATE TABLE IF NOT EXISTS ticket_outcomes ( odd_name VARCHAR(255) NOT NULL, odd_header VARCHAR(255) NOT NULL, odd_handicap VARCHAR(255) NOT NULL, + status INT NOT NULL DEFAULT 0, expires TIMESTAMP NOT NULL ); CREATE VIEW bet_with_outcomes AS @@ -238,6 +240,12 @@ 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, + admin_id BIGINT NOT NULL, + wallet_id BIGINT NOT NULL +); ALTER TABLE refresh_tokens ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users(id); ALTER TABLE bets diff --git a/db/query/bet.sql b/db/query/bet.sql index c4eb124..9ebbb30 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -27,7 +27,20 @@ INSERT INTO bet_outcomes ( odd_handicap, expires ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12); +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12 + ); -- name: GetAllBets :many SELECT * FROM bet_with_outcomes; @@ -48,6 +61,10 @@ UPDATE bets SET cashed_out = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1; +-- name: UpdateBetOutcomeStatus :exec +UPDATE bet_outcomes +SET status = $1 +WHERE id = $2; -- name: UpdateStatus :exec UPDATE bets SET status = $2, diff --git a/db/query/company.sql b/db/query/company.sql new file mode 100644 index 0000000..d82cb7a --- /dev/null +++ b/db/query/company.sql @@ -0,0 +1,24 @@ +-- name: CreateCompany :one +INSERT INTO companies ( + name, + admin_id, + wallet_id + ) +VALUES ($1, $2, $3) +RETURNING *; +-- name: GetAllCompanies :many +SELECT * +FROM companies; +-- name: GetCompanyByID :one +SELECT * +FROM companies +WHERE id = $1; +-- name: UpdateCompany :one +UPDATE companies +SET name = $1, + admin_id = $2 +WHERE id = $3 +RETURNING *; +-- name: DeleteCompany :exec +DELETE FROM companies +WHERE id = $1; \ No newline at end of file diff --git a/db/query/events.sql b/db/query/events.sql index 61dbdbb..ab459ae 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -144,7 +144,15 @@ ORDER BY start_time ASC; SELECT COUNT(*) FROM events WHERE is_live = false - AND status = 'upcoming'; + AND status = 'upcoming' + AND ( + league_id = $1 + OR $1 IS NULL + ) + AND ( + sport_id = $2 + OR $2 IS NULL + ); -- name: GetPaginatedUpcomingEvents :many SELECT id, sport_id, @@ -165,6 +173,14 @@ SELECT id, FROM events WHERE is_live = false AND status = 'upcoming' + AND ( + league_id = $3 + OR $3 IS NULL + ) + AND ( + sport_id = $4 + OR $4 IS NULL + ) ORDER BY start_time ASC LIMIT $1 OFFSET $2; -- name: GetUpcomingByID :one diff --git a/db/query/ticket.sql b/db/query/ticket.sql index d8db732..8e2daaf 100644 --- a/db/query/ticket.sql +++ b/db/query/ticket.sql @@ -42,6 +42,10 @@ WHERE id = $1; SELECT * FROM ticket_outcomes WHERE ticket_id = $1; +-- name: UpdateTicketOutcomeStatus :exec +UPDATE ticket_outcomes +SET status = $1 +WHERE id = $2; -- name: DeleteTicket :exec DELETE FROM tickets WHERE id = $1; diff --git a/docs/docs.go b/docs/docs.go index abbb190..cb1802c 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -993,6 +993,225 @@ const docTemplate = `{ } } }, + "/company": { + "get": { + "description": "Gets all companies", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "company" + ], + "summary": "Gets all companies", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.CompanyRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Creates a company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "company" + ], + "summary": "Create a company", + "parameters": [ + { + "description": "Creates company", + "name": "createCompany", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateCompanyReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CompanyRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/company/{id}": { + "get": { + "description": "Gets a single company by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "company" + ], + "summary": "Gets company by id", + "parameters": [ + { + "type": "integer", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CompanyRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "description": "Updates a company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "company" + ], + "summary": "Updates a company", + "parameters": [ + { + "type": "integer", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update Company", + "name": "updateCompany", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateCompanyReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CompanyRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "description": "Delete the company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "company" + ], + "summary": "Delete the company", + "parameters": [ + { + "type": "integer", + "description": "Company 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" + } + } + } + } + }, "/company/{id}/branch": { "get": { "description": "Gets branches by company id", @@ -1315,6 +1534,18 @@ const docTemplate = `{ "description": "Page size", "name": "page_size", "in": "query" + }, + { + "type": "string", + "description": "League ID Filter", + "name": "league_id", + "in": "query" + }, + { + "type": "string", + "description": "Sport ID Filter", + "name": "sport_id", + "in": "query" } ], "responses": { @@ -2696,24 +2927,17 @@ const docTemplate = `{ "odd_name": { "type": "string", "example": "1" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.OutcomeStatus" + } + ], + "example": 1 } } }, - "domain.BetStatus": { - "type": "integer", - "enum": [ - 0, - 1, - 2, - 3 - ], - "x-enum-varnames": [ - "BET_STATUS_PENDING", - "BET_STATUS_WIN", - "BET_STATUS_LOSS", - "BET_STATUS_ERROR" - ] - }, "domain.Odd": { "type": "object", "properties": { @@ -2765,6 +2989,21 @@ const docTemplate = `{ } } }, + "domain.OutcomeStatus": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "OUTCOME_STATUS_PENDING", + "OUTCOME_STATUS_WIN", + "OUTCOME_STATUS_LOSS", + "OUTCOME_STATUS_ERROR" + ] + }, "domain.PaymentOption": { "type": "integer", "enum": [ @@ -2869,6 +3108,14 @@ const docTemplate = `{ "type": "string", "example": "1" }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.OutcomeStatus" + } + ], + "example": 1 + }, "ticket_id": { "type": "integer", "example": 1 @@ -2976,7 +3223,7 @@ const docTemplate = `{ "status": { "allOf": [ { - "$ref": "#/definitions/domain.BetStatus" + "$ref": "#/definitions/domain.OutcomeStatus" } ], "example": 1 @@ -3102,6 +3349,27 @@ const docTemplate = `{ } } }, + "handlers.CompanyRes": { + "type": "object", + "properties": { + "admin_id": { + "type": "integer", + "example": 1 + }, + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "CompanyName" + }, + "wallet_id": { + "type": "integer", + "example": 1 + } + } + }, "handlers.CreateBetOutcomeReq": { "type": "object", "properties": { @@ -3148,7 +3416,7 @@ const docTemplate = `{ "status": { "allOf": [ { - "$ref": "#/definitions/domain.BetStatus" + "$ref": "#/definitions/domain.OutcomeStatus" } ], "example": 1 @@ -3232,6 +3500,19 @@ const docTemplate = `{ } } }, + "handlers.CreateCompanyReq": { + "type": "object", + "properties": { + "admin_id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "CompanyName" + } + } + }, "handlers.CreateManagerReq": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 536186b..ad7007a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -985,6 +985,225 @@ } } }, + "/company": { + "get": { + "description": "Gets all companies", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "company" + ], + "summary": "Gets all companies", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.CompanyRes" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "post": { + "description": "Creates a company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "company" + ], + "summary": "Create a company", + "parameters": [ + { + "description": "Creates company", + "name": "createCompany", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateCompanyReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CompanyRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/company/{id}": { + "get": { + "description": "Gets a single company by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "company" + ], + "summary": "Gets company by id", + "parameters": [ + { + "type": "integer", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CompanyRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "description": "Updates a company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "company" + ], + "summary": "Updates a company", + "parameters": [ + { + "type": "integer", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update Company", + "name": "updateCompany", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateCompanyReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CompanyRes" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "description": "Delete the company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "company" + ], + "summary": "Delete the company", + "parameters": [ + { + "type": "integer", + "description": "Company 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" + } + } + } + } + }, "/company/{id}/branch": { "get": { "description": "Gets branches by company id", @@ -1307,6 +1526,18 @@ "description": "Page size", "name": "page_size", "in": "query" + }, + { + "type": "string", + "description": "League ID Filter", + "name": "league_id", + "in": "query" + }, + { + "type": "string", + "description": "Sport ID Filter", + "name": "sport_id", + "in": "query" } ], "responses": { @@ -2688,24 +2919,17 @@ "odd_name": { "type": "string", "example": "1" + }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.OutcomeStatus" + } + ], + "example": 1 } } }, - "domain.BetStatus": { - "type": "integer", - "enum": [ - 0, - 1, - 2, - 3 - ], - "x-enum-varnames": [ - "BET_STATUS_PENDING", - "BET_STATUS_WIN", - "BET_STATUS_LOSS", - "BET_STATUS_ERROR" - ] - }, "domain.Odd": { "type": "object", "properties": { @@ -2757,6 +2981,21 @@ } } }, + "domain.OutcomeStatus": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "OUTCOME_STATUS_PENDING", + "OUTCOME_STATUS_WIN", + "OUTCOME_STATUS_LOSS", + "OUTCOME_STATUS_ERROR" + ] + }, "domain.PaymentOption": { "type": "integer", "enum": [ @@ -2861,6 +3100,14 @@ "type": "string", "example": "1" }, + "status": { + "allOf": [ + { + "$ref": "#/definitions/domain.OutcomeStatus" + } + ], + "example": 1 + }, "ticket_id": { "type": "integer", "example": 1 @@ -2968,7 +3215,7 @@ "status": { "allOf": [ { - "$ref": "#/definitions/domain.BetStatus" + "$ref": "#/definitions/domain.OutcomeStatus" } ], "example": 1 @@ -3094,6 +3341,27 @@ } } }, + "handlers.CompanyRes": { + "type": "object", + "properties": { + "admin_id": { + "type": "integer", + "example": 1 + }, + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "CompanyName" + }, + "wallet_id": { + "type": "integer", + "example": 1 + } + } + }, "handlers.CreateBetOutcomeReq": { "type": "object", "properties": { @@ -3140,7 +3408,7 @@ "status": { "allOf": [ { - "$ref": "#/definitions/domain.BetStatus" + "$ref": "#/definitions/domain.OutcomeStatus" } ], "example": 1 @@ -3224,6 +3492,19 @@ } } }, + "handlers.CreateCompanyReq": { + "type": "object", + "properties": { + "admin_id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "CompanyName" + } + } + }, "handlers.CreateManagerReq": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7a5d23b..cfa3120 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -40,19 +40,11 @@ definitions: odd_name: example: "1" type: string + status: + allOf: + - $ref: '#/definitions/domain.OutcomeStatus' + example: 1 type: object - domain.BetStatus: - enum: - - 0 - - 1 - - 2 - - 3 - type: integer - x-enum-varnames: - - BET_STATUS_PENDING - - BET_STATUS_WIN - - BET_STATUS_LOSS - - BET_STATUS_ERROR domain.Odd: properties: category: @@ -87,6 +79,18 @@ definitions: source: type: string type: object + domain.OutcomeStatus: + enum: + - 0 + - 1 + - 2 + - 3 + type: integer + x-enum-varnames: + - OUTCOME_STATUS_PENDING + - OUTCOME_STATUS_WIN + - OUTCOME_STATUS_LOSS + - OUTCOME_STATUS_ERROR domain.PaymentOption: enum: - 0 @@ -165,6 +169,10 @@ definitions: odd_name: example: "1" type: string + status: + allOf: + - $ref: '#/definitions/domain.OutcomeStatus' + example: 1 ticket_id: example: 1 type: integer @@ -243,7 +251,7 @@ definitions: type: string status: allOf: - - $ref: '#/definitions/domain.BetStatus' + - $ref: '#/definitions/domain.OutcomeStatus' example: 1 total_odds: example: 4.22 @@ -331,6 +339,21 @@ definitions: phone_number_exist: type: boolean type: object + handlers.CompanyRes: + properties: + admin_id: + example: 1 + type: integer + id: + example: 1 + type: integer + name: + example: CompanyName + type: string + wallet_id: + example: 1 + type: integer + type: object handlers.CreateBetOutcomeReq: properties: event_id: @@ -364,7 +387,7 @@ definitions: type: string status: allOf: - - $ref: '#/definitions/domain.BetStatus' + - $ref: '#/definitions/domain.OutcomeStatus' example: 1 total_odds: example: 4.22 @@ -422,6 +445,15 @@ definitions: example: "1234567890" type: string type: object + handlers.CreateCompanyReq: + properties: + admin_id: + example: 1 + type: integer + name: + example: CompanyName + type: string + type: object handlers.CreateManagerReq: properties: email: @@ -1512,6 +1544,151 @@ paths: summary: Update cashier tags: - cashier + /company: + get: + consumes: + - application/json + description: Gets all companies + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handlers.CompanyRes' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets all companies + tags: + - company + post: + consumes: + - application/json + description: Creates a company + parameters: + - description: Creates company + in: body + name: createCompany + required: true + schema: + $ref: '#/definitions/handlers.CreateCompanyReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.CompanyRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Create a company + tags: + - company + /company/{id}: + delete: + consumes: + - application/json + description: Delete the company + parameters: + - description: Company 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 the company + tags: + - company + get: + consumes: + - application/json + description: Gets a single company by id + parameters: + - description: Company ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.CompanyRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Gets company by id + tags: + - company + put: + consumes: + - application/json + description: Updates a company + parameters: + - description: Company ID + in: path + name: id + required: true + type: integer + - description: Update Company + in: body + name: updateCompany + required: true + schema: + $ref: '#/definitions/handlers.CreateCompanyReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.CompanyRes' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Updates a company + tags: + - company /company/{id}/branch: get: consumes: @@ -1721,6 +1898,14 @@ paths: in: query name: page_size type: integer + - description: League ID Filter + in: query + name: league_id + type: string + - description: Sport ID Filter + in: query + name: sport_id + type: string produces: - application/json responses: diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index 34f74a2..11a44ad 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -242,6 +242,22 @@ func (q *Queries) GetBetByID(ctx context.Context, id int64) (BetWithOutcome, err return i, err } +const UpdateBetOutcomeStatus = `-- name: UpdateBetOutcomeStatus :exec +UPDATE bet_outcomes +SET status = $1 +WHERE id = $2 +` + +type UpdateBetOutcomeStatusParams struct { + Status int32 `json:"status"` + ID int64 `json:"id"` +} + +func (q *Queries) UpdateBetOutcomeStatus(ctx context.Context, arg UpdateBetOutcomeStatusParams) error { + _, err := q.db.Exec(ctx, UpdateBetOutcomeStatus, arg.Status, arg.ID) + return err +} + const UpdateCashOut = `-- name: UpdateCashOut :exec UPDATE bets SET cashed_out = $2, diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go new file mode 100644 index 0000000..fb83066 --- /dev/null +++ b/gen/db/company.sql.go @@ -0,0 +1,122 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: company.sql + +package dbgen + +import ( + "context" +) + +const CreateCompany = `-- name: CreateCompany :one +INSERT INTO companies ( + name, + admin_id, + wallet_id + ) +VALUES ($1, $2, $3) +RETURNING id, name, admin_id, wallet_id +` + +type CreateCompanyParams struct { + Name string `json:"name"` + AdminID int64 `json:"admin_id"` + WalletID int64 `json:"wallet_id"` +} + +func (q *Queries) CreateCompany(ctx context.Context, arg CreateCompanyParams) (Company, error) { + row := q.db.QueryRow(ctx, CreateCompany, arg.Name, arg.AdminID, arg.WalletID) + var i Company + err := row.Scan( + &i.ID, + &i.Name, + &i.AdminID, + &i.WalletID, + ) + return i, err +} + +const DeleteCompany = `-- name: DeleteCompany :exec +DELETE FROM companies +WHERE id = $1 +` + +func (q *Queries) DeleteCompany(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteCompany, id) + return err +} + +const GetAllCompanies = `-- name: GetAllCompanies :many +SELECT id, name, admin_id, wallet_id +FROM companies +` + +func (q *Queries) GetAllCompanies(ctx context.Context) ([]Company, error) { + rows, err := q.db.Query(ctx, GetAllCompanies) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Company + for rows.Next() { + var i Company + if err := rows.Scan( + &i.ID, + &i.Name, + &i.AdminID, + &i.WalletID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetCompanyByID = `-- name: GetCompanyByID :one +SELECT id, name, admin_id, wallet_id +FROM companies +WHERE id = $1 +` + +func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (Company, error) { + row := q.db.QueryRow(ctx, GetCompanyByID, id) + var i Company + err := row.Scan( + &i.ID, + &i.Name, + &i.AdminID, + &i.WalletID, + ) + return i, err +} + +const UpdateCompany = `-- name: UpdateCompany :one +UPDATE companies +SET name = $1, + admin_id = $2 +WHERE id = $3 +RETURNING id, name, admin_id, wallet_id +` + +type UpdateCompanyParams struct { + Name string `json:"name"` + AdminID int64 `json:"admin_id"` + ID int64 `json:"id"` +} + +func (q *Queries) UpdateCompany(ctx context.Context, arg UpdateCompanyParams) (Company, error) { + row := q.db.QueryRow(ctx, UpdateCompany, arg.Name, arg.AdminID, arg.ID) + var i Company + err := row.Scan( + &i.ID, + &i.Name, + &i.AdminID, + &i.WalletID, + ) + return i, err +} diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 4654a3e..16fad8f 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -110,13 +110,23 @@ SELECT id, FROM events WHERE is_live = false AND status = 'upcoming' + AND ( + league_id = $3 + OR $3 IS NULL + ) + AND ( + sport_id = $4 + OR $4 IS NULL + ) ORDER BY start_time ASC LIMIT $1 OFFSET $2 ` type GetPaginatedUpcomingEventsParams struct { - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` + LeagueID pgtype.Text `json:"league_id"` + SportID pgtype.Text `json:"sport_id"` } type GetPaginatedUpcomingEventsRow struct { @@ -139,7 +149,12 @@ type GetPaginatedUpcomingEventsRow struct { } func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginatedUpcomingEventsParams) ([]GetPaginatedUpcomingEventsRow, error) { - rows, err := q.db.Query(ctx, GetPaginatedUpcomingEvents, arg.Limit, arg.Offset) + rows, err := q.db.Query(ctx, GetPaginatedUpcomingEvents, + arg.Limit, + arg.Offset, + arg.LeagueID, + arg.SportID, + ) if err != nil { return nil, err } @@ -180,10 +195,23 @@ SELECT COUNT(*) FROM events WHERE is_live = false AND status = 'upcoming' + AND ( + league_id = $1 + OR $1 IS NULL + ) + AND ( + sport_id = $2 + OR $2 IS NULL + ) ` -func (q *Queries) GetTotalEvents(ctx context.Context) (int64, error) { - row := q.db.QueryRow(ctx, GetTotalEvents) +type GetTotalEventsParams struct { + LeagueID pgtype.Text `json:"league_id"` + SportID pgtype.Text `json:"sport_id"` +} + +func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) (int64, error) { + row := q.db.QueryRow(ctx, GetTotalEvents, arg.LeagueID, arg.SportID) var count int64 err := row.Scan(&count) return count, err diff --git a/gen/db/models.go b/gen/db/models.go index 4b297d6..1be7269 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -37,6 +37,7 @@ type BetOutcome struct { OddName string `json:"odd_name"` OddHeader string `json:"odd_header"` OddHandicap string `json:"odd_handicap"` + Status int32 `json:"status"` Expires pgtype.Timestamp `json:"expires"` } @@ -97,6 +98,13 @@ type BranchOperation struct { UpdatedAt pgtype.Timestamp `json:"updated_at"` } +type Company struct { + ID int64 `json:"id"` + Name string `json:"name"` + AdminID int64 `json:"admin_id"` + WalletID int64 `json:"wallet_id"` +} + type CustomerWallet struct { ID int64 `json:"id"` CustomerID int64 `json:"customer_id"` @@ -215,6 +223,7 @@ type TicketOutcome struct { OddName string `json:"odd_name"` OddHeader string `json:"odd_header"` OddHandicap string `json:"odd_handicap"` + Status int32 `json:"status"` Expires pgtype.Timestamp `json:"expires"` } diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index 2dc219c..054372d 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -133,7 +133,7 @@ func (q *Queries) GetTicketByID(ctx context.Context, id int64) (TicketWithOutcom } const GetTicketOutcome = `-- name: GetTicketOutcome :many -SELECT id, ticket_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, expires +SELECT id, ticket_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 ticket_outcomes WHERE ticket_id = $1 ` @@ -160,6 +160,7 @@ func (q *Queries) GetTicketOutcome(ctx context.Context, ticketID int64) ([]Ticke &i.OddName, &i.OddHeader, &i.OddHandicap, + &i.Status, &i.Expires, ); err != nil { return nil, err @@ -171,3 +172,19 @@ func (q *Queries) GetTicketOutcome(ctx context.Context, ticketID int64) ([]Ticke } return items, nil } + +const UpdateTicketOutcomeStatus = `-- name: UpdateTicketOutcomeStatus :exec +UPDATE ticket_outcomes +SET status = $1 +WHERE id = $2 +` + +type UpdateTicketOutcomeStatusParams struct { + Status int32 `json:"status"` + ID int64 `json:"id"` +} + +func (q *Queries) UpdateTicketOutcomeStatus(ctx context.Context, arg UpdateTicketOutcomeStatusParams) error { + _, err := q.db.Exec(ctx, UpdateTicketOutcomeStatus, arg.Status, arg.ID) + return err +} diff --git a/internal/domain/bet.go b/internal/domain/bet.go index 1fe05f2..af9f03b 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -1,22 +1,24 @@ package domain -import "time" +import ( + "time" +) type BetOutcome struct { - ID int64 `json:"id" example:"1"` - BetID int64 `json:"bet_id" example:"1"` - EventID int64 `json:"event_id" example:"1"` - OddID int64 `json:"odd_id" example:"1"` - HomeTeamName string `json:"home_team_name" example:"Manchester"` - AwayTeamName string `json:"away_team_name" example:"Liverpool"` - MarketID int64 `json:"market_id" example:"1"` - MarketName string `json:"market_name" example:"Fulltime Result"` - Odd float32 `json:"odd" example:"1.5"` - OddName string `json:"odd_name" example:"1"` - OddHeader string `json:"odd_header" example:"1"` - OddHandicap string `json:"odd_handicap" example:"1"` - - Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` + ID int64 `json:"id" example:"1"` + BetID int64 `json:"bet_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` + HomeTeamName string `json:"home_team_name" example:"Manchester"` + AwayTeamName string `json:"away_team_name" example:"Liverpool"` + MarketID int64 `json:"market_id" example:"1"` + MarketName string `json:"market_name" example:"Fulltime Result"` + Odd float32 `json:"odd" example:"1.5"` + OddName string `json:"odd_name" example:"1"` + OddHeader string `json:"odd_header" example:"1"` + OddHandicap string `json:"odd_handicap" example:"1"` + Status OutcomeStatus `json:"status" example:"1"` + Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } type CreateBetOutcome struct { @@ -34,22 +36,13 @@ type CreateBetOutcome struct { Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } -type BetStatus int - -const ( - BET_STATUS_PENDING BetStatus = iota - BET_STATUS_WIN - BET_STATUS_LOSS - BET_STATUS_ERROR -) - // If it is a ShopBet then UserID will be the cashier // If it is a DigitalBet then UserID will be the user and the branchID will be 0 or nil type Bet struct { ID int64 Amount Currency TotalOdds float32 - Status BetStatus + Status OutcomeStatus FullName string PhoneNumber string BranchID ValidInt64 // Can Be Nullable @@ -63,7 +56,7 @@ type GetBet struct { ID int64 Amount Currency TotalOdds float32 - Status BetStatus + Status OutcomeStatus FullName string PhoneNumber string BranchID ValidInt64 // Can Be Nullable @@ -77,7 +70,7 @@ type GetBet struct { type CreateBet struct { Amount Currency TotalOdds float32 - Status BetStatus + Status OutcomeStatus FullName string PhoneNumber string BranchID ValidInt64 // Can Be Nullable @@ -86,8 +79,3 @@ type CreateBet struct { CashoutID string } -func (b BetStatus) String() string { - return []string{"Pending", "Win", "Loss", "Error"}[b] -} - -// func isBetStatusValid() diff --git a/internal/domain/common.go b/internal/domain/common.go index 88273f3..e3a5e52 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -35,7 +35,17 @@ func (m Currency) String() string { x := float32(m) x = x / 100 return fmt.Sprintf("$%.2f", x) - } +type OutcomeStatus int +const ( + OUTCOME_STATUS_PENDING OutcomeStatus = iota + OUTCOME_STATUS_WIN + OUTCOME_STATUS_LOSS + OUTCOME_STATUS_ERROR +) + +func (b OutcomeStatus) String() string { + return []string{"Pending", "Win", "Loss", "Error"}[b] +} diff --git a/internal/domain/company.go b/internal/domain/company.go index cf2a807..989f306 100644 --- a/internal/domain/company.go +++ b/internal/domain/company.go @@ -4,7 +4,18 @@ package domain // they are the ones that manage the branches and branch managers // they will have their own wallet that they will use to distribute to the branch wallets type Company struct { - ID int64 - Name string - -} \ No newline at end of file + ID int64 + Name string + AdminID int64 + WalletID int64 +} +type CreateCompany struct { + Name string + AdminID int64 + WalletID int64 +} + +type UpdateCompany struct { + Name string + AdminID int64 +} diff --git a/internal/domain/ticket.go b/internal/domain/ticket.go index 8fedd64..15dd180 100644 --- a/internal/domain/ticket.go +++ b/internal/domain/ticket.go @@ -3,19 +3,20 @@ package domain import "time" type TicketOutcome struct { - ID int64 `json:"id" example:"1"` - TicketID int64 `json:"ticket_id" example:"1"` - EventID int64 `json:"event_id" example:"1"` - HomeTeamName string `json:"home_team_name" example:"Manchester"` - AwayTeamName string `json:"away_team_name" example:"Liverpool"` - MarketID int64 `json:"market_id" example:"1"` - MarketName string `json:"market_name" example:"Fulltime Result"` - OddID int64 `json:"odd_id" example:"1"` - Odd float32 `json:"odd" example:"1.5"` - OddName string `json:"odd_name" example:"1"` - OddHeader string `json:"odd_header" example:"1"` - OddHandicap string `json:"odd_handicap" example:"1"` - Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` + ID int64 `json:"id" example:"1"` + TicketID int64 `json:"ticket_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + HomeTeamName string `json:"home_team_name" example:"Manchester"` + AwayTeamName string `json:"away_team_name" example:"Liverpool"` + MarketID int64 `json:"market_id" example:"1"` + MarketName string `json:"market_name" example:"Fulltime Result"` + OddID int64 `json:"odd_id" example:"1"` + Odd float32 `json:"odd" example:"1.5"` + OddName string `json:"odd_name" example:"1"` + OddHeader string `json:"odd_header" example:"1"` + OddHandicap string `json:"odd_handicap" example:"1"` + Status OutcomeStatus `json:"status" example:"1"` + Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } type CreateTicketOutcome struct { diff --git a/internal/repository/bet.go b/internal/repository/bet.go index b486756..c19db94 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -14,7 +14,7 @@ func convertDBBet(bet dbgen.Bet) domain.Bet { ID: bet.ID, Amount: domain.Currency(bet.Amount), TotalOdds: bet.TotalOdds, - Status: domain.BetStatus(bet.Status), + Status: domain.OutcomeStatus(bet.Status), FullName: bet.FullName, PhoneNumber: bet.PhoneNumber, BranchID: domain.ValidInt64{ @@ -48,6 +48,7 @@ func convertDBBetOutcomes(bet dbgen.BetWithOutcome) domain.GetBet { OddName: outcome.OddName, OddHeader: outcome.OddHeader, OddHandicap: outcome.OddHandicap, + Status: domain.OutcomeStatus(outcome.Status), Expires: outcome.Expires.Time, }) } @@ -55,7 +56,7 @@ func convertDBBetOutcomes(bet dbgen.BetWithOutcome) domain.GetBet { ID: bet.ID, Amount: domain.Currency(bet.Amount), TotalOdds: bet.TotalOdds, - Status: domain.BetStatus(bet.Status), + Status: domain.OutcomeStatus(bet.Status), FullName: bet.FullName, PhoneNumber: bet.PhoneNumber, BranchID: domain.ValidInt64{ @@ -197,7 +198,7 @@ func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) err return err } -func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.BetStatus) error { +func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { err := s.queries.UpdateStatus(ctx, dbgen.UpdateStatusParams{ ID: id, Status: int32(status), @@ -205,6 +206,14 @@ func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.BetSta return err } +func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { + err := s.queries.UpdateBetOutcomeStatus(ctx, dbgen.UpdateBetOutcomeStatusParams{ + Status: int32(status), + ID: id, + }) + return err +} + func (s *Store) DeleteBet(ctx context.Context, id int64) error { return s.queries.DeleteBet(ctx, id) } diff --git a/internal/repository/company.go b/internal/repository/company.go new file mode 100644 index 0000000..0e52cb6 --- /dev/null +++ b/internal/repository/company.go @@ -0,0 +1,74 @@ +package repository + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +func convertCreateCompany(company domain.CreateCompany) dbgen.CreateCompanyParams { + return dbgen.CreateCompanyParams{ + Name: company.Name, + AdminID: company.AdminID, + WalletID: company.WalletID, + } +} + +func convertDBCompany(dbCompany dbgen.Company) domain.Company { + return domain.Company{ + ID: dbCompany.ID, + Name: dbCompany.Name, + AdminID: dbCompany.AdminID, + WalletID: dbCompany.WalletID, + } +} + +func (s *Store) CreateCompany(ctx context.Context, company domain.CreateCompany) (domain.Company, error) { + dbCompany, err := s.queries.CreateCompany(ctx, convertCreateCompany(company)) + if err != nil { + return domain.Company{}, err + } + return convertDBCompany(dbCompany), nil +} + +func (s *Store) GetAllCompanies(ctx context.Context) ([]domain.Company, error) { + dbCompanies, err := s.queries.GetAllCompanies(ctx) + if err != nil { + return nil, err + } + + var companies []domain.Company = make([]domain.Company, 0, len(dbCompanies)) + for _, dbCompany := range dbCompanies { + companies = append(companies, convertDBCompany(dbCompany)) + } + + return companies, nil +} + +func (s *Store) GetCompanyByID(ctx context.Context, id int64) (domain.Company, error) { + dbCompany, err := s.queries.GetCompanyByID(ctx, id) + + if err != nil { + return domain.Company{}, err + } + return convertDBCompany(dbCompany), nil +} + +func (s *Store) UpdateCompany(ctx context.Context, id int64, company domain.UpdateCompany) (domain.Company, error) { + dbCompany, err := s.queries.UpdateCompany(ctx, dbgen.UpdateCompanyParams{ + ID: id, + Name: company.Name, + AdminID: company.AdminID, + }) + + if err != nil { + return domain.Company{}, err + } + + return convertDBCompany(dbCompany), nil +} + +func (s *Store) DeleteCompany(ctx context.Context, id int64) error { + return s.queries.DeleteCompany(ctx, id) +} diff --git a/internal/repository/event.go b/internal/repository/event.go index d0ff7d6..b64973d 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -2,6 +2,7 @@ package repository import ( "context" + "math" "time" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" @@ -87,8 +88,16 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven } return upcomingEvents, nil } -func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32) ([]domain.UpcomingEvent, int64, error) { +func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32, leagueID domain.ValidString, sportID domain.ValidString) ([]domain.UpcomingEvent, int64, error) { events, err := s.queries.GetPaginatedUpcomingEvents(ctx, dbgen.GetPaginatedUpcomingEventsParams{ + LeagueID: pgtype.Text{ + String: leagueID.Value, + Valid: leagueID.Valid, + }, + SportID: pgtype.Text{ + String: sportID.Value, + Valid: sportID.Valid, + }, Limit: limit, Offset: offset * limit, }) @@ -115,13 +124,22 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, off StartTime: e.StartTime.Time.UTC(), } } - totalCount, err := s.queries.GetTotalEvents(ctx) + totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{ + LeagueID: pgtype.Text{ + String: leagueID.Value, + Valid: leagueID.Valid, + }, + SportID: pgtype.Text{ + String: sportID.Value, + Valid: sportID.Valid, + }, + }) if err != nil { return nil, 0, err } - numberOfPages := (totalCount) / int64(limit) - return upcomingEvents, numberOfPages, nil + numberOfPages := math.Ceil(float64(totalCount) / float64(limit)) + return upcomingEvents, int64(numberOfPages), nil } func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { event, err := s.queries.GetUpcomingByID(ctx, ID) diff --git a/internal/repository/ticket.go b/internal/repository/ticket.go index 911ad9e..5083f65 100644 --- a/internal/repository/ticket.go +++ b/internal/repository/ticket.go @@ -34,6 +34,7 @@ func convertDBTicketOutcomes(ticket dbgen.TicketWithOutcome) domain.GetTicket { OddName: outcome.OddName, OddHeader: outcome.OddHeader, OddHandicap: outcome.OddHandicap, + Status: domain.OutcomeStatus(outcome.Status), Expires: outcome.Expires.Time, }) } @@ -122,6 +123,14 @@ func (s *Store) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) { return result, nil } +func (s *Store) UpdateTicketOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { + err := s.queries.UpdateTicketOutcomeStatus(ctx, dbgen.UpdateTicketOutcomeStatusParams{ + Status: int32(status), + ID: id, + }) + return err +} + func (s *Store) DeleteOldTickets(ctx context.Context) error { return s.queries.DeleteOldTickets(ctx) } diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index 8066c50..d5ea609 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -14,6 +14,7 @@ type BetStore interface { GetAllBets(ctx context.Context) ([]domain.GetBet, error) GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.GetBet, error) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error - UpdateStatus(ctx context.Context, id int64, status domain.BetStatus) error + UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error + UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error DeleteBet(ctx context.Context, id int64) error } diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index a464094..1ff9565 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -17,6 +17,7 @@ func NewService(betStore BetStore) *Service { } func (s *Service) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) { + return s.betStore.CreateBet(ctx, bet) } @@ -42,10 +43,14 @@ func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) e return s.betStore.UpdateCashOut(ctx, id, cashedOut) } -func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.BetStatus) error { +func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { return s.betStore.UpdateStatus(ctx, id, status) } +func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { + return s.betStore.UpdateBetOutcomeStatus(ctx, id, status) +} + func (s *Service) DeleteBet(ctx context.Context, id int64) error { return s.betStore.DeleteBet(ctx, id) } diff --git a/internal/services/company/port.go b/internal/services/company/port.go new file mode 100644 index 0000000..5263ed1 --- /dev/null +++ b/internal/services/company/port.go @@ -0,0 +1,15 @@ +package company + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type CompanyStore interface { + CreateCompany(ctx context.Context, company domain.CreateCompany) (domain.Company, error) + GetAllCompanies(ctx context.Context) ([]domain.Company, error) + GetCompanyByID(ctx context.Context, id int64) (domain.Company, error) + UpdateCompany(ctx context.Context, id int64, company domain.UpdateCompany) (domain.Company, error) + DeleteCompany(ctx context.Context, id int64) error +} diff --git a/internal/services/company/service.go b/internal/services/company/service.go new file mode 100644 index 0000000..b5b6f7e --- /dev/null +++ b/internal/services/company/service.go @@ -0,0 +1,35 @@ +package company + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type Service struct { + companyStore CompanyStore +} + +func NewService(companyStore CompanyStore) *Service { + return &Service{ + companyStore: companyStore, + } +} + +func (s *Service) CreateCompany(ctx context.Context, company domain.CreateCompany) (domain.Company, error) { + return s.companyStore.CreateCompany(ctx, company) +} +func (s *Service) GetAllCompanies(ctx context.Context) ([]domain.Company, error) { + return s.companyStore.GetAllCompanies(ctx) +} + +func (s *Service) GetCompanyByID(ctx context.Context, id int64) (domain.Company, error) { + return s.companyStore.GetCompanyByID(ctx, id) +} + +func (s *Service) UpdateCompany(ctx context.Context, id int64, company domain.UpdateCompany) (domain.Company, error) { + return s.companyStore.UpdateCompany(ctx, id, company) +} +func (s *Service) DeleteCompany(ctx context.Context, id int64) error { + return s.companyStore.DeleteCompany(ctx, id) +} diff --git a/internal/services/event/port.go b/internal/services/event/port.go index e1c3c64..1404b09 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -10,6 +10,6 @@ type Service interface { FetchLiveEvents(ctx context.Context) error FetchUpcomingEvents(ctx context.Context) error GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) - GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32) ([]domain.UpcomingEvent, int64, error) + GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32, leagueID domain.ValidString, sportID domain.ValidString) ([]domain.UpcomingEvent, int64, error) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 5ab1ceb..4eb5601 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -178,8 +178,8 @@ func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEv return s.store.GetAllUpcomingEvents(ctx) } -func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32) ([]domain.UpcomingEvent, int64, error) { - return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset) +func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32, leagueID domain.ValidString, sportID domain.ValidString) ([]domain.UpcomingEvent, int64, error) { + return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset, leagueID, sportID) } func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { diff --git a/internal/services/ticket/port.go b/internal/services/ticket/port.go index ae531c6..930026e 100644 --- a/internal/services/ticket/port.go +++ b/internal/services/ticket/port.go @@ -11,6 +11,7 @@ type TicketStore interface { CreateTicketOutcome(ctx context.Context, outcomes []domain.CreateTicketOutcome) (int64, error) GetTicketByID(ctx context.Context, id int64) (domain.GetTicket, error) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) + UpdateTicketOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error DeleteOldTickets(ctx context.Context) error DeleteTicket(ctx context.Context, id int64) error } diff --git a/internal/services/ticket/service.go b/internal/services/ticket/service.go index 46036f4..1d86313 100644 --- a/internal/services/ticket/service.go +++ b/internal/services/ticket/service.go @@ -30,6 +30,10 @@ func (s *Service) GetTicketByID(ctx context.Context, id int64) (domain.GetTicket func (s *Service) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) { return s.ticketStore.GetAllTickets(ctx) } + +func (s *Service) UpdateTicketOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { + return s.ticketStore.UpdateTicketOutcomeStatus(ctx, id, status) +} func (s *Service) DeleteTicket(ctx context.Context, id int64) error { return s.ticketStore.DeleteTicket(ctx, id) } diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go index 2644a39..ced664d 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -91,6 +91,8 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain. return s.walletStore.UpdateBalance(ctx, id, wallet.Balance+amount) } + + func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive bool) error { return s.walletStore.UpdateWalletActive(ctx, id, isActive) } diff --git a/internal/web_server/app.go b/internal/web_server/app.go index b41dcf0..d999648 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -7,6 +7,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" @@ -34,6 +35,7 @@ type App struct { walletSvc *wallet.Service transactionSvc *transaction.Service branchSvc *branch.Service + companySvc *company.Service validator *customvalidator.CustomValidator JwtConfig jwtutil.JwtConfig Logger *slog.Logger @@ -52,6 +54,7 @@ func NewApp( walletSvc *wallet.Service, transactionSvc *transaction.Service, branchSvc *branch.Service, + companySvc *company.Service, notidicationStore notificationservice.NotificationStore, prematchSvc *odds.ServiceImpl, eventSvc event.Service, @@ -83,6 +86,7 @@ func NewApp( walletSvc: walletSvc, transactionSvc: transactionSvc, branchSvc: branchSvc, + companySvc: companySvc, NotidicationStore: notidicationStore, Logger: logger, prematchSvc: prematchSvc, diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index b04a9f3..a6b07d4 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -21,82 +21,47 @@ import ( ) type CreateBetOutcomeReq struct { - // BetID int64 `json:"bet_id" example:"1"` EventID int64 `json:"event_id" example:"1"` OddID int64 `json:"odd_id" example:"1"` MarketID int64 `json:"market_id" example:"1"` - // HomeTeamName string `json:"home_team_name" example:"Manchester"` - // AwayTeamName string `json:"away_team_name" example:"Liverpool"` - // MarketName string `json:"market_name" example:"Fulltime Result"` - // Odd float32 `json:"odd" example:"1.5"` - // OddName string `json:"odd_name" example:"1"` - // Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` -} - -type NullableInt64 struct { - Value int64 - Valid bool -} - -func (n *NullableInt64) UnmarshalJSON(data []byte) error { - if string(data) == "null" { - n.Valid = false - return nil - } - - var value int64 - if err := json.Unmarshal(data, &value); err != nil { - return err - } - - n.Value = value - n.Valid = true - return nil -} - -func (n NullableInt64) MarshalJSON() ([]byte, error) { - if !n.Valid { - return []byte("null"), nil - } - return json.Marshal(n.Value) } type CreateBetReq struct { Outcomes []CreateBetOutcomeReq `json:"outcomes"` Amount float32 `json:"amount" example:"100.0"` TotalOdds float32 `json:"total_odds" example:"4.22"` - Status domain.BetStatus `json:"status" example:"1"` + Status domain.OutcomeStatus `json:"status" example:"1"` FullName string `json:"full_name" example:"John"` PhoneNumber string `json:"phone_number" example:"1234567890"` IsShopBet bool `json:"is_shop_bet" example:"false"` } 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.BetStatus `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"` + 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.BetStatus `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"` + 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 { @@ -152,15 +117,12 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, if err := c.BodyParser(&req); err != nil { logger.Error("CreateBetReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := validator.Validate(c, req) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } // Validating user by role @@ -174,9 +136,7 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, branch, err := branchSvc.GetBranchByCashier(c.Context(), user.ID) if err != nil { logger.Error("CreateBetReq failed, branch id invalid") - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Branch ID invalid", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } // Deduct a percentage of the amount @@ -186,9 +146,7 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, if err != nil { logger.Error("CreateBetReq failed, unable to deduct from WalletID") - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Unable to deduct from branch wallet", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } bet, err = betSvc.CreateBet(c.Context(), domain.CreateBet{ @@ -240,8 +198,7 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, // TODO Validate Outcomes Here and make sure they didn't expire // Validation for creating tickets if len(req.Outcomes) > 30 { - response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil) } var outcomes []domain.CreateBetOutcome = make([]domain.CreateBetOutcome, 0, len(req.Outcomes)) for _, outcome := range req.Outcomes { @@ -250,22 +207,19 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, oddIDStr := strconv.FormatInt(outcome.OddID, 10) event, err := eventSvc.GetUpcomingEventByID(c.Context(), eventIDStr) if err != nil { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid event id", err, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid event id", err, nil) } // Checking to make sure the event hasn't already started currentTime := time.Now() if event.StartTime.Before(currentTime) { - response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) } odds, err := oddSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr) if err != nil { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil) } type rawOddType struct { ID string @@ -291,8 +245,7 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, } if !isOddFound { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil) } parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32) @@ -454,15 +407,12 @@ func UpdateCashOut(logger *slog.Logger, betSvc *bet.Service, var req UpdateCashOutReq if err := c.BodyParser(&req); err != nil { logger.Error("UpdateCashOutReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := validator.Validate(c, req) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } err = betSvc.UpdateCashOut(c.Context(), id, req.CashedOut) diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index ab97316..0741049 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -116,15 +116,12 @@ func CreateBranch(logger *slog.Logger, branchSvc *branch.Service, walletSvc *wal if err := c.BodyParser(&req); err != nil { logger.Error("CreateBranchReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := validator.Validate(c, req) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } // Create Branch Wallet @@ -194,14 +191,12 @@ func CreateSupportedOperation(logger *slog.Logger, branchSvc *branch.Service, va if err := c.BodyParser(&req); err != nil { logger.Error("CreateSupportedOperationReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := validator.Validate(c, req) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } operation, err := branchSvc.CreateSupportedOperation(c.Context(), domain.CreateSupportedOperation{ Name: req.Name, @@ -241,15 +236,12 @@ func CreateBranchOperation(logger *slog.Logger, branchSvc *branch.Service, valid var req CreateBranchOperationReq if err := c.BodyParser(&req); err != nil { logger.Error("CreateBranchOperationReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := validator.Validate(c, req) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } err := branchSvc.CreateBranchOperation(c.Context(), domain.CreateBranchOperation{ @@ -383,6 +375,7 @@ func GetBranchByCompanyID(logger *slog.Logger, branchSvc *branch.Service, valida // @Router /branch [get] func GetAllBranches(logger *slog.Logger, branchSvc *branch.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { + // TODO: Limit the get all branches to only the companies for branch manager and cashiers branches, err := branchSvc.GetAllBranches(c.Context()) if err != nil { logger.Error("Failed to get branches", "error", err) @@ -566,14 +559,11 @@ func UpdateBranch(logger *slog.Logger, branchSvc *branch.Service, validator *cus if err := c.BodyParser(&req); err != nil { logger.Error("CreateBranchReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := validator.Validate(c, req) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } branch, err := branchSvc.UpdateBranch(c.Context(), id, domain.UpdateBranch{ diff --git a/internal/web_server/handlers/cashier.go b/internal/web_server/handlers/cashier.go index b2b6418..ffce3ae 100644 --- a/internal/web_server/handlers/cashier.go +++ b/internal/web_server/handlers/cashier.go @@ -40,14 +40,11 @@ func CreateCashier(logger *slog.Logger, userSvc *user.Service, branchSvc *branch var req CreateCashierReq if err := c.BodyParser(&req); err != nil { logger.Error("RegisterUser failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := validator.Validate(c, req) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } userRequest := domain.CreateUserReq{ FirstName: req.FirstName, @@ -60,19 +57,15 @@ func CreateCashier(logger *slog.Logger, userSvc *user.Service, branchSvc *branch newUser, err := userSvc.CreateUser(c.Context(), userRequest) if err != nil { logger.Error("CreateCashier failed", "error", err) - response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create cashier", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create cashier", nil, nil) } err = branchSvc.CreateBranchCashier(c.Context(), req.BranchID, newUser.ID) if err != nil { logger.Error("CreateCashier failed", "error", err) - response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create cashier", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create cashier", nil, nil) } - - response.WriteJSON(c, fiber.StatusOK, "Cashier created successfully", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusOK, "Cashier created successfully", nil, nil) } } @@ -119,15 +112,13 @@ func GetAllCashiers(logger *slog.Logger, userSvc *user.Service, validator *custo // } // valErrs, ok := validator.Validate(c, filter) // if !ok { - // response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - // return nil + // return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) // } cashiers, err := userSvc.GetAllCashiers(c.Context()) if err != nil { logger.Error("GetAllCashiers failed", "error", err) - response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil) } var result []GetCashierRes @@ -148,8 +139,8 @@ func GetAllCashiers(logger *slog.Logger, userSvc *user.Service, validator *custo Suspended: cashier.Suspended, }) } - response.WriteJSON(c, fiber.StatusOK, "Cashiers retrieved successfully", result, nil) - return nil + + return response.WriteJSON(c, fiber.StatusOK, "Cashiers retrieved successfully", result, nil) } } @@ -177,22 +168,19 @@ func UpdateCashier(logger *slog.Logger, userSvc *user.Service, validator *custom var req updateUserReq if err := c.BodyParser(&req); err != nil { logger.Error("UpdateCashier failed", "error", err) - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) } valErrs, ok := validator.Validate(c, req) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } cashierIdStr := c.Params("id") cashierId, err := strconv.ParseInt(cashierIdStr, 10, 64) if err != nil { logger.Error("UpdateCashier failed", "error", err) - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashier ID", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashier ID", nil, nil) } err = userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{ UserId: cashierId, @@ -212,11 +200,9 @@ func UpdateCashier(logger *slog.Logger, userSvc *user.Service, validator *custom ) if err != nil { logger.Error("UpdateCashier failed", "error", err) - response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update cashier", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update cashier", nil, nil) } - response.WriteJSON(c, fiber.StatusOK, "Cashier updated successfully", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusOK, "Cashier updated successfully", nil, nil) } } diff --git a/internal/web_server/handlers/company_handler.go b/internal/web_server/handlers/company_handler.go new file mode 100644 index 0000000..0fb9449 --- /dev/null +++ b/internal/web_server/handlers/company_handler.go @@ -0,0 +1,229 @@ +package handlers + +import ( + "log/slog" + "strconv" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" + "github.com/gofiber/fiber/v2" +) + +type CreateCompanyReq struct { + Name string `json:"name" example:"CompanyName"` + AdminID int64 `json:"admin_id" example:"1"` +} + +type CompanyRes struct { + ID int64 `json:"id" example:"1"` + Name string `json:"name" example:"CompanyName"` + AdminID int64 `json:"admin_id" example:"1"` + WalletID int64 `json:"wallet_id" example:"1"` +} + +func convertCompany(company domain.Company) CompanyRes { + return CompanyRes{ + ID: company.ID, + Name: company.Name, + AdminID: company.AdminID, + WalletID: company.WalletID, + } +} + +// CreateCompany godoc +// @Summary Create a company +// @Description Creates a company +// @Tags company +// @Accept json +// @Produce json +// @Param createCompany body CreateCompanyReq true "Creates company" +// @Success 200 {object} CompanyRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /company [post] +func CreateCompany(logger *slog.Logger, companySvc *company.Service, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + var req CreateCompanyReq + if err := c.BodyParser(&req); err != nil { + logger.Error("CreateCompanyReq failed", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) + } + + valErrs, ok := validator.Validate(c, req) + if !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + // Create Branch Wallet + newWallet, err := walletSvc.CreateWallet(c.Context(), domain.CreateWallet{ + IsWithdraw: false, + IsBettable: true, + IsTransferable: true, + UserID: req.AdminID, + }) + + if err != nil { + logger.Error("Create Company Wallet failed", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create company wallet", err, nil) + } + + company, err := companySvc.CreateCompany(c.Context(), domain.CreateCompany{ + Name: req.Name, + AdminID: req.AdminID, + WalletID: newWallet.ID, + }) + + if err != nil { + logger.Error("CreateCompanyReq failed", "error", err) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Internal server error", + }) + } + + res := convertCompany(company) + + return response.WriteJSON(c, fiber.StatusCreated, "Company Created", res, nil) + + } +} + +// GetAllCompanies godoc +// @Summary Gets all companies +// @Description Gets all companies +// @Tags company +// @Accept json +// @Produce json +// @Success 200 {array} CompanyRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /company [get] +func GetAllCompanies(logger *slog.Logger, companySvc *company.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + companies, err := companySvc.GetAllCompanies(c.Context()) + if err != nil { + logger.Error("Failed to get companies", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get companies", err, nil) + } + + var result []CompanyRes = make([]CompanyRes, 0, len(companies)) + + for _, company := range companies { + result = append(result, convertCompany(company)) + } + + return response.WriteJSON(c, fiber.StatusOK, "All Companies retrieved", result, nil) + } +} + +// GetCompanyByID godoc +// @Summary Gets company by id +// @Description Gets a single company by id +// @Tags company +// @Accept json +// @Produce json +// @Param id path int true "Company ID" +// @Success 200 {object} CompanyRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /company/{id} [get] +func GetCompanyByID(logger *slog.Logger, companySvc *company.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + companyID := c.Params("id") + id, err := strconv.ParseInt(companyID, 10, 64) + if err != nil { + logger.Error("Invalid company ID", "companyID", companyID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid company ID", err, nil) + } + + company, err := companySvc.GetCompanyByID(c.Context(), id) + + if err != nil { + logger.Error("Failed to get company by ID", "companyID", id, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to company branch", err, nil) + } + + res := convertCompany(company) + + return response.WriteJSON(c, fiber.StatusOK, "Company retrieved successfully", res, nil) + } +} + +// UpdateCompany godoc +// @Summary Updates a company +// @Description Updates a company +// @Tags company +// @Accept json +// @Produce json +// @Param id path int true "Company ID" +// @Param updateCompany body CreateCompanyReq true "Update Company" +// @Success 200 {object} CompanyRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /company/{id} [put] +func UpdateCompany(logger *slog.Logger, companySvc *company.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + companyID := c.Params("id") + id, err := strconv.ParseInt(companyID, 10, 64) + if err != nil { + logger.Error("Invalid company ID", "companyID", companyID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid company ID", err, nil) + } + + var req CreateCompanyReq + if err := c.BodyParser(&req); err != nil { + logger.Error("CreateCompanyReq failed", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) + } + valErrs, ok := validator.Validate(c, req) + if !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + company, err := companySvc.UpdateCompany(c.Context(), id, domain.UpdateCompany{ + Name: req.Name, + AdminID: req.AdminID, + }) + + if err != nil { + logger.Error("Failed to update company", "companyID", id, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update company", err, nil) + } + + res := convertCompany(company) + + return response.WriteJSON(c, fiber.StatusOK, "Company Updated", res, nil) + } +} + +// DeleteCompany godoc +// @Summary Delete the company +// @Description Delete the company +// @Tags company +// @Accept json +// @Produce json +// @Param id path int true "Company ID"" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /company/{id} [delete] +func DeleteCompany(logger *slog.Logger, companySvc *company.Service, validator *customvalidator.CustomValidator) fiber.Handler { + return func(c *fiber.Ctx) error { + companyID := c.Params("id") + id, err := strconv.ParseInt(companyID, 10, 64) + + if err != nil { + logger.Error("Invalid Company ID", "companyID", companyID, "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid Company ID", err, nil) + } + + err = companySvc.DeleteCompany(c.Context(), id) + if err != nil { + logger.Error("Failed to delete by ID", "Company ID", id, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to Delete Company", err, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Company removed successfully", nil, nil) + } +} diff --git a/internal/web_server/handlers/manager.go b/internal/web_server/handlers/manager.go index be428c9..577420d 100644 --- a/internal/web_server/handlers/manager.go +++ b/internal/web_server/handlers/manager.go @@ -36,14 +36,11 @@ func CreateManager(logger *slog.Logger, userSvc *user.Service, validator *custom var req CreateManagerReq if err := c.BodyParser(&req); err != nil { logger.Error("RegisterUser failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := validator.Validate(c, req) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } user := domain.CreateUserReq{ FirstName: req.FirstName, @@ -56,11 +53,9 @@ func CreateManager(logger *slog.Logger, userSvc *user.Service, validator *custom _, err := userSvc.CreateUser(c.Context(), user) if err != nil { logger.Error("CreateManagers failed", "error", err) - response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create Managers", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create Managers", nil, nil) } - response.WriteJSON(c, fiber.StatusOK, "Managers created successfully", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusOK, "Managers created successfully", nil, nil) } } @@ -91,17 +86,15 @@ func GetAllManagers(logger *slog.Logger, userSvc *user.Service, validator *custo } valErrs, ok := validator.Validate(c, filter) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } Managers, err := userSvc.GetAllUsers(c.Context(), filter) if err != nil { logger.Error("GetAllManagers failed", "error", err) - response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get Managers", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get Managers", nil, nil) } - response.WriteJSON(c, fiber.StatusOK, "Managers retrieved successfully", Managers, nil) - return nil + + return response.WriteJSON(c, fiber.StatusOK, "Managers retrieved successfully", Managers, nil) } } @@ -123,22 +116,19 @@ func UPdateManagers(logger *slog.Logger, userSvc *user.Service, validator *custo var req updateUserReq if err := c.BodyParser(&req); err != nil { logger.Error("UpdateManagers failed", "error", err) - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil) } valErrs, ok := validator.Validate(c, req) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } ManagersIdStr := c.Params("id") ManagersId, err := strconv.ParseInt(ManagersIdStr, 10, 64) if err != nil { logger.Error("UpdateManagers failed", "error", err) - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid Managers ID", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid Managers ID", nil, nil) } err = userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{ UserId: ManagersId, @@ -158,11 +148,9 @@ func UPdateManagers(logger *slog.Logger, userSvc *user.Service, validator *custo ) if err != nil { logger.Error("UpdateManagers failed", "error", err) - response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update Managers", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update Managers", nil, nil) } - response.WriteJSON(c, fiber.StatusOK, "Managers updated successfully", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusOK, "Managers updated successfully", nil, nil) } } diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index 493bf94..10df2a5 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -4,6 +4,7 @@ import ( "log/slog" "strconv" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" @@ -98,6 +99,8 @@ func GetRawOddsByMarketID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fi // @Produce json // @Param page query int false "Page number" // @Param page_size query int false "Page size" +// @Param league_id query string false "League ID Filter" +// @Param sport_id query string false "Sport ID Filter" // @Success 200 {array} domain.UpcomingEvent // @Failure 500 {object} response.APIResponse // @Router /prematch/events [get] @@ -105,8 +108,20 @@ func GetAllUpcomingEvents(logger *slog.Logger, eventSvc event.Service) fiber.Han return func(c *fiber.Ctx) error { page := c.QueryInt("page", 1) pageSize := c.QueryInt("page_size", 10) + leagueIDQuery := c.Query("league_id") + sportIDQuery := c.Query("sport_id") + + leagueID := domain.ValidString{ + Value: leagueIDQuery, + Valid: leagueIDQuery != "", + } + sportID := domain.ValidString{ + Value: sportIDQuery, + Valid: sportIDQuery != "", + } + + events, total, err := eventSvc.GetPaginatedUpcomingEvents(c.Context(), int32(pageSize), int32(page)-1, leagueID, sportID) - events, total, err := eventSvc.GetPaginatedUpcomingEvents(c.Context(), int32(pageSize), int32(page) - 1) if err != nil { return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve all upcoming events", nil, nil) } diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index bf1bb87..5011f24 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -38,6 +38,12 @@ type CreateTicketRes struct { FastCode int64 `json:"fast_code" example:"1234"` CreatedNumber int64 `json:"created_number" example:"3"` } +type TicketRes struct { + ID int64 `json:"id" example:"1"` + Outcomes []domain.TicketOutcome `json:"outcomes"` + Amount float32 `json:"amount" example:"100.0"` + TotalOdds float32 `json:"total_odds" example:"4.22"` +} // CreateTicket godoc // @Summary Create a temporary ticket @@ -55,22 +61,18 @@ func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, eventSvc event var req CreateTicketReq if err := c.BodyParser(&req); err != nil { logger.Error("CreateTicketReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := validator.Validate(c, req) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } // TODO Validate Outcomes Here and make sure they didn't expire // Validation for creating tickets if len(req.Outcomes) > 30 { - response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil) } var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes)) for _, outcome := range req.Outcomes { @@ -79,22 +81,19 @@ func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, eventSvc event oddIDStr := strconv.FormatInt(outcome.OddID, 10) event, err := eventSvc.GetUpcomingEventByID(c.Context(), eventIDStr) if err != nil { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid event id", err, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid event id", err, nil) } // Checking to make sure the event hasn't already started currentTime := time.Now() if event.StartTime.Before(currentTime) { - response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) } odds, err := oddSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr) if err != nil { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil) } type rawOddType struct { ID string @@ -120,8 +119,7 @@ func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, eventSvc event } if !isOddFound { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil) } parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32) @@ -173,13 +171,6 @@ func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, eventSvc event } } -type TicketRes struct { - ID int64 `json:"id" example:"1"` - Outcomes []domain.TicketOutcome `json:"outcomes"` - Amount float32 `json:"amount" example:"100.0"` - TotalOdds float32 `json:"total_odds" example:"4.22"` -} - // GetTicketByID godoc // @Summary Get ticket by ID // @Description Retrieve ticket details by ticket ID diff --git a/internal/web_server/handlers/transaction_handler.go b/internal/web_server/handlers/transaction_handler.go index 7cf85e1..3819c3f 100644 --- a/internal/web_server/handlers/transaction_handler.go +++ b/internal/web_server/handlers/transaction_handler.go @@ -97,9 +97,7 @@ func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service, branch, err := branchSvc.GetBranchByID(c.Context(), 1) if err != nil { logger.Error("CreateTransactionReq no branches") - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "This user type doesn't have branches", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "This user type doesn't have branches", err, nil) } branchID = branch.ID @@ -108,9 +106,7 @@ func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service, branch, err := branchSvc.GetBranchByCashier(c.Context(), user.ID) if err != nil { logger.Error("CreateTransactionReq failed, branch id invalid") - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Branch ID invalid", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Branch ID invalid", err, nil) } branchID = branch.ID } @@ -118,16 +114,13 @@ func CreateTransaction(logger *slog.Logger, transactionSvc *transaction.Service, var req CreateTransactionReq if err := c.BodyParser(&req); err != nil { logger.Error("CreateTransactionReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := validator.Validate(c, req) if !ok { logger.Error("CreateTransactionReq failed v", "error", valErrs) - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } transaction, err := transactionSvc.CreateTransaction(c.Context(), domain.CreateTransaction{ @@ -288,15 +281,12 @@ func UpdateTransactionVerified(logger *slog.Logger, transactionSvc *transaction. var req UpdateTransactionVerifiedReq if err := c.BodyParser(&req); err != nil { logger.Error("UpdateTransactionVerifiedReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := validator.Validate(c, req) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } err = transactionSvc.UpdateTransactionVerified(c.Context(), id, req.Verified) diff --git a/internal/web_server/handlers/transfer_handler.go b/internal/web_server/handlers/transfer_handler.go index 983164e..df85550 100644 --- a/internal/web_server/handlers/transfer_handler.go +++ b/internal/web_server/handlers/transfer_handler.go @@ -158,22 +158,18 @@ func TransferToWallet(logger *slog.Logger, walletSvc *wallet.Service, branchSvc if err := c.BodyParser(&req); err != nil { logger.Error("CreateTransferReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := validator.Validate(c, req) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } transfer, err := walletSvc.TransferToWallet(c.Context(), senderID, receiverID, domain.ToCurrency(req.Amount), domain.PaymentMethod(req.PaymentMethod), domain.ValidInt64{Value: userID, Valid: true}) if !ok { - response.WriteJSON(c, fiber.StatusInternalServerError, "Transfer Failed", err, nil) - return nil + return response.WriteJSON(c, fiber.StatusInternalServerError, "Transfer Failed", err, nil) } res := convertTransfer(transfer) @@ -218,15 +214,12 @@ func RefillWallet(logger *slog.Logger, walletSvc *wallet.Service, validator *cus if err := c.BodyParser(&req); err != nil { logger.Error("CreateRefillReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := validator.Validate(c, req) if !ok { - response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) - return nil + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } transfer, err := walletSvc.RefillWallet(c.Context(), domain.CreateTransfer{ @@ -241,8 +234,7 @@ func RefillWallet(logger *slog.Logger, walletSvc *wallet.Service, validator *cus }) if !ok { - response.WriteJSON(c, fiber.StatusInternalServerError, "Creating Transfer Failed", err, nil) - return nil + return response.WriteJSON(c, fiber.StatusInternalServerError, "Creating Transfer Failed", err, nil) } res := convertTransfer(transfer) diff --git a/internal/web_server/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index 37b72f4..e74d91f 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -213,9 +213,7 @@ func UpdateWalletActive(logger *slog.Logger, walletSvc *wallet.Service, validato var req UpdateWalletActiveReq if err := c.BodyParser(&req); err != nil { logger.Error("UpdateWalletActiveReq failed", "error", err) - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid request", - }) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } err = walletSvc.UpdateWalletActive(c.Context(), id, req.IsActive) @@ -249,7 +247,7 @@ func GetCustomerWallet(logger *slog.Logger, walletSvc *wallet.Service, validator vendorID, err := strconv.ParseInt(c.Get("vendor_id"), 10, 64) if err != nil { - return c.Status(fiber.StatusBadRequest).SendString("Invalid company_id") + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid company_id", err, nil) } logger.Info("Company ID: " + strconv.FormatInt(vendorID, 10)) diff --git a/internal/web_server/middleware.go b/internal/web_server/middleware.go index aa51c1a..057295b 100644 --- a/internal/web_server/middleware.go +++ b/internal/web_server/middleware.go @@ -47,9 +47,13 @@ func (a *App) authMiddleware(c *fiber.Ctx) error { c.Locals("branch_id", claim.BranchId) c.Locals("refresh_token", refreshToken) - if claim.Role != domain.RoleCustomer { - // TODO: Add branch id here from the user - // c.Locals("branch_id", claim.) + return c.Next() +} + +func (a *App) SuperAdminOnly(c *fiber.Ctx) error { + userRole := c.Locals("role").(domain.Role) + if userRole != domain.RoleSuperAdmin { + return fiber.NewError(fiber.StatusUnauthorized, "Invalid access token") } return c.Next() } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index e730bc0..576ff50 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -25,7 +25,6 @@ func (a *App) initAppRoutes() { if err != nil { return c.Status(fiber.StatusBadRequest).SendString("Invalid company_id") } - // a.logger.Info("User ID: " + string(userId.(string))) //panic: interface conversion: interface {} is int64, not string a.logger.Info("User ID: " + strconv.FormatInt(userId, 10)) fmt.Printf("User ID: %d\n", userId) a.logger.Info("Role: " + role) @@ -85,6 +84,13 @@ func (a *App) initAppRoutes() { a.fiber.Get("/branch/:id/operation", a.authMiddleware, handlers.GetBranchOperations(a.logger, a.branchSvc, a.validator)) a.fiber.Delete("/branch/:id/operation/:opID", a.authMiddleware, handlers.DeleteBranchOperation(a.logger, a.branchSvc, a.validator)) + // Company + a.fiber.Post("/company", a.authMiddleware, a.SuperAdminOnly, handlers.CreateCompany(a.logger, a.companySvc, a.walletSvc, a.validator)) + a.fiber.Get("/company", a.authMiddleware, a.SuperAdminOnly, handlers.GetAllCompanies(a.logger, a.companySvc, a.validator)) + a.fiber.Get("/company/:id", a.authMiddleware, a.SuperAdminOnly, handlers.GetCompanyByID(a.logger, a.companySvc, a.validator)) + a.fiber.Put("/company/:id", a.authMiddleware, a.SuperAdminOnly, handlers.UpdateCompany(a.logger, a.companySvc, a.validator)) + a.fiber.Delete("/company/:id", a.authMiddleware, a.SuperAdminOnly, handlers.DeleteCompany(a.logger, a.companySvc, a.validator)) + // Ticket a.fiber.Post("/ticket", handlers.CreateTicket(a.logger, a.ticketSvc, a.eventSvc, *a.prematchSvc, a.validator)) a.fiber.Get("/ticket", handlers.GetAllTickets(a.logger, a.ticketSvc, a.validator))