diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 77ef8ee..6892944 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -133,6 +133,7 @@ CREATE TABLE IF NOT EXISTS wallets ( is_bettable BOOLEAN NOT NULL, is_transferable BOOLEAN NOT NULL, user_id BIGINT NOT NULL, + type VARCHAR(255) NOT NULL, is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP @@ -335,12 +336,16 @@ CREATE TABLE flags ( reason TEXT, flagged_at TIMESTAMP DEFAULT NOW(), resolved BOOLEAN DEFAULT FALSE, - -- either bet or odd is flagged (not at the same time) CHECK ( - (bet_id IS NOT NULL AND odd_id IS NULL) - OR - (bet_id IS NULL AND odd_id IS NOT NULL) + ( + bet_id IS NOT NULL + AND odd_id IS NULL + ) + OR ( + bet_id IS NULL + AND odd_id IS NOT NULL + ) ) ); -- Views diff --git a/db/migrations/000009_location_data.up.sql b/db/migrations/000009_location_data.up.sql index 3d9c67d..156831d 100644 --- a/db/migrations/000009_location_data.up.sql +++ b/db/migrations/000009_location_data.up.sql @@ -45,8 +45,7 @@ VALUES ('addis_ababa', 'Addis Ababa'), ('meki', 'Meki'), ('negele_borana', 'Negele Borana'), ('alaba_kulito', 'Alaba Kulito'), - ('alamata 14,', 'Alamata 14,'), - ('030', '030'), + ('alamata,', 'Alamata,'), ('chiro', 'Chiro'), ('tepi', 'Tepi'), ('durame', 'Durame'), diff --git a/db/query/wallet.sql b/db/query/wallet.sql index d22effe..a6c9998 100644 --- a/db/query/wallet.sql +++ b/db/query/wallet.sql @@ -3,9 +3,10 @@ INSERT INTO wallets ( is_withdraw, is_bettable, is_transferable, - user_id + user_id, + type ) -VALUES ($1, $2, $3, $4) +VALUES ($1, $2, $3, $4, $5) RETURNING *; -- name: CreateCustomerWallet :one INSERT INTO customer_wallets ( diff --git a/gen/db/models.go b/gen/db/models.go index 705b55f..575526a 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -674,6 +674,7 @@ type Wallet struct { IsBettable bool `json:"is_bettable"` IsTransferable bool `json:"is_transferable"` UserID int64 `json:"user_id"` + Type string `json:"type"` IsActive bool `json:"is_active"` CreatedAt pgtype.Timestamp `json:"created_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"` diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index 4b94209..1cb7387 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -46,17 +46,19 @@ INSERT INTO wallets ( is_withdraw, is_bettable, is_transferable, - user_id + user_id, + type ) -VALUES ($1, $2, $3, $4) -RETURNING id, balance, is_withdraw, is_bettable, is_transferable, user_id, is_active, created_at, updated_at, currency, bonus_balance, cash_balance +VALUES ($1, $2, $3, $4, $5) +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 { - IsWithdraw bool `json:"is_withdraw"` - IsBettable bool `json:"is_bettable"` - IsTransferable bool `json:"is_transferable"` - UserID int64 `json:"user_id"` + IsWithdraw bool `json:"is_withdraw"` + IsBettable bool `json:"is_bettable"` + IsTransferable bool `json:"is_transferable"` + UserID int64 `json:"user_id"` + Type string `json:"type"` } 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.IsTransferable, arg.UserID, + arg.Type, ) var i Wallet err := row.Scan( @@ -74,6 +77,7 @@ func (q *Queries) CreateWallet(ctx context.Context, arg CreateWalletParams) (Wal &i.IsBettable, &i.IsTransferable, &i.UserID, + &i.Type, &i.IsActive, &i.CreatedAt, &i.UpdatedAt, @@ -184,7 +188,7 @@ func (q *Queries) GetAllCustomerWallet(ctx context.Context) ([]CustomerWalletDet } 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 ` @@ -204,6 +208,7 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) { &i.IsBettable, &i.IsTransferable, &i.UserID, + &i.Type, &i.IsActive, &i.CreatedAt, &i.UpdatedAt, @@ -314,7 +319,7 @@ func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (Cust } 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 WHERE id = $1 ` @@ -329,6 +334,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { &i.IsBettable, &i.IsTransferable, &i.UserID, + &i.Type, &i.IsActive, &i.CreatedAt, &i.UpdatedAt, @@ -340,7 +346,7 @@ func (q *Queries) GetWalletByID(ctx context.Context, id int64) (Wallet, error) { } 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 WHERE user_id = $1 ` @@ -361,6 +367,7 @@ func (q *Queries) GetWalletByUserID(ctx context.Context, userID int64) ([]Wallet &i.IsBettable, &i.IsTransferable, &i.UserID, + &i.Type, &i.IsActive, &i.CreatedAt, &i.UpdatedAt, diff --git a/internal/domain/bet.go b/internal/domain/bet.go index 83c537e..bc6aae0 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -104,7 +104,7 @@ type CreateBetOutcomeReq struct { type CreateBetReq struct { Outcomes []CreateBetOutcomeReq `json:"outcomes" validate:"required"` 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 { diff --git a/internal/domain/company.go b/internal/domain/company.go index b21e519..406f0fe 100644 --- a/internal/domain/company.go +++ b/internal/domain/company.go @@ -54,8 +54,9 @@ type UpdateCompany struct { } type CreateCompanyReq struct { - Name string `json:"name" example:"CompanyName"` - AdminID int64 `json:"admin_id" example:"1"` + Name string `json:"name" example:"CompanyName"` + AdminID int64 `json:"admin_id" example:"1"` + DeductedPercentage float32 `json:"deducted_percentage" example:"0.1" validate:"lt=1"` } type UpdateCompanyReq struct { Name *string `json:"name,omitempty" example:"CompanyName"` diff --git a/internal/domain/otp.go b/internal/domain/otp.go index 23c8640..8eb4106 100644 --- a/internal/domain/otp.go +++ b/internal/domain/otp.go @@ -30,7 +30,7 @@ type OtpProvider string const ( TwilioSms OtpProvider = "twilio" - AfroMessage OtpProvider = "aformessage" + AfroMessage OtpProvider = "afro_message" ) type Otp struct { diff --git a/internal/domain/wallet.go b/internal/domain/wallet.go index 7fe8f73..6ae6a1f 100644 --- a/internal/domain/wallet.go +++ b/internal/domain/wallet.go @@ -11,6 +11,7 @@ type Wallet struct { IsTransferable bool IsActive bool UserID int64 + Type WalletType UpdatedAt time.Time CreatedAt time.Time } @@ -63,6 +64,7 @@ type CreateWallet struct { IsBettable bool IsTransferable bool UserID int64 + Type WalletType } type CreateCustomerWallet struct { diff --git a/internal/repository/wallet.go b/internal/repository/wallet.go index 4a6ae45..4aa764e 100644 --- a/internal/repository/wallet.go +++ b/internal/repository/wallet.go @@ -17,6 +17,7 @@ func convertDBWallet(wallet dbgen.Wallet) domain.Wallet { IsTransferable: wallet.IsTransferable, IsActive: wallet.IsActive, UserID: wallet.UserID, + Type: domain.WalletType(wallet.Type), UpdatedAt: wallet.UpdatedAt.Time, CreatedAt: wallet.CreatedAt.Time, } @@ -28,6 +29,7 @@ func convertCreateWallet(wallet domain.CreateWallet) dbgen.CreateWalletParams { IsBettable: wallet.IsBettable, IsTransferable: wallet.IsTransferable, UserID: wallet.UserID, + Type: string(wallet.Type), } } @@ -275,4 +277,3 @@ func (s *Store) GetTotalWallets(ctx context.Context, filter domain.ReportFilter) return total, nil } - diff --git a/internal/services/user/common.go b/internal/services/user/common.go index 0094210..683f39f 100644 --- a/internal/services/user/common.go +++ b/internal/services/user/common.go @@ -23,11 +23,11 @@ func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpF switch medium { case domain.OtpMediumSms: switch provider { - case "twilio": + case domain.TwilioSms: if err := s.SendTwilioSMSOTP(ctx, sentTo, message, provider); err != nil { return err } - case "afromessage": + case domain.AfroMessage: if err := s.SendAfroMessageSMSOTP(ctx, sentTo, message, provider); err != nil { return err } @@ -107,7 +107,7 @@ func (s *Service) SendTwilioSMSOTP(ctx context.Context, receiverPhone, message s _, err := client.Api.CreateMessage(params) if err != nil { - return fmt.Errorf("%s", "Error sending SMS message: %s" + err.Error()) + return fmt.Errorf("%s", "Error sending SMS message: %s"+err.Error()) } return nil diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go index 9973a69..66f8bad 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -126,11 +126,29 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain. if wallet.Balance < amount { // Send Wallet low to admin if walletType == domain.CompanyWalletType || walletType == domain.BranchWalletType { - s.SendAdminWalletLowNotification(ctx, wallet, amount) + s.SendAdminWalletInsufficientNotification(ctx, wallet, amount) } 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) 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) } -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 adminNotification := &domain.Notification{ RecipientID: adminWallet.UserID, Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT, - Level: domain.NotificationLevelError, + Level: domain.NotificationLevelWarning, Reciever: domain.NotificationRecieverSideAdmin, DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel Payload: domain.NotificationPayload{ Headline: "CREDIT WARNING: System Running Out of Funds", 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.Balance.Float32(), - amount.Float32(), ), }, Priority: 1, // High priority for admin alerts Metadata: fmt.Appendf(nil, `{ "wallet_id": %d, "balance": %d, - "required_amount": %d, "notification_type": "admin_alert" - }`, adminWallet.ID, adminWallet.Balance, amount), + }`, adminWallet.ID, adminWallet.Balance), } // Get admin recipients and send to all @@ -240,3 +256,85 @@ func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWalle } 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 +} diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 8d45a17..56565af 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -24,22 +24,22 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S spec string task func() }{ - { - spec: "0 0 * * * *", // Every 1 hour - task: func() { - if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - log.Printf("FetchUpcomingEvents error: %v", err) - } - }, - }, - { - spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events) - task: func() { - if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - log.Printf("FetchNonLiveOdds error: %v", err) - } - }, - }, + // { + // spec: "0 0 * * * *", // Every 1 hour + // task: func() { + // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + // log.Printf("FetchUpcomingEvents error: %v", err) + // } + // }, + // }, + // { + // spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events) + // task: func() { + // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + // log.Printf("FetchNonLiveOdds error: %v", err) + // } + // }, + // }, { spec: "0 */5 * * * *", // Every 5 Minutes task: func() { @@ -114,10 +114,10 @@ func SetupReportCronJobs(ctx context.Context, reportService *report.Service) { spec string period string }{ - { - spec: "*/300 * * * * *", // Every 5 minutes (300 seconds) - period: "5min", - }, + // { + // spec: "*/300 * * * * *", // Every 5 minutes (300 seconds) + // period: "5min", + // }, { spec: "0 0 0 * * *", // Daily at midnight period: "daily", diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index 46ef873..6d92b98 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -15,7 +15,7 @@ import ( // loginCustomerReq represents the request body for the LoginCustomer endpoint. 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"` Password string `json:"password" validate:"required" example:"password123"` } diff --git a/internal/web_server/handlers/report.go b/internal/web_server/handlers/report.go index e15faae..a34b8e5 100644 --- a/internal/web_server/handlers/report.go +++ b/internal/web_server/handlers/report.go @@ -73,21 +73,28 @@ func (h *Handler) GetDashboardReport(c *fiber.Ctx) error { func parseReportFilter(c *fiber.Ctx) (domain.ReportFilter, error) { var filter domain.ReportFilter 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) if err != nil { return domain.ReportFilter{}, fmt.Errorf("invalid company_id: %w", err) } 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) if err != nil { return domain.ReportFilter{}, fmt.Errorf("invalid branch_id: %w", err) } filter.BranchID = domain.ValidInt64{Value: branchID, Valid: true} + } else { + filter.BranchID = c.Locals("branch_id").(domain.ValidInt64) } if c.Query("user_id") != "" { diff --git a/internal/web_server/handlers/shop_handler.go b/internal/web_server/handlers/shop_handler.go index a81de8f..f63341f 100644 --- a/internal/web_server/handlers/shop_handler.go +++ b/internal/web_server/handlers/shop_handler.go @@ -116,6 +116,89 @@ func (h *Handler) GetShopBetByBetID(c *fiber.Ctx) error { 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 // @Summary Cashout bet at branch // @Description Cashout bet at branch diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index 381078b..8779de9 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -248,7 +248,7 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { // TODO: Remove later _, err = h.walletSvc.AddToWallet( 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 { h.mongoLoggerSvc.Error("Failed to update wallet for user", @@ -417,20 +417,121 @@ type UserProfileRes struct { LastLogin time.Time `json:"last_login"` SuspendedAt time.Time `json:"suspended_at"` 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 // @Description Get user profile // @Tags user // @Accept json // @Produce json -// @Success 200 {object} UserProfileRes +// @Success 200 {object} CustomerProfileRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Security Bearer -// @Router /api/v1/user/profile [get] -func (h *Handler) UserProfile(c *fiber.Ctx) error { +// @Router /api/v1/user/customer-profile [get] +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) if !ok || userID == 0 { diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 232490b..acb6a45 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -105,7 +105,8 @@ func (a *App) initAppRoutes() { groupV1.Post("/user/register", h.RegisterUser) groupV1.Post("/user/sendRegisterCode", h.SendRegisterCode) 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.Delete("/user/delete/:id", a.authMiddleware, h.DeleteUser) 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.Get("/search/branch", a.authMiddleware, h.SearchBranch) + groupV1.Get("/branchLocation", a.authMiddleware, h.GetAllBranchLocations) groupV1.Get("/branch/:id/cashiers", a.authMiddleware, h.GetBranchCashiers) @@ -287,6 +289,7 @@ func (a *App) initAppRoutes() { // Transactions /shop/transactions 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.Post("/shop/bet/:id/cashout", a.authMiddleware, a.CompanyOnly, h.CashoutBet) groupV1.Post("/shop/bet/:id/generate", a.authMiddleware, a.CompanyOnly, h.CashoutBet)