package services import ( "database/sql" "encoding/json" "errors" "fmt" "log" "os" "github.com/stripe/stripe-go/v76" "github.com/stripe/stripe-go/v76/checkout/session" "github.com/stripe/stripe-go/v76/webhook" ) type SubscriptionService struct { DB *sql.DB } func NewSubscriptionService(db *sql.DB) *SubscriptionService { // Initialize Stripe stripe.Key = os.Getenv("STRIPE_SECRET_KEY") return &SubscriptionService{DB: db} } // CreateCheckoutSession создает сессию checkout для подписки func (s *SubscriptionService) CreateCheckoutSession(companyID int, planID string, userEmail string) (string, error) { // Define price ID based on plan var priceID string switch planID { case "professional": priceID = os.Getenv("STRIPE_PRICE_PROFESSIONAL") case "enterprise": priceID = os.Getenv("STRIPE_PRICE_ENTERPRISE") default: // starter priceID = os.Getenv("STRIPE_PRICE_STARTER") } if priceID == "" { // Fallback for demo/development if envs are missing // In production this should error out if planID == "starter" { return "", errors.New("starter plan is free") } return "", fmt.Errorf("price id not configured for plan %s", planID) } frontendURL := os.Getenv("FRONTEND_URL") if frontendURL == "" { frontendURL = "http://localhost:8963" } params := &stripe.CheckoutSessionParams{ CustomerEmail: stripe.String(userEmail), PaymentMethodTypes: stripe.StringSlice([]string{ "card", }), LineItems: []*stripe.CheckoutSessionLineItemParams{ { Price: stripe.String(priceID), Quantity: stripe.Int64(1), }, }, Mode: stripe.String(string(stripe.CheckoutSessionModeSubscription)), SuccessURL: stripe.String(frontendURL + "/dashboard?payment=success&session_id={CHECKOUT_SESSION_ID}"), CancelURL: stripe.String(frontendURL + "/dashboard?payment=cancelled"), Metadata: map[string]string{ "company_id": fmt.Sprintf("%d", companyID), "plan_id": planID, }, } // Add Pix if configured (usually requires BRL currency and specialized setup) // checking if we should add it dynamically or just rely on Stripe Dashboard settings // For API 2022-11-15+ payment_method_types is often inferred from dashboard // but adding it explicitly if we want to force it. // Note: Pix for subscriptions might have limitations. // Standard approach: use "card" and "boleto" or others if supported by the price currency (BRL). // If we want to support Pix, we might need to check if it's a one-time payment or subscription. // Recurring Pix is not fully standard in Stripe Checkout yet for all regions. // Let's stick generic for now and user can enable methods in Dashboard. sess, err := session.New(params) if err != nil { return "", err } return sess.URL, nil } // HandleWebhook processes Stripe events func (s *SubscriptionService) HandleWebhook(payload []byte, signature string) error { endpointSecret := os.Getenv("STRIPE_WEBHOOK_SECRET") event, err := webhook.ConstructEvent(payload, signature, endpointSecret) if err != nil { return err } switch event.Type { case "checkout.session.completed": var session stripe.CheckoutSession if err := json.Unmarshal(event.Data.Raw, &session); err != nil { return err } // Extract company ID from metadata companyIDStr := session.Metadata["company_id"] planID := session.Metadata["plan_id"] if companyIDStr != "" && planID != "" { // Update company status _, err := s.DB.Exec(` UPDATE companies SET stripe_customer_id = $1, subscription_plan = $2, subscription_status = 'active', updated_at = NOW() WHERE id = $3 `, session.Customer.ID, planID, companyIDStr) if err != nil { log.Printf("Error updating company subscription: %v", err) return err } } case "invoice.payment_succeeded": var invoice stripe.Invoice if err := json.Unmarshal(event.Data.Raw, &invoice); err != nil { return err } if invoice.Subscription != nil { // Maintain active status // In a more complex system, we'd lookup company by stripe_customer_id _, err := s.DB.Exec(` UPDATE companies SET subscription_status = 'active', updated_at = NOW() WHERE stripe_customer_id = $1 `, invoice.Customer.ID) if err != nil { log.Printf("Error updating subscription status: %v", err) } } case "invoice.payment_failed": var invoice stripe.Invoice if err := json.Unmarshal(event.Data.Raw, &invoice); err != nil { return err } if invoice.Subscription != nil { // Mark as past_due or canceled _, err := s.DB.Exec(` UPDATE companies SET subscription_status = 'past_due', updated_at = NOW() WHERE stripe_customer_id = $1 `, invoice.Customer.ID) if err != nil { log.Printf("Error updating subscription status: %v", err) } } } return nil }