package wallet import ( "context" "errors" "fmt" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/event" "go.uber.org/zap" ) var ( ErrBalanceInsufficient = errors.New("wallet balance is insufficient") ) func (s *Service) CreateWallet(ctx context.Context, wallet domain.CreateWallet) (domain.Wallet, error) { return s.walletStore.CreateWallet(ctx, wallet) } func (s *Service) CreateCustomerWallet(ctx context.Context, customerID int64) (domain.CustomerWallet, error) { regularWallet, err := s.CreateWallet(ctx, domain.CreateWallet{ IsWithdraw: true, IsBettable: true, IsTransferable: true, UserID: customerID, Type: domain.RegularWalletType, }) if err != nil { return domain.CustomerWallet{}, err } staticWallet, err := s.CreateWallet(ctx, domain.CreateWallet{ IsWithdraw: false, IsBettable: true, IsTransferable: true, UserID: customerID, Type: domain.StaticWalletType, }) if err != nil { return domain.CustomerWallet{}, err } return s.walletStore.CreateCustomerWallet(ctx, domain.CreateCustomerWallet{ CustomerID: customerID, RegularWalletID: regularWallet.ID, StaticWalletID: staticWallet.ID, }) } func (s *Service) GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error) { return s.walletStore.GetWalletByID(ctx, id) } func (s *Service) GetAllWallets(ctx context.Context) ([]domain.Wallet, error) { return s.walletStore.GetAllWallets(ctx) } func (s *Service) GetWalletsByUser(ctx context.Context, id int64) ([]domain.Wallet, error) { return s.walletStore.GetWalletsByUser(ctx, id) } func (s *Service) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) { return s.walletStore.GetCompanyByWalletID(ctx, walletID) } func (s *Service) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) { return s.walletStore.GetBranchByWalletID(ctx, walletID) } func (s *Service) GetAllCustomerWallet(ctx context.Context) ([]domain.GetCustomerWallet, error) { return s.walletStore.GetAllCustomerWallets(ctx) } func (s *Service) GetCustomerWallet(ctx context.Context, customerID int64) (domain.GetCustomerWallet, error) { return s.walletStore.GetCustomerWallet(ctx, customerID) } func (s *Service) GetAllBranchWallets(ctx context.Context) ([]domain.BranchWallet, error) { return s.walletStore.GetAllBranchWallets(ctx) } func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error { err := s.walletStore.UpdateBalance(ctx, id, balance) if err != nil { return err } wallet, err := s.GetWalletByID(ctx, id) if err != nil { return err } go func() { s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.UserID), event.WalletEvent{ EventType: event.WalletBalanceUpdated, WalletID: wallet.ID, UserID: wallet.UserID, Balance: balance, Trigger: "UpdateBalance", }) }() return nil } func (s *Service) AddToWallet( ctx context.Context, id int64, amount domain.Currency, cashierID domain.ValidInt64, paymentMethod domain.PaymentMethod, paymentDetails domain.PaymentDetails, message string) (domain.Transfer, error) { wallet, err := s.GetWalletByID(ctx, id) if err != nil { return domain.Transfer{}, err } err = s.walletStore.UpdateBalance(ctx, id, wallet.Balance+amount) if err != nil { return domain.Transfer{}, err } go func() { s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.ID), event.WalletEvent{ EventType: event.WalletBalanceUpdated, WalletID: wallet.ID, UserID: wallet.UserID, Balance: wallet.Balance + amount, Trigger: "AddToWallet", }) }() // Log the transfer here for reference newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ Message: message, Amount: amount, Verified: true, ReceiverWalletID: domain.ValidInt64{ Value: wallet.ID, Valid: true, }, CashierID: cashierID, PaymentMethod: paymentMethod, Type: domain.DEPOSIT, ReferenceNumber: paymentDetails.ReferenceNumber.Value, }) return newTransfer, err } func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.Currency, cashierID domain.ValidInt64, paymentMethod domain.PaymentMethod, message string) (domain.Transfer, error) { wallet, err := s.GetWalletByID(ctx, id) if err != nil { return domain.Transfer{}, err } if wallet.Balance < amount { // Send Wallet low to admin if wallet.Type == domain.CompanyWalletType || wallet.Type == domain.BranchWalletType { s.SendAdminWalletInsufficientNotification(ctx, wallet, amount) } else { s.SendCustomerWalletInsufficientNotification(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 { return domain.Transfer{}, nil } go func() { s.kafkaProducer.Publish(ctx, fmt.Sprint(wallet.ID), event.WalletEvent{ EventType: event.WalletBalanceUpdated, WalletID: wallet.ID, UserID: wallet.UserID, Balance: wallet.Balance - amount, Trigger: "DeductFromWallet", }) }() // Log the transfer here for reference newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ Message: message, Amount: amount, Verified: true, SenderWalletID: domain.ValidInt64{ Value: wallet.ID, Valid: true, }, CashierID: cashierID, PaymentMethod: paymentMethod, Type: domain.WITHDRAW, ReferenceNumber: "", }) return newTransfer, err } // Directly Refilling wallet without // func (s *Service) RefillWallet(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) { // receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID) // if err != nil { // return domain.Transfer{}, err // } // // Add to receiver // senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID) // if err != nil { // return domain.Transfer{}, err // } else if senderWallet.Balance < transfer.Amount { // return domain.Transfer{}, ErrInsufficientBalance // } // err = s.walletStore.UpdateBalance(ctx, receiverWallet.ID, receiverWallet.Balance+transfer.Amount) // if err != nil { // return domain.Transfer{}, err // } // // Log the transfer so that if there is a mistake, it can be reverted // newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ // CashierID: transfer.CashierID, // ReceiverWalletID: transfer.ReceiverWalletID, // Amount: transfer.Amount, // Type: domain.DEPOSIT, // PaymentMethod: transfer.PaymentMethod, // Verified: true, // }) // if err != nil { // return domain.Transfer{}, err // } // return newTransfer, nil // } func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive bool) error { return s.walletStore.UpdateWalletActive(ctx, id, isActive) } func (s *Service) GetAdminNotificationRecipients(ctx context.Context, walletID int64, walletType domain.WalletType) ([]int64, error) { var recipients []int64 switch walletType { case domain.BranchWalletType: branch, err := s.GetBranchByWalletID(ctx, walletID) if err != nil { return nil, err } recipients = append(recipients, branch.BranchManagerID) cashiers, err := s.userSvc.GetCashiersByBranch(ctx, branch.ID) if err != nil { return nil, err } for _, cashier := range cashiers { recipients = append(recipients, cashier.ID) } admin, err := s.userSvc.GetAdminByCompanyID(ctx, branch.CompanyID) if err != nil { return nil, err } recipients = append(recipients, admin.ID) case domain.CompanyWalletType: company, err := s.GetCompanyByWalletID(ctx, walletID) if err != nil { return nil, err } recipients = append(recipients, company.AdminID) default: return nil, fmt.Errorf("Invalid wallet type") } users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ Role: string(domain.RoleSuperAdmin), }) if err != nil { return nil, err } for _, user := range users { recipients = append(recipients, user.ID) } return recipients, nil } func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWallet domain.Wallet) error { // Send notification to admin team adminNotification := &domain.Notification{ ErrorSeverity: "low", IsRead: false, DeliveryStatus: domain.DeliveryStatusPending, RecipientID: adminWallet.UserID, Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT, Level: domain.NotificationLevelWarning, Reciever: domain.NotificationRecieverSideAdmin, DeliveryChannel: domain.DeliveryChannelInApp, // Or any preferred admin channel Payload: domain.NotificationPayload{ Headline: "CREDIT WARNING: System Running Out of Funds", Message: fmt.Sprintf( "Wallet ID %d is running low. Current balance: %.2f", adminWallet.ID, adminWallet.Balance.Float32(), ), }, Priority: 1, // High priority for admin alerts Metadata: fmt.Appendf(nil, `{ "wallet_id": %d, "balance": %d, "notification_type": "admin_alert" }`, adminWallet.ID, adminWallet.Balance), } // Get admin recipients and send to all adminRecipients, err := s.GetAdminNotificationRecipients(ctx, adminWallet.ID, adminWallet.Type) if err != nil { s.mongoLogger.Error("failed to get admin recipients", zap.Error(err), zap.Time("timestamp", time.Now()), ) return err } for _, adminID := range adminRecipients { adminNotification.RecipientID = adminID if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil { s.mongoLogger.Error("failed to send admin notification", zap.Int64("admin_id", adminID), zap.Error(err), zap.Time("timestamp", time.Now()), ) } adminNotification.DeliveryChannel = domain.DeliveryChannelEmail if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil { s.mongoLogger.Error("failed to send email admin notification", zap.Int64("admin_id", adminID), zap.Error(err), zap.Time("timestamp", time.Now()), ) return err } } return nil } func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, adminWallet domain.Wallet, amount domain.Currency) error { // Send notification to admin team adminNotification := &domain.Notification{ ErrorSeverity: domain.NotificationErrorSeverityLow, IsRead: false, DeliveryStatus: domain.DeliveryStatusPending, RecipientID: adminWallet.UserID, Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT, Level: domain.NotificationLevelError, Reciever: domain.NotificationRecieverSideAdmin, DeliveryChannel: domain.DeliveryChannelInApp, // 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 recipients, err := s.GetAdminNotificationRecipients(ctx, adminWallet.ID, adminWallet.Type) if err != nil { s.mongoLogger.Error("failed to get admin recipients", zap.Error(err), zap.Time("timestamp", time.Now()), ) return err } for _, adminID := range recipients { adminNotification.RecipientID = adminID if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil { s.mongoLogger.Error("failed to send admin notification", zap.Int64("admin_id", adminID), zap.Error(err), zap.Time("timestamp", time.Now()), ) } adminNotification.DeliveryChannel = domain.DeliveryChannelEmail if err := s.notificationSvc.SendNotification(ctx, adminNotification); err != nil { s.mongoLogger.Error("failed to send email admin notification", zap.Int64("admin_id", adminID), zap.Error(err), zap.Time("timestamp", time.Now()), ) return err } } return nil } func (s *Service) SendCustomerWalletInsufficientNotification(ctx context.Context, customerWallet domain.Wallet, amount domain.Currency) error { // Send notification to admin team customerNotification := &domain.Notification{ ErrorSeverity: domain.NotificationErrorSeverityLow, IsRead: false, DeliveryStatus: domain.DeliveryStatusPending, RecipientID: customerWallet.UserID, Type: domain.NOTIFICATION_TYPE_WALLET, Level: domain.NotificationLevelError, Reciever: domain.NotificationRecieverSideCustomer, DeliveryChannel: domain.DeliveryChannelInApp, // Or any preferred admin channel Payload: domain.NotificationPayload{ Headline: "CREDIT Error: Wallet insufficient", 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.notificationSvc.SendNotification(ctx, customerNotification); err != nil { s.mongoLogger.Error("failed to create customer notification", zap.Int64("customer_id", customerWallet.UserID), zap.Error(err), zap.Time("timestamp", time.Now()), ) return err } return nil }