package payment import ( "context" "encoding/json" "io" bizpayment "kra/internal/biz/payment" "net/http" "net/http/httptest" "strings" "testing" gopayWechatV3 "github.com/go-pay/gopay/wechat/v3" ) func TestWechatV3CodePayUsesGoPaySDK(t *testing.T) { platformKey, _, platformPublicPEM := testRSAKeyPair(t) _, merchantPrivatePEM, _ := testRSAKeyPair(t) const ( platformSerial = "PLATFORM-SERIAL" timestamp = "1787068800" nonce = "codepay-response-nonce" ) response := []byte(`{"appid":"wx-test","mchid":"mch-test","out_trade_no":"T-WECHAT-CODEPAY","transaction_id":"WX-CODEPAY-1","trade_type":"MICROPAY","trade_state":"SUCCESS","amount":{"total":123,"payer_total":100,"currency":"CNY","payer_currency":"CNY"},"promotion_detail":[{"coupon_id":"COUPON-1","amount":23}]}`) signature := testRSA2Sign(t, platformKey, []byte(timestamp+"\n"+nonce+"\n"+string(response)+"\n")) var requestBody map[string]any server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/v3/pay/transactions/codepay" { t.Errorf("request = %s %s", r.Method, r.URL.Path) } if strings.TrimSpace(r.Header.Get("Authorization")) == "" { t.Error("missing GoPay authorization header") } body, err := io.ReadAll(r.Body) if err != nil { t.Errorf("read request: %v", err) return } if err = json.Unmarshal(body, &requestBody); err != nil { t.Errorf("decode request: %v", err) return } w.Header().Set(gopayWechatV3.HeaderTimestamp, timestamp) w.Header().Set(gopayWechatV3.HeaderNonce, nonce) w.Header().Set(gopayWechatV3.HeaderSerial, platformSerial) w.Header().Set(gopayWechatV3.HeaderSignature, signature) w.Header().Set("Content-Type", "application/json") _, _ = w.Write(response) })) defer server.Close() result, err := (&wechatV3Adapter{}).Create(context.Background(), &bizpayment.PaymentRequest{ Provider: bizpayment.PaymentWechatV3, TradeNo: "T-WECHAT-CODEPAY", Subject: "subject", Amount: 123, Currency: "CNY", NotifyURL: "https://merchant.test/payment/callback", Extra: map[string]any{ "method": "codepay", "auth_code": "AUTH-CODE-1", "payer": map[string]any{"auth_code": "OVERRIDE"}, }, }, map[string]any{ "app_id": "wx-test", "merchant_id": "mch-test", "serial_no": "merchant-serial", "private_key": merchantPrivatePEM, "api_v3_key": "01234567890123456789012345678901", "platform_cert": platformPublicPEM, "platform_serial_no": platformSerial, "create_url": server.URL, }) if err != nil { t.Fatal(err) } payer, _ := requestBody["payer"].(map[string]any) amount, _ := requestBody["amount"].(map[string]any) if payer["auth_code"] != "AUTH-CODE-1" || requestBody["out_trade_no"] != "T-WECHAT-CODEPAY" { t.Fatalf("request identity = %#v", requestBody) } if amount["total"] != float64(123) || amount["currency"] != "CNY" { t.Fatalf("request amount = %#v", amount) } if result.Provider != bizpayment.PaymentWechatV3 || result.Status != "success" || result.TradeNo != "T-WECHAT-CODEPAY" || result.ProviderTradeNo != "WX-CODEPAY-1" || result.QueryID != "" { t.Fatalf("result identity = %+v", result) } if result.Amount != 123 || result.Currency != "CNY" || result.PayerCurrency != "CNY" || !result.AmountBreakdownKnown || result.PayerPaidAmount != 100 || result.CashPaidAmount != 100 || result.DiscountAmount != 23 { t.Fatalf("result amount = %+v", result) } } func TestWechatV3CodePayAliasesRequireAuthCode(t *testing.T) { _, merchantPrivatePEM, _ := testRSAKeyPair(t) _, _, platformPublicPEM := testRSAKeyPair(t) config := map[string]any{ "app_id": "wx-test", "merchant_id": "mch-test", "serial_no": "merchant-serial", "private_key": merchantPrivatePEM, "api_v3_key": "01234567890123456789012345678901", "platform_cert": platformPublicPEM, "platform_serial_no": "platform-serial", } for _, method := range []string{"micropay", "micro_pay", "micro-pay", "codepay", "code_pay", "code-pay", "barcode", "barcode_pay", "facepay", "face_pay"} { t.Run(method, func(t *testing.T) { _, err := (&wechatV3Adapter{}).Create(context.Background(), &bizpayment.PaymentRequest{ TradeNo: "T-WECHAT-CODEPAY", Subject: "subject", Amount: 1, Currency: "CNY", Extra: map[string]any{"method": method}, }, config) if err == nil || !strings.Contains(err.Error(), "auth_code") { t.Fatalf("method %q error = %v", method, err) } }) } } func TestWechatV3CodePayResultRejectsInvalidIdentity(t *testing.T) { _, err := wechatV3CodePayResult("T-WECHAT-CODEPAY", &gopayWechatV3.CodePayRsp{ Code: gopayWechatV3.Success, Response: &gopayWechatV3.CodePay{ OutTradeNo: "OTHER-ORDER", TradeState: "SUCCESS", Amount: &gopayWechatV3.Amount{Total: 1, Currency: "CNY"}, }, }) if err == nil || !strings.Contains(err.Error(), "商户订单号不匹配") { t.Fatalf("identity mismatch error = %v", err) } } func TestWechatV3CodePayResultRequiresMerchantOrderNumber(t *testing.T) { _, err := wechatV3CodePayResult("T-WECHAT-CODEPAY", &gopayWechatV3.CodePayRsp{ Code: gopayWechatV3.Success, Response: &gopayWechatV3.CodePay{ TradeState: "SUCCESS", TransactionId: "WX-CODEPAY-1", Amount: &gopayWechatV3.Amount{Total: 1, Currency: "CNY"}, }, }) if err == nil || !strings.Contains(err.Error(), "out_trade_no") { t.Fatalf("missing out_trade_no error = %v", err) } } func TestWechatV3CodePayResultPreservesFullyDiscountedBreakdown(t *testing.T) { result, err := wechatV3CodePayResult("T-WECHAT-CODEPAY", &gopayWechatV3.CodePayRsp{ Code: gopayWechatV3.Success, Response: &gopayWechatV3.CodePay{ OutTradeNo: "T-WECHAT-CODEPAY", TransactionId: "WX-CODEPAY-1", TradeState: "SUCCESS", Amount: &gopayWechatV3.Amount{ Total: 100, PayerTotal: 0, Currency: "CNY", PayerCurrency: "CNY", }, }, }) if err != nil { t.Fatal(err) } if !result.AmountBreakdownKnown || result.PayerPaidAmount != 0 || result.CashPaidAmount != 0 || result.DiscountAmount != 100 || result.PayerCurrency != "CNY" { t.Fatalf("fully discounted result = %+v", result) } } func TestWechatV3CodePayDoesNotInferMissingPayerTotal(t *testing.T) { result, err := wechatV3CodePayResult("T-WECHAT-CODEPAY", &gopayWechatV3.CodePayRsp{ Code: gopayWechatV3.Success, SignInfo: &gopayWechatV3.SignInfo{SignBody: `{"out_trade_no":"T-WECHAT-CODEPAY","transaction_id":"WX-CODEPAY-2","trade_state":"SUCCESS","amount":{"total":100,"currency":"CNY"}}`}, Response: &gopayWechatV3.CodePay{ OutTradeNo: "T-WECHAT-CODEPAY", TransactionId: "WX-CODEPAY-2", TradeState: "SUCCESS", Amount: &gopayWechatV3.Amount{Total: 100, Currency: "CNY"}, }, }) if err != nil { t.Fatal(err) } if result.AmountBreakdownKnown || result.PayerPaidAmount != 0 || result.DiscountAmount != 0 { t.Fatalf("missing payer_total was treated as a complete breakdown: %+v", result) } } func TestWechatV3CodePayUserPayingRemainsPending(t *testing.T) { result, err := wechatV3CodePayResult("T-WECHAT-CODEPAY", &gopayWechatV3.CodePayRsp{ Code: gopayWechatV3.Success, Response: &gopayWechatV3.CodePay{ OutTradeNo: "T-WECHAT-CODEPAY", TradeState: "USERPAYING", }, }) if err != nil { t.Fatal(err) } if result.Status != "pending" || result.TradeNo != "T-WECHAT-CODEPAY" || result.ProviderTradeNo != "" || result.QueryID != "" { t.Fatalf("pending result = %+v", result) } }