stripe.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. package payment
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "log"
  7. "net/http"
  8. "os"
  9. "git.linuxforward.com/byom/byom-onboard/internal/platform/mailer"
  10. "github.com/gin-gonic/gin"
  11. "github.com/stripe/stripe-go/v81"
  12. "github.com/stripe/stripe-go/v81/account"
  13. portalsession "github.com/stripe/stripe-go/v81/billingportal/session"
  14. "github.com/stripe/stripe-go/v81/checkout/session"
  15. "github.com/stripe/stripe-go/v81/price"
  16. "github.com/stripe/stripe-go/v81/product"
  17. "github.com/stripe/stripe-go/v81/webhook"
  18. )
  19. type StripeHandler struct {
  20. domain string
  21. endpointSecret string
  22. mailer *mailer.Mailer
  23. }
  24. func NewStripeHandler(domain string, mailer *mailer.Mailer) (*StripeHandler, error) {
  25. apiKey := os.Getenv("STRIPE_SECRET_KEY")
  26. if apiKey == "" {
  27. return nil, fmt.Errorf("STRIPE_SECRET_KEY environment variable is not set")
  28. }
  29. endpointSecret := os.Getenv("STRIPE_WEBHOOK_SECRET")
  30. if endpointSecret == "" {
  31. return nil, fmt.Errorf("STRIPE_WEBHOOK_SECRET environment variable is not set")
  32. }
  33. stripe.Key = apiKey
  34. // Test connection by retrieving account information
  35. _, err := account.Get()
  36. if err != nil {
  37. return nil, fmt.Errorf("failed to connect to Stripe: %w", err)
  38. }
  39. return &StripeHandler{
  40. domain: domain,
  41. endpointSecret: endpointSecret,
  42. mailer: mailer,
  43. }, nil
  44. }
  45. func (h *StripeHandler) CreateCheckoutSession(c *gin.Context) {
  46. // Get email and priceID from request
  47. customerEmail := c.PostForm("email")
  48. priceID := c.PostForm("priceId")
  49. if customerEmail == "" {
  50. c.JSON(http.StatusBadRequest, gin.H{"error": "Email is required"})
  51. return
  52. }
  53. if priceID == "" {
  54. c.JSON(http.StatusBadRequest, gin.H{"error": "Price ID is required"})
  55. return
  56. }
  57. checkoutParams := &stripe.CheckoutSessionParams{
  58. Mode: stripe.String(string(stripe.CheckoutSessionModeSubscription)),
  59. LineItems: []*stripe.CheckoutSessionLineItemParams{
  60. {
  61. Price: stripe.String(priceID),
  62. Quantity: stripe.Int64(1),
  63. },
  64. },
  65. SuccessURL: stripe.String("https://byom.moooffle.com/payment/success?session_id={CHECKOUT_SESSION_ID}"),
  66. CancelURL: stripe.String("https://byom.moooffle.com/payment/cancel"),
  67. CustomerEmail: stripe.String(customerEmail),
  68. }
  69. s, err := session.New(checkoutParams)
  70. if err != nil {
  71. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  72. return
  73. }
  74. c.JSON(http.StatusOK, gin.H{
  75. "url": s.URL,
  76. "sessionId": s.ID,
  77. })
  78. }
  79. func (h *StripeHandler) CreatePortalSession(c *gin.Context) {
  80. sessionID := c.PostForm("session_id")
  81. s, err := session.Get(sessionID, nil)
  82. if err != nil {
  83. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  84. log.Printf("session.Get: %v", err)
  85. return
  86. }
  87. params := &stripe.BillingPortalSessionParams{
  88. Customer: stripe.String(s.Customer.ID),
  89. ReturnURL: stripe.String(h.domain),
  90. }
  91. ps, err := portalsession.New(params)
  92. if err != nil {
  93. c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
  94. log.Printf("ps.New: %v", err)
  95. return
  96. }
  97. c.Redirect(http.StatusSeeOther, ps.URL)
  98. }
  99. func (h *StripeHandler) HandleWebhook(c *gin.Context) {
  100. const MaxBodyBytes = int64(65536)
  101. bodyReader := http.MaxBytesReader(c.Writer, c.Request.Body, MaxBodyBytes)
  102. payload, err := io.ReadAll(bodyReader)
  103. if err != nil {
  104. log.Printf("[Webhook Debug] Error reading body: %v", err)
  105. c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service unavailable"})
  106. return
  107. }
  108. log.Printf("[Webhook Debug] Received payload: %s", string(payload))
  109. signatureHeader := c.Request.Header.Get("Stripe-Signature")
  110. log.Printf("[Webhook Debug] Stripe-Signature header: %s", signatureHeader)
  111. log.Printf("[Webhook Debug] Using endpoint secret: %s", h.endpointSecret)
  112. event, err := webhook.ConstructEvent(payload, signatureHeader, h.endpointSecret)
  113. if err != nil {
  114. log.Printf("[Webhook Debug] Signature verification failed: %v", err)
  115. log.Printf("[Webhook Debug] Headers received: %+v", c.Request.Header)
  116. c.JSON(http.StatusBadRequest, gin.H{"error": "Bad request"})
  117. return
  118. }
  119. log.Printf("[Webhook Debug] Event type received: %s", event.Type)
  120. log.Printf("[Webhook Debug] Event data: %s", string(event.Data.Raw))
  121. switch event.Type {
  122. case "customer.subscription.deleted":
  123. var subscription stripe.Subscription
  124. if err := json.Unmarshal(event.Data.Raw, &subscription); err != nil {
  125. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  126. return
  127. }
  128. log.Printf("Subscription deleted for %s.", subscription.ID)
  129. case "customer.subscription.updated":
  130. var subscription stripe.Subscription
  131. if err := json.Unmarshal(event.Data.Raw, &subscription); err != nil {
  132. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  133. return
  134. }
  135. log.Printf("Subscription updated for %s.", subscription.ID)
  136. case "customer.subscription.created":
  137. var subscription stripe.Subscription
  138. if err := json.Unmarshal(event.Data.Raw, &subscription); err != nil {
  139. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  140. return
  141. }
  142. log.Printf("Subscription created for %s.", subscription.ID)
  143. case "checkout.session.completed":
  144. var session stripe.CheckoutSession
  145. if err := json.Unmarshal(event.Data.Raw, &session); err != nil {
  146. log.Printf("[Webhook Debug] Failed to parse session data: %v", err)
  147. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  148. return
  149. }
  150. log.Printf("[Webhook Debug] Payment successful for session: %s", session.ID)
  151. customerEmail := session.CustomerEmail
  152. log.Printf("[Webhook Debug] Customer email: %s", customerEmail)
  153. if err := h.mailer.SendWelcomeEmail(customerEmail, &mailer.EmailData{
  154. Username: customerEmail,
  155. WebAppURL: fmt.Sprintf("http://test.byom.fr/login?email=%s", customerEmail),
  156. SetupGuide: "Votre espace de travail est prêt ! Connectez-vous pour commencer à utiliser Byom.",
  157. }); err != nil {
  158. log.Printf("[Webhook Debug] Failed to send welcome email: %v", err)
  159. }
  160. case "checkout.session.expired":
  161. var session stripe.CheckoutSession
  162. if err := json.Unmarshal(event.Data.Raw, &session); err != nil {
  163. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  164. return
  165. }
  166. log.Printf("Payment session expired: %s", session.ID)
  167. // TODO: Gérer l'expiration de la session
  168. default:
  169. fmt.Fprintf(os.Stderr, "Unhandled event type: %s\n", event.Type)
  170. }
  171. c.JSON(http.StatusOK, gin.H{"status": "success"})
  172. }
  173. //implement this func
  174. // const fetchPricingData = async () => {
  175. // try {
  176. // const response = await fetch('http://172.27.28.86:8080/api/v1/onboard/products');
  177. // if (!response.ok) throw new Error('Failed to fetch products');
  178. // const data = await response.json();
  179. // // Transform API data to match UI requirements
  180. // const transformedProducts = data.map(product => ({
  181. // name: product.name,
  182. // prices: {
  183. // monthly: product.prices.monthly,
  184. // yearly: product.prices.yearly,
  185. // monthlyPriceId: product.prices.monthly_price_id,
  186. // yearlyPriceId: product.prices.yearly_price_id
  187. // },
  188. // features: product.features,
  189. // description: product.description,
  190. // popular: product.metadata?.is_popular === "true",
  191. // maxUsers: product.max_users,
  192. // storageLimit: product.storage_limit,
  193. // apiRateLimit: product.api_rate_limit,
  194. // supportTier: product.support_tier,
  195. // customDomain: product.custom_domain
  196. // }));
  197. // setPricingData(transformedProducts);
  198. // } catch (error) {
  199. // console.error('Error fetching products:', error);
  200. // // Set fallback pricing data
  201. // setPricingData([
  202. // {
  203. // name: 'Starter',
  204. // prices: { monthly: 29, yearly: 290 },
  205. // features: [
  206. // 'Basic AI trend analysis',
  207. // '25 AI image generations/month',
  208. // 'Basic analytics dashboard',
  209. // 'Content calendar',
  210. // 'Email support',
  211. // 'Single user'
  212. // ],
  213. // },
  214. // // ... other fallback plans
  215. // ]);
  216. // } finally {
  217. // setIsLoading(false);
  218. // }
  219. // };
  220. func (h *StripeHandler) GetProducts(c *gin.Context) {
  221. params := &stripe.ProductListParams{
  222. Active: stripe.Bool(true),
  223. }
  224. products := product.List(params)
  225. var response []ProductResponse
  226. for products.Next() {
  227. p := products.Product()
  228. // Parse product metadata
  229. var metadata ProductMetadata
  230. if metadataJSON, ok := p.Metadata["product_config"]; ok {
  231. if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil {
  232. log.Printf("Error parsing metadata for product %s: %v", p.ID, err)
  233. continue
  234. }
  235. }
  236. // Fetch prices (rest of the price fetching logic remains the same)
  237. priceParams := &stripe.PriceListParams{
  238. Product: stripe.String(p.ID),
  239. Active: stripe.Bool(true),
  240. }
  241. prices := price.List(priceParams)
  242. var monthlyPrice, yearlyPrice int64
  243. var monthlyPriceID, yearlyPriceID string
  244. for prices.Next() {
  245. pr := prices.Price()
  246. if pr.Recurring.Interval == "month" {
  247. monthlyPrice = pr.UnitAmount / 100
  248. monthlyPriceID = pr.ID
  249. }
  250. if pr.Recurring.Interval == "year" {
  251. yearlyPrice = pr.UnitAmount / 100
  252. yearlyPriceID = pr.ID
  253. }
  254. }
  255. response = append(response, ProductResponse{
  256. ID: p.ID,
  257. Name: p.Name,
  258. Description: metadata.Description,
  259. Prices: Prices{
  260. Monthly: monthlyPrice,
  261. Yearly: yearlyPrice,
  262. MonthlyPriceID: monthlyPriceID,
  263. YearlyPriceID: yearlyPriceID,
  264. },
  265. Features: metadata.Features,
  266. MaxUsers: metadata.MaxUsers,
  267. StorageLimit: metadata.StorageLimit,
  268. ApiRateLimit: metadata.ApiRateLimit,
  269. SupportTier: metadata.SupportTier,
  270. CustomDomain: metadata.CustomDomain,
  271. })
  272. }
  273. c.JSON(200, response)
  274. }
  275. type ProductMetadata struct {
  276. Description string `json:"description"`
  277. Features []string `json:"features"`
  278. MaxUsers int `json:"max_users"`
  279. StorageLimit string `json:"storage_limit"`
  280. ApiRateLimit int `json:"api_rate_limit"`
  281. SupportTier string `json:"support_tier"`
  282. CustomDomain bool `json:"custom_domain"`
  283. }
  284. type ProductResponse struct {
  285. ID string `json:"id"`
  286. Name string `json:"name"`
  287. Description string `json:"description"`
  288. Prices Prices `json:"prices"`
  289. Features []string `json:"features"`
  290. MaxUsers int `json:"max_users"`
  291. Metadata map[string]interface{} `json:"metadata"`
  292. StorageLimit string `json:"storage_limit"`
  293. ApiRateLimit int `json:"api_rate_limit"`
  294. SupportTier string `json:"support_tier"`
  295. CustomDomain bool `json:"custom_domain"`
  296. }
  297. type Prices struct {
  298. Monthly int64 `json:"monthly"`
  299. Yearly int64 `json:"yearly"`
  300. MonthlyPriceID string `json:"monthly_price_id"`
  301. YearlyPriceID string `json:"yearly_price_id"`
  302. }