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:
YourDreamNameHere
2025-11-20 16:36:28 -05:00
parent aa93326897
commit 89443f213b
57 changed files with 14404 additions and 0 deletions

View 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
}