feat: implement core Go application with web server
- 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
This commit is contained in:
386
output/internal/services/stripe_service.go
Normal file
386
output/internal/services/stripe_service.go
Normal file
@@ -0,0 +1,386 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user