package payment import ( "encoding/json" "fmt" "io" "log" "net/http" "os" "git.linuxforward.com/byom/byom-onboard/internal/platform/mailer" "github.com/gin-gonic/gin" "github.com/stripe/stripe-go/v81" "github.com/stripe/stripe-go/v81/account" portalsession "github.com/stripe/stripe-go/v81/billingportal/session" "github.com/stripe/stripe-go/v81/checkout/session" "github.com/stripe/stripe-go/v81/price" "github.com/stripe/stripe-go/v81/product" "github.com/stripe/stripe-go/v81/webhook" ) type StripeHandler struct { domain string endpointSecret string mailer *mailer.Mailer } func NewStripeHandler(domain string, mailer *mailer.Mailer) (*StripeHandler, error) { apiKey := os.Getenv("STRIPE_SECRET_KEY") if apiKey == "" { return nil, fmt.Errorf("STRIPE_SECRET_KEY environment variable is not set") } endpointSecret := os.Getenv("STRIPE_WEBHOOK_SECRET") if endpointSecret == "" { return nil, fmt.Errorf("STRIPE_WEBHOOK_SECRET environment variable is not set") } stripe.Key = apiKey // Test connection by retrieving account information _, err := account.Get() if err != nil { return nil, fmt.Errorf("failed to connect to Stripe: %w", err) } return &StripeHandler{ domain: domain, endpointSecret: endpointSecret, mailer: mailer, }, nil } func (h *StripeHandler) CreateCheckoutSession(c *gin.Context) { // Get email and priceID from request customerEmail := c.PostForm("email") priceID := c.PostForm("priceId") if customerEmail == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Email is required"}) return } if priceID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Price ID is required"}) return } checkoutParams := &stripe.CheckoutSessionParams{ Mode: stripe.String(string(stripe.CheckoutSessionModeSubscription)), LineItems: []*stripe.CheckoutSessionLineItemParams{ { Price: stripe.String(priceID), Quantity: stripe.Int64(1), }, }, SuccessURL: stripe.String("https://byom.moooffle.com/payment/success?session_id={CHECKOUT_SESSION_ID}"), CancelURL: stripe.String("https://byom.moooffle.com/payment/cancel"), CustomerEmail: stripe.String(customerEmail), } s, err := session.New(checkoutParams) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "url": s.URL, "sessionId": s.ID, }) } func (h *StripeHandler) CreatePortalSession(c *gin.Context) { sessionID := c.PostForm("session_id") s, err := session.Get(sessionID, nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) log.Printf("session.Get: %v", err) return } params := &stripe.BillingPortalSessionParams{ Customer: stripe.String(s.Customer.ID), ReturnURL: stripe.String(h.domain), } ps, err := portalsession.New(params) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) log.Printf("ps.New: %v", err) return } c.Redirect(http.StatusSeeOther, ps.URL) } func (h *StripeHandler) HandleWebhook(c *gin.Context) { const MaxBodyBytes = int64(65536) bodyReader := http.MaxBytesReader(c.Writer, c.Request.Body, MaxBodyBytes) payload, err := io.ReadAll(bodyReader) if err != nil { log.Printf("[Webhook Debug] Error reading body: %v", err) c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service unavailable"}) return } log.Printf("[Webhook Debug] Received payload: %s", string(payload)) signatureHeader := c.Request.Header.Get("Stripe-Signature") log.Printf("[Webhook Debug] Stripe-Signature header: %s", signatureHeader) log.Printf("[Webhook Debug] Using endpoint secret: %s", h.endpointSecret) event, err := webhook.ConstructEvent(payload, signatureHeader, h.endpointSecret) if err != nil { log.Printf("[Webhook Debug] Signature verification failed: %v", err) log.Printf("[Webhook Debug] Headers received: %+v", c.Request.Header) c.JSON(http.StatusBadRequest, gin.H{"error": "Bad request"}) return } log.Printf("[Webhook Debug] Event type received: %s", event.Type) log.Printf("[Webhook Debug] Event data: %s", string(event.Data.Raw)) switch event.Type { case "customer.subscription.deleted": var subscription stripe.Subscription if err := json.Unmarshal(event.Data.Raw, &subscription); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } log.Printf("Subscription deleted for %s.", subscription.ID) case "customer.subscription.updated": var subscription stripe.Subscription if err := json.Unmarshal(event.Data.Raw, &subscription); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } log.Printf("Subscription updated for %s.", subscription.ID) case "customer.subscription.created": var subscription stripe.Subscription if err := json.Unmarshal(event.Data.Raw, &subscription); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } log.Printf("Subscription created for %s.", subscription.ID) case "checkout.session.completed": var session stripe.CheckoutSession if err := json.Unmarshal(event.Data.Raw, &session); err != nil { log.Printf("[Webhook Debug] Failed to parse session data: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } log.Printf("[Webhook Debug] Payment successful for session: %s", session.ID) customerEmail := session.CustomerEmail log.Printf("[Webhook Debug] Customer email: %s", customerEmail) if err := h.mailer.SendWelcomeEmail(customerEmail, &mailer.EmailData{ Username: customerEmail, WebAppURL: fmt.Sprintf("http://test.byom.fr/login?email=%s", customerEmail), SetupGuide: "Votre espace de travail est prêt ! Connectez-vous pour commencer à utiliser Byom.", }); err != nil { log.Printf("[Webhook Debug] Failed to send welcome email: %v", err) } case "checkout.session.expired": var session stripe.CheckoutSession if err := json.Unmarshal(event.Data.Raw, &session); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } log.Printf("Payment session expired: %s", session.ID) // TODO: Gérer l'expiration de la session default: fmt.Fprintf(os.Stderr, "Unhandled event type: %s\n", event.Type) } c.JSON(http.StatusOK, gin.H{"status": "success"}) } //implement this func // const fetchPricingData = async () => { // try { // const response = await fetch('http://172.27.28.86:8080/api/v1/onboard/products'); // if (!response.ok) throw new Error('Failed to fetch products'); // const data = await response.json(); // // Transform API data to match UI requirements // const transformedProducts = data.map(product => ({ // name: product.name, // prices: { // monthly: product.prices.monthly, // yearly: product.prices.yearly, // monthlyPriceId: product.prices.monthly_price_id, // yearlyPriceId: product.prices.yearly_price_id // }, // features: product.features, // description: product.description, // popular: product.metadata?.is_popular === "true", // maxUsers: product.max_users, // storageLimit: product.storage_limit, // apiRateLimit: product.api_rate_limit, // supportTier: product.support_tier, // customDomain: product.custom_domain // })); // setPricingData(transformedProducts); // } catch (error) { // console.error('Error fetching products:', error); // // Set fallback pricing data // setPricingData([ // { // name: 'Starter', // prices: { monthly: 29, yearly: 290 }, // features: [ // 'Basic AI trend analysis', // '25 AI image generations/month', // 'Basic analytics dashboard', // 'Content calendar', // 'Email support', // 'Single user' // ], // }, // // ... other fallback plans // ]); // } finally { // setIsLoading(false); // } // }; func (h *StripeHandler) GetProducts(c *gin.Context) { params := &stripe.ProductListParams{ Active: stripe.Bool(true), } products := product.List(params) var response []ProductResponse for products.Next() { p := products.Product() // Parse product metadata var metadata ProductMetadata if metadataJSON, ok := p.Metadata["product_config"]; ok { if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil { log.Printf("Error parsing metadata for product %s: %v", p.ID, err) continue } } // Fetch prices (rest of the price fetching logic remains the same) priceParams := &stripe.PriceListParams{ Product: stripe.String(p.ID), Active: stripe.Bool(true), } prices := price.List(priceParams) var monthlyPrice, yearlyPrice int64 var monthlyPriceID, yearlyPriceID string for prices.Next() { pr := prices.Price() if pr.Recurring.Interval == "month" { monthlyPrice = pr.UnitAmount / 100 monthlyPriceID = pr.ID } if pr.Recurring.Interval == "year" { yearlyPrice = pr.UnitAmount / 100 yearlyPriceID = pr.ID } } response = append(response, ProductResponse{ ID: p.ID, Name: p.Name, Description: metadata.Description, Prices: Prices{ Monthly: monthlyPrice, Yearly: yearlyPrice, MonthlyPriceID: monthlyPriceID, YearlyPriceID: yearlyPriceID, }, Features: metadata.Features, MaxUsers: metadata.MaxUsers, StorageLimit: metadata.StorageLimit, ApiRateLimit: metadata.ApiRateLimit, SupportTier: metadata.SupportTier, CustomDomain: metadata.CustomDomain, }) } c.JSON(200, response) } type ProductMetadata struct { Description string `json:"description"` Features []string `json:"features"` MaxUsers int `json:"max_users"` StorageLimit string `json:"storage_limit"` ApiRateLimit int `json:"api_rate_limit"` SupportTier string `json:"support_tier"` CustomDomain bool `json:"custom_domain"` } type ProductResponse struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Prices Prices `json:"prices"` Features []string `json:"features"` MaxUsers int `json:"max_users"` Metadata map[string]interface{} `json:"metadata"` StorageLimit string `json:"storage_limit"` ApiRateLimit int `json:"api_rate_limit"` SupportTier string `json:"support_tier"` CustomDomain bool `json:"custom_domain"` } type Prices struct { Monthly int64 `json:"monthly"` Yearly int64 `json:"yearly"` MonthlyPriceID string `json:"monthly_price_id"` YearlyPriceID string `json:"yearly_price_id"` }