diff --git a/internal/services/arifpay/service.go b/internal/services/arifpay/service.go index 3b75f7d..a85e63b 100644 --- a/internal/services/arifpay/service.go +++ b/internal/services/arifpay/service.go @@ -596,10 +596,62 @@ func (s *ArifpayService) InitiateDirectPayment(ctx context.Context, userID int64 return nil, fmt.Errorf("failed to create payment record: %w", err) } - // Create ArifPay checkout session with specific payment method - checkoutReq := domain.CheckoutSessionRequest{ + phone := formatPhone(req.Phone) + checkoutReq := s.buildDirectCheckoutRequest(req, phone, nonce, expiresAt, plan) + + var ( + sessionID string + directResp string + ) + + if req.PaymentMethod == domain.DirectPaymentTelebirrUSSD { + // TELEBIRR_USSD uses a direct proxy endpoint with full checkout payload. + sessionID, directResp, err = s.initiateTelebirrUSSDDirect(ctx, checkoutReq) + } else { + sessionID, err = s.createCheckoutSessionForDirect(ctx, checkoutReq) + if err == nil { + directResp, err = s.initiateDirectTransfer(ctx, sessionID, phone, req.PaymentMethod) + } + } + if err != nil { + // Update payment status to failed + s.paymentStore.UpdatePaymentStatus(ctx, payment.ID, string(domain.PaymentStatusFailed)) + return nil, fmt.Errorf("failed to initiate direct transfer: %w", err) + } + + // Update payment with session info + if err := s.paymentStore.UpdatePaymentSessionID(ctx, payment.ID, sessionID, ""); err != nil { + return nil, fmt.Errorf("failed to update payment session: %w", err) + } + + requiresOTP := s.paymentMethodRequiresOTP(req.PaymentMethod) + message := "Payment initiated" + if requiresOTP { + message = "OTP sent to your phone. Please verify to complete payment." + } else { + message = directResp + } + + return &domain.InitiateDirectPaymentResponse{ + PaymentID: payment.ID, + SessionID: sessionID, + RequiresOTP: requiresOTP, + Message: message, + Amount: plan.Price, + Currency: plan.Currency, + }, nil +} + +func (s *ArifpayService) buildDirectCheckoutRequest( + req domain.InitiateDirectPaymentRequest, + phone string, + nonce string, + expiresAt time.Time, + plan *domain.SubscriptionPlan, +) domain.CheckoutSessionRequest { + return domain.CheckoutSessionRequest{ CancelURL: s.cfg.ARIFPAY.CancelUrl, - Phone: formatPhone(req.Phone), + Phone: phone, Email: req.Email, Nonce: nonce, SuccessURL: s.cfg.ARIFPAY.SuccessUrl, @@ -633,77 +685,101 @@ func (s *ArifpayService) InitiateDirectPayment(ctx context.Context, userID int64 }, Lang: s.cfg.ARIFPAY.Lang, } +} - // Create session +func (s *ArifpayService) createCheckoutSessionForDirect(ctx context.Context, checkoutReq domain.CheckoutSessionRequest) (string, error) { payload, err := json.Marshal(checkoutReq) if err != nil { - return nil, fmt.Errorf("failed to marshal checkout request: %w", err) + return "", fmt.Errorf("failed to marshal checkout request: %w", err) } url := fmt.Sprintf("%s/api/checkout/session", s.cfg.ARIFPAY.BaseURL) - httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(payload)) + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(payload)) if err != nil { - return nil, err + return "", err } httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey) resp, err := s.httpClient.Do(httpReq) if err != nil { - return nil, fmt.Errorf("failed to call ArifPay API: %w", err) + return "", fmt.Errorf("failed to call ArifPay API: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return nil, err + return "", err } - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("ArifPay API error: %s", string(body)) + return "", fmt.Errorf("ArifPay API error: %s", string(body)) } var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("invalid response from ArifPay: %w", err) + return "", fmt.Errorf("invalid response from ArifPay: %w", err) } data, ok := result["data"].(map[string]interface{}) if !ok { - return nil, errors.New("invalid response structure from ArifPay") + return "", errors.New("invalid response structure from ArifPay") } - sessionID := fmt.Sprintf("%v", data["sessionId"]) + return fmt.Sprintf("%v", data["sessionId"]), nil +} - // Update payment with session info - if err := s.paymentStore.UpdatePaymentSessionID(ctx, payment.ID, sessionID, ""); err != nil { - return nil, fmt.Errorf("failed to update payment session: %w", err) - } - - // Now initiate direct transfer based on payment method - directResp, err := s.initiateDirectTransfer(ctx, sessionID, formatPhone(req.Phone), req.PaymentMethod) +func (s *ArifpayService) initiateTelebirrUSSDDirect(ctx context.Context, checkoutReq domain.CheckoutSessionRequest) (string, string, error) { + payload, err := json.Marshal(checkoutReq) if err != nil { - // Update payment status to failed - s.paymentStore.UpdatePaymentStatus(ctx, payment.ID, string(domain.PaymentStatusFailed)) - return nil, fmt.Errorf("failed to initiate direct transfer: %w", err) + return "", "", fmt.Errorf("failed to marshal telebirr ussd request: %w", err) } - requiresOTP := s.paymentMethodRequiresOTP(req.PaymentMethod) - message := "Payment initiated" - if requiresOTP { - message = "OTP sent to your phone. Please verify to complete payment." - } else { - message = directResp + endpoint := fmt.Sprintf("%s/api/checkout/telebirr-ussd/transfer/direct", s.cfg.ARIFPAY.BaseURL) + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(payload)) + if err != nil { + return "", "", err + } + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("x-arifpay-key", s.cfg.ARIFPAY.APIKey) + + resp, err := s.httpClient.Do(httpReq) + if err != nil { + return "", "", fmt.Errorf("failed to call telebirr ussd direct API: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", "", err + } + if resp.StatusCode != http.StatusOK { + return "", "", fmt.Errorf("telebirr ussd direct failed: %s", string(body)) } - return &domain.InitiateDirectPaymentResponse{ - PaymentID: payment.ID, - SessionID: sessionID, - RequiresOTP: requiresOTP, - Message: message, - Amount: plan.Price, - Currency: plan.Currency, - }, nil + var result struct { + Msg string `json:"msg"` + Data struct { + SessionID interface{} `json:"sessionId"` + URL string `json:"url"` + } `json:"data"` + } + if err := json.Unmarshal(body, &result); err != nil { + return "", "", fmt.Errorf("invalid telebirr ussd response: %w", err) + } + + sessionID := fmt.Sprintf("%v", result.Data.SessionID) + if sessionID == "" || sessionID == "" { + sessionID = checkoutReq.Nonce + } + message := result.Msg + if message == "" { + message = "USSD transfer initiated" + } + if result.Data.URL != "" { + message = result.Data.URL + } + + return sessionID, message, nil } // initiateDirectTransfer calls the appropriate direct transfer endpoint