支付相关
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"kra/internal/biz"
|
||||
)
|
||||
|
||||
func TestPayPalCreateAndQueryKeepOrderAndCaptureIDsSeparate(t *testing.T) {
|
||||
var createBody map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/oauth2/token":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"access_token":"TOKEN","token_type":"Bearer","expires_in":3600}`))
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v2/checkout/orders":
|
||||
if got := r.Header.Get("PayPal-Request-Id"); got != "LOCAL-1" {
|
||||
t.Errorf("PayPal-Request-Id = %q, want LOCAL-1", got)
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&createBody); err != nil {
|
||||
t.Errorf("decode create body: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_, _ = w.Write([]byte(`{
|
||||
"id":"ORDER-1","status":"CREATED",
|
||||
"purchase_units":[{"reference_id":"LOCAL-1","invoice_id":"LOCAL-1","custom_id":"LOCAL-1","amount":{"currency_code":"USD","value":"10.99"}}],
|
||||
"links":[{"rel":"approve","href":"https://example.test/approve","method":"GET"}]
|
||||
}`))
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v2/checkout/orders/ORDER-1":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{
|
||||
"id":"ORDER-1","status":"COMPLETED",
|
||||
"purchase_units":[{
|
||||
"reference_id":"LOCAL-1","invoice_id":"LOCAL-1","custom_id":"LOCAL-1",
|
||||
"amount":{"currency_code":"USD","value":"10.99"},
|
||||
"payments":{"captures":[{"id":"CAPTURE-1","status":"COMPLETED","amount":{"currency_code":"USD","value":"10.99"}}]}
|
||||
}]
|
||||
}`))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
adapter := &paypalAdapter{}
|
||||
config := paypalTestConfig(server.URL)
|
||||
created, err := adapter.Create(context.Background(), &biz.PaymentRequest{
|
||||
TradeNo: "LOCAL-1",
|
||||
Subject: "Order one",
|
||||
Amount: 1099,
|
||||
Currency: "USD",
|
||||
ReturnURL: "https://merchant.test/return",
|
||||
Extra: map[string]any{"payment_source": map[string]any{"paypal": map[string]any{}}},
|
||||
}, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
if created.TradeNo != "LOCAL-1" || created.QueryID != "ORDER-1" || created.ProviderTradeNo != "" || created.Status != "pending" {
|
||||
t.Fatalf("Create() result = %+v", created)
|
||||
}
|
||||
units, _ := createBody["purchase_units"].([]any)
|
||||
if len(units) != 1 {
|
||||
t.Fatalf("purchase_units = %#v", createBody["purchase_units"])
|
||||
}
|
||||
unit, _ := units[0].(map[string]any)
|
||||
amount, _ := unit["amount"].(map[string]any)
|
||||
if unit["invoice_id"] != "LOCAL-1" || amount["value"] != "10.99" {
|
||||
t.Fatalf("purchase unit = %#v", unit)
|
||||
}
|
||||
if _, ok := createBody["payment_source"]; !ok {
|
||||
t.Fatalf("payment_source was not forwarded: %#v", createBody)
|
||||
}
|
||||
|
||||
queried, err := adapter.Query(context.Background(), created.QueryID, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Query() error = %v", err)
|
||||
}
|
||||
if queried.Status != "success" || queried.TradeNo != "LOCAL-1" || queried.QueryID != "ORDER-1" || queried.ProviderTradeNo != "CAPTURE-1" || queried.Amount != 1099 || queried.Currency != "USD" {
|
||||
t.Fatalf("Query() result = %+v", queried)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayPalRefundUsesPersistedCaptureID(t *testing.T) {
|
||||
var refundBody map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/oauth2/token":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"access_token":"TOKEN","token_type":"Bearer","expires_in":3600}`))
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v2/payments/captures/CAPTURE-1/refund":
|
||||
if got := r.Header.Get("PayPal-Request-Id"); got != "REFUND-1" {
|
||||
t.Errorf("PayPal-Request-Id = %q, want REFUND-1", got)
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&refundBody); err != nil {
|
||||
t.Errorf("decode refund body: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_, _ = w.Write([]byte(`{"id":"PAYPAL-REFUND-1","status":"PENDING","amount":{"currency_code":"USD","value":"1.23"}}`))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := paypalTestConfig(server.URL)
|
||||
config["provider_trade_no"] = "WRONG-CONFIG-CAPTURE"
|
||||
result, err := (&paypalAdapter{}).Refund(context.Background(), &biz.PaymentRefundRequest{
|
||||
Provider: biz.PaymentPayPal, TradeNo: "LOCAL-1", ProviderTradeNo: "CAPTURE-1",
|
||||
QueryID: "ORDER-1", RefundNo: "REFUND-1", Amount: 123, TotalAmount: 1099, Currency: "USD",
|
||||
}, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Refund() error = %v", err)
|
||||
}
|
||||
if result.TradeNo != "LOCAL-1" || result.ProviderTradeNo != "PAYPAL-REFUND-1" || result.Amount != 123 || result.Currency != "USD" || result.Status != "pending" {
|
||||
t.Fatalf("Refund() result = %+v", result)
|
||||
}
|
||||
amount, _ := refundBody["amount"].(map[string]any)
|
||||
if refundBody["invoice_id"] != "REFUND-1" || amount["value"] != "1.23" {
|
||||
t.Fatalf("refund body = %#v", refundBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayPalRefundResolvesCaptureFromPersistedQueryID(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/oauth2/token":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"access_token":"TOKEN","token_type":"Bearer","expires_in":3600}`))
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v2/checkout/orders/ORDER-1":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{
|
||||
"id":"ORDER-1","status":"COMPLETED",
|
||||
"purchase_units":[{
|
||||
"invoice_id":"LOCAL-1",
|
||||
"amount":{"currency_code":"USD","value":"10.99"},
|
||||
"payments":{"captures":[{"id":"CAPTURE-FROM-ORDER","status":"COMPLETED"}]}
|
||||
}]
|
||||
}`))
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v2/payments/captures/CAPTURE-FROM-ORDER/refund":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_, _ = w.Write([]byte(`{"id":"PAYPAL-REFUND-2","status":"COMPLETED","amount":{"currency_code":"USD","value":"1.23"}}`))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := paypalTestConfig(server.URL)
|
||||
config["capture_id"] = "WRONG-CONFIG-CAPTURE"
|
||||
result, err := (&paypalAdapter{}).Refund(context.Background(), &biz.PaymentRefundRequest{
|
||||
Provider: biz.PaymentPayPal, TradeNo: "LOCAL-1", QueryID: "ORDER-1",
|
||||
RefundNo: "REFUND-2", Amount: 123, TotalAmount: 1099, Currency: "USD",
|
||||
}, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Refund() error = %v", err)
|
||||
}
|
||||
if result.ProviderTradeNo != "PAYPAL-REFUND-2" || result.Status != "success" {
|
||||
t.Fatalf("Refund() result = %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayPalRefundRejectsStaticCaptureIDWithoutDurableIdentity(t *testing.T) {
|
||||
config := paypalTestConfig("https://paypal.invalid")
|
||||
config["capture_id"] = "CONFIG-CAPTURE"
|
||||
_, err := (&paypalAdapter{}).Refund(context.Background(), &biz.PaymentRefundRequest{
|
||||
Provider: biz.PaymentPayPal, TradeNo: "LOCAL-1", RefundNo: "REFUND-1", Amount: 123, Currency: "USD",
|
||||
}, config)
|
||||
if err == nil {
|
||||
t.Fatal("Refund() accepted a static config capture ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPayPalCallbackUsesGoPayWebhookVerification(t *testing.T) {
|
||||
var verifyBody map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/oauth2/token":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"access_token":"TOKEN","token_type":"Bearer","expires_in":3600}`))
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/notifications/verify-webhook-signature":
|
||||
if err := json.NewDecoder(r.Body).Decode(&verifyBody); err != nil {
|
||||
t.Errorf("decode verify body: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"verification_status":"SUCCESS"}`))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
body := []byte(`{
|
||||
"id":"WEBHOOK-1","event_type":"PAYMENT.CAPTURE.COMPLETED",
|
||||
"resource":{
|
||||
"id":"CAPTURE-1","status":"COMPLETED","invoice_id":"LOCAL-1",
|
||||
"supplementary_data":{"related_ids":{"order_id":"ORDER-1"}}
|
||||
}
|
||||
}`)
|
||||
config := paypalTestConfig(server.URL)
|
||||
config["webhook_id"] = "HOOK-1"
|
||||
result, err := (&paypalAdapter{}).Callback(context.Background(), &biz.PaymentCallback{
|
||||
Body: body,
|
||||
Headers: map[string]string{
|
||||
"Paypal-Auth-Algo": "SHA256withRSA",
|
||||
"Paypal-Cert-Url": "https://api.paypal.test/cert",
|
||||
"Paypal-Transmission-Id": "TRANSMISSION-1",
|
||||
"Paypal-Transmission-Sig": "SIGNATURE",
|
||||
"Paypal-Transmission-Time": "2026-08-19T00:00:00Z",
|
||||
},
|
||||
}, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Callback() error = %v", err)
|
||||
}
|
||||
if result.TradeNo != "LOCAL-1" || result.QueryID != "ORDER-1" || result.ProviderTradeNo != "CAPTURE-1" || result.EventID != "WEBHOOK-1" || result.Status != "success" {
|
||||
t.Fatalf("Callback() result = %+v", result)
|
||||
}
|
||||
if verifyBody["webhook_id"] != "HOOK-1" || verifyBody["transmission_id"] != "TRANSMISSION-1" {
|
||||
t.Fatalf("verify body = %#v", verifyBody)
|
||||
}
|
||||
if _, ok := verifyBody["webhook_event"].(map[string]any); !ok {
|
||||
t.Fatalf("webhook_event was not sent as an object: %#v", verifyBody["webhook_event"])
|
||||
}
|
||||
}
|
||||
|
||||
func paypalTestConfig(baseURL string) map[string]any {
|
||||
return map[string]any{
|
||||
"client_id": "client-id",
|
||||
"client_secret": "client-secret",
|
||||
"environment": "sandbox",
|
||||
"api_base_url": baseURL,
|
||||
"cancel_url": "https://merchant.test/cancel",
|
||||
"notify_url": "https://merchant.test/paypal/webhook",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user