package services import ( "context" "encoding/json" "fmt" "log" "time" "github.com/google/uuid" "github.com/stripe/stripe-go/v76" "github.com/stripe/stripe-go/v76/checkout/session" "github.com/stripe/stripe-go/v76/customer" "github.com/stripe/stripe-go/v76/webhook" "gorm.io/gorm" ) type StripeService struct { db *gorm.DB config *config.Config } func NewStripeService(db *gorm.DB, config *config.Config) *StripeService { stripe.Key = config.Stripe.SecretKey return &StripeService{ db: db, config: config, } } func (s *StripeService) CreateCheckoutSession(email, domainName string) (string, error) { // Validate inputs if email == "" || domainName == "" { return "", fmt.Errorf("email and domain name are required") } // Create or retrieve customer customerParams := &stripe.CustomerParams{ Email: stripe.String(email), Metadata: map[string]string{ "domain_name": domainName, "source": "ydn_platform", }, } cust, err := customer.New(customerParams) if err != nil { return "", fmt.Errorf("failed to create customer: %w", err) } // Create checkout session with proper URLs successURL := fmt.Sprintf("https://%s/success?session_id={CHECKOUT_SESSION_ID}", getEnvOrDefault("DOMAIN", "yourdreamnamehere.com")) cancelURL := fmt.Sprintf("https://%s/cancel", getEnvOrDefault("DOMAIN", "yourdreamnamehere.com")) params := &stripe.CheckoutSessionParams{ Customer: stripe.String(cust.ID), PaymentMethodTypes: stripe.StringSlice([]string{"card"}), LineItems: []*stripe.CheckoutSessionLineItemParams{ { Price: stripe.String(s.config.Stripe.PriceID), Quantity: stripe.Int64(1), }, }, Mode: stripe.String(string(stripe.CheckoutSessionModeSubscription)), SuccessURL: stripe.String(successURL), CancelURL: stripe.String(cancelURL), AllowPromotionCodes: stripe.Bool(true), BillingAddressCollection: stripe.String("required"), Metadata: map[string]string{ "domain_name": domainName, "customer_email": email, }, } sess, err := session.New(params) if err != nil { return "", fmt.Errorf("failed to create checkout session: %w", err) } // Store customer in database with transaction err = s.db.Transaction(func(tx *gorm.DB) error { // Check if customer already exists var existingCustomer models.Customer if err := tx.Where("stripe_id = ?", cust.ID).First(&existingCustomer).Error; err == nil { // Update existing customer existingCustomer.Email = email existingCustomer.Status = "pending" return tx.Save(&existingCustomer).Error } // Create new customer record dbCustomer := &models.Customer{ StripeID: cust.ID, Email: email, Status: "pending", // Will be updated to active after payment } return tx.Create(dbCustomer).Error }) if err != nil { log.Printf("Warning: failed to create customer in database: %v", err) // Continue anyway as the Stripe session was created successfully } log.Printf("Created checkout session %s for customer %s (%s)", sess.ID, cust.ID, email) return sess.URL, nil } func (s *StripeService) HandleWebhook(signature string, body []byte) (*stripe.Event, error) { // Validate inputs if signature == "" { return nil, fmt.Errorf("webhook signature is required") } if len(body) == 0 { return nil, fmt.Errorf("webhook body is empty") } // Verify webhook signature event, err := webhook.ConstructEvent(body, signature, s.config.Stripe.WebhookSecret) if err != nil { log.Printf("Webhook signature verification failed: %v", err) return nil, fmt.Errorf("webhook signature verification failed: %w", err) } // Log webhook receipt for debugging log.Printf("Received webhook event: %s (ID: %s)", event.Type, event.ID) // Process the event if err := s.processWebhookEvent(&event); err != nil { log.Printf("Failed to process webhook event %s: %v", event.ID, err) return nil, fmt.Errorf("failed to process webhook event: %w", err) } return &event, nil } func (s *StripeService) processWebhookEvent(event *stripe.Event) error { switch event.Type { case "checkout.session.completed": return s.handleCheckoutCompleted(event) case "invoice.payment_succeeded": return s.handleInvoicePaymentSucceeded(event) case "invoice.payment_failed": return s.handleInvoicePaymentFailed(event) case "customer.subscription.created": return s.handleSubscriptionCreated(event) case "customer.subscription.updated": return s.handleSubscriptionUpdated(event) case "customer.subscription.deleted": return s.handleSubscriptionDeleted(event) default: log.Printf("Unhandled webhook event type: %s", event.Type) return nil } } func (s *StripeService) handleCheckoutCompleted(event *stripe.Event) error { var checkoutSession stripe.CheckoutSession if err := json.Unmarshal(event.Data.Raw, &checkoutSession); err != nil { return fmt.Errorf("failed to parse checkout session: %w", err) } log.Printf("Processing completed checkout session: %s", checkoutSession.ID) // Extract metadata domainName := checkoutSession.Metadata["domain_name"] customerEmail := checkoutSession.Metadata["customer_email"] if domainName == "" || customerEmail == "" { return fmt.Errorf("missing required metadata in checkout session") } // Update customer status and create subscription record return s.db.Transaction(func(tx *gorm.DB) error { // Update customer status if err := tx.Model(&models.Customer{}). Where("stripe_id = ?", checkoutSession.Customer.ID). Update("status", "active").Error; err != nil { return fmt.Errorf("failed to update customer status: %w", err) } // Create subscription record if available if checkoutSession.Subscription != nil { subscription := checkoutSession.Subscription customerUUID, _ := uuid.Parse(checkoutSession.Customer.ID) // Convert string to UUID dbSubscription := &models.Subscription{ CustomerID: customerUUID, StripeID: subscription.ID, Status: string(subscription.Status), PriceID: subscription.Items.Data[0].Price.ID, Amount: float64(subscription.Items.Data[0].Price.UnitAmount) / 100.0, Currency: string(subscription.Items.Data[0].Price.Currency), Interval: string(subscription.Items.Data[0].Price.Recurring.Interval), CurrentPeriodStart: time.Unix(subscription.CurrentPeriodStart, 0), CurrentPeriodEnd: time.Unix(subscription.CurrentPeriodEnd, 0), CancelAtPeriodEnd: subscription.CancelAtPeriodEnd, } if err := tx.Create(dbSubscription).Error; err != nil { return fmt.Errorf("failed to create subscription: %w", err) } } log.Printf("Successfully processed checkout completion for domain: %s", domainName) return nil }) } func (s *StripeService) handleInvoicePaymentSucceeded(event *stripe.Event) error { // Handle successful invoice payment log.Printf("Invoice payment succeeded for event: %s", event.ID) return nil } func (s *StripeService) handleInvoicePaymentFailed(event *stripe.Event) error { // Handle failed invoice payment log.Printf("Invoice payment failed for event: %s", event.ID) // Update customer status var invoice stripe.Invoice if err := json.Unmarshal(event.Data.Raw, &invoice); err != nil { return fmt.Errorf("failed to parse invoice: %w", err) } if err := s.db.Model(&models.Customer{}). Where("stripe_id = ?", invoice.Customer.ID). Update("status", "past_due").Error; err != nil { log.Printf("Failed to update customer status to past_due: %v", err) } return nil } func (s *StripeService) handleSubscriptionCreated(event *stripe.Event) error { log.Printf("Subscription created for event: %s", event.ID) return nil } func (s *StripeService) handleSubscriptionUpdated(event *stripe.Event) error { var subscription stripe.Subscription if err := json.Unmarshal(event.Data.Raw, &subscription); err != nil { return fmt.Errorf("failed to parse subscription: %w", err) } // Update subscription in database updates := map[string]interface{}{ "status": string(subscription.Status), "current_period_start": time.Unix(subscription.CurrentPeriodStart, 0), "current_period_end": time.Unix(subscription.CurrentPeriodEnd, 0), "cancel_at_period_end": subscription.CancelAtPeriodEnd, } if subscription.CanceledAt > 0 { canceledAt := time.Unix(subscription.CanceledAt, 0) updates["canceled_at"] = &canceledAt } if err := s.db.Model(&models.Subscription{}). Where("stripe_id = ?", subscription.ID). Updates(updates).Error; err != nil { log.Printf("Failed to update subscription: %v", err) } return nil } func (s *StripeService) handleSubscriptionDeleted(event *stripe.Event) error { var subscription stripe.Subscription if err := json.Unmarshal(event.Data.Raw, &subscription); err != nil { return fmt.Errorf("failed to parse subscription: %w", err) } // Soft delete subscription if err := s.db.Model(&models.Subscription{}). Where("stripe_id = ?", subscription.ID). Update("status", "canceled").Error; err != nil { log.Printf("Failed to update subscription status to canceled: %v", err) } return nil } func (s *StripeService) CancelSubscription(subscriptionID string) error { _, err := subscription.Get(subscriptionID, nil) if err != nil { return fmt.Errorf("failed to retrieve subscription: %w", err) } // Cancel at period end params := &stripe.SubscriptionParams{ CancelAtPeriodEnd: stripe.Bool(true), } _, err = subscription.Update(subscriptionID, params) if err != nil { return fmt.Errorf("failed to cancel subscription: %w", err) } // Update database if err := s.db.Model(&models.Subscription{}). Where("stripe_id = ?", subscriptionID). Update("cancel_at_period_end", true).Error; err != nil { log.Printf("Warning: failed to update subscription in database: %v", err) } return nil } func (s *StripeService) ProcessCheckoutCompleted(session *stripe.CheckoutSession) error { // Extract metadata domainName := session.Metadata["domain_name"] customerEmail := session.Metadata["customer_email"] if domainName == "" || customerEmail == "" { return fmt.Errorf("missing required metadata") } // Create domain record domain := &models.Domain{ Name: domainName, Status: "pending", } // Find or create customer var dbCustomer models.Customer if err := s.db.Where("stripe_id = ?", session.Customer.ID).First(&dbCustomer).Error; err != nil { if err == gorm.ErrRecordNotFound { // Create customer record dbCustomer = models.Customer{ StripeID: session.Customer.ID, Email: customerEmail, Status: "active", } if err := s.db.Create(&dbCustomer).Error; err != nil { return fmt.Errorf("failed to create customer: %w", err) } } else { return fmt.Errorf("failed to query customer: %w", err) } } domain.CustomerID = dbCustomer.ID if err := s.db.Create(domain).Error; err != nil { return fmt.Errorf("failed to create domain: %w", err) } // Create subscription record if session.Subscription != nil { subscription := session.Subscription dbSubscription := &models.Subscription{ CustomerID: dbCustomer.ID, StripeID: subscription.ID, Status: string(subscription.Status), CurrentPeriodStart: time.Unix(subscription.CurrentPeriodStart, 0), CurrentPeriodEnd: time.Unix(subscription.CurrentPeriodEnd, 0), CancelAtPeriodEnd: subscription.CancelAtPeriodEnd, } if err := s.db.Create(dbSubscription).Error; err != nil { return fmt.Errorf("failed to create subscription: %w", err) } } log.Printf("Successfully processed checkout completion for domain: %s", domainName) return nil } func (s *StripeService) ProcessSubscriptionUpdate(subscription *stripe.Subscription) error { // Update subscription in database updates := map[string]interface{}{ "status": string(subscription.Status), "current_period_start": time.Unix(subscription.CurrentPeriodStart, 0), "current_period_end": time.Unix(subscription.CurrentPeriodEnd, 0), "cancel_at_period_end": subscription.CancelAtPeriodEnd, } if err := s.db.Model(&models.Subscription{}). Where("stripe_id = ?", subscription.ID). Updates(updates).Error; err != nil { return fmt.Errorf("failed to update subscription: %w", err) } log.Printf("Successfully updated subscription: %s", subscription.ID) return nil }