package services import ( "encoding/json" "fmt" "log" "attune-heart-therapy/internal/config" "attune-heart-therapy/internal/models" "attune-heart-therapy/internal/repositories" "github.com/stripe/stripe-go/v76" "github.com/stripe/stripe-go/v76/paymentintent" "github.com/stripe/stripe-go/v76/webhook" ) // paymentService implements the PaymentService interface type paymentService struct { config *config.Config bookingRepo repositories.BookingRepository userRepo repositories.UserRepository notificationService NotificationService } // NewPaymentService creates a new instance of PaymentService func NewPaymentService(cfg *config.Config, bookingRepo repositories.BookingRepository, userRepo repositories.UserRepository, notificationService NotificationService) PaymentService { // Set Stripe API key stripe.Key = cfg.Stripe.SecretKey return &paymentService{ config: cfg, bookingRepo: bookingRepo, userRepo: userRepo, notificationService: notificationService, } } // CreatePaymentIntent creates a new Stripe payment intent for payment initialization func (s *paymentService) CreatePaymentIntent(amount float64, currency string) (*stripe.PaymentIntent, error) { if amount <= 0 { return nil, fmt.Errorf("amount must be greater than 0") } if currency == "" { currency = "usd" // Default to USD } // Convert amount to cents (Stripe expects amounts in smallest currency unit) amountCents := int64(amount * 100) params := &stripe.PaymentIntentParams{ Amount: stripe.Int64(amountCents), Currency: stripe.String(currency), AutomaticPaymentMethods: &stripe.PaymentIntentAutomaticPaymentMethodsParams{ Enabled: stripe.Bool(true), }, } pi, err := paymentintent.New(params) if err != nil { log.Printf("Failed to create payment intent: %v", err) return nil, fmt.Errorf("failed to create payment intent: %w", err) } return pi, nil } // ConfirmPayment confirms a payment intent for payment completion func (s *paymentService) ConfirmPayment(paymentIntentID string) (*stripe.PaymentIntent, error) { if paymentIntentID == "" { return nil, fmt.Errorf("payment intent ID is required") } params := &stripe.PaymentIntentConfirmParams{} pi, err := paymentintent.Confirm(paymentIntentID, params) if err != nil { log.Printf("Failed to confirm payment intent %s: %v", paymentIntentID, err) return nil, fmt.Errorf("failed to confirm payment: %w", err) } return pi, nil } // HandleWebhook processes Stripe webhook events for webhook processing func (s *paymentService) HandleWebhook(payload []byte, signature string) error { if len(payload) == 0 { return fmt.Errorf("webhook payload is empty") } if signature == "" { return fmt.Errorf("webhook signature is required") } // Verify webhook signature event, err := webhook.ConstructEvent(payload, signature, s.config.Stripe.WebhookSecret) if err != nil { log.Printf("Failed to verify webhook signature: %v", err) return fmt.Errorf("failed to verify webhook signature: %w", err) } // Handle different event types switch event.Type { case "payment_intent.succeeded": var paymentIntent stripe.PaymentIntent objectBytes, err := json.Marshal(event.Data.Object) if err != nil { log.Printf("Failed to marshal event data: %v", err) return fmt.Errorf("failed to parse event data: %w", err) } if err := json.Unmarshal(objectBytes, &paymentIntent); err != nil { log.Printf("Failed to unmarshal payment intent: %v", err) return fmt.Errorf("failed to parse payment intent: %w", err) } log.Printf("Payment succeeded for payment intent: %s", paymentIntent.ID) // Find booking by payment ID and update status if err := s.handlePaymentSuccess(paymentIntent.ID); err != nil { log.Printf("Failed to handle payment success for %s: %v", paymentIntent.ID, err) } case "payment_intent.payment_failed": var paymentIntent stripe.PaymentIntent objectBytes, err := json.Marshal(event.Data.Object) if err != nil { log.Printf("Failed to marshal event data: %v", err) return fmt.Errorf("failed to parse event data: %w", err) } if err := json.Unmarshal(objectBytes, &paymentIntent); err != nil { log.Printf("Failed to unmarshal payment intent: %v", err) return fmt.Errorf("failed to parse payment intent: %w", err) } log.Printf("Payment failed for payment intent: %s", paymentIntent.ID) // Find booking by payment ID and update status if err := s.handlePaymentFailure(paymentIntent.ID); err != nil { log.Printf("Failed to handle payment failure for %s: %v", paymentIntent.ID, err) } case "payment_intent.canceled": var paymentIntent stripe.PaymentIntent objectBytes, err := json.Marshal(event.Data.Object) if err != nil { log.Printf("Failed to marshal event data: %v", err) return fmt.Errorf("failed to parse event data: %w", err) } if err := json.Unmarshal(objectBytes, &paymentIntent); err != nil { log.Printf("Failed to unmarshal payment intent: %v", err) return fmt.Errorf("failed to parse payment intent: %w", err) } log.Printf("Payment canceled for payment intent: %s", paymentIntent.ID) // TODO: Update booking status to canceled // This will be handled when booking service is integrated default: log.Printf("Unhandled webhook event type: %s", event.Type) } return nil } // handlePaymentSuccess processes successful payment and sends notifications func (s *paymentService) handlePaymentSuccess(paymentIntentID string) error { // Find booking by payment ID booking, err := s.bookingRepo.GetByPaymentID(paymentIntentID) if err != nil { return fmt.Errorf("failed to find booking for payment %s: %w", paymentIntentID, err) } // Update booking payment status booking.PaymentStatus = models.PaymentStatusSucceeded if err := s.bookingRepo.Update(booking); err != nil { return fmt.Errorf("failed to update booking payment status: %w", err) } // Get user for notification user, err := s.userRepo.GetByID(booking.UserID) if err != nil { return fmt.Errorf("failed to get user for notification: %w", err) } // Send payment success notification if err := s.notificationService.SendPaymentNotification(user, booking, true); err != nil { log.Printf("Failed to send payment success notification: %v", err) // Don't return error as payment processing was successful } return nil } // handlePaymentFailure processes failed payment and sends notifications func (s *paymentService) handlePaymentFailure(paymentIntentID string) error { // Find booking by payment ID booking, err := s.bookingRepo.GetByPaymentID(paymentIntentID) if err != nil { return fmt.Errorf("failed to find booking for payment %s: %w", paymentIntentID, err) } // Update booking payment status booking.PaymentStatus = models.PaymentStatusFailed if err := s.bookingRepo.Update(booking); err != nil { return fmt.Errorf("failed to update booking payment status: %w", err) } // Get user for notification user, err := s.userRepo.GetByID(booking.UserID) if err != nil { return fmt.Errorf("failed to get user for notification: %w", err) } // Send payment failure notification if err := s.notificationService.SendPaymentNotification(user, booking, false); err != nil { log.Printf("Failed to send payment failure notification: %v", err) // Don't return error as the main payment processing was handled } return nil }