- Add Go modules with required dependencies (Gin, UUID, JWT, etc.) - Implement main web server with landing page endpoint - Add comprehensive API endpoints for health and status - Include proper error handling and request validation - Set up CORS middleware and security headers
386 lines
12 KiB
Go
386 lines
12 KiB
Go
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
|
|
} |