fix: notifications for win bet and wallet balance low
This commit is contained in:
parent
65bd5ab3f5
commit
6e6ed2c9a9
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
|
|
@ -7,5 +7,12 @@
|
||||||
],
|
],
|
||||||
"cSpell.enabledFileTypes": {
|
"cSpell.enabledFileTypes": {
|
||||||
"sql": false
|
"sql": false
|
||||||
}
|
},
|
||||||
|
"workbench.editor.customLabels.enabled": true,
|
||||||
|
"workbench.editor.customLabels.patterns": {
|
||||||
|
"**/internal/services/**/service.go": "${dirname}.service",
|
||||||
|
"**/internal/services/**/*.go": "${filename}.${dirname}.service",
|
||||||
|
"**/internal/domain/**/*.go": "${filename}.${dirname}",
|
||||||
|
"**/internal/repository/**/*.go": "${filename}.repo",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
13
cmd/main.go
13
cmd/main.go
|
|
@ -39,7 +39,8 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
|
||||||
issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting"
|
issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
|
||||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/messenger"
|
||||||
|
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
|
||||||
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
|
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
|
||||||
|
|
@ -101,13 +102,15 @@ func main() {
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
settingSvc := settings.NewService(store)
|
settingSvc := settings.NewService(store)
|
||||||
|
messengerSvc := messenger.NewService(settingSvc, cfg)
|
||||||
|
|
||||||
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
|
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
|
||||||
userSvc := user.NewService(store, store, cfg)
|
userSvc := user.NewService(store, store, messengerSvc, cfg)
|
||||||
eventSvc := event.New(cfg.Bet365Token, store)
|
eventSvc := event.New(cfg.Bet365Token, store)
|
||||||
oddsSvc := odds.New(store, cfg, logger)
|
oddsSvc := odds.New(store, cfg, logger)
|
||||||
notificationRepo := repository.NewNotificationRepository(store)
|
notificationRepo := repository.NewNotificationRepository(store)
|
||||||
virtuaGamesRepo := repository.NewVirtualGameRepository(store)
|
virtuaGamesRepo := repository.NewVirtualGameRepository(store)
|
||||||
notificationSvc := notificationservice.New(notificationRepo, logger, cfg)
|
notificationSvc := notificationservice.New(notificationRepo, domain.MongoDBLogger, logger, cfg, messengerSvc, userSvc)
|
||||||
|
|
||||||
var notificatioStore notificationservice.NotificationStore
|
var notificatioStore notificationservice.NotificationStore
|
||||||
// var userStore user.UserStore
|
// var userStore user.UserStore
|
||||||
|
|
@ -118,6 +121,8 @@ func main() {
|
||||||
notificatioStore,
|
notificatioStore,
|
||||||
// userStore,
|
// userStore,
|
||||||
notificationSvc,
|
notificationSvc,
|
||||||
|
userSvc,
|
||||||
|
domain.MongoDBLogger,
|
||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -125,7 +130,7 @@ func main() {
|
||||||
companySvc := company.NewService(store)
|
companySvc := company.NewService(store)
|
||||||
leagueSvc := league.New(store)
|
leagueSvc := league.New(store)
|
||||||
ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc)
|
ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc)
|
||||||
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, *companySvc, *settingSvc, notificationSvc, logger, domain.MongoDBLogger)
|
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, *companySvc, *settingSvc, *userSvc, notificationSvc, logger, domain.MongoDBLogger)
|
||||||
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc)
|
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc)
|
||||||
bonusSvc := bonus.NewService(store)
|
bonusSvc := bonus.NewService(store)
|
||||||
referalRepo := repository.NewReferralRepository(store)
|
referalRepo := repository.NewReferralRepository(store)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
-- Settings Initial Data
|
-- Settings Initial Data
|
||||||
INSERT INTO settings (key, value)
|
INSERT INTO settings (key, value)
|
||||||
VALUES ('max_number_of_outcomes', '30'),
|
VALUES ('sms_provider', '30'),
|
||||||
|
('max_number_of_outcomes', '30'),
|
||||||
('bet_amount_limit', '100000'),
|
('bet_amount_limit', '100000'),
|
||||||
('daily_ticket_limit', '50'),
|
('daily_ticket_limit', '50'),
|
||||||
('total_winnings_limit', '1000000'),
|
('total_winnings_limit', '1000000'),
|
||||||
|
|
|
||||||
|
|
@ -10,26 +10,4 @@ INSERT INTO settings (key, value, updated_at)
|
||||||
VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (key) DO
|
VALUES ($1, $2, CURRENT_TIMESTAMP) ON CONFLICT (key) DO
|
||||||
UPDATE
|
UPDATE
|
||||||
SET value = EXCLUDED.value
|
SET value = EXCLUDED.value
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: SetInitialData :exec
|
|
||||||
INSERT INTO settings (key, value)
|
|
||||||
VALUES ('max_number_of_outcomes', '30') ON CONFLICT (key) DO
|
|
||||||
UPDATE
|
|
||||||
SET value = EXCLUDED.value;
|
|
||||||
INSERT INTO settings (key, value)
|
|
||||||
VALUES ('bet_amount_limit', '100000') ON CONFLICT (key) DO
|
|
||||||
UPDATE
|
|
||||||
SET value = EXCLUDED.value;
|
|
||||||
INSERT INTO settings (key, value)
|
|
||||||
VALUES ('daily_ticket_limit', '50') ON CONFLICT (key) DO
|
|
||||||
UPDATE
|
|
||||||
SET value = EXCLUDED.value;
|
|
||||||
INSERT INTO settings (key, value)
|
|
||||||
VALUES ('total_winnings_limit', '1000000') ON CONFLICT (key) DO
|
|
||||||
UPDATE
|
|
||||||
SET value = EXCLUDED.value;
|
|
||||||
INSERT INTO settings (key, value)
|
|
||||||
VALUES ('amount_for_bet_referral', '1000000') ON CONFLICT (key) DO
|
|
||||||
UPDATE
|
|
||||||
SET value = EXCLUDED.value;
|
|
||||||
|
|
@ -192,4 +192,9 @@ SET password = $1,
|
||||||
WHERE (
|
WHERE (
|
||||||
email = $2
|
email = $2
|
||||||
OR phone_number = $3
|
OR phone_number = $3
|
||||||
);
|
);
|
||||||
|
-- name: GetAdminByCompanyID :one
|
||||||
|
SELECT users.*
|
||||||
|
FROM companies
|
||||||
|
JOIN users ON companies.admin_id = users.id
|
||||||
|
where companies.id = $1;
|
||||||
|
|
@ -81,15 +81,3 @@ func (q *Queries) SaveSetting(ctx context.Context, arg SaveSettingParams) (Setti
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const SetInitialData = `-- name: SetInitialData :exec
|
|
||||||
INSERT INTO settings (key, value)
|
|
||||||
VALUES ('max_number_of_outcomes', '30') ON CONFLICT (key) DO
|
|
||||||
UPDATE
|
|
||||||
SET value = EXCLUDED.value
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) SetInitialData(ctx context.Context) error {
|
|
||||||
_, err := q.db.Exec(ctx, SetInitialData)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,37 @@ func (q *Queries) DeleteUser(ctx context.Context, id int64) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetAdminByCompanyID = `-- name: GetAdminByCompanyID :one
|
||||||
|
SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by
|
||||||
|
FROM companies
|
||||||
|
JOIN users ON companies.admin_id = users.id
|
||||||
|
where companies.id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetAdminByCompanyID(ctx context.Context, id int64) (User, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetAdminByCompanyID, id)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.FirstName,
|
||||||
|
&i.LastName,
|
||||||
|
&i.Email,
|
||||||
|
&i.PhoneNumber,
|
||||||
|
&i.Role,
|
||||||
|
&i.Password,
|
||||||
|
&i.EmailVerified,
|
||||||
|
&i.PhoneVerified,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.CompanyID,
|
||||||
|
&i.SuspendedAt,
|
||||||
|
&i.Suspended,
|
||||||
|
&i.ReferralCode,
|
||||||
|
&i.ReferredBy,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const GetAllUsers = `-- name: GetAllUsers :many
|
const GetAllUsers = `-- name: GetAllUsers :many
|
||||||
SELECT id,
|
SELECT id,
|
||||||
first_name,
|
first_name,
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,7 @@ const (
|
||||||
OtpMediumSms OtpMedium = "sms"
|
OtpMediumSms OtpMedium = "sms"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OtpProvider string
|
|
||||||
|
|
||||||
const (
|
|
||||||
TwilioSms OtpProvider = "twilio"
|
|
||||||
AfroMessage OtpProvider = "afro_message"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Otp struct {
|
type Otp struct {
|
||||||
ID int64
|
ID int64
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,17 @@ type SettingRes struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SettingList struct {
|
type SettingList struct {
|
||||||
MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"`
|
SMSProvider SMSProvider `json:"sms_provider"`
|
||||||
BetAmountLimit Currency `json:"bet_amount_limit"`
|
MaxNumberOfOutcomes int64 `json:"max_number_of_outcomes"`
|
||||||
DailyTicketPerIP int64 `json:"daily_ticket_limit"`
|
BetAmountLimit Currency `json:"bet_amount_limit"`
|
||||||
TotalWinningLimit Currency `json:"total_winning_limit"`
|
DailyTicketPerIP int64 `json:"daily_ticket_limit"`
|
||||||
AmountForBetReferral Currency `json:"amount_for_bet_referral"`
|
TotalWinningLimit Currency `json:"total_winning_limit"`
|
||||||
CashbackAmountCap Currency `json:"cashback_amount_cap"`
|
AmountForBetReferral Currency `json:"amount_for_bet_referral"`
|
||||||
|
CashbackAmountCap Currency `json:"cashback_amount_cap"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DBSettingList struct {
|
type DBSettingList struct {
|
||||||
|
SMSProvider ValidString
|
||||||
MaxNumberOfOutcomes ValidInt64
|
MaxNumberOfOutcomes ValidInt64
|
||||||
BetAmountLimit ValidInt64
|
BetAmountLimit ValidInt64
|
||||||
DailyTicketPerIP ValidInt64
|
DailyTicketPerIP ValidInt64
|
||||||
|
|
@ -45,8 +47,27 @@ func ConvertInt64SettingsMap(dbSettingList *DBSettingList) map[string]*ValidInt6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ConvertStringSettingsMap(dbSettingList *DBSettingList) map[string]*ValidString {
|
||||||
|
return map[string]*ValidString{
|
||||||
|
"sms_provider": &dbSettingList.SMSProvider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertBoolSettingsMap(dbSettingList *DBSettingList) map[string]*ValidBool {
|
||||||
|
return map[string]*ValidBool{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertFloat32SettingsMap(dbSettingList *DBSettingList) map[string]*ValidFloat32 {
|
||||||
|
return map[string]*ValidFloat32{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertTimeSettingsMap(dbSettingList *DBSettingList) map[string]*ValidTime {
|
||||||
|
return map[string]*ValidTime{}
|
||||||
|
}
|
||||||
|
|
||||||
func ConvertDBSetting(dbSettingList DBSettingList) SettingList {
|
func ConvertDBSetting(dbSettingList DBSettingList) SettingList {
|
||||||
return SettingList{
|
return SettingList{
|
||||||
|
SMSProvider: SMSProvider(dbSettingList.SMSProvider.Value),
|
||||||
MaxNumberOfOutcomes: dbSettingList.MaxNumberOfOutcomes.Value,
|
MaxNumberOfOutcomes: dbSettingList.MaxNumberOfOutcomes.Value,
|
||||||
BetAmountLimit: Currency(dbSettingList.BetAmountLimit.Value),
|
BetAmountLimit: Currency(dbSettingList.BetAmountLimit.Value),
|
||||||
DailyTicketPerIP: dbSettingList.DailyTicketPerIP.Value,
|
DailyTicketPerIP: dbSettingList.DailyTicketPerIP.Value,
|
||||||
|
|
|
||||||
18
internal/domain/sms.go
Normal file
18
internal/domain/sms.go
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
type SMSProvider string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TwilioSms SMSProvider = "twilio"
|
||||||
|
AfroMessage SMSProvider = "afro_message"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsValid checks if the SMSProvider is a valid enum value
|
||||||
|
func (s SMSProvider) IsValid() bool {
|
||||||
|
switch s {
|
||||||
|
case TwilioSms, AfroMessage:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -317,39 +317,7 @@ func (s *Store) CountUnreadNotifications(ctx context.Context, userID int64) (int
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) {
|
|
||||||
dbCompany, err := s.queries.GetCompanyByWalletID(ctx, walletID)
|
|
||||||
if err != nil {
|
|
||||||
return domain.Company{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return domain.Company{
|
|
||||||
ID: dbCompany.ID,
|
|
||||||
Name: dbCompany.Name,
|
|
||||||
AdminID: dbCompany.AdminID,
|
|
||||||
WalletID: dbCompany.WalletID,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) {
|
|
||||||
dbBranch, err := s.queries.GetBranchByWalletID(ctx, walletID)
|
|
||||||
if err != nil {
|
|
||||||
return domain.Branch{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return domain.Branch{
|
|
||||||
ID: dbBranch.ID,
|
|
||||||
Name: dbBranch.Name,
|
|
||||||
Location: dbBranch.Location,
|
|
||||||
IsActive: dbBranch.IsActive,
|
|
||||||
WalletID: dbBranch.WalletID,
|
|
||||||
BranchManagerID: dbBranch.BranchManagerID,
|
|
||||||
CompanyID: dbBranch.CompanyID,
|
|
||||||
IsSelfOwned: dbBranch.IsSelfOwned,
|
|
||||||
// Creat: dbBranch.CreatedAt.Time,
|
|
||||||
// UpdatedAt: dbBranch.UpdatedAt.Time,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// func (s *Store) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
|
// func (s *Store) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
|
||||||
// dbNotifications, err := s.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{
|
// dbNotifications, err := s.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
|
@ -14,6 +15,10 @@ func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) {
|
||||||
var dbSettingList domain.DBSettingList
|
var dbSettingList domain.DBSettingList
|
||||||
|
|
||||||
var int64SettingsMap = domain.ConvertInt64SettingsMap(&dbSettingList)
|
var int64SettingsMap = domain.ConvertInt64SettingsMap(&dbSettingList)
|
||||||
|
var stringSettingsMap = domain.ConvertStringSettingsMap(&dbSettingList)
|
||||||
|
var boolSettingsMap = domain.ConvertBoolSettingsMap(&dbSettingList)
|
||||||
|
var float32SettingsMap = domain.ConvertFloat32SettingsMap(&dbSettingList)
|
||||||
|
var timeSettingsMap = domain.ConvertTimeSettingsMap(&dbSettingList)
|
||||||
|
|
||||||
for _, setting := range settings {
|
for _, setting := range settings {
|
||||||
is_setting_unknown := true
|
is_setting_unknown := true
|
||||||
|
|
@ -31,6 +36,57 @@ func GetDBSettingList(settings []dbgen.Setting) (domain.SettingList, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for key, dbSetting := range stringSettingsMap {
|
||||||
|
if setting.Key == key {
|
||||||
|
*dbSetting = domain.ValidString{
|
||||||
|
Value: setting.Value,
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
is_setting_unknown = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, dbSetting := range boolSettingsMap {
|
||||||
|
if setting.Key == key {
|
||||||
|
value, err := strconv.ParseBool(setting.Value)
|
||||||
|
if err != nil {
|
||||||
|
return domain.SettingList{}, err
|
||||||
|
}
|
||||||
|
*dbSetting = domain.ValidBool{
|
||||||
|
Value: value,
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
is_setting_unknown = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, dbSetting := range float32SettingsMap {
|
||||||
|
if setting.Key == key {
|
||||||
|
value, err := strconv.ParseFloat(setting.Value, 32)
|
||||||
|
if err != nil {
|
||||||
|
return domain.SettingList{}, err
|
||||||
|
}
|
||||||
|
*dbSetting = domain.ValidFloat32{
|
||||||
|
Value: float32(value),
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
is_setting_unknown = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key, dbSetting := range timeSettingsMap {
|
||||||
|
if setting.Key == key {
|
||||||
|
value, err := time.Parse(time.RFC3339, setting.Value)
|
||||||
|
if err != nil {
|
||||||
|
return domain.SettingList{}, err
|
||||||
|
}
|
||||||
|
*dbSetting = domain.ValidTime{
|
||||||
|
Value: value,
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
is_setting_unknown = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if is_setting_unknown {
|
if is_setting_unknown {
|
||||||
domain.MongoDBLogger.Warn("unknown setting found on database", zap.String("setting", setting.Key))
|
domain.MongoDBLogger.Warn("unknown setting found on database", zap.String("setting", setting.Key))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -490,6 +490,27 @@ func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.User, is_c
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetAdminByCompanyID(ctx context.Context, companyID int64) (domain.User, error) {
|
||||||
|
userRes, err := s.queries.GetAdminByCompanyID(ctx, companyID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return domain.User{}, err
|
||||||
|
}
|
||||||
|
return domain.User{
|
||||||
|
ID: userRes.ID,
|
||||||
|
FirstName: userRes.FirstName,
|
||||||
|
LastName: userRes.LastName,
|
||||||
|
Email: userRes.Email.String,
|
||||||
|
PhoneNumber: userRes.PhoneNumber.String,
|
||||||
|
Role: domain.Role(userRes.Role),
|
||||||
|
EmailVerified: userRes.EmailVerified,
|
||||||
|
PhoneVerified: userRes.PhoneVerified,
|
||||||
|
CreatedAt: userRes.CreatedAt.Time,
|
||||||
|
UpdatedAt: userRes.UpdatedAt.Time,
|
||||||
|
Suspended: userRes.Suspended,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetCustomerCounts returns total and active customer counts
|
// GetCustomerCounts returns total and active customer counts
|
||||||
func (s *Store) GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
|
func (s *Store) GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
|
||||||
query := `SELECT
|
query := `SELECT
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,40 @@ func (s *Store) UpdateWalletActive(ctx context.Context, id int64, isActive bool)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) {
|
||||||
|
dbCompany, err := s.queries.GetCompanyByWalletID(ctx, walletID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Company{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.Company{
|
||||||
|
ID: dbCompany.ID,
|
||||||
|
Name: dbCompany.Name,
|
||||||
|
AdminID: dbCompany.AdminID,
|
||||||
|
WalletID: dbCompany.WalletID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) {
|
||||||
|
dbBranch, err := s.queries.GetBranchByWalletID(ctx, walletID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Branch{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.Branch{
|
||||||
|
ID: dbBranch.ID,
|
||||||
|
Name: dbBranch.Name,
|
||||||
|
Location: dbBranch.Location,
|
||||||
|
IsActive: dbBranch.IsActive,
|
||||||
|
WalletID: dbBranch.WalletID,
|
||||||
|
BranchManagerID: dbBranch.BranchManagerID,
|
||||||
|
CompanyID: dbBranch.CompanyID,
|
||||||
|
IsSelfOwned: dbBranch.IsSelfOwned,
|
||||||
|
// Creat: dbBranch.CreatedAt.Time,
|
||||||
|
// UpdatedAt: dbBranch.UpdatedAt.Time,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetBalanceSummary returns wallet balance summary
|
// GetBalanceSummary returns wallet balance summary
|
||||||
func (s *Store) GetBalanceSummary(ctx context.Context, filter domain.ReportFilter) (domain.BalanceSummary, error) {
|
func (s *Store) GetBalanceSummary(ctx context.Context, filter domain.ReportFilter) (domain.BalanceSummary, error) {
|
||||||
var summary domain.BalanceSummary
|
var summary domain.BalanceSummary
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,10 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
||||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
@ -54,6 +55,7 @@ type Service struct {
|
||||||
branchSvc branch.Service
|
branchSvc branch.Service
|
||||||
companySvc company.Service
|
companySvc company.Service
|
||||||
settingSvc settings.Service
|
settingSvc settings.Service
|
||||||
|
userSvc user.Service
|
||||||
notificationSvc *notificationservice.Service
|
notificationSvc *notificationservice.Service
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
mongoLogger *zap.Logger
|
mongoLogger *zap.Logger
|
||||||
|
|
@ -67,6 +69,7 @@ func NewService(
|
||||||
branchSvc branch.Service,
|
branchSvc branch.Service,
|
||||||
companySvc company.Service,
|
companySvc company.Service,
|
||||||
settingSvc settings.Service,
|
settingSvc settings.Service,
|
||||||
|
userSvc user.Service,
|
||||||
notificationSvc *notificationservice.Service,
|
notificationSvc *notificationservice.Service,
|
||||||
logger *slog.Logger,
|
logger *slog.Logger,
|
||||||
mongoLogger *zap.Logger,
|
mongoLogger *zap.Logger,
|
||||||
|
|
@ -215,6 +218,9 @@ func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketI
|
||||||
func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID int64, role domain.Role, companyID domain.ValidInt64) (domain.CreateBetRes, error) {
|
func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID int64, role domain.Role, companyID domain.ValidInt64) (domain.CreateBetRes, error) {
|
||||||
settingsList, err := s.settingSvc.GetSettingList(ctx)
|
settingsList, err := s.settingSvc.GetSettingList(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return domain.CreateBetRes{}, err
|
||||||
|
}
|
||||||
if req.Amount < 1 {
|
if req.Amount < 1 {
|
||||||
return domain.CreateBetRes{}, ErrInvalidAmount
|
return domain.CreateBetRes{}, ErrInvalidAmount
|
||||||
}
|
}
|
||||||
|
|
@ -490,7 +496,7 @@ func (s *Service) DeductBetFromBranchWallet(ctx context.Context, amount float32,
|
||||||
|
|
||||||
deductedAmount := amount * company.DeductedPercentage
|
deductedAmount := amount * company.DeductedPercentage
|
||||||
_, err = s.walletSvc.DeductFromWallet(ctx,
|
_, err = s.walletSvc.DeductFromWallet(ctx,
|
||||||
walletID, domain.ToCurrency(deductedAmount), domain.BranchWalletType, domain.ValidInt64{
|
walletID, domain.ToCurrency(deductedAmount), domain.ValidInt64{
|
||||||
Value: userID,
|
Value: userID,
|
||||||
Valid: true,
|
Valid: true,
|
||||||
}, domain.TRANSFER_DIRECT,
|
}, domain.TRANSFER_DIRECT,
|
||||||
|
|
@ -519,7 +525,7 @@ func (s *Service) DeductBetFromCustomerWallet(ctx context.Context, amount float3
|
||||||
}
|
}
|
||||||
if amount < wallets.RegularBalance.Float32() {
|
if amount < wallets.RegularBalance.Float32() {
|
||||||
_, err = s.walletSvc.DeductFromWallet(ctx, wallets.RegularID,
|
_, err = s.walletSvc.DeductFromWallet(ctx, wallets.RegularID,
|
||||||
domain.ToCurrency(amount), domain.CustomerWalletType, domain.ValidInt64{},
|
domain.ToCurrency(amount), domain.ValidInt64{},
|
||||||
domain.TRANSFER_DIRECT, fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", amount))
|
domain.TRANSFER_DIRECT, fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", amount))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.mongoLogger.Error("wallet deduction failed for customer regular wallet",
|
s.mongoLogger.Error("wallet deduction failed for customer regular wallet",
|
||||||
|
|
@ -538,7 +544,7 @@ func (s *Service) DeductBetFromCustomerWallet(ctx context.Context, amount float3
|
||||||
}
|
}
|
||||||
// Empty the regular balance
|
// Empty the regular balance
|
||||||
_, err = s.walletSvc.DeductFromWallet(ctx, wallets.RegularID,
|
_, err = s.walletSvc.DeductFromWallet(ctx, wallets.RegularID,
|
||||||
wallets.RegularBalance, domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT,
|
wallets.RegularBalance, domain.ValidInt64{}, domain.TRANSFER_DIRECT,
|
||||||
fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", wallets.RegularBalance.Float32()))
|
fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", wallets.RegularBalance.Float32()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.mongoLogger.Error("wallet deduction failed for customer regular wallet",
|
s.mongoLogger.Error("wallet deduction failed for customer regular wallet",
|
||||||
|
|
@ -553,7 +559,7 @@ func (s *Service) DeductBetFromCustomerWallet(ctx context.Context, amount float3
|
||||||
// Empty remaining from static balance
|
// Empty remaining from static balance
|
||||||
remainingAmount := wallets.RegularBalance - domain.Currency(amount)
|
remainingAmount := wallets.RegularBalance - domain.Currency(amount)
|
||||||
_, err = s.walletSvc.DeductFromWallet(ctx, wallets.StaticID,
|
_, err = s.walletSvc.DeductFromWallet(ctx, wallets.StaticID,
|
||||||
remainingAmount, domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT,
|
remainingAmount, domain.ValidInt64{}, domain.TRANSFER_DIRECT,
|
||||||
fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", remainingAmount.Float32()))
|
fmt.Sprintf("Deducted %v amount from wallet by system while placing bet", remainingAmount.Float32()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.mongoLogger.Error("wallet deduction failed for customer static wallet",
|
s.mongoLogger.Error("wallet deduction failed for customer static wallet",
|
||||||
|
|
@ -894,10 +900,19 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if bet.IsShopBet ||
|
switch {
|
||||||
status == domain.OUTCOME_STATUS_ERROR ||
|
case bet.IsShopBet:
|
||||||
status == domain.OUTCOME_STATUS_PENDING ||
|
return s.betStore.UpdateStatus(ctx, id, status)
|
||||||
status == domain.OUTCOME_STATUS_LOSS {
|
case status == domain.OUTCOME_STATUS_ERROR, status == domain.OUTCOME_STATUS_PENDING:
|
||||||
|
s.SendErrorStatusNotification(ctx, status, bet.UserID, "")
|
||||||
|
s.SendAdminErrorAlertNotification(ctx, status, "")
|
||||||
|
s.mongoLogger.Error("Bet Status is error",
|
||||||
|
zap.Int64("bet_id", id),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return s.betStore.UpdateStatus(ctx, id, status)
|
||||||
|
case status == domain.OUTCOME_STATUS_LOSS:
|
||||||
|
s.SendLosingStatusNotification(ctx, status, bet.UserID, "")
|
||||||
return s.betStore.UpdateStatus(ctx, id, status)
|
return s.betStore.UpdateStatus(ctx, id, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -914,10 +929,15 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
|
||||||
switch status {
|
switch status {
|
||||||
case domain.OUTCOME_STATUS_WIN:
|
case domain.OUTCOME_STATUS_WIN:
|
||||||
amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds)
|
amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds)
|
||||||
|
s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "")
|
||||||
case domain.OUTCOME_STATUS_HALF:
|
case domain.OUTCOME_STATUS_HALF:
|
||||||
amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds) / 2
|
amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds) / 2
|
||||||
default:
|
s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "")
|
||||||
|
case domain.OUTCOME_STATUS_VOID:
|
||||||
amount = bet.Amount
|
amount = bet.Amount
|
||||||
|
s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid outcome status")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount, domain.ValidInt64{},
|
_, err = s.walletSvc.AddToWallet(ctx, customerWallet.RegularID, amount, domain.ValidInt64{},
|
||||||
|
|
@ -935,6 +955,207 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
|
||||||
return s.betStore.UpdateStatus(ctx, id, status)
|
return s.betStore.UpdateStatus(ctx, id, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendWinningStatusNotification(ctx context.Context, status domain.OutcomeStatus, userID int64, winningAmount domain.Currency, extra string) error {
|
||||||
|
|
||||||
|
var headline string
|
||||||
|
var message string
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case domain.OUTCOME_STATUS_WIN:
|
||||||
|
headline = "You Bet Has Won!"
|
||||||
|
message = fmt.Sprintf(
|
||||||
|
"You have been awarded %.2f",
|
||||||
|
winningAmount.Float32(),
|
||||||
|
)
|
||||||
|
case domain.OUTCOME_STATUS_HALF:
|
||||||
|
headline = "You have a half win"
|
||||||
|
message = fmt.Sprintf(
|
||||||
|
"You have been awarded %.2f",
|
||||||
|
winningAmount.Float32(),
|
||||||
|
)
|
||||||
|
case domain.OUTCOME_STATUS_VOID:
|
||||||
|
headline = "Your bet has been refunded"
|
||||||
|
message = fmt.Sprintf(
|
||||||
|
"You have been awarded %.2f",
|
||||||
|
winningAmount.Float32(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
betNotification := &domain.Notification{
|
||||||
|
RecipientID: userID,
|
||||||
|
Type: domain.NOTIFICATION_TYPE_BET_RESULT,
|
||||||
|
Level: domain.NotificationLevelSuccess,
|
||||||
|
Reciever: domain.NotificationRecieverSideCustomer,
|
||||||
|
DeliveryChannel: domain.DeliveryChannelInApp,
|
||||||
|
Payload: domain.NotificationPayload{
|
||||||
|
Headline: headline,
|
||||||
|
Message: message,
|
||||||
|
},
|
||||||
|
Priority: 2,
|
||||||
|
Metadata: fmt.Appendf(nil, `{
|
||||||
|
"winning_amount":%.2f,
|
||||||
|
"status":%v
|
||||||
|
"more": %v
|
||||||
|
}`, winningAmount.Float32(), status, extra),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
betNotification.DeliveryChannel = domain.DeliveryChannelEmail
|
||||||
|
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendLosingStatusNotification(ctx context.Context, status domain.OutcomeStatus, userID int64, extra string) error {
|
||||||
|
|
||||||
|
var headline string
|
||||||
|
var message string
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case domain.OUTCOME_STATUS_LOSS:
|
||||||
|
headline = "Your bet has lost"
|
||||||
|
message = "Better luck next time"
|
||||||
|
}
|
||||||
|
|
||||||
|
betNotification := &domain.Notification{
|
||||||
|
RecipientID: userID,
|
||||||
|
Type: domain.NOTIFICATION_TYPE_BET_RESULT,
|
||||||
|
Level: domain.NotificationLevelSuccess,
|
||||||
|
Reciever: domain.NotificationRecieverSideCustomer,
|
||||||
|
DeliveryChannel: domain.DeliveryChannelInApp,
|
||||||
|
Payload: domain.NotificationPayload{
|
||||||
|
Headline: headline,
|
||||||
|
Message: message,
|
||||||
|
},
|
||||||
|
Priority: 2,
|
||||||
|
Metadata: fmt.Appendf(nil, `{
|
||||||
|
"status":%v
|
||||||
|
"more": %v
|
||||||
|
}`, status, extra),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
betNotification.DeliveryChannel = domain.DeliveryChannelEmail
|
||||||
|
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendErrorStatusNotification(ctx context.Context, 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 = "There was an error with your bet"
|
||||||
|
message = "We have encounter an error with your bet. We will fix it as soon as we can"
|
||||||
|
}
|
||||||
|
|
||||||
|
errorSeverityLevel := domain.NotificationErrorSeverityFatal
|
||||||
|
|
||||||
|
betNotification := &domain.Notification{
|
||||||
|
RecipientID: userID,
|
||||||
|
Type: domain.NOTIFICATION_TYPE_BET_RESULT,
|
||||||
|
Level: domain.NotificationLevelSuccess,
|
||||||
|
Reciever: domain.NotificationRecieverSideCustomer,
|
||||||
|
DeliveryChannel: domain.DeliveryChannelInApp,
|
||||||
|
Payload: domain.NotificationPayload{
|
||||||
|
Headline: headline,
|
||||||
|
Message: message,
|
||||||
|
},
|
||||||
|
Priority: 1,
|
||||||
|
ErrorSeverity: &errorSeverityLevel,
|
||||||
|
Metadata: fmt.Appendf(nil, `{
|
||||||
|
"status":%v
|
||||||
|
"more": %v
|
||||||
|
}`, status, extra),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
betNotification.DeliveryChannel = domain.DeliveryChannelEmail
|
||||||
|
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendAdminErrorAlertNotification(ctx context.Context, status domain.OutcomeStatus, extra string) error {
|
||||||
|
|
||||||
|
var headline string
|
||||||
|
var message string
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case domain.OUTCOME_STATUS_ERROR, domain.OUTCOME_STATUS_PENDING:
|
||||||
|
headline = "There was an error with your bet"
|
||||||
|
message = "We have encounter an error with your bet. We will fix it as soon as we can"
|
||||||
|
}
|
||||||
|
|
||||||
|
betNotification := &domain.Notification{
|
||||||
|
Type: domain.NOTIFICATION_TYPE_BET_RESULT,
|
||||||
|
Level: domain.NotificationLevelSuccess,
|
||||||
|
Reciever: domain.NotificationRecieverSideCustomer,
|
||||||
|
DeliveryChannel: domain.DeliveryChannelEmail,
|
||||||
|
Payload: domain.NotificationPayload{
|
||||||
|
Headline: headline,
|
||||||
|
Message: message,
|
||||||
|
},
|
||||||
|
Priority: 2,
|
||||||
|
Metadata: fmt.Appendf(nil, `{
|
||||||
|
"status":%v
|
||||||
|
"more": %v
|
||||||
|
}`, status, extra),
|
||||||
|
}
|
||||||
|
|
||||||
|
users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{
|
||||||
|
Role: string(domain.RoleAdmin),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("failed to get admin recipients",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
betNotification.RecipientID = user.ID
|
||||||
|
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
|
||||||
|
s.mongoLogger.Error("failed to send admin notification",
|
||||||
|
zap.Int64("admin_id", user.ID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
betNotification.DeliveryChannel = domain.DeliveryChannelEmail
|
||||||
|
if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil {
|
||||||
|
s.mongoLogger.Error("failed to send email admin notification",
|
||||||
|
zap.Int64("admin_id", user.ID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) {
|
func (s *Service) CheckBetOutcomeForBet(ctx context.Context, betID int64) (domain.OutcomeStatus, error) {
|
||||||
betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID)
|
betOutcomes, err := s.betStore.GetBetOutcomeByBetID(ctx, betID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
26
internal/services/messenger/email.go
Normal file
26
internal/services/messenger/email.go
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package messenger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/resend/resend-go/v2"
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) SendEmail(ctx context.Context, receiverEmail, message string, subject string) error {
|
||||||
|
apiKey := s.config.ResendApiKey
|
||||||
|
client := resend.NewClient(apiKey)
|
||||||
|
formattedSenderEmail := "FortuneBets <" + s.config.ResendSenderEmail + ">"
|
||||||
|
params := &resend.SendEmailRequest{
|
||||||
|
From: formattedSenderEmail,
|
||||||
|
To: []string{receiverEmail},
|
||||||
|
Subject: subject,
|
||||||
|
Text: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.Emails.Send(params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
21
internal/services/messenger/service.go
Normal file
21
internal/services/messenger/service.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package messenger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
settingSvc *settings.Service
|
||||||
|
config *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(
|
||||||
|
settingSvc *settings.Service,
|
||||||
|
cfg *config.Config,
|
||||||
|
) *Service {
|
||||||
|
return &Service{
|
||||||
|
settingSvc: settingSvc,
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
85
internal/services/messenger/sms.go
Normal file
85
internal/services/messenger/sms.go
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
package messenger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
afro "github.com/amanuelabay/afrosms-go"
|
||||||
|
"github.com/twilio/twilio-go"
|
||||||
|
twilioApi "github.com/twilio/twilio-go/rest/api/v2010"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrSMSProviderNotFound = errors.New("SMS Provider Not Found")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) SendSMS(ctx context.Context, receiverPhone, message string) error {
|
||||||
|
|
||||||
|
settingsList, err := s.settingSvc.GetSettingList(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch settingsList.SMSProvider {
|
||||||
|
case domain.AfroMessage:
|
||||||
|
return s.SendAfroMessageSMS(ctx, receiverPhone, message)
|
||||||
|
case domain.TwilioSms:
|
||||||
|
return s.SendTwilioSMS(ctx, receiverPhone, message)
|
||||||
|
default:
|
||||||
|
return ErrSMSProviderNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendAfroMessageSMS(ctx context.Context, receiverPhone, message string) error {
|
||||||
|
apiKey := s.config.AFRO_SMS_API_KEY
|
||||||
|
senderName := s.config.AFRO_SMS_SENDER_NAME
|
||||||
|
hostURL := s.config.ADRO_SMS_HOST_URL
|
||||||
|
endpoint := "/api/send"
|
||||||
|
|
||||||
|
// API endpoint has been updated
|
||||||
|
// TODO: no need for package for the afro message operations (pretty simple stuff)
|
||||||
|
request := afro.GetRequest(apiKey, endpoint, hostURL)
|
||||||
|
request.BaseURL = "https://api.afromessage.com/api/send"
|
||||||
|
|
||||||
|
request.Method = "GET"
|
||||||
|
request.Sender(senderName)
|
||||||
|
request.To(receiverPhone, message)
|
||||||
|
|
||||||
|
response, err := afro.MakeRequestWithContext(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response["acknowledge"] == "success" {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
fmt.Println(response["response"].(map[string]interface{}))
|
||||||
|
return errors.New("SMS delivery failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendTwilioSMS(ctx context.Context, receiverPhone, message string) error {
|
||||||
|
accountSid := s.config.TwilioAccountSid
|
||||||
|
authToken := s.config.TwilioAuthToken
|
||||||
|
senderPhone := s.config.TwilioSenderPhoneNumber
|
||||||
|
|
||||||
|
client := twilio.NewRestClientWithParams(twilio.ClientParams{
|
||||||
|
Username: accountSid,
|
||||||
|
Password: authToken,
|
||||||
|
})
|
||||||
|
|
||||||
|
params := &twilioApi.CreateMessageParams{}
|
||||||
|
params.SetTo(receiverPhone)
|
||||||
|
params.SetFrom(senderPhone)
|
||||||
|
params.SetBody(message)
|
||||||
|
|
||||||
|
_, err := client.Api.CreateMessage(params)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s", "Error sending SMS message: %s"+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -1,475 +0,0 @@
|
||||||
package notificationservice
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"log/slog"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
|
||||||
|
|
||||||
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws"
|
|
||||||
afro "github.com/amanuelabay/afrosms-go"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
repo repository.NotificationRepository
|
|
||||||
Hub *ws.NotificationHub
|
|
||||||
notificationStore NotificationStore
|
|
||||||
connections sync.Map
|
|
||||||
notificationCh chan *domain.Notification
|
|
||||||
stopCh chan struct{}
|
|
||||||
config *config.Config
|
|
||||||
logger *slog.Logger
|
|
||||||
redisClient *redis.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *config.Config) *Service {
|
|
||||||
hub := ws.NewNotificationHub()
|
|
||||||
rdb := redis.NewClient(&redis.Options{
|
|
||||||
Addr: cfg.RedisAddr, // e.g., "redis:6379"
|
|
||||||
})
|
|
||||||
|
|
||||||
svc := &Service{
|
|
||||||
repo: repo,
|
|
||||||
Hub: hub,
|
|
||||||
logger: logger,
|
|
||||||
connections: sync.Map{},
|
|
||||||
notificationCh: make(chan *domain.Notification, 1000),
|
|
||||||
stopCh: make(chan struct{}),
|
|
||||||
config: cfg,
|
|
||||||
redisClient: rdb,
|
|
||||||
}
|
|
||||||
|
|
||||||
go hub.Run()
|
|
||||||
go svc.startWorker()
|
|
||||||
go svc.startRetryWorker()
|
|
||||||
go svc.RunRedisSubscriber(context.Background())
|
|
||||||
|
|
||||||
return svc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) addConnection(recipientID int64, c *websocket.Conn) {
|
|
||||||
if c == nil {
|
|
||||||
s.logger.Warn("[NotificationSvc.AddConnection] Attempted to add nil WebSocket connection", "recipientID", recipientID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.connections.Store(recipientID, c)
|
|
||||||
s.logger.Info("[NotificationSvc.AddConnection] Added WebSocket connection", "recipientID", recipientID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SendNotification(ctx context.Context, notification *domain.Notification) error {
|
|
||||||
notification.ID = helpers.GenerateID()
|
|
||||||
notification.Timestamp = time.Now()
|
|
||||||
notification.DeliveryStatus = domain.DeliveryStatusPending
|
|
||||||
|
|
||||||
created, err := s.repo.CreateNotification(ctx, notification)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("[NotificationSvc.SendNotification] Failed to create notification", "id", notification.ID, "error", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
notification = created
|
|
||||||
|
|
||||||
if notification.DeliveryChannel == domain.DeliveryChannelInApp {
|
|
||||||
s.Hub.Broadcast <- map[string]interface{}{
|
|
||||||
"type": "CREATED_NOTIFICATION",
|
|
||||||
"recipient_id": notification.RecipientID,
|
|
||||||
"payload": notification,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case s.notificationCh <- notification:
|
|
||||||
default:
|
|
||||||
s.logger.Warn("[NotificationSvc.SendNotification] Notification channel full, dropping notification", "id", notification.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) MarkAsRead(ctx context.Context, notificationIDs []string, recipientID int64) error {
|
|
||||||
for _, notificationID := range notificationIDs {
|
|
||||||
_, err := s.repo.UpdateNotificationStatus(ctx, notificationID, string(domain.DeliveryStatusSent), true, nil)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("[NotificationSvc.MarkAsRead] Failed to mark notification as read", "notificationID", notificationID, "recipientID", recipientID, "error", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// count, err := s.repo.CountUnreadNotifications(ctx, recipientID)
|
|
||||||
// if err != nil {
|
|
||||||
// s.logger.Error("[NotificationSvc.MarkAsRead] Failed to count unread notifications", "recipientID", recipientID, "error", err)
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// s.Hub.Broadcast <- map[string]interface{}{
|
|
||||||
// "type": "COUNT_NOT_OPENED_NOTIFICATION",
|
|
||||||
// "recipient_id": recipientID,
|
|
||||||
// "payload": map[string]int{
|
|
||||||
// "not_opened_notifications_count": int(count),
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
|
|
||||||
s.logger.Info("[NotificationSvc.MarkAsRead] Notification marked as read", "notificationID", notificationID, "recipientID", recipientID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) {
|
|
||||||
notifications, err := s.repo.ListNotifications(ctx, recipientID, limit, offset)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("[NotificationSvc.ListNotifications] Failed to list notifications", "recipientID", recipientID, "limit", limit, "offset", offset, "error", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.logger.Info("[NotificationSvc.ListNotifications] Successfully listed notifications", "recipientID", recipientID, "count", len(notifications))
|
|
||||||
return notifications, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
|
|
||||||
notifications, err := s.repo.GetAllNotifications(ctx, limit, offset)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("[NotificationSvc.ListNotifications] Failed to get all notifications")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.logger.Info("[NotificationSvc.ListNotifications] Successfully retrieved all notifications", "count", len(notifications))
|
|
||||||
return notifications, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error {
|
|
||||||
s.addConnection(recipientID, c)
|
|
||||||
s.logger.Info("[NotificationSvc.ConnectWebSocket] WebSocket connection established", "recipientID", recipientID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) DisconnectWebSocket(recipientID int64) {
|
|
||||||
if conn, loaded := s.connections.LoadAndDelete(recipientID); loaded {
|
|
||||||
conn.(*websocket.Conn).Close()
|
|
||||||
s.logger.Info("[NotificationSvc.DisconnectWebSocket] Disconnected WebSocket", "recipientID", recipientID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SendSMS(ctx context.Context, recipientID int64, message string) error {
|
|
||||||
s.logger.Info("[NotificationSvc.SendSMS] SMS notification requested", "recipientID", recipientID, "message", message)
|
|
||||||
|
|
||||||
apiKey := s.config.AFRO_SMS_API_KEY
|
|
||||||
senderName := s.config.AFRO_SMS_SENDER_NAME
|
|
||||||
receiverPhone := s.config.AFRO_SMS_RECEIVER_PHONE_NUMBER
|
|
||||||
hostURL := s.config.ADRO_SMS_HOST_URL
|
|
||||||
endpoint := "/api/send"
|
|
||||||
|
|
||||||
request := afro.GetRequest(apiKey, endpoint, hostURL)
|
|
||||||
request.Method = "GET"
|
|
||||||
request.Sender(senderName)
|
|
||||||
request.To(receiverPhone, message)
|
|
||||||
|
|
||||||
response, err := afro.MakeRequestWithContext(ctx, request)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("[NotificationSvc.SendSMS] Failed to send SMS", "recipientID", recipientID, "error", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if response["acknowledge"] == "success" {
|
|
||||||
s.logger.Info("[NotificationSvc.SendSMS] SMS sent successfully", "recipientID", recipientID)
|
|
||||||
} else {
|
|
||||||
s.logger.Error("[NotificationSvc.SendSMS] Failed to send SMS", "recipientID", recipientID, "response", response["response"])
|
|
||||||
return errors.New("SMS delivery failed: " + response["response"].(string))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SendEmail(ctx context.Context, recipientID int64, subject, message string) error {
|
|
||||||
s.logger.Info("[NotificationSvc.SendEmail] Email notification requested", "recipientID", recipientID, "subject", subject)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) startWorker() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case notification := <-s.notificationCh:
|
|
||||||
s.handleNotification(notification)
|
|
||||||
case <-s.stopCh:
|
|
||||||
s.logger.Info("[NotificationSvc.StartWorker] Worker stopped")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) {
|
|
||||||
return s.repo.ListRecipientIDs(ctx, receiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) handleNotification(notification *domain.Notification) {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
switch notification.DeliveryChannel {
|
|
||||||
case domain.DeliveryChannelSMS:
|
|
||||||
err := s.SendSMS(ctx, notification.RecipientID, notification.Payload.Message)
|
|
||||||
if err != nil {
|
|
||||||
notification.DeliveryStatus = domain.DeliveryStatusFailed
|
|
||||||
} else {
|
|
||||||
notification.DeliveryStatus = domain.DeliveryStatusSent
|
|
||||||
}
|
|
||||||
case domain.DeliveryChannelEmail:
|
|
||||||
err := s.SendEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message)
|
|
||||||
if err != nil {
|
|
||||||
notification.DeliveryStatus = domain.DeliveryStatusFailed
|
|
||||||
} else {
|
|
||||||
notification.DeliveryStatus = domain.DeliveryStatusSent
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if notification.DeliveryChannel != domain.DeliveryChannelInApp {
|
|
||||||
s.logger.Warn("[NotificationSvc.HandleNotification] Unsupported delivery channel", "channel", notification.DeliveryChannel)
|
|
||||||
notification.DeliveryStatus = domain.DeliveryStatusFailed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
|
|
||||||
s.logger.Error("[NotificationSvc.HandleNotification] Failed to update notification status", "id", notification.ID, "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) startRetryWorker() {
|
|
||||||
ticker := time.NewTicker(1 * time.Minute)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
s.retryFailedNotifications()
|
|
||||||
case <-s.stopCh:
|
|
||||||
s.logger.Info("[NotificationSvc.StartRetryWorker] Retry worker stopped")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) retryFailedNotifications() {
|
|
||||||
ctx := context.Background()
|
|
||||||
failedNotifications, err := s.repo.ListFailedNotifications(ctx, 100)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("[NotificationSvc.RetryFailedNotifications] Failed to list failed notifications", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, n := range failedNotifications {
|
|
||||||
notification := &n
|
|
||||||
go func(notification *domain.Notification) {
|
|
||||||
for attempt := 0; attempt < 3; attempt++ {
|
|
||||||
time.Sleep(time.Duration(attempt) * time.Second)
|
|
||||||
switch notification.DeliveryChannel {
|
|
||||||
case domain.DeliveryChannelSMS:
|
|
||||||
if err := s.SendSMS(ctx, notification.RecipientID, notification.Payload.Message); err == nil {
|
|
||||||
notification.DeliveryStatus = domain.DeliveryStatusSent
|
|
||||||
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
|
|
||||||
s.logger.Error("[NotificationSvc.RetryFailedNotifications] Failed to update after retry", "id", notification.ID, "error", err)
|
|
||||||
}
|
|
||||||
s.logger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification", "id", notification.ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case domain.DeliveryChannelEmail:
|
|
||||||
if err := s.SendEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message); err == nil {
|
|
||||||
notification.DeliveryStatus = domain.DeliveryStatusSent
|
|
||||||
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
|
|
||||||
s.logger.Error("[NotificationSvc.RetryFailedNotifications] Failed to update after retry", "id", notification.ID, "error", err)
|
|
||||||
}
|
|
||||||
s.logger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification", "id", notification.ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.logger.Error("[NotificationSvc.RetryFailedNotifications] Max retries reached for notification", "id", notification.ID)
|
|
||||||
}(notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) {
|
|
||||||
return s.repo.CountUnreadNotifications(ctx, recipient_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// func (s *Service) GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error){
|
|
||||||
// return s.repo.Get(ctx, filter)
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (s *Service) RunRedisSubscriber(ctx context.Context) {
|
|
||||||
pubsub := s.redisClient.Subscribe(ctx, "live_metrics")
|
|
||||||
defer pubsub.Close()
|
|
||||||
|
|
||||||
ch := pubsub.Channel()
|
|
||||||
for msg := range ch {
|
|
||||||
var parsed map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(msg.Payload), &parsed); err != nil {
|
|
||||||
s.logger.Error("invalid Redis message format", "payload", msg.Payload, "error", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
eventType, _ := parsed["type"].(string)
|
|
||||||
payload := parsed["payload"]
|
|
||||||
recipientID, hasRecipient := parsed["recipient_id"]
|
|
||||||
recipientType, _ := parsed["recipient_type"].(string)
|
|
||||||
|
|
||||||
message := map[string]interface{}{
|
|
||||||
"type": eventType,
|
|
||||||
"payload": payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasRecipient {
|
|
||||||
message["recipient_id"] = recipientID
|
|
||||||
message["recipient_type"] = recipientType
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Hub.Broadcast <- message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) UpdateLiveWalletMetrics(ctx context.Context, companies []domain.GetCompany, branches []domain.BranchWallet) error {
|
|
||||||
const key = "live_metrics"
|
|
||||||
|
|
||||||
companyBalances := make([]domain.CompanyWalletBalance, 0, len(companies))
|
|
||||||
for _, c := range companies {
|
|
||||||
companyBalances = append(companyBalances, domain.CompanyWalletBalance{
|
|
||||||
CompanyID: c.ID,
|
|
||||||
CompanyName: c.Name,
|
|
||||||
Balance: float64(c.WalletBalance.Float32()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
branchBalances := make([]domain.BranchWalletBalance, 0, len(branches))
|
|
||||||
for _, b := range branches {
|
|
||||||
branchBalances = append(branchBalances, domain.BranchWalletBalance{
|
|
||||||
BranchID: b.ID,
|
|
||||||
BranchName: b.Name,
|
|
||||||
CompanyID: b.CompanyID,
|
|
||||||
Balance: float64(b.Balance.Float32()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := domain.LiveWalletMetrics{
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
CompanyBalances: companyBalances,
|
|
||||||
BranchBalances: branchBalances,
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedData, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.redisClient.Set(ctx, key, updatedData, 0).Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.redisClient.Publish(ctx, key, updatedData).Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error) {
|
|
||||||
const key = "live_metrics"
|
|
||||||
var metric domain.LiveMetric
|
|
||||||
|
|
||||||
val, err := s.redisClient.Get(ctx, key).Result()
|
|
||||||
if err == redis.Nil {
|
|
||||||
// Key does not exist yet, return zero-valued struct
|
|
||||||
return domain.LiveMetric{}, nil
|
|
||||||
} else if err != nil {
|
|
||||||
return domain.LiveMetric{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(val), &metric); err != nil {
|
|
||||||
return domain.LiveMetric{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return metric, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) UpdateLiveWalletMetricForWallet(ctx context.Context, wallet domain.Wallet) {
|
|
||||||
var (
|
|
||||||
payload domain.LiveWalletMetrics
|
|
||||||
event map[string]interface{}
|
|
||||||
key = "live_metrics"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Try company first
|
|
||||||
company, companyErr := s.notificationStore.GetCompanyByWalletID(ctx, wallet.ID)
|
|
||||||
if companyErr == nil {
|
|
||||||
payload = domain.LiveWalletMetrics{
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
CompanyBalances: []domain.CompanyWalletBalance{{
|
|
||||||
CompanyID: company.ID,
|
|
||||||
CompanyName: company.Name,
|
|
||||||
Balance: float64(wallet.Balance),
|
|
||||||
}},
|
|
||||||
BranchBalances: []domain.BranchWalletBalance{},
|
|
||||||
}
|
|
||||||
|
|
||||||
event = map[string]interface{}{
|
|
||||||
"type": "LIVE_WALLET_METRICS_UPDATE",
|
|
||||||
"recipient_id": company.ID,
|
|
||||||
"recipient_type": "company",
|
|
||||||
"payload": payload,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Try branch next
|
|
||||||
branch, branchErr := s.notificationStore.GetBranchByWalletID(ctx, wallet.ID)
|
|
||||||
if branchErr == nil {
|
|
||||||
payload = domain.LiveWalletMetrics{
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
CompanyBalances: []domain.CompanyWalletBalance{},
|
|
||||||
BranchBalances: []domain.BranchWalletBalance{{
|
|
||||||
BranchID: branch.ID,
|
|
||||||
BranchName: branch.Name,
|
|
||||||
CompanyID: branch.CompanyID,
|
|
||||||
Balance: float64(wallet.Balance),
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
|
|
||||||
event = map[string]interface{}{
|
|
||||||
"type": "LIVE_WALLET_METRICS_UPDATE",
|
|
||||||
"recipient_id": branch.ID,
|
|
||||||
"recipient_type": "branch",
|
|
||||||
"payload": payload,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Neither company nor branch matched this wallet
|
|
||||||
s.logger.Warn("wallet not linked to any company or branch", "walletID", wallet.ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save latest metric to Redis
|
|
||||||
if jsonBytes, err := json.Marshal(payload); err == nil {
|
|
||||||
s.redisClient.Set(ctx, key, jsonBytes, 0)
|
|
||||||
} else {
|
|
||||||
s.logger.Error("failed to marshal wallet metrics payload", "walletID", wallet.ID, "err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish via Redis
|
|
||||||
if jsonEvent, err := json.Marshal(event); err == nil {
|
|
||||||
s.redisClient.Publish(ctx, key, jsonEvent)
|
|
||||||
} else {
|
|
||||||
s.logger.Error("failed to marshal event payload", "walletID", wallet.ID, "err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast over WebSocket
|
|
||||||
s.Hub.Broadcast <- event
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) {
|
|
||||||
return s.notificationStore.GetCompanyByWalletID(ctx, walletID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) {
|
|
||||||
return s.notificationStore.GetBranchByWalletID(ctx, walletID)
|
|
||||||
}
|
|
||||||
|
|
@ -8,8 +8,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type NotificationStore interface {
|
type NotificationStore interface {
|
||||||
GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error)
|
|
||||||
GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error)
|
|
||||||
SendNotification(ctx context.Context, notification *domain.Notification) error
|
SendNotification(ctx context.Context, notification *domain.Notification) error
|
||||||
MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error
|
MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error
|
||||||
ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error)
|
ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error)
|
||||||
646
internal/services/notification/service.go
Normal file
646
internal/services/notification/service.go
Normal file
|
|
@ -0,0 +1,646 @@
|
||||||
|
package notificationservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
// "errors"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/messenger"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws"
|
||||||
|
// afro "github.com/amanuelabay/afrosms-go"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
repo repository.NotificationRepository
|
||||||
|
Hub *ws.NotificationHub
|
||||||
|
notificationStore NotificationStore
|
||||||
|
connections sync.Map
|
||||||
|
notificationCh chan *domain.Notification
|
||||||
|
stopCh chan struct{}
|
||||||
|
config *config.Config
|
||||||
|
userSvc *user.Service
|
||||||
|
messengerSvc *messenger.Service
|
||||||
|
mongoLogger *zap.Logger
|
||||||
|
logger *slog.Logger
|
||||||
|
redisClient *redis.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(repo repository.NotificationRepository,
|
||||||
|
mongoLogger *zap.Logger,
|
||||||
|
logger *slog.Logger,
|
||||||
|
cfg *config.Config,
|
||||||
|
messengerSvc *messenger.Service,
|
||||||
|
userSvc *user.Service,
|
||||||
|
) *Service {
|
||||||
|
hub := ws.NewNotificationHub()
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: cfg.RedisAddr, // e.g., "redis:6379"
|
||||||
|
})
|
||||||
|
|
||||||
|
svc := &Service{
|
||||||
|
repo: repo,
|
||||||
|
Hub: hub,
|
||||||
|
mongoLogger: mongoLogger,
|
||||||
|
logger: logger,
|
||||||
|
connections: sync.Map{},
|
||||||
|
notificationCh: make(chan *domain.Notification, 1000),
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
messengerSvc: messengerSvc,
|
||||||
|
userSvc: userSvc,
|
||||||
|
config: cfg,
|
||||||
|
redisClient: rdb,
|
||||||
|
}
|
||||||
|
|
||||||
|
go hub.Run()
|
||||||
|
go svc.startWorker()
|
||||||
|
go svc.startRetryWorker()
|
||||||
|
go svc.RunRedisSubscriber(context.Background())
|
||||||
|
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) addConnection(recipientID int64, c *websocket.Conn) error {
|
||||||
|
if c == nil {
|
||||||
|
s.mongoLogger.Warn("[NotificationSvc.AddConnection] Attempted to add nil WebSocket connection",
|
||||||
|
zap.Int64("recipientID", recipientID),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return fmt.Errorf("Invalid Websocket Connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.connections.Store(recipientID, c)
|
||||||
|
s.mongoLogger.Info("[NotificationSvc.AddConnection] Added WebSocket connection",
|
||||||
|
zap.Int64("recipientID", recipientID),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendNotification(ctx context.Context, notification *domain.Notification) error {
|
||||||
|
notification.ID = helpers.GenerateID()
|
||||||
|
notification.Timestamp = time.Now()
|
||||||
|
notification.DeliveryStatus = domain.DeliveryStatusPending
|
||||||
|
|
||||||
|
created, err := s.repo.CreateNotification(ctx, notification)
|
||||||
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("[NotificationSvc.SendNotification] Failed to create notification",
|
||||||
|
zap.String("id", notification.ID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
notification = created
|
||||||
|
|
||||||
|
if notification.DeliveryChannel == domain.DeliveryChannelInApp {
|
||||||
|
s.Hub.Broadcast <- map[string]interface{}{
|
||||||
|
"type": "CREATED_NOTIFICATION",
|
||||||
|
"recipient_id": notification.RecipientID,
|
||||||
|
"payload": notification,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case s.notificationCh <- notification:
|
||||||
|
default:
|
||||||
|
s.mongoLogger.Warn("[NotificationSvc.SendNotification] Notification channel full, dropping notification",
|
||||||
|
zap.String("id", notification.ID),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) MarkAsRead(ctx context.Context, notificationIDs []string, recipientID int64) error {
|
||||||
|
for _, notificationID := range notificationIDs {
|
||||||
|
_, err := s.repo.UpdateNotificationStatus(ctx, notificationID, string(domain.DeliveryStatusSent), true, nil)
|
||||||
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("[NotificationSvc.MarkAsRead] Failed to mark notification as read",
|
||||||
|
zap.String("notificationID", notificationID),
|
||||||
|
zap.Int64("recipientID", recipientID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// count, err := s.repo.CountUnreadNotifications(ctx, recipientID)
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("[NotificationSvc.MarkAsRead] Failed to count unread notifications", "recipientID", recipientID, "error", err)
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// s.Hub.Broadcast <- map[string]interface{}{
|
||||||
|
// "type": "COUNT_NOT_OPENED_NOTIFICATION",
|
||||||
|
// "recipient_id": recipientID,
|
||||||
|
// "payload": map[string]int{
|
||||||
|
// "not_opened_notifications_count": int(count),
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
|
||||||
|
s.mongoLogger.Info("[NotificationSvc.MarkAsRead] Notification marked as read",
|
||||||
|
zap.String("notificationID", notificationID),
|
||||||
|
zap.Int64("recipientID", recipientID),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) {
|
||||||
|
notifications, err := s.repo.ListNotifications(ctx, recipientID, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("[NotificationSvc.ListNotifications] Failed to list notifications",
|
||||||
|
zap.Int64("recipientID", recipientID),
|
||||||
|
zap.Int("limit", limit),
|
||||||
|
zap.Int("offset", offset),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.mongoLogger.Info("[NotificationSvc.ListNotifications] Successfully listed notifications",
|
||||||
|
zap.Int64("recipientID", recipientID),
|
||||||
|
zap.Int("count", len(notifications)),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return notifications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
|
||||||
|
notifications, err := s.repo.GetAllNotifications(ctx, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("[NotificationSvc.ListNotifications] Failed to get all notifications",
|
||||||
|
zap.Int("limit", limit),
|
||||||
|
zap.Int("offset", offset),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.mongoLogger.Info("[NotificationSvc.ListNotifications] Successfully retrieved all notifications",
|
||||||
|
zap.Int("count", len(notifications)),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return notifications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error {
|
||||||
|
err := s.addConnection(recipientID, c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("[NotificationSvc.ConnectWebSocket] Failed to create WebSocket connection",
|
||||||
|
zap.Int64("recipientID", recipientID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.mongoLogger.Info("[NotificationSvc.ConnectWebSocket] WebSocket connection established",
|
||||||
|
zap.Int64("recipientID", recipientID),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DisconnectWebSocket(recipientID int64) {
|
||||||
|
if conn, loaded := s.connections.LoadAndDelete(recipientID); loaded {
|
||||||
|
conn.(*websocket.Conn).Close()
|
||||||
|
// s.logger.Info("[NotificationSvc.DisconnectWebSocket] Disconnected WebSocket", "recipientID", recipientID)
|
||||||
|
s.mongoLogger.Info("[NotificationSvc.DisconnectWebSocket] Disconnected WebSocket",
|
||||||
|
zap.Int64("recipientID", recipientID),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (s *Service) SendSMS(ctx context.Context, recipientID int64, message string) error {
|
||||||
|
// s.logger.Info("[NotificationSvc.SendSMS] SMS notification requested", "recipientID", recipientID, "message", message)
|
||||||
|
|
||||||
|
// apiKey := s.config.AFRO_SMS_API_KEY
|
||||||
|
// senderName := s.config.AFRO_SMS_SENDER_NAME
|
||||||
|
// receiverPhone := s.config.AFRO_SMS_RECEIVER_PHONE_NUMBER
|
||||||
|
// hostURL := s.config.ADRO_SMS_HOST_URL
|
||||||
|
// endpoint := "/api/send"
|
||||||
|
|
||||||
|
// request := afro.GetRequest(apiKey, endpoint, hostURL)
|
||||||
|
// request.Method = "GET"
|
||||||
|
// request.Sender(senderName)
|
||||||
|
// request.To(receiverPhone, message)
|
||||||
|
|
||||||
|
// response, err := afro.MakeRequestWithContext(ctx, request)
|
||||||
|
// if err != nil {
|
||||||
|
// s.logger.Error("[NotificationSvc.SendSMS] Failed to send SMS", "recipientID", recipientID, "error", err)
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if response["acknowledge"] == "success" {
|
||||||
|
// s.logger.Info("[NotificationSvc.SendSMS] SMS sent successfully", "recipientID", recipientID)
|
||||||
|
// } else {
|
||||||
|
// s.logger.Error("[NotificationSvc.SendSMS] Failed to send SMS", "recipientID", recipientID, "response", response["response"])
|
||||||
|
// return errors.New("SMS delivery failed: " + response["response"].(string))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (s *Service) SendEmail(ctx context.Context, recipientID int64, subject, message string) error {
|
||||||
|
// s.logger.Info("[NotificationSvc.SendEmail] Email notification requested", "recipientID", recipientID, "subject", subject)
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (s *Service) startWorker() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case notification := <-s.notificationCh:
|
||||||
|
s.handleNotification(notification)
|
||||||
|
case <-s.stopCh:
|
||||||
|
s.mongoLogger.Info("[NotificationSvc.StartWorker] Worker stopped",
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) {
|
||||||
|
return s.repo.ListRecipientIDs(ctx, receiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) handleNotification(notification *domain.Notification) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
switch notification.DeliveryChannel {
|
||||||
|
case domain.DeliveryChannelSMS:
|
||||||
|
err := s.SendNotificationSMS(ctx, notification.RecipientID, notification.Payload.Message)
|
||||||
|
if err != nil {
|
||||||
|
notification.DeliveryStatus = domain.DeliveryStatusFailed
|
||||||
|
} else {
|
||||||
|
notification.DeliveryStatus = domain.DeliveryStatusSent
|
||||||
|
}
|
||||||
|
|
||||||
|
case domain.DeliveryChannelEmail:
|
||||||
|
err := s.SendNotificationEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message)
|
||||||
|
if err != nil {
|
||||||
|
notification.DeliveryStatus = domain.DeliveryStatusFailed
|
||||||
|
} else {
|
||||||
|
notification.DeliveryStatus = domain.DeliveryStatusSent
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if notification.DeliveryChannel != domain.DeliveryChannelInApp {
|
||||||
|
s.mongoLogger.Warn("[NotificationSvc.HandleNotification] Unsupported delivery channel",
|
||||||
|
zap.String("channel", string(notification.DeliveryChannel)),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
notification.DeliveryStatus = domain.DeliveryStatusFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
|
||||||
|
s.mongoLogger.Error("[NotificationSvc.HandleNotification] Failed to update notification status",
|
||||||
|
zap.String("id", notification.ID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendNotificationSMS(ctx context.Context, recipientID int64, message string) error {
|
||||||
|
// Get User Phone Number
|
||||||
|
user, err := s.userSvc.GetUserByID(ctx, recipientID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.PhoneVerified {
|
||||||
|
return fmt.Errorf("Cannot send notification to unverified phone number")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.PhoneNumber == "" {
|
||||||
|
return fmt.Errorf("Phone Number is invalid")
|
||||||
|
}
|
||||||
|
err = s.messengerSvc.SendSMS(ctx, user.PhoneNumber, message)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendNotificationEmail(ctx context.Context, recipientID int64, message string, subject string) error {
|
||||||
|
// Get User Phone Number
|
||||||
|
user, err := s.userSvc.GetUserByID(ctx, recipientID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.EmailVerified {
|
||||||
|
return fmt.Errorf("Cannot send notification to unverified email")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.PhoneNumber == "" {
|
||||||
|
return fmt.Errorf("Email is invalid")
|
||||||
|
}
|
||||||
|
err = s.messengerSvc.SendEmail(ctx, user.PhoneNumber, message, subject)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) startRetryWorker() {
|
||||||
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
s.retryFailedNotifications()
|
||||||
|
case <-s.stopCh:
|
||||||
|
s.mongoLogger.Info("[NotificationSvc.StartRetryWorker] Retry worker stopped",
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) retryFailedNotifications() {
|
||||||
|
ctx := context.Background()
|
||||||
|
failedNotifications, err := s.repo.ListFailedNotifications(ctx, 100)
|
||||||
|
if err != nil {
|
||||||
|
s.mongoLogger.Error("[NotificationSvc.RetryFailedNotifications] Failed to list failed notifications",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range failedNotifications {
|
||||||
|
notification := &n
|
||||||
|
go func(notification *domain.Notification) {
|
||||||
|
for attempt := 0; attempt < 3; attempt++ {
|
||||||
|
time.Sleep(time.Duration(attempt) * time.Second)
|
||||||
|
switch notification.DeliveryChannel {
|
||||||
|
case domain.DeliveryChannelSMS:
|
||||||
|
if err := s.SendNotificationSMS(ctx, notification.RecipientID, notification.Payload.Message); err == nil {
|
||||||
|
notification.DeliveryStatus = domain.DeliveryStatusSent
|
||||||
|
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
|
||||||
|
s.mongoLogger.Error("[NotificationSvc.RetryFailedNotifications] Failed to update after retry",
|
||||||
|
zap.String("id", notification.ID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
s.mongoLogger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification",
|
||||||
|
zap.String("id", notification.ID),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case domain.DeliveryChannelEmail:
|
||||||
|
if err := s.SendNotificationEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message); err == nil {
|
||||||
|
notification.DeliveryStatus = domain.DeliveryStatusSent
|
||||||
|
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
|
||||||
|
s.mongoLogger.Error("[NotificationSvc.RetryFailedNotifications] Failed to update after retry",
|
||||||
|
zap.String("id", notification.ID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
s.mongoLogger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification",
|
||||||
|
zap.String("id", notification.ID),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.mongoLogger.Error("[NotificationSvc.RetryFailedNotifications] Max retries reached for notification",
|
||||||
|
zap.String("id", notification.ID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
}(notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) {
|
||||||
|
return s.repo.CountUnreadNotifications(ctx, recipient_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (s *Service) GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error){
|
||||||
|
// return s.repo.Get(ctx, filter)
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (s *Service) RunRedisSubscriber(ctx context.Context) {
|
||||||
|
pubsub := s.redisClient.Subscribe(ctx, "live_metrics")
|
||||||
|
defer pubsub.Close()
|
||||||
|
|
||||||
|
ch := pubsub.Channel()
|
||||||
|
for msg := range ch {
|
||||||
|
var parsed map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(msg.Payload), &parsed); err != nil {
|
||||||
|
// s.logger.Error("invalid Redis message format", "payload", msg.Payload, "error", err)
|
||||||
|
s.mongoLogger.Error("invalid Redis message format",
|
||||||
|
zap.String("payload", msg.Payload),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
eventType, _ := parsed["type"].(string)
|
||||||
|
payload := parsed["payload"]
|
||||||
|
recipientID, hasRecipient := parsed["recipient_id"]
|
||||||
|
recipientType, _ := parsed["recipient_type"].(string)
|
||||||
|
|
||||||
|
message := map[string]interface{}{
|
||||||
|
"type": eventType,
|
||||||
|
"payload": payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasRecipient {
|
||||||
|
message["recipient_id"] = recipientID
|
||||||
|
message["recipient_type"] = recipientType
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Hub.Broadcast <- message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateLiveWalletMetrics(ctx context.Context, companies []domain.GetCompany, branches []domain.BranchWallet) error {
|
||||||
|
const key = "live_metrics"
|
||||||
|
|
||||||
|
companyBalances := make([]domain.CompanyWalletBalance, 0, len(companies))
|
||||||
|
for _, c := range companies {
|
||||||
|
companyBalances = append(companyBalances, domain.CompanyWalletBalance{
|
||||||
|
CompanyID: c.ID,
|
||||||
|
CompanyName: c.Name,
|
||||||
|
Balance: float64(c.WalletBalance.Float32()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
branchBalances := make([]domain.BranchWalletBalance, 0, len(branches))
|
||||||
|
for _, b := range branches {
|
||||||
|
branchBalances = append(branchBalances, domain.BranchWalletBalance{
|
||||||
|
BranchID: b.ID,
|
||||||
|
BranchName: b.Name,
|
||||||
|
CompanyID: b.CompanyID,
|
||||||
|
Balance: float64(b.Balance.Float32()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := domain.LiveWalletMetrics{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
CompanyBalances: companyBalances,
|
||||||
|
BranchBalances: branchBalances,
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedData, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.redisClient.Set(ctx, key, updatedData, 0).Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.redisClient.Publish(ctx, key, updatedData).Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error) {
|
||||||
|
const key = "live_metrics"
|
||||||
|
var metric domain.LiveMetric
|
||||||
|
|
||||||
|
val, err := s.redisClient.Get(ctx, key).Result()
|
||||||
|
if err == redis.Nil {
|
||||||
|
// Key does not exist yet, return zero-valued struct
|
||||||
|
return domain.LiveMetric{}, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return domain.LiveMetric{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(val), &metric); err != nil {
|
||||||
|
return domain.LiveMetric{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return metric, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (s *Service) UpdateLiveWalletMetricForWallet(ctx context.Context, wallet domain.Wallet) {
|
||||||
|
// var (
|
||||||
|
// payload domain.LiveWalletMetrics
|
||||||
|
// event map[string]interface{}
|
||||||
|
// key = "live_metrics"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// // Try company first
|
||||||
|
// company, companyErr := s.notificationStore.GetCompanyByWalletID(ctx, wallet.ID)
|
||||||
|
// if companyErr == nil {
|
||||||
|
// payload = domain.LiveWalletMetrics{
|
||||||
|
// Timestamp: time.Now(),
|
||||||
|
// CompanyBalances: []domain.CompanyWalletBalance{{
|
||||||
|
// CompanyID: company.ID,
|
||||||
|
// CompanyName: company.Name,
|
||||||
|
// Balance: float64(wallet.Balance),
|
||||||
|
// }},
|
||||||
|
// BranchBalances: []domain.BranchWalletBalance{},
|
||||||
|
// }
|
||||||
|
|
||||||
|
// event = map[string]interface{}{
|
||||||
|
// "type": "LIVE_WALLET_METRICS_UPDATE",
|
||||||
|
// "recipient_id": company.ID,
|
||||||
|
// "recipient_type": "company",
|
||||||
|
// "payload": payload,
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // Try branch next
|
||||||
|
// branch, branchErr := s.notificationStore.GetBranchByWalletID(ctx, wallet.ID)
|
||||||
|
// if branchErr == nil {
|
||||||
|
// payload = domain.LiveWalletMetrics{
|
||||||
|
// Timestamp: time.Now(),
|
||||||
|
// CompanyBalances: []domain.CompanyWalletBalance{},
|
||||||
|
// BranchBalances: []domain.BranchWalletBalance{{
|
||||||
|
// BranchID: branch.ID,
|
||||||
|
// BranchName: branch.Name,
|
||||||
|
// CompanyID: branch.CompanyID,
|
||||||
|
// Balance: float64(wallet.Balance),
|
||||||
|
// }},
|
||||||
|
// }
|
||||||
|
|
||||||
|
// event = map[string]interface{}{
|
||||||
|
// "type": "LIVE_WALLET_METRICS_UPDATE",
|
||||||
|
// "recipient_id": branch.ID,
|
||||||
|
// "recipient_type": "branch",
|
||||||
|
// "payload": payload,
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // Neither company nor branch matched this wallet
|
||||||
|
// // s.logger.Warn("wallet not linked to any company or branch", "walletID", wallet.ID)
|
||||||
|
// s.mongoLogger.Warn("wallet not linked to any company or branch",
|
||||||
|
// zap.Int64("walletID", wallet.ID),
|
||||||
|
// zap.Time("timestamp", time.Now()),
|
||||||
|
// )
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Save latest metric to Redis
|
||||||
|
// if jsonBytes, err := json.Marshal(payload); err == nil {
|
||||||
|
// s.redisClient.Set(ctx, key, jsonBytes, 0)
|
||||||
|
// } else {
|
||||||
|
// // s.logger.Error("failed to marshal wallet metrics payload", "walletID", wallet.ID, "err", err)
|
||||||
|
// s.mongoLogger.Error("failed to marshal wallet metrics payload",
|
||||||
|
// zap.Int64("walletID", wallet.ID),
|
||||||
|
// zap.Error(err),
|
||||||
|
// zap.Time("timestamp", time.Now()),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Publish via Redis
|
||||||
|
// if jsonEvent, err := json.Marshal(event); err == nil {
|
||||||
|
// s.redisClient.Publish(ctx, key, jsonEvent)
|
||||||
|
// } else {
|
||||||
|
// // s.logger.Error("failed to marshal event payload", "walletID", wallet.ID, "err", err)
|
||||||
|
// s.mongoLogger.Error("failed to marshal event payload",
|
||||||
|
// zap.Int64("walletID", wallet.ID),
|
||||||
|
// zap.Error(err),
|
||||||
|
// zap.Time("timestamp", time.Now()),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Broadcast over WebSocket
|
||||||
|
// s.Hub.Broadcast <- event
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
|
||||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
||||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
|
||||||
|
|
@ -2,40 +2,36 @@ package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
|
||||||
afro "github.com/amanuelabay/afrosms-go"
|
|
||||||
"github.com/resend/resend-go/v2"
|
|
||||||
"github.com/twilio/twilio-go"
|
|
||||||
twilioApi "github.com/twilio/twilio-go/rest/api/v2010"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, provider domain.OtpProvider) error {
|
func (s *Service) SendOtp(ctx context.Context, sentTo string, otpFor domain.OtpFor, medium domain.OtpMedium, provider domain.SMSProvider) error {
|
||||||
otpCode := helpers.GenerateOTP()
|
otpCode := helpers.GenerateOTP()
|
||||||
|
|
||||||
message := fmt.Sprintf("Welcome to Fortune bets, your OTP is %s please don't share with anyone.", otpCode)
|
message := fmt.Sprintf("Welcome to Fortune bets, your OTP is %s please don't share with anyone.", otpCode)
|
||||||
|
|
||||||
switch medium {
|
switch medium {
|
||||||
case domain.OtpMediumSms:
|
case domain.OtpMediumSms:
|
||||||
|
|
||||||
switch provider {
|
switch provider {
|
||||||
case domain.TwilioSms:
|
case domain.TwilioSms:
|
||||||
if err := s.SendTwilioSMSOTP(ctx, sentTo, message, provider); err != nil {
|
if err := s.messengerSvc.SendTwilioSMS(ctx, sentTo, message); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case domain.AfroMessage:
|
case domain.AfroMessage:
|
||||||
if err := s.SendAfroMessageSMSOTP(ctx, sentTo, message, provider); err != nil {
|
if err := s.messengerSvc.SendAfroMessageSMS(ctx, sentTo, message); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid sms provider: %s", provider)
|
return fmt.Errorf("invalid sms provider: %s", provider)
|
||||||
}
|
}
|
||||||
case domain.OtpMediumEmail:
|
case domain.OtpMediumEmail:
|
||||||
if err := s.SendEmailOTP(ctx, sentTo, message); err != nil {
|
if err := s.messengerSvc.SendEmail(ctx, sentTo, message, "FortuneBets - One Time Password"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,73 +57,3 @@ func hashPassword(plaintextPassword string) ([]byte, error) {
|
||||||
|
|
||||||
return hash, nil
|
return hash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SendAfroMessageSMSOTP(ctx context.Context, receiverPhone, message string, provider domain.OtpProvider) error {
|
|
||||||
apiKey := s.config.AFRO_SMS_API_KEY
|
|
||||||
senderName := s.config.AFRO_SMS_SENDER_NAME
|
|
||||||
hostURL := s.config.ADRO_SMS_HOST_URL
|
|
||||||
endpoint := "/api/send"
|
|
||||||
|
|
||||||
// API endpoint has been updated
|
|
||||||
// TODO: no need for package for the afro message operations (pretty simple stuff)
|
|
||||||
request := afro.GetRequest(apiKey, endpoint, hostURL)
|
|
||||||
request.BaseURL = "https://api.afromessage.com/api/send"
|
|
||||||
|
|
||||||
request.Method = "GET"
|
|
||||||
request.Sender(senderName)
|
|
||||||
request.To(receiverPhone, message)
|
|
||||||
|
|
||||||
response, err := afro.MakeRequestWithContext(ctx, request)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if response["acknowledge"] == "success" {
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
fmt.Println(response["response"].(map[string]interface{}))
|
|
||||||
return errors.New("SMS delivery failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SendTwilioSMSOTP(ctx context.Context, receiverPhone, message string, provider domain.OtpProvider) error {
|
|
||||||
accountSid := s.config.TwilioAccountSid
|
|
||||||
authToken := s.config.TwilioAuthToken
|
|
||||||
senderPhone := s.config.TwilioSenderPhoneNumber
|
|
||||||
|
|
||||||
client := twilio.NewRestClientWithParams(twilio.ClientParams{
|
|
||||||
Username: accountSid,
|
|
||||||
Password: authToken,
|
|
||||||
})
|
|
||||||
|
|
||||||
params := &twilioApi.CreateMessageParams{}
|
|
||||||
params.SetTo(receiverPhone)
|
|
||||||
params.SetFrom(senderPhone)
|
|
||||||
params.SetBody(message)
|
|
||||||
|
|
||||||
_, err := client.Api.CreateMessage(params)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s", "Error sending SMS message: %s"+err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SendEmailOTP(ctx context.Context, receiverEmail, message string) error {
|
|
||||||
apiKey := s.config.ResendApiKey
|
|
||||||
client := resend.NewClient(apiKey)
|
|
||||||
formattedSenderEmail := "FortuneBets <" + s.config.ResendSenderEmail + ">"
|
|
||||||
params := &resend.SendEmailRequest{
|
|
||||||
From: formattedSenderEmail,
|
|
||||||
To: []string{receiverEmail},
|
|
||||||
Subject: "FortuneBets - One Time Password",
|
|
||||||
Text: message,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := client.Emails.Send(params)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,6 @@ func (s *Service) DeleteUser(ctx context.Context, id int64) error {
|
||||||
return s.userStore.DeleteUser(ctx, id)
|
return s.userStore.DeleteUser(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func (s *Service) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) {
|
func (s *Service) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) {
|
||||||
// Get all Users
|
// Get all Users
|
||||||
return s.userStore.GetAllUsers(ctx, filter)
|
return s.userStore.GetAllUsers(ctx, filter)
|
||||||
|
|
@ -58,7 +56,10 @@ func (s *Service) GetCashiersByBranch(ctx context.Context, branchID int64) ([]do
|
||||||
return s.userStore.GetCashiersByBranch(ctx, branchID)
|
return s.userStore.GetCashiersByBranch(ctx, branchID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetAllCashiers(ctx context.Context, filter domain.UserFilter) ([]domain.GetCashier, int64, error){
|
func (s *Service) GetAdminByCompanyID(ctx context.Context, companyID int64) (domain.User, error) {
|
||||||
|
return s.userStore.GetAdminByCompanyID(ctx, companyID)
|
||||||
|
}
|
||||||
|
func (s *Service) GetAllCashiers(ctx context.Context, filter domain.UserFilter) ([]domain.GetCashier, int64, error) {
|
||||||
return s.userStore.GetAllCashiers(ctx, filter)
|
return s.userStore.GetAllCashiers(ctx, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ type UserStore interface {
|
||||||
GetAllCashiers(ctx context.Context, filter domain.UserFilter) ([]domain.GetCashier, int64, error)
|
GetAllCashiers(ctx context.Context, filter domain.UserFilter) ([]domain.GetCashier, int64, error)
|
||||||
GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, error)
|
GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, error)
|
||||||
GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error)
|
GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error)
|
||||||
|
GetAdminByCompanyID(ctx context.Context, companyID int64) (domain.User, error)
|
||||||
UpdateUser(ctx context.Context, user domain.UpdateUserReq) error
|
UpdateUser(ctx context.Context, user domain.UpdateUserReq) error
|
||||||
UpdateUserCompany(ctx context.Context, id int64, companyID int64) error
|
UpdateUserCompany(ctx context.Context, id int64, companyID int64) error
|
||||||
UpdateUserSuspend(ctx context.Context, id int64, status bool) error
|
UpdateUserSuspend(ctx context.Context, id int64, status bool) error
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error
|
func (s *Service) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) { // email,phone,error
|
||||||
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email)
|
return s.userStore.CheckPhoneEmailExist(ctx, phoneNum, email)
|
||||||
}
|
}
|
||||||
func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.OtpProvider) error {
|
func (s *Service) SendRegisterCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider) error {
|
||||||
var err error
|
var err error
|
||||||
// check if user exists
|
// check if user exists
|
||||||
switch medium {
|
switch medium {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.OtpProvider) error {
|
func (s *Service) SendResetCode(ctx context.Context, medium domain.OtpMedium, sentTo string, provider domain.SMSProvider) error {
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
// check if user exists
|
// check if user exists
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/messenger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -11,19 +12,22 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
userStore UserStore
|
userStore UserStore
|
||||||
otpStore OtpStore
|
otpStore OtpStore
|
||||||
config *config.Config
|
messengerSvc *messenger.Service
|
||||||
|
config *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
userStore UserStore,
|
userStore UserStore,
|
||||||
otpStore OtpStore,
|
otpStore OtpStore,
|
||||||
|
messengerSvc *messenger.Service,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
userStore: userStore,
|
userStore: userStore,
|
||||||
otpStore: otpStore,
|
otpStore: otpStore,
|
||||||
config: cfg,
|
messengerSvc: messengerSvc,
|
||||||
|
config: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,7 +254,7 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (
|
||||||
return &domain.PopOKBetResponse{}, fmt.Errorf("Failed to read user wallets")
|
return &domain.PopOKBetResponse{}, fmt.Errorf("Failed to read user wallets")
|
||||||
}
|
}
|
||||||
_, err = s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents),
|
_, err = s.walletSvc.DeductFromWallet(ctx, claims.UserID, domain.Currency(amountCents),
|
||||||
domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT,
|
domain.ValidInt64{}, domain.TRANSFER_DIRECT,
|
||||||
fmt.Sprintf("Deducted %v amount from wallet by system while placing virtual game bet", amountCents))
|
fmt.Sprintf("Deducted %v amount from wallet by system while placing virtual game bet", amountCents))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("insufficient balance")
|
return nil, fmt.Errorf("insufficient balance")
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ func (c *Client) ProcessBet(ctx context.Context, req domain.BetRequest) (*domain
|
||||||
return &domain.BetResponse{}, err
|
return &domain.BetResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.walletSvc.DeductFromWallet(ctx, wallets[0].ID, domain.Currency(req.Amount.Amount), domain.CustomerWalletType, domain.ValidInt64{}, domain.TRANSFER_DIRECT,
|
c.walletSvc.DeductFromWallet(ctx, wallets[0].ID, domain.Currency(req.Amount.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT,
|
||||||
fmt.Sprintf("Deducting %v from wallet for creating Veli Game Bet", req.Amount.Amount),
|
fmt.Sprintf("Deducting %v from wallet for creating Veli Game Bet", req.Amount.Amount),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
|
||||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type WalletStore interface {
|
type WalletStore interface {
|
||||||
// GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error)
|
GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error)
|
||||||
// GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error)
|
GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error)
|
||||||
CreateWallet(ctx context.Context, wallet domain.CreateWallet) (domain.Wallet, error)
|
CreateWallet(ctx context.Context, wallet domain.CreateWallet) (domain.Wallet, error)
|
||||||
CreateCustomerWallet(ctx context.Context, customerWallet domain.CreateCustomerWallet) (domain.CustomerWallet, error)
|
CreateCustomerWallet(ctx context.Context, customerWallet domain.CreateCustomerWallet) (domain.CustomerWallet, error)
|
||||||
GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error)
|
GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ package wallet
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
|
@ -12,17 +14,29 @@ type Service struct {
|
||||||
transferStore TransferStore
|
transferStore TransferStore
|
||||||
notificationStore notificationservice.NotificationStore
|
notificationStore notificationservice.NotificationStore
|
||||||
notificationSvc *notificationservice.Service
|
notificationSvc *notificationservice.Service
|
||||||
|
userSvc *user.Service
|
||||||
|
mongoLogger *zap.Logger
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
// userStore user.UserStore
|
// userStore user.UserStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(walletStore WalletStore, transferStore TransferStore, notificationStore notificationservice.NotificationStore, notificationSvc *notificationservice.Service, logger *slog.Logger) *Service {
|
func NewService(
|
||||||
|
walletStore WalletStore,
|
||||||
|
transferStore TransferStore,
|
||||||
|
notificationStore notificationservice.NotificationStore,
|
||||||
|
notificationSvc *notificationservice.Service,
|
||||||
|
userSvc *user.Service,
|
||||||
|
mongoLogger *zap.Logger,
|
||||||
|
logger *slog.Logger,
|
||||||
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
walletStore: walletStore,
|
walletStore: walletStore,
|
||||||
transferStore: transferStore,
|
transferStore: transferStore,
|
||||||
// approvalStore: approvalStore,
|
// approvalStore: approvalStore,
|
||||||
notificationStore: notificationStore,
|
notificationStore: notificationStore,
|
||||||
notificationSvc: notificationSvc,
|
notificationSvc: notificationSvc,
|
||||||
|
userSvc: userSvc,
|
||||||
|
mongoLogger: mongoLogger,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
// userStore: userStore,
|
// userStore: userStore,
|
||||||
// userStore users
|
// userStore users
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -59,6 +61,14 @@ func (s *Service) GetWalletsByUser(ctx context.Context, id int64) ([]domain.Wall
|
||||||
return s.walletStore.GetWalletsByUser(ctx, id)
|
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) {
|
func (s *Service) GetAllCustomerWallet(ctx context.Context) ([]domain.GetCustomerWallet, error) {
|
||||||
return s.walletStore.GetAllCustomerWallets(ctx)
|
return s.walletStore.GetAllCustomerWallets(ctx)
|
||||||
}
|
}
|
||||||
|
|
@ -76,12 +86,12 @@ func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Cu
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
wallet, err := s.GetWalletByID(ctx, id)
|
_, err = s.GetWalletByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
|
// go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,7 +127,7 @@ func (s *Service) AddToWallet(
|
||||||
return newTransfer, err
|
return newTransfer, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.Currency, walletType domain.WalletType, cashierID domain.ValidInt64, paymentMethod domain.PaymentMethod, message string) (domain.Transfer, error) {
|
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)
|
wallet, err := s.GetWalletByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Transfer{}, err
|
return domain.Transfer{}, err
|
||||||
|
|
@ -125,8 +135,10 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
|
||||||
|
|
||||||
if wallet.Balance < amount {
|
if wallet.Balance < amount {
|
||||||
// Send Wallet low to admin
|
// Send Wallet low to admin
|
||||||
if walletType == domain.CompanyWalletType || walletType == domain.BranchWalletType {
|
if wallet.Type == domain.CompanyWalletType || wallet.Type == domain.BranchWalletType {
|
||||||
s.SendAdminWalletInsufficientNotification(ctx, wallet, amount)
|
s.SendAdminWalletInsufficientNotification(ctx, wallet, amount)
|
||||||
|
} else {
|
||||||
|
s.SendCustomerWalletInsufficientNotification(ctx, wallet, amount)
|
||||||
}
|
}
|
||||||
return domain.Transfer{}, ErrBalanceInsufficient
|
return domain.Transfer{}, ErrBalanceInsufficient
|
||||||
}
|
}
|
||||||
|
|
@ -215,6 +227,55 @@ func (s *Service) UpdateWalletActive(ctx context.Context, id int64, isActive boo
|
||||||
return s.walletStore.UpdateWalletActive(ctx, id, isActive)
|
return s.walletStore.UpdateWalletActive(ctx, id, isActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetAdminNotificationRecipients(ctx context.Context, walletID int64, walletType domain.WalletType) ([]int64, error) {
|
||||||
|
var recipients []int64
|
||||||
|
|
||||||
|
if walletType == 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)
|
||||||
|
|
||||||
|
} else if walletType == domain.CompanyWalletType {
|
||||||
|
company, err := s.GetCompanyByWalletID(ctx, walletID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
recipients = append(recipients, company.AdminID)
|
||||||
|
} else {
|
||||||
|
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 {
|
func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWallet domain.Wallet) error {
|
||||||
// Send notification to admin team
|
// Send notification to admin team
|
||||||
adminNotification := &domain.Notification{
|
adminNotification := &domain.Notification{
|
||||||
|
|
@ -222,7 +283,7 @@ func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWalle
|
||||||
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
|
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
|
||||||
Level: domain.NotificationLevelWarning,
|
Level: domain.NotificationLevelWarning,
|
||||||
Reciever: domain.NotificationRecieverSideAdmin,
|
Reciever: domain.NotificationRecieverSideAdmin,
|
||||||
DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel
|
DeliveryChannel: domain.DeliveryChannelInApp, // Or any preferred admin channel
|
||||||
Payload: domain.NotificationPayload{
|
Payload: domain.NotificationPayload{
|
||||||
Headline: "CREDIT WARNING: System Running Out of Funds",
|
Headline: "CREDIT WARNING: System Running Out of Funds",
|
||||||
Message: fmt.Sprintf(
|
Message: fmt.Sprintf(
|
||||||
|
|
@ -240,35 +301,48 @@ func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWalle
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get admin recipients and send to all
|
// Get admin recipients and send to all
|
||||||
adminRecipients, err := s.notificationStore.ListRecipientIDs(ctx, domain.NotificationRecieverSideAdmin)
|
adminRecipients, err := s.GetAdminNotificationRecipients(ctx, adminWallet.ID, adminWallet.Type)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("failed to get admin recipients", "error", err)
|
s.mongoLogger.Error("failed to get admin recipients",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return err
|
return err
|
||||||
} else {
|
}
|
||||||
for _, adminID := range adminRecipients {
|
|
||||||
adminNotification.RecipientID = adminID
|
for _, adminID := range adminRecipients {
|
||||||
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
|
adminNotification.RecipientID = adminID
|
||||||
s.logger.Error("failed to send admin notification",
|
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
|
||||||
"admin_id", adminID,
|
s.mongoLogger.Error("failed to send admin notification",
|
||||||
"error", err)
|
zap.Int64("admin_id", adminID),
|
||||||
}
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
adminNotification.DeliveryChannel = domain.DeliveryChannelEmail
|
||||||
|
|
||||||
|
if err := s.notificationStore.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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, adminWallet domain.Wallet, amount domain.Currency) error {
|
func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, adminWallet domain.Wallet, amount domain.Currency) error {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Send notification to admin team
|
// Send notification to admin team
|
||||||
adminNotification := &domain.Notification{
|
adminNotification := &domain.Notification{
|
||||||
RecipientID: adminWallet.UserID,
|
RecipientID: adminWallet.UserID,
|
||||||
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
|
Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT,
|
||||||
Level: domain.NotificationLevelError,
|
Level: domain.NotificationLevelError,
|
||||||
Reciever: domain.NotificationRecieverSideAdmin,
|
Reciever: domain.NotificationRecieverSideAdmin,
|
||||||
DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel
|
DeliveryChannel: domain.DeliveryChannelInApp, // Or any preferred admin channel
|
||||||
Payload: domain.NotificationPayload{
|
Payload: domain.NotificationPayload{
|
||||||
Headline: "CREDIT Error: Admin Wallet insufficient to process customer request",
|
Headline: "CREDIT Error: Admin Wallet insufficient to process customer request",
|
||||||
Message: fmt.Sprintf(
|
Message: fmt.Sprintf(
|
||||||
|
|
@ -288,33 +362,49 @@ func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, a
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get admin recipients and send to all
|
// Get admin recipients and send to all
|
||||||
adminRecipients, err := s.notificationStore.ListRecipientIDs(ctx, domain.NotificationRecieverSideAdmin)
|
|
||||||
|
recipients, err := s.GetAdminNotificationRecipients(ctx, adminWallet.ID, adminWallet.Type)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("failed to get admin recipients", "error", err)
|
s.mongoLogger.Error("failed to get admin recipients",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return err
|
return err
|
||||||
} else {
|
}
|
||||||
for _, adminID := range adminRecipients {
|
for _, adminID := range recipients {
|
||||||
adminNotification.RecipientID = adminID
|
adminNotification.RecipientID = adminID
|
||||||
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
|
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
|
||||||
s.logger.Error("failed to send admin notification",
|
s.mongoLogger.Error("failed to send admin notification",
|
||||||
"admin_id", adminID,
|
zap.Int64("admin_id", adminID),
|
||||||
"error", err)
|
zap.Error(err),
|
||||||
}
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
adminNotification.DeliveryChannel = domain.DeliveryChannelEmail
|
||||||
|
|
||||||
|
if err := s.notificationStore.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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SendCustomerWalletInsufficientNotification(ctx context.Context, customerWallet domain.Wallet, amount domain.Currency) error {
|
func (s *Service) SendCustomerWalletInsufficientNotification(ctx context.Context, customerWallet domain.Wallet, amount domain.Currency) error {
|
||||||
// Send notification to admin team
|
// Send notification to admin team
|
||||||
adminNotification := &domain.Notification{
|
customerNotification := &domain.Notification{
|
||||||
RecipientID: customerWallet.UserID,
|
RecipientID: customerWallet.UserID,
|
||||||
Type: domain.NOTIFICATION_TYPE_WALLET,
|
Type: domain.NOTIFICATION_TYPE_WALLET,
|
||||||
Level: domain.NotificationLevelError,
|
Level: domain.NotificationLevelError,
|
||||||
Reciever: domain.NotificationRecieverSideAdmin,
|
Reciever: domain.NotificationRecieverSideCustomer,
|
||||||
DeliveryChannel: domain.DeliveryChannelEmail, // Or any preferred admin channel
|
DeliveryChannel: domain.DeliveryChannelInApp, // Or any preferred admin channel
|
||||||
Payload: domain.NotificationPayload{
|
Payload: domain.NotificationPayload{
|
||||||
Headline: "CREDIT Error: Admin Wallet insufficient to process customer request",
|
Headline: "CREDIT Error: Wallet insufficient",
|
||||||
Message: fmt.Sprintf(
|
Message: fmt.Sprintf(
|
||||||
"Wallet ID %d. Transaction Amount %.2f. Current balance: %.2f",
|
"Wallet ID %d. Transaction Amount %.2f. Current balance: %.2f",
|
||||||
customerWallet.ID,
|
customerWallet.ID,
|
||||||
|
|
@ -331,10 +421,14 @@ func (s *Service) SendCustomerWalletInsufficientNotification(ctx context.Context
|
||||||
}`, customerWallet.ID, customerWallet.Balance, amount.Float32()),
|
}`, customerWallet.ID, customerWallet.Balance, amount.Float32()),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.notificationStore.SendNotification(ctx, adminNotification); err != nil {
|
if err := s.notificationStore.SendNotification(ctx, customerNotification); err != nil {
|
||||||
s.logger.Error("failed to send customer notification",
|
s.mongoLogger.Error("failed to create customer notification",
|
||||||
"admin_id", customerWallet.UserID,
|
zap.Int64("customer_id", customerWallet.UserID),
|
||||||
"error", err)
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import (
|
||||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions"
|
||||||
issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting"
|
issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/league"
|
||||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation"
|
||||||
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
|
referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal"
|
||||||
|
|
@ -58,13 +58,13 @@ type Handler struct {
|
||||||
virtualGameSvc virtualgameservice.VirtualGameService
|
virtualGameSvc virtualgameservice.VirtualGameService
|
||||||
aleaVirtualGameSvc alea.AleaVirtualGameService
|
aleaVirtualGameSvc alea.AleaVirtualGameService
|
||||||
veliVirtualGameSvc veli.VeliVirtualGameService
|
veliVirtualGameSvc veli.VeliVirtualGameService
|
||||||
recommendationSvc recommendation.RecommendationService
|
recommendationSvc recommendation.RecommendationService
|
||||||
authSvc *authentication.Service
|
authSvc *authentication.Service
|
||||||
resultSvc result.Service
|
resultSvc result.Service
|
||||||
jwtConfig jwtutil.JwtConfig
|
jwtConfig jwtutil.JwtConfig
|
||||||
validator *customvalidator.CustomValidator
|
validator *customvalidator.CustomValidator
|
||||||
Cfg *config.Config
|
Cfg *config.Config
|
||||||
mongoLoggerSvc *zap.Logger
|
mongoLoggerSvc *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(
|
func New(
|
||||||
|
|
@ -124,11 +124,11 @@ func New(
|
||||||
virtualGameSvc: virtualGameSvc,
|
virtualGameSvc: virtualGameSvc,
|
||||||
aleaVirtualGameSvc: aleaVirtualGameSvc,
|
aleaVirtualGameSvc: aleaVirtualGameSvc,
|
||||||
veliVirtualGameSvc: veliVirtualGameSvc,
|
veliVirtualGameSvc: veliVirtualGameSvc,
|
||||||
recommendationSvc: recommendationSvc,
|
recommendationSvc: recommendationSvc,
|
||||||
authSvc: authSvc,
|
authSvc: authSvc,
|
||||||
resultSvc: resultSvc,
|
resultSvc: resultSvc,
|
||||||
jwtConfig: jwtConfig,
|
jwtConfig: jwtConfig,
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
mongoLoggerSvc: mongoLoggerSvc,
|
mongoLoggerSvc: mongoLoggerSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user