fix: added wallet type to wallet and other minor fixes

This commit is contained in:
Samuel Tariku 2025-07-20 22:40:50 +03:00
parent 972d62d568
commit 65bd5ab3f5
18 changed files with 371 additions and 62 deletions

View File

@ -133,6 +133,7 @@ CREATE TABLE IF NOT EXISTS wallets (
is_bettable BOOLEAN NOT NULL, is_bettable BOOLEAN NOT NULL,
is_transferable BOOLEAN NOT NULL, is_transferable BOOLEAN NOT NULL,
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
type VARCHAR(255) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true, is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
@ -335,12 +336,16 @@ CREATE TABLE flags (
reason TEXT, reason TEXT,
flagged_at TIMESTAMP DEFAULT NOW(), flagged_at TIMESTAMP DEFAULT NOW(),
resolved BOOLEAN DEFAULT FALSE, resolved BOOLEAN DEFAULT FALSE,
-- either bet or odd is flagged (not at the same time) -- either bet or odd is flagged (not at the same time)
CHECK ( CHECK (
(bet_id IS NOT NULL AND odd_id IS NULL) (
OR bet_id IS NOT NULL
(bet_id IS NULL AND odd_id IS NOT NULL) AND odd_id IS NULL
)
OR (
bet_id IS NULL
AND odd_id IS NOT NULL
)
) )
); );
-- Views -- Views

View File

@ -45,8 +45,7 @@ VALUES ('addis_ababa', 'Addis Ababa'),
('meki', 'Meki'), ('meki', 'Meki'),
('negele_borana', 'Negele Borana'), ('negele_borana', 'Negele Borana'),
('alaba_kulito', 'Alaba Kulito'), ('alaba_kulito', 'Alaba Kulito'),
('alamata 14,', 'Alamata 14,'), ('alamata,', 'Alamata,'),
('030', '030'),
('chiro', 'Chiro'), ('chiro', 'Chiro'),
('tepi', 'Tepi'), ('tepi', 'Tepi'),
('durame', 'Durame'), ('durame', 'Durame'),

View File

@ -3,9 +3,10 @@ INSERT INTO wallets (
is_withdraw, is_withdraw,
is_bettable, is_bettable,
is_transferable, is_transferable,
user_id user_id,
type
) )
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4, $5)
RETURNING *; RETURNING *;
-- name: CreateCustomerWallet :one -- name: CreateCustomerWallet :one
INSERT INTO customer_wallets ( INSERT INTO customer_wallets (

View File

@ -674,6 +674,7 @@ type Wallet struct {
IsBettable bool `json:"is_bettable"` IsBettable bool `json:"is_bettable"`
IsTransferable bool `json:"is_transferable"` IsTransferable bool `json:"is_transferable"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Type string `json:"type"`
IsActive bool `json:"is_active"` IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamp `json:"created_at"` CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"`

View File

@ -46,10 +46,11 @@ INSERT INTO wallets (
is_withdraw, is_withdraw,
is_bettable, is_bettable,
is_transferable, is_transferable,
user_id user_id,
type
) )
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4, $5)
RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, currency, bonus_balance, cash_balance
` `
type CreateWalletParams struct { type CreateWalletParams struct {
@ -57,6 +58,7 @@ type CreateWalletParams struct {
IsBettable bool `json:"is_bettable"` IsBettable bool `json:"is_bettable"`
IsTransferable bool `json:"is_transferable"` IsTransferable bool `json:"is_transferable"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Type string `json:"type"`
} }
func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wallet, error) { func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wallet, error) {
@ -65,6 +67,7 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal
arg.IsBettable, arg.IsBettable,
arg.IsTransferable, arg.IsTransferable,
arg.UserID, arg.UserID,
arg.Type,
) )
var i Wallet var i Wallet
err := row.Scan( err := row.Scan(
@ -74,6 +77,7 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal
&i.IsBettable, &i.IsBettable,
&i.IsTransferable, &i.IsTransferable,
&i.UserID, &i.UserID,
&i.Type,
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -184,7 +188,7 @@ func (q *Queries) GetAllCustomerWallet(ctx context.Context) ([]CustomerWalletDet
} }
const GetAllWallets = `-- name: GetAllWallets :many const GetAllWallets = `-- name: GetAllWallets :many
SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, currency, bonus_balance, cash_balance
FROM wallets FROM wallets
` `
@ -204,6 +208,7 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) {
&i.IsBettable, &i.IsBettable,
&i.IsTransferable, &i.IsTransferable,
&i.UserID, &i.UserID,
&i.Type,
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -314,7 +319,7 @@ func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (Cust
} }
const GetWalletByID = `-- name: GetWalletByID :one const GetWalletByID = `-- name: GetWalletByID :one
SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, currency, bonus_balance, cash_balance
FROM wallets FROM wallets
WHERE id = $1 WHERE id = $1
` `
@ -329,6 +334,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) {
&i.IsBettable, &i.IsBettable,
&i.IsTransferable, &i.IsTransferable,
&i.UserID, &i.UserID,
&i.Type,
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -340,7 +346,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) {
} }
const GetWalletByUserID = `-- name: GetWalletByUserID :many const GetWalletByUserID = `-- name: GetWalletByUserID :many
SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance SELECT id, balance, is_withdraw, is_bettable, is_transferable, user_id, type, is_active, created_at, updated_at, currency, bonus_balance, cash_balance
FROM wallets FROM wallets
WHERE user_id = $1 WHERE user_id = $1
` `
@ -361,6 +367,7 @@ func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet
&i.IsBettable, &i.IsBettable,
&i.IsTransferable, &i.IsTransferable,
&i.UserID, &i.UserID,
&i.Type,
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,

View File

@ -104,7 +104,7 @@ type CreateBetOutcomeReq struct {
type CreateBetReq struct { type CreateBetReq struct {
Outcomes []CreateBetOutcomeReq `json:"outcomes" validate:"required"` Outcomes []CreateBetOutcomeReq `json:"outcomes" validate:"required"`
Amount float32 `json:"amount" validate:"required,gt=0" example:"100.0"` Amount float32 `json:"amount" validate:"required,gt=0" example:"100.0"`
BranchID *int64 `json:"branch_id,omitempty" validate:"required" example:"1"` BranchID *int64 `json:"branch_id,omitempty" example:"1"`
} }
type CreateBetWithFastCodeReq struct { type CreateBetWithFastCodeReq struct {

View File

@ -56,6 +56,7 @@ type UpdateCompany struct {
type CreateCompanyReq struct { type CreateCompanyReq struct {
Name string `json:"name" example:"CompanyName"` Name string `json:"name" example:"CompanyName"`
AdminID int64 `json:"admin_id" example:"1"` AdminID int64 `json:"admin_id" example:"1"`
DeductedPercentage float32 `json:"deducted_percentage" example:"0.1" validate:"lt=1"`
} }
type UpdateCompanyReq struct { type UpdateCompanyReq struct {
Name *string `json:"name,omitempty" example:"CompanyName"` Name *string `json:"name,omitempty" example:"CompanyName"`

View File

@ -30,7 +30,7 @@ type OtpProvider string
const ( const (
TwilioSms OtpProvider = "twilio" TwilioSms OtpProvider = "twilio"
AfroMessage OtpProvider = "aformessage" AfroMessage OtpProvider = "afro_message"
) )
type Otp struct { type Otp struct {

View File

@ -11,6 +11,7 @@ type Wallet struct {
IsTransferable bool IsTransferable bool
IsActive bool IsActive bool
UserID int64 UserID int64
Type WalletType
UpdatedAt time.Time UpdatedAt time.Time
CreatedAt time.Time CreatedAt time.Time
} }
@ -63,6 +64,7 @@ type CreateWallet struct {
IsBettable bool IsBettable bool
IsTransferable bool IsTransferable bool
UserID int64 UserID int64
Type WalletType
} }
type CreateCustomerWallet struct { type CreateCustomerWallet struct {

View File

@ -17,6 +17,7 @@ func convertDBWallet(wallet dbgen.Wallet) domain.Wallet {
IsTransferable: wallet.IsTransferable, IsTransferable: wallet.IsTransferable,
IsActive: wallet.IsActive, IsActive: wallet.IsActive,
UserID: wallet.UserID, UserID: wallet.UserID,
Type: domain.WalletType(wallet.Type),
UpdatedAt: wallet.UpdatedAt.Time, UpdatedAt: wallet.UpdatedAt.Time,
CreatedAt: wallet.CreatedAt.Time, CreatedAt: wallet.CreatedAt.Time,
} }
@ -28,6 +29,7 @@ func convertCreateWallet(wallet domain.CreateWallet) dbgen.CreateWalletParams {
IsBettable: wallet.IsBettable, IsBettable: wallet.IsBettable,
IsTransferable: wallet.IsTransferable, IsTransferable: wallet.IsTransferable,
UserID: wallet.UserID, UserID: wallet.UserID,
Type: string(wallet.Type),
} }
} }
@ -275,4 +277,3 @@ func (s *Store) GetTotalWallets(ctx context.Context, filter domain.ReportFilter)
return total, nil return total, nil
} }

View File

@ -23,11 +23,11 @@ func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpF
switch medium { switch medium {
case domain.OtpMediumSms: case domain.OtpMediumSms:
switch provider { switch provider {
case "twilio": case domain.TwilioSms:
if err := s.SendTwilioSMSOTP(ctx, sentTo, message, provider); err != nil { if err := s.SendTwilioSMSOTP(ctx, sentTo, message, provider); err != nil {
return err return err
} }
case "afromessage": case domain.AfroMessage:
if err := s.SendAfroMessageSMSOTP(ctx, sentTo, message, provider); err != nil { if err := s.SendAfroMessageSMSOTP(ctx, sentTo, message, provider); err != nil {
return err return err
} }

View File

@ -126,11 +126,29 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
if wallet.Balance < amount { if wallet.Balance < amount {
// Send Wallet low to admin // Send Wallet low to admin
if walletType == domain.CompanyWalletType || walletType == domain.BranchWalletType { if walletType == domain.CompanyWalletType || walletType == domain.BranchWalletType {
s.SendAdminWalletLowNotification(ctx, wallet, amount) s.SendAdminWalletInsufficientNotification(ctx, wallet, amount)
} }
return domain.Transfer{}, ErrBalanceInsufficient return domain.Transfer{}, ErrBalanceInsufficient
} }
if wallet.Type == domain.BranchWalletType || wallet.Type == domain.CompanyWalletType {
var thresholds []float32
if wallet.Type == domain.CompanyWalletType {
thresholds = []float32{100000, 50000, 25000, 10000, 5000, 3000, 1000, 500}
} else {
thresholds = []float32{5000, 3000, 1000, 500}
}
balance := wallet.Balance.Float32()
for _, threshold := range thresholds {
if balance < threshold {
s.SendAdminWalletLowNotification(ctx, wallet)
break // only send once per check
}
}
}
err = s.walletStore.UpdateBalance(ctx, id, wallet.Balance-amount) err = s.walletStore.UpdateBalance(ctx, id, wallet.Balance-amount)
if err != nil { if err != nil {
@ -197,30 +215,28 @@ func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive boo
return s.walletStore.UpdateWalletActive(ctx, id, isActive) return s.walletStore.UpdateWalletActive(ctx, id, isActive)
} }
func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWallet domain.Wallet, amount domain.Currency) error { func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWallet domain.Wallet) error {
// Send notification to admin team // Send notification to admin team
adminNotification := &domain.Notification{ adminNotification := &domain.Notification{
RecipientID: adminWallet.UserID, RecipientID: adminWallet.UserID,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT, Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelError, Level: domain.NotificationLevelWarning,
Reciever: domain.NotificationRecieverSideAdmin, Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel
Payload: domain.NotificationPayload{ Payload: domain.NotificationPayload{
Headline: "CREDIT WARNING: System Running Out of Funds", Headline: "CREDIT WARNING: System Running Out of Funds",
Message: fmt.Sprintf( Message: fmt.Sprintf(
"Wallet ID %d has insufficient balance for transfer. Current balance: %.2f, Attempted transfer: %.2f", "Wallet ID %d is running low. Current balance: %.2f",
adminWallet.ID, adminWallet.ID,
adminWallet.Balance.Float32(), adminWallet.Balance.Float32(),
amount.Float32(),
), ),
}, },
Priority: 1, // High priority for admin alerts Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{ Metadata: fmt.Appendf(nil, `{
"wallet_id": %d, "wallet_id": %d,
"balance": %d, "balance": %d,
"required_amount": %d,
"notification_type": "admin_alert" "notification_type": "admin_alert"
}`, adminWallet.ID, adminWallet.Balance, amount), }`, adminWallet.ID, adminWallet.Balance),
} }
// Get admin recipients and send to all // Get admin recipients and send to all
@ -240,3 +256,85 @@ func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWalle
} }
return nil return nil
} }
func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, adminWallet domain.Wallet, amount domain.Currency) error {
// Send notification to admin team
adminNotification := &domain.Notification{
RecipientID: adminWallet.UserID,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelError,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT Error: Admin Wallet insufficient to process customer request",
Message: fmt.Sprintf(
"Wallet ID %d. Transaction Amount %.2f. Current balance: %.2f",
adminWallet.ID,
amount.Float32(),
adminWallet.Balance.Float32(),
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"transaction amount": %.2f,
"notification_type": "admin_alert"
}`, adminWallet.ID, adminWallet.Balance, amount.Float32()),
}
// Get admin recipients and send to all
adminRecipients, err := s.notificationStore.ListRecipientIDs(ctx, domain.NotificationRecieverSideAdmin)
if err != nil {
s.logger.Error("failed to get admin recipients", "error", err)
return err
} else {
for _, adminID := range adminRecipients {
adminNotification.RecipientID = adminID
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
s.logger.Error("failed to send admin notification",
"admin_id", adminID,
"error", err)
}
}
}
return nil
}
func (s *Service) SendCustomerWalletInsufficientNotification(ctx context.Context, customerWallet domain.Wallet, amount domain.Currency) error {
// Send notification to admin team
adminNotification := &domain.Notification{
RecipientID: customerWallet.UserID,
Type: domain.NOTIFICATION_TYPE_WALLET,
Level: domain.NotificationLevelError,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel
Payload: domain.NotificationPayload{
Headline: "CREDIT Error: Admin Wallet insufficient to process customer request",
Message: fmt.Sprintf(
"Wallet ID %d. Transaction Amount %.2f. Current balance: %.2f",
customerWallet.ID,
amount.Float32(),
customerWallet.Balance.Float32(),
),
},
Priority: 1, // High priority for admin alerts
Metadata: fmt.Appendf(nil, `{
"wallet_id": %d,
"balance": %d,
"transaction amount": %.2f,
"notification_type": "admin_alert"
}`, customerWallet.ID, customerWallet.Balance, amount.Float32()),
}
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
s.logger.Error("failed to send customer notification",
"admin_id", customerWallet.UserID,
"error", err)
}
return nil
}

View File

@ -24,22 +24,22 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
spec string spec string
task func() task func()
}{ }{
{ // {
spec: "0 0 * * * *", // Every 1 hour // spec: "0 0 * * * *", // Every 1 hour
task: func() { // task: func() {
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
log.Printf("FetchUpcomingEvents error: %v", err) // log.Printf("FetchUpcomingEvents error: %v", err)
} // }
}, // },
}, // },
{ // {
spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events) // spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
task: func() { // task: func() {
if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
log.Printf("FetchNonLiveOdds error: %v", err) // log.Printf("FetchNonLiveOdds error: %v", err)
} // }
}, // },
}, // },
{ {
spec: "0 */5 * * * *", // Every 5 Minutes spec: "0 */5 * * * *", // Every 5 Minutes
task: func() { task: func() {
@ -114,10 +114,10 @@ func SetupReportCronJobs(ctx context.Context, reportService *report.Service) {
spec string spec string
period string period string
}{ }{
{ // {
spec: "*/300 * * * * *", // Every 5 minutes (300 seconds) // spec: "*/300 * * * * *", // Every 5 minutes (300 seconds)
period: "5min", // period: "5min",
}, // },
{ {
spec: "0 0 0 * * *", // Daily at midnight spec: "0 0 0 * * *", // Daily at midnight
period: "daily", period: "daily",

View File

@ -15,7 +15,7 @@ import (
// loginCustomerReq represents the request body for the LoginCustomer endpoint. // loginCustomerReq represents the request body for the LoginCustomer endpoint.
type loginCustomerReq struct { type loginCustomerReq struct {
Email string `json:"email" validate:"email" example:"john.doe@example.com"` Email string `json:"email" validate:"required_without=PhoneNumber" example:"john.doe@example.com"`
PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"`
Password string `json:"password" validate:"required" example:"password123"` Password string `json:"password" validate:"required" example:"password123"`
} }

View File

@ -73,21 +73,28 @@ func (h *Handler) GetDashboardReport(c *fiber.Ctx) error {
func parseReportFilter(c *fiber.Ctx) (domain.ReportFilter, error) { func parseReportFilter(c *fiber.Ctx) (domain.ReportFilter, error) {
var filter domain.ReportFilter var filter domain.ReportFilter
var err error var err error
role := c.Locals("role").(domain.Role)
if c.Query("company_id") != "" && role == domain.RoleSuperAdmin {
if c.Query("company_id") != "" {
companyID, err := strconv.ParseInt(c.Query("company_id"), 10, 64) companyID, err := strconv.ParseInt(c.Query("company_id"), 10, 64)
if err != nil { if err != nil {
return domain.ReportFilter{}, fmt.Errorf("invalid company_id: %w", err) return domain.ReportFilter{}, fmt.Errorf("invalid company_id: %w", err)
} }
filter.CompanyID = domain.ValidInt64{Value: companyID, Valid: true} filter.CompanyID = domain.ValidInt64{Value: companyID, Valid: true}
} else {
filter.CompanyID = c.Locals("company_id").(domain.ValidInt64)
} }
if c.Query("branch_id") != "" { if c.Query("branch_id") != "" && role == domain.RoleSuperAdmin {
branchID, err := strconv.ParseInt(c.Query("branch_id"), 10, 64) branchID, err := strconv.ParseInt(c.Query("branch_id"), 10, 64)
if err != nil { if err != nil {
return domain.ReportFilter{}, fmt.Errorf("invalid branch_id: %w", err) return domain.ReportFilter{}, fmt.Errorf("invalid branch_id: %w", err)
} }
filter.BranchID = domain.ValidInt64{Value: branchID, Valid: true} filter.BranchID = domain.ValidInt64{Value: branchID, Valid: true}
} else {
filter.BranchID = c.Locals("branch_id").(domain.ValidInt64)
} }
if c.Query("user_id") != "" { if c.Query("user_id") != "" {

View File

@ -116,6 +116,89 @@ func (h *Handler) GetShopBetByBetID(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Shop bet fetched successfully", res, nil) return response.WriteJSON(c, fiber.StatusOK, "Shop bet fetched successfully", res, nil)
} }
// GetAllShopBets godoc
// @Summary Gets all shop bets
// @Description Gets all the shop bets
// @Tags bet
// @Accept json
// @Produce json
// @Success 200 {array} domain.ShopBetRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/shop/bet [get]
func (h *Handler) GetAllShopBets(c *fiber.Ctx) error {
// role := c.Locals("role").(domain.Role)
companyID := c.Locals("company_id").(domain.ValidInt64)
branchID := c.Locals("branch_id").(domain.ValidInt64)
searchQuery := c.Query("query")
searchString := domain.ValidString{
Value: searchQuery,
Valid: searchQuery != "",
}
createdBeforeQuery := c.Query("created_before")
var createdBefore domain.ValidTime
if createdBeforeQuery != "" {
createdBeforeParsed, err := time.Parse(time.RFC3339, createdBeforeQuery)
if err != nil {
h.mongoLoggerSvc.Info("invalid created_before format",
zap.String("time", createdBeforeQuery),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid created_before format")
}
createdBefore = domain.ValidTime{
Value: createdBeforeParsed,
Valid: true,
}
}
createdAfterQuery := c.Query("created_after")
var createdAfter domain.ValidTime
if createdAfterQuery != "" {
createdAfterParsed, err := time.Parse(time.RFC3339, createdAfterQuery)
if err != nil {
h.mongoLoggerSvc.Info("invalid created_after format",
zap.String("created_after", createdAfterQuery),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid created_after format")
}
createdAfter = domain.ValidTime{
Value: createdAfterParsed,
Valid: true,
}
}
bets, err := h.transactionSvc.GetAllShopBet(c.Context(), domain.ShopBetFilter{
Query: searchString,
CreatedBefore: createdBefore,
CreatedAfter: createdAfter,
CompanyID: companyID,
BranchID: branchID,
})
if err != nil {
h.mongoLoggerSvc.Error("Failed to get all bets",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets፡"+err.Error())
}
res := make([]domain.ShopBetRes, len(bets))
for i, bet := range bets {
res[i] = domain.ConvertShopBetDetail(bet)
}
return response.WriteJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil)
}
// CashoutBet godoc // CashoutBet godoc
// @Summary Cashout bet at branch // @Summary Cashout bet at branch
// @Description Cashout bet at branch // @Description Cashout bet at branch

View File

@ -248,7 +248,7 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
// TODO: Remove later // TODO: Remove later
_, err = h.walletSvc.AddToWallet( _, err = h.walletSvc.AddToWallet(
c.Context(), newWallet.RegularID, domain.ToCurrency(10000.0), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, c.Context(), newWallet.RegularID, domain.ToCurrency(10000.0), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{},
"Added 100.0 to wallet only as test for deployment") "Added 10000.0 to wallet only as test for deployment")
if err != nil { if err != nil {
h.mongoLoggerSvc.Error("Failed to update wallet for user", h.mongoLoggerSvc.Error("Failed to update wallet for user",
@ -417,20 +417,121 @@ type UserProfileRes struct {
LastLogin time.Time `json:"last_login"` LastLogin time.Time `json:"last_login"`
SuspendedAt time.Time `json:"suspended_at"` SuspendedAt time.Time `json:"suspended_at"`
Suspended bool `json:"suspended"` Suspended bool `json:"suspended"`
ReferralCode string `json:"referral_code"`
} }
// UserProfile godoc type CustomerProfileRes 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"`
LastLogin time.Time `json:"last_login"`
SuspendedAt time.Time `json:"suspended_at"`
Suspended bool `json:"suspended"`
ReferralCode string `json:"referral_code"`
}
// CustomerProfile godoc
// @Summary Get user profile // @Summary Get user profile
// @Description Get user profile // @Description Get user profile
// @Tags user // @Tags user
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} UserProfileRes // @Success 200 {object} CustomerProfileRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Security Bearer // @Security Bearer
// @Router /api/v1/user/profile [get] // @Router /api/v1/user/customer-profile [get]
func (h *Handler) UserProfile(c *fiber.Ctx) error { func (h *Handler) CustomerProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.mongoLoggerSvc.Error("Invalid user ID in context",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification")
}
user, err := h.userSvc.GetUserByID(c.Context(), userID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get user profile",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user profile:"+err.Error())
}
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
if err != nil {
if err != authentication.ErrRefreshTokenNotFound {
h.mongoLoggerSvc.Error("Failed to get user last login",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login:"+err.Error())
}
lastLogin = &user.CreatedAt
}
res := CustomerProfileRes{
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,
LastLogin: *lastLogin,
}
return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil)
}
type AdminProfileRes 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"`
LastLogin time.Time `json:"last_login"`
SuspendedAt time.Time `json:"suspended_at"`
Suspended bool `json:"suspended"`
}
// AdminProfile godoc
// @Summary Get user profile
// @Description Get user profile
// @Tags user
// @Accept json
// @Produce json
// @Success 200 {object} AdminProfileRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security Bearer
// @Router /api/v1/user/admin-profile [get]
func (h *Handler) AdminProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64) userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 { if !ok || userID == 0 {

View File

@ -105,7 +105,8 @@ func (a *App) initAppRoutes() {
groupV1.Post("/user/register", h.RegisterUser) groupV1.Post("/user/register", h.RegisterUser)
groupV1.Post("/user/sendRegisterCode", h.SendRegisterCode) groupV1.Post("/user/sendRegisterCode", h.SendRegisterCode)
groupV1.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist) groupV1.Post("/user/checkPhoneEmailExist", h.CheckPhoneEmailExist)
groupV1.Get("/user/profile", a.authMiddleware, h.UserProfile) groupV1.Get("/user/customer-profile", a.authMiddleware, h.CustomerProfile)
groupV1.Get("/user/admin-profile", a.authMiddleware, h.AdminProfile)
groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID) groupV1.Get("/user/single/:id", a.authMiddleware, h.GetUserByID)
groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser) groupV1.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser)
groupV1.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend) groupV1.Post("/user/suspend", a.authMiddleware, h.UpdateUserSuspend)
@ -174,6 +175,7 @@ func (a *App) initAppRoutes() {
groupV1.Delete("/branch/:id", a.authMiddleware, h.DeleteBranch) groupV1.Delete("/branch/:id", a.authMiddleware, h.DeleteBranch)
groupV1.Get("/search/branch", a.authMiddleware, h.SearchBranch) groupV1.Get("/search/branch", a.authMiddleware, h.SearchBranch)
groupV1.Get("/branchLocation", a.authMiddleware, h.GetAllBranchLocations) groupV1.Get("/branchLocation", a.authMiddleware, h.GetAllBranchLocations)
groupV1.Get("/branch/:id/cashiers", a.authMiddleware, h.GetBranchCashiers) groupV1.Get("/branch/:id/cashiers", a.authMiddleware, h.GetBranchCashiers)
@ -287,6 +289,7 @@ func (a *App) initAppRoutes() {
// Transactions /shop/transactions // Transactions /shop/transactions
groupV1.Post("/shop/bet", a.authMiddleware, a.CompanyOnly, h.CreateShopBet) groupV1.Post("/shop/bet", a.authMiddleware, a.CompanyOnly, h.CreateShopBet)
groupV1.Get("/shop/bet", a.authMiddleware, a.CompanyOnly, h.GetAllShopBets)
groupV1.Get("/shop/bet/:id", a.authMiddleware, a.CompanyOnly, h.GetShopBetByBetID) groupV1.Get("/shop/bet/:id", a.authMiddleware, a.CompanyOnly, h.GetShopBetByBetID)
groupV1.Post("/shop/bet/:id/cashout", a.authMiddleware, a.CompanyOnly, h.CashoutBet) groupV1.Post("/shop/bet/:id/cashout", a.authMiddleware, a.CompanyOnly, h.CashoutBet)
groupV1.Post("/shop/bet/:id/generate", a.authMiddleware, a.CompanyOnly, h.CashoutBet) groupV1.Post("/shop/bet/:id/generate", a.authMiddleware, a.CompanyOnly, h.CashoutBet)