package santimpay import ( "bytes" "context" "encoding/json" "fmt" "net/http" "strconv" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/google/uuid" ) // type SantimPayService interface { // GeneratePaymentURL(req domain.GeneratePaymentURLreq) (map[string]string, error) // } type SantimPayService struct { client SantimPayClient cfg *config.Config transferStore wallet.TransferStore walletSvc *wallet.Service } func NewSantimPayService(client SantimPayClient, cfg *config.Config, transferStore wallet.TransferStore, walletSvc *wallet.Service) *SantimPayService { return &SantimPayService{ client: client, cfg: cfg, transferStore: transferStore, walletSvc: walletSvc, } } func (s *SantimPayService) InitiatePayment(req domain.GeneratePaymentURLRequest) (map[string]string, error) { paymentID := uuid.NewString() tokenPayload := domain.SantimTokenPayload{ Amount: req.Amount, Reason: req.Reason, } // 1. Generate signed token (used as Bearer token in headers) token, err := s.client.GenerateSignedToken(tokenPayload) if err != nil { return nil, fmt.Errorf("token generation failed: %w", err) } // 2. Prepare payload (without token in body) payload := domain.InitiatePaymentRequest{ ID: paymentID, Amount: req.Amount, Reason: req.Reason, MerchantID: s.cfg.SANTIMPAY.MerchantID, SuccessRedirectURL: s.cfg.SANTIMPAY.SuccessUrl, FailureRedirectURL: s.cfg.SANTIMPAY.CancelUrl, NotifyURL: s.cfg.SANTIMPAY.NotifyURL, CancelRedirectURL: s.cfg.SANTIMPAY.CancelUrl, PhoneNumber: req.PhoneNumber, SignedToken: token, } jsonData, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("failed to marshal payload: %w", err) } // 3. Prepare request with Bearer token header httpReq, err := http.NewRequest("POST", s.cfg.SANTIMPAY.BaseURL+"/gateway/initiate-payment", bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create HTTP request: %w", err) } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+token) client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(httpReq) if err != nil { return nil, fmt.Errorf("failed to send HTTP request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("non-200 status code received: %d", resp.StatusCode) } var responseBody map[string]string if err := json.NewDecoder(resp.Body).Decode(&responseBody); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } // 4. Save transfer transfer := domain.CreateTransfer{ Amount: domain.Currency(req.Amount), Verified: false, Type: domain.DEPOSIT, ReferenceNumber: paymentID, Status: string(domain.PaymentStatusPending), } if _, err := s.transferStore.CreateTransfer(context.Background(), transfer); err != nil { return nil, fmt.Errorf("failed to create transfer: %w", err) } // 5. Optionally check transaction status asynchronously // go s.client.CheckTransactionStatus(paymentID) return responseBody, nil } func (s *SantimPayService) ProcessCallback(ctx context.Context, payload domain.SantimPayCallbackPayload) error { // 1. Parse amount amount, err := strconv.ParseFloat(payload.Amount, 64) if err != nil { return fmt.Errorf("invalid amount in callback: %w", err) } // 2. Retrieve the corresponding transfer by txnId or refId transfer, err := s.transferStore.GetTransferByReference(ctx, payload.TxnId) if err != nil { return fmt.Errorf("failed to fetch transfer for txnId %s: %w", payload.TxnId, err) } // 3. Update transfer status based on callback status switch payload.Status { case "COMPLETED": transfer.Status = string(domain.PaymentStatusSuccessful) transfer.Verified = true userID, err := strconv.ParseInt(payload.ThirdPartyId, 10, 64) if err != nil { return fmt.Errorf("invalid ThirdPartyId '%s': %w", payload.ThirdPartyId, err) } wallet, err := s.walletSvc.GetCustomerWallet(ctx, userID) if err != nil { return fmt.Errorf("failed to get wallets for customer %d: %w", userID, err) } // Optionally, credit user wallet if transfer.Type == domain.DEPOSIT { if _, err := s.walletSvc.AddToWallet( ctx, wallet.RegularID, domain.Currency(amount), domain.ValidInt64{}, domain.TRANSFER_SANTIMPAY, domain.PaymentDetails{ ReferenceNumber: domain.ValidString{ Value: payload.TxnId, Valid: true, }, BankNumber: domain.ValidString{}, }, "", ); err != nil { return fmt.Errorf("failed to credit wallet: %w", err) } } case "FAILED", "CANCELLED": transfer.Status = string(domain.PaymentStatusFailed) transfer.Verified = false default: // Unknown status return fmt.Errorf("unknown callback status: %s", payload.Status) } // 4. Save the updated transfer if err := s.transferStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.PaymentStatusCompleted)); err != nil { return fmt.Errorf("failed to update transfer status: %w", err) } if err := s.transferStore.UpdateTransferVerification(ctx, transfer.ID, true); err != nil { return fmt.Errorf("failed to update transfer verification: %w", err) } return nil } func (s *SantimPayService) ProcessDirectPayment(ctx context.Context, req domain.GeneratePaymentURLRequest) (map[string]any, error) { paymentID := uuid.NewString() tokenPayload := domain.SantimTokenPayload{ Amount: req.Amount, Reason: req.Reason, PaymentMethod: req.PaymentMethod, PhoneNumber: req.PhoneNumber, } // 1. Generate signed token for direct payment token, err := s.client.GenerateSignedToken(tokenPayload) if err != nil { return nil, fmt.Errorf("failed to generate signed token: %w", err) } // 2. Build payload payload := domain.InitiatePaymentRequest{ ID: paymentID, Amount: req.Amount, Reason: req.Reason, MerchantID: s.cfg.SANTIMPAY.MerchantID, SignedToken: token, PhoneNumber: req.PhoneNumber, NotifyURL: s.cfg.SANTIMPAY.NotifyURL, PaymentMethod: req.PaymentMethod, } jsonData, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("failed to marshal payload: %w", err) } // 3. Prepare HTTP request httpReq, err := http.NewRequest("POST", s.cfg.SANTIMPAY.BaseURL+"/direct-payment", bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create HTTP request: %w", err) } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+token) client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(httpReq) if err != nil { return nil, fmt.Errorf("failed to send HTTP request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("non-200 status code received: %d", resp.StatusCode) } // 4. Decode response var responseBody map[string]any if err := json.NewDecoder(resp.Body).Decode(&responseBody); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } // 5. Save transfer in DB transfer := domain.CreateTransfer{ Amount: domain.Currency(req.Amount), Verified: false, Type: domain.DEPOSIT, ReferenceNumber: paymentID, Status: string(domain.PaymentStatusPending), } if _, err := s.transferStore.CreateTransfer(context.Background(), transfer); err != nil { return nil, fmt.Errorf("failed to create transfer: %w", err) } // 6. Optionally check transaction status async // go s.client.CheckTransactionStatus(paymentID) return responseBody, nil } func (s *SantimPayService) GetB2CPartners(ctx context.Context) (*domain.B2CPartnersResponse, error) { url := fmt.Sprintf("%s/api/v1/gateway/payout/partners", s.cfg.SANTIMPAY.BaseURL) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } HTTPClient := &http.Client{Timeout: 15 * time.Second} resp, err := HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to call SantimPay API: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } var partnersResp domain.B2CPartnersResponse if err := json.NewDecoder(resp.Body).Decode(&partnersResp); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &partnersResp, nil } func (s *SantimPayService) ProcessB2CWithdrawal(ctx context.Context, req domain.GeneratePaymentURLRequest, userId int64) (map[string]any, error) { transactID := uuid.NewString() // 1. Generate signed token for B2C tokenPayload := domain.SantimTokenPayload{ Amount: req.Amount, Reason: req.Reason, PaymentMethod: req.PaymentMethod, PhoneNumber: req.PhoneNumber, } signedToken, err := s.client.GenerateSignedToken(tokenPayload) if err != nil { return nil, fmt.Errorf("failed to generate signed token for B2C: %w", err) } // 2. Build payload payload := domain.SantimpayB2CWithdrawalRequest{ ID: transactID, ClientReference: string(rune(userId)), Amount: float64(req.Amount), Reason: req.Reason, MerchantID: s.cfg.SANTIMPAY.MerchantID, SignedToken: signedToken, ReceiverAccountNumber: req.PhoneNumber, NotifyURL: s.cfg.SANTIMPAY.NotifyURL, PaymentMethod: req.PaymentMethod, } jsonData, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("failed to marshal B2C payload: %w", err) } // 3. Send HTTP request url := s.cfg.SANTIMPAY.BaseURL + "/payout-transfer" httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create B2C request: %w", err) } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+signedToken) client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(httpReq) if err != nil { return nil, fmt.Errorf("failed to send B2C request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("B2C request failed with status code: %d", resp.StatusCode) } // 4. Decode response var responseBody map[string]any if err := json.NewDecoder(resp.Body).Decode(&responseBody); err != nil { return nil, fmt.Errorf("failed to decode B2C response: %w", err) } // 5. Persist withdrawal record in DB withdrawal := domain.CreateTransfer{ Amount: domain.Currency(req.Amount), Verified: false, Type: domain.WITHDRAW, ReferenceNumber: transactID, Status: string(domain.PaymentStatusPending), } if _, err := s.transferStore.CreateTransfer(context.Background(), withdrawal); err != nil { return nil, fmt.Errorf("failed to create withdrawal transfer: %w", err) } return responseBody, nil } func (s *SantimPayService) CheckTransactionStatus(ctx context.Context, req domain.TransactionStatusRequest) (map[string]any, error) { // 1. Generate signed token for status check tokenPayload := domain.SantimTokenPayload{ ID: req.TransactionID, } signedToken, err := s.client.GenerateSignedToken(tokenPayload) if err != nil { return nil, fmt.Errorf("failed to generate signed token for transaction status: %w", err) } // 2. Build request payload payload := map[string]any{ "id": req.TransactionID, "merchantId": s.cfg.SANTIMPAY.MerchantID, "signedToken": signedToken, "fullParams": req.FullParams, "generated": time.Now().Unix(), } jsonData, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("failed to marshal transaction status payload: %w", err) } // 3. Send HTTP request url := s.cfg.SANTIMPAY.BaseURL + "/fetch-transaction-status" httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("failed to create transaction status request: %w", err) } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+signedToken) client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(httpReq) if err != nil { return nil, fmt.Errorf("failed to send transaction status request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("transaction status request failed with status code: %d", resp.StatusCode) } // 4. Decode response var responseBody map[string]any if err := json.NewDecoder(resp.Body).Decode(&responseBody); err != nil { return nil, fmt.Errorf("failed to decode transaction status response: %w", err) } return responseBody, nil }