init
This commit is contained in:
commit
d69342b2e9
160 changed files with 28681 additions and 0 deletions
234
internal/billing/lemon.go
Normal file
234
internal/billing/lemon.go
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue