fix: fix paypal payment provider and refactor payment code (#2159)

* feat: support paypal payment provider

* feat: support paypal flow

* feat: use owner replace org for payment

* feat: update paypal logic

* feat: gofumpt

* feat: update payment

* fix: fix notify

* feat: delete log
This commit is contained in:
haiwu 2023-07-30 11:54:42 +08:00 committed by GitHub
parent 026fb207b3
commit eefa1e6df4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 353 additions and 172 deletions

View File

@ -31,7 +31,6 @@ import (
// @router /get-payments [get] // @router /get-payments [get]
func (c *ApiController) GetPayments() { func (c *ApiController) GetPayments() {
owner := c.Input().Get("owner") owner := c.Input().Get("owner")
organization := c.Input().Get("organization")
limit := c.Input().Get("pageSize") limit := c.Input().Get("pageSize")
page := c.Input().Get("p") page := c.Input().Get("p")
field := c.Input().Get("field") field := c.Input().Get("field")
@ -49,14 +48,14 @@ func (c *ApiController) GetPayments() {
c.ResponseOk(payments) c.ResponseOk(payments)
} else { } else {
limit := util.ParseInt(limit) limit := util.ParseInt(limit)
count, err := object.GetPaymentCount(owner, organization, field, value) count, err := object.GetPaymentCount(owner, field, value)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
paginator := pagination.SetPaginator(c.Ctx, limit, count) paginator := pagination.SetPaginator(c.Ctx, limit, count)
payments, err := object.GetPaginationPayments(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder) payments, err := object.GetPaginationPayments(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -77,10 +76,9 @@ func (c *ApiController) GetPayments() {
// @router /get-user-payments [get] // @router /get-user-payments [get]
func (c *ApiController) GetUserPayments() { func (c *ApiController) GetUserPayments() {
owner := c.Input().Get("owner") owner := c.Input().Get("owner")
organization := c.Input().Get("organization")
user := c.Input().Get("user") user := c.Input().Get("user")
payments, err := object.GetUserPayments(owner, organization, user) payments, err := object.GetUserPayments(owner, user)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -177,24 +175,18 @@ func (c *ApiController) DeletePayment() {
// @router /notify-payment [post] // @router /notify-payment [post]
func (c *ApiController) NotifyPayment() { func (c *ApiController) NotifyPayment() {
owner := c.Ctx.Input.Param(":owner") owner := c.Ctx.Input.Param(":owner")
providerName := c.Ctx.Input.Param(":provider")
productName := c.Ctx.Input.Param(":product")
paymentName := c.Ctx.Input.Param(":payment") paymentName := c.Ctx.Input.Param(":payment")
orderId := c.Ctx.Input.Param("order") orderId := c.Ctx.Input.Param("order")
body := c.Ctx.Input.RequestBody body := c.Ctx.Input.RequestBody
err, errorResponse := object.NotifyPayment(c.Ctx.Request, body, owner, providerName, productName, paymentName, orderId) payment, err := object.NotifyPayment(c.Ctx.Request, body, owner, paymentName, orderId)
_, err2 := c.Ctx.ResponseWriter.Write([]byte(errorResponse))
if err2 != nil {
panic(err2)
}
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
c.ResponseOk(payment)
} }
// InvoicePayment // InvoicePayment

View File

@ -431,7 +431,7 @@ func organizationChangeTrigger(oldName string, newName string) error {
} }
payment := new(Payment) payment := new(Payment)
payment.Organization = newName payment.Owner = newName
_, err = session.Where("organization=?", oldName).Update(payment) _, err = session.Where("organization=?", oldName).Update(payment)
if err != nil { if err != nil {
return err return err

View File

@ -18,6 +18,8 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/xorm-io/core" "github.com/xorm-io/core"
) )
@ -27,38 +29,39 @@ type Payment struct {
Name string `xorm:"varchar(100) notnull pk" json:"name"` Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"` CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"` DisplayName string `xorm:"varchar(100)" json:"displayName"`
// Payment Provider Info
Provider string `xorm:"varchar(100)" json:"provider"` Provider string `xorm:"varchar(100)" json:"provider"`
Type string `xorm:"varchar(100)" json:"type"` Type string `xorm:"varchar(100)" json:"type"`
Organization string `xorm:"varchar(100)" json:"organization"` // Product Info
User string `xorm:"varchar(100)" json:"user"` ProductName string `xorm:"varchar(100)" json:"productName"`
ProductName string `xorm:"varchar(100)" json:"productName"` ProductDisplayName string `xorm:"varchar(100)" json:"productDisplayName"`
ProductDisplayName string `xorm:"varchar(100)" json:"productDisplayName"` Detail string `xorm:"varchar(255)" json:"detail"`
Tag string `xorm:"varchar(100)" json:"tag"`
Detail string `xorm:"varchar(255)" json:"detail"` Currency string `xorm:"varchar(100)" json:"currency"`
Tag string `xorm:"varchar(100)" json:"tag"` Price float64 `json:"price"`
Currency string `xorm:"varchar(100)" json:"currency"` ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
Price float64 `json:"price"` // Payer Info
User string `xorm:"varchar(100)" json:"user"`
PayUrl string `xorm:"varchar(2000)" json:"payUrl"` PersonName string `xorm:"varchar(100)" json:"personName"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"` PersonIdCard string `xorm:"varchar(100)" json:"personIdCard"`
State string `xorm:"varchar(100)" json:"state"` PersonEmail string `xorm:"varchar(100)" json:"personEmail"`
Message string `xorm:"varchar(2000)" json:"message"` PersonPhone string `xorm:"varchar(100)" json:"personPhone"`
// Invoice Info
PersonName string `xorm:"varchar(100)" json:"personName"`
PersonIdCard string `xorm:"varchar(100)" json:"personIdCard"`
PersonEmail string `xorm:"varchar(100)" json:"personEmail"`
PersonPhone string `xorm:"varchar(100)" json:"personPhone"`
InvoiceType string `xorm:"varchar(100)" json:"invoiceType"` InvoiceType string `xorm:"varchar(100)" json:"invoiceType"`
InvoiceTitle string `xorm:"varchar(100)" json:"invoiceTitle"` InvoiceTitle string `xorm:"varchar(100)" json:"invoiceTitle"`
InvoiceTaxId string `xorm:"varchar(100)" json:"invoiceTaxId"` InvoiceTaxId string `xorm:"varchar(100)" json:"invoiceTaxId"`
InvoiceRemark string `xorm:"varchar(100)" json:"invoiceRemark"` InvoiceRemark string `xorm:"varchar(100)" json:"invoiceRemark"`
InvoiceUrl string `xorm:"varchar(255)" json:"invoiceUrl"` InvoiceUrl string `xorm:"varchar(255)" json:"invoiceUrl"`
// Order Info
OutOrderId string `xorm:"varchar(100)" json:"outOrderId"`
PayUrl string `xorm:"varchar(2000)" json:"payUrl"`
State pp.PaymentState `xorm:"varchar(100)" json:"state"`
Message string `xorm:"varchar(2000)" json:"message"`
} }
func GetPaymentCount(owner, organization, field, value string) (int64, error) { func GetPaymentCount(owner, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "") session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Payment{Organization: organization}) return session.Count(&Payment{Owner: owner})
} }
func GetPayments(owner string) ([]*Payment, error) { func GetPayments(owner string) ([]*Payment, error) {
@ -71,9 +74,9 @@ func GetPayments(owner string) ([]*Payment, error) {
return payments, nil return payments, nil
} }
func GetUserPayments(owner string, organization string, user string) ([]*Payment, error) { func GetUserPayments(owner, user string) ([]*Payment, error) {
payments := []*Payment{} payments := []*Payment{}
err := ormer.Engine.Desc("created_time").Find(&payments, &Payment{Owner: owner, Organization: organization, User: user}) err := ormer.Engine.Desc("created_time").Find(&payments, &Payment{Owner: owner, User: user})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -81,10 +84,10 @@ func GetUserPayments(owner string, organization string, user string) ([]*Payment
return payments, nil return payments, nil
} }
func GetPaginationPayments(owner, organization string, offset, limit int, field, value, sortField, sortOrder string) ([]*Payment, error) { func GetPaginationPayments(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Payment, error) {
payments := []*Payment{} payments := []*Payment{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder) session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&payments, &Payment{Organization: organization}) err := session.Find(&payments, &Payment{Owner: owner})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -125,7 +128,7 @@ func UpdatePayment(id string, payment *Payment) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(payment) affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(payment)
if err != nil { if err != nil {
panic(err) return false, err
} }
return affected != 0, nil return affected != 0, nil
@ -149,73 +152,72 @@ func DeletePayment(payment *Payment) (bool, error) {
return affected != 0, nil return affected != 0, nil
} }
func notifyPayment(request *http.Request, body []byte, owner string, providerName string, productName string, paymentName string, orderId string) (*Payment, error, string) { func notifyPayment(request *http.Request, body []byte, owner string, paymentName string, orderId string) (*Payment, *pp.NotifyResult, error) {
provider, err := getProvider(owner, providerName)
if err != nil {
panic(err)
}
pProvider, cert, err := provider.getPaymentProvider()
if err != nil {
panic(err)
}
payment, err := getPayment(owner, paymentName) payment, err := getPayment(owner, paymentName)
if err != nil { if err != nil {
panic(err) return nil, nil, err
} }
if payment == nil { if payment == nil {
err = fmt.Errorf("the payment: %s does not exist", paymentName) err = fmt.Errorf("the payment: %s does not exist", paymentName)
return nil, err, pProvider.GetResponseError(err) return nil, nil, err
} }
product, err := getProduct(owner, productName) provider, err := getProvider(owner, payment.Provider)
if err != nil { if err != nil {
panic(err) return nil, nil, err
}
pProvider, cert, err := provider.getPaymentProvider()
if err != nil {
return nil, nil, err
} }
product, err := getProduct(owner, payment.ProductName)
if err != nil {
return nil, nil, err
}
if product == nil { if product == nil {
err = fmt.Errorf("the product: %s does not exist", productName) err = fmt.Errorf("the product: %s does not exist", payment.ProductName)
return payment, err, pProvider.GetResponseError(err) return nil, nil, err
} }
productDisplayName, paymentName, price, productName, providerName, err := pProvider.Notify(request, body, cert.AuthorityPublicKey, orderId) if orderId == "" {
orderId = payment.OutOrderId
}
notifyResult, err := pProvider.Notify(request, body, cert.AuthorityPublicKey, orderId)
if err != nil { if err != nil {
return payment, err, pProvider.GetResponseError(err) return payment, notifyResult, err
} }
if productDisplayName != "" && productDisplayName != product.DisplayName { if notifyResult.ProductDisplayName != "" && notifyResult.ProductDisplayName != product.DisplayName {
err = fmt.Errorf("the payment's product name: %s doesn't equal to the expected product name: %s", productDisplayName, product.DisplayName) err = fmt.Errorf("the payment's product name: %s doesn't equal to the expected product name: %s", notifyResult.ProductDisplayName, product.DisplayName)
return payment, err, pProvider.GetResponseError(err) return payment, notifyResult, err
} }
if price != product.Price { if notifyResult.Price != product.Price {
err = fmt.Errorf("the payment's price: %f doesn't equal to the expected price: %f", price, product.Price) err = fmt.Errorf("the payment's price: %f doesn't equal to the expected price: %f", notifyResult.Price, product.Price)
return payment, err, pProvider.GetResponseError(err) return payment, notifyResult, err
} }
err = nil return payment, notifyResult, err
return payment, err, pProvider.GetResponseError(err)
} }
func NotifyPayment(request *http.Request, body []byte, owner string, providerName string, productName string, paymentName string, orderId string) (error, string) { func NotifyPayment(request *http.Request, body []byte, owner string, paymentName string, orderId string) (*Payment, error) {
payment, err, errorResponse := notifyPayment(request, body, owner, providerName, productName, paymentName, orderId) payment, notifyResult, err := notifyPayment(request, body, owner, paymentName, orderId)
if payment != nil { if payment != nil {
if err != nil { if err != nil {
payment.State = "Error" payment.State = pp.PaymentStateError
payment.Message = err.Error() payment.Message = err.Error()
} else { } else {
payment.State = "Paid" payment.State = notifyResult.PaymentStatus
} }
_, err = UpdatePayment(payment.GetId(), payment) _, err = UpdatePayment(payment.GetId(), payment)
if err != nil { if err != nil {
panic(err) return nil, err
} }
} }
return err, errorResponse return payment, nil
} }
func invoicePayment(payment *Payment) (string, error) { func invoicePayment(payment *Payment) (string, error) {
@ -242,7 +244,7 @@ func invoicePayment(payment *Payment) (string, error) {
} }
func InvoicePayment(payment *Payment) (string, error) { func InvoicePayment(payment *Payment) (string, error) {
if payment.State != "Paid" { if payment.State != pp.PaymentStatePaid {
return "", fmt.Errorf("the payment state is supposed to be: \"%s\", got: \"%s\"", "Paid", payment.State) return "", fmt.Errorf("the payment state is supposed to be: \"%s\", got: \"%s\"", "Paid", payment.State)
} }

View File

@ -17,6 +17,8 @@ package object
import ( import (
"fmt" "fmt"
"github.com/casdoor/casdoor/pp"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/xorm-io/core" "github.com/xorm-io/core"
) )
@ -183,36 +185,39 @@ func BuyProduct(id string, providerName string, user *User, host string) (string
productDisplayName := product.DisplayName productDisplayName := product.DisplayName
originFrontend, originBackend := getOriginFromHost(host) originFrontend, originBackend := getOriginFromHost(host)
returnUrl := fmt.Sprintf("%s/payments/%s/result", originFrontend, paymentName) returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName)
notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s/%s/%s", originBackend, owner, providerName, productName, paymentName) notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s", originBackend, owner, paymentName)
// Create an Order and get the payUrl
payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl) payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
// Create a Payment linked with Product and Order
payment := Payment{ payment := Payment{
Owner: "admin", Owner: product.Owner,
Name: paymentName, Name: paymentName,
CreatedTime: util.GetCurrentTime(), CreatedTime: util.GetCurrentTime(),
DisplayName: paymentName, DisplayName: paymentName,
Provider: provider.Name,
Type: provider.Type, Provider: provider.Name,
Organization: user.Owner, Type: provider.Type,
User: user.Name,
ProductName: productName, ProductName: productName,
ProductDisplayName: productDisplayName, ProductDisplayName: productDisplayName,
Detail: product.Detail, Detail: product.Detail,
Tag: product.Tag, Tag: product.Tag,
Currency: product.Currency, Currency: product.Currency,
Price: product.Price, Price: product.Price,
PayUrl: payUrl,
ReturnUrl: product.ReturnUrl, ReturnUrl: product.ReturnUrl,
State: "Created",
User: user.Name,
PayUrl: payUrl,
State: pp.PaymentStateCreated,
OutOrderId: orderId,
} }
if provider.Type == "Dummy" { if provider.Type == "Dummy" {
payment.State = "Paid" payment.State = pp.PaymentStatePaid
} }
affected, err := AddPayment(&payment) affected, err := AddPayment(&payment)

View File

@ -16,7 +16,6 @@ package pp
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
@ -67,10 +66,10 @@ func (pp *AlipayPaymentProvider) Pay(providerName string, productName string, pa
return payUrl, "", nil return payUrl, "", nil
} }
func (pp *AlipayPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (string, string, float64, string, string, error) { func (pp *AlipayPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) {
bm, err := alipay.ParseNotifyToBodyMap(request) bm, err := alipay.ParseNotifyToBodyMap(request)
if err != nil { if err != nil {
return "", "", 0, "", "", err return nil, err
} }
providerName := bm.Get("providerName") providerName := bm.Get("providerName")
@ -82,13 +81,21 @@ func (pp *AlipayPaymentProvider) Notify(request *http.Request, body []byte, auth
ok, err := alipay.VerifySignWithCert(authorityPublicKey, bm) ok, err := alipay.VerifySignWithCert(authorityPublicKey, bm)
if err != nil { if err != nil {
return "", "", 0, "", "", err return nil, err
} }
if !ok { if !ok {
return "", "", 0, "", "", fmt.Errorf("VerifySignWithCert() failed: %v", ok) return nil, err
} }
notifyResult := &NotifyResult{
return productDisplayName, paymentName, price, productName, providerName, nil ProductName: productName,
ProductDisplayName: productDisplayName,
ProviderName: providerName,
OutOrderId: orderId,
PaymentStatus: PaymentStatePaid,
Price: price,
PaymentName: paymentName,
}
return notifyResult, nil
} }
func (pp *AlipayPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) { func (pp *AlipayPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {

View File

@ -31,8 +31,10 @@ func (pp *DummyPaymentProvider) Pay(providerName string, productName string, pay
return payUrl, "", nil return payUrl, "", nil
} }
func (pp *DummyPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (string, string, float64, string, string, error) { func (pp *DummyPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) {
return "", "", 0, "", "", nil return &NotifyResult{
PaymentStatus: PaymentStatePaid,
}, nil
} }
func (pp *DummyPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) { func (pp *DummyPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {

View File

@ -216,11 +216,11 @@ func (pp *GcPaymentProvider) Pay(providerName string, productName string, payerN
return payRespInfo.PayUrl, "", nil return payRespInfo.PayUrl, "", nil
} }
func (pp *GcPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (string, string, float64, string, string, error) { func (pp *GcPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) {
reqBody := GcRequestBody{} reqBody := GcRequestBody{}
m, err := url.ParseQuery(string(body)) m, err := url.ParseQuery(string(body))
if err != nil { if err != nil {
return "", "", 0, "", "", err return nil, err
} }
reqBody.Op = m["op"][0] reqBody.Op = m["op"][0]
@ -232,13 +232,13 @@ func (pp *GcPaymentProvider) Notify(request *http.Request, body []byte, authorit
notifyReqInfoBytes, err := base64.StdEncoding.DecodeString(reqBody.Data) notifyReqInfoBytes, err := base64.StdEncoding.DecodeString(reqBody.Data)
if err != nil { if err != nil {
return "", "", 0, "", "", err return nil, err
} }
var notifyRespInfo GcNotifyRespInfo var notifyRespInfo GcNotifyRespInfo
err = json.Unmarshal(notifyReqInfoBytes, &notifyRespInfo) err = json.Unmarshal(notifyReqInfoBytes, &notifyRespInfo)
if err != nil { if err != nil {
return "", "", 0, "", "", err return nil, err
} }
providerName := "" providerName := ""
@ -249,10 +249,18 @@ func (pp *GcPaymentProvider) Notify(request *http.Request, body []byte, authorit
price := notifyRespInfo.Amount price := notifyRespInfo.Amount
if notifyRespInfo.OrderState != "1" { if notifyRespInfo.OrderState != "1" {
return "", "", 0, "", "", fmt.Errorf("error order state: %s", notifyRespInfo.OrderDate) return nil, fmt.Errorf("error order state: %s", notifyRespInfo.OrderDate)
} }
notifyResult := &NotifyResult{
return productDisplayName, paymentName, price, productName, providerName, nil ProductName: productName,
ProductDisplayName: productDisplayName,
ProviderName: providerName,
OutOrderId: orderId,
Price: price,
PaymentStatus: PaymentStatePaid,
PaymentName: paymentName,
}
return notifyResult, nil
} }
func (pp *GcPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) { func (pp *GcPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {

View File

@ -20,6 +20,8 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"github.com/casdoor/casdoor/conf"
"github.com/go-pay/gopay" "github.com/go-pay/gopay"
"github.com/go-pay/gopay/paypal" "github.com/go-pay/gopay/paypal"
"github.com/go-pay/gopay/pkg/util" "github.com/go-pay/gopay/pkg/util"
@ -31,8 +33,14 @@ type PaypalPaymentProvider struct {
func NewPaypalPaymentProvider(clientID string, secret string) (*PaypalPaymentProvider, error) { func NewPaypalPaymentProvider(clientID string, secret string) (*PaypalPaymentProvider, error) {
pp := &PaypalPaymentProvider{} pp := &PaypalPaymentProvider{}
isProd := false
client, err := paypal.NewClient(clientID, secret, false) if conf.GetConfigString("runmode") == "prod" {
isProd = true
}
client, err := paypal.NewClient(clientID, secret, isProd)
//if !isProd {
// client.DebugSwitch = gopay.DebugOn
//}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -42,27 +50,27 @@ 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) { 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) {
// pp.Client.DebugSwitch = gopay.DebugOn // Set log to terminal stdout // https://github.com/go-pay/gopay/blob/main/doc/paypal.md
priceStr := strconv.FormatFloat(price, 'f', 2, 64) priceStr := strconv.FormatFloat(price, 'f', 2, 64)
var pus []*paypal.PurchaseUnit units := make([]*paypal.PurchaseUnit, 0, 1)
item := &paypal.PurchaseUnit{ unit := &paypal.PurchaseUnit{
ReferenceId: util.GetRandomString(16), ReferenceId: util.GetRandomString(16),
Amount: &paypal.Amount{ Amount: &paypal.Amount{
CurrencyCode: currency, CurrencyCode: currency, // e.g."USD"
Value: priceStr, Value: priceStr, // e.g."100.00"
}, },
Description: joinAttachString([]string{productDisplayName, productName, providerName}), Description: joinAttachString([]string{productDisplayName, productName, providerName}),
} }
pus = append(pus, item) units = append(units, unit)
bm := make(gopay.BodyMap) bm := make(gopay.BodyMap)
bm.Set("intent", "CAPTURE") bm.Set("intent", "CAPTURE")
bm.Set("purchase_units", pus) bm.Set("purchase_units", units)
bm.SetBodyMap("application_context", func(b gopay.BodyMap) { bm.SetBodyMap("application_context", func(b gopay.BodyMap) {
b.Set("brand_name", "Casdoor") b.Set("brand_name", "Casdoor")
b.Set("locale", "en-PT") b.Set("locale", "en-PT")
b.Set("return_url", returnUrl) b.Set("return_url", returnUrl)
b.Set("cancel_url", returnUrl)
}) })
ppRsp, err := pp.Client.CreateOrder(context.Background(), bm) ppRsp, err := pp.Client.CreateOrder(context.Background(), bm)
@ -72,31 +80,65 @@ func (pp *PaypalPaymentProvider) Pay(providerName string, productName string, pa
if ppRsp.Code != paypal.Success { if ppRsp.Code != paypal.Success {
return "", "", errors.New(ppRsp.Error) return "", "", errors.New(ppRsp.Error)
} }
// {"id":"9BR68863NE220374S","status":"CREATED",
// "links":[{"href":"https://api.sandbox.paypal.com/v2/checkout/orders/9BR68863NE220374S","rel":"self","method":"GET"},
// {"href":"https://www.sandbox.paypal.com/checkoutnow?token=9BR68863NE220374S","rel":"approve","method":"GET"},
// {"href":"https://api.sandbox.paypal.com/v2/checkout/orders/9BR68863NE220374S","rel":"update","method":"PATCH"},
// {"href":"https://api.sandbox.paypal.com/v2/checkout/orders/9BR68863NE220374S/capture","rel":"capture","method":"POST"}]}
return ppRsp.Response.Links[1].Href, ppRsp.Response.Id, nil return ppRsp.Response.Links[1].Href, ppRsp.Response.Id, nil
} }
func (pp *PaypalPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (string, string, float64, string, string, error) { func (pp *PaypalPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) {
ppRsp, err := pp.Client.OrderCapture(context.Background(), orderId, nil) captureRsp, err := pp.Client.OrderCapture(context.Background(), orderId, nil)
if err != nil { if err != nil {
return "", "", 0, "", "", err return nil, err
} }
if ppRsp.Code != paypal.Success { if captureRsp.Code != paypal.Success {
return "", "", 0, "", "", errors.New(ppRsp.Error) // If order is already captured, just skip this type of error and check the order detail
if !(len(captureRsp.ErrorResponse.Details) == 1 && captureRsp.ErrorResponse.Details[0].Issue == "ORDER_ALREADY_CAPTURED") {
return nil, errors.New(captureRsp.ErrorResponse.Message)
}
}
// Check the order detail
detailRsp, err := pp.Client.OrderDetail(context.Background(), orderId, nil)
if err != nil {
return nil, err
}
if captureRsp.Code != paypal.Success {
return nil, errors.New(captureRsp.ErrorResponse.Message)
} }
paymentName := ppRsp.Response.Id paymentName := detailRsp.Response.Id
price, err := strconv.ParseFloat(ppRsp.Response.PurchaseUnits[0].Amount.Value, 64) price, err := strconv.ParseFloat(detailRsp.Response.PurchaseUnits[0].Amount.Value, 64)
if err != nil { if err != nil {
return "", "", 0, "", "", err return nil, err
} }
currency := detailRsp.Response.PurchaseUnits[0].Amount.CurrencyCode
productDisplayName, productName, providerName, err := parseAttachString(ppRsp.Response.PurchaseUnits[0].Description) productDisplayName, productName, providerName, err := parseAttachString(detailRsp.Response.PurchaseUnits[0].Description)
if err != nil { if err != nil {
return "", "", 0, "", "", err return nil, err
} }
// TODO: status better handler, e.g.`hanging`
var paymentStatus PaymentState
switch detailRsp.Response.Status { // CREATED、SAVED、APPROVED、VOIDED、COMPLETED、PAYER_ACTION_REQUIRED
case "COMPLETED":
paymentStatus = PaymentStatePaid
default:
paymentStatus = PaymentStateError
}
notifyResult := &NotifyResult{
PaymentStatus: paymentStatus,
PaymentName: paymentName,
return productDisplayName, paymentName, price, productName, providerName, nil ProductName: productName,
ProductDisplayName: productDisplayName,
ProviderName: providerName,
Price: price,
Currency: currency,
OutOrderId: orderId,
}
return notifyResult, nil
} }
func (pp *PaypalPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) { func (pp *PaypalPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {

View File

@ -14,11 +14,34 @@
package pp package pp
import "net/http" import (
"net/http"
)
type PaymentState string
const (
PaymentStatePaid PaymentState = "Paid"
PaymentStateCreated PaymentState = "Created"
PaymentStateError PaymentState = "Error"
)
type NotifyResult struct {
PaymentName string
PaymentStatus PaymentState
ProviderName string
ProductName string
ProductDisplayName string
Price float64
Currency string
OutOrderId string
}
type PaymentProvider interface { type PaymentProvider interface {
Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error)
Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (string, string, float64, string, string, error) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error)
GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error)
GetResponseError(err error) string GetResponseError(err error) string
} }

View File

@ -83,22 +83,22 @@ func (pp *WechatPaymentProvider) Pay(providerName string, productName string, pa
return wxRsp.Response.CodeUrl, "", nil return wxRsp.Response.CodeUrl, "", nil
} }
func (pp *WechatPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (string, string, float64, string, string, error) { func (pp *WechatPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) {
notifyReq, err := wechat.V3ParseNotify(request) notifyReq, err := wechat.V3ParseNotify(request)
if err != nil { if err != nil {
panic(err) return nil, err
} }
cert := pp.Client.WxPublicKey() cert := pp.Client.WxPublicKey()
err = notifyReq.VerifySignByPK(cert) err = notifyReq.VerifySignByPK(cert)
if err != nil { if err != nil {
return "", "", 0, "", "", err return nil, err
} }
apiKey := string(pp.Client.ApiV3Key) apiKey := string(pp.Client.ApiV3Key)
result, err := notifyReq.DecryptCipherText(apiKey) result, err := notifyReq.DecryptCipherText(apiKey)
if err != nil { if err != nil {
return "", "", 0, "", "", err return nil, err
} }
paymentName := result.OutTradeNo paymentName := result.OutTradeNo
@ -106,10 +106,19 @@ func (pp *WechatPaymentProvider) Notify(request *http.Request, body []byte, auth
productDisplayName, productName, providerName, err := parseAttachString(result.Attach) productDisplayName, productName, providerName, err := parseAttachString(result.Attach)
if err != nil { if err != nil {
return "", "", 0, "", "", err return nil, err
} }
return productDisplayName, paymentName, price, productName, providerName, nil notifyResult := &NotifyResult{
ProductName: productName,
ProductDisplayName: productDisplayName,
ProviderName: providerName,
OutOrderId: orderId,
Price: price,
PaymentStatus: PaymentStatePaid,
PaymentName: paymentName,
}
return notifyResult, nil
} }
func (pp *WechatPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) { func (pp *WechatPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {

View File

@ -255,7 +255,7 @@ func initAPI() {
beego.Router("/api/update-payment", &controllers.ApiController{}, "POST:UpdatePayment") beego.Router("/api/update-payment", &controllers.ApiController{}, "POST:UpdatePayment")
beego.Router("/api/add-payment", &controllers.ApiController{}, "POST:AddPayment") beego.Router("/api/add-payment", &controllers.ApiController{}, "POST:AddPayment")
beego.Router("/api/delete-payment", &controllers.ApiController{}, "POST:DeletePayment") beego.Router("/api/delete-payment", &controllers.ApiController{}, "POST:DeletePayment")
beego.Router("/api/notify-payment/?:owner/?:provider/?:product/?:payment", &controllers.ApiController{}, "POST:NotifyPayment") beego.Router("/api/notify-payment/?:owner/?:payment", &controllers.ApiController{}, "POST:NotifyPayment")
beego.Router("/api/invoice-payment", &controllers.ApiController{}, "POST:InvoicePayment") beego.Router("/api/invoice-payment", &controllers.ApiController{}, "POST:InvoicePayment")
beego.Router("/api/send-email", &controllers.ApiController{}, "POST:SendEmail") beego.Router("/api/send-email", &controllers.ApiController{}, "POST:SendEmail")

View File

@ -643,8 +643,8 @@ class App extends Component {
<Route exact path="/products/:organizationName/:productName" render={(props) => this.renderLoginIfNotLoggedIn(<ProductEditPage account={this.state.account} {...props} />)} /> <Route exact path="/products/:organizationName/:productName" render={(props) => this.renderLoginIfNotLoggedIn(<ProductEditPage account={this.state.account} {...props} />)} />
<Route exact path="/products/:organizationName/:productName/buy" render={(props) => this.renderLoginIfNotLoggedIn(<ProductBuyPage account={this.state.account} {...props} />)} /> <Route exact path="/products/:organizationName/:productName/buy" render={(props) => this.renderLoginIfNotLoggedIn(<ProductBuyPage account={this.state.account} {...props} />)} />
<Route exact path="/payments" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentListPage account={this.state.account} {...props} />)} /> <Route exact path="/payments" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentListPage account={this.state.account} {...props} />)} />
<Route exact path="/payments/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)} /> <Route exact path="/payments/:organizationName/:paymentName" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentEditPage account={this.state.account} {...props} />)} />
<Route exact path="/payments/:paymentName/result" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentResultPage account={this.state.account} {...props} />)} /> <Route exact path="/payments/:organizationName/:paymentName/result" render={(props) => this.renderLoginIfNotLoggedIn(<PaymentResultPage account={this.state.account} {...props} />)} />
<Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)} /> <Route exact path="/records" render={(props) => this.renderLoginIfNotLoggedIn(<RecordListPage account={this.state.account} {...props} />)} />
<Route exact path="/mfa/setup" render={(props) => this.renderLoginIfNotLoggedIn(<MfaSetupPage account={this.state.account} onfinish={() => this.setState({requiredEnableMfa: false})} {...props} />)} /> <Route exact path="/mfa/setup" render={(props) => this.renderLoginIfNotLoggedIn(<MfaSetupPage account={this.state.account} onfinish={() => this.setState({requiredEnableMfa: false})} {...props} />)} />
<Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />} /> <Route exact path="/.well-known/openid-configuration" render={(props) => <OdicDiscoveryPage />} />

View File

@ -40,7 +40,7 @@ class PaymentEditPage extends React.Component {
} }
getPayment() { getPayment() {
PaymentBackend.getPayment("admin", this.state.paymentName) PaymentBackend.getPayment(this.state.organizationName, this.state.paymentName)
.then((res) => { .then((res) => {
if (res.data === null) { if (res.data === null) {
this.props.history.push("/404"); this.props.history.push("/404");

View File

@ -28,13 +28,12 @@ class PaymentListPage extends BaseListPage {
const randomName = Setting.getRandomName(); const randomName = Setting.getRandomName();
const organizationName = Setting.getRequestOrganization(this.props.account); const organizationName = Setting.getRequestOrganization(this.props.account);
return { return {
owner: "admin", owner: organizationName,
name: `payment_${randomName}`, name: `payment_${randomName}`,
createdTime: moment().format(), createdTime: moment().format(),
displayName: `New Payment - ${randomName}`, displayName: `New Payment - ${randomName}`,
provider: "provider_pay_paypal", provider: "provider_pay_paypal",
type: "PayPal", type: "PayPal",
organization: organizationName,
user: "admin", user: "admin",
productName: "computer-1", productName: "computer-1",
productDisplayName: "A notebook computer", productDisplayName: "A notebook computer",
@ -54,7 +53,7 @@ class PaymentListPage extends BaseListPage {
PaymentBackend.addPayment(newPayment) PaymentBackend.addPayment(newPayment)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
this.props.history.push({pathname: `/payments/${newPayment.name}`, mode: "add"}); this.props.history.push({pathname: `/payments/${newPayment.owner}/${newPayment.name}`, mode: "add"});
Setting.showMessage("success", i18next.t("general:Successfully added")); Setting.showMessage("success", i18next.t("general:Successfully added"));
} else { } else {
Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`); Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
@ -96,7 +95,7 @@ class PaymentListPage extends BaseListPage {
...this.getColumnSearchProps("name"), ...this.getColumnSearchProps("name"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/payments/${text}`}> <Link to={`/payments/${record.owner}/${text}`}>
{text} {text}
</Link> </Link>
); );
@ -112,7 +111,7 @@ class PaymentListPage extends BaseListPage {
...this.getColumnSearchProps("provider"), ...this.getColumnSearchProps("provider"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/providers/${text}`}> <Link to={`/providers/${record.owner}/${text}`}>
{text} {text}
</Link> </Link>
); );
@ -120,11 +119,11 @@ class PaymentListPage extends BaseListPage {
}, },
{ {
title: i18next.t("general:Organization"), title: i18next.t("general:Organization"),
dataIndex: "organization", dataIndex: "owner",
key: "organization", key: "owner",
width: "120px", width: "120px",
sorter: true, sorter: true,
...this.getColumnSearchProps("organization"), ...this.getColumnSearchProps("owner"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/organizations/${text}`}> <Link to={`/organizations/${text}`}>
@ -142,7 +141,7 @@ class PaymentListPage extends BaseListPage {
...this.getColumnSearchProps("user"), ...this.getColumnSearchProps("user"),
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<Link to={`/users/${record.organization}/${text}`}> <Link to={`/users/${record.owner}/${text}`}>
{text} {text}
</Link> </Link>
); );
@ -222,8 +221,8 @@ class PaymentListPage extends BaseListPage {
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/payments/${record.name}/result`)}>{i18next.t("payment:Result")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/payments/${record.owner}/${record.name}/result`)}>{i18next.t("payment:Result")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/payments/${record.name}`)}>{i18next.t("general:Edit")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/payments/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal <PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`} title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deletePayment(index)} onConfirm={() => this.deletePayment(index)}
@ -266,7 +265,7 @@ class PaymentListPage extends BaseListPage {
value = params.type; value = params.type;
} }
this.setState({loading: true}); this.setState({loading: true});
PaymentBackend.getPayments("admin", Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder) PaymentBackend.getPayments(Setting.getRequestOrganization(this.props.account), Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => { .then((res) => {
this.setState({ this.setState({
loading: false, loading: false,

View File

@ -24,6 +24,7 @@ class PaymentResultPage extends React.Component {
this.state = { this.state = {
classes: props, classes: props,
paymentName: props.match.params.paymentName, paymentName: props.match.params.paymentName,
organizationName: props.match.params.organizationName,
payment: null, payment: null,
timeout: null, timeout: null,
}; };
@ -40,18 +41,37 @@ class PaymentResultPage extends React.Component {
} }
getPayment() { getPayment() {
PaymentBackend.getPayment("admin", this.state.paymentName) PaymentBackend.getPayment(this.state.organizationName, this.state.paymentName)
.then((res) => { .then((res) => {
this.setState({ this.setState({
payment: res.data, payment: res.data,
}); });
// window.console.log("payment=", res.data);
if (res.data.state === "Created") { if (res.data.state === "Created") {
this.setState({timeout: setTimeout(() => this.getPayment(), 1000)}); if (res.data.type === "PayPal") {
this.setState({
timeout: setTimeout(() => {
PaymentBackend.notifyPayment(this.state.organizationName, this.state.paymentName)
.then((res) => {
this.getPayment();
});
}, 1000),
});
} else {
this.setState({timeout: setTimeout(() => this.getPayment(), 1000)});
}
} }
}); });
} }
goToPaymentUrl(payment) {
if (payment.returnUrl === undefined || payment.returnUrl === null || payment.returnUrl === "") {
Setting.goToLink(`${window.location.origin}/products/${payment.owner}/${payment.productName}/buy`);
} else {
Setting.goToLink(payment.returnUrl);
}
}
render() { render() {
const payment = this.state.payment; const payment = this.state.payment;
@ -71,7 +91,7 @@ class PaymentResultPage extends React.Component {
subTitle={i18next.t("payment:Please click the below button to return to the original website")} subTitle={i18next.t("payment:Please click the below button to return to the original website")}
extra={[ extra={[
<Button type="primary" key="returnUrl" onClick={() => { <Button type="primary" key="returnUrl" onClick={() => {
Setting.goToLink(payment.returnUrl); this.goToPaymentUrl(payment);
}}> }}>
{i18next.t("payment:Return to Website")} {i18next.t("payment:Return to Website")}
</Button>, </Button>,
@ -107,7 +127,7 @@ class PaymentResultPage extends React.Component {
subTitle={i18next.t("payment:Please click the below button to return to the original website")} subTitle={i18next.t("payment:Please click the below button to return to the original website")}
extra={[ extra={[
<Button type="primary" key="returnUrl" onClick={() => { <Button type="primary" key="returnUrl" onClick={() => {
Setting.goToLink(payment.returnUrl); this.goToPaymentUrl(payment);
}}> }}>
{i18next.t("payment:Return to Website")} {i18next.t("payment:Return to Website")}
</Button>, </Button>,

View File

@ -24,7 +24,8 @@ class ProductBuyPage extends React.Component {
super(props); super(props);
this.state = { this.state = {
classes: props, classes: props,
productName: props.match?.params.productName, organizationName: props.organizationName !== undefined ? props.organizationName : props?.match?.params?.organizationName,
productName: props.productName !== undefined ? props.productName : props?.match?.params?.productName,
product: null, product: null,
isPlacingOrder: false, isPlacingOrder: false,
qrCodeModalProvider: null, qrCodeModalProvider: null,
@ -36,17 +37,15 @@ class ProductBuyPage extends React.Component {
} }
getProduct() { getProduct() {
if (this.state.productName === undefined) { if (this.state.productName === undefined || this.state.organizationName === undefined) {
return; return ;
} }
ProductBackend.getProduct(this.state.organizationName, this.state.productName)
ProductBackend.getProduct(this.props.account.owner, this.state.productName)
.then((res) => { .then((res) => {
if (res.status === "error") { if (res.status === "error") {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
return; return;
} }
this.setState({ this.setState({
product: res.data, product: res.data,
}); });
@ -97,7 +96,7 @@ class ProductBuyPage extends React.Component {
isPlacingOrder: true, isPlacingOrder: true,
}); });
ProductBackend.buyProduct(this.state.product.owner, this.state.productName, provider.name) ProductBackend.buyProduct(product.owner, product.name, provider.name)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
const payUrl = res.data; const payUrl = res.data;

View File

@ -102,6 +102,13 @@ class ProductListPage extends BaseListPage {
width: "150px", width: "150px",
sorter: true, sorter: true,
...this.getColumnSearchProps("owner"), ...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
}, },
{ {
title: i18next.t("general:Created time"), title: i18next.t("general:Created time"),
@ -189,6 +196,7 @@ class ProductListPage extends BaseListPage {
width: "500px", width: "500px",
...this.getColumnSearchProps("providers"), ...this.getColumnSearchProps("providers"),
render: (text, record, index) => { render: (text, record, index) => {
const providerOwner = record.owner;
const providers = text; const providers = text;
if (providers.length === 0) { if (providers.length === 0) {
return `(${i18next.t("general:empty")})`; return `(${i18next.t("general:empty")})`;
@ -207,9 +215,9 @@ class ProductListPage extends BaseListPage {
<List.Item> <List.Item>
<div style={{display: "inline"}}> <div style={{display: "inline"}}>
<Tooltip placement="topLeft" title="Edit"> <Tooltip placement="topLeft" title="Edit">
<Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/providers/${providerName}`)} /> <Button style={{marginRight: "5px"}} icon={<EditOutlined />} size="small" onClick={() => Setting.goToLinkSoft(this, `/providers/${providerOwner}/${providerName}`)} />
</Tooltip> </Tooltip>
<Link to={`/providers/${providerName}`}> <Link to={`/providers/${providerOwner}/${providerName}`}>
{providerName} {providerName}
</Link> </Link>
</div> </div>

View File

@ -79,3 +79,13 @@ export function invoicePayment(owner, name) {
}, },
}).then(res => res.json()); }).then(res => res.json());
} }
export function notifyPayment(owner, name) {
return fetch(`${Setting.ServerUrl}/api/notify-payment/${owner}/${name}`, {
method: "POST",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}

View File

@ -152,6 +152,10 @@
"Sending": "Sendet", "Sending": "Sendet",
"Submit and complete": "Einreichen und abschließen" "Submit and complete": "Einreichen und abschließen"
}, },
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer"
},
"forget": { "forget": {
"Account": "Konto", "Account": "Konto",
"Change Password": "Passwort ändern", "Change Password": "Passwort ändern",
@ -215,6 +219,7 @@
"Enable": "Enable", "Enable": "Enable",
"Enabled": "Enabled", "Enabled": "Enabled",
"Enabled successfully": "Enabled successfully", "Enabled successfully": "Enabled successfully",
"Enforcers": "Enforcers",
"Failed to add": "Fehler beim hinzufügen", "Failed to add": "Fehler beim hinzufügen",
"Failed to connect to server": "Die Verbindung zum Server konnte nicht hergestellt werden", "Failed to connect to server": "Die Verbindung zum Server konnte nicht hergestellt werden",
"Failed to delete": "Konnte nicht gelöscht werden", "Failed to delete": "Konnte nicht gelöscht werden",

View File

@ -152,6 +152,10 @@
"Sending": "Sending", "Sending": "Sending",
"Submit and complete": "Submit and complete" "Submit and complete": "Submit and complete"
}, },
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer"
},
"forget": { "forget": {
"Account": "Account", "Account": "Account",
"Change Password": "Change Password", "Change Password": "Change Password",
@ -215,6 +219,7 @@
"Enable": "Enable", "Enable": "Enable",
"Enabled": "Enabled", "Enabled": "Enabled",
"Enabled successfully": "Enabled successfully", "Enabled successfully": "Enabled successfully",
"Enforcers": "Enforcers",
"Failed to add": "Failed to add", "Failed to add": "Failed to add",
"Failed to connect to server": "Failed to connect to server", "Failed to connect to server": "Failed to connect to server",
"Failed to delete": "Failed to delete", "Failed to delete": "Failed to delete",

View File

@ -152,6 +152,10 @@
"Sending": "Envío", "Sending": "Envío",
"Submit and complete": "Enviar y completar" "Submit and complete": "Enviar y completar"
}, },
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer"
},
"forget": { "forget": {
"Account": "Cuenta", "Account": "Cuenta",
"Change Password": "Cambiar contraseña", "Change Password": "Cambiar contraseña",
@ -215,6 +219,7 @@
"Enable": "Enable", "Enable": "Enable",
"Enabled": "Enabled", "Enabled": "Enabled",
"Enabled successfully": "Enabled successfully", "Enabled successfully": "Enabled successfully",
"Enforcers": "Enforcers",
"Failed to add": "No se pudo agregar", "Failed to add": "No se pudo agregar",
"Failed to connect to server": "No se pudo conectar al servidor", "Failed to connect to server": "No se pudo conectar al servidor",
"Failed to delete": "No se pudo eliminar", "Failed to delete": "No se pudo eliminar",

View File

@ -152,6 +152,10 @@
"Sending": "Envoi", "Sending": "Envoi",
"Submit and complete": "Soumettre et compléter" "Submit and complete": "Soumettre et compléter"
}, },
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer"
},
"forget": { "forget": {
"Account": "Compte", "Account": "Compte",
"Change Password": "Changer le mot de passe", "Change Password": "Changer le mot de passe",
@ -215,6 +219,7 @@
"Enable": "Enable", "Enable": "Enable",
"Enabled": "Enabled", "Enabled": "Enabled",
"Enabled successfully": "Enabled successfully", "Enabled successfully": "Enabled successfully",
"Enforcers": "Enforcers",
"Failed to add": "Échec d'ajout", "Failed to add": "Échec d'ajout",
"Failed to connect to server": "Échec de la connexion au serveur", "Failed to connect to server": "Échec de la connexion au serveur",
"Failed to delete": "Échec de la suppression", "Failed to delete": "Échec de la suppression",

View File

@ -152,6 +152,10 @@
"Sending": "Mengirimkan", "Sending": "Mengirimkan",
"Submit and complete": "Kirim dan selesaikan" "Submit and complete": "Kirim dan selesaikan"
}, },
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer"
},
"forget": { "forget": {
"Account": "Akun", "Account": "Akun",
"Change Password": "Ubah Kata Sandi", "Change Password": "Ubah Kata Sandi",
@ -215,6 +219,7 @@
"Enable": "Enable", "Enable": "Enable",
"Enabled": "Enabled", "Enabled": "Enabled",
"Enabled successfully": "Enabled successfully", "Enabled successfully": "Enabled successfully",
"Enforcers": "Enforcers",
"Failed to add": "Gagal menambahkan", "Failed to add": "Gagal menambahkan",
"Failed to connect to server": "Gagal terhubung ke server", "Failed to connect to server": "Gagal terhubung ke server",
"Failed to delete": "Gagal menghapus", "Failed to delete": "Gagal menghapus",

View File

@ -152,6 +152,10 @@
"Sending": "送信", "Sending": "送信",
"Submit and complete": "提出して完了してください" "Submit and complete": "提出して完了してください"
}, },
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer"
},
"forget": { "forget": {
"Account": "アカウント", "Account": "アカウント",
"Change Password": "パスワードを変更", "Change Password": "パスワードを変更",
@ -215,6 +219,7 @@
"Enable": "Enable", "Enable": "Enable",
"Enabled": "Enabled", "Enabled": "Enabled",
"Enabled successfully": "Enabled successfully", "Enabled successfully": "Enabled successfully",
"Enforcers": "Enforcers",
"Failed to add": "追加できませんでした", "Failed to add": "追加できませんでした",
"Failed to connect to server": "サーバーに接続できませんでした", "Failed to connect to server": "サーバーに接続できませんでした",
"Failed to delete": "削除に失敗しました", "Failed to delete": "削除に失敗しました",

View File

@ -152,6 +152,10 @@
"Sending": "전송하기", "Sending": "전송하기",
"Submit and complete": "제출하고 완료하십시오" "Submit and complete": "제출하고 완료하십시오"
}, },
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer"
},
"forget": { "forget": {
"Account": "계정", "Account": "계정",
"Change Password": "비밀번호 변경", "Change Password": "비밀번호 변경",
@ -215,6 +219,7 @@
"Enable": "Enable", "Enable": "Enable",
"Enabled": "Enabled", "Enabled": "Enabled",
"Enabled successfully": "Enabled successfully", "Enabled successfully": "Enabled successfully",
"Enforcers": "Enforcers",
"Failed to add": "추가하지 못했습니다", "Failed to add": "추가하지 못했습니다",
"Failed to connect to server": "서버에 연결하지 못했습니다", "Failed to connect to server": "서버에 연결하지 못했습니다",
"Failed to delete": "삭제에 실패했습니다", "Failed to delete": "삭제에 실패했습니다",

View File

@ -152,6 +152,10 @@
"Sending": "Enviando", "Sending": "Enviando",
"Submit and complete": "Enviar e concluir" "Submit and complete": "Enviar e concluir"
}, },
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer"
},
"forget": { "forget": {
"Account": "Conta", "Account": "Conta",
"Change Password": "Alterar Senha", "Change Password": "Alterar Senha",
@ -215,6 +219,7 @@
"Enable": "Habilitar", "Enable": "Habilitar",
"Enabled": "Habilitado", "Enabled": "Habilitado",
"Enabled successfully": "Habilitado com sucesso", "Enabled successfully": "Habilitado com sucesso",
"Enforcers": "Enforcers",
"Failed to add": "Falha ao adicionar", "Failed to add": "Falha ao adicionar",
"Failed to connect to server": "Falha ao conectar ao servidor", "Failed to connect to server": "Falha ao conectar ao servidor",
"Failed to delete": "Falha ao excluir", "Failed to delete": "Falha ao excluir",

View File

@ -152,6 +152,10 @@
"Sending": "Отправка", "Sending": "Отправка",
"Submit and complete": "Отправить и завершить" "Submit and complete": "Отправить и завершить"
}, },
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer"
},
"forget": { "forget": {
"Account": "Счет", "Account": "Счет",
"Change Password": "Изменить пароль", "Change Password": "Изменить пароль",
@ -215,6 +219,7 @@
"Enable": "Enable", "Enable": "Enable",
"Enabled": "Enabled", "Enabled": "Enabled",
"Enabled successfully": "Enabled successfully", "Enabled successfully": "Enabled successfully",
"Enforcers": "Enforcers",
"Failed to add": "Не удалось добавить", "Failed to add": "Не удалось добавить",
"Failed to connect to server": "Не удалось подключиться к серверу", "Failed to connect to server": "Не удалось подключиться к серверу",
"Failed to delete": "Не удалось удалить", "Failed to delete": "Не удалось удалить",

View File

@ -152,6 +152,10 @@
"Sending": "Gửi", "Sending": "Gửi",
"Submit and complete": "Nộp và hoàn thành" "Submit and complete": "Nộp và hoàn thành"
}, },
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer"
},
"forget": { "forget": {
"Account": "Tài khoản", "Account": "Tài khoản",
"Change Password": "Đổi mật khẩu", "Change Password": "Đổi mật khẩu",
@ -215,6 +219,7 @@
"Enable": "Enable", "Enable": "Enable",
"Enabled": "Enabled", "Enabled": "Enabled",
"Enabled successfully": "Enabled successfully", "Enabled successfully": "Enabled successfully",
"Enforcers": "Enforcers",
"Failed to add": "Không thể thêm được", "Failed to add": "Không thể thêm được",
"Failed to connect to server": "Không thể kết nối đến máy chủ", "Failed to connect to server": "Không thể kết nối đến máy chủ",
"Failed to delete": "Không thể xoá", "Failed to delete": "Không thể xoá",

View File

@ -152,6 +152,10 @@
"Sending": "发送中", "Sending": "发送中",
"Submit and complete": "完成提交" "Submit and complete": "完成提交"
}, },
"enforcer": {
"Edit Enforcer": "Edit Enforcer",
"New Enforcer": "New Enforcer"
},
"forget": { "forget": {
"Account": "账号", "Account": "账号",
"Change Password": "修改密码", "Change Password": "修改密码",
@ -215,6 +219,7 @@
"Enable": "启用", "Enable": "启用",
"Enabled": "已开启", "Enabled": "已开启",
"Enabled successfully": "启用成功", "Enabled successfully": "启用成功",
"Enforcers": "Enforcers",
"Failed to add": "添加失败", "Failed to add": "添加失败",
"Failed to connect to server": "连接服务器失败", "Failed to connect to server": "连接服务器失败",
"Failed to delete": "删除失败", "Failed to delete": "删除失败",