Yimaru-BackEnd/internal/web_server/handlers/wallet_handler.go
Samuel Tariku 485cba3c9c feat: Add new stat stores and reporting functionalities for bets, branches, and wallets
- Introduced BetStatStore, BranchStatStore, and WalletStatStore interfaces for handling statistics.
- Implemented repository methods for fetching and updating bet, branch, and wallet statistics.
- Created reporting services for generating interval reports for bets, branches, companies, and wallets.
- Enhanced CSV writing functionality to support dynamic struct to CSV conversion.
- Added cron jobs for periodic updates of branch and wallet statistics.
- Updated wallet handler to include transaction statistics in the response.
2025-10-29 07:14:38 +03:00

474 lines
16 KiB
Go

package handlers
import (
"fmt"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
type WalletRes struct {
ID int64 `json:"id" example:"1"`
Balance float32 `json:"amount" example:"100.0"`
IsWithdraw bool `json:"is_withdraw" example:"true"`
IsBettable bool `json:"is_bettable" example:"true"`
IsTransferable bool `json:"is_transferable" example:"true"`
IsActive bool `json:"is_active" example:"true"`
UserID int64 `json:"user_id" example:"1"`
UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
}
func convertWallet(wallet domain.Wallet) WalletRes {
return WalletRes{
ID: wallet.ID,
Balance: wallet.Balance.Float32(),
IsWithdraw: wallet.IsWithdraw,
IsBettable: wallet.IsBettable,
IsTransferable: wallet.IsTransferable,
IsActive: wallet.IsActive,
UserID: wallet.UserID,
UpdatedAt: wallet.UpdatedAt,
CreatedAt: wallet.CreatedAt,
}
}
type CustomerWalletRes struct {
ID int64 `json:"id" example:"1"`
RegularID int64 `json:"regular_id" example:"1"`
RegularBalance float32 `json:"regular_balance" example:"100.0"`
StaticID int64 `json:"static_id" example:"1"`
StaticBalance float32 `json:"static_balance" example:"100.0"`
CustomerID int64 `json:"customer_id" example:"1"`
RegularIsActive bool `json:"regular_is_active" example:"true"`
StaticIsActive bool `json:"static_is_active" example:"true"`
RegularUpdatedAt time.Time `json:"regular_updated_at"`
StaticUpdatedAt time.Time `json:"static_updated_at"`
CreatedAt time.Time `json:"created_at"`
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Smith"`
PhoneNumber string `json:"phone_number" example:"0911111111"`
NumberOfTransactions int64 `json:"number_of_transactions"`
TotalTransactions float32 `json:"total_transactions"`
NumberOfDeposits int64 `json:"number_of_deposits"`
TotalDepositsAmount float32 `json:"total_deposits_amount"`
NumberOfWithdraws int64 `json:"number_of_withdraws"`
TotalWithdrawsAmount float32 `json:"total_withdraws_amount"`
NumberOfTransfers int64 `json:"number_of_transfers"`
TotalTransfersAmount float32 `json:"total_transfers_amount"`
UpdatedAt time.Time `json:"updated_at"`
}
func ConvertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes {
return CustomerWalletRes{
ID: wallet.ID,
RegularID: wallet.RegularID,
RegularBalance: wallet.RegularBalance.Float32(),
StaticID: wallet.StaticID,
StaticBalance: wallet.StaticBalance.Float32(),
CustomerID: wallet.CustomerID,
RegularIsActive: wallet.RegularIsActive,
StaticIsActive: wallet.StaticIsActive,
RegularUpdatedAt: wallet.RegularUpdatedAt,
StaticUpdatedAt: wallet.StaticUpdatedAt,
CreatedAt: wallet.CreatedAt,
FirstName: wallet.FirstName,
LastName: wallet.LastName,
PhoneNumber: wallet.PhoneNumber,
NumberOfTransactions: wallet.NumberOfTransactions,
TotalTransactions: wallet.TotalTransactions.Float32(),
NumberOfDeposits: wallet.NumberOfDeposits,
TotalDepositsAmount: wallet.TotalDepositsAmount.Float32(),
NumberOfWithdraws: wallet.NumberOfWithdraws,
TotalWithdrawsAmount: wallet.TotalWithdrawsAmount.Float32(),
NumberOfTransfers: wallet.NumberOfTransfers,
TotalTransfersAmount: wallet.TotalTransfersAmount.Float32(),
UpdatedAt: wallet.UpdatedAt,
}
}
type BranchWalletRes struct {
ID int64 `json:"id" example:"1"`
Balance float32 `json:"balance" example:"100.0"`
IsActive bool `json:"is_active" example:"true"`
Name string `json:"name" example:"true"`
Location string `json:"location" example:"somewhere"`
BranchManagerID int64 `json:"branch_manager_id" example:"1"`
CompanyID int64 `json:"company_id" example:"1"`
IsSelfOwned bool `json:"is_self_owned" example:"false"`
UpdatedAt time.Time `json:"updated_at"`
CreatedAt time.Time `json:"created_at"`
}
// GetWalletByID godoc
// @Summary Get wallet by ID
// @Description Retrieve wallet details by wallet ID
// @Tags wallet
// @Accept json
// @Produce json
// @Param id path int true "Wallet ID"
// @Success 200 {object} WalletRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/wallet/{id} [get]
func (h *Handler) GetWalletByID(c *fiber.Ctx) error {
walletID := c.Params("id")
id, err := strconv.ParseInt(walletID, 10, 64)
if err != nil {
h.mongoLoggerSvc.Error("Invalid wallet ID",
zap.String("walletID", walletID),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid wallet ID")
}
wallet, err := h.walletSvc.GetWalletByID(c.Context(), id)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get wallet by ID",
zap.Int64("walletID", id),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve wallet")
}
res := convertWallet(wallet)
return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil)
}
// GetAllWallets godoc
// @Summary Get all wallets
// @Description Retrieve all wallets
// @Tags wallet
// @Accept json
// @Produce json
// @Success 200 {array} WalletRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/wallet [get]
func (h *Handler) GetAllWallets(c *fiber.Ctx) error {
wallets, err := h.walletSvc.GetAllWallets(c.Context())
if err != nil {
h.mongoLoggerSvc.Error("Failed to get wallets",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
var res []WalletRes = make([]WalletRes, 0, len(wallets))
for _, wallet := range wallets {
res = append(res, convertWallet(wallet))
}
return response.WriteJSON(c, fiber.StatusOK, "All wallets retrieved successfully", res, nil)
}
// GetAllBranchWallets godoc
// @Summary Get all branch wallets
// @Description Retrieve all branch wallets
// @Tags wallet
// @Accept json
// @Produce json
// @Success 200 {array} WalletRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/branchWallet [get]
func (h *Handler) GetAllBranchWallets(c *fiber.Ctx) error {
wallets, err := h.walletSvc.GetAllBranchWallets(c.Context())
if err != nil {
h.mongoLoggerSvc.Error("Failed to get wallets",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve wallets")
}
var res []BranchWalletRes = make([]BranchWalletRes, 0, len(wallets))
for _, wallet := range wallets {
res = append(res, BranchWalletRes{
ID: wallet.ID,
Balance: wallet.Balance.Float32(),
IsActive: wallet.IsActive,
Name: wallet.Name,
Location: wallet.Location,
BranchManagerID: wallet.BranchManagerID,
CompanyID: wallet.CompanyID,
IsSelfOwned: wallet.IsSelfOwned,
UpdatedAt: wallet.UpdatedAt,
CreatedAt: wallet.CreatedAt,
})
}
return response.WriteJSON(c, fiber.StatusOK, "All Wallets retrieved", res, nil)
}
// GetAllCustomerWallets godoc
// @Summary Get all customer wallets
// @Description Retrieve all customer wallets
// @Tags wallet
// @Accept json
// @Produce json
// @Success 200 {array} CustomerWalletRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/customerWallet [get]
func (h *Handler) GetAllCustomerWallets(c *fiber.Ctx) error {
wallets, err := h.walletSvc.GetAllCustomerWallet(c.Context())
if err != nil {
h.mongoLoggerSvc.Error("Failed to get customer wallets",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve wallets")
}
var res []CustomerWalletRes = make([]CustomerWalletRes, 0, len(wallets))
for _, wallet := range wallets {
res = append(res, ConvertCustomerWallet(wallet))
}
return response.WriteJSON(c, fiber.StatusOK, "All Wallets retrieved", res, nil)
}
type UpdateWalletActiveReq struct {
IsActive bool `json:"is_active" validate:"required" example:"true"`
}
// UpdateWalletActive godoc
// @Summary Activate and Deactivate Wallet
// @Description Can activate and deactivate wallet
// @Tags wallet
// @Accept json
// @Produce json
// @Param id path int true "Wallet ID"
// @Param updateCashOut body UpdateWalletActiveReq true "Update Wallet Active"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/wallet/{id} [patch]
func (h *Handler) UpdateWalletActive(c *fiber.Ctx) error {
walletID := c.Params("id")
id, err := strconv.ParseInt(walletID, 10, 64)
if err != nil {
h.mongoLoggerSvc.Info("Invalid wallet ID",
zap.String("walletID", walletID),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid wallet ID")
}
var req UpdateWalletActiveReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse UpdateWalletActive request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
h.mongoLoggerSvc.Info("Failed to validate UpdateWalletActiveReq",
zap.String("errMsg", errMsg),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
err = h.walletSvc.UpdateWalletActive(c.Context(), id, req.IsActive)
if err != nil {
h.mongoLoggerSvc.Error("Failed to update wallet active status",
zap.Int64("walletID", id),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update wallet")
}
return response.WriteJSON(c, fiber.StatusOK, "Wallet successfully updated", nil, nil)
}
// GetCustomerWallet godoc
// @Summary Get customer wallet
// @Description Retrieve customer wallet details
// @Tags wallet
// @Accept json
// @Produce json
// @Param company_id header int true "Company ID"
// @Security Bearer
// @Success 200 {object} CustomerWalletRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/user/wallet [get]
func (h *Handler) GetCustomerWallet(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(int64)
if !ok || userID == 0 {
h.mongoLoggerSvc.Info("Invalid user ID in context",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user id in context")
}
role, ok := c.Locals("role").(domain.Role)
if !ok {
h.mongoLoggerSvc.Error("Invalid role in context",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.String("role", string(role)),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, "Invalid role")
}
if role != domain.RoleCustomer {
h.mongoLoggerSvc.Error("Unauthorized access",
zap.Int64("userID", userID),
zap.Int("status_code", fiber.StatusForbidden),
zap.String("role", string(role)),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusForbidden, "Unauthorized access")
}
// companyID, err := strconv.ParseInt(c.Get("company_id"), 10, 64)
// if err != nil {
// h.logger.Error("Invalid company_id header", "value", c.Get("company_id"), "error", err)
// return fiber.NewError(fiber.StatusBadRequest, "Invalid company_id")
// }
// h.logger.Info("Fetching customer wallet", "userID", userID)
wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), userID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get customer wallet",
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 wallet")
}
res := ConvertCustomerWallet(wallet)
return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil)
}
// GetWalletForCashier godoc
// @Summary Get wallet for cashier
// @Description Get wallet for cashier
// @Tags cashier
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} UserProfileRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/cashierWallet [get]
func (h *Handler) GetWalletForCashier(c *fiber.Ctx) error {
cashierID, ok := c.Locals("user_id").(int64)
if !ok || cashierID == 0 {
h.mongoLoggerSvc.Error("Invalid cashier ID in context",
zap.Int64("cashierID", cashierID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusUnauthorized, "Invalid cashier id")
}
role, ok := c.Locals("role").(domain.Role)
if !ok || role != domain.RoleCashier {
h.mongoLoggerSvc.Error("Unauthorized access",
zap.Int64("cashierID", cashierID),
zap.Int("status_code", fiber.StatusForbidden),
zap.String("role", string(role)),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusForbidden, "Unauthorized access")
}
branchID, ok := c.Locals("branch_id").(domain.ValidInt64)
if !ok || !branchID.Valid {
h.mongoLoggerSvc.Info("Invalid branch ID in context",
zap.Int64("cashierID", cashierID),
zap.Int("status_code", fiber.StatusBadRequest),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid branch ID")
}
branch, err := h.branchSvc.GetBranchByID(c.Context(), branchID.Value)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get branch by ID",
zap.Int64("branchID", branchID.Value),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
wallet, err := h.walletSvc.GetWalletByID(c.Context(), branch.WalletID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get wallet for cashier",
zap.Int64("cashierID", cashierID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
res := WalletRes{
ID: wallet.ID,
Balance: wallet.Balance.Float32(),
IsWithdraw: wallet.IsWithdraw,
IsBettable: wallet.IsBettable,
IsTransferable: wallet.IsTransferable,
IsActive: wallet.IsActive,
UserID: wallet.UserID,
UpdatedAt: wallet.UpdatedAt,
CreatedAt: wallet.CreatedAt,
}
return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil)
}