优化结构
This commit is contained in:
@@ -1,781 +0,0 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"kra/internal/biz"
|
||||
|
||||
"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 *biz.PaymentRequest, c map[string]any) (*biz.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) (*biz.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 *biz.PaymentRefundRequest, c map[string]any) (*biz.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 *biz.PaymentRefundRequest, refund *paypal.PaymentCaptureRefund, c map[string]any) (*biz.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 := &biz.PaymentResult{
|
||||
Provider: biz.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 *biz.PaymentRequest, order *paypal.OrderDetail, c map[string]any) (*biz.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 *biz.PaymentCallback, c map[string]any) (*biz.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 *biz.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) (*biz.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 := &biz.PaymentResult{
|
||||
Provider: biz.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 paypalOrderTradeNo(order *paypal.OrderDetail) string {
|
||||
tradeNo, _ := paypalOrderIdentity(order)
|
||||
return tradeNo
|
||||
}
|
||||
|
||||
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 *biz.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) (*biz.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 &biz.PaymentResult{
|
||||
Provider: biz.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 *biz.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 normalizePayPalOrderState(state string) string {
|
||||
switch strings.ToUpper(strings.TrimSpace(state)) {
|
||||
case "COMPLETED":
|
||||
return "success"
|
||||
case "VOIDED":
|
||||
return "failed"
|
||||
default:
|
||||
return "pending"
|
||||
}
|
||||
}
|
||||
|
||||
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 firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if value = strings.TrimSpace(value); value != "" && value != "<nil>" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user