756 lines
22 KiB
Go
756 lines
22 KiB
Go
package payment
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
bizpayment "kra/internal/biz/payment"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/go-pay/gopay"
|
|
"github.com/go-pay/gopay/paypal"
|
|
)
|
|
|
|
type paypalAdapter struct{}
|
|
|
|
func (a *paypalAdapter) client(c map[string]any) (*paypal.Client, error) {
|
|
environment := strings.ToLower(strings.TrimSpace(text(c, "environment")))
|
|
isProd := environment != "sandbox"
|
|
options := []paypal.Option{paypal.WithoutAutoRefreshToken()}
|
|
if baseURL := strings.TrimRight(firstAny(c, "api_base_url", "base_url", "proxy_url"), "/"); baseURL != "" {
|
|
options = append(options, paypal.WithProxyUrl(baseURL, baseURL))
|
|
}
|
|
client, err := paypal.NewClient(text(c, "client_id"), text(c, "client_secret"), isProd, options...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("PayPal 客户端初始化失败: %w", err)
|
|
}
|
|
client.SetRequestHeader("Prefer", "return=representation")
|
|
return client, nil
|
|
}
|
|
|
|
func (a *paypalAdapter) Create(ctx context.Context, req *bizpayment.PaymentRequest, c map[string]any) (*bizpayment.PaymentResult, error) {
|
|
if req == nil {
|
|
return nil, errors.New("PayPal 下单参数为空")
|
|
}
|
|
client, err := a.client(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
currency := strings.ToUpper(strings.TrimSpace(req.Currency))
|
|
value, err := paypalFormatAmount(req.Amount, currency, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
purchaseUnit := &paypal.PurchaseUnit{
|
|
ReferenceId: req.TradeNo,
|
|
Description: req.Subject,
|
|
CustomId: req.TradeNo,
|
|
InvoiceId: req.TradeNo,
|
|
Amount: &paypal.Amount{CurrencyCode: currency, Value: value},
|
|
}
|
|
bm := gopay.BodyMap{
|
|
"intent": "CAPTURE",
|
|
"purchase_units": []*paypal.PurchaseUnit{purchaseUnit},
|
|
}
|
|
if applicationContext := paypalApplicationContext(req, c); len(applicationContext) > 0 {
|
|
bm["application_context"] = applicationContext
|
|
}
|
|
mergeGoPayExtras(bm, req.Extra, "intent", "purchase_units", "application_context")
|
|
client.SetRequestHeader("PayPal-Request-Id", req.TradeNo)
|
|
rsp, err := client.CreateOrder(ctx, bm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rsp == nil || rsp.Code != paypal.Success || rsp.Response == nil {
|
|
return nil, paypalResponseError("下单", paypalResponseCode(rsp), paypalCreateError(rsp))
|
|
}
|
|
order := rsp.Response
|
|
if strings.TrimSpace(order.Id) == "" {
|
|
return nil, errors.New("PayPal 下单成功但缺少 order ID")
|
|
}
|
|
return paypalCreateResult(req, order, c)
|
|
}
|
|
|
|
func (a *paypalAdapter) Query(ctx context.Context, orderID string, c map[string]any) (*bizpayment.PaymentResult, error) {
|
|
orderID = strings.TrimSpace(orderID)
|
|
if orderID == "" {
|
|
return nil, errors.New("PayPal 查单缺少 order ID")
|
|
}
|
|
client, err := a.client(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rsp, err := client.OrderDetail(ctx, orderID, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rsp == nil || rsp.Code != paypal.Success || rsp.Response == nil {
|
|
return nil, paypalResponseError("查单", paypalResponseCode(rsp), paypalOrderError(rsp))
|
|
}
|
|
if rsp.Response.Id != "" && rsp.Response.Id != orderID {
|
|
return nil, errors.New("PayPal 查单响应的 order ID 不匹配")
|
|
}
|
|
order := rsp.Response
|
|
if _, err = paypalOrderIdentity(order); err != nil {
|
|
return nil, err
|
|
}
|
|
if paypalAutoCaptureEnabled(c) && strings.EqualFold(strings.TrimSpace(order.Status), "APPROVED") {
|
|
order, err = paypalCaptureOrder(ctx, client, orderID, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return paypalOrderResult(order, orderID, "", c)
|
|
}
|
|
|
|
// paypalCaptureOrder completes an order after the buyer approves it. PayPal's
|
|
// CAPTURE intent deliberately separates approval from settlement; treating an
|
|
// APPROVED order as paid would allow fulfillment before funds are captured.
|
|
func paypalCaptureOrder(ctx context.Context, client *paypal.Client, orderID string, c map[string]any) (*paypal.OrderDetail, error) {
|
|
if client == nil || strings.TrimSpace(orderID) == "" {
|
|
return nil, errors.New("PayPal 捕获订单参数为空")
|
|
}
|
|
bm := gopay.BodyMap{}
|
|
mergeGoPayConfigExtras(bm, c, "capture_extra")
|
|
client.SetRequestHeader("PayPal-Request-Id", strings.TrimSpace(orderID)+"-capture")
|
|
rsp, err := client.OrderCapture(ctx, orderID, bm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rsp == nil || rsp.Code != paypal.Success || rsp.Response == nil {
|
|
return nil, paypalResponseError("捕获订单", paypalResponseCode(rsp), paypalCaptureError(rsp))
|
|
}
|
|
if returnedID := strings.TrimSpace(rsp.Response.Id); returnedID != "" && returnedID != strings.TrimSpace(orderID) {
|
|
return nil, errors.New("PayPal 捕获响应的 order ID 不匹配")
|
|
}
|
|
return rsp.Response, nil
|
|
}
|
|
|
|
func paypalAutoCaptureEnabled(c map[string]any) bool {
|
|
value, ok := c["auto_capture"]
|
|
if !ok || value == nil {
|
|
return true
|
|
}
|
|
switch parsed := value.(type) {
|
|
case bool:
|
|
return parsed
|
|
case string:
|
|
if result, err := strconv.ParseBool(strings.TrimSpace(parsed)); err == nil {
|
|
return result
|
|
}
|
|
case json.Number:
|
|
return parsed != "0"
|
|
case float64:
|
|
return parsed != 0
|
|
case int:
|
|
return parsed != 0
|
|
case int64:
|
|
return parsed != 0
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (a *paypalAdapter) Refund(ctx context.Context, req *bizpayment.PaymentRefundRequest, c map[string]any) (*bizpayment.PaymentResult, error) {
|
|
if req == nil {
|
|
return nil, errors.New("PayPal 退款请求为空")
|
|
}
|
|
tradeNo := strings.TrimSpace(req.TradeNo)
|
|
refundNo := strings.TrimSpace(req.RefundNo)
|
|
if tradeNo == "" || refundNo == "" || req.Amount <= 0 {
|
|
return nil, errors.New("PayPal 退款缺少商户订单号、退款单号或有效金额")
|
|
}
|
|
effective := *req
|
|
effective.TradeNo = tradeNo
|
|
effective.RefundNo = refundNo
|
|
req = &effective
|
|
client, err := a.client(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
captureID, err := paypalRefundCaptureID(ctx, client, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
currency := strings.ToUpper(strings.TrimSpace(req.Currency))
|
|
value, err := paypalFormatAmount(req.Amount, currency, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bm := gopay.BodyMap{
|
|
"invoice_id": refundNo,
|
|
"amount": &paypal.Amount{
|
|
CurrencyCode: currency,
|
|
Value: value,
|
|
},
|
|
}
|
|
if note := strings.TrimSpace(text(c, "refund_note")); note != "" {
|
|
bm["note_to_payer"] = note
|
|
}
|
|
mergeGoPayConfigExtras(bm, c, "refund_extra", "amount", "invoice_id", "capture_id", "order_id", "query_id", "provider_trade_no")
|
|
client.SetRequestHeader("PayPal-Request-Id", refundNo)
|
|
rsp, err := client.PaymentCaptureRefund(ctx, captureID, bm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rsp == nil || rsp.Code != paypal.Success || rsp.Response == nil {
|
|
return nil, paypalResponseError("退款", paypalResponseCode(rsp), paypalRefundError(rsp))
|
|
}
|
|
return paypalRefundResult(req, rsp.Response, c)
|
|
}
|
|
|
|
func paypalRefundResult(req *bizpayment.PaymentRefundRequest, refund *paypal.PaymentCaptureRefund, c map[string]any) (*bizpayment.PaymentResult, error) {
|
|
if req == nil || refund == nil {
|
|
return nil, errors.New("PayPal 退款响应为空")
|
|
}
|
|
if invoiceID := strings.TrimSpace(refund.InvoiceId); invoiceID == "" {
|
|
return nil, errors.New("PayPal 退款响应缺少 invoice_id")
|
|
} else if invoiceID != strings.TrimSpace(req.RefundNo) {
|
|
return nil, errors.New("PayPal 退款响应的 invoice_id 不匹配")
|
|
}
|
|
providerRefundID := strings.TrimSpace(refund.Id)
|
|
if providerRefundID == "" {
|
|
return nil, errors.New("PayPal 退款响应缺少 refund ID")
|
|
}
|
|
currency := strings.ToUpper(strings.TrimSpace(req.Currency))
|
|
result := &bizpayment.PaymentResult{
|
|
Provider: bizpayment.PaymentPayPal,
|
|
Status: normalizePayPalRefundState(refund.Status),
|
|
TradeNo: req.TradeNo,
|
|
ProviderTradeNo: providerRefundID,
|
|
Amount: req.Amount,
|
|
Currency: currency,
|
|
Payload: mustJSON(refund),
|
|
}
|
|
if refund.Amount == nil {
|
|
return nil, errors.New("PayPal 退款响应缺少金额")
|
|
}
|
|
parsed, err := paypalParseAmount(refund.Amount, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if parsed != req.Amount {
|
|
return nil, errors.New("PayPal 退款响应金额不匹配")
|
|
}
|
|
responseCurrency := strings.ToUpper(strings.TrimSpace(refund.Amount.CurrencyCode))
|
|
if responseCurrency == "" {
|
|
return nil, errors.New("PayPal 退款响应缺少币种")
|
|
}
|
|
if currency != "" && responseCurrency != currency {
|
|
return nil, errors.New("PayPal 退款响应币种不匹配")
|
|
}
|
|
result.Amount = parsed
|
|
result.Currency = responseCurrency
|
|
return result, nil
|
|
}
|
|
|
|
func paypalCreateResult(req *bizpayment.PaymentRequest, order *paypal.OrderDetail, c map[string]any) (*bizpayment.PaymentResult, error) {
|
|
if req == nil || order == nil {
|
|
return nil, errors.New("PayPal 下单响应为空")
|
|
}
|
|
tradeNo, err := paypalOrderIdentity(order)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if tradeNo != "" && tradeNo != strings.TrimSpace(req.TradeNo) {
|
|
return nil, errors.New("PayPal 下单响应的商户订单号不匹配")
|
|
}
|
|
amount, currency, found, err := paypalOrderAmount(order, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if found {
|
|
if amount != req.Amount {
|
|
return nil, errors.New("PayPal 下单响应金额不匹配")
|
|
}
|
|
if expected := strings.ToUpper(strings.TrimSpace(req.Currency)); currency == "" || currency != expected {
|
|
return nil, errors.New("PayPal 下单响应币种不匹配")
|
|
}
|
|
}
|
|
result, err := paypalOrderResult(order, order.Id, req.TradeNo, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.TradeNo = req.TradeNo
|
|
return result, nil
|
|
}
|
|
|
|
func (a *paypalAdapter) Callback(ctx context.Context, callback *bizpayment.PaymentCallback, c map[string]any) (*bizpayment.PaymentResult, error) {
|
|
if callback == nil || len(callback.Body) == 0 {
|
|
return nil, errors.New("PayPal 回调为空")
|
|
}
|
|
webhookID := strings.TrimSpace(text(c, "webhook_id"))
|
|
if webhookID == "" {
|
|
return nil, errors.New("PayPal 回调验签缺少 webhook_id")
|
|
}
|
|
var event paypal.WebhookEvent
|
|
if err := json.Unmarshal(callback.Body, &event); err != nil {
|
|
return nil, fmt.Errorf("PayPal 回调 JSON 无效: %w", err)
|
|
}
|
|
if strings.TrimSpace(event.Id) == "" || len(event.Resource) == 0 {
|
|
return nil, errors.New("PayPal 回调事件不完整")
|
|
}
|
|
var webhookEvent any
|
|
if err := json.Unmarshal(callback.Body, &webhookEvent); err != nil {
|
|
return nil, err
|
|
}
|
|
verifyBody := gopay.BodyMap{
|
|
"auth_algo": paypalCallbackHeader(callback, "Paypal-Auth-Algo"),
|
|
"cert_url": paypalCallbackHeader(callback, "Paypal-Cert-Url"),
|
|
"transmission_id": paypalCallbackHeader(callback, "Paypal-Transmission-Id"),
|
|
"transmission_sig": paypalCallbackHeader(callback, "Paypal-Transmission-Sig"),
|
|
"transmission_time": paypalCallbackHeader(callback, "Paypal-Transmission-Time"),
|
|
"webhook_id": webhookID,
|
|
"webhook_event": webhookEvent,
|
|
}
|
|
client, err := a.client(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
verified, err := client.VerifyWebhookSignature(ctx, verifyBody)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if verified == nil || !strings.EqualFold(verified.VerificationStatus, "SUCCESS") {
|
|
return nil, errors.New("PayPal 回调签名校验失败")
|
|
}
|
|
result, err := paypalWebhookResult(&event)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.Payload = append([]byte(nil), callback.Body...)
|
|
result.EventID = event.Id
|
|
return result, nil
|
|
}
|
|
|
|
func paypalApplicationContext(req *bizpayment.PaymentRequest, c map[string]any) map[string]any {
|
|
applicationContext := map[string]any{}
|
|
if configured, ok := req.Extra["application_context"].(map[string]any); ok {
|
|
for key, value := range configured {
|
|
applicationContext[key] = value
|
|
}
|
|
}
|
|
for _, key := range []string{"brand_name", "locale", "landing_page", "shipping_preference", "user_action"} {
|
|
if _, exists := applicationContext[key]; !exists {
|
|
if value := strings.TrimSpace(text(c, key)); value != "" {
|
|
applicationContext[key] = value
|
|
}
|
|
}
|
|
}
|
|
if returnURL := strings.TrimSpace(req.ReturnURL); returnURL != "" {
|
|
applicationContext["return_url"] = returnURL
|
|
}
|
|
cancelURL := firstAny(c, "cancel_url", "return_url")
|
|
if cancelURL == "" {
|
|
cancelURL = strings.TrimSpace(req.ReturnURL)
|
|
}
|
|
if cancelURL != "" {
|
|
applicationContext["cancel_url"] = cancelURL
|
|
}
|
|
return applicationContext
|
|
}
|
|
|
|
func paypalOrderResult(order *paypal.OrderDetail, queryID, fallbackTradeNo string, c map[string]any) (*bizpayment.PaymentResult, error) {
|
|
if order == nil {
|
|
return nil, errors.New("PayPal 订单结果为空")
|
|
}
|
|
tradeNo, err := paypalOrderIdentity(order)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if tradeNo == "" {
|
|
tradeNo = fallbackTradeNo
|
|
}
|
|
capture := paypalOrderCapture(order)
|
|
result := &bizpayment.PaymentResult{
|
|
Provider: bizpayment.PaymentPayPal,
|
|
Status: paypalOrderCaptureState(order),
|
|
TradeNo: tradeNo,
|
|
QueryID: queryID,
|
|
Payload: mustJSON(order),
|
|
}
|
|
if capture != nil {
|
|
result.ProviderTradeNo = capture.Id
|
|
}
|
|
amount, currency, found, err := paypalOrderAmount(order, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if found {
|
|
result.Amount = amount
|
|
result.Currency = currency
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func paypalOrderIdentity(order *paypal.OrderDetail) (string, error) {
|
|
if order == nil {
|
|
return "", nil
|
|
}
|
|
identity := ""
|
|
for _, unit := range order.PurchaseUnits {
|
|
if unit == nil {
|
|
continue
|
|
}
|
|
for _, value := range []string{unit.InvoiceId, unit.CustomId, unit.ReferenceId} {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
if identity == "" {
|
|
identity = value
|
|
continue
|
|
}
|
|
if value != identity {
|
|
return "", errors.New("PayPal 订单响应包含冲突的商户订单号")
|
|
}
|
|
}
|
|
}
|
|
return identity, nil
|
|
}
|
|
|
|
func paypalOrderCapture(order *paypal.OrderDetail) *paypal.Capture {
|
|
if order == nil {
|
|
return nil
|
|
}
|
|
for _, unit := range order.PurchaseUnits {
|
|
if unit == nil || unit.Payments == nil {
|
|
continue
|
|
}
|
|
for _, capture := range unit.Payments.Captures {
|
|
if capture == nil || strings.TrimSpace(capture.Id) == "" {
|
|
continue
|
|
}
|
|
if strings.EqualFold(capture.Status, "COMPLETED") {
|
|
return capture
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func paypalOrderCaptureState(order *paypal.OrderDetail) string {
|
|
if order == nil {
|
|
return "pending"
|
|
}
|
|
pending, failed, seen := false, false, false
|
|
for _, unit := range order.PurchaseUnits {
|
|
if unit == nil || unit.Payments == nil {
|
|
continue
|
|
}
|
|
for _, capture := range unit.Payments.Captures {
|
|
if capture == nil || strings.TrimSpace(capture.Id) == "" {
|
|
continue
|
|
}
|
|
seen = true
|
|
switch normalizePayPalCaptureState(capture.Status) {
|
|
case "success":
|
|
return "success"
|
|
case "pending":
|
|
pending = true
|
|
case "failed":
|
|
failed = true
|
|
}
|
|
}
|
|
}
|
|
if pending || !seen {
|
|
if strings.EqualFold(order.Status, "VOIDED") {
|
|
return "failed"
|
|
}
|
|
return "pending"
|
|
}
|
|
if failed {
|
|
return "failed"
|
|
}
|
|
return "pending"
|
|
}
|
|
|
|
func paypalOrderAmount(order *paypal.OrderDetail, c map[string]any) (int64, string, bool, error) {
|
|
var total int64
|
|
var currency string
|
|
found := false
|
|
for _, unit := range order.PurchaseUnits {
|
|
if unit == nil || unit.Amount == nil || strings.TrimSpace(unit.Amount.Value) == "" {
|
|
continue
|
|
}
|
|
unitCurrency := strings.ToUpper(strings.TrimSpace(unit.Amount.CurrencyCode))
|
|
amount, err := paypalParseAmount(unit.Amount, c)
|
|
if err != nil {
|
|
return 0, "", false, err
|
|
}
|
|
if found && currency != unitCurrency {
|
|
return 0, "", false, errors.New("PayPal 订单包含不同币种的 purchase unit")
|
|
}
|
|
if amount > math.MaxInt64-total {
|
|
return 0, "", false, errors.New("PayPal 订单金额超出范围")
|
|
}
|
|
currency = unitCurrency
|
|
total += amount
|
|
found = true
|
|
}
|
|
return total, currency, found, nil
|
|
}
|
|
|
|
func paypalRefundCaptureID(ctx context.Context, client *paypal.Client, req *bizpayment.PaymentRefundRequest) (string, error) {
|
|
if req == nil {
|
|
return "", errors.New("PayPal 退款请求为空")
|
|
}
|
|
if captureID := strings.TrimSpace(req.ProviderTradeNo); captureID != "" {
|
|
return captureID, nil
|
|
}
|
|
orderID := strings.TrimSpace(req.QueryID)
|
|
if orderID == "" {
|
|
return "", errors.New("PayPal 退款缺少已持久化的 capture ID")
|
|
}
|
|
rsp, err := client.OrderDetail(ctx, orderID, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if rsp == nil || rsp.Code != paypal.Success || rsp.Response == nil {
|
|
return "", paypalResponseError("退款前查单", paypalResponseCode(rsp), paypalOrderError(rsp))
|
|
}
|
|
if rsp.Response.Id != "" && rsp.Response.Id != orderID {
|
|
return "", errors.New("PayPal 退款查单响应的 order ID 不匹配")
|
|
}
|
|
returnedTradeNo, identityErr := paypalOrderIdentity(rsp.Response)
|
|
if identityErr != nil {
|
|
return "", identityErr
|
|
}
|
|
if returnedTradeNo == "" || returnedTradeNo != req.TradeNo {
|
|
return "", errors.New("PayPal 退款订单与本地商户订单号不匹配")
|
|
}
|
|
capture := paypalOrderCapture(rsp.Response)
|
|
if capture == nil || strings.TrimSpace(capture.Id) == "" {
|
|
return "", errors.New("PayPal 退款订单缺少 capture ID")
|
|
}
|
|
return capture.Id, nil
|
|
}
|
|
|
|
func paypalWebhookResult(event *paypal.WebhookEvent) (*bizpayment.PaymentResult, error) {
|
|
var resource map[string]any
|
|
if err := json.Unmarshal(event.Resource, &resource); err != nil {
|
|
return nil, fmt.Errorf("PayPal 回调 resource 无效: %w", err)
|
|
}
|
|
eventType := strings.ToUpper(strings.TrimSpace(event.EventType))
|
|
tradeNo := firstNonEmpty(paypalMapString(resource, "invoice_id"), paypalMapString(resource, "custom_id"))
|
|
queryID := paypalNestedMapString(resource, "supplementary_data", "related_ids", "order_id")
|
|
providerTradeNo := ""
|
|
status := normalizePayPalCaptureState(paypalMapString(resource, "status"))
|
|
if strings.HasPrefix(eventType, "CHECKOUT.ORDER.") {
|
|
var order paypal.OrderDetail
|
|
if err := json.Unmarshal(event.Resource, &order); err != nil {
|
|
return nil, err
|
|
}
|
|
identity, identityErr := paypalOrderIdentity(&order)
|
|
if identityErr != nil {
|
|
return nil, identityErr
|
|
}
|
|
tradeNo = identity
|
|
queryID = order.Id
|
|
if capture := paypalOrderCapture(&order); capture != nil {
|
|
providerTradeNo = capture.Id
|
|
}
|
|
status = paypalOrderCaptureState(&order)
|
|
} else {
|
|
providerTradeNo = paypalMapString(resource, "id")
|
|
}
|
|
if tradeNo == "" {
|
|
tradeNo = paypalPurchaseUnitTradeNo(resource)
|
|
}
|
|
if tradeNo == "" || queryID == "" {
|
|
return nil, errors.New("PayPal 回调缺少商户订单号或 order ID")
|
|
}
|
|
return &bizpayment.PaymentResult{
|
|
Provider: bizpayment.PaymentPayPal,
|
|
Status: status,
|
|
TradeNo: tradeNo,
|
|
ProviderTradeNo: providerTradeNo,
|
|
QueryID: queryID,
|
|
}, nil
|
|
}
|
|
|
|
func paypalPurchaseUnitTradeNo(resource map[string]any) string {
|
|
units, _ := resource["purchase_units"].([]any)
|
|
for _, value := range units {
|
|
unit, _ := value.(map[string]any)
|
|
if tradeNo := firstNonEmpty(paypalMapString(unit, "invoice_id"), paypalMapString(unit, "custom_id"), paypalMapString(unit, "reference_id")); tradeNo != "" {
|
|
return tradeNo
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func paypalCallbackHeader(callback *bizpayment.PaymentCallback, name string) string {
|
|
for key, value := range callback.Headers {
|
|
if strings.EqualFold(key, name) {
|
|
return strings.TrimSpace(value)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func paypalFormatAmount(amount int64, currency string, c map[string]any) (string, error) {
|
|
scale, err := paypalAmountScale(currency, c)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return formatDecimalAmount(amount, scale)
|
|
}
|
|
|
|
func paypalParseAmount(amount *paypal.Amount, c map[string]any) (int64, error) {
|
|
if amount == nil {
|
|
return 0, errors.New("PayPal 金额为空")
|
|
}
|
|
scale, err := paypalAmountScale(amount.CurrencyCode, c)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return parseDecimalAmount(amount.Value, scale)
|
|
}
|
|
|
|
func paypalAmountScale(currency string, c map[string]any) (int64, error) {
|
|
currency = strings.ToUpper(strings.TrimSpace(currency))
|
|
for _, key := range []string{"amount_scales", "currency_scales"} {
|
|
if scales, ok := c[key].(map[string]any); ok {
|
|
for configuredCurrency, raw := range scales {
|
|
if !strings.EqualFold(configuredCurrency, currency) {
|
|
continue
|
|
}
|
|
scale, err := strconv.ParseInt(strings.TrimSpace(fmt.Sprint(raw)), 10, 64)
|
|
if err != nil || scale <= 0 {
|
|
return 0, fmt.Errorf("PayPal %s 金额换算比例无效", currency)
|
|
}
|
|
if _, err = formatDecimalAmount(scale, scale); err != nil {
|
|
return 0, fmt.Errorf("PayPal %s 金额换算比例无效: %w", currency, err)
|
|
}
|
|
return scale, nil
|
|
}
|
|
}
|
|
}
|
|
if scale := configuredInt64(c, "amount_scale", 0); scale > 0 {
|
|
if _, err := formatDecimalAmount(scale, scale); err != nil {
|
|
return 0, fmt.Errorf("PayPal 金额换算比例无效: %w", err)
|
|
}
|
|
return scale, nil
|
|
}
|
|
switch currency {
|
|
case "HUF", "JPY", "TWD":
|
|
return 1, nil
|
|
default:
|
|
return 100, nil
|
|
}
|
|
}
|
|
|
|
func normalizePayPalCaptureState(state string) string {
|
|
switch strings.ToUpper(strings.TrimSpace(state)) {
|
|
case "COMPLETED", "PARTIALLY_REFUNDED", "REFUNDED":
|
|
return "success"
|
|
case "DECLINED", "FAILED", "DENIED", "VOIDED":
|
|
return "failed"
|
|
default:
|
|
return "pending"
|
|
}
|
|
}
|
|
|
|
func normalizePayPalRefundState(state string) string {
|
|
switch strings.ToUpper(strings.TrimSpace(state)) {
|
|
case "COMPLETED":
|
|
return "success"
|
|
case "CANCELLED", "FAILED":
|
|
return "failed"
|
|
default:
|
|
return "pending"
|
|
}
|
|
}
|
|
|
|
func paypalMapString(values map[string]any, key string) string {
|
|
value := values[key]
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(fmt.Sprint(value))
|
|
}
|
|
|
|
func paypalNestedMapString(values map[string]any, keys ...string) string {
|
|
var current any = values
|
|
for _, key := range keys {
|
|
object, ok := current.(map[string]any)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
current = object[key]
|
|
}
|
|
if current == nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(fmt.Sprint(current))
|
|
}
|
|
|
|
func paypalResponseError(operation string, code int, detail string) error {
|
|
detail = strings.TrimSpace(detail)
|
|
if detail == "" {
|
|
detail = "未知错误"
|
|
}
|
|
if code == 0 {
|
|
return fmt.Errorf("PayPal %s失败: %s", operation, detail)
|
|
}
|
|
return fmt.Errorf("PayPal %s失败(%d): %s", operation, code, detail)
|
|
}
|
|
|
|
func paypalResponseCode(value any) int {
|
|
switch rsp := value.(type) {
|
|
case *paypal.CreateOrderRsp:
|
|
if rsp != nil {
|
|
return rsp.Code
|
|
}
|
|
case *paypal.OrderDetailRsp:
|
|
if rsp != nil {
|
|
return rsp.Code
|
|
}
|
|
case *paypal.PaymentCaptureRefundRsp:
|
|
if rsp != nil {
|
|
return rsp.Code
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func paypalCreateError(rsp *paypal.CreateOrderRsp) string {
|
|
if rsp == nil {
|
|
return "响应为空"
|
|
}
|
|
return paypalErrorDetail(rsp.Error, rsp.ErrorResponse)
|
|
}
|
|
|
|
func paypalOrderError(rsp *paypal.OrderDetailRsp) string {
|
|
if rsp == nil {
|
|
return "响应为空"
|
|
}
|
|
return paypalErrorDetail(rsp.Error, rsp.ErrorResponse)
|
|
}
|
|
|
|
func paypalCaptureError(rsp *paypal.OrderCaptureRsp) string {
|
|
if rsp == nil {
|
|
return "响应为空"
|
|
}
|
|
return paypalErrorDetail(rsp.Error, rsp.ErrorResponse)
|
|
}
|
|
|
|
func paypalRefundError(rsp *paypal.PaymentCaptureRefundRsp) string {
|
|
if rsp == nil {
|
|
return "响应为空"
|
|
}
|
|
return paypalErrorDetail(rsp.Error, rsp.ErrorResponse)
|
|
}
|
|
|
|
func paypalErrorDetail(raw string, response *paypal.ErrorResponse) string {
|
|
if response != nil {
|
|
if value := firstNonEmpty(response.Message, response.Name); value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return raw
|
|
}
|