2025-11-05 15:30:53 +00:00
|
|
|
package services
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
|
|
|
|
|
"attune-heart-therapy/internal/config"
|
2025-11-05 16:35:36 +00:00
|
|
|
"attune-heart-therapy/internal/models"
|
|
|
|
|
"attune-heart-therapy/internal/repositories"
|
2025-11-05 15:30:53 +00:00
|
|
|
|
|
|
|
|
"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 {
|
2025-11-05 16:35:36 +00:00
|
|
|
config *config.Config
|
|
|
|
|
bookingRepo repositories.BookingRepository
|
|
|
|
|
userRepo repositories.UserRepository
|
|
|
|
|
notificationService NotificationService
|
2025-11-05 15:30:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewPaymentService creates a new instance of PaymentService
|
2025-11-05 16:35:36 +00:00
|
|
|
func NewPaymentService(cfg *config.Config, bookingRepo repositories.BookingRepository, userRepo repositories.UserRepository, notificationService NotificationService) PaymentService {
|
2025-11-05 15:30:53 +00:00
|
|
|
// Set Stripe API key
|
|
|
|
|
stripe.Key = cfg.Stripe.SecretKey
|
|
|
|
|
|
|
|
|
|
return &paymentService{
|
2025-11-05 16:35:36 +00:00
|
|
|
config: cfg,
|
|
|
|
|
bookingRepo: bookingRepo,
|
|
|
|
|
userRepo: userRepo,
|
|
|
|
|
notificationService: notificationService,
|
2025-11-05 15:30:53 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)
|
2025-11-05 16:35:36 +00:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
2025-11-05 15:30:53 +00:00
|
|
|
|
|
|
|
|
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)
|
2025-11-05 16:35:36 +00:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
2025-11-05 15:30:53 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2025-11-05 16:35:36 +00:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|