386 lines
12 KiB
Go
386 lines
12 KiB
Go
package wallet
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
|
)
|
|
|
|
var (
|
|
ErrSenderWalletNotTransferable = errors.New("sender wallet is not transferable")
|
|
ErrReceiverWalletNotTransferable = errors.New("receiver wallet is not transferable")
|
|
ErrInsufficientBalance = errors.New("wallet balance is insufficient")
|
|
)
|
|
|
|
func (s *Service) CreateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) {
|
|
// This is just a transfer log when
|
|
return s.transferStore.CreateTransfer(ctx, transfer)
|
|
}
|
|
|
|
func (s *Service) GetAllTransfers(ctx context.Context) ([]domain.TransferDetail, error) {
|
|
return s.transferStore.GetAllTransfers(ctx)
|
|
}
|
|
|
|
func (s *Service) GetTransferByReference(ctx context.Context, reference string) (domain.TransferDetail, error) {
|
|
return s.transferStore.GetTransferByReference(ctx, reference)
|
|
}
|
|
|
|
func (s *Service) GetTransferByID(ctx context.Context, id int64) (domain.TransferDetail, error) {
|
|
return s.transferStore.GetTransferByID(ctx, id)
|
|
}
|
|
|
|
func (s *Service) GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.TransferDetail, error) {
|
|
return s.transferStore.GetTransfersByWallet(ctx, walletID)
|
|
}
|
|
|
|
func (s *Service) UpdateTransferVerification(ctx context.Context, id int64, verified bool) error {
|
|
return s.transferStore.UpdateTransferVerification(ctx, id, verified)
|
|
}
|
|
|
|
func (s *Service) UpdateTransferStatus(ctx context.Context, id int64, status string) error {
|
|
return s.transferStore.UpdateTransferStatus(ctx, id, status)
|
|
}
|
|
|
|
func (s *Service) TransferToWallet(ctx context.Context, senderID int64, receiverID int64,
|
|
amount domain.Currency, paymentMethod domain.PaymentMethod,
|
|
cashierID domain.ValidInt64, message string) (domain.Transfer, error) {
|
|
|
|
senderWallet, err := s.GetWalletByID(ctx, senderID)
|
|
if err != nil {
|
|
|
|
return domain.Transfer{}, err
|
|
}
|
|
|
|
if !senderWallet.IsTransferable {
|
|
fmt.Printf("Error: %d Sender Wallet is not transferable \n", senderWallet.ID)
|
|
return domain.Transfer{}, ErrSenderWalletNotTransferable
|
|
}
|
|
|
|
receiverWallet, err := s.GetWalletByID(ctx, receiverID)
|
|
if err != nil {
|
|
return domain.Transfer{}, err
|
|
}
|
|
|
|
if !receiverWallet.IsTransferable {
|
|
fmt.Printf("Error: %d Receiver Wallet is not transferable \n", senderWallet.ID)
|
|
return domain.Transfer{}, ErrReceiverWalletNotTransferable
|
|
}
|
|
|
|
// Deduct from sender
|
|
if senderWallet.Balance < amount {
|
|
return domain.Transfer{}, ErrBalanceInsufficient
|
|
}
|
|
|
|
err = s.walletStore.UpdateBalance(ctx, senderID, senderWallet.Balance-amount)
|
|
|
|
if err != nil {
|
|
return domain.Transfer{}, err
|
|
}
|
|
|
|
// Add to receiver
|
|
err = s.walletStore.UpdateBalance(ctx, receiverID, receiverWallet.Balance+amount)
|
|
|
|
if err != nil {
|
|
return domain.Transfer{}, err
|
|
}
|
|
|
|
// Log the transfer so that if there is a mistake, it can be reverted
|
|
transfer, err := s.CreateTransfer(ctx, domain.CreateTransfer{
|
|
Message: message,
|
|
SenderWalletID: domain.ValidInt64{
|
|
Value: senderID,
|
|
Valid: true,
|
|
},
|
|
ReceiverWalletID: domain.ValidInt64{
|
|
Value: receiverID,
|
|
Valid: true,
|
|
},
|
|
CashierID: cashierID,
|
|
Amount: amount,
|
|
Type: domain.WALLET,
|
|
PaymentMethod: paymentMethod,
|
|
Verified: true,
|
|
})
|
|
if err != nil {
|
|
return domain.Transfer{}, err
|
|
}
|
|
|
|
return transfer, nil
|
|
}
|
|
|
|
func (s *Service) SendTransferNotification(ctx context.Context, senderWallet domain.Wallet, receiverWallet domain.Wallet,
|
|
senderRole domain.Role, receiverRole domain.Role, amount domain.Currency) error {
|
|
// Send notification to sender (this could be any role) that money was transferred
|
|
senderWalletReceiverSide := domain.ReceiverFromRole(senderRole)
|
|
|
|
senderNotify := &domain.Notification{
|
|
RecipientID: senderWallet.UserID,
|
|
Type: domain.NOTIFICATION_TYPE_TRANSFER_SUCCESS,
|
|
Level: domain.NotificationLevelSuccess,
|
|
Reciever: senderWalletReceiverSide,
|
|
DeliveryChannel: domain.DeliveryChannelInApp,
|
|
Payload: domain.NotificationPayload{
|
|
Headline: "Wallet has been deducted",
|
|
Message: fmt.Sprintf(`%s %d has been transferred from your wallet`, senderWallet.Currency, amount),
|
|
},
|
|
Priority: 2,
|
|
Metadata: []byte(fmt.Sprintf(`{
|
|
"transfer_amount": %d,
|
|
"current_balance": %d,
|
|
"wallet_id": %d,
|
|
"notification_type": "customer_facing"
|
|
}`, amount, senderWallet.Balance, senderWallet.ID)),
|
|
}
|
|
|
|
// Sender notifications
|
|
if err := s.notificationSvc.SendNotification(ctx, senderNotify); err != nil {
|
|
s.logger.Error("failed to send sender notification",
|
|
"user_id", "",
|
|
"error", err)
|
|
return err
|
|
}
|
|
|
|
receiverWalletReceiverSide := domain.ReceiverFromRole(receiverRole)
|
|
|
|
receiverNotify := &domain.Notification{
|
|
RecipientID: receiverWallet.UserID,
|
|
Type: domain.NOTIFICATION_TYPE_TRANSFER_SUCCESS,
|
|
Level: domain.NotificationLevelSuccess,
|
|
Reciever: receiverWalletReceiverSide,
|
|
DeliveryChannel: domain.DeliveryChannelInApp,
|
|
Payload: domain.NotificationPayload{
|
|
Headline: "Wallet has been credited",
|
|
Message: fmt.Sprintf(`%s %d has been transferred to your wallet`, receiverWallet.Currency, amount),
|
|
},
|
|
Priority: 2,
|
|
Metadata: []byte(fmt.Sprintf(`{
|
|
"transfer_amount": %d,
|
|
"current_balance": %d,
|
|
"wallet_id": %d,
|
|
"notification_type": "customer_facing"
|
|
}`, amount, receiverWallet.Balance, receiverWallet.ID)),
|
|
}
|
|
// Sender notifications
|
|
if err := s.notificationSvc.SendNotification(ctx, receiverNotify); err != nil {
|
|
s.logger.Error("failed to send sender notification",
|
|
"user_id", "",
|
|
"error", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error {
|
|
return nil
|
|
}
|
|
|
|
/////////////////////////////////Transaction Make-Cheker/////////////////////////////////////////////////
|
|
|
|
// func (s *Service) InitiateTransfer(ctx context.Context, transfer domain.CreateTransfer) (domain.Transfer, error) {
|
|
// // Set transfer as unverified by default
|
|
// transfer.Verified = false
|
|
|
|
// // Create the transfer record
|
|
// newTransfer, err := s.transferStore.CreateTransfer(ctx, transfer)
|
|
// if err != nil {
|
|
// return domain.Transfer{}, err
|
|
// }
|
|
|
|
// // Create approval record
|
|
// approval := domain.TransactionApproval{
|
|
// TransferID: newTransfer.ID,
|
|
// Status: "pending",
|
|
// }
|
|
|
|
// if err := s.approvalStore.CreateApproval(ctx, approval); err != nil {
|
|
// return domain.Transfer{}, err
|
|
// }
|
|
|
|
// // Notify approvers
|
|
// go s.notifyApprovers(ctx, newTransfer.ID)
|
|
|
|
// return newTransfer, nil
|
|
// }
|
|
|
|
// func (s *Service) ApproveTransfer(ctx context.Context, approval domain.ApprovalRequest) error {
|
|
// // Get the transfer
|
|
// transfer, err := s.transferStore.GetTransferByID(ctx, approval.TransferID)
|
|
// if err != nil {
|
|
// return err
|
|
// }
|
|
|
|
// // Only allow approval of pending transfers
|
|
// if transfer.Status != "pending" {
|
|
// return errors.New("only pending transfers can be approved")
|
|
// }
|
|
|
|
// // Update approval record
|
|
// if err := s.approvalStore.UpdateApprovalStatus(ctx, approval.TransferID, "approved", approval.Comments); err != nil {
|
|
// return err
|
|
// }
|
|
|
|
// // Execute the actual transfer
|
|
// if transfer.Type == domain.WALLET {
|
|
// _, err = s.executeWalletTransfer(ctx, transfer)
|
|
// return err
|
|
// }
|
|
|
|
// // For other transfer types, implement similar execution logic
|
|
|
|
// return nil
|
|
// }
|
|
|
|
// func (s *Service) executeWalletTransfer(ctx context.Context, transfer domain.TransferDetail) (domain.Transfer, error) {
|
|
// // Original transfer logic from TransferToWallet, but now guaranteed to be approved
|
|
// senderWallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID.Value)
|
|
// if err != nil {
|
|
// return domain.Transfer{}, err
|
|
// }
|
|
|
|
// receiverWallet, err := s.GetWalletByID(ctx, transfer.ReceiverWalletID.Value)
|
|
// if err != nil {
|
|
// return domain.Transfer{}, err
|
|
// }
|
|
|
|
// // Deduct from sender
|
|
// if senderWallet.Balance < transfer.Amount {
|
|
// return domain.Transfer{}, ErrBalanceInsufficient
|
|
// }
|
|
|
|
// err = s.walletStore.UpdateBalance(ctx, transfer.SenderWalletID.Value, senderWallet.Balance-transfer.Amount)
|
|
// if err != nil {
|
|
// return domain.Transfer{}, err
|
|
// }
|
|
|
|
// // Add to receiver
|
|
// err = s.walletStore.UpdateBalance(ctx, transfer.ReceiverWalletID.Value, receiverWallet.Balance+transfer.Amount)
|
|
// if err != nil {
|
|
// return domain.Transfer{}, err
|
|
// }
|
|
|
|
// // Mark transfer as completed
|
|
// if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, "completed"); err != nil {
|
|
// return domain.Transfer{}, err
|
|
// }
|
|
|
|
// // Send notifications
|
|
// go s.SendTransferNotification(ctx, senderWallet, receiverWallet,
|
|
// domain.RoleFromString(transfer.SenderWalletID.Value),
|
|
// domain.RoleFromString(transfer.ReceiverWalletID.Value),
|
|
// transfer.Amount)
|
|
|
|
// return transfer, nil
|
|
// }
|
|
|
|
// func (s *Service) RejectTransfer(ctx context.Context, approval domain.ApprovalRequest) error {
|
|
// // Update approval record
|
|
// if err := s.approvalStore.UpdateApprovalStatus(ctx, approval.TransferID, "rejected", approval.Comments); err != nil {
|
|
// return err
|
|
// }
|
|
|
|
// // Update transfer status
|
|
// if err := s.transferStore.UpdateTransferStatus(ctx, approval.TransferID, "rejected"); err != nil {
|
|
// return err
|
|
// }
|
|
|
|
// // Notify the initiator
|
|
// go s.notifyInitiator(ctx, approval.TransferID, approval.Comments)
|
|
|
|
// return nil
|
|
// }
|
|
|
|
// func (s *Service) GetPendingApprovals(ctx context.Context) ([]domain.TransferDetail, error) {
|
|
// return s.approvalStore.GetPendingApprovals(ctx)
|
|
// }
|
|
|
|
// func (s *Service) GetTransferApprovalHistory(ctx context.Context, transferID int64) ([]domain.TransactionApproval, error) {
|
|
// return s.approvalStore.GetApprovalsByTransfer(ctx, transferID)
|
|
// }
|
|
|
|
// func (s *Service) notifyApprovers(ctx context.Context, transferID int64) {
|
|
// // Get approvers (could be from a config or role-based)
|
|
// approvers, err := s.userStore.GetUsersByRole(ctx, "approver")
|
|
// if err != nil {
|
|
// s.logger.Error("failed to get approvers", zap.Error(err))
|
|
// return
|
|
// }
|
|
|
|
// transfer, err := s.transferStore.GetTransferByID(ctx, transferID)
|
|
// if err != nil {
|
|
// s.logger.Error("failed to get transfer for notification", zap.Error(err))
|
|
// return
|
|
// }
|
|
|
|
// for _, approver := range approvers {
|
|
// notification := &domain.Notification{
|
|
// RecipientID: approver.ID,
|
|
// Type: domain.NOTIFICATION_TYPE_APPROVAL_REQUIRED,
|
|
// Level: domain.NotificationLevelWarning,
|
|
// Reciever: domain.NotificationRecieverSideAdmin,
|
|
// DeliveryChannel: domain.DeliveryChannelInApp,
|
|
// Payload: domain.NotificationPayload{
|
|
// Headline: "Transfer Approval Required",
|
|
// Message: fmt.Sprintf("Transfer #%d requires your approval", transfer.ID),
|
|
// },
|
|
// Priority: 1,
|
|
// Metadata: []byte(fmt.Sprintf(`{
|
|
// "transfer_id": %d,
|
|
// "amount": %d,
|
|
// "notification_type": "approval_request"
|
|
// }`, transfer.ID, transfer.Amount)),
|
|
// }
|
|
|
|
// if err := s.notificationStore.SendNotification(ctx, notification); err != nil {
|
|
// s.logger.Error("failed to send approval notification",
|
|
// zap.Int64("approver_id", approver.ID),
|
|
// zap.Error(err))
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// func (s *Service) notifyInitiator(ctx context.Context, transferID int64, comments string) {
|
|
// transfer, err := s.transferStore.GetTransferByID(ctx, transferID)
|
|
// if err != nil {
|
|
// s.logger.Error("failed to get transfer for notification", zap.Error(err))
|
|
// return
|
|
// }
|
|
|
|
// // Determine who initiated the transfer (could be cashier or user)
|
|
// recipientID := transfer.CashierID.Value
|
|
// if !transfer.CashierID.Valid {
|
|
// // If no cashier, assume user initiated
|
|
// wallet, err := s.GetWalletByID(ctx, transfer.SenderWalletID.Value)
|
|
// if err != nil {
|
|
// s.logger.Error("failed to get wallet for notification", zap.Error(err))
|
|
// return
|
|
// }
|
|
// recipientID = wallet.UserID
|
|
// }
|
|
|
|
// notification := &domain.Notification{
|
|
// RecipientID: recipientID,
|
|
// Type: domain.NOTIFICATION_TYPE_TRANSFER_REJECTED,
|
|
// Level: domain.NotificationLevelWarning,
|
|
// Reciever: domain.NotificationRecieverSideCustomer,
|
|
// DeliveryChannel: domain.DeliveryChannelInApp,
|
|
// Payload: domain.NotificationPayload{
|
|
// Headline: "Transfer Rejected",
|
|
// Message: fmt.Sprintf("Your transfer #%d was rejected. Comments: %s", transfer.ID, comments),
|
|
// },
|
|
// Priority: 2,
|
|
// Metadata: []byte(fmt.Sprintf(`{
|
|
// "transfer_id": %d,
|
|
// "notification_type": "transfer_rejected"
|
|
// }`, transfer.ID)),
|
|
// }
|
|
|
|
// if err := s.notificationStore.SendNotification(ctx, notification); err != nil {
|
|
// s.logger.Error("failed to send rejection notification",
|
|
// zap.Int64("recipient_id", recipientID),
|
|
// zap.Error(err))
|
|
// }
|
|
// }
|