Yimaru-BackEnd/internal/services/bet/notification.go
Samuel Tariku e49ff366d5 feat: Implement wallet notification system and refactor related services
- Added new notification handling in the wallet service to notify admins when wallet balances are low or insufficient.
- Created a new file for wallet notifications and moved relevant functions from the wallet service to this new file.
- Updated the wallet service to publish wallet events including wallet type.
- Refactored the client code to improve readability and maintainability.
- Enhanced the bet handler to support pagination and status filtering for bets.
- Updated routes and handlers for user search functionality to improve clarity and organization.
- Modified cron job scheduling to comment out unused jobs for clarity.
- Updated the WebSocket broadcast to include wallet type in notifications.
- Adjusted the makefile to include Kafka in the docker-compose setup for local development.
2025-09-25 21:26:24 +03:00

317 lines
8.4 KiB
Go

package bet
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"go.uber.org/zap"
)
func newBetResultNotification(userID int64, level domain.NotificationLevel, channel domain.DeliveryChannel, headline, message string, metadata any) *domain.Notification {
raw, _ := json.Marshal(metadata)
return &domain.Notification{
RecipientID: userID,
DeliveryStatus: domain.DeliveryStatusPending,
IsRead: false,
Type: domain.NOTIFICATION_TYPE_BET_RESULT,
Level: level,
Reciever: domain.NotificationRecieverSideCustomer,
DeliveryChannel: channel,
Payload: domain.NotificationPayload{
Headline: headline,
Message: message,
},
Priority: 2,
Metadata: raw,
}
}
type SendResultNotificationParam struct {
BetID int64
Status domain.OutcomeStatus
UserID int64
WinningAmount domain.Currency
Extra string
SendEmail bool
SendSMS bool
}
func (p SendResultNotificationParam) Validate() error {
if p.BetID == 0 {
return errors.New("BetID is required")
}
if p.UserID == 0 {
return errors.New("UserID is required")
}
return nil
}
func shouldSend(channel domain.DeliveryChannel, sendEmail, sendSMS bool) bool {
switch {
case channel == domain.DeliveryChannelEmail && sendEmail:
return true
case channel == domain.DeliveryChannelSMS && sendSMS:
return true
case channel == domain.DeliveryChannelInApp:
return true
default:
return false
}
}
func (s *Service) SendWinningStatusNotification(ctx context.Context, param SendResultNotificationParam) error {
if err := param.Validate(); err != nil {
return err
}
var headline string
var message string
switch param.Status {
case domain.OUTCOME_STATUS_WIN:
headline = fmt.Sprintf("Bet #%v Won!", param.BetID)
message = fmt.Sprintf(
"Congratulations! Your bet #%v has won. %.2f has been credited to your wallet.",
param.BetID,
param.WinningAmount.Float32(),
)
case domain.OUTCOME_STATUS_HALF:
headline = fmt.Sprintf("Bet #%v Half-Win", param.BetID)
message = fmt.Sprintf(
"Your bet #%v resulted in a half-win. %.2f has been credited to your wallet.",
param.BetID,
param.WinningAmount.Float32(),
)
case domain.OUTCOME_STATUS_VOID:
headline = fmt.Sprintf("Bet #%v Refunded", param.BetID)
message = fmt.Sprintf(
"Your bet #%v has been voided. %.2f has been refunded to your wallet.",
param.BetID,
param.WinningAmount.Float32(),
)
default:
return fmt.Errorf("unsupported status: %v", param.Status)
}
for _, channel := range []domain.DeliveryChannel{
domain.DeliveryChannelInApp,
domain.DeliveryChannelEmail,
domain.DeliveryChannelSMS,
} {
if !shouldSend(channel, param.SendEmail, param.SendSMS) {
continue
}
n := newBetResultNotification(param.UserID, domain.NotificationLevelSuccess, channel, headline, message, map[string]any{
"winning_amount": param.WinningAmount.Float32(),
"status": param.Status,
"more": param.Extra,
})
if err := s.notificationSvc.SendNotification(ctx, n); err != nil {
return err
}
}
return nil
}
func (s *Service) SendLosingStatusNotification(ctx context.Context, param SendResultNotificationParam) error {
if err := param.Validate(); err != nil {
return err
}
var headline string
var message string
switch param.Status {
case domain.OUTCOME_STATUS_LOSS:
headline = fmt.Sprintf("Bet #%v Lost", param.BetID)
message = "Unfortunately, your bet did not win this time. Better luck next time!"
default:
return fmt.Errorf("unsupported status: %v", param.Status)
}
for _, channel := range []domain.DeliveryChannel{
domain.DeliveryChannelInApp,
domain.DeliveryChannelEmail,
domain.DeliveryChannelSMS,
} {
if !shouldSend(channel, param.SendEmail, param.SendSMS) {
continue
}
n := newBetResultNotification(param.UserID, domain.NotificationLevelWarning, channel, headline, message, map[string]any{
"status": param.Status,
"more": param.Extra,
})
if err := s.notificationSvc.SendNotification(ctx, n); err != nil {
return err
}
}
return nil
}
func (s *Service) SendErrorStatusNotification(ctx context.Context, betID int64, status domain.OutcomeStatus, userID int64, extra string) error {
var headline string
var message string
switch status {
case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING:
headline = fmt.Sprintf("Bet #%v Processing Issue", betID)
message = "We encountered a problem while processing your bet. Our team is working to resolve it as soon as possible."
default:
return fmt.Errorf("unsupported status: %v", status)
}
for _, channel := range []domain.DeliveryChannel{
domain.DeliveryChannelInApp,
domain.DeliveryChannelEmail,
} {
n := newBetResultNotification(userID, domain.NotificationLevelError, channel, headline, message, map[string]any{
"status": status,
"more": extra,
})
if err := s.notificationSvc.SendNotification(ctx, n); err != nil {
return err
}
}
return nil
}
func (s *Service) SendAdminErrorNotification(ctx context.Context, betID int64, status domain.OutcomeStatus, extra string, companyID int64) error {
var headline string
var message string
switch status {
case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING:
headline = fmt.Sprintf("Processing Error for Bet #%v", betID)
message = "A processing error occurred with this bet. Please review and take corrective action."
default:
return fmt.Errorf("unsupported status: %v", status)
}
super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleSuperAdmin),
})
if err != nil {
s.mongoLogger.Error("failed to get super_admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleAdmin),
CompanyID: domain.ValidInt64{
Value: companyID,
Valid: true,
},
})
if err != nil {
s.mongoLogger.Error("failed to get admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
users := append(super_admin_users, admin_users...)
for _, user := range users {
for _, channel := range []domain.DeliveryChannel{
domain.DeliveryChannelInApp,
domain.DeliveryChannelEmail,
} {
n := newBetResultNotification(user.ID, domain.NotificationLevelError, channel, headline, message, map[string]any{
"status": status,
"more": extra,
})
if err := s.notificationSvc.SendNotification(ctx, n); err != nil {
return err
}
}
}
return nil
}
func (s *Service) SendAdminLargeBetNotification(ctx context.Context, betID int64, totalWinnings float32, extra string, companyID int64) error {
headline := fmt.Sprintf("SYSTEM WARNING: High Risk Bet", betID, totalWinnings)
message := fmt.Sprintf("Bet #%d has been created with %v payout", betID, totalWinnings)
super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleSuperAdmin),
})
if err != nil {
s.mongoLogger.Error("failed to get super_admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
Role: string(domain.RoleAdmin),
CompanyID: domain.ValidInt64{
Value: companyID,
Valid: true,
},
})
if err != nil {
s.mongoLogger.Error("failed to get admin recipients",
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return err
}
users := append(super_admin_users, admin_users...)
for _, user := range users {
for _, channel := range []domain.DeliveryChannel{
domain.DeliveryChannelInApp,
domain.DeliveryChannelEmail,
} {
raw, _ := json.Marshal(map[string]any{
"winnings": totalWinnings,
"more": extra,
})
n := &domain.Notification{
RecipientID: user.ID,
DeliveryStatus: domain.DeliveryStatusPending,
IsRead: false,
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
Level: domain.NotificationLevelWarning,
Reciever: domain.NotificationRecieverSideAdmin,
DeliveryChannel: channel,
Payload: domain.NotificationPayload{
Headline: headline,
Message: message,
},
Priority: 2,
Metadata: raw,
}
// n := newBetResultNotification(user.ID, domain.NotificationLevelWarning, channel, headline, message)
if err := s.notificationSvc.SendNotification(ctx, n); err != nil {
return err
}
}
}
return nil
}