├── .gitignore ├── .travis.yml ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── lib ├── wx_pay.rb └── wx_pay │ ├── result.rb │ ├── service.rb │ ├── sign.rb │ └── version.rb ├── test ├── test_helper.rb └── wx_pay │ ├── result_test.rb │ ├── service_test.rb │ └── sign_test.rb └── wx_pay.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/dummy/db/*.sqlite3 5 | test/dummy/db/*.sqlite3-journal 6 | test/dummy/log/*.log 7 | test/dummy/tmp/ 8 | test/dummy/.sass-cache 9 | *.gem 10 | *.rbc 11 | .bundle 12 | .config 13 | .yardoc 14 | Gemfile.lock 15 | tmp 16 | test/tmp 17 | test/version_tmp 18 | .idea 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | before_install: 3 | - gem install bundler 4 | rvm: 5 | - 2.3.8 6 | - 2.4.5 7 | - 2.5.3 8 | - 2.6.1 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Declare your gem's dependencies in tenpay.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | # Declare any dependencies that are still in development here instead of in 8 | # your gemspec. These might include edge Rails or gems from your path or 9 | # Git. Remember to move these dependencies to your gemspec before releasing 10 | # your gem to rubygems.org. 11 | 12 | # To use debugger 13 | # gem 'debugger' 14 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Jasl 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WxPay 2 | 3 | A simple Wechat pay ruby gem, without unnecessary magic or wrapper. 4 | copied from [alipay](https://github.com/chloerei/alipay) . 5 | 6 | Please read official document first: https://pay.weixin.qq.com/wiki/doc/api/index.html. 7 | 8 | [![Build Status](https://travis-ci.org/jasl/wx_pay.svg?branch=master)](https://travis-ci.org/jasl/wx_pay) 9 | 10 | ## Installation 11 | 12 | Add this line to your Gemfile: 13 | 14 | ```ruby 15 | gem 'wx_pay' 16 | ``` 17 | 18 | or development version 19 | 20 | ```ruby 21 | gem 'wx_pay', :github => 'jasl/wx_pay' 22 | ``` 23 | 24 | And then execute: 25 | 26 | ```sh 27 | $ bundle 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Config 33 | 34 | Create `config/initializers/wx_pay.rb` and put following configurations into it. 35 | 36 | ```ruby 37 | # required 38 | WxPay.appid = 'YOUR_APPID' 39 | WxPay.key = 'YOUR_KEY' 40 | WxPay.mch_id = 'YOUR_MCH_ID' # required type is String, otherwise there will be cases where JS_PAY can pay but the APP cannot pay 41 | WxPay.debug_mode = true # default is `true` 42 | 43 | # cert, see https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_3 44 | # using PCKS12 45 | WxPay.set_apiclient_by_pkcs12(File.read(pkcs12_filepath), cert_password) 46 | 47 | # if you want to use `generate_authorize_req` and `authenticate` 48 | WxPay.appsecret = 'YOUR_SECRET' 49 | 50 | # optional - configurations for RestClient timeout, etc. 51 | WxPay.extra_rest_client_options = {timeout: 2, open_timeout: 3} 52 | ``` 53 | 54 | ~~If you need to use sandbox mode.~~ 55 | (Please be aware, the WeChat has aborted the sandbox env already) 56 | ```ruby 57 | WxPay.appid = 'YOUR_APPID' 58 | WxPay.mch_id = 'YOUR_MCH_ID' # required type is String, otherwise there will be cases where JS_PAY can pay but the APP cannot pay 59 | WxPay.debug_mode = true # default is `true` 60 | WxPay.sandbox_mode = true # default is `false` 61 | result = WxPay::Service.get_sandbox_signkey 62 | WxPay.key = result['sandbox_signkey'] 63 | 64 | ``` 65 | 66 | Note: You should create your APIKEY (Link to [微信商户平台](https://pay.weixin.qq.com/index.php/home/login)) first if you haven't, and pay attention that **the length of the APIKEY should be 32**. 67 | 68 | ### APIs 69 | 70 | **Check official document for detailed request params and return fields** 71 | 72 | #### unifiedorder 73 | 74 | WxPay supports MWEB, JSAPI, NATIVE and APP. 75 | 76 | ```ruby 77 | # required fields 78 | params = { 79 | body: '测试商品', 80 | out_trade_no: 'test003', 81 | total_fee: 1, 82 | spbill_create_ip: '127.0.0.1', 83 | notify_url: 'http://making.dev/notify', 84 | trade_type: 'JSAPI', # could be "MWEB", ""JSAPI", "NATIVE" or "APP", 85 | openid: 'OPENID' # required when trade_type is `JSAPI` 86 | } 87 | ``` 88 | 89 | `WxPay::Service.invoke_unifiedorder params` will create an payment request and return a WxPay::Result instance(subclass of Hash) contains parsed result. 90 | 91 | If your trade type is "MWEB", the result would be like this. 92 | 93 | ```ruby 94 | r = WxPay::Service.invoke_unifiedorder params 95 | # => { 96 | # "return_code"=>"SUCCESS", 97 | # "return_msg"=>"OK", 98 | # "appid"=>"YOUR APPID", 99 | # "mch_id"=>"YOUR MCH_ID", 100 | # "nonce_str"=>"8RN7YfTZ3OUgWX5e", 101 | # "sign"=>"623AE90C9679729DDD7407DC7A1151B2", 102 | # "result_code"=>"SUCCESS", 103 | # "prepay_id"=>"wx2014111104255143b7605afb0314593866", 104 | # "mweb_url"=>"https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx2016121516420242444321ca0631331346&package=1405458241", 105 | # "trade_type"=>"MWEB" 106 | # } 107 | ``` 108 | 109 | If your trade type is "JSAPI", the result would be like this. 110 | 111 | ```ruby 112 | r = WxPay::Service.invoke_unifiedorder params 113 | # => { 114 | # "return_code"=>"SUCCESS", 115 | # "return_msg"=>"OK", 116 | # "appid"=>"YOUR APPID", 117 | # "mch_id"=>"YOUR MCH_ID", 118 | # "nonce_str"=>"8RN7YfTZ3OUgWX5e", 119 | # "sign"=>"623AE90C9679729DDD7407DC7A1151B2", 120 | # "result_code"=>"SUCCESS", 121 | # "prepay_id"=>"wx2014111104255143b7605afb0314593866", 122 | # "trade_type"=>"JSAPI" 123 | # } 124 | ``` 125 | 126 | > "JSAPI" requires openid in params, 127 | in most cases I suggest you using [omniauth](https://github.com/omniauth/omniauth) with [omniauth-wechat-oauth2](https://github.com/skinnyworm/omniauth-wechat-oauth2) to resolve this, 128 | but `wx_pay` provides `generate_authorize_url` and `authenticate` to help you get Wechat authorization in simple case. 129 | 130 | If your trade type is "NATIVE", the result would be like this. 131 | 132 | ```ruby 133 | r = WxPay::Service.invoke_unifiedorder params 134 | # => { 135 | # "return_code"=>"SUCCESS", 136 | # "return_msg"=>"OK", 137 | # "appid"=>"YOUR APPID", 138 | # "mch_id"=>"YOUR MCH_ID", 139 | # "nonce_str"=>"8RN7YfTZ3OUgWX5e", 140 | # "sign"=>"623AE90C9679729DDD7407DC7A1151B2", 141 | # "result_code"=>"SUCCESS", 142 | # "prepay_id"=>"wx2014111104255143b7605afb0314593866", 143 | # "code_url"=>"weixin://" 144 | # "trade_type"=>"NATIVE" 145 | # } 146 | ``` 147 | 148 | Return true if both `return_code` and `result_code` equal `SUCCESS` 149 | 150 | ```ruby 151 | r.success? # => true 152 | ``` 153 | 154 | #### pay request for app 155 | 156 | ```ruby 157 | # required fields 158 | params = { 159 | prepayid: '1101000000140415649af9fc314aa427', # fetch by call invoke_unifiedorder with `trade_type` is `APP` 160 | noncestr: '1101000000140429eb40476f8896f4c9' # must same as given to invoke_unifiedorder 161 | } 162 | 163 | # call generate_app_pay_req 164 | r = WxPay::Service.generate_app_pay_req params 165 | # => { 166 | # appid: 'wxd930ea5d5a258f4f', 167 | # partnerid: '1900000109', 168 | # prepayid: '1101000000140415649af9fc314aa427', 169 | # package: 'Sign=WXPay', 170 | # noncestr: '1101000000140429eb40476f8896f4c9', 171 | # timestamp: '1398746574', 172 | # sign: '7FFECB600D7157C5AA49810D2D8F28BC2811827B' 173 | # } 174 | ``` 175 | 176 | #### pay request for JSAPI 177 | 178 | ``` ruby 179 | # required fields 180 | params = { 181 | prepayid: '1101000000140415649af9fc314aa427', # fetch by call invoke_unifiedorder with `trade_type` is `JSAPI` 182 | noncestr: SecureRandom.hex(16), 183 | } 184 | 185 | # call generate_js_pay_req 186 | r = WxPay::Service.generate_js_pay_req params 187 | # { 188 | # "appId": "wx020c5c792c8537de", 189 | # "package": "prepay_id=wx20160902211806a11ccee7a20956539837", 190 | # "nonceStr": "2vS5AJUD7uyaa5h9", 191 | # "timeStamp": "1472822286", 192 | # "signType": "MD5", 193 | # "paySign": "A52433CB75CA8D58B67B2BB45A79AA01" 194 | # } 195 | ``` 196 | 197 | #### Notify Process 198 | 199 | A simple example of processing notify for Rails Action Controller. 200 | 201 | ```ruby 202 | # config/routes.rb 203 | post "notify" => "orders#notify" 204 | 205 | # app/controllers/orders_controller.rb 206 | 207 | def notify 208 | result = Hash.from_xml(request.body.read)["xml"] 209 | 210 | if WxPay::Sign.verify?(result) 211 | 212 | # find your order and process the post-paid logic. 213 | 214 | render :xml => {return_code: "SUCCESS"}.to_xml(root: 'xml', dasherize: false) 215 | else 216 | render :xml => {return_code: "FAIL", return_msg: "签名失败"}.to_xml(root: 'xml', dasherize: false) 217 | end 218 | end 219 | ``` 220 | 221 | A simple example of processing notify for Grape v1.2.2 . 222 | 223 | ```ruby 224 | # Gemfile 225 | gem 'multi_xml' 226 | 227 | # config/routes.rb 228 | mount WechatPay::Api => '/' 229 | 230 | # app/api/wechat_pay/api.rb 231 | module WechatPay 232 | class Api < Grape::API 233 | content_type :xml, 'text/xml' 234 | format :xml 235 | formatter :xml, lambda { |object, env| object.to_xml(root: 'xml', dasherize: false) } 236 | 237 | post "notify" do 238 | result = params["xml"] 239 | if WxPay::Sign.verify?(result) 240 | # find your order and process the post-paid logic. 241 | 242 | status 200 243 | {return_code: "SUCCESS"} 244 | else 245 | status 200 246 | {return_code: "FAIL", return_msg: "签名失败"} 247 | end 248 | end 249 | end 250 | end 251 | ``` 252 | 253 | 254 | ### Integrate with QRCode(二维码) 255 | 256 | Wechat payment integrating with QRCode is a recommended process flow which will bring users comfortable experience. It is recommended to generate QRCode using `rqrcode` and `rqrcode_png`. 257 | 258 | **Example Code** (please make sure that `public/uploads/qrcode` was created): 259 | 260 | ```ruby 261 | r = WxPay::Service.invoke_unifiedorder params 262 | qrcode_png = RQRCode::QRCode.new( r["code_url"], :size => 5, :level => :h ).to_img.resize(200, 200).save("public/uploads/qrcode/#{@order.id.to_s}_#{Time.now.to_i.to_s}.png") 263 | @qrcode_url = "/uploads/qrcode/#{@order.id.to_s}_#{Time.now.to_i.to_s}.png" 264 | ``` 265 | 266 | ### More 267 | 268 | No documents yet, check `lib/wx_pay/service.rb` 269 | 270 | ## Multi-account support 271 | 272 | All functions have third argument `options`, 273 | you can pass `appid`, `mch_id`, `key`, `apiclient_cert`, `apiclient_key` as a hash. 274 | 275 | For example 276 | ```ruby 277 | another_account = {appid: 'APPID', mch_id: 'MCH_ID', key: 'KEY'}.freeze 278 | WxPay::Service.generate_app_pay_req params, another_account.dup 279 | ``` 280 | 281 | ## Contributing 282 | 283 | Bug report or pull request are welcome. 284 | 285 | ### Make a pull request 286 | 287 | 1. Fork it 288 | 2. Create your feature branch (`git checkout -b my-new-feature`) 289 | 3. Commit your changes (`git commit -am 'Add some feature'`) 290 | 4. Push to the branch (`git push origin my-new-feature`) 291 | 5. Create new Pull Request 292 | 293 | Please write unit test with your code if necessary. 294 | 295 | ## License 296 | 297 | This project rocks and uses MIT-LICENSE. 298 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'Tenpay' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.rdoc') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | Bundler::GemHelper.install_tasks 18 | 19 | require 'rake/testtask' 20 | 21 | Rake::TestTask.new(:test) do |t| 22 | t.libs << 'lib' 23 | t.libs << 'test' 24 | t.pattern = 'test/**/*_test.rb' 25 | t.verbose = false 26 | end 27 | 28 | task default: :test 29 | -------------------------------------------------------------------------------- /lib/wx_pay.rb: -------------------------------------------------------------------------------- 1 | require 'wx_pay/result' 2 | require 'wx_pay/sign' 3 | require 'wx_pay/service' 4 | require 'wx_pay/version' 5 | require 'openssl' 6 | 7 | module WxPay 8 | @extra_rest_client_options = {} 9 | @debug_mode = true 10 | 11 | class << self 12 | attr_accessor :appid, :mch_id, :key, :appsecret, :extra_rest_client_options, :debug_mode 13 | attr_reader :apiclient_cert, :apiclient_key 14 | 15 | def set_apiclient_by_pkcs12(str, pass) 16 | pkcs12 = OpenSSL::PKCS12.new(str, pass) 17 | @apiclient_cert = pkcs12.certificate 18 | @apiclient_key = pkcs12.key 19 | 20 | pkcs12 21 | end 22 | 23 | def apiclient_cert=(cert) 24 | @apiclient_cert = OpenSSL::X509::Certificate.new(cert) 25 | end 26 | 27 | def apiclient_key=(key) 28 | @apiclient_key = OpenSSL::PKey::RSA.new(key) 29 | end 30 | 31 | def debug_mode? 32 | @debug_mode 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/wx_pay/result.rb: -------------------------------------------------------------------------------- 1 | module WxPay 2 | class Result < ::Hash 3 | SUCCESS_FLAG = 'SUCCESS'.freeze 4 | 5 | def initialize(result) 6 | super nil # Or it will call `super result` 7 | 8 | self[:raw] = result 9 | 10 | if result['xml'].class == Hash 11 | result['xml'].each_pair do |k, v| 12 | self[k] = v 13 | end 14 | end 15 | end 16 | 17 | def success? 18 | self['return_code'] == SUCCESS_FLAG && self['result_code'] == SUCCESS_FLAG 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/wx_pay/service.rb: -------------------------------------------------------------------------------- 1 | require 'rest_client' 2 | require 'json' 3 | require 'cgi' 4 | require 'securerandom' 5 | require 'active_support/isolated_execution_state' 6 | require 'active_support/xml_mini' 7 | require 'active_support/core_ext/hash/conversions' 8 | 9 | module WxPay 10 | module Service 11 | GATEWAY_URL = 'https://api.mch.weixin.qq.com'.freeze 12 | FRAUD_GATEWAY_URL = 'https://fraud.mch.weixin.qq.com'.freeze 13 | 14 | def self.generate_authorize_url(redirect_uri, state = nil) 15 | state ||= SecureRandom.hex 16 16 | "https://open.weixin.qq.com/connect/oauth2/authorize?appid=#{WxPay.appid}&redirect_uri=#{CGI::escape redirect_uri}&response_type=code&scope=snsapi_base&state=#{state}" 17 | end 18 | 19 | def self.authenticate(authorization_code, options = {}) 20 | options = WxPay.extra_rest_client_options.merge(options) 21 | payload = { 22 | appid: options.delete(:appid) || WxPay.appid, 23 | secret: options.delete(:appsecret) || WxPay.appsecret, 24 | code: authorization_code, 25 | grant_type: 'authorization_code' 26 | } 27 | url = "https://api.weixin.qq.com/sns/oauth2/access_token" 28 | 29 | ::JSON.parse(RestClient::Request.execute( 30 | { 31 | method: :get, 32 | headers: {params: payload}, 33 | url: url 34 | }.merge(options) 35 | ), quirks_mode: true) 36 | end 37 | 38 | def self.authenticate_from_weapp(js_code, options = {}) 39 | options = WxPay.extra_rest_client_options.merge(options) 40 | payload = { 41 | appid: options.delete(:appid) || WxPay.appid, 42 | secret: options.delete(:appsecret) || WxPay.appsecret, 43 | js_code: js_code, 44 | grant_type: 'authorization_code' 45 | } 46 | url = "https://api.weixin.qq.com/sns/jscode2session" 47 | 48 | ::JSON.parse(RestClient::Request.execute( 49 | { 50 | method: :get, 51 | headers: {params: payload}, 52 | url: url 53 | }.merge(options) 54 | ), quirks_mode: true) 55 | end 56 | 57 | INVOKE_UNIFIEDORDER_REQUIRED_FIELDS = [:body, :out_trade_no, :total_fee, :spbill_create_ip, :notify_url, :trade_type] 58 | def self.invoke_unifiedorder(params, options = {}) 59 | params = { 60 | appid: options.delete(:appid) || WxPay.appid, 61 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 62 | key: options.delete(:key) || WxPay.key, 63 | nonce_str: SecureRandom.uuid.tr('-', '') 64 | }.merge(params) 65 | 66 | check_required_options(params, INVOKE_UNIFIEDORDER_REQUIRED_FIELDS) 67 | 68 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/pay/unifiedorder", make_payload(params), options))) 69 | 70 | yield r if block_given? 71 | 72 | r 73 | end 74 | 75 | INVOKE_CLOSEORDER_REQUIRED_FIELDS = [:out_trade_no] 76 | def self.invoke_closeorder(params, options = {}) 77 | params = { 78 | appid: options.delete(:appid) || WxPay.appid, 79 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 80 | key: options.delete(:key) || WxPay.key, 81 | nonce_str: SecureRandom.uuid.tr('-', '') 82 | }.merge(params) 83 | 84 | check_required_options(params, INVOKE_CLOSEORDER_REQUIRED_FIELDS) 85 | 86 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/pay/closeorder", make_payload(params), options))) 87 | 88 | yield r if block_given? 89 | 90 | r 91 | end 92 | 93 | GENERATE_APP_PAY_REQ_REQUIRED_FIELDS = [:prepayid, :noncestr] 94 | def self.generate_app_pay_req(params, options = {}) 95 | params = { 96 | appid: options.delete(:appid) || WxPay.appid, 97 | partnerid: options.delete(:mch_id) || WxPay.mch_id, 98 | key: options.delete(:key) || WxPay.key, 99 | package: 'Sign=WXPay', 100 | timestamp: Time.now.to_i.to_s 101 | }.merge(params) 102 | 103 | check_required_options(params, GENERATE_APP_PAY_REQ_REQUIRED_FIELDS) 104 | 105 | params[:sign] = WxPay::Sign.generate(params) 106 | 107 | params 108 | end 109 | 110 | GENERATE_JS_PAY_REQ_REQUIRED_FIELDS = [:prepayid, :noncestr] 111 | def self.generate_js_pay_req(params, options = {}) 112 | check_required_options(params, GENERATE_JS_PAY_REQ_REQUIRED_FIELDS) 113 | 114 | params = { 115 | appId: options.delete(:appid) || WxPay.appid, 116 | package: "prepay_id=#{params.delete(:prepayid)}", 117 | key: options.delete(:key) || WxPay.key, 118 | nonceStr: params.delete(:noncestr), 119 | timeStamp: Time.now.to_i.to_s, 120 | signType: 'MD5' 121 | }.merge(params) 122 | 123 | params[:paySign] = WxPay::Sign.generate(params) 124 | params 125 | end 126 | 127 | INVOKE_REFUND_REQUIRED_FIELDS = [:out_refund_no, :total_fee, :refund_fee, :op_user_id] 128 | # out_trade_no 和 transaction_id 是二选一(必填) 129 | def self.invoke_refund(params, options = {}) 130 | params = { 131 | appid: options.delete(:appid) || WxPay.appid, 132 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 133 | key: options.delete(:key) || WxPay.key, 134 | nonce_str: SecureRandom.uuid.tr('-', ''), 135 | }.merge(params) 136 | 137 | params[:op_user_id] ||= params[:mch_id] 138 | 139 | check_required_options(params, INVOKE_REFUND_REQUIRED_FIELDS) 140 | warn("WxPay Warn: missing required option: out_trade_no or transaction_id must have one") if ([:out_trade_no, :transaction_id] & params.keys) == [] 141 | 142 | options = { 143 | ssl_client_cert: options.delete(:apiclient_cert) || WxPay.apiclient_cert, 144 | ssl_client_key: options.delete(:apiclient_key) || WxPay.apiclient_key, 145 | verify_ssl: OpenSSL::SSL::VERIFY_NONE 146 | }.merge(options) 147 | 148 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/secapi/pay/refund", make_payload(params), options))) 149 | 150 | yield r if block_given? 151 | 152 | r 153 | end 154 | 155 | REFUND_QUERY_REQUIRED_FIELDS = [:out_trade_no] 156 | def self.refund_query(params, options = {}) 157 | params = { 158 | appid: options.delete(:appid) || WxPay.appid, 159 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 160 | key: options.delete(:key) || WxPay.key, 161 | nonce_str: SecureRandom.uuid.tr('-', '') 162 | }.merge(params) 163 | 164 | check_required_options(params, ORDER_QUERY_REQUIRED_FIELDS) 165 | 166 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/pay/refundquery", make_payload(params), options))) 167 | 168 | yield r if block_given? 169 | 170 | r 171 | end 172 | 173 | INVOKE_TRANSFER_REQUIRED_FIELDS = [:partner_trade_no, :openid, :check_name, :amount, :desc, :spbill_create_ip] 174 | def self.invoke_transfer(params, options = {}) 175 | params = { 176 | mch_appid: options.delete(:appid) || WxPay.appid, 177 | mchid: options.delete(:mch_id) || WxPay.mch_id, 178 | nonce_str: SecureRandom.uuid.tr('-', ''), 179 | key: options.delete(:key) || WxPay.key 180 | }.merge(params) 181 | 182 | check_required_options(params, INVOKE_TRANSFER_REQUIRED_FIELDS) 183 | 184 | options = { 185 | ssl_client_cert: options.delete(:apiclient_cert) || WxPay.apiclient_cert, 186 | ssl_client_key: options.delete(:apiclient_key) || WxPay.apiclient_key, 187 | verify_ssl: OpenSSL::SSL::VERIFY_NONE 188 | }.merge(options) 189 | 190 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/mmpaymkttransfers/promotion/transfers", make_payload(params), options))) 191 | 192 | yield r if block_given? 193 | 194 | r 195 | end 196 | 197 | GETTRANSFERINFO_FIELDS = [:partner_trade_no] 198 | def self.gettransferinfo(params, options = {}) 199 | params = { 200 | appid: options.delete(:appid) || WxPay.appid, 201 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 202 | nonce_str: SecureRandom.uuid.tr('-', ''), 203 | key: options.delete(:key) || WxPay.key 204 | }.merge(params) 205 | 206 | check_required_options(params, GETTRANSFERINFO_FIELDS) 207 | 208 | options = { 209 | ssl_client_cert: options.delete(:apiclient_cert) || WxPay.apiclient_cert, 210 | ssl_client_key: options.delete(:apiclient_key) || WxPay.apiclient_key, 211 | verify_ssl: OpenSSL::SSL::VERIFY_NONE 212 | }.merge(options) 213 | 214 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/mmpaymkttransfers/gettransferinfo", make_payload(params), options))) 215 | 216 | yield r if block_given? 217 | 218 | r 219 | end 220 | 221 | # 获取加密银行卡号和收款方用户名的RSA公钥 222 | def self.risk_get_public_key(options = {}) 223 | params = { 224 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 225 | nonce_str: SecureRandom.uuid.tr('-', ''), 226 | key: options.delete(:key) || WxPay.key, 227 | sign_type: 'MD5' 228 | } 229 | 230 | options = { 231 | ssl_client_cert: options.delete(:apiclient_cert) || WxPay.apiclient_cert, 232 | ssl_client_key: options.delete(:apiclient_key) || WxPay.apiclient_key, 233 | verify_ssl: OpenSSL::SSL::VERIFY_NONE, 234 | gateway_url: FRAUD_GATEWAY_URL 235 | }.merge(options) 236 | 237 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/risk/getpublickey", make_payload(params), options))) 238 | 239 | yield r if block_given? 240 | 241 | r 242 | end 243 | 244 | PAY_BANK_FIELDS = [:enc_bank_no, :enc_true_name, :bank_code, :amount, :desc] 245 | def self.pay_bank(params, options = {}) 246 | params = { 247 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 248 | nonce_str: SecureRandom.uuid.tr('-', ''), 249 | key: options.delete(:key) || WxPay.key, 250 | }.merge(params) 251 | 252 | check_required_options(params, PAY_BANK_FIELDS) 253 | 254 | options = { 255 | ssl_client_cert: options.delete(:apiclient_cert) || WxPay.apiclient_cert, 256 | ssl_client_key: options.delete(:apiclient_key) || WxPay.apiclient_key, 257 | verify_ssl: OpenSSL::SSL::VERIFY_NONE 258 | }.merge(options) 259 | 260 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/mmpaysptrans/pay_bank", make_payload(params), options))) 261 | 262 | yield r if block_given? 263 | 264 | r 265 | end 266 | 267 | QUERY_BANK_FIELDS = [:partner_trade_no] 268 | def self.query_bank(params, options = {}) 269 | params = { 270 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 271 | nonce_str: SecureRandom.uuid.tr('-', ''), 272 | key: options.delete(:key) || WxPay.key, 273 | }.merge(params) 274 | 275 | check_required_options(params, QUERY_BANK_FIELDS) 276 | 277 | options = { 278 | ssl_client_cert: options.delete(:apiclient_cert) || WxPay.apiclient_cert, 279 | ssl_client_key: options.delete(:apiclient_key) || WxPay.apiclient_key, 280 | verify_ssl: OpenSSL::SSL::VERIFY_NONE 281 | }.merge(options) 282 | 283 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/mmpaysptrans/query_bank", make_payload(params), options))) 284 | 285 | yield r if block_given? 286 | 287 | r 288 | end 289 | 290 | INVOKE_REVERSE_REQUIRED_FIELDS = [:out_trade_no] 291 | def self.invoke_reverse(params, options = {}) 292 | params = { 293 | appid: options.delete(:appid) || WxPay.appid, 294 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 295 | key: options.delete(:key) || WxPay.key, 296 | nonce_str: SecureRandom.uuid.tr('-', '') 297 | }.merge(params) 298 | 299 | check_required_options(params, INVOKE_REVERSE_REQUIRED_FIELDS) 300 | 301 | options = { 302 | ssl_client_cert: options.delete(:apiclient_cert) || WxPay.apiclient_cert, 303 | ssl_client_key: options.delete(:apiclient_key) || WxPay.apiclient_key, 304 | verify_ssl: OpenSSL::SSL::VERIFY_NONE 305 | }.merge(options) 306 | 307 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/secapi/pay/reverse", make_payload(params), options))) 308 | 309 | yield r if block_given? 310 | 311 | r 312 | end 313 | 314 | INVOKE_MICROPAY_REQUIRED_FIELDS = [:body, :out_trade_no, :total_fee, :spbill_create_ip, :auth_code] 315 | def self.invoke_micropay(params, options = {}) 316 | params = { 317 | appid: options.delete(:appid) || WxPay.appid, 318 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 319 | key: options.delete(:key) || WxPay.key, 320 | nonce_str: SecureRandom.uuid.tr('-', '') 321 | }.merge(params) 322 | 323 | check_required_options(params, INVOKE_MICROPAY_REQUIRED_FIELDS) 324 | 325 | options = { 326 | ssl_client_cert: options.delete(:apiclient_cert) || WxPay.apiclient_cert, 327 | ssl_client_key: options.delete(:apiclient_key) || WxPay.apiclient_key, 328 | verify_ssl: OpenSSL::SSL::VERIFY_NONE 329 | }.merge(options) 330 | 331 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/pay/micropay", make_payload(params), options))) 332 | 333 | yield r if block_given? 334 | 335 | r 336 | end 337 | 338 | ORDER_QUERY_REQUIRED_FIELDS = [:out_trade_no] 339 | def self.order_query(params, options = {}) 340 | params = { 341 | appid: options.delete(:appid) || WxPay.appid, 342 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 343 | key: options.delete(:key) || WxPay.key, 344 | nonce_str: SecureRandom.uuid.tr('-', '') 345 | }.merge(params) 346 | 347 | 348 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/pay/orderquery", make_payload(params), options))) 349 | check_required_options(params, ORDER_QUERY_REQUIRED_FIELDS) 350 | 351 | yield r if block_given? 352 | 353 | r 354 | end 355 | 356 | DOWNLOAD_BILL_REQUIRED_FIELDS = [:bill_date, :bill_type] 357 | def self.download_bill(params, options = {}) 358 | params = { 359 | appid: options.delete(:appid) || WxPay.appid, 360 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 361 | key: options.delete(:key) || WxPay.key, 362 | nonce_str: SecureRandom.uuid.tr('-', ''), 363 | }.merge(params) 364 | 365 | check_required_options(params, DOWNLOAD_BILL_REQUIRED_FIELDS) 366 | 367 | r = invoke_remote("/pay/downloadbill", make_payload(params), options) 368 | 369 | yield r if block_given? 370 | 371 | r 372 | end 373 | 374 | DOWNLOAD_FUND_FLOW_REQUIRED_FIELDS = [:bill_date, :account_type] 375 | def self.download_fund_flow(params, options = {}) 376 | params = { 377 | appid: options.delete(:appid) || WxPay.appid, 378 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 379 | nonce_str: SecureRandom.uuid.tr('-', ''), 380 | key: options.delete(:key) || WxPay.key 381 | }.merge(params) 382 | 383 | check_required_options(params, DOWNLOAD_FUND_FLOW_REQUIRED_FIELDS) 384 | 385 | options = { 386 | ssl_client_cert: options.delete(:apiclient_cert) || WxPay.apiclient_cert, 387 | ssl_client_key: options.delete(:apiclient_key) || WxPay.apiclient_key, 388 | verify_ssl: OpenSSL::SSL::VERIFY_NONE 389 | }.merge(options) 390 | 391 | r = invoke_remote("/pay/downloadfundflow", make_payload(params, WxPay::Sign::SIGN_TYPE_HMAC_SHA256), options) 392 | 393 | yield r if block_given? 394 | 395 | r 396 | end 397 | 398 | def self.sendgroupredpack(params, options={}) 399 | params = { 400 | wxappid: options.delete(:appid) || WxPay.appid, 401 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 402 | key: options.delete(:key) || WxPay.key, 403 | nonce_str: SecureRandom.uuid.tr('-', '') 404 | }.merge(params) 405 | 406 | #check_required_options(params, INVOKE_MICROPAY_REQUIRED_FIELDS) 407 | 408 | options = { 409 | ssl_client_cert: options.delete(:apiclient_cert) || WxPay.apiclient_cert, 410 | ssl_client_key: options.delete(:apiclient_key) || WxPay.apiclient_key, 411 | verify_ssl: OpenSSL::SSL::VERIFY_NONE 412 | }.merge(options) 413 | 414 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/mmpaymkttransfers/sendgroupredpack", make_payload(params), options))) 415 | 416 | yield r if block_given? 417 | 418 | r 419 | end 420 | 421 | def self.sendredpack(params, options={}) 422 | params = { 423 | wxappid: options.delete(:appid) || WxPay.appid, 424 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 425 | key: options.delete(:key) || WxPay.key, 426 | nonce_str: SecureRandom.uuid.tr('-', '') 427 | }.merge(params) 428 | 429 | #check_required_options(params, INVOKE_MICROPAY_REQUIRED_FIELDS) 430 | 431 | options = { 432 | ssl_client_cert: options.delete(:apiclient_cert) || WxPay.apiclient_cert, 433 | ssl_client_key: options.delete(:apiclient_key) || WxPay.apiclient_key, 434 | verify_ssl: OpenSSL::SSL::VERIFY_NONE 435 | }.merge(options) 436 | 437 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/mmpaymkttransfers/sendredpack", make_payload(params), options))) 438 | 439 | yield r if block_given? 440 | 441 | r 442 | end 443 | 444 | # 用于商户对已发放的红包进行查询红包的具体信息,可支持普通红包和裂变包。 445 | GETHBINFO_FIELDS = [:mch_billno, :bill_type] 446 | def self.gethbinfo(params, options = {}) 447 | params = { 448 | appid: options.delete(:appid) || WxPay.appid, 449 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 450 | nonce_str: SecureRandom.uuid.tr('-', ''), 451 | key: options.delete(:key) || WxPay.key 452 | }.merge(params) 453 | 454 | check_required_options(params, GETHBINFO_FIELDS) 455 | 456 | options = { 457 | ssl_client_cert: options.delete(:apiclient_cert) || WxPay.apiclient_cert, 458 | ssl_client_key: options.delete(:apiclient_key) || WxPay.apiclient_key, 459 | verify_ssl: OpenSSL::SSL::VERIFY_NONE 460 | }.merge(options) 461 | 462 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/mmpaymkttransfers/gethbinfo", make_payload(params), options))) 463 | 464 | yield r if block_given? 465 | 466 | r 467 | end 468 | 469 | PROFITSHARINGADDRECEIVER = [:nonce_str, :receiver] 470 | 471 | # 添加分账接收方 472 | def self.profitsharingaddreceiver(params, options = {}) 473 | params = { 474 | appid: options.delete(:appid) || WxPay.appid, 475 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 476 | nonce_str: SecureRandom.uuid.tr('-', ''), 477 | key: options.delete(:key) || WxPay.key 478 | }.merge(params) 479 | 480 | check_required_options(params, PROFITSHARINGADDRECEIVER) 481 | 482 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/pay/profitsharingaddreceiver", make_payload(params, WxPay::Sign::SIGN_TYPE_HMAC_SHA256), options))) 483 | 484 | yield r if block_given? 485 | 486 | r 487 | end 488 | 489 | PROFITSHARINGREMOVERECEIVER = [:nonce_str, :receiver] 490 | # 删除分账接收方 491 | def self.profitsharingremovereceiver(params, options = {}) 492 | params = { 493 | appid: options.delete(:appid) || WxPay.appid, 494 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 495 | nonce_str: SecureRandom.uuid.tr('-', ''), 496 | key: options.delete(:key) || WxPay.key 497 | }.merge(params) 498 | 499 | check_required_options(params, PROFITSHARINGADDRECEIVER) 500 | 501 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/pay/profitsharingremovereceiver", make_payload(params, WxPay::Sign::SIGN_TYPE_HMAC_SHA256), options))) 502 | 503 | yield r if block_given? 504 | 505 | r 506 | end 507 | 508 | # 单次分账 509 | 510 | PROFITSHARING = [:nonce_str, :receivers, :transaction_id, :out_order_no] 511 | 512 | def self.profitsharing(params, options = {}) 513 | params = { 514 | appid: options.delete(:appid) || WxPay.appid, 515 | mch_id: options.delete(:mch_id) || WxPay.mch_id, 516 | nonce_str: SecureRandom.uuid.tr('-', ''), 517 | key: options.delete(:key) || WxPay.key 518 | }.merge(params) 519 | 520 | check_required_options(params, PROFITSHARING) 521 | 522 | options = { 523 | ssl_client_cert: options.delete(:apiclient_cert) || WxPay.apiclient_cert, 524 | ssl_client_key: options.delete(:apiclient_key) || WxPay.apiclient_key, 525 | verify_ssl: OpenSSL::SSL::VERIFY_NONE 526 | }.merge(options) 527 | 528 | r = WxPay::Result.new(Hash.from_xml(invoke_remote("/secapi/pay/profitsharing", make_payload(params, WxPay::Sign::SIGN_TYPE_HMAC_SHA256), options))) 529 | 530 | yield r if block_given? 531 | 532 | r 533 | end 534 | 535 | class << self 536 | private 537 | 538 | def get_gateway_url 539 | GATEWAY_URL 540 | end 541 | 542 | def check_required_options(options, names) 543 | return unless WxPay.debug_mode? 544 | 545 | names.each do |name| 546 | warn("WxPay Warn: missing required option: #{name}") unless options.has_key?(name) 547 | end 548 | end 549 | 550 | def xmlify_payload(params, sign_type = WxPay::Sign::SIGN_TYPE_MD5) 551 | sign = WxPay::Sign.generate(params, sign_type) 552 | "#{params.except(:key).sort.map { |k, v| "<#{k}>#{v}" }.join}#{sign}" 553 | end 554 | 555 | def make_payload(params, sign_type = WxPay::Sign::SIGN_TYPE_MD5) 556 | xmlify_payload(params, sign_type) 557 | end 558 | 559 | def invoke_remote(url, payload, options = {}) 560 | options = WxPay.extra_rest_client_options.merge(options) 561 | gateway_url = options.delete(:gateway_url) || get_gateway_url 562 | url = "#{gateway_url}#{url}" 563 | 564 | RestClient::Request.execute( 565 | { 566 | method: :post, 567 | url: url, 568 | payload: payload, 569 | headers: { content_type: 'application/xml' } 570 | }.merge(options) 571 | ) 572 | end 573 | end 574 | end 575 | end 576 | -------------------------------------------------------------------------------- /lib/wx_pay/sign.rb: -------------------------------------------------------------------------------- 1 | require 'digest/md5' 2 | 3 | module WxPay 4 | module Sign 5 | 6 | SIGN_TYPE_MD5 = 'MD5' 7 | SIGN_TYPE_HMAC_SHA256 = 'HMAC-SHA256' 8 | 9 | def self.generate(params, sign_type = SIGN_TYPE_MD5) 10 | key = params.delete(:key) 11 | 12 | new_key = params["key"] #after 13 | key = params.delete("key") if params["key"] #after 14 | 15 | query = params.sort.map do |k, v| 16 | "#{k}=#{v}" if v.to_s != '' 17 | end.compact.join('&') 18 | 19 | string_sign_temp = "#{query}&key=#{key || new_key || WxPay.key}" #after 20 | 21 | if sign_type == SIGN_TYPE_MD5 22 | Digest::MD5.hexdigest(string_sign_temp).upcase 23 | elsif sign_type == SIGN_TYPE_HMAC_SHA256 24 | OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), key, string_sign_temp).upcase 25 | else 26 | warn("WxPay Warn: unknown sign_type : #{sign_type}") 27 | end 28 | end 29 | 30 | def self.verify?(params, options = {}) 31 | params = params.dup 32 | params["appid"] = options[:appid] if options[:appid] 33 | params["mch_id"] = options[:mch_id] if options[:mch_id] 34 | params["key"] = options[:key] if options[:key] 35 | 36 | sign = params.delete('sign') || params.delete(:sign) 37 | 38 | generate(params, options[:sign_type] || SIGN_TYPE_MD5) == sign 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/wx_pay/version.rb: -------------------------------------------------------------------------------- 1 | module WxPay 2 | VERSION = '0.21.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/hash/conversions' 2 | require 'minitest/autorun' 3 | require 'wx_pay' 4 | require 'webmock/minitest' 5 | 6 | WxPay.appid = 'wxd930ea5d5a258f4f' 7 | WxPay.key = '8934e7d15453e97507ef794cf7b0519d' 8 | WxPay.mch_id = '1900000109' 9 | WxPay.debug_mode = true 10 | -------------------------------------------------------------------------------- /test/wx_pay/result_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'active_support/core_ext/hash/conversions' 3 | 4 | class WxPay::ResultTest < MiniTest::Test 5 | def test_success_method_with_true 6 | r = WxPay::Result.new( 7 | Hash.from_xml( 8 | <<-XML 9 | 10 | SUCCESS 11 | SUCCESS 12 | 13 | XML 14 | )) 15 | 16 | assert_equal r.success?, true 17 | end 18 | 19 | def test_success_method_with_false 20 | r = WxPay::Result.new( 21 | Hash.from_xml( 22 | <<-XML 23 | 24 | 25 | XML 26 | )) 27 | 28 | assert_equal r.success?, false 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/wx_pay/service_test.rb: -------------------------------------------------------------------------------- 1 | 2 | class ServiceTest < MiniTest::Test 3 | 4 | def setup 5 | @params = { 6 | transaction_id: '1217752501201407033233368018', 7 | op_user_id: '10000100', 8 | out_refund_no: '1415701182', 9 | out_trade_no: '1415757673', 10 | refund_fee: 1, 11 | total_fee: 1 12 | } 13 | end 14 | 15 | def test_invoke_refund 16 | response_body = <<-EOF 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 1 31 | 0 32 | 33 | EOF 34 | 35 | stub_request(:post, 'api.mch.weixin.qq.com').to_return(body: response_body) 36 | end 37 | 38 | def test_accept_multiple_app_id_when_invoke 39 | params = { 40 | body: '测试商品', 41 | out_trade_no: 'test003', 42 | total_fee: 1, 43 | spbill_create_ip: '127.0.0.1', 44 | notify_url: 'http://making.dev/notify', 45 | trade_type: 'JSAPI', 46 | openid: 'OPENID', 47 | app_id: 'app_id', 48 | mch_id: 'mch_id', 49 | key: 'key' 50 | } 51 | xml_str = 'app_id测试商品mch_idhttp://making.dev/notifyOPENIDtest003127.0.0.11JSAPI172A2D487A37D13FDE32B874BA823DD6' 52 | assert_equal xml_str, WxPay::Service.send(:make_payload, params) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/wx_pay/sign_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class WxPay::SignTest < MiniTest::Test 4 | def setup 5 | @params = { 6 | appid: 'wxd930ea5d5a258f4f', 7 | auth_code: 123456, 8 | body: 'test', 9 | device_info: 123, 10 | mch_id: 1900000109, 11 | nonce_str: '960f228109051b9969f76c82bde183ac', 12 | out_trade_no: '1400755861', 13 | spbill_create_ip: '127.0.0.1', 14 | total_fee: 1 15 | } 16 | 17 | @sign_md5 = '729A68AC3DE268DBD9ADE442382E7B24' 18 | @sign_hmac_sha256 = 'A58C01F990B45A4D0E496F835B2739E391C6C734927B1DA740DC873E607FB42A' 19 | end 20 | 21 | def test_generate_sign 22 | assert_equal @sign_md5, WxPay::Sign.generate(@params) 23 | end 24 | 25 | def test_generate_sign_md5 26 | assert_equal @sign_md5, WxPay::Sign.generate(@params, WxPay::Sign::SIGN_TYPE_MD5) 27 | end 28 | 29 | def test_generate_sign_hmac_sha256 30 | @params.merge!(key: "key") 31 | assert_equal @sign_hmac_sha256, WxPay::Sign.generate(@params, WxPay::Sign::SIGN_TYPE_HMAC_SHA256) 32 | end 33 | 34 | def test_verify_sign 35 | assert WxPay::Sign.verify?(@params.merge(:sign => @sign_md5)) 36 | end 37 | 38 | def test_verify_sign_when_fails 39 | assert !WxPay::Sign.verify?(@params.merge(:danger => 'danger', :sign => @sign_md5)) 40 | end 41 | 42 | def test_accept_pars_key_to_generate_sign 43 | @params.merge!(key: "key") 44 | 45 | assert_equal "1454C32E885B8D9E4A05E976D1C45B88", WxPay::Sign.generate(@params) 46 | end 47 | 48 | def test_verify_sign_when_use_hmac_sha256 49 | opts = { key: "key", sign_type: WxPay::Sign::SIGN_TYPE_HMAC_SHA256 } 50 | 51 | assert WxPay::Sign.verify?(@params.merge(:sign => @sign_hmac_sha256), opts) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /wx_pay.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | require "wx_pay/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "wx_pay" 7 | s.version = WxPay::VERSION 8 | s.authors = ["Jasl"] 9 | s.email = ["jasl9187@hotmail.com"] 10 | s.homepage = "https://github.com/jasl/wx_pay" 11 | s.summary = "An unofficial simple wechat pay gem" 12 | s.description = "An unofficial simple wechat pay gem" 13 | s.license = "MIT" 14 | 15 | s.require_paths = ["lib"] 16 | 17 | s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"] 18 | s.test_files = Dir["test/**/*"] 19 | 20 | s.add_runtime_dependency "rest-client", '>= 2.0.0' 21 | s.add_runtime_dependency "activesupport", '>= 3.2' 22 | 23 | s.add_development_dependency "rake", '>= 12.3.3' 24 | s.add_development_dependency "webmock", '~> 2.3' 25 | s.add_development_dependency "minitest", '~> 5' 26 | end 27 | --------------------------------------------------------------------------------