Yimaru-BackEnd/internal/services/wallet/wallet.go

473 lines
14 KiB
Go

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
}