From 89e3d7de781dd03240dcc520c340a4a435c73752 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Mon, 1 Sep 2025 23:47:27 +0300 Subject: [PATCH] raffle service structure --- db/migrations/000001_fortune.up.sql | 18 ++- db/query/raffle.sql | 36 ++++++ gen/db/models.go | 17 +++ gen/db/raffle.sql.go | 190 ++++++++++++++++++++++++++++ internal/domain/raffle.go | 36 ++++++ internal/repository/reffel.go | 123 ++++++++++++++++++ internal/services/raffle/port.go | 18 +++ internal/services/raffle/service.go | 45 +++++++ internal/web_server/routes.go | 4 - 9 files changed, 482 insertions(+), 5 deletions(-) create mode 100644 db/query/raffle.sql create mode 100644 gen/db/raffle.sql.go create mode 100644 internal/domain/raffle.go create mode 100644 internal/repository/reffel.go create mode 100644 internal/services/raffle/port.go create mode 100644 internal/services/raffle/service.go diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index d05dcbe..f2f852d 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -498,6 +498,22 @@ CREATE TABLE IF NOT EXISTS supported_operations ( name VARCHAR(255) NOT NULL, description VARCHAR(255) NOT NULL ); +CREATE TABLE IF NOT EXISTS raffles ( + id SERIAL PRIMARY KEY, + company_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + expires_at TIMESTAMP NOT NULL, + type VARCHAR(50) NOT NULL CHECK (type IN ('virtual', 'sport')), + status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'completed')) +); +CREATE TABLE IF NOT EXISTS raffle_tickets ( + id SERIAL PRIMARY KEY, + raffle_id INT NOT NULL REFERENCES raffles(id) ON DELETE CASCADE, + user_id INT NOT NULL, + is_active BOOL DEFAULT true, + UNIQUE (raffle_id, user_id) +); CREATE VIEW bet_with_outcomes AS SELECT bets.*, CONCAT(users.first_name, ' ', users.last_name) AS full_name, @@ -686,4 +702,4 @@ ADD CONSTRAINT fk_event_settings_company FOREIGN KEY (company_id) REFERENCES com ADD CONSTRAINT fk_event_settings_event FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE; ALTER TABLE company_odd_settings ADD CONSTRAINT fk_odds_settings_company FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, - ADD CONSTRAINT fk_odds_settings_odds_market FOREIGN KEY (odds_market_id) REFERENCES odds_market(id) ON DELETE CASCADE; \ No newline at end of file + ADD CONSTRAINT fk_odds_settings_odds_market FOREIGN KEY (odds_market_id) REFERENCES odds_market(id) ON DELETE CASCADE; diff --git a/db/query/raffle.sql b/db/query/raffle.sql new file mode 100644 index 0000000..4e900fd --- /dev/null +++ b/db/query/raffle.sql @@ -0,0 +1,36 @@ +-- name: CreateRaffle :one +INSERT INTO raffles (company_id, name, expires_at, type) +VALUES ($1, $2, $3, $4) +RETURNING *; + +-- name: GetRafflesOfCompany :many +SELECT * FROM raffles WHERE company_id = $1; + +-- name: UpdateRaffle :exec +UPDATE raffles +SET name = $1, + expires_at = $2, + status = $3 +WHERE id = $4; + +-- name: UpdateRaffleTicketStatus :exec +UPDATE raffle_tickets +SET is_active = $1 +WHERE id = $2; + +-- name: CreateRaffleTicket :one +INSERT INTO raffle_tickets (raffle_id, user_id) +VALUES ($1, $2) +RETURNING *; + +-- name: GetUserRaffleTickets :many +SELECT + rt.id AS ticket_id, + rt.user_id, + r.name, + r.type, + r.expires_at, + r.status +FROM raffle_tickets rt +JOIN raffles r ON rt.raffle_id = r.id +WHERE rt.user_id = $1; diff --git a/gen/db/models.go b/gen/db/models.go index d91961f..0d18e73 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -538,6 +538,23 @@ type Otp struct { ExpiresAt pgtype.Timestamptz `json:"expires_at"` } +type Raffle struct { + ID int32 `json:"id"` + CompanyID int32 `json:"company_id"` + Name string `json:"name"` + CreatedAt pgtype.Timestamp `json:"created_at"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + Type string `json:"type"` + Status string `json:"status"` +} + +type RaffleTicket struct { + ID int32 `json:"id"` + RaffleID int32 `json:"raffle_id"` + UserID int32 `json:"user_id"` + IsActive pgtype.Bool `json:"is_active"` +} + type Referral struct { ID int64 `json:"id"` ReferralCode string `json:"referral_code"` diff --git a/gen/db/raffle.sql.go b/gen/db/raffle.sql.go new file mode 100644 index 0000000..64fdd88 --- /dev/null +++ b/gen/db/raffle.sql.go @@ -0,0 +1,190 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: raffle.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreateRaffle = `-- name: CreateRaffle :one +INSERT INTO raffles (company_id, name, expires_at, type) +VALUES ($1, $2, $3, $4) +RETURNING id, company_id, name, created_at, expires_at, type, status +` + +type CreateRaffleParams struct { + CompanyID int32 `json:"company_id"` + Name string `json:"name"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + Type string `json:"type"` +} + +func (q *Queries) CreateRaffle(ctx context.Context, arg CreateRaffleParams) (Raffle, error) { + row := q.db.QueryRow(ctx, CreateRaffle, + arg.CompanyID, + arg.Name, + arg.ExpiresAt, + arg.Type, + ) + var i Raffle + err := row.Scan( + &i.ID, + &i.CompanyID, + &i.Name, + &i.CreatedAt, + &i.ExpiresAt, + &i.Type, + &i.Status, + ) + return i, err +} + +const CreateRaffleTicket = `-- name: CreateRaffleTicket :one +INSERT INTO raffle_tickets (raffle_id, user_id) +VALUES ($1, $2) +RETURNING id, raffle_id, user_id, is_active +` + +type CreateRaffleTicketParams struct { + RaffleID int32 `json:"raffle_id"` + UserID int32 `json:"user_id"` +} + +func (q *Queries) CreateRaffleTicket(ctx context.Context, arg CreateRaffleTicketParams) (RaffleTicket, error) { + row := q.db.QueryRow(ctx, CreateRaffleTicket, arg.RaffleID, arg.UserID) + var i RaffleTicket + err := row.Scan( + &i.ID, + &i.RaffleID, + &i.UserID, + &i.IsActive, + ) + return i, err +} + +const GetRafflesOfCompany = `-- name: GetRafflesOfCompany :many +SELECT id, company_id, name, created_at, expires_at, type, status FROM raffles WHERE company_id = $1 +` + +func (q *Queries) GetRafflesOfCompany(ctx context.Context, companyID int32) ([]Raffle, error) { + rows, err := q.db.Query(ctx, GetRafflesOfCompany, companyID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Raffle + for rows.Next() { + var i Raffle + if err := rows.Scan( + &i.ID, + &i.CompanyID, + &i.Name, + &i.CreatedAt, + &i.ExpiresAt, + &i.Type, + &i.Status, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetUserRaffleTickets = `-- name: GetUserRaffleTickets :many +SELECT + rt.id AS ticket_id, + rt.user_id, + r.name, + r.type, + r.expires_at, + r.status +FROM raffle_tickets rt +JOIN raffles r ON rt.raffle_id = r.id +WHERE rt.user_id = $1 +` + +type GetUserRaffleTicketsRow struct { + TicketID int32 `json:"ticket_id"` + UserID int32 `json:"user_id"` + Name string `json:"name"` + Type string `json:"type"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + Status string `json:"status"` +} + +func (q *Queries) GetUserRaffleTickets(ctx context.Context, userID int32) ([]GetUserRaffleTicketsRow, error) { + rows, err := q.db.Query(ctx, GetUserRaffleTickets, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetUserRaffleTicketsRow + for rows.Next() { + var i GetUserRaffleTicketsRow + if err := rows.Scan( + &i.TicketID, + &i.UserID, + &i.Name, + &i.Type, + &i.ExpiresAt, + &i.Status, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const UpdateRaffle = `-- name: UpdateRaffle :exec +UPDATE raffles +SET name = $1, + expires_at = $2, + status = $3 +WHERE id = $4 +` + +type UpdateRaffleParams struct { + Name string `json:"name"` + ExpiresAt pgtype.Timestamp `json:"expires_at"` + Status string `json:"status"` + ID int32 `json:"id"` +} + +func (q *Queries) UpdateRaffle(ctx context.Context, arg UpdateRaffleParams) error { + _, err := q.db.Exec(ctx, UpdateRaffle, + arg.Name, + arg.ExpiresAt, + arg.Status, + arg.ID, + ) + return err +} + +const UpdateRaffleTicketStatus = `-- name: UpdateRaffleTicketStatus :exec +UPDATE raffle_tickets +SET is_active = $1 +WHERE id = $2 +` + +type UpdateRaffleTicketStatusParams struct { + IsActive pgtype.Bool `json:"is_active"` + ID int32 `json:"id"` +} + +func (q *Queries) UpdateRaffleTicketStatus(ctx context.Context, arg UpdateRaffleTicketStatusParams) error { + _, err := q.db.Exec(ctx, UpdateRaffleTicketStatus, arg.IsActive, arg.ID) + return err +} diff --git a/internal/domain/raffle.go b/internal/domain/raffle.go new file mode 100644 index 0000000..b4b1e70 --- /dev/null +++ b/internal/domain/raffle.go @@ -0,0 +1,36 @@ +package domain + +import "time" + +type Raffle struct { + ID int32 + CompanyID int32 + Name string + CreatedAt time.Time + ExpiresAt time.Time + Type string + Status string +} + +type RaffleTicket struct { + ID int32 + RaffleID int32 + UserID int32 + IsActive bool +} + +type RaffleTicketRes struct { + TicketID int32 + UserID int32 + Name string + Type string + ExpiresAt time.Time + Status string +} + +type CreateRaffle struct { + CompanyID int32 + Name string + ExpiresAt time.Time + Type string +} diff --git a/internal/repository/reffel.go b/internal/repository/reffel.go new file mode 100644 index 0000000..a28096a --- /dev/null +++ b/internal/repository/reffel.go @@ -0,0 +1,123 @@ +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 convertRaffleOutcome(raffle dbgen.Raffle) domain.Raffle { + return domain.Raffle{ + ID: raffle.ID, + CompanyID: raffle.CompanyID, + Name: raffle.Name, + CreatedAt: raffle.CreatedAt.Time, + ExpiresAt: raffle.ExpiresAt.Time, + Type: raffle.Type, + Status: raffle.Status, + } +} + +func convertRaffleTicketOutcome(raffle dbgen.RaffleTicket) domain.RaffleTicket { + return domain.RaffleTicket{ + ID: raffle.ID, + RaffleID: raffle.RaffleID, + UserID: raffle.UserID, + IsActive: raffle.IsActive.Bool, + } +} + +func convertJoinedRaffleTicketOutcome(raffle dbgen.GetUserRaffleTicketsRow) domain.RaffleTicketRes { + return domain.RaffleTicketRes{ + TicketID: raffle.TicketID, + UserID: raffle.UserID, + Name: raffle.Name, + Type: raffle.Type, + ExpiresAt: raffle.ExpiresAt.Time, + Status: raffle.Status, + } +} + +func convertCreateRaffle(raffle domain.CreateRaffle) dbgen.CreateRaffleParams { + return dbgen.CreateRaffleParams{ + CompanyID: raffle.CompanyID, + Name: raffle.Name, + ExpiresAt: pgtype.Timestamp{ + Time: raffle.ExpiresAt, + Valid: true, + }, + Type: raffle.Type, + } +} + +func (s *Store) CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) (domain.Raffle, error) { + raffleRes, err := s.queries.CreateRaffle(ctx, convertCreateRaffle(raffle)) + if err != nil { + return domain.Raffle{}, err + } + + return convertRaffleOutcome(raffleRes), nil +} + +func (s *Store) GetRafflesOfCompany(ctx context.Context, companyID int32) ([]dbgen.Raffle, error) { + raffles, err := s.queries.GetRafflesOfCompany(ctx, companyID) + if err != nil { + return nil, err + } + + return raffles, nil +} + +func (s *Store) UpdateRaffle(ctx context.Context, raffleParams dbgen.UpdateRaffleParams) error { + return s.queries.UpdateRaffle(ctx, raffleParams) +} + +func (s *Store) SuspendRaffleTicket(ctx context.Context, raffleID int32) error { + return s.queries.UpdateRaffleTicketStatus(ctx, dbgen.UpdateRaffleTicketStatusParams{ + ID: raffleID, + IsActive: pgtype.Bool{ + Bool: false, + Valid: true, + }, + }) +} + +func (s *Store) UnSuspendRaffleTicket(ctx context.Context, raffleID int32) error { + return s.queries.UpdateRaffleTicketStatus(ctx, dbgen.UpdateRaffleTicketStatusParams{ + ID: raffleID, + IsActive: pgtype.Bool{ + Bool: true, + Valid: true, + }, + }) +} + +// TODO: could also add -> suspend a specific user's raffle tickets + +func (s *Store) CreateRaffleTicket(ctx context.Context, raffleID, userID int32) (domain.RaffleTicket, error) { + raffleTicket, err := s.queries.CreateRaffleTicket(ctx, dbgen.CreateRaffleTicketParams{ + RaffleID: raffleID, + UserID: userID, + }) + if err != nil { + return domain.RaffleTicket{}, err + } + + return convertRaffleTicketOutcome(raffleTicket), nil +} + +func (s *Store) GetUserRaffleTickets(ctx context.Context, userID int32) ([]domain.RaffleTicketRes, error) { + raffleTickets, err := s.queries.GetUserRaffleTickets(ctx, userID) + if err != nil { + return nil, err + } + + res := []domain.RaffleTicketRes{} + for _, raffle := range raffleTickets { + res = append(res, convertJoinedRaffleTicketOutcome(raffle)) + } + + return res, nil +} diff --git a/internal/services/raffle/port.go b/internal/services/raffle/port.go new file mode 100644 index 0000000..a457f66 --- /dev/null +++ b/internal/services/raffle/port.go @@ -0,0 +1,18 @@ +package raffle + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type RaffleStore interface { + CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) (domain.Raffle, error) + GetRafflesOfCompany(ctx context.Context, companyID int32) ([]dbgen.Raffle, error) + UpdateRaffle(ctx context.Context, raffleParams dbgen.UpdateRaffleParams) error + SuspendRaffleTicket(ctx context.Context, raffleID int32) error + UnSuspendRaffleTicket(ctx context.Context, raffleID int32) error + CreateRaffleTicket(ctx context.Context, raffleID, userID int32) (domain.RaffleTicket, error) + GetUserRaffleTickets(ctx context.Context, userID int32) ([]domain.RaffleTicketRes, error) +} diff --git a/internal/services/raffle/service.go b/internal/services/raffle/service.go new file mode 100644 index 0000000..6048ca7 --- /dev/null +++ b/internal/services/raffle/service.go @@ -0,0 +1,45 @@ +package raffle + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type Service struct { + raffleStore RaffleStore +} + +func NewService(raffleStore RaffleStore) *Service { + return &Service{ + raffleStore: raffleStore, + } +} + +func (s *Service) CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) (domain.Raffle, error) { + return s.raffleStore.CreateRaffle(ctx, raffle) +} + +func (s *Service) GetRafflesOfCompany(ctx context.Context, companyID int32) ([]dbgen.Raffle, error) { + return s.GetRafflesOfCompany(ctx, companyID) +} +func (s *Service) UpdateRaffle(ctx context.Context, raffleParams dbgen.UpdateRaffleParams) error { + return s.raffleStore.UpdateRaffle(ctx, raffleParams) +} + +func (s *Service) SuspendRaffleTicket(ctx context.Context, raffleID int32) error { + return s.raffleStore.SuspendRaffleTicket(ctx, raffleID) +} + +func (s *Service) UnSuspendRaffleTicket(ctx context.Context, raffleID int32) error { + return s.raffleStore.UnSuspendRaffleTicket(ctx, raffleID) +} + +func (s *Service) CreateRaffleTicket(ctx context.Context, raffleID, userID int32) (domain.RaffleTicket, error) { + return s.raffleStore.CreateRaffleTicket(ctx, raffleID, userID) +} + +func (s *Service) GetUserRaffleTickets(ctx context.Context, userID int32) ([]domain.RaffleTicketRes, error) { + return s.raffleStore.GetUserRaffleTickets(ctx, userID) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index c6c1f7f..82e49e8 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -71,7 +71,6 @@ func (a *App) initAppRoutes() { groupV1.Post("/direct_deposit", a.authMiddleware, h.InitiateDirectDeposit) groupV1.Post("/direct_deposit/verify", a.authMiddleware, h.VerifyDirectDeposit) groupV1.Get("/direct_deposit/pending", a.authMiddleware, h.GetPendingDirectDeposits) - // Swagger a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) @@ -141,9 +140,6 @@ func (a *App) initAppRoutes() { // groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler) // groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler) - - - // User Routes tenant.Post("/user/resetPassword", h.ResetPassword) tenant.Post("/user/sendResetCode", h.SendResetCode)