package handlers import ( "fmt" "log/slog" "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 ShopTransactionRes struct { ID int64 `json:"id" example:"1"` Amount float32 `json:"amount" example:"100.0"` BranchID int64 `json:"branch_id" example:"1"` BranchName string `json:"branch_name" example:"Branch Name"` BranchLocation string `json:"branch_location" example:"Branch Location"` CompanyID int64 `json:"company_id" example:"1"` CashierID int64 `json:"cashier_id" example:"1"` CashierName string `json:"cashier_name" example:"John Smith"` BetID int64 `json:"bet_id" example:"1"` NumberOfOutcomes int64 `json:"number_of_outcomes" example:"1"` Type int64 `json:"type" example:"1"` PaymentOption domain.PaymentOption `json:"payment_option" example:"1"` FullName string `json:"full_name" example:"John Smith"` PhoneNumber string `json:"phone_number" example:"0911111111"` BankCode string `json:"bank_code"` BeneficiaryName string `json:"beneficiary_name"` AccountName string `json:"account_name"` AccountNumber string `json:"account_number"` ReferenceNumber string `json:"reference_number"` Verified bool `json:"verified" example:"true"` ApprovedBy *int64 `json:"approved_by" example:"1"` ApproverName *string `json:"approver_name" example:"John Smith"` UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"` } type CashoutReq struct { CashoutID string `json:"cashout_id" example:"191212"` Amount float32 `json:"amount" example:"100.0"` BetID int64 `json:"bet_id" example:"1"` Type int64 `json:"type" example:"1"` PaymentOption domain.PaymentOption `json:"payment_option" example:"1"` FullName string `json:"full_name" example:"John Smith"` PhoneNumber string `json:"phone_number" example:"0911111111"` BankCode string `json:"bank_code"` BeneficiaryName string `json:"beneficiary_name"` AccountName string `json:"account_name"` AccountNumber string `json:"account_number"` ReferenceNumber string `json:"reference_number"` BranchID *int64 `json:"branch_id,omitempty" example:"1"` } func convertShopTransaction(transaction domain.ShopTransaction) ShopTransactionRes { newTransaction := ShopTransactionRes{ ID: transaction.ID, Amount: transaction.Amount.Float32(), BranchID: transaction.BranchID, BranchName: transaction.BranchName, BranchLocation: transaction.BranchLocation, CompanyID: transaction.CompanyID, CashierID: transaction.CashierID, CashierName: transaction.CashierName, BetID: transaction.BetID, Type: int64(transaction.Type), PaymentOption: transaction.PaymentOption, FullName: transaction.FullName, PhoneNumber: transaction.PhoneNumber, BankCode: transaction.BankCode, BeneficiaryName: transaction.BeneficiaryName, AccountName: transaction.AccountName, AccountNumber: transaction.AccountNumber, ReferenceNumber: transaction.ReferenceNumber, Verified: transaction.Verified, NumberOfOutcomes: transaction.NumberOfOutcomes, CreatedAt: transaction.CreatedAt, UpdatedAt: transaction.UpdatedAt, } if transaction.ApprovedBy.Valid { newTransaction.ApprovedBy = &transaction.ApprovedBy.Value newTransaction.ApproverName = &transaction.ApproverName.Value } return newTransaction } // CashoutBet godoc // @Summary Cashout bet at branch // @Description Cashout bet at branch // @Tags transaction // @Accept json // @Produce json // @Param createBet body CashoutReq true "cashout bet" // @Success 200 {object} TransactionRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /shop/cashout [post] func (h *Handler) CashoutBet(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) // user, err := h.userSvc.GetUserByID(c.Context(), userID) // TODO: Make a "Only Company" middleware auth and move this into that if role == domain.RoleCustomer { h.logger.Error("CashoutReq failed due to unauthorized access") return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "unauthorized access", }) } var req CashoutReq if err := c.BodyParser(&req); err != nil { h.logger.Error("CashoutReq failed to parse request", "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := h.validator.Validate(c, req) if !ok { h.logger.Error("CashoutReq failed v", "error", valErrs) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } var branchID int64 var branchName string var branchLocation string var companyID int64 if role == domain.RoleAdmin || role == domain.RoleBranchManager || role == domain.RoleSuperAdmin { if req.BranchID == nil { h.logger.Error("CashoutReq Branch ID is required for this user role") return response.WriteJSON(c, fiber.StatusBadRequest, "Branch ID is required for this user role", nil, nil) } branch, err := h.branchSvc.GetBranchByID(c.Context(), *req.BranchID) if err != nil { h.logger.Error("CashoutReq no branches") return response.WriteJSON(c, fiber.StatusBadRequest, "cannot find Branch ID", err, nil) } branchID = branch.ID branchName = branch.Name branchLocation = branch.Location companyID = branch.CompanyID } else { branch, err := h.branchSvc.GetBranchByCashier(c.Context(), userID) if err != nil { h.logger.Error("CashoutReq failed, branch id invalid") return response.WriteJSON(c, fiber.StatusBadRequest, "Branch ID invalid", err, nil) } branchID = branch.ID branchName = branch.Name branchLocation = branch.Location companyID = branch.CompanyID } bet, err := h.betSvc.GetBetByID(c.Context(), req.BetID) if err != nil { h.logger.Error("CashoutReq failed", "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Bet ID invalid", err, nil) } // if bet.Status != domain.OUTCOME_STATUS_WIN { // h.logger.Error("CashoutReq failed, bet has not won") // return response.WriteJSON(c, fiber.StatusBadRequest, "User has not won bet", err, nil) // } if bet.CashedOut { h.logger.Error(("Bet has already been cashed out")) return response.WriteJSON(c, fiber.StatusBadRequest, "This bet has already been cashed out", err, nil) } user, err := h.userSvc.GetUserByID(c.Context(), userID) if err != nil { h.logger.Error("CashoutReq failed, user id invalid", "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "User ID invalid", err, nil) } transaction, err := h.transactionSvc.CreateShopTransaction(c.Context(), domain.CreateShopTransaction{ BranchID: branchID, CashierID: userID, Amount: domain.ToCurrency(req.Amount), BetID: bet.ID, NumberOfOutcomes: int64(len(bet.Outcomes)), Type: domain.ShopTransactionType(req.Type), PaymentOption: domain.PaymentOption(req.PaymentOption), FullName: req.FullName, PhoneNumber: req.PhoneNumber, BankCode: req.BankCode, BeneficiaryName: req.BeneficiaryName, AccountName: req.AccountName, AccountNumber: req.AccountNumber, ReferenceNumber: req.ReferenceNumber, CashierName: user.FirstName + " " + user.LastName, BranchName: branchName, BranchLocation: branchLocation, CompanyID: companyID, }) if err != nil { h.logger.Error("CashoutReq failed", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) } err = h.betSvc.UpdateCashOut(c.Context(), req.BetID, true) if err != nil { h.logger.Error("CashoutReq failed", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) } res := convertShopTransaction(transaction) return response.WriteJSON(c, fiber.StatusOK, "Transaction created successfully", res, nil) } type DepositForCustomerReq struct { Amount float32 `json:"amount" example:"100.0"` PaymentMethod string `json:"payment_method" example:"cash"` BankCode string `json:"bank_code"` BeneficiaryName string `json:"beneficiary_name"` AccountName string `json:"account_name"` AccountNumber string `json:"account_number"` ReferenceNumber string `json:"reference_number"` BranchID *int64 `json:"branch_id,omitempty" example:"1"` } // DepositForCustomer godoc // @Summary Shop deposit into customer wallet // @Description Transfers money from branch wallet to customer wallet // @Tags transaction // @Accept json // @Produce json // @Param transferToWallet body DepositForCustomerReq true "DepositForCustomer" // @Success 200 {object} TransferWalletRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /shop/deposit/:id [post] func (h *Handler) DepositForCustomer(c *fiber.Ctx) error { customerIDString := c.Params("id") customerID, err := strconv.ParseInt(customerIDString, 10, 64) if err != nil { h.logger.Error("Invalid customer ID", "customerID", customerID, "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid wallet ID", err, nil) } // Get sender ID from the cashier userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) var req DepositForCustomerReq if err := c.BodyParser(&req); err != nil { h.logger.Error("CreateTransferReq failed", "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } valErrs, ok := h.validator.Validate(c, req) if !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } var senderID int64 switch role { case domain.RoleCustomer: h.logger.Error("Unauthorized access", "userID", userID, "role", role) return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) case domain.RoleAdmin, domain.RoleSuperAdmin, domain.RoleBranchManager: if req.BranchID == nil { h.logger.Error("CashoutReq Branch ID is required for this user role") return response.WriteJSON(c, fiber.StatusBadRequest, "Branch ID is required for this user role", nil, nil) } branch, err := h.branchSvc.GetBranchByID(c.Context(), *req.BranchID) if err != nil { h.logger.Error("CashoutReq no branches") return response.WriteJSON(c, fiber.StatusBadRequest, "cannot find Branch ID", err, nil) } senderID = branch.WalletID case domain.RoleCashier: cashierBranch, err := h.branchSvc.GetBranchByCashier(c.Context(), userID) if err != nil { h.logger.Error("Failed to get branch", "user ID", userID, "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve cashier branch", err, nil) } senderID = cashierBranch.WalletID default: return response.WriteJSON(c, fiber.StatusInternalServerError, "Unknown Role", err, nil) } customerWallet, err := h.walletSvc.GetCustomerWallet(c.Context(), customerID) if err != nil { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid customer id", err, nil) } transfer, err := h.walletSvc.TransferToWallet(c.Context(), senderID, customerWallet.RegularID, domain.ToCurrency(req.Amount), domain.PaymentMethod(req.PaymentMethod), domain.ValidInt64{Value: userID, Valid: true}, fmt.Sprintf("Transferred %v from wallet to customer wallet", req.Amount), ) if err != nil { h.mongoLoggerSvc.Error("Failed to transfer money to wallet", zap.Error(err)) return response.WriteJSON(c, fiber.StatusInternalServerError, "Transfer Failed", err, nil) } transaction, err := h.transactionSvc.CreateShopTransaction(c.Context(), domain.CreateShopTransaction{ BranchID: branchID, CashierID: userID, Amount: domain.ToCurrency(req.Amount), Type: domain.TRANSACTION_DEPOSIT, PaymentOption: domain.PaymentOption(req.PaymentOption), FullName: req.FullName, PhoneNumber: req.PhoneNumber, BankCode: req.BankCode, BeneficiaryName: req.BeneficiaryName, AccountName: req.AccountName, AccountNumber: req.AccountNumber, ReferenceNumber: req.ReferenceNumber, CashierName: user.FirstName + " " + user.LastName, BranchName: branchName, BranchLocation: branchLocation, CompanyID: companyID, }) res := convertTransfer(transfer) return response.WriteJSON(c, fiber.StatusOK, "Transfer Successful", res, nil) } // GetAllTransactions godoc // @Summary Gets all transactions // @Description Gets all the transactions // @Tags transaction // @Accept json // @Produce json // @Success 200 {array} TransactionRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /shop/transaction [get] func (h *Handler) GetAllTransactions(c *fiber.Ctx) error { // Get user_id from middleware // userID := c.Locals("user_id").(int64) // 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.logger.Error("invalid start_time format", "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) } 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.logger.Error("invalid start_time format", "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) } createdAfter = domain.ValidTime{ Value: createdAfterParsed, Valid: true, } } // Check user role and fetch transactions accordingly transactions, err := h.transactionSvc.GetAllShopTransactions(c.Context(), domain.ShopTransactionFilter{ CompanyID: companyID, BranchID: branchID, Query: searchString, CreatedBefore: createdBefore, CreatedAfter: createdAfter, }) if err != nil { h.logger.Error("Failed to get transactions", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve transactions", err, nil) } res := make([]ShopTransactionRes, len(transactions)) for i, transaction := range transactions { res[i] = convertShopTransaction(transaction) } return response.WriteJSON(c, fiber.StatusOK, "Transactions retrieved successfully", res, nil) } // GetTransactionByID godoc // @Summary Gets transaction by id // @Description Gets a single transaction by id // @Tags transaction // @Accept json // @Produce json // @Param id path int true "Transaction ID" // @Success 200 {object} TransactionRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /shop/transaction/{id} [get] func (h *Handler) GetTransactionByID(c *fiber.Ctx) error { transactionID := c.Params("id") id, err := strconv.ParseInt(transactionID, 10, 64) if err != nil { h.logger.Error("Invalid transaction ID", "transactionID", transactionID, "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid transaction ID") } transaction, err := h.transactionSvc.GetShopTransactionByID(c.Context(), id) if err != nil { h.logger.Error("Failed to get transaction by ID", "transactionID", id, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve transaction") } res := convertShopTransaction(transaction) return response.WriteJSON(c, fiber.StatusOK, "Transaction retrieved successfully", res, nil) } type UpdateTransactionVerifiedReq struct { Verified bool `json:"verified" example:"true"` } // UpdateTransactionVerified godoc // @Summary Updates the verified field of a transaction // @Description Updates the verified status of a transaction // @Tags transaction // @Accept json // @Produce json // @Param id path int true "Transaction ID" // @Param updateVerified body UpdateTransactionVerifiedReq true "Updates Transaction Verification" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /shop/transaction/{id} [put] func (h *Handler) UpdateTransactionVerified(c *fiber.Ctx) error { transactionID := c.Params("id") userID := c.Locals("user_id").(int64) companyID := c.Locals("company_id").(domain.ValidInt64) role := c.Locals("role").(domain.Role) id, err := strconv.ParseInt(transactionID, 10, 64) if err != nil { h.logger.Error("Invalid transaction ID", "transactionID", transactionID, "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid transaction ID") } var req UpdateTransactionVerifiedReq if err := c.BodyParser(&req); err != nil { h.logger.Error("Failed to parse UpdateTransactionVerified request", "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } h.logger.Info("Update Transaction Verified", slog.Bool("verified", req.Verified)) if valErrs, ok := h.validator.Validate(c, req); !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } transaction, err := h.transactionSvc.GetShopTransactionByID(c.Context(), id) if role != domain.RoleSuperAdmin { if !companyID.Valid || companyID.Value != transaction.CompanyID { h.logger.Error("Failed to parse UpdateTransactionVerified request", "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } } user, err := h.userSvc.GetUserById(c.Context(), userID) if err != nil { h.logger.Error("Invalid user ID", "userID", userID, "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid user ID") } err = h.transactionSvc.UpdateShopTransactionVerified(c.Context(), id, req.Verified, userID, user.FirstName+" "+user.LastName) if err != nil { h.logger.Error("Failed to update transaction verification", "transactionID", id, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transaction verification") } return response.WriteJSON(c, fiber.StatusOK, "Transaction updated successfully", nil, nil) }