package wallet import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/event" ) // InitiateDirectDeposit creates a pending deposit request func (s *Service) InitiateDirectDeposit( ctx context.Context, customerID int64, amount domain.Currency, bankRef string, // Mobile banking transaction reference senderAccount string, // Customer's account number ) (domain.DirectDeposit, error) { // Get customer's betting wallet customerWallet, err := s.GetCustomerWallet(ctx, customerID) if err != nil { return domain.DirectDeposit{}, fmt.Errorf("failed to get customer wallet: %w", err) } // Create pending deposit record deposit, err := s.directDepositStore.CreateDirectDeposit(ctx, domain.CreateDirectDeposit{ CustomerID: customerID, WalletID: customerWallet.ID, Amount: amount, BankReference: bankRef, SenderAccount: senderAccount, Status: domain.DepositStatusPending, }) if err != nil { return domain.DirectDeposit{}, fmt.Errorf("failed to create deposit record: %w", err) } // Notify cashiers for manual verification go s.notifyCashiersForVerification(ctx, deposit.ID, customerID, amount) return deposit, nil } // VerifyDirectDeposit verifies and processes the deposit func (s *Service) VerifyDirectDeposit( ctx context.Context, depositID int64, cashierID int64, isVerified bool, verificationNotes string, ) (domain.DirectDeposit, error) { // Get the deposit record deposit, err := s.directDepositStore.GetDirectDeposit(ctx, depositID) if err != nil { return domain.DirectDeposit{}, fmt.Errorf("failed to get deposit: %w", err) } // Validate deposit status if deposit.Status != domain.DepositStatusPending { return domain.DirectDeposit{}, errors.New("only pending deposits can be verified") } // Update based on verification result if isVerified { // Credit the wallet err = s.walletStore.UpdateBalance(ctx, deposit.WalletID, deposit.Wallet.Balance+deposit.Amount) if err != nil { return domain.DirectDeposit{}, fmt.Errorf("failed to update wallet balance: %w", err) } // Publish wallet update event go s.publishWalletUpdate(ctx, deposit.WalletID, deposit.Wallet.UserID, deposit.Wallet.Balance+deposit.Amount, "direct_deposit_verified") // Update deposit status deposit.Status = domain.DepositStatusCompleted } else { deposit.Status = domain.DepositStatusRejected } // Update deposit record updatedDeposit, err := s.directDepositStore.UpdateDirectDeposit(ctx, domain.UpdateDirectDeposit{ ID: depositID, Status: deposit.Status, VerifiedBy: cashierID, VerificationNotes: verificationNotes, VerifiedAt: time.Now(), }) if err != nil { return domain.DirectDeposit{}, fmt.Errorf("failed to update deposit: %w", err) } // Notify customer of verification result go s.notifyCustomerVerificationResult(ctx, updatedDeposit) return updatedDeposit, nil } // GetPendingDirectDeposits returns deposits needing verification func (s *Service) GetPendingDirectDeposits(ctx context.Context) ([]domain.DirectDeposit, error) { return s.directDepositStore.GetDirectDepositsByStatus(ctx, domain.DepositStatusPending) } // Helper functions func (s *Service) notifyCashiersForVerification(ctx context.Context, depositID, customerID int64, amount domain.Currency) { cashiers, _, err := s.userSvc.GetAllCashiers(ctx, domain.UserFilter{Role: string(domain.RoleCashier)}) if err != nil { s.logger.Error("failed to get cashiers for notification", "error", err, "deposit_id", depositID) return } customer, err := s.userSvc.GetUserByID(ctx, customerID) if err != nil { s.logger.Error("failed to get customer details", "error", err, "customer_id", customerID) return } for _, cashier := range cashiers { metadataMap := map[string]interface{}{ "deposit_id": depositID, "customer_id": customerID, "amount": amount.Float32(), } metadataJSON, err := json.Marshal(metadataMap) if err != nil { s.logger.Error("failed to marshal notification metadata", "error", err, "deposit_id", depositID) continue } notification := &domain.Notification{ RecipientID: cashier.ID, Type: domain.NotificationTypeDepositVerification, Level: domain.NotificationLevelInfo, DeliveryChannel: domain.DeliveryChannelInApp, Payload: domain.NotificationPayload{ Headline: "Direct Deposit Requires Verification", Message: fmt.Sprintf("Customer %s deposited %.2f - please verify", customer.FirstName+" "+customer.LastName, amount.Float32()), }, Metadata: metadataJSON, } if err := s.notificationSvc.SendNotification(ctx, notification); err != nil { s.logger.Error("failed to send verification notification", "cashier_id", cashier.ID, "error", err) } } } func (s *Service) notifyCustomerVerificationResult(ctx context.Context, deposit domain.DirectDeposit) { var ( headline string message string level domain.NotificationLevel ) if deposit.Status == domain.DepositStatusCompleted { headline = "Deposit Verified" message = fmt.Sprintf("Your deposit of %.2f has been credited to your wallet", deposit.Amount.Float32()) level = domain.NotificationLevelSuccess } else { headline = "Deposit Rejected" message = fmt.Sprintf("Your deposit of %.2f was not verified. Reason: %s", deposit.Amount.Float32(), deposit.VerificationNotes) level = domain.NotificationLevelError } metadataMap := map[string]interface{}{ "deposit_id": deposit.ID, "amount": deposit.Amount.Float32(), "status": string(deposit.Status), } metadataJSON, err := json.Marshal(metadataMap) if err != nil { s.logger.Error("failed to marshal notification metadata", "error", err, "deposit_id", deposit.ID) return } notification := &domain.Notification{ RecipientID: deposit.CustomerID, Type: domain.NotificationTypeDepositResult, Level: level, DeliveryChannel: domain.DeliveryChannelInApp, Payload: domain.NotificationPayload{ Headline: headline, Message: message, }, Metadata: metadataJSON, } if err := s.notificationSvc.SendNotification(ctx, notification); err != nil { s.logger.Error("failed to send deposit result notification", "customer_id", deposit.CustomerID, "error", err) } } func (s *Service) publishWalletUpdate(ctx context.Context, walletID, userID int64, newBalance domain.Currency, trigger string) { s.kafkaProducer.Publish(ctx, fmt.Sprint(walletID), event.WalletEvent{ EventType: event.WalletBalanceUpdated, WalletID: walletID, UserID: userID, Balance: newBalance, Trigger: trigger, }) }