From 60887accd20509e941ae8e456a056d73add7d4c1 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sat, 29 Mar 2025 19:11:31 +0300 Subject: [PATCH] bet and ticket crud --- db/migrations/000001_fortune.down.sql | 5 +- db/migrations/000001_fortune.up.sql | 35 ++++++- db/query/bet.sql | 16 +++ db/query/ticket.sql | 4 +- gen/db/bet.sql.go | 137 ++++++++++++++++++++++++++ gen/db/models.go | 18 +++- gen/db/ticket.sql.go | 11 +-- internal/domain/bet.go | 27 +++++ internal/domain/branch.go | 13 +++ internal/domain/common.go | 29 ++++++ internal/domain/event.go | 6 ++ internal/domain/ticket.go | 6 +- internal/repository/bet.go | 118 ++++++++++++++++++++++ internal/repository/ticket.go | 22 ++--- internal/services/bet/port.go | 15 +++ internal/services/bet/service.go | 35 +++++++ internal/services/ticket/port.go | 2 +- internal/services/ticket/service.go | 6 +- 18 files changed, 469 insertions(+), 36 deletions(-) create mode 100644 db/query/bet.sql create mode 100644 gen/db/bet.sql.go create mode 100644 internal/domain/bet.go create mode 100644 internal/domain/branch.go create mode 100644 internal/domain/common.go create mode 100644 internal/domain/event.go create mode 100644 internal/repository/bet.go create mode 100644 internal/services/bet/port.go create mode 100644 internal/services/bet/service.go diff --git a/db/migrations/000001_fortune.down.sql b/db/migrations/000001_fortune.down.sql index ffaa92e..2109826 100644 --- a/db/migrations/000001_fortune.down.sql +++ b/db/migrations/000001_fortune.down.sql @@ -73,6 +73,7 @@ DROP TYPE IF EXISTS ua_pin_status; DROP TYPE IF EXISTS ua_status; DROP TYPE IF EXISTS ua_registaration_type; --- Drop Ticket -DROP TABLE IF EXIST ticket; +-- Drop FortuneBet +DROP TABLE IF EXIST tickets; +DROP TABLE IF EXIST bets; diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 98e59ff..0001a4a 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -7,14 +7,43 @@ CREATE TABLE users ( password TEXT NOT NULL, role VARCHAR(50) NOT NULL, verified BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP , + created_at TIMESTAMP, updated_at TIMESTAMP ); + +CREATE TABLE IF NOT EXISTS bets ( + 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, + branch_id BIGINT, + user_id BIGINT, + cashed_out BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP, + updated_at TIMESTAMP, + CHECK (user_id IS NOT NULL OR branch_id IS NOT NULL) +); + CREATE TABLE IF NOT EXISTS tickets ( id BIGSERIAL PRIMARY KEY, - amount DECIMAL(12,2) NULL, - total_odds DECIMAL(12,2) NOT NULL, + amount BIGINT NULL, + total_odds REAL NOT NULL, created_at TIMESTAMP, updated_at TIMESTAMP ); + + +-- CREATE TABLE IF NOT EXISTS bet_outcomes ( +-- id BIGSERIAL PRIMARY KEY, +-- bet_id BIGINT NOT NULL, +-- outcome_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, +-- ); \ No newline at end of file diff --git a/db/query/bet.sql b/db/query/bet.sql new file mode 100644 index 0000000..f2f48e3 --- /dev/null +++ b/db/query/bet.sql @@ -0,0 +1,16 @@ +-- name: CreateBet :one +INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id) +VALUES ($1, $2, $3, $4, $5, $6, $7) +RETURNING *; + +-- name: GetAllBets :many +SELECT * FROM bets; + +-- name: GetBetByID :one +SELECT * FROM bets WHERE id = $1; + +-- name: UpdateCashOut :exec +UPDATE bets SET cashed_out = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1; + +-- name: DeleteBet :exec +DELETE FROM bets WHERE id = $1; diff --git a/db/query/ticket.sql b/db/query/ticket.sql index 5b3a99a..04be763 100644 --- a/db/query/ticket.sql +++ b/db/query/ticket.sql @@ -1,6 +1,6 @@ -- name: CreateTicket :one -INSERT INTO tickets (id, amount, total_odds) -VALUES ($1, $2, $3) +INSERT INTO tickets (amount, total_odds) +VALUES ($1, $2) RETURNING *; -- name: GetAllTickets :many diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go new file mode 100644 index 0000000..6163071 --- /dev/null +++ b/gen/db/bet.sql.go @@ -0,0 +1,137 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: bet.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreateBet = `-- name: CreateBet :one +INSERT INTO bets (amount, total_odds, status, full_name, phone_number, branch_id, user_id) +VALUES ($1, $2, $3, $4, $5, $6, $7) +RETURNING id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, created_at, updated_at +` + +type CreateBetParams struct { + Amount int64 + TotalOdds float32 + Status int32 + FullName string + PhoneNumber string + BranchID pgtype.Int8 + UserID pgtype.Int8 +} + +func (q *Queries) CreateBet(ctx context.Context, arg CreateBetParams) (Bet, error) { + row := q.db.QueryRow(ctx, CreateBet, + arg.Amount, + arg.TotalOdds, + arg.Status, + arg.FullName, + arg.PhoneNumber, + arg.BranchID, + arg.UserID, + ) + var i Bet + err := row.Scan( + &i.ID, + &i.Amount, + &i.TotalOdds, + &i.Status, + &i.FullName, + &i.PhoneNumber, + &i.BranchID, + &i.UserID, + &i.CashedOut, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const DeleteBet = `-- name: DeleteBet :exec +DELETE FROM bets WHERE id = $1 +` + +func (q *Queries) DeleteBet(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteBet, id) + return err +} + +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 FROM bets +` + +func (q *Queries) GetAllBets(ctx context.Context) ([]Bet, error) { + rows, err := q.db.Query(ctx, GetAllBets) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Bet + for rows.Next() { + var i Bet + if err := rows.Scan( + &i.ID, + &i.Amount, + &i.TotalOdds, + &i.Status, + &i.FullName, + &i.PhoneNumber, + &i.BranchID, + &i.UserID, + &i.CashedOut, + &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 GetBetByID = `-- name: GetBetByID :one +SELECT id, amount, total_odds, status, full_name, phone_number, branch_id, user_id, cashed_out, created_at, updated_at FROM bets WHERE id = $1 +` + +func (q *Queries) GetBetByID(ctx context.Context, id int64) (Bet, error) { + row := q.db.QueryRow(ctx, GetBetByID, id) + var i Bet + err := row.Scan( + &i.ID, + &i.Amount, + &i.TotalOdds, + &i.Status, + &i.FullName, + &i.PhoneNumber, + &i.BranchID, + &i.UserID, + &i.CashedOut, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const UpdateCashOut = `-- name: UpdateCashOut :exec +UPDATE bets SET cashed_out = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1 +` + +type UpdateCashOutParams struct { + ID int64 + CashedOut pgtype.Bool +} + +func (q *Queries) UpdateCashOut(ctx context.Context, arg UpdateCashOutParams) error { + _, err := q.db.Exec(ctx, UpdateCashOut, arg.ID, arg.CashedOut) + return err +} diff --git a/gen/db/models.go b/gen/db/models.go index dcbf8be..35776c9 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -8,10 +8,24 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +type Bet struct { + ID int64 + Amount int64 + TotalOdds float32 + Status int32 + FullName string + PhoneNumber string + BranchID pgtype.Int8 + UserID pgtype.Int8 + CashedOut pgtype.Bool + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} + type Ticket struct { ID int64 - Amount pgtype.Numeric - TotalOdds pgtype.Numeric + Amount pgtype.Int8 + TotalOdds float32 CreatedAt pgtype.Timestamp UpdatedAt pgtype.Timestamp } diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index 24d11f3..d7e5ff3 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -12,19 +12,18 @@ import ( ) const CreateTicket = `-- name: CreateTicket :one -INSERT INTO tickets (id, amount, total_odds) -VALUES ($1, $2, $3) +INSERT INTO tickets (amount, total_odds) +VALUES ($1, $2) RETURNING id, amount, total_odds, created_at, updated_at ` type CreateTicketParams struct { - ID int64 - Amount pgtype.Numeric - TotalOdds pgtype.Numeric + Amount pgtype.Int8 + TotalOdds float32 } func (q *Queries) CreateTicket(ctx context.Context, arg CreateTicketParams) (Ticket, error) { - row := q.db.QueryRow(ctx, CreateTicket, arg.ID, arg.Amount, arg.TotalOdds) + row := q.db.QueryRow(ctx, CreateTicket, arg.Amount, arg.TotalOdds) var i Ticket err := row.Scan( &i.ID, diff --git a/internal/domain/bet.go b/internal/domain/bet.go new file mode 100644 index 0000000..d4540ad --- /dev/null +++ b/internal/domain/bet.go @@ -0,0 +1,27 @@ +package domain + +type BetStatus int + +const ( + BET_STATUS_PENDING BetStatus = iota + BET_STATUS_WIN + BET_STATUS_LOSS + BET_STATUS_ERROR +) + +type Bet struct { + ID int64 + Outcome []Outcome + Amount Currency + TotalOdds float32 + Status BetStatus + FullName string + PhoneNumber string + BranchID ValidInt64 // Can Be Nullable + UserID ValidInt64 // Can Be Nullable + CashedOut bool +} + +func (b BetStatus) String() string { + return []string{"Pending", "Win", "Loss", "Error"}[b] +} diff --git a/internal/domain/branch.go b/internal/domain/branch.go new file mode 100644 index 0000000..54bdcf7 --- /dev/null +++ b/internal/domain/branch.go @@ -0,0 +1,13 @@ +package domain + + +type Branch struct { + ID int64 + Name string + Location string + BranchManagerID int64 + IsSelfOwned bool + IsSupportingSportBook bool + IsSupportingVirtual bool + IsSupportingGameZone bool +} \ No newline at end of file diff --git a/internal/domain/common.go b/internal/domain/common.go new file mode 100644 index 0000000..e705f3f --- /dev/null +++ b/internal/domain/common.go @@ -0,0 +1,29 @@ +package domain + +import "fmt" + +type ValidInt64 struct { + Value int64 + Valid bool +} + +type Currency int64 + +// ToCurrency converts a float32 to Currency +func ToCurrency(f float32) Currency { + return Currency((f * 100) + 0.5) +} + +// Float64 converts a Currency to float32 +func (m Currency) Float64() float32 { + x := float32(m) + x = x / 100 + return x +} + +// String returns a formatted Currency value +func (m Currency) String() string { + x := float32(m) + x = x / 100 + return fmt.Sprintf("$%.2f", x) +} diff --git a/internal/domain/event.go b/internal/domain/event.go new file mode 100644 index 0000000..e5cc881 --- /dev/null +++ b/internal/domain/event.go @@ -0,0 +1,6 @@ +package domain + +type Event struct {} + +type Outcome struct {} + diff --git a/internal/domain/ticket.go b/internal/domain/ticket.go index 3ca75e3..53d49e8 100644 --- a/internal/domain/ticket.go +++ b/internal/domain/ticket.go @@ -1,9 +1,9 @@ package domain -// TODO: Adding Outcome Array Once the event is done // ID will serve as the fast code since this doesn't need to be secure type Ticket struct { ID int64 - Amount int32 - TotalOdds int32 + Outcome []Outcome + Amount Currency + TotalOdds float32 } diff --git a/internal/repository/bet.go b/internal/repository/bet.go new file mode 100644 index 0000000..614bd37 --- /dev/null +++ b/internal/repository/bet.go @@ -0,0 +1,118 @@ +package repository + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Store) CreateBet(ctx context.Context, bet domain.Bet) (domain.Bet, error) { + + newBet, err := s.queries.CreateBet(ctx, dbgen.CreateBetParams{ + Amount: int64(bet.Amount), + TotalOdds: bet.TotalOdds, + Status: int32(bet.Status), + FullName: bet.FullName, + PhoneNumber: bet.PhoneNumber, + BranchID: pgtype.Int8{ + Int64: bet.BranchID.Value, + Valid: bet.BranchID.Valid, + }, + UserID: pgtype.Int8{ + Int64: bet.UserID.Value, + Valid: bet.UserID.Valid, + }, + }) + + if err != nil { + return domain.Bet{}, err + } + + return domain.Bet{ + ID: newBet.ID, + Amount: domain.Currency(newBet.Amount), + TotalOdds: newBet.TotalOdds, + Status: domain.BetStatus(newBet.Status), + FullName: newBet.FullName, + PhoneNumber: newBet.PhoneNumber, + BranchID: domain.ValidInt64{ + Value: newBet.BranchID.Int64, + Valid: newBet.BranchID.Valid, + }, + UserID: domain.ValidInt64{ + Value: newBet.UserID.Int64, + Valid: newBet.UserID.Valid, + }, + }, err + +} + +func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.Bet, error) { + bet, err := s.queries.GetBetByID(ctx, id) + if err != nil { + return domain.Bet{}, err + } + + return domain.Bet{ + 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, + }, + }, nil +} + +func (s *Store) GetAllBets(ctx context.Context) ([]domain.Bet, error) { + bets, err := s.queries.GetAllBets(ctx) + + if err != nil { + return nil, err + } + + var result []domain.Bet + for _, bet := range bets { + result = append(result, domain.Bet{ + 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, + }, + }) + } + + return result, nil +} + +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, + }, + }) + return err +} + +func (s *Store) DeleteBet(ctx context.Context, id int64) error { + return s.queries.DeleteBet(ctx, id) +} diff --git a/internal/repository/ticket.go b/internal/repository/ticket.go index 513196d..3c35b2a 100644 --- a/internal/repository/ticket.go +++ b/internal/repository/ticket.go @@ -8,15 +8,13 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -func (s *Store) CreateTicket(ctx context.Context, amount int32, totalOdds int32) (domain.Ticket, error) { +func (s *Store) CreateTicket(ctx context.Context, amount domain.Currency, totalOdds float32) (domain.Ticket, error) { ticket, err := s.queries.CreateTicket(ctx, dbgen.CreateTicketParams{ - Amount: pgtype.Numeric{ - Exp: amount, - }, - TotalOdds: pgtype.Numeric{ - Exp: totalOdds, + Amount: pgtype.Int8{ + Int64: int64(amount), }, + TotalOdds: totalOdds, }) if err != nil { @@ -25,8 +23,8 @@ func (s *Store) CreateTicket(ctx context.Context, amount int32, totalOdds int32) return domain.Ticket{ ID: ticket.ID, - Amount: ticket.Amount.Exp, - TotalOdds: ticket.TotalOdds.Exp, + Amount: amount, + TotalOdds: totalOdds, }, err } @@ -39,8 +37,8 @@ func (s *Store) GetTicketByID(ctx context.Context, id int64) (domain.Ticket, err return domain.Ticket{ ID: ticket.ID, - Amount: ticket.Amount.Exp, - TotalOdds: ticket.TotalOdds.Exp, + Amount: domain.Currency(ticket.Amount.Int64), + TotalOdds: ticket.TotalOdds, }, nil } @@ -55,8 +53,8 @@ func (s *Store) GetAllTickets(ctx context.Context) ([]domain.Ticket, error) { for _, ticket := range tickets { result = append(result, domain.Ticket{ ID: ticket.ID, - Amount: ticket.Amount.Exp, - TotalOdds: ticket.TotalOdds.Exp, + Amount: domain.Currency(ticket.Amount.Int64), + TotalOdds: ticket.TotalOdds, }) } diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go new file mode 100644 index 0000000..9892ca2 --- /dev/null +++ b/internal/services/bet/port.go @@ -0,0 +1,15 @@ +package ticket + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type BetStore interface { + CreateBet(ctx context.Context, bet domain.Bet) (domain.Bet, error) + GetBetByID(ctx context.Context, id int64) (domain.Bet, error) + GetAllBets(ctx context.Context) ([]domain.Bet, 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 new file mode 100644 index 0000000..84c32fb --- /dev/null +++ b/internal/services/bet/service.go @@ -0,0 +1,35 @@ +package ticket + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type Service struct { + betStore BetStore +} + +func NewService(betStore BetStore) *Service { + return &Service{ + betStore: betStore, + } +} + +func (s *Service) CreateBet(ctx context.Context, bet domain.Bet) (domain.Bet, error) { + return s.betStore.CreateBet(ctx, bet) +} +func (s *Service) GetBetByID(ctx context.Context, id int64) (domain.Bet, error) { + return s.betStore.GetBetByID(ctx, id) +} +func (s *Service) GetAllBets(ctx context.Context) ([]domain.Bet, error) { + return s.betStore.GetAllBets(ctx) +} + +func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error { + return s.betStore.UpdateCashOut(ctx, id, cashedOut) +} + +func (s *Service) DeleteBet(ctx context.Context, id int64) error { + return s.betStore.DeleteBet(ctx, id) +} diff --git a/internal/services/ticket/port.go b/internal/services/ticket/port.go index d9591d6..b89328f 100644 --- a/internal/services/ticket/port.go +++ b/internal/services/ticket/port.go @@ -7,7 +7,7 @@ import ( ) type TicketStore interface { - CreateTicket(ctx context.Context, amount int32, totalOdds int32) (domain.Ticket, error) + CreateTicket(ctx context.Context, amount domain.Currency, totalOdds float32) (domain.Ticket, error) GetTicketByID(ctx context.Context, id int64) (domain.Ticket, error) GetAllTickets(ctx context.Context) ([]domain.Ticket, error) DeleteOldTickets(ctx context.Context) error diff --git a/internal/services/ticket/service.go b/internal/services/ticket/service.go index b0bca28..940c0fa 100644 --- a/internal/services/ticket/service.go +++ b/internal/services/ticket/service.go @@ -16,7 +16,7 @@ func NewService(ticketStore TicketStore) *Service { } } -func (s *Service) CreateUser(ctx context.Context, amount int32, totalOdds int32) (domain.Ticket, error) { +func (s *Service) CreateTicket(ctx context.Context, amount domain.Currency, totalOdds float32) (domain.Ticket, error) { return s.ticketStore.CreateTicket(ctx, amount, totalOdds) } func (s *Service) GetTicketByID(ctx context.Context, id int64) (domain.Ticket, error) { @@ -28,7 +28,3 @@ func (s *Service) GetAllTickets(ctx context.Context) ([]domain.Ticket, error) { func (s *Service) DeleteTicket(ctx context.Context, id int64) error { return s.ticketStore.DeleteTicket(ctx, id) } - -func (s *Service) DeleteOldTickets(ctx context.Context) error { - return s.ticketStore.DeleteOldTickets(ctx) -}