resolve conflict

This commit is contained in:
Asher Samuel 2025-09-13 21:22:27 +03:00
commit c8edbd07a5
11 changed files with 328 additions and 2 deletions

View File

@ -515,7 +515,13 @@ CREATE TABLE IF NOT EXISTS raffle_tickets (
raffle_id INT NOT NULL REFERENCES raffles(id) ON DELETE CASCADE, raffle_id INT NOT NULL REFERENCES raffles(id) ON DELETE CASCADE,
user_id INT NOT NULL, user_id INT NOT NULL,
is_active BOOL DEFAULT true, is_active BOOL DEFAULT true,
UNIQUE (raffle_id, user_id) );
CREATE TABLE IF NOT EXISTS raffle_winners (
id SERIAL PRIMARY KEY,
raffle_id INT NOT NULL REFERENCES raffles(id) ON DELETE CASCADE,
user_id INT NOT NULL,
rank INT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
); );
------ Views ------ Views
CREATE VIEW companies_details AS CREATE VIEW companies_details AS

View File

@ -32,3 +32,30 @@ SELECT
FROM raffle_tickets rt FROM raffle_tickets rt
JOIN raffles r ON rt.raffle_id = r.id JOIN raffles r ON rt.raffle_id = r.id
WHERE rt.user_id = $1; WHERE rt.user_id = $1;
-- name: GetRaffleStanding :many
SELECT
u.id AS user_id,
rt.raffle_id,
u.first_name,
u.last_name,
u.phone_number,
u.email,
COUNT(*) AS ticket_count
FROM raffle_tickets rt
JOIN users u ON rt.user_id = u.id
WHERE rt.is_active = true
AND rt.raffle_id = $1
GROUP BY u.id, rt.raffle_id, u.first_name, u.last_name, u.phone_number, u.email
ORDER BY ticket_count DESC
LIMIT $2;
-- name: CreateRaffleWinner :one
INSERT INTO raffle_winners (raffle_id, user_id, rank)
VALUES ($1, $2, $3)
RETURNING *;
-- name: SetRaffleComplete :exec
UPDATE raffles
SET status = 'completed'
WHERE id = $1;

View File

@ -514,6 +514,14 @@ type ReferralCode struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
type RaffleWinner struct {
ID int32 `json:"id"`
RaffleID int32 `json:"raffle_id"`
UserID int32 `json:"user_id"`
Rank int32 `json:"rank"`
CreatedAt pgtype.Timestamp `json:"created_at"`
}
type RefreshToken struct { type RefreshToken struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`

View File

@ -67,6 +67,31 @@ func (q *Queries) CreateRaffleTicket(ctx context.Context, arg CreateRaffleTicket
return i, err return i, err
} }
const CreateRaffleWinner = `-- name: CreateRaffleWinner :one
INSERT INTO raffle_winners (raffle_id, user_id, rank)
VALUES ($1, $2, $3)
RETURNING id, raffle_id, user_id, rank, created_at
`
type CreateRaffleWinnerParams struct {
RaffleID int32 `json:"raffle_id"`
UserID int32 `json:"user_id"`
Rank int32 `json:"rank"`
}
func (q *Queries) CreateRaffleWinner(ctx context.Context, arg CreateRaffleWinnerParams) (RaffleWinner, error) {
row := q.db.QueryRow(ctx, CreateRaffleWinner, arg.RaffleID, arg.UserID, arg.Rank)
var i RaffleWinner
err := row.Scan(
&i.ID,
&i.RaffleID,
&i.UserID,
&i.Rank,
&i.CreatedAt,
)
return i, err
}
const DeleteRaffle = `-- name: DeleteRaffle :one const DeleteRaffle = `-- name: DeleteRaffle :one
DELETE FROM raffles DELETE FROM raffles
WHERE id = $1 WHERE id = $1
@ -88,6 +113,67 @@ func (q *Queries) DeleteRaffle(ctx context.Context, id int32) (Raffle, error) {
return i, err return i, err
} }
const GetRaffleStanding = `-- name: GetRaffleStanding :many
SELECT
u.id AS user_id,
rt.raffle_id,
u.first_name,
u.last_name,
u.phone_number,
u.email,
COUNT(*) AS ticket_count
FROM raffle_tickets rt
JOIN users u ON rt.user_id = u.id
WHERE rt.is_active = true
AND rt.raffle_id = $1
GROUP BY u.id, rt.raffle_id, u.first_name, u.last_name, u.phone_number, u.email
ORDER BY ticket_count DESC
LIMIT $2
`
type GetRaffleStandingParams struct {
RaffleID int32 `json:"raffle_id"`
Limit int32 `json:"limit"`
}
type GetRaffleStandingRow struct {
UserID int64 `json:"user_id"`
RaffleID int32 `json:"raffle_id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhoneNumber pgtype.Text `json:"phone_number"`
Email pgtype.Text `json:"email"`
TicketCount int64 `json:"ticket_count"`
}
func (q *Queries) GetRaffleStanding(ctx context.Context, arg GetRaffleStandingParams) ([]GetRaffleStandingRow, error) {
rows, err := q.db.Query(ctx, GetRaffleStanding, arg.RaffleID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetRaffleStandingRow
for rows.Next() {
var i GetRaffleStandingRow
if err := rows.Scan(
&i.UserID,
&i.RaffleID,
&i.FirstName,
&i.LastName,
&i.PhoneNumber,
&i.Email,
&i.TicketCount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetRafflesOfCompany = `-- name: GetRafflesOfCompany :many const GetRafflesOfCompany = `-- name: GetRafflesOfCompany :many
SELECT id, company_id, name, created_at, expires_at, type, status FROM raffles WHERE company_id = $1 SELECT id, company_id, name, created_at, expires_at, type, status FROM raffles WHERE company_id = $1
` `
@ -169,6 +255,17 @@ func (q *Queries) GetUserRaffleTickets(ctx context.Context, userID int32) ([]Get
return items, nil return items, nil
} }
const SetRaffleComplete = `-- name: SetRaffleComplete :exec
UPDATE raffles
SET status = 'completed'
WHERE id = $1
`
func (q *Queries) SetRaffleComplete(ctx context.Context, id int32) error {
_, err := q.db.Exec(ctx, SetRaffleComplete, id)
return err
}
const UpdateRaffleTicketStatus = `-- name: UpdateRaffleTicketStatus :exec const UpdateRaffleTicketStatus = `-- name: UpdateRaffleTicketStatus :exec
UPDATE raffle_tickets UPDATE raffle_tickets
SET is_active = $1 SET is_active = $1

View File

@ -12,6 +12,22 @@ type Raffle struct {
Status string Status string
} }
type RaffleStanding struct {
UserID int64
RaffleID int32
FirstName string
LastName string
PhoneNumber string
Email string
TicketCount int64
}
type RaffleWinnerParams struct {
RaffleID int32
UserID int32
Rank int32
}
type RaffleTicket struct { type RaffleTicket struct {
ID int32 ID int32
RaffleID int32 RaffleID int32

View File

@ -3,6 +3,8 @@ package helpers
import ( import (
random "crypto/rand" random "crypto/rand"
"fmt" "fmt"
"strings"
"github.com/google/uuid" "github.com/google/uuid"
"math/big" "math/big"
"math/rand/v2" "math/rand/v2"
@ -42,3 +44,17 @@ func GenerateCashoutID() (string, error) {
return string(result), nil return string(result), nil
} }
func MaskPhone(phone string) string {
if phone == "" {
return ""
}
return phone[:4] + "**" + phone[len(phone)-2:]
}
func MaskEmail(email string) string {
if email == "" {
return ""
}
return email[:3] + "**" + email[strings.Index(email, "@"):]
}

View File

@ -52,6 +52,18 @@ func convertCreateRaffle(raffle domain.CreateRaffle) dbgen.CreateRaffleParams {
} }
} }
func convertRaffleStanding(raffleStanding dbgen.GetRaffleStandingRow) domain.RaffleStanding {
return domain.RaffleStanding{
UserID: raffleStanding.UserID,
RaffleID: raffleStanding.RaffleID,
FirstName: raffleStanding.FirstName,
LastName: raffleStanding.LastName,
PhoneNumber: raffleStanding.PhoneNumber.String,
Email: raffleStanding.Email.String,
TicketCount: raffleStanding.TicketCount,
}
}
func (s *Store) CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) (domain.Raffle, error) { func (s *Store) CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) (domain.Raffle, error) {
raffleRes, err := s.queries.CreateRaffle(ctx, convertCreateRaffle(raffle)) raffleRes, err := s.queries.CreateRaffle(ctx, convertCreateRaffle(raffle))
if err != nil { if err != nil {
@ -126,3 +138,34 @@ func (s *Store) UnSuspendRaffleTicket(ctx context.Context, raffleID int32) error
} }
// TODO: could also add -> suspend a specific user's raffle tickets // TODO: could also add -> suspend a specific user's raffle tickets
func (s *Store) GetRaffleStanding(ctx context.Context, raffleID, limit int32) ([]domain.RaffleStanding, error) {
raffleStanding, err := s.queries.GetRaffleStanding(ctx, dbgen.GetRaffleStandingParams{
RaffleID: raffleID,
Limit: limit,
})
if err != nil {
return nil, err
}
res := []domain.RaffleStanding{}
for _, standing := range raffleStanding {
res = append(res, convertRaffleStanding(standing))
}
return res, nil
}
func (s *Store) CreateRaffleWinner(ctx context.Context, raffleWinnerParams domain.RaffleWinnerParams) error {
_, err := s.queries.CreateRaffleWinner(ctx, dbgen.CreateRaffleWinnerParams{
RaffleID: raffleWinnerParams.RaffleID,
UserID: raffleWinnerParams.UserID,
Rank: raffleWinnerParams.Rank,
})
return err
}
func (s *Store) SetRaffleComplete(ctx context.Context, raffleID int32) error {
return s.queries.SetRaffleComplete(ctx, raffleID)
}

View File

@ -11,6 +11,9 @@ type RaffleStore interface {
CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) (domain.Raffle, error) CreateRaffle(ctx context.Context, raffle domain.CreateRaffle) (domain.Raffle, error)
DeleteRaffle(ctx context.Context, raffleID int32) (domain.Raffle, error) DeleteRaffle(ctx context.Context, raffleID int32) (domain.Raffle, error)
GetRafflesOfCompany(ctx context.Context, companyID int32) ([]dbgen.Raffle, error) GetRafflesOfCompany(ctx context.Context, companyID int32) ([]dbgen.Raffle, error)
GetRaffleStanding(ctx context.Context, raffleID, limit int32) ([]domain.RaffleStanding, error)
CreateRaffleWinner(ctx context.Context, raffleWinnerParams domain.RaffleWinnerParams) error
SetRaffleComplete(ctx context.Context, raffleID int32) error
CreateRaffleTicket(ctx context.Context, raffleTicketParams domain.CreateRaffleTicket) (domain.RaffleTicket, error) CreateRaffleTicket(ctx context.Context, raffleTicketParams domain.CreateRaffleTicket) (domain.RaffleTicket, error)
GetUserRaffleTickets(ctx context.Context, userID int32) ([]domain.RaffleTicketRes, error) GetUserRaffleTickets(ctx context.Context, userID int32) ([]domain.RaffleTicketRes, error)
SuspendRaffleTicket(ctx context.Context, raffleTicketID int32) error SuspendRaffleTicket(ctx context.Context, raffleTicketID int32) error

View File

@ -29,6 +29,18 @@ func (s *Service) GetRafflesOfCompany(ctx context.Context, companyID int32) ([]d
return s.raffleStore.GetRafflesOfCompany(ctx, companyID) return s.raffleStore.GetRafflesOfCompany(ctx, companyID)
} }
func (s *Service) GetRaffleStanding(ctx context.Context, raffleID, limit int32) ([]domain.RaffleStanding, error) {
return s.raffleStore.GetRaffleStanding(ctx, raffleID, limit)
}
func (s *Service) CreateRaffleWinner(ctx context.Context, raffleWinnerParams domain.RaffleWinnerParams) error {
return s.raffleStore.CreateRaffleWinner(ctx, raffleWinnerParams)
}
func (s *Service) SetRaffleComplete(ctx context.Context, raffleID int32) error {
return s.raffleStore.SetRaffleComplete(ctx, raffleID)
}
func (s *Service) CreateRaffleTicket(ctx context.Context, raffleTicketParams domain.CreateRaffleTicket) (domain.RaffleTicket, error) { func (s *Service) CreateRaffleTicket(ctx context.Context, raffleTicketParams domain.CreateRaffleTicket) (domain.RaffleTicket, error) {
return s.raffleStore.CreateRaffleTicket(ctx, raffleTicketParams) return s.raffleStore.CreateRaffleTicket(ctx, raffleTicketParams)
} }

View File

@ -1,11 +1,13 @@
package handlers package handlers
import ( import (
"errors"
"fmt" "fmt"
"strconv" "strconv"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"go.uber.org/zap" "go.uber.org/zap"
@ -101,9 +103,102 @@ func (h *Handler) GetRafflesOfCompany(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Company Raffles fetched successfully", companyRaffles, nil) return response.WriteJSON(c, fiber.StatusOK, "Company Raffles fetched successfully", companyRaffles, nil)
} }
func (h *Handler) GetRaffleStanding(c *fiber.Ctx) error {
raffleIDStr := c.Params("id")
limitStr := c.Params("limit")
// if error happens while parsing, it just uses zero values
// resulting in empty standing
raffleID, _ := strconv.Atoi(raffleIDStr)
limit, _ := strconv.Atoi(limitStr)
raffleStanding, err := h.raffleSvc.GetRaffleStanding(c.Context(), int32(raffleID), int32(limit))
if err != nil {
h.mongoLoggerSvc.Error("Failed to fetch raffle standing",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch raffle standing")
}
maskedRaffleStanding := []domain.RaffleStanding{}
for _, standing := range raffleStanding {
maskedStanding := domain.RaffleStanding{
UserID: standing.UserID,
RaffleID: standing.RaffleID,
FirstName: standing.FirstName,
LastName: standing.LastName,
PhoneNumber: helpers.MaskPhone(standing.PhoneNumber),
Email: helpers.MaskEmail(standing.Email),
TicketCount: standing.TicketCount,
}
maskedRaffleStanding = append(maskedRaffleStanding, maskedStanding)
}
return response.WriteJSON(c, fiber.StatusOK, "Raffles standing fetched successfully", maskedRaffleStanding, nil)
}
func (h *Handler) GetRaffleWinners(c *fiber.Ctx) error {
raffleIDStr := c.Params("id")
limitStr := c.Params("limit")
// if error happens while parsing, it just uses zero values
// resulting in empty standing
raffleID, _ := strconv.Atoi(raffleIDStr)
limit, _ := strconv.Atoi(limitStr)
raffleStanding, err := h.raffleSvc.GetRaffleStanding(c.Context(), int32(raffleID), int32(limit))
if err != nil {
h.mongoLoggerSvc.Error("Failed to fetch raffle standing",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch raffle standing")
}
// set raffle as complete
if err := h.raffleSvc.SetRaffleComplete(c.Context(), int32(raffleID)); err != nil {
h.mongoLoggerSvc.Error("Failed to set raffle complete",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to set raffle complete")
}
// add winners to table
var errs []error
for i, standing := range raffleStanding {
err = h.raffleSvc.CreateRaffleWinner(c.Context(), domain.RaffleWinnerParams{
RaffleID: standing.RaffleID,
UserID: int32(standing.UserID),
Rank: int32(i + 1),
})
if err != nil {
errs = append(errs, err)
}
}
if len(errs) != 0 {
h.mongoLoggerSvc.Error("Failed to create raffle winners",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(errors.Join(errs...)),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create raffle winners")
}
return nil
}
func (h *Handler) CreateRaffleTicket(c *fiber.Ctx) error { func (h *Handler) CreateRaffleTicket(c *fiber.Ctx) error {
var req domain.CreateRaffleTicket var req domain.CreateRaffleTicket
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
fmt.Println("parser error: ", err)
h.mongoLoggerSvc.Info("Failed to parse raffle ticket request", h.mongoLoggerSvc.Info("Failed to parse raffle ticket request",
zap.Int("status_code", fiber.StatusBadRequest), zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err), zap.Error(err),
@ -127,6 +222,7 @@ func (h *Handler) CreateRaffleTicket(c *fiber.Ctx) error {
raffleTicket, err := h.raffleSvc.CreateRaffleTicket(c.Context(), req) raffleTicket, err := h.raffleSvc.CreateRaffleTicket(c.Context(), req)
if err != nil { if err != nil {
fmt.Println("raffle ticket create error: ", err)
h.mongoLoggerSvc.Error("Failed to create raffle ticket", h.mongoLoggerSvc.Error("Failed to create raffle ticket",
zap.Int("status_code", fiber.StatusInternalServerError), zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err), zap.Error(err),

View File

@ -201,6 +201,8 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/raffle/create", a.authMiddleware, h.CreateRaffle) a.fiber.Post("/raffle/create", a.authMiddleware, h.CreateRaffle)
a.fiber.Get("/raffle/delete/:id", a.authMiddleware, h.DeleteRaffle) a.fiber.Get("/raffle/delete/:id", a.authMiddleware, h.DeleteRaffle)
a.fiber.Get("/raffle/company/:id", a.authMiddleware, h.GetRafflesOfCompany) a.fiber.Get("/raffle/company/:id", a.authMiddleware, h.GetRafflesOfCompany)
a.fiber.Get("/raffle/standing/:id/:limit", a.authMiddleware, h.GetRaffleStanding)
a.fiber.Get("raffle/winners/:id/:limit", a.authMiddleware, h.GetRaffleWinners)
a.fiber.Post("/raffle-ticket/create", a.authMiddleware, h.CreateRaffleTicket) a.fiber.Post("/raffle-ticket/create", a.authMiddleware, h.CreateRaffleTicket)
a.fiber.Get("/raffle-ticket/:id", a.authMiddleware, h.GetUserRaffleTickets) a.fiber.Get("/raffle-ticket/:id", a.authMiddleware, h.GetUserRaffleTickets)
a.fiber.Get("/raffle-ticket/suspend/:id", a.authMiddleware, h.SuspendRaffleTicket) a.fiber.Get("/raffle-ticket/suspend/:id", a.authMiddleware, h.SuspendRaffleTicket)