forked from casdoor/casdoor
387 lines
11 KiB
Go
387 lines
11 KiB
Go
// Copyright 2025 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 object
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/casdoor/casdoor/idp"
|
|
"github.com/casdoor/casdoor/pp"
|
|
"github.com/casdoor/casdoor/util"
|
|
)
|
|
|
|
func PlaceOrder(owner string, reqProductInfos []ProductInfo, user *User) (*Order, error) {
|
|
if len(reqProductInfos) == 0 {
|
|
return nil, fmt.Errorf("order has no products")
|
|
}
|
|
|
|
productNames := make([]string, 0, len(reqProductInfos))
|
|
for _, reqInfo := range reqProductInfos {
|
|
if reqInfo.Name == "" {
|
|
return nil, fmt.Errorf("product name cannot be empty")
|
|
}
|
|
productNames = append(productNames, reqInfo.Name)
|
|
}
|
|
|
|
products, err := getOrderProducts(owner, productNames)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
productMap := make(map[string]Product, len(reqProductInfos))
|
|
for _, product := range products {
|
|
productMap[product.Name] = product
|
|
}
|
|
|
|
orderCurrency := products[0].Currency
|
|
if orderCurrency == "" {
|
|
orderCurrency = "USD"
|
|
}
|
|
|
|
if err := validateProductCurrencies(products, orderCurrency); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var productInfos []ProductInfo
|
|
orderPrice := 0.0
|
|
for _, productInfo := range reqProductInfos {
|
|
product := productMap[productInfo.Name]
|
|
|
|
var productPrice float64
|
|
if product.IsRecharge {
|
|
productPrice = productInfo.Price
|
|
if productPrice <= 0 {
|
|
return nil, fmt.Errorf("the custom price should be greater than zero")
|
|
}
|
|
} else {
|
|
productPrice = product.Price
|
|
}
|
|
productInfos = append(productInfos, ProductInfo{
|
|
Owner: owner,
|
|
Name: product.Name,
|
|
DisplayName: product.DisplayName,
|
|
Image: product.Image,
|
|
Detail: product.Detail,
|
|
Price: productPrice,
|
|
Currency: product.Currency,
|
|
IsRecharge: product.IsRecharge,
|
|
Quantity: productInfo.Quantity,
|
|
PricingName: productInfo.PricingName,
|
|
PlanName: productInfo.PlanName,
|
|
})
|
|
|
|
orderPrice += productPrice * float64(productInfo.Quantity)
|
|
}
|
|
|
|
orderName := fmt.Sprintf("order_%v", util.GenerateTimeId())
|
|
order := &Order{
|
|
Owner: owner,
|
|
Name: orderName,
|
|
DisplayName: orderName,
|
|
CreatedTime: util.GetCurrentTime(),
|
|
Products: productNames,
|
|
ProductInfos: productInfos,
|
|
User: user.Name,
|
|
Payment: "", // Payment will be set when user pays
|
|
Price: orderPrice,
|
|
Currency: orderCurrency,
|
|
State: "Created",
|
|
Message: "",
|
|
UpdateTime: "",
|
|
}
|
|
|
|
affected, err := AddOrder(order)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !affected {
|
|
return nil, fmt.Errorf("failed to add order: %s", util.StructToJson(order))
|
|
}
|
|
|
|
return order, nil
|
|
}
|
|
|
|
func PayOrder(providerName, host, paymentEnv string, order *Order, lang string) (payment *Payment, attachInfo map[string]interface{}, err error) {
|
|
if order.State != "Created" {
|
|
return nil, nil, fmt.Errorf("cannot pay for order: %s, current state is %s", order.GetId(), order.State)
|
|
}
|
|
productNames := order.Products
|
|
products, err := getOrderProducts(order.Owner, productNames)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if len(products) == 0 {
|
|
return nil, nil, fmt.Errorf("order has no products")
|
|
}
|
|
|
|
orderCurrency := order.Currency
|
|
if orderCurrency == "" {
|
|
orderCurrency = "USD"
|
|
}
|
|
|
|
if err := validateProductCurrencies(products, orderCurrency); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
user, err := GetUser(util.GetId(order.Owner, order.User))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if user == nil {
|
|
return nil, nil, fmt.Errorf("the user: %s does not exist", order.User)
|
|
}
|
|
|
|
// For multi-product orders, the payment provider is determined by the first product
|
|
baseProduct := products[0]
|
|
provider, err := baseProduct.getProvider(providerName)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
pProvider, err := GetPaymentProvider(provider)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
owner := baseProduct.Owner
|
|
payerName := fmt.Sprintf("%s | %s", user.Name, user.DisplayName)
|
|
paymentName := fmt.Sprintf("payment_%v", util.GenerateTimeId())
|
|
|
|
originFrontend, originBackend := getOriginFromHost(host)
|
|
returnUrl := fmt.Sprintf("%s/payments/%s/%s/result", originFrontend, owner, paymentName)
|
|
notifyUrl := fmt.Sprintf("%s/api/notify-payment/%s/%s", originBackend, owner, paymentName)
|
|
|
|
orderProductInfos := order.ProductInfos
|
|
// Create a subscription when pricing and plan are provided
|
|
// This allows both free users and paid users to subscribe to plans
|
|
for i, productInfo := range orderProductInfos {
|
|
if productInfo.PricingName == "" || productInfo.PlanName == "" {
|
|
continue
|
|
}
|
|
|
|
plan, err := GetPlan(util.GetId(owner, productInfo.PlanName))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if plan == nil {
|
|
return nil, nil, fmt.Errorf("the plan: %s does not exist", productInfo.PlanName)
|
|
}
|
|
|
|
// Check if plan restricts user to one subscription
|
|
if plan.IsExclusive {
|
|
hasSubscription, err := HasActiveSubscriptionForPlan(owner, user.Name, plan.Name)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if hasSubscription {
|
|
return nil, nil, fmt.Errorf("user already has an active subscription for plan: %s", plan.Name)
|
|
}
|
|
}
|
|
|
|
sub, err := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
affected, err := AddSubscription(sub)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if !affected {
|
|
return nil, nil, fmt.Errorf("failed to add subscription: %s", sub.Name)
|
|
}
|
|
|
|
if i == 0 {
|
|
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, productInfo.PricingName, sub.Name)
|
|
}
|
|
}
|
|
|
|
if baseProduct.SuccessUrl != "" {
|
|
returnUrl = fmt.Sprintf("%s?transactionOwner=%s&transactionName=%s", baseProduct.SuccessUrl, owner, paymentName)
|
|
}
|
|
|
|
displayNames := make([]string, len(products))
|
|
descriptions := make([]string, len(products))
|
|
for i, p := range products {
|
|
displayNames[i] = p.DisplayName
|
|
descriptions[i] = p.Description
|
|
}
|
|
reqProductName := strings.Join(productNames, ", ")
|
|
reqProductDisplayName := strings.Join(displayNames, ", ")
|
|
reqProductDescription := strings.Join(descriptions, ", ")
|
|
|
|
payReq := &pp.PayReq{
|
|
ProviderName: providerName,
|
|
ProductName: reqProductName,
|
|
PayerName: payerName,
|
|
PayerId: user.Id,
|
|
PayerEmail: user.Email,
|
|
PaymentName: paymentName,
|
|
ProductDisplayName: reqProductDisplayName,
|
|
ProductDescription: reqProductDescription,
|
|
ProductImage: baseProduct.Image,
|
|
Price: order.Price,
|
|
Currency: order.Currency,
|
|
ReturnUrl: returnUrl,
|
|
NotifyUrl: notifyUrl,
|
|
PaymentEnv: paymentEnv,
|
|
}
|
|
|
|
if provider.Type == "WeChat Pay" {
|
|
payReq.PayerId, err = getUserExtraProperty(user, "WeChat", idp.BuildWechatOpenIdKey(provider.ClientId2))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
} else if provider.Type == "Balance" {
|
|
payReq.PayerId = user.GetId()
|
|
}
|
|
|
|
payResp, err := pProvider.Pay(payReq)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
payment = &Payment{
|
|
Owner: baseProduct.Owner,
|
|
Name: paymentName,
|
|
CreatedTime: util.GetCurrentTime(),
|
|
DisplayName: paymentName,
|
|
|
|
Provider: provider.Name,
|
|
Type: provider.Type,
|
|
|
|
Products: productNames,
|
|
ProductsDisplayName: reqProductDisplayName,
|
|
Detail: reqProductDescription,
|
|
Currency: order.Currency,
|
|
Price: order.Price,
|
|
|
|
User: user.Name,
|
|
Order: order.Name,
|
|
PayUrl: payResp.PayUrl,
|
|
SuccessUrl: returnUrl,
|
|
State: pp.PaymentStateCreated,
|
|
OutOrderId: payResp.OrderId,
|
|
}
|
|
|
|
if provider.Type == "Balance" {
|
|
payment.State = pp.PaymentStatePaid
|
|
}
|
|
|
|
affected, err := AddPayment(payment)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if !affected {
|
|
return nil, nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
|
|
}
|
|
|
|
if provider.Type == "Balance" {
|
|
transaction := &Transaction{
|
|
Owner: payment.Owner,
|
|
CreatedTime: util.GetCurrentTime(),
|
|
Application: user.SignupApplication,
|
|
Amount: -payment.Price,
|
|
Currency: order.Currency,
|
|
Payment: payment.Name,
|
|
Category: TransactionCategoryPurchase,
|
|
Type: provider.Category,
|
|
Subtype: provider.Type,
|
|
Provider: provider.Name,
|
|
Tag: "User",
|
|
User: payment.User,
|
|
State: string(pp.PaymentStatePaid),
|
|
}
|
|
|
|
affected, err = AddInternalPaymentTransaction(transaction, lang)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if !affected {
|
|
return nil, nil, fmt.Errorf("failed to add transaction: %s", util.StructToJson(transaction))
|
|
}
|
|
|
|
hasRecharge := false
|
|
rechargeAmount := 0.0
|
|
for _, productInfo := range orderProductInfos {
|
|
if productInfo.IsRecharge {
|
|
hasRecharge = true
|
|
rechargeAmount += productInfo.Price * float64(productInfo.Quantity)
|
|
}
|
|
}
|
|
|
|
if hasRecharge {
|
|
rechargeTransaction := &Transaction{
|
|
Owner: payment.Owner,
|
|
CreatedTime: util.GetCurrentTime(),
|
|
Application: user.SignupApplication,
|
|
Amount: rechargeAmount,
|
|
Currency: order.Currency,
|
|
Payment: payment.Name,
|
|
Category: TransactionCategoryRecharge,
|
|
Type: provider.Category,
|
|
Subtype: provider.Type,
|
|
Provider: provider.Name,
|
|
Tag: "User",
|
|
User: payment.User,
|
|
State: string(pp.PaymentStatePaid),
|
|
}
|
|
|
|
affected, err = AddInternalPaymentTransaction(rechargeTransaction, lang)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if !affected {
|
|
return nil, nil, fmt.Errorf("failed to add recharge transaction: %s", util.StructToJson(rechargeTransaction))
|
|
}
|
|
}
|
|
}
|
|
|
|
order.Payment = payment.Name
|
|
if provider.Type == "Balance" {
|
|
order.State = "Paid"
|
|
order.Message = "Payment successful"
|
|
order.UpdateTime = util.GetCurrentTime()
|
|
}
|
|
|
|
// Update order state first to avoid inconsistency
|
|
_, err = UpdateOrder(order.GetId(), order)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Update product stock after order state is persisted (for instant payment methods)
|
|
if provider.Type == "Balance" {
|
|
err = UpdateProductStock(orderProductInfos)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
return payment, payResp.AttachInfo, nil
|
|
}
|
|
|
|
func CancelOrder(order *Order) (bool, error) {
|
|
if order.State != "Created" {
|
|
return false, fmt.Errorf("cannot cancel order in state: %s", order.State)
|
|
}
|
|
|
|
order.State = "Canceled"
|
|
order.Message = "Canceled by user"
|
|
order.UpdateTime = util.GetCurrentTime()
|
|
return UpdateOrder(order.GetId(), order)
|
|
}
|