payment_order_result_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. package service
  2. import (
  3. "context"
  4. "strings"
  5. "testing"
  6. "time"
  7. dbent "github.com/Wei-Shaw/sub2api/ent"
  8. "github.com/Wei-Shaw/sub2api/internal/payment"
  9. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  10. )
  11. func TestShouldUseAlipayMobilePrecreate(t *testing.T) {
  12. t.Parallel()
  13. enabled := &PaymentConfig{AlipayMobilePrecreateDeepLink: true}
  14. officialAlipay := &payment.InstanceSelection{ProviderKey: payment.TypeAlipay}
  15. tests := []struct {
  16. name string
  17. req CreateOrderRequest
  18. cfg *PaymentConfig
  19. sel *payment.InstanceSelection
  20. want bool
  21. }{
  22. {name: "mobile official alipay with switch", req: CreateOrderRequest{IsMobile: true}, cfg: enabled, sel: officialAlipay, want: true},
  23. {name: "desktop remains unchanged", req: CreateOrderRequest{IsMobile: false}, cfg: enabled, sel: officialAlipay, want: false},
  24. {name: "switch disabled keeps wap", req: CreateOrderRequest{IsMobile: true}, cfg: &PaymentConfig{}, sel: officialAlipay, want: false},
  25. {name: "other provider remains unchanged", req: CreateOrderRequest{IsMobile: true}, cfg: enabled, sel: &payment.InstanceSelection{ProviderKey: payment.TypeEasyPay}, want: false},
  26. }
  27. for _, tt := range tests {
  28. t.Run(tt.name, func(t *testing.T) {
  29. t.Parallel()
  30. if got := shouldUseAlipayMobilePrecreate(tt.req, tt.cfg, tt.sel); got != tt.want {
  31. t.Fatalf("shouldUseAlipayMobilePrecreate() = %v, want %v", got, tt.want)
  32. }
  33. })
  34. }
  35. }
  36. func TestIsOfficialAlipayProviderInstance(t *testing.T) {
  37. t.Parallel()
  38. tests := []struct {
  39. name string
  40. instance *dbent.PaymentProviderInstance
  41. want bool
  42. }{
  43. {name: "nil instance", instance: nil, want: false},
  44. {name: "official alipay", instance: &dbent.PaymentProviderInstance{ProviderKey: payment.TypeAlipay}, want: true},
  45. {name: "normalized official alipay", instance: &dbent.PaymentProviderInstance{ProviderKey: " ALIPAY "}, want: true},
  46. {name: "easypay alipay route", instance: &dbent.PaymentProviderInstance{ProviderKey: payment.TypeEasyPay}, want: false},
  47. }
  48. for _, tt := range tests {
  49. t.Run(tt.name, func(t *testing.T) {
  50. t.Parallel()
  51. if got := isOfficialAlipayProviderInstance(tt.instance); got != tt.want {
  52. t.Fatalf("isOfficialAlipayProviderInstance() = %v, want %v", got, tt.want)
  53. }
  54. })
  55. }
  56. }
  57. func TestBuildCreateOrderResponseDefaultsToOrderCreated(t *testing.T) {
  58. t.Parallel()
  59. expiresAt := time.Date(2026, 4, 16, 12, 0, 0, 0, time.UTC)
  60. resp := buildCreateOrderResponse(
  61. &dbent.PaymentOrder{
  62. ID: 42,
  63. Amount: 12.34,
  64. FeeRate: 0.03,
  65. ExpiresAt: expiresAt,
  66. OutTradeNo: "sub2_42",
  67. },
  68. CreateOrderRequest{PaymentType: payment.TypeWxpay},
  69. 12.71,
  70. &payment.InstanceSelection{PaymentMode: "qrcode"},
  71. &payment.CreatePaymentResponse{
  72. TradeNo: "sub2_42",
  73. QRCode: "weixin://wxpay/bizpayurl?pr=test",
  74. },
  75. payment.CreatePaymentResultOrderCreated,
  76. )
  77. if resp.ResultType != payment.CreatePaymentResultOrderCreated {
  78. t.Fatalf("result type = %q, want %q", resp.ResultType, payment.CreatePaymentResultOrderCreated)
  79. }
  80. if resp.OutTradeNo != "sub2_42" {
  81. t.Fatalf("out_trade_no = %q, want %q", resp.OutTradeNo, "sub2_42")
  82. }
  83. if resp.QRCode != "weixin://wxpay/bizpayurl?pr=test" {
  84. t.Fatalf("qr_code = %q, want %q", resp.QRCode, "weixin://wxpay/bizpayurl?pr=test")
  85. }
  86. if resp.JSAPI != nil || resp.JSAPIPayload != nil {
  87. t.Fatal("order_created response should not include jsapi payload")
  88. }
  89. if !resp.ExpiresAt.Equal(expiresAt) {
  90. t.Fatalf("expires_at = %v, want %v", resp.ExpiresAt, expiresAt)
  91. }
  92. }
  93. func TestBuildCreateOrderResponseCopiesJSAPIPayload(t *testing.T) {
  94. t.Parallel()
  95. jsapiPayload := &payment.WechatJSAPIPayload{
  96. AppID: "wx123",
  97. TimeStamp: "1712345678",
  98. NonceStr: "nonce-123",
  99. Package: "prepay_id=wx123",
  100. SignType: "RSA",
  101. PaySign: "signed-payload",
  102. }
  103. resp := buildCreateOrderResponse(
  104. &dbent.PaymentOrder{
  105. ID: 88,
  106. Amount: 66.88,
  107. FeeRate: 0.01,
  108. ExpiresAt: time.Date(2026, 4, 16, 13, 0, 0, 0, time.UTC),
  109. OutTradeNo: "sub2_88",
  110. },
  111. CreateOrderRequest{PaymentType: payment.TypeWxpay},
  112. 67.55,
  113. &payment.InstanceSelection{PaymentMode: "popup"},
  114. &payment.CreatePaymentResponse{
  115. TradeNo: "sub2_88",
  116. ResultType: payment.CreatePaymentResultJSAPIReady,
  117. JSAPI: jsapiPayload,
  118. },
  119. payment.CreatePaymentResultJSAPIReady,
  120. )
  121. if resp.ResultType != payment.CreatePaymentResultJSAPIReady {
  122. t.Fatalf("result type = %q, want %q", resp.ResultType, payment.CreatePaymentResultJSAPIReady)
  123. }
  124. if resp.JSAPI == nil || resp.JSAPIPayload == nil {
  125. t.Fatal("expected jsapi payload aliases to be populated")
  126. }
  127. if resp.JSAPI != jsapiPayload || resp.JSAPIPayload != jsapiPayload {
  128. t.Fatal("expected jsapi aliases to preserve the original pointer")
  129. }
  130. }
  131. func TestSanitizeCreatePaymentResponseDetailsRemovesNULBytes(t *testing.T) {
  132. t.Parallel()
  133. resp := &payment.CreatePaymentResponse{
  134. TradeNo: "trade\x00-no",
  135. PayURL: "https://pay.example.com/\x00checkout",
  136. QRCode: "wxp://payment-token\x00",
  137. ClientSecret: "secret\x00unchanged",
  138. }
  139. sanitizeCreatePaymentResponseDetails(resp)
  140. if strings.ContainsRune(resp.TradeNo, 0) {
  141. t.Fatalf("trade_no still contains NUL: %q", resp.TradeNo)
  142. }
  143. if strings.ContainsRune(resp.PayURL, 0) {
  144. t.Fatalf("pay_url still contains NUL: %q", resp.PayURL)
  145. }
  146. if strings.ContainsRune(resp.QRCode, 0) {
  147. t.Fatalf("qr_code still contains NUL: %q", resp.QRCode)
  148. }
  149. if resp.TradeNo != "trade-no" {
  150. t.Fatalf("trade_no = %q, want trade-no", resp.TradeNo)
  151. }
  152. if resp.PayURL != "https://pay.example.com/checkout" {
  153. t.Fatalf("pay_url = %q, want sanitized URL", resp.PayURL)
  154. }
  155. if resp.QRCode != "wxp://payment-token" {
  156. t.Fatalf("qr_code = %q, want sanitized QR code", resp.QRCode)
  157. }
  158. if resp.ClientSecret != "secret\x00unchanged" {
  159. t.Fatalf("client_secret = %q, should not be touched by payment detail sanitization", resp.ClientSecret)
  160. }
  161. }
  162. func TestValidateSelectedCreateOrderAmountCurrencyRejectsFractionalZeroDecimal(t *testing.T) {
  163. t.Parallel()
  164. err := validateSelectedCreateOrderAmountCurrency("100.50", &payment.InstanceSelection{
  165. ProviderKey: payment.TypeStripe,
  166. Config: map[string]string{"currency": "JPY"},
  167. })
  168. if err == nil {
  169. t.Fatal("expected fractional JPY amount to fail")
  170. }
  171. if appErr := infraerrors.FromError(err); appErr.Reason != "INVALID_AMOUNT" {
  172. t.Fatalf("reason = %q, want INVALID_AMOUNT", appErr.Reason)
  173. }
  174. }
  175. func TestCalculateCreateOrderPayAmountUsesCurrencyPrecision(t *testing.T) {
  176. t.Parallel()
  177. amountStr, amount, err := calculateCreateOrderPayAmount(100, 2.5, "JPY")
  178. if err != nil {
  179. t.Fatalf("unexpected error: %v", err)
  180. }
  181. if amountStr != "103" || amount != 103 {
  182. t.Fatalf("JPY pay amount = (%q, %v), want (103, 103)", amountStr, amount)
  183. }
  184. amountStr, amount, err = calculateCreateOrderPayAmount(12.345, 1, "KWD")
  185. if err != nil {
  186. t.Fatalf("unexpected error: %v", err)
  187. }
  188. if amountStr != "12.469" || amount != 12.469 {
  189. t.Fatalf("KWD pay amount = (%q, %v), want (12.469, 12.469)", amountStr, amount)
  190. }
  191. }
  192. func TestCalculateCreateOrderPayAmountForSubscriptionConvertsCNYPriceWhenRateConfigured(t *testing.T) {
  193. t.Parallel()
  194. amountStr, amount, err := calculateCreateOrderPayAmountForOrderType(9.99, 0, "CNY", payment.OrderTypeSubscription, 7.15)
  195. if err != nil {
  196. t.Fatalf("unexpected error: %v", err)
  197. }
  198. if amountStr != "71.43" || amount != 71.43 {
  199. t.Fatalf("subscription CNY pay amount = (%q, %v), want (71.43, 71.43)", amountStr, amount)
  200. }
  201. }
  202. func TestCalculateCreateOrderPayAmountForSubscriptionAppliesFeeAfterCNYConversion(t *testing.T) {
  203. t.Parallel()
  204. amountStr, amount, err := calculateCreateOrderPayAmountForOrderType(9.99, 2.5, "CNY", payment.OrderTypeSubscription, 7.15)
  205. if err != nil {
  206. t.Fatalf("unexpected error: %v", err)
  207. }
  208. if amountStr != "73.22" || amount != 73.22 {
  209. t.Fatalf("subscription CNY pay amount with fee = (%q, %v), want (73.22, 73.22)", amountStr, amount)
  210. }
  211. }
  212. func TestCalculateCreateOrderPayAmountForSubscriptionKeepsNonCNYPrice(t *testing.T) {
  213. t.Parallel()
  214. amountStr, amount, err := calculateCreateOrderPayAmountForOrderType(9.99, 0, "USD", payment.OrderTypeSubscription, 7.15)
  215. if err != nil {
  216. t.Fatalf("unexpected error: %v", err)
  217. }
  218. if amountStr != "9.99" || amount != 9.99 {
  219. t.Fatalf("subscription USD pay amount = (%q, %v), want (9.99, 9.99)", amountStr, amount)
  220. }
  221. }
  222. // 换算是 opt-in:未配置汇率(rate=0)时,CNY 订阅保持 price 直付的存量行为。
  223. // 该测试锁住存量部署升级后行为不变的兼容承诺。
  224. func TestCalculateCreateOrderPayAmountForSubscriptionKeepsDirectPriceWhenRateDisabled(t *testing.T) {
  225. t.Parallel()
  226. amountStr, amount, err := calculateCreateOrderPayAmountForOrderType(9.99, 0, "CNY", payment.OrderTypeSubscription, 0)
  227. if err != nil {
  228. t.Fatalf("unexpected error: %v", err)
  229. }
  230. if amountStr != "9.99" || amount != 9.99 {
  231. t.Fatalf("subscription CNY pay amount without rate = (%q, %v), want (9.99, 9.99)", amountStr, amount)
  232. }
  233. }
  234. // 汇率只作用于订阅订单,余额充值订单不受影响。
  235. func TestCalculateCreateOrderPayAmountForBalanceIgnoresSubscriptionRate(t *testing.T) {
  236. t.Parallel()
  237. amountStr, amount, err := calculateCreateOrderPayAmountForOrderType(50, 0, "CNY", payment.OrderTypeBalance, 7.15)
  238. if err != nil {
  239. t.Fatalf("unexpected error: %v", err)
  240. }
  241. if amountStr != "50.00" || amount != 50 {
  242. t.Fatalf("balance CNY pay amount = (%q, %v), want (50.00, 50)", amountStr, amount)
  243. }
  244. }
  245. func TestCalculateCreditedBalanceStillUsesRechargeMultiplier(t *testing.T) {
  246. t.Parallel()
  247. got := calculateCreditedBalance(10, 0.14)
  248. if got != 1.4 {
  249. t.Fatalf("credited balance = %v, want 1.4", got)
  250. }
  251. got = calculateCreditedBalance(5, 10)
  252. if got != 50 {
  253. t.Fatalf("credited balance = %v, want 50", got)
  254. }
  255. }
  256. func TestCalculateCreateOrderPayAmountRejectsFractionalZeroDecimal(t *testing.T) {
  257. t.Parallel()
  258. _, _, err := calculateCreateOrderPayAmount(100.5, 0, "JPY")
  259. if err == nil {
  260. t.Fatal("expected fractional JPY amount to fail")
  261. }
  262. if appErr := infraerrors.FromError(err); appErr.Reason != "INVALID_AMOUNT" {
  263. t.Fatalf("reason = %q, want INVALID_AMOUNT", appErr.Reason)
  264. }
  265. }
  266. func TestComputeValidityDaysSupportsSingularAndPluralUnits(t *testing.T) {
  267. t.Parallel()
  268. tests := []struct {
  269. name string
  270. days int
  271. unit string
  272. want int
  273. }{
  274. {name: "days", days: 1, unit: "days", want: 1},
  275. {name: "week", days: 1, unit: "week", want: 7},
  276. {name: "weeks", days: 2, unit: "weeks", want: 14},
  277. {name: "month", days: 1, unit: "month", want: 30},
  278. {name: "months", days: 1, unit: "months", want: 30},
  279. }
  280. for _, tt := range tests {
  281. tt := tt
  282. t.Run(tt.name, func(t *testing.T) {
  283. t.Parallel()
  284. if got := psComputeValidityDays(tt.days, tt.unit); got != tt.want {
  285. t.Fatalf("psComputeValidityDays(%d, %q) = %d, want %d", tt.days, tt.unit, got, tt.want)
  286. }
  287. })
  288. }
  289. }
  290. func TestBuildPaymentSubjectAppliesAffixToSubscriptionPlanProductName(t *testing.T) {
  291. t.Parallel()
  292. svc := &PaymentService{}
  293. cfg := &PaymentConfig{
  294. ProductNamePrefix: "PRE",
  295. ProductNameSuffix: "SUF",
  296. }
  297. plan := &dbent.SubscriptionPlan{
  298. Name: "Pro Monthly",
  299. ProductName: "Claude Pro",
  300. }
  301. got := svc.buildPaymentSubject(plan, 0, cfg, nil)
  302. if got != "PRE Claude Pro SUF" {
  303. t.Fatalf("buildPaymentSubject() = %q, want %q", got, "PRE Claude Pro SUF")
  304. }
  305. }
  306. func TestBuildPaymentSubjectAppliesAffixToSubscriptionPlanDefaultName(t *testing.T) {
  307. t.Parallel()
  308. svc := &PaymentService{}
  309. cfg := &PaymentConfig{
  310. ProductNamePrefix: "PRE",
  311. ProductNameSuffix: "SUF",
  312. }
  313. plan := &dbent.SubscriptionPlan{Name: "Team Monthly"}
  314. got := svc.buildPaymentSubject(plan, 0, cfg, nil)
  315. if got != "PRE Sub2API Subscription Team Monthly SUF" {
  316. t.Fatalf("buildPaymentSubject() = %q, want %q", got, "PRE Sub2API Subscription Team Monthly SUF")
  317. }
  318. }
  319. func TestMaybeBuildWeChatOAuthRequiredResponse(t *testing.T) {
  320. t.Setenv("PAYMENT_RESUME_SIGNING_KEY", "0123456789abcdef0123456789abcdef")
  321. svc := newWeChatPaymentOAuthTestService(map[string]string{
  322. SettingKeyWeChatConnectEnabled: "true",
  323. SettingKeyWeChatConnectAppID: "wx123456",
  324. SettingKeyWeChatConnectAppSecret: "wechat-secret",
  325. SettingKeyWeChatConnectMode: "mp",
  326. SettingKeyWeChatConnectScopes: "snsapi_base",
  327. SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
  328. SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
  329. })
  330. resp, err := svc.maybeBuildWeChatOAuthRequiredResponse(context.Background(), CreateOrderRequest{
  331. Amount: 12.5,
  332. PaymentType: payment.TypeWxpay,
  333. IsWeChatBrowser: true,
  334. SrcURL: "https://merchant.example/payment?from=wechat",
  335. OrderType: payment.OrderTypeBalance,
  336. }, 12.5, 12.88, 0.03)
  337. if err != nil {
  338. t.Fatalf("unexpected error: %v", err)
  339. }
  340. if resp == nil {
  341. t.Fatal("expected oauth_required response, got nil")
  342. }
  343. if resp.ResultType != payment.CreatePaymentResultOAuthRequired {
  344. t.Fatalf("result type = %q, want %q", resp.ResultType, payment.CreatePaymentResultOAuthRequired)
  345. }
  346. if resp.OAuth == nil {
  347. t.Fatal("expected oauth payload, got nil")
  348. }
  349. if resp.OAuth.AppID != "wx123456" {
  350. t.Fatalf("appid = %q, want %q", resp.OAuth.AppID, "wx123456")
  351. }
  352. if resp.OAuth.Scope != "snsapi_base" {
  353. t.Fatalf("scope = %q, want %q", resp.OAuth.Scope, "snsapi_base")
  354. }
  355. if resp.OAuth.RedirectURL != "/auth/wechat/payment/callback" {
  356. t.Fatalf("redirect_url = %q, want %q", resp.OAuth.RedirectURL, "/auth/wechat/payment/callback")
  357. }
  358. if resp.OAuth.AuthorizeURL != "/api/v1/auth/oauth/wechat/payment/start?amount=12.5&order_type=balance&payment_type=wxpay&redirect=%2Fpurchase%3Ffrom%3Dwechat&scope=snsapi_base" {
  359. t.Fatalf("authorize_url = %q", resp.OAuth.AuthorizeURL)
  360. }
  361. }
  362. func TestMaybeBuildWeChatOAuthRequiredResponseRequiresMPConfigInWeChat(t *testing.T) {
  363. t.Parallel()
  364. svc := newWeChatPaymentOAuthTestService(nil)
  365. resp, err := svc.maybeBuildWeChatOAuthRequiredResponse(context.Background(), CreateOrderRequest{
  366. Amount: 12.5,
  367. PaymentType: payment.TypeWxpay,
  368. IsWeChatBrowser: true,
  369. SrcURL: "https://merchant.example/payment?from=wechat",
  370. OrderType: payment.OrderTypeBalance,
  371. }, 12.5, 12.88, 0.03)
  372. if resp != nil {
  373. t.Fatalf("expected nil response, got %+v", resp)
  374. }
  375. if err == nil {
  376. t.Fatal("expected error, got nil")
  377. }
  378. appErr := infraerrors.FromError(err)
  379. if appErr.Reason != "WECHAT_PAYMENT_MP_NOT_CONFIGURED" {
  380. t.Fatalf("reason = %q, want %q", appErr.Reason, "WECHAT_PAYMENT_MP_NOT_CONFIGURED")
  381. }
  382. }
  383. func TestMaybeBuildWeChatOAuthRequiredResponseRequiresResumeSigningKey(t *testing.T) {
  384. t.Parallel()
  385. svc := &PaymentService{
  386. configService: &PaymentConfigService{
  387. settingRepo: &paymentConfigSettingRepoStub{values: map[string]string{
  388. SettingKeyWeChatConnectEnabled: "true",
  389. SettingKeyWeChatConnectAppID: "wx123456",
  390. SettingKeyWeChatConnectAppSecret: "wechat-secret",
  391. SettingKeyWeChatConnectMode: "mp",
  392. SettingKeyWeChatConnectScopes: "snsapi_base",
  393. SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
  394. SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
  395. }},
  396. // Intentionally missing payment resume signing key.
  397. encryptionKey: nil,
  398. },
  399. }
  400. resp, err := svc.maybeBuildWeChatOAuthRequiredResponse(context.Background(), CreateOrderRequest{
  401. Amount: 12.5,
  402. PaymentType: payment.TypeWxpay,
  403. IsWeChatBrowser: true,
  404. SrcURL: "https://merchant.example/payment?from=wechat",
  405. OrderType: payment.OrderTypeBalance,
  406. }, 12.5, 12.88, 0.03)
  407. if resp != nil {
  408. t.Fatalf("expected nil response, got %+v", resp)
  409. }
  410. if err == nil {
  411. t.Fatal("expected error, got nil")
  412. }
  413. appErr := infraerrors.FromError(err)
  414. if appErr.Reason != "PAYMENT_RESUME_NOT_CONFIGURED" {
  415. t.Fatalf("reason = %q, want %q", appErr.Reason, "PAYMENT_RESUME_NOT_CONFIGURED")
  416. }
  417. }
  418. func TestMaybeBuildWeChatOAuthRequiredResponseFallsBackToConfiguredLegacySigningKey(t *testing.T) {
  419. svc := &PaymentService{
  420. configService: &PaymentConfigService{
  421. settingRepo: &paymentConfigSettingRepoStub{values: map[string]string{
  422. SettingKeyWeChatConnectEnabled: "true",
  423. SettingKeyWeChatConnectAppID: "wx123456",
  424. SettingKeyWeChatConnectAppSecret: "wechat-secret",
  425. SettingKeyWeChatConnectMode: "mp",
  426. SettingKeyWeChatConnectScopes: "snsapi_base",
  427. SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
  428. SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
  429. }},
  430. // Legacy stable signing key remains available for no-config upgrade compatibility.
  431. encryptionKey: []byte("0123456789abcdef0123456789abcdef"),
  432. },
  433. }
  434. resp, err := svc.maybeBuildWeChatOAuthRequiredResponse(context.Background(), CreateOrderRequest{
  435. Amount: 12.5,
  436. PaymentType: payment.TypeWxpay,
  437. IsWeChatBrowser: true,
  438. SrcURL: "https://merchant.example/payment?from=wechat",
  439. OrderType: payment.OrderTypeBalance,
  440. }, 12.5, 12.88, 0.03)
  441. if err != nil {
  442. t.Fatalf("expected nil error, got %v", err)
  443. }
  444. if resp == nil {
  445. t.Fatal("expected oauth-required response, got nil")
  446. }
  447. if resp.ResultType != payment.CreatePaymentResultOAuthRequired {
  448. t.Fatalf("result type = %q, want %q", resp.ResultType, payment.CreatePaymentResultOAuthRequired)
  449. }
  450. if resp.OAuth == nil || strings.TrimSpace(resp.OAuth.AuthorizeURL) == "" {
  451. t.Fatalf("expected oauth redirect payload, got %+v", resp.OAuth)
  452. }
  453. }
  454. func TestMaybeBuildWeChatOAuthRequiredResponseForSelectionSkipsEasyPayProvider(t *testing.T) {
  455. svc := newWeChatPaymentOAuthTestService(map[string]string{
  456. SettingKeyWeChatConnectEnabled: "true",
  457. SettingKeyWeChatConnectAppID: "wx123456",
  458. SettingKeyWeChatConnectAppSecret: "wechat-secret",
  459. SettingKeyWeChatConnectMode: "mp",
  460. SettingKeyWeChatConnectScopes: "snsapi_base",
  461. SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
  462. SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
  463. })
  464. resp, err := svc.maybeBuildWeChatOAuthRequiredResponseForSelection(context.Background(), CreateOrderRequest{
  465. Amount: 12.5,
  466. PaymentType: payment.TypeWxpay,
  467. IsWeChatBrowser: true,
  468. OrderType: payment.OrderTypeBalance,
  469. }, 12.5, 12.88, 0.03, &payment.InstanceSelection{
  470. ProviderKey: payment.TypeEasyPay,
  471. })
  472. if err != nil {
  473. t.Fatalf("unexpected error: %v", err)
  474. }
  475. if resp != nil {
  476. t.Fatalf("expected nil response, got %+v", resp)
  477. }
  478. }
  479. func newWeChatPaymentOAuthTestService(values map[string]string) *PaymentService {
  480. return &PaymentService{
  481. configService: &PaymentConfigService{
  482. settingRepo: &paymentConfigSettingRepoStub{values: values},
  483. encryptionKey: []byte("0123456789abcdef0123456789abcdef"),
  484. },
  485. }
  486. }