forked from casdoor/casdoor
feat: add Lemon Squeezy payment provider (#4604)
This commit is contained in:
1
go.mod
1
go.mod
@@ -4,6 +4,7 @@ go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.3
|
||||
github.com/NdoleStudio/lemonsqueezy-go v1.2.4
|
||||
github.com/PaddleHQ/paddle-go-sdk v1.0.0
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.4
|
||||
|
||||
5
go.sum
5
go.sum
@@ -78,6 +78,8 @@ github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA4
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/NdoleStudio/lemonsqueezy-go v1.2.4 h1:BhWlCUH+DIPfSn4g/V7f2nFkMCQuzno9DXKZ7YDrXXA=
|
||||
github.com/NdoleStudio/lemonsqueezy-go v1.2.4/go.mod h1:2uZlWgn9sbNxOx3JQWLlPrDOC6NT/wmSTOgL3U/fMMw=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PaddleHQ/paddle-go-sdk v1.0.0 h1:+EXitsPFbRcc0CpQE/MIeudxiVOR8pFe/aOWTEUHDKU=
|
||||
github.com/PaddleHQ/paddle-go-sdk v1.0.0/go.mod h1:kbBBzf0BHEj38QvhtoELqlGip3alKgA/I+vl7RQzB58=
|
||||
@@ -605,8 +607,9 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
|
||||
@@ -363,6 +363,12 @@ func GetPaymentProvider(p *Provider) (pp.PaymentProvider, error) {
|
||||
return nil, err
|
||||
}
|
||||
return pp, nil
|
||||
} else if typ == "Lemon Squeezy" {
|
||||
pp, err := pp.NewLemonSqueezyPaymentProvider(p.ClientId, p.ClientSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pp, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("the payment provider type: %s is not supported", p.Type)
|
||||
}
|
||||
|
||||
194
pp/lemonsqueezy.go
Normal file
194
pp/lemonsqueezy.go
Normal file
@@ -0,0 +1,194 @@
|
||||
// Copyright 2024 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/NdoleStudio/lemonsqueezy-go"
|
||||
)
|
||||
|
||||
type LemonSqueezyPaymentProvider struct {
|
||||
Client *lemonsqueezy.Client
|
||||
StoreID int
|
||||
}
|
||||
|
||||
func NewLemonSqueezyPaymentProvider(storeId string, apiKey string) (*LemonSqueezyPaymentProvider, error) {
|
||||
client := lemonsqueezy.New(
|
||||
lemonsqueezy.WithAPIKey(apiKey),
|
||||
)
|
||||
|
||||
storeID, err := strconv.Atoi(storeId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid store ID: %w", err)
|
||||
}
|
||||
|
||||
pp := &LemonSqueezyPaymentProvider{
|
||||
Client: client,
|
||||
StoreID: storeID,
|
||||
}
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
func (pp *LemonSqueezyPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Parse variant ID from the product name (expected to be the variant ID)
|
||||
variantID, err := strconv.Atoi(r.ProductName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid variant ID in product name: %w", err)
|
||||
}
|
||||
|
||||
// Store product info in custom data for later retrieval
|
||||
description := joinAttachString([]string{r.ProductName, r.ProductDisplayName, r.ProviderName})
|
||||
|
||||
customData := map[string]any{
|
||||
"payment_name": r.PaymentName,
|
||||
"product_description": description,
|
||||
"product_name": r.ProductName,
|
||||
"product_display_name": r.ProductDisplayName,
|
||||
"provider_name": r.ProviderName,
|
||||
"price": priceFloat64ToString(r.Price),
|
||||
"currency": r.Currency,
|
||||
}
|
||||
|
||||
// Create checkout attributes
|
||||
attributes := &lemonsqueezy.CheckoutCreateAttributes{
|
||||
ProductOptions: lemonsqueezy.CheckoutCreateProductOptions{
|
||||
Name: r.ProductDisplayName,
|
||||
Description: r.ProductDescription,
|
||||
RedirectURL: r.ReturnUrl,
|
||||
},
|
||||
CheckoutData: lemonsqueezy.CheckoutCreateData{
|
||||
Email: r.PayerEmail,
|
||||
Name: r.PayerName,
|
||||
Custom: customData,
|
||||
},
|
||||
}
|
||||
|
||||
// Create checkout
|
||||
checkout, _, err := pp.Client.Checkouts.Create(ctx, pp.StoreID, variantID, attributes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if checkout == nil {
|
||||
return nil, fmt.Errorf("lemonsqueezy checkout response is nil")
|
||||
}
|
||||
|
||||
payResp := &PayResp{
|
||||
PayUrl: checkout.Data.Attributes.URL,
|
||||
OrderId: checkout.Data.ID,
|
||||
}
|
||||
return payResp, nil
|
||||
}
|
||||
|
||||
func (pp *LemonSqueezyPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Get checkout status
|
||||
checkout, _, err := pp.Client.Checkouts.Get(ctx, orderId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if checkout == nil {
|
||||
return nil, fmt.Errorf("lemonsqueezy checkout not found for order: %s", orderId)
|
||||
}
|
||||
|
||||
// Check if checkout has expired
|
||||
if checkout.Data.Attributes.ExpiresAt != nil {
|
||||
if time.Now().After(*checkout.Data.Attributes.ExpiresAt) {
|
||||
return &NotifyResult{PaymentStatus: PaymentStateTimeout}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Extract payment details from custom data
|
||||
var (
|
||||
paymentName string
|
||||
productName string
|
||||
productDisplayName string
|
||||
providerName string
|
||||
price float64
|
||||
currency string
|
||||
)
|
||||
|
||||
if checkout.Data.Attributes.CheckoutData.Custom != nil {
|
||||
if customData, ok := checkout.Data.Attributes.CheckoutData.Custom.(map[string]any); ok {
|
||||
if v, ok := customData["payment_name"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
paymentName = str
|
||||
}
|
||||
}
|
||||
if v, ok := customData["product_name"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
productName = str
|
||||
}
|
||||
}
|
||||
if v, ok := customData["product_display_name"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
productDisplayName = str
|
||||
}
|
||||
}
|
||||
if v, ok := customData["provider_name"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
providerName = str
|
||||
}
|
||||
}
|
||||
if v, ok := customData["price"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
price = priceStringToFloat64(str)
|
||||
}
|
||||
}
|
||||
if v, ok := customData["currency"]; ok {
|
||||
if str, ok := v.(string); ok {
|
||||
currency = str
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lemon Squeezy checkouts don't have a direct status field for payment completion.
|
||||
// The checkout remains valid until it expires or is used.
|
||||
// For proper payment status tracking, webhooks should be configured.
|
||||
// Here we return PaymentStateCreated to indicate the checkout is still pending.
|
||||
return &NotifyResult{
|
||||
PaymentName: paymentName,
|
||||
PaymentStatus: PaymentStateCreated,
|
||||
|
||||
ProductName: productName,
|
||||
ProductDisplayName: productDisplayName,
|
||||
ProviderName: providerName,
|
||||
|
||||
Price: price,
|
||||
Currency: currency,
|
||||
|
||||
OrderId: orderId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (pp *LemonSqueezyPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (pp *LemonSqueezyPaymentProvider) GetResponseError(err error) string {
|
||||
if err == nil {
|
||||
return "success"
|
||||
}
|
||||
return "fail"
|
||||
}
|
||||
@@ -300,6 +300,10 @@ export const OtherProviderInfo = {
|
||||
logo: `${StaticBaseUrl}/img/payment_fastspring.png`,
|
||||
url: "https://fastspring.com/",
|
||||
},
|
||||
"Lemon Squeezy": {
|
||||
logo: `${StaticBaseUrl}/img/payment_lemonsqueezy.png`,
|
||||
url: "https://www.lemonsqueezy.com/",
|
||||
},
|
||||
},
|
||||
Captcha: {
|
||||
"Default": {
|
||||
@@ -1277,6 +1281,7 @@ export function getProviderTypeOptions(category) {
|
||||
{id: "Polar", name: "Polar"},
|
||||
{id: "Paddle", name: "Paddle"},
|
||||
{id: "FastSpring", name: "FastSpring"},
|
||||
{id: "Lemon Squeezy", name: "Lemon Squeezy"},
|
||||
]);
|
||||
} else if (category === "Captcha") {
|
||||
return ([
|
||||
|
||||
Reference in New Issue
Block a user