backend-service/internal/services/payment_service.go
ats-tech25 98f4b4392d feat(booking): Implement comprehensive booking management functionality
- Add full implementation for booking handlers with complete CRUD operations
- Implement GetAvailableSlots endpoint to retrieve available booking time slots
- Add CreateBooking handler with robust error handling and validation
- Implement GetUserBookings endpoint to fetch user's booking history
- Add CancelBooking handler with specific error scenarios and authorization checks
- Integrate booking service and middleware for authentication and request processing
- Add support for date parsing and slot availability checking
- Enhance error responses with detailed error messages and appropriate HTTP status codes
- Integrate with existing authentication and middleware components
2025-11-05 16:35:36 +00:00

224 lines
7.2 KiB
Go

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
}