raffle service structure

This commit is contained in:
Asher Samuel 2025-09-01 23:47:27 +03:00
parent 3624acbacb
commit 89e3d7de78
9 changed files with 482 additions and 5 deletions

View File

@ -498,6 +498,22 @@ CREATE TABLE IF NOT EXISTS supported_operations (
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
description 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 CREATE VIEW bet_with_outcomes AS
SELECT bets.*, SELECT bets.*,
CONCAT(users.first_name, ' ', users.last_name) AS full_name, CONCAT(users.first_name, ' ', users.last_name) AS full_name,

36
db/query/raffle.sql Normal file
View File

@ -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;

View File

@ -538,6 +538,23 @@ type Otp struct {
ExpiresAt pgtype.Timestamptz `json:"expires_at"` 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 { type Referral struct {
ID int64 `json:"id"` ID int64 `json:"id"`
ReferralCode string `json:"referral_code"` ReferralCode string `json:"referral_code"`

190
gen/db/raffle.sql.go Normal file
View File

@ -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
}

36
internal/domain/raffle.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -72,7 +72,6 @@ func (a *App) initAppRoutes() {
groupV1.Post("/direct_deposit/verify", a.authMiddleware, h.VerifyDirectDeposit) groupV1.Post("/direct_deposit/verify", a.authMiddleware, h.VerifyDirectDeposit)
groupV1.Get("/direct_deposit/pending", a.authMiddleware, h.GetPendingDirectDeposits) groupV1.Get("/direct_deposit/pending", a.authMiddleware, h.GetPendingDirectDeposits)
// Swagger // Swagger
a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) 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.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler)
// groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler) // groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler)
// User Routes // User Routes
tenant.Post("/user/resetPassword", h.ResetPassword) tenant.Post("/user/resetPassword", h.ResetPassword)
tenant.Post("/user/sendResetCode", h.SendResetCode) tenant.Post("/user/sendResetCode", h.SendResetCode)