mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-23 02:35:49 +08:00
feat: support Stripe payment provider (#2204)
* feat: add stripe payment provider * feat: support stripe payment * feat: delete todo comment * feat: remove description struct * feat: change outOrderId->orderId
This commit is contained in:
parent
abaf4ca8d9
commit
2ff9020884
1
go.mod
1
go.mod
@ -51,6 +51,7 @@ require (
|
||||
github.com/siddontang/go-log v0.0.0-20190221022429-1e957dd83bed
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/stretchr/testify v1.8.3
|
||||
github.com/stripe/stripe-go/v74 v74.29.0 // indirect
|
||||
github.com/tealeg/xlsx v1.0.5
|
||||
github.com/thanhpk/randstr v1.0.4
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
|
3
go.sum
3
go.sum
@ -1476,6 +1476,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stripe/stripe-go/v74 v74.29.0 h1:ffJ+1Ta1Ccg7yDDz+SfjixX0KizEEJ/wNVRoFYkdwFY=
|
||||
github.com/stripe/stripe-go/v74 v74.29.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
|
||||
github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
@ -1699,6 +1701,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
|
@ -36,7 +36,7 @@ type Provider struct {
|
||||
Type string `xorm:"varchar(100)" json:"type"`
|
||||
SubType string `xorm:"varchar(100)" json:"subType"`
|
||||
Method string `xorm:"varchar(100)" json:"method"`
|
||||
ClientId string `xorm:"varchar(100)" json:"clientId"`
|
||||
ClientId string `xorm:"varchar(200)" json:"clientId"`
|
||||
ClientSecret string `xorm:"varchar(2000)" json:"clientSecret"`
|
||||
ClientId2 string `xorm:"varchar(100)" json:"clientId2"`
|
||||
ClientSecret2 string `xorm:"varchar(100)" json:"clientSecret2"`
|
||||
|
@ -90,7 +90,7 @@ func (pp *AlipayPaymentProvider) Notify(request *http.Request, body []byte, auth
|
||||
ProductName: productName,
|
||||
ProductDisplayName: productDisplayName,
|
||||
ProviderName: providerName,
|
||||
OutOrderId: orderId,
|
||||
OrderId: orderId,
|
||||
PaymentStatus: PaymentStatePaid,
|
||||
Price: price,
|
||||
PaymentName: paymentName,
|
||||
|
2
pp/gc.go
2
pp/gc.go
@ -255,7 +255,7 @@ func (pp *GcPaymentProvider) Notify(request *http.Request, body []byte, authorit
|
||||
ProductName: productName,
|
||||
ProductDisplayName: productDisplayName,
|
||||
ProviderName: providerName,
|
||||
OutOrderId: orderId,
|
||||
OrderId: orderId,
|
||||
Price: price,
|
||||
PaymentStatus: PaymentStatePaid,
|
||||
PaymentName: paymentName,
|
||||
|
12
pp/paypal.go
12
pp/paypal.go
@ -52,13 +52,12 @@ func NewPaypalPaymentProvider(clientID string, secret string) (*PaypalPaymentPro
|
||||
|
||||
func (pp *PaypalPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
|
||||
// https://github.com/go-pay/gopay/blob/main/doc/paypal.md
|
||||
priceStr := strconv.FormatFloat(price, 'f', 2, 64)
|
||||
units := make([]*paypal.PurchaseUnit, 0, 1)
|
||||
unit := &paypal.PurchaseUnit{
|
||||
ReferenceId: util.GetRandomString(16),
|
||||
Amount: &paypal.Amount{
|
||||
CurrencyCode: currency, // e.g."USD"
|
||||
Value: priceStr, // e.g."100.00"
|
||||
CurrencyCode: currency, // e.g."USD"
|
||||
Value: priceFloat64ToString(price), // e.g."100.00"
|
||||
},
|
||||
Description: joinAttachString([]string{productDisplayName, productName, providerName}),
|
||||
}
|
||||
@ -147,16 +146,15 @@ func (pp *PaypalPaymentProvider) Notify(request *http.Request, body []byte, auth
|
||||
paymentStatus = PaymentStateError
|
||||
}
|
||||
notifyResult = &NotifyResult{
|
||||
PaymentStatus: paymentStatus,
|
||||
PaymentName: paymentName,
|
||||
|
||||
PaymentStatus: paymentStatus,
|
||||
PaymentName: paymentName,
|
||||
ProductName: productName,
|
||||
ProductDisplayName: productDisplayName,
|
||||
ProviderName: providerName,
|
||||
Price: price,
|
||||
Currency: currency,
|
||||
|
||||
OutOrderId: orderId,
|
||||
OrderId: orderId,
|
||||
}
|
||||
return notifyResult, nil
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ const (
|
||||
PaymentStatePaid PaymentState = "Paid"
|
||||
PaymentStateCreated PaymentState = "Created"
|
||||
PaymentStateCanceled PaymentState = "Canceled"
|
||||
PaymentStateTimeout PaymentState = "Timeout"
|
||||
PaymentStateError PaymentState = "Error"
|
||||
)
|
||||
|
||||
@ -32,13 +33,13 @@ type NotifyResult struct {
|
||||
PaymentStatus PaymentState
|
||||
NotifyMessage string
|
||||
|
||||
ProviderName string
|
||||
ProductName string
|
||||
ProductDisplayName string
|
||||
ProviderName string
|
||||
Price float64
|
||||
Currency string
|
||||
|
||||
OutOrderId string
|
||||
OrderId string
|
||||
}
|
||||
|
||||
type PaymentProvider interface {
|
||||
@ -75,6 +76,12 @@ func GetPaymentProvider(typ string, clientId string, clientSecret string, host s
|
||||
return nil, err
|
||||
}
|
||||
return pp, nil
|
||||
} else if typ == "Stripe" {
|
||||
pp, err := NewStripePaymentProvider(clientId, clientSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
166
pp/stripe.go
Normal file
166
pp/stripe.go
Normal file
@ -0,0 +1,166 @@
|
||||
// Copyright 2023 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/casdoor/casdoor/conf"
|
||||
"github.com/stripe/stripe-go/v74"
|
||||
stripeCheckout "github.com/stripe/stripe-go/v74/checkout/session"
|
||||
stripeIntent "github.com/stripe/stripe-go/v74/paymentintent"
|
||||
stripePrice "github.com/stripe/stripe-go/v74/price"
|
||||
stripeProduct "github.com/stripe/stripe-go/v74/product"
|
||||
)
|
||||
|
||||
type StripePaymentProvider struct {
|
||||
PublishableKey string
|
||||
SecretKey string
|
||||
isProd bool
|
||||
}
|
||||
|
||||
func NewStripePaymentProvider(PublishableKey, SecretKey string) (*StripePaymentProvider, error) {
|
||||
isProd := false
|
||||
if conf.GetConfigString("runmode") == "prod" {
|
||||
isProd = true
|
||||
}
|
||||
pp := &StripePaymentProvider{
|
||||
PublishableKey: PublishableKey,
|
||||
SecretKey: SecretKey,
|
||||
isProd: isProd,
|
||||
}
|
||||
stripe.Key = pp.SecretKey
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
func (pp *StripePaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (payUrl string, orderId string, err error) {
|
||||
// Create a temp product
|
||||
description := joinAttachString([]string{productName, productDisplayName, providerName})
|
||||
productParams := &stripe.ProductParams{
|
||||
Name: stripe.String(productDisplayName),
|
||||
Description: stripe.String(description),
|
||||
DefaultPriceData: &stripe.ProductDefaultPriceDataParams{
|
||||
UnitAmount: stripe.Int64(priceFloat64ToInt64(price)),
|
||||
Currency: stripe.String(currency),
|
||||
},
|
||||
}
|
||||
sProduct, err := stripeProduct.New(productParams)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// Create a price for an existing product
|
||||
priceParams := &stripe.PriceParams{
|
||||
Currency: stripe.String(currency),
|
||||
UnitAmount: stripe.Int64(priceFloat64ToInt64(price)),
|
||||
Product: stripe.String(sProduct.ID),
|
||||
}
|
||||
sPrice, err := stripePrice.New(priceParams)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// Create a Checkout Session
|
||||
checkoutParams := &stripe.CheckoutSessionParams{
|
||||
LineItems: []*stripe.CheckoutSessionLineItemParams{
|
||||
{
|
||||
Price: stripe.String(sPrice.ID),
|
||||
Quantity: stripe.Int64(1),
|
||||
},
|
||||
},
|
||||
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
|
||||
SuccessURL: stripe.String(returnUrl),
|
||||
CancelURL: stripe.String(returnUrl),
|
||||
ClientReferenceID: stripe.String(paymentName),
|
||||
ExpiresAt: stripe.Int64(time.Now().Add(30 * time.Minute).Unix()),
|
||||
}
|
||||
checkoutParams.AddMetadata("product_description", description)
|
||||
sCheckout, err := stripeCheckout.New(checkoutParams)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return sCheckout.URL, sCheckout.ID, nil
|
||||
}
|
||||
|
||||
func (pp *StripePaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) {
|
||||
notifyResult := &NotifyResult{}
|
||||
sCheckout, err := stripeCheckout.Get(orderId, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch sCheckout.Status {
|
||||
case "open":
|
||||
// The checkout session is still in progress. Payment processing has not started
|
||||
notifyResult.PaymentStatus = PaymentStateCreated
|
||||
return notifyResult, nil
|
||||
case "complete":
|
||||
// The checkout session is complete. Payment processing may still be in progress
|
||||
case "expired":
|
||||
// The checkout session has expired. No further processing will occur
|
||||
notifyResult.PaymentStatus = PaymentStateTimeout
|
||||
return notifyResult, nil
|
||||
default:
|
||||
notifyResult.PaymentStatus = PaymentStateError
|
||||
notifyResult.NotifyMessage = fmt.Sprintf("unexpected stripe checkout status: %v", sCheckout.Status)
|
||||
return notifyResult, nil
|
||||
}
|
||||
switch sCheckout.PaymentStatus {
|
||||
case "paid":
|
||||
// Skip
|
||||
case "unpaid":
|
||||
notifyResult.PaymentStatus = PaymentStateCreated
|
||||
return notifyResult, nil
|
||||
default:
|
||||
notifyResult.PaymentStatus = PaymentStateError
|
||||
notifyResult.NotifyMessage = fmt.Sprintf("unexpected stripe checkout payment status: %v", sCheckout.PaymentStatus)
|
||||
return notifyResult, nil
|
||||
}
|
||||
// Once payment is successful, the Checkout Session will contain a reference to the successful `PaymentIntent`
|
||||
sIntent, err := stripeIntent.Get(sCheckout.PaymentIntent.ID, nil)
|
||||
var (
|
||||
productName string
|
||||
productDisplayName string
|
||||
providerName string
|
||||
)
|
||||
if description, ok := sCheckout.Metadata["product_description"]; ok {
|
||||
productName, productDisplayName, providerName, _ = parseAttachString(description)
|
||||
}
|
||||
notifyResult = &NotifyResult{
|
||||
PaymentName: sCheckout.ClientReferenceID,
|
||||
PaymentStatus: PaymentStatePaid,
|
||||
|
||||
ProductName: productName,
|
||||
ProductDisplayName: productDisplayName,
|
||||
ProviderName: providerName,
|
||||
|
||||
Price: priceInt64ToFloat64(sIntent.Amount),
|
||||
Currency: string(sIntent.Currency),
|
||||
|
||||
OrderId: orderId,
|
||||
}
|
||||
return notifyResult, nil
|
||||
}
|
||||
|
||||
func (pp *StripePaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (pp *StripePaymentProvider) GetResponseError(err error) string {
|
||||
if err == nil {
|
||||
return "success"
|
||||
} else {
|
||||
return "fail"
|
||||
}
|
||||
}
|
14
pp/util.go
14
pp/util.go
@ -16,6 +16,8 @@ package pp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -35,3 +37,15 @@ func parseAttachString(s string) (string, string, string, error) {
|
||||
}
|
||||
return tokens[0], tokens[1], tokens[2], nil
|
||||
}
|
||||
|
||||
func priceInt64ToFloat64(price int64) float64 {
|
||||
return float64(price) / 100
|
||||
}
|
||||
|
||||
func priceFloat64ToInt64(price float64) int64 {
|
||||
return int64(math.Round(price * 100))
|
||||
}
|
||||
|
||||
func priceFloat64ToString(price float64) string {
|
||||
return strconv.FormatFloat(price, 'f', 2, 64)
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ func (pp *WechatPaymentProvider) Notify(request *http.Request, body []byte, auth
|
||||
ProductName: productName,
|
||||
ProductDisplayName: productDisplayName,
|
||||
ProviderName: providerName,
|
||||
OutOrderId: orderId,
|
||||
OrderId: orderId,
|
||||
Price: price,
|
||||
PaymentStatus: PaymentStatePaid,
|
||||
PaymentName: paymentName,
|
||||
|
@ -48,7 +48,7 @@ class PaymentResultPage extends React.Component {
|
||||
});
|
||||
// window.console.log("payment=", res.data);
|
||||
if (res.data.state === "Created") {
|
||||
if (res.data.type === "PayPal") {
|
||||
if (["PayPal", "Stripe"].includes(res.data.type)) {
|
||||
this.setState({
|
||||
timeout: setTimeout(() => {
|
||||
PaymentBackend.notifyPayment(this.state.organizationName, this.state.paymentName)
|
||||
@ -135,6 +135,26 @@ class PaymentResultPage extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (payment.state === "Timeout") {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
Setting.renderHelmet(payment)
|
||||
}
|
||||
<Result
|
||||
status="warning"
|
||||
title={`${i18next.t("payment:The payment has time out")}: ${payment.productDisplayName}, ${i18next.t("payment:the current state is")}: ${payment.state}`}
|
||||
subTitle={i18next.t("payment:Please click the below button to return to the original website")}
|
||||
extra={[
|
||||
<Button type="primary" key="returnUrl" onClick={() => {
|
||||
this.goToPaymentUrl(payment);
|
||||
}}>
|
||||
{i18next.t("payment:Return to Website")}
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
|
@ -163,6 +163,8 @@ class ProductBuyPage extends React.Component {
|
||||
text = i18next.t("product:WeChat Pay");
|
||||
} else if (provider.type === "PayPal") {
|
||||
text = i18next.t("product:PayPal");
|
||||
} else if (provider.type === "Stripe") {
|
||||
text = i18next.t("product:Stripe");
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -217,6 +217,10 @@ export const OtherProviderInfo = {
|
||||
logo: `${StaticBaseUrl}/img/payment_paypal.png`,
|
||||
url: "https://www.paypal.com/",
|
||||
},
|
||||
"Stripe": {
|
||||
logo: `${StaticBaseUrl}/img/social_stripe.png`,
|
||||
url: "https://stripe.com/",
|
||||
},
|
||||
"GC": {
|
||||
logo: `${StaticBaseUrl}/img/payment_gc.png`,
|
||||
url: "https://gc.org",
|
||||
@ -933,6 +937,7 @@ export function getProviderTypeOptions(category) {
|
||||
{id: "Alipay", name: "Alipay"},
|
||||
{id: "WeChat Pay", name: "WeChat Pay"},
|
||||
{id: "PayPal", name: "PayPal"},
|
||||
{id: "Stripe", name: "Stripe"},
|
||||
{id: "GC", name: "GC"},
|
||||
]);
|
||||
} else if (category === "Captcha") {
|
||||
|
Loading…
x
Reference in New Issue
Block a user