package billing import ( "bytes" "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" ) const lemonBaseURL = "https://api.lemonsqueezy.com/v1" type LemonClient struct { apiKey string storeID string webhookSecret string httpClient *http.Client } func NewLemonClient() *LemonClient { return &LemonClient{ apiKey: os.Getenv("LEMON_API_KEY"), storeID: os.Getenv("LEMON_STORE_ID"), webhookSecret: os.Getenv("LEMON_WEBHOOK_SECRET"), httpClient: &http.Client{}, } } func (c *LemonClient) IsConfigured() bool { return c.apiKey != "" && c.storeID != "" } type CheckoutRequest struct { StoreID string `json:"store_id"` VariantID string `json:"variant_id"` CustomPrice *int `json:"custom_price,omitempty"` CheckoutOptions *CheckoutOptions `json:"checkout_options,omitempty"` CheckoutData *CheckoutData `json:"checkout_data,omitempty"` } type CheckoutOptions struct { Embed bool `json:"embed,omitempty"` } type CheckoutData struct { Email string `json:"email,omitempty"` Name string `json:"name,omitempty"` Custom map[string]string `json:"custom,omitempty"` } type CheckoutResponse struct { Data struct { ID string `json:"id"` Attributes struct { URL string `json:"url"` } `json:"attributes"` } `json:"data"` } func (c *LemonClient) CreateCheckout(ctx context.Context, req *CheckoutRequest) (*CheckoutResponse, error) { if req.StoreID == "" { req.StoreID = c.storeID } payload := map[string]any{ "data": map[string]any{ "type": "checkouts", "attributes": req, "relationships": map[string]any{ "store": map[string]any{ "data": map[string]any{"type": "stores", "id": req.StoreID}, }, "variant": map[string]any{ "data": map[string]any{"type": "variants", "id": req.VariantID}, }, }, }, } body, err := json.Marshal(payload) if err != nil { return nil, err } httpReq, err := http.NewRequestWithContext(ctx, "POST", lemonBaseURL+"/checkouts", bytes.NewReader(body)) if err != nil { return nil, err } httpReq.Header.Set("Authorization", "Bearer "+c.apiKey) httpReq.Header.Set("Content-Type", "application/vnd.api+json") httpReq.Header.Set("Accept", "application/vnd.api+json") resp, err := c.httpClient.Do(httpReq) if err != nil { return nil, err } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("checkout creation failed (status %d): %s", resp.StatusCode, string(respBody)) } var checkoutResp CheckoutResponse if err := json.Unmarshal(respBody, &checkoutResp); err != nil { return nil, err } return &checkoutResp, nil } func (c *LemonClient) CreateSubscriptionCheckout(ctx context.Context, variantID, email, name string, custom map[string]string) (string, error) { req := &CheckoutRequest{ VariantID: variantID, CheckoutData: &CheckoutData{ Email: email, Name: name, Custom: custom, }, CheckoutOptions: &CheckoutOptions{Embed: true}, } resp, err := c.CreateCheckout(ctx, req) if err != nil { return "", err } return resp.Data.Attributes.URL, nil } func (c *LemonClient) CreateDonationCheckout(ctx context.Context, variantID string, amountCents int, email, name, message string, custom map[string]string) (string, error) { if custom == nil { custom = make(map[string]string) } if message != "" { custom["message"] = message } req := &CheckoutRequest{ VariantID: variantID, CustomPrice: &amountCents, CheckoutData: &CheckoutData{ Email: email, Name: name, Custom: custom, }, CheckoutOptions: &CheckoutOptions{Embed: true}, } resp, err := c.CreateCheckout(ctx, req) if err != nil { return "", err } return resp.Data.Attributes.URL, nil } func (c *LemonClient) VerifyWebhook(payload []byte, signature string) bool { if c.webhookSecret == "" { return false } mac := hmac.New(sha256.New, []byte(c.webhookSecret)) mac.Write(payload) expectedSig := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expectedSig)) } type WebhookEvent struct { Meta struct { EventName string `json:"event_name"` CustomData map[string]string `json:"custom_data"` } `json:"meta"` Data json.RawMessage `json:"data"` } type SubscriptionData struct { ID string `json:"id"` Attributes struct { CustomerID int `json:"customer_id"` VariantName string `json:"variant_name"` UserName string `json:"user_name"` UserEmail string `json:"user_email"` Status string `json:"status"` RenewsAt string `json:"renews_at"` FirstSubscriptionItem struct { Price int `json:"price"` } `json:"first_subscription_item"` } `json:"attributes"` } type OrderData struct { ID string `json:"id"` Attributes struct { UserName string `json:"user_name"` UserEmail string `json:"user_email"` TotalUsd int `json:"total_usd"` } `json:"attributes"` } func (c *LemonClient) ParseWebhookEvent(payload []byte) (*WebhookEvent, error) { var event WebhookEvent if err := json.Unmarshal(payload, &event); err != nil { return nil, err } return &event, nil } func (e *WebhookEvent) GetSubscriptionData() (*SubscriptionData, error) { var data SubscriptionData if err := json.Unmarshal(e.Data, &data); err != nil { return nil, err } return &data, nil } func (e *WebhookEvent) GetOrderData() (*OrderData, error) { var data OrderData if err := json.Unmarshal(e.Data, &data); err != nil { return nil, err } return &data, nil }