├── .codeclimate.yml ├── .gitignore ├── .idea └── vcs.xml ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── certs └── Eric-Guo.pem ├── cmb-pay.sublime-project ├── cmb_pay.gemspec ├── lib ├── cmb_pay.rb └── cmb_pay │ ├── merchant_code.rb │ ├── message │ ├── bill_records_message.rb │ ├── pay_message.rb │ ├── refund_order_message.rb │ └── single_order_message.rb │ ├── public.key │ ├── service.rb │ ├── sign.rb │ ├── util.rb │ └── version.rb └── spec ├── cmb_pay ├── merchant_code_spec.rb ├── service_spec.rb ├── sign_spec.rb └── util_spec.rb ├── cmb_pay_spec.rb └── spec_helper.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | exclude_fingerprints: 6 | - 02bd778d4f4ff959546967a3bcaffc6e 7 | config: 8 | languages: 9 | - ruby 10 | fixme: 11 | enabled: true 12 | rubocop: 13 | enabled: true 14 | ratings: 15 | paths: 16 | - "**.rb" 17 | exclude_paths: 18 | - spec/ 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore rubymine files 2 | /.idea 3 | .DS_Store 4 | /.bundle/ 5 | /.yardoc 6 | /Gemfile.lock 7 | /_yardoc/ 8 | /coverage/ 9 | /doc/ 10 | /pkg/ 11 | /spec/reports/ 12 | /tmp/ 13 | *.sublime-workspace 14 | spec/examples.txt 15 | .byebug_history 16 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 3.0 3 | 4 | Documentation: 5 | Enabled: false 6 | 7 | Metrics/LineLength: 8 | Max: 300 9 | 10 | Metrics/AbcSize: 11 | Max: 57 12 | 13 | Metrics/ClassLength: 14 | Max: 150 15 | 16 | Metrics/MethodLength: 17 | Max: 20 18 | 19 | Style/NumericLiterals: 20 | MinDigits: 7 21 | 22 | Style/AsciiComments: 23 | Enabled: false 24 | 25 | Style/ParameterLists: 26 | Max: 15 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.6.10 5 | - 2.7.6 6 | - 3.0.4 7 | before_install: gem install bundler -v 2.3.8 8 | script: 9 | - bundle exec rake 10 | - bundle exec codeclimate-test-reporter 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at guochunzhong@bayekeji.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in cmb_pay.gemspec 4 | gemspec 5 | 6 | gem 'byebug' 7 | 8 | group :test do 9 | gem 'codeclimate-test-reporter', '~> 1.0.0' 10 | gem 'simplecov' 11 | end 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 guochunzhong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CMB Pay [![Gem Version][version-badge]][rubygems] [![Build Status][travis-badge]][travis] [![Code Climate][codeclimate-badge]][codeclimate] [![Code Coverage][codecoverage-badge]][codecoverage] 2 | ======= 3 | 4 | An unofficial cmb (China Merchants Bank) pay ruby gem, inspired from [alipay](https://github.com/chloerei/alipay), [wx_pay](https://github.com/jasl/wx_pay) and [cmbchina](https://github.com/yellong/cmbchina) a lot. 5 | 6 | ## Feature 7 | 8 | * Payment URL generation for Web, App and WAP(Mobile Web). 9 | * CMB Bank notification payment callback parse and verify. 10 | * Direct refund API. 11 | * Single order query. 12 | * Multi orders query by transact/merchant date/settled date. 13 | 14 | ## Installation 15 | 16 | Add this line to your application's Gemfile: 17 | 18 | ```ruby 19 | gem 'cmb_pay' 20 | ``` 21 | 22 | And then execute: 23 | 24 | $ bundle 25 | 26 | Or install it yourself as: 27 | 28 | $ gem install cmb_pay 29 | 30 | ## Usage 31 | 32 | ### Config 33 | 34 | Create `config/initializers/cmb_pay.rb` and put following configurations into it. 35 | 36 | ```ruby 37 | # required 38 | CmbPay.branch_id = '0755' # 支付商户开户分行号,4位 39 | CmbPay.co_no = '000056' # 支付商户号/收单商户号,6位长数字,由银行在商户开户时确定 40 | CmbPay.co_key = '' # 商户校验码/商户密钥,测试环境为空,注意保密 41 | CmbPay.environment = 'test' if Rails.env.development? || Rails.env.staging? 42 | # only require by uri_of_pre_pay_euserp 43 | CmbPay.mch_no = 'P0019844' # 协议商户企业编号,或者说是8位虚拟企业网银编号 44 | CmbPay.default_payee_id = '1' # 默认收款方的用户标识 45 | # onlyl require if you need refund via cmb_pay (no need if you using CMB bank web) 46 | CmbPay.operator = '9999' # 操作员号,一般是9999 47 | CmbPay.operator_password = '' # 操作员的密码,默认是支付商户号,但建议修改,注意保密,仅直连退款需要 48 | ``` 49 | 50 | ## Development 51 | 52 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 53 | 54 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 55 | 56 | ## Contributing 57 | 58 | Bug reports and pull requests are welcome on GitHub at https://github.com/bayetech/cmb_pay. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 59 | 60 | 61 | ## License 62 | 63 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 64 | 65 | [version-badge]: https://badge.fury.io/rb/cmb_pay.svg 66 | [rubygems]: https://rubygems.org/gems/cmb_pay 67 | [travis-badge]: https://travis-ci.org/bayetech/cmb_pay.svg 68 | [travis]: https://travis-ci.org/bayetech/cmb_pay 69 | [codeclimate-badge]: https://codeclimate.com/github/bayetech/cmb_pay/badges/gpa.svg 70 | [codeclimate]: https://codeclimate.com/github/bayetech/cmb_pay 71 | [codecoverage-badge]: https://codeclimate.com/github/bayetech/cmb_pay/badges/coverage.svg 72 | [codecoverage]: https://codeclimate.com/github/bayetech/cmb_pay/coverage 73 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'cmb_pay' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require 'irb' 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /certs/Eric-Guo.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEODCCAqCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDDBhlcmlj 3 | L0RDPWNsb3VkLW1lcy9EQz1jb20wHhcNMjEwOTE1MDIxMDMzWhcNMjIwOTE1MDIx 4 | MDMzWjAjMSEwHwYDVQQDDBhlcmljL0RDPWNsb3VkLW1lcy9EQz1jb20wggGiMA0G 5 | CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDfweHJdAqu5+BQ6t+F923czZvUynqe 6 | DyacqWVUbDg53oVwYxldDNqUea5hMlSs1UWj2sJ5ZHiU02ly0QVyDCw/5pFP2CKJ 7 | ukQbw35ZoCF0t2i0/GPYAtBMxb1qUynkxDAtCefQG33lBt2u9scgE9xOIiPrxtJg 8 | shl3XFYmOx1ol66JR540l7NBS1OHR7UV6WiJrRW1cZTcR+py7jIbo6ud/rhZVHCO 9 | B2RTZtpH1I7ilknT1/NXMX6aw+XoNda4w+4lsrHfqKssfwJcsGMq1IbbG8illxRT 10 | wsYLiUXNJaAacT9HVO4B0jIFPP5Me7FIkiqZZKr7uyHNQE0S/5OOUkIM0v5kkUOF 11 | IE+A6WwVCyi05+JNcbLuxeSZnNeqQcXiqxl1RcodIJxw0VTcSE1CO2eYCsvfzLJ2 12 | fn9fLaSA/mrosg72UV126GeXu/y/N6gf3F6ZHM2qAMZhJ9ZUR03bUVIqApsJN28g 13 | JOkm/lV0PutN0Y2UrD0gSKxgrN24ZSsWlBECAwEAAaN3MHUwCQYDVR0TBAIwADAL 14 | BgNVHQ8EBAMCBLAwHQYDVR0OBBYEFHHJIHijHoPxeoZDyGAvLbBalVvoMB0GA1Ud 15 | EQQWMBSBEmVyaWNAY2xvdWQtbWVzLmNvbTAdBgNVHRIEFjAUgRJlcmljQGNsb3Vk 16 | LW1lcy5jb20wDQYJKoZIhvcNAQELBQADggGBACYpors2s6sGEq0ErgijhSkAQRoj 17 | W1EO3ORYYyCRFPFX1KLtUpFuXi/1rZoMoSug2Lpr+GWqt7eZIwNoryjYMbuE/sOn 18 | sANkOvLx8x4RMlmTFe+WkPiV9NasFqNn7EBSpjqQRWRlCuh6rMiYzzxNbbNvbRT4 19 | WMhBf7eWRpr1TBXDr51E8RtA+LG6wZuJFnKWBisgKOmpUw79f7EvIQAGS3MEWk/g 20 | fSvIf14zM5Dw0whGa/n60jgSc5yiW3/75GXt8608BK+bs5dViJ/3ofuIhqpOvvdp 21 | 4Oiv2zIXsfUIGAIwHN5mLwAwHty1d0s8Kt0jtJAXDUODgTuXaBj/aOqTZUUgp8Kv 22 | 6SoPdaa0LFPbkI2eiUN1xUPelsgKz0kyRBJtkMnSKFxcCxw7VHGRGFsw0ORZodQ3 23 | ZM9IDtdMg8E/4ujwilV8HKmgU77vVN6vSMvxx8zQFSz9a6GbdpB4egPZ++peSk/Q 24 | uaIJtOX6M4VC6u7eZfotARKyUy6EcoN2zNqEAQ== 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /cmb-pay.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": ".", 6 | "folder_exclude_patterns": [".bundle",".idea","tmp","log","pkg","coverage"], 7 | "file_exclude_patterns": ["*.sublime-workspace","public.key",".DS_Store","*.sqlite3","spec/examples.txt",".byebug_history"] 8 | } 9 | ], 10 | "settings": 11 | { 12 | "translate_tabs_to_spaces": true, 13 | "trim_trailing_white_space_on_save": true, 14 | "tab_size": 2 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cmb_pay.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'cmb_pay/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'cmb_pay' 7 | spec.version = CmbPay::VERSION 8 | spec.authors = ['Eric Guo'] 9 | spec.email = ['eric.guocz@gmail.com'] 10 | 11 | spec.summary = 'An unofficial cmb (China Merchants Bank) pay gem.' 12 | spec.description = 'Helping rubyist integration with cmb (China Merchants Bank) payment service (招商银行一网通支付) easier.' 13 | spec.homepage = 'https://github.com/bayetech/cmb_pay' 14 | spec.license = 'MIT' 15 | spec.required_ruby_version = '>= 2.5' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|features)/}) } \ 18 | - %w(CODE_OF_CONDUCT.md cmb-pay.sublime-project Gemfile Rakefile cmb_pay.gemspec bin/setup bin/console certs/Eric-Guo.pem) 19 | spec.bindir = 'exe' 20 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 21 | spec.require_paths = ['lib'] 22 | 23 | spec.cert_chain = ['certs/Eric-Guo.pem'] 24 | spec.signing_key = File.expand_path('~/.ssh/gem-private_key.pem') if $PROGRAM_NAME.end_with?('gem') 25 | 26 | spec.add_runtime_dependency 'http', '>= 1.0.4', '< 6' 27 | spec.add_runtime_dependency 'rexml' 28 | 29 | spec.add_development_dependency 'rake', '~> 12.3' 30 | spec.add_development_dependency 'rspec', '~> 3.11' 31 | end 32 | -------------------------------------------------------------------------------- /lib/cmb_pay.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | require 'uri' 3 | require 'http' # https://github.com/httprb/http 4 | require 'cmb_pay/version' 5 | require 'cmb_pay/util' 6 | require 'cmb_pay/sign' 7 | require 'cmb_pay/merchant_code' 8 | require 'cmb_pay/service' 9 | 10 | module CmbPay 11 | autoload(:PayMessage, File.expand_path('cmb_pay/message/pay_message', __dir__)) 12 | autoload(:RefundOrderMessage, File.expand_path('cmb_pay/message/refund_order_message', __dir__)) 13 | autoload(:SingleOrderMessage, File.expand_path('cmb_pay/message/single_order_message', __dir__)) 14 | autoload(:BillRecordsMessage, File.expand_path('cmb_pay/message/bill_records_message', __dir__)) 15 | 16 | class << self 17 | attr_accessor :branch_id # 开户分行号 18 | attr_accessor :co_no # 支付商户号/收单商户号 19 | attr_accessor :co_key # 商户校验码/商户密钥,测试环境为空 20 | attr_accessor :mch_no # 协议商户企业编号,或者说是8位虚拟企业网银编号 21 | attr_accessor :operator # 操作员号,一般是9999 22 | attr_accessor :operator_password # 操作员的密码,默认是支付商户号,但建议修改。 23 | attr_accessor :expire_in_minutes # 会话有效时间 24 | attr_accessor :environment # 调用的招商银行支付环境,默认生产,测试填test 25 | attr_accessor :default_payee_id # 默认收款方的用户标识 26 | attr_accessor :default_goods_type # 默认收款方的商品类型 27 | attr_accessor :one_cent_as_newspaper # 仅支付一分钱,商品类型直接归类到书报杂志,方便在生产上做快速测试和对账,默认启用 28 | end 29 | @co_key = '' 30 | @mch_no = '' 31 | @operator = '9999' 32 | @operator_password = '' 33 | @expire_in_minutes = 30 34 | @environment = :production 35 | @one_cent_as_newspaper = true 36 | 37 | SUPPORTED_BANK = { 38 | '招商银行' => nil, 39 | '兴业银行' => 309, 40 | '中信银行' => 302, 41 | '中国民生银行' => 305, 42 | '光大银行' => 303, 43 | '华夏银行' => 304, 44 | '北京农村商业银行' => 1418, 45 | '深圳发展银行' => 307, 46 | '中国银行' => 104, 47 | '北京银行' => 403, 48 | '中国邮政储蓄银行' => 100, 49 | '上海浦东发展银行' => 310, 50 | '东亚银行' => 2502, 51 | '广东发展银行' => 306, 52 | '南京银行' => 424, 53 | '上海交通银行' => 301, 54 | '平安银行' => 410, 55 | '中国工商银行' => 102, 56 | '杭州银行' => 423, 57 | '中国建设银行' => 105, 58 | '宁波银行' => 408, 59 | '中国农业银行' => 103, 60 | '浙商银行' => 316, 61 | '渤海银行' => 318, 62 | '上海农村商业银行' => 402, 63 | '上海银行' => 313 64 | }.freeze 65 | 66 | SUPPORTED_BANK_EN = { 67 | 'China Merchants Bank' => nil, 68 | 'Industrial Bank Co.,Ltd' => 309, 69 | 'China Citic Bank' => 302, 70 | 'China Minsheng Bank' => 305, 71 | 'China Everbright Bank' => 303, 72 | 'Huaxia Bank' => 304, 73 | 'Beijing Rural Commercial Bank' => 1418, 74 | 'Shenzhen Development Bank' => 307, 75 | 'Bank of China' => 104, 76 | 'Bank of Beijing' => 403, 77 | 'Postal Savings Bank of China' => 100, 78 | 'SPD Bank' => 310, 79 | 'BEA Bank of East Asia Limited' => 2502, 80 | 'CGB China Guangfa Bank' => 306, 81 | 'Bank of Nanjing' => 424, 82 | 'Bank of Communications' => 301, 83 | 'Ping An Bank' => 410, 84 | 'ICBC Industrial And Commercial Bank of China' => 102, 85 | 'HCCB Bank of Hangzhou' => 423, 86 | 'China Construction Bank' => 105, 87 | 'Bank of Ningbo' => 408, 88 | 'ABC Agricultural Bank of China' => 103, 89 | 'China Zheshang Bank' => 316, 90 | 'China Bohai Bank' => 318, 91 | 'Shanghai Rural Commercial Bank Co., Ltd.' => 402, 92 | 'Bank of Shanghai' => 313 93 | }.freeze 94 | 95 | GOODS_TYPE = { 96 | '网上支付' => 54_011_600, 97 | '充值缴费' => 54_011_610, 98 | '彩票' => 54_011_611, 99 | '话费' => 54_011_612, 100 | '虚拟商品' => 54_011_620, 101 | '游戏点卡' => 54_011_621, 102 | '旅游出行' => 54_010_6, 103 | '酒店' => 54_010_650, 104 | '机票' => 54_010_651, 105 | '服装' => 54_010_5, 106 | '文化教育' => 54_011_630, 107 | '书报杂志' => 54_011_631, 108 | '考试培训' => 54_011_632, 109 | '数码产品' => 54_011_640, 110 | '护肤化妆' => 54_010_7, 111 | '母婴用品' => 54_011_650, 112 | '生活日用' => 54_010_111, 113 | '食品' => 54_010_112, 114 | '汽车费用' => 54_010_2, 115 | '信用卡还款' => 54_010_306, # 此商品类型只允许使用借记卡支付 116 | '保险' => 54_011_1, 117 | '家居物品' => 54_011_2, 118 | '医疗保健' => 54_012_2, 119 | '礼品' => 54_012_3, 120 | '办公设备' => 54_011_660, 121 | '家用电器' => 54_011_670, 122 | '体育用品' => 54_011_680, 123 | '娱乐休闲' => 54_010_8, 124 | '网络服务' => 54_011_690, 125 | '收藏爱好' => 54_011_691, 126 | '投资理财' => 54_011_692 127 | }.freeze 128 | 129 | def self.uri_of_pre_pay_euserp(payer_id:, bill_no:, amount_in_cents:, merchant_url:, merchant_para: nil, 130 | protocol:, merchant_ret_url:, merchant_ret_para: nil, 131 | options: {}) 132 | generate_pay_link_of('PrePayEUserP', 133 | payer_id, bill_no, amount_in_cents, merchant_url, merchant_para, 134 | protocol, merchant_ret_url, merchant_ret_para, nil, 135 | options) 136 | end 137 | 138 | def self.uri_of_pre_pay_c2(bill_no:, amount_in_cents:, merchant_url:, merchant_para: nil, 139 | merchant_ret_url:, merchant_ret_para: nil, card_bank: nil, 140 | options: {}) 141 | generate_pay_link_of('PrePayC2', 142 | nil, bill_no, amount_in_cents, merchant_url, merchant_para, 143 | nil, merchant_ret_url, merchant_ret_para, card_bank, 144 | options) 145 | end 146 | 147 | def self.uri_of_pre_pay_wap(bill_no:, amount_in_cents:, merchant_url:, merchant_para: nil, 148 | merchant_ret_url:, merchant_ret_para: nil, card_bank: nil, 149 | options: {}) 150 | generate_pay_link_of('PrePayWAP', 151 | nil, bill_no, amount_in_cents, merchant_url, merchant_para, 152 | nil, merchant_ret_url, merchant_ret_para, card_bank, 153 | options) 154 | end 155 | 156 | def self.pay_message(query_string) 157 | PayMessage.new query_string 158 | end 159 | 160 | # 退款接口 161 | def self.refund_no_dup(bill_no:, refund_no:, refund_amount_in_cents:, memo:, 162 | bill_date: nil, operator: nil, operator_password: nil, 163 | branch_id: nil, co_no: nil, co_key: nil, time_stamp: nil) 164 | refund_in_yuan, refund_in_cent = refund_amount_in_cents.to_i.divmod(100) 165 | refund_amount = "#{refund_in_yuan}.#{format('%02d', refund_in_cent)}" 166 | desc = memo.encode(xml: :text) 167 | bill_date = Time.now.strftime('%Y%m%d') if bill_date.nil? 168 | head_inner_xml = build_direct_request_x_head('Refund_No_Dup', branch_id, co_no, time_stamp, 169 | with_operator: true, operator: operator, operator_password: operator_password) 170 | body_inner_xml = "#{bill_date}#{Util.cmb_bill_no(bill_no)}#{Util.cmb_bill_no(refund_no)}#{refund_amount}#{desc}" 171 | http_response = hash_and_direct_request_x(co_key, head_inner_xml, body_inner_xml) 172 | RefundOrderMessage.new(http_response) 173 | end 174 | 175 | # 单笔定单查询接口 176 | def self.query_single_order(bill_no:, trade_date: nil, 177 | branch_id: nil, co_no: nil, co_key: nil, time_stamp: nil) 178 | trade_date = Time.now.strftime('%Y%m%d') if trade_date.nil? 179 | head_inner_xml = build_direct_request_x_head('QuerySingleOrder', branch_id, co_no, time_stamp) 180 | body_inner_xml = "#{trade_date}#{Util.cmb_bill_no(bill_no)}" 181 | http_response = hash_and_direct_request_x(co_key, head_inner_xml, body_inner_xml) 182 | SingleOrderMessage.new(http_response) 183 | end 184 | 185 | # 商户入账查询接口 186 | # pos: 当Y续传标记为Y时(表示仍有后续的通讯包,采用多次通讯方式续传时使用) 187 | # 填写续传包请求数据中的数据 188 | def self.query_transact(begin_date:, end_date:, count:, operator: nil, pos: nil, 189 | branch_id: nil, co_no: nil, co_key: nil, time_stamp: nil) 190 | query_order_by_cmb_command('QueryTransact', begin_date, end_date, count, operator, pos, branch_id, co_no, co_key, time_stamp) 191 | end 192 | 193 | # 按商户日期查询已结账订单接口 194 | def self.query_settled_order_by_merchant_date(begin_date:, end_date:, count:, operator: nil, pos: nil, 195 | branch_id: nil, co_no: nil, co_key: nil, time_stamp: nil) 196 | query_order_by_cmb_command('QuerySettledOrderByMerchantDate', begin_date, end_date, count, operator, pos, branch_id, co_no, co_key, time_stamp) 197 | end 198 | 199 | # 按结账日期查询已结账订单接口 200 | def self.query_settled_order_by_settled_date(begin_date:, end_date:, count:, operator: nil, pos: nil, 201 | branch_id: nil, co_no: nil, co_key: nil, time_stamp: nil) 202 | query_order_by_cmb_command('QuerySettledOrderBySettledDate', begin_date, end_date, count, operator, pos, branch_id, co_no, co_key, time_stamp) 203 | end 204 | 205 | private_class_method 206 | 207 | def self.query_order_by_cmb_command(cmb_command, begin_date, end_date, count, operator, pos, 208 | branch_id, co_no, co_key, time_stamp) 209 | head_inner_xml = build_direct_request_x_head(cmb_command, branch_id, co_no, time_stamp) 210 | body_inner_xml = build_direct_request_x_query_body(begin_date, end_date, count, operator, pos) 211 | http_response = hash_and_direct_request_x(co_key, head_inner_xml, body_inner_xml) 212 | BillRecordsMessage.new(http_response) 213 | end 214 | 215 | def self.build_direct_request_x_head(cmb_command, branch_id, co_no, time_stamp, 216 | with_operator: false, operator: nil, operator_password: nil) 217 | branch_id = CmbPay.branch_id if branch_id.nil? 218 | co_no = CmbPay.co_no if co_no.nil? 219 | if with_operator 220 | operator = CmbPay.operator if operator.nil? 221 | operator_password = CmbPay.operator_password if operator_password.nil? 222 | "#{branch_id}#{co_no}#{operator}#{operator_password}#{Util.cmb_timestamp(t: time_stamp)}#{cmb_command}" 223 | else 224 | "#{branch_id}#{co_no}#{Util.cmb_timestamp(t: time_stamp)}#{cmb_command}" 225 | end 226 | end 227 | 228 | def self.build_direct_request_x_query_body(begin_date, end_date, count, operator, pos) 229 | operator = '9999' if operator.nil? 230 | begin_date = begin_date.strftime('%Y%m%d') unless begin_date.is_a?(String) 231 | end_date = end_date.strftime('%Y%m%d') unless end_date.is_a?(String) 232 | "#{begin_date}#{end_date}#{count}#{operator}#{pos}" 233 | end 234 | 235 | def self.hash_and_direct_request_x(co_key, head_inner_xml, body_inner_xml) 236 | co_key = CmbPay.co_key if co_key.nil? 237 | hash_input = "#{co_key}#{head_inner_xml}#{body_inner_xml}" 238 | hash_xml = "#{Sign.sha1_digest(hash_input)}" 239 | request_xml = "#{head_inner_xml}#{body_inner_xml}#{hash_xml}" 240 | HTTP.post(Service.request_gateway_url(:DirectRequestX), form: { 'Request' => request_xml }) 241 | end 242 | 243 | def self.generate_pay_link_of(pay_type, payer_id, bill_no, amount_in_cents, merchant_url, merchant_para, 244 | protocol, merchant_ret_url, merchant_ret_para, card_bank, options) 245 | branch_id = options.delete(:branch_id) || CmbPay.branch_id 246 | co_no = options.delete(:co_no) || CmbPay.co_no 247 | co_key = options.delete(:co_key) || CmbPay.co_key 248 | mch_no = options.delete(:mch_no) || CmbPay.mch_no 249 | cmb_bill_no = Util.cmb_bill_no(bill_no) 250 | expire_in_minutes = options.delete(:expire_in_minutes) || CmbPay.expire_in_minutes 251 | pay_in_yuan, pay_in_cent = amount_in_cents.to_i.divmod(100) 252 | pay_amount = "#{pay_in_yuan}.#{format('%02d', pay_in_cent)}" 253 | cmb_merchant_para = Service.encode_merchant_para(merchant_para) 254 | trade_date = options.delete(:trade_date) || Time.now.strftime('%Y%m%d') 255 | payee_id = options.delete(:payee_id) || CmbPay.default_payee_id 256 | goods_type = if amount_in_cents == 1 && CmbPay.one_cent_as_newspaper == true 257 | '书报杂志' 258 | else 259 | options.delete(:goods_type) || CmbPay.default_goods_type 260 | end 261 | random = options.delete(:random) 262 | if protocol.is_a?(Hash) && !payer_id.nil? 263 | cmb_reserved_xml = "#{protocol['PNo']}#{protocol['TS'] || Time.now.strftime('%Y%m%d%H%M%S')}#{mch_no}#{protocol['Seq']}#{payer_id}#{merchant_url}#{cmb_merchant_para}" 264 | else 265 | cmb_reserved_xml = generate_cmb_card_bank_xml(card_bank) 266 | payee_id = nil 267 | end 268 | m_code = MerchantCode.generate(random: random, strkey: co_key, date: trade_date, 269 | branch_id: branch_id, co_no: co_no, 270 | bill_no: cmb_bill_no, amount: pay_amount, 271 | merchant_para: cmb_merchant_para, merchant_url: merchant_url, 272 | payer_id: payer_id, payee_id: payee_id, goods_type: encode_goods_type(goods_type), 273 | reserved: cmb_reserved_xml) 274 | uri_params = { 275 | 'BranchID' => branch_id, 276 | 'CoNo' => co_no, 277 | 'BillNo' => cmb_bill_no, 278 | 'Amount' => pay_amount, 279 | 'Date' => trade_date, 280 | 'ExpireTimeSpan' => expire_in_minutes, 281 | 'MerchantUrl' => merchant_url, 282 | 'MerchantPara' => cmb_merchant_para, 283 | 'MerchantCode' => m_code 284 | } 285 | uri_params['MerchantRetUrl'] = merchant_ret_url unless merchant_ret_url.nil? 286 | uri_params['MerchantRetPara'] = Service.encode_merchant_para(merchant_ret_para) unless merchant_ret_para.nil? 287 | Service.request_uri(pay_type, uri_params) 288 | end 289 | 290 | def self.generate_cmb_card_bank_xml(card_bank) 291 | return "#{format('%04d', card_bank.to_i)}" if /^\d+$/ =~ card_bank.to_s 292 | card_back_id = SUPPORTED_BANK[card_bank] 293 | card_back_id = SUPPORTED_BANK_EN[card_bank] if card_back_id.nil? 294 | return nil if card_back_id.nil? 295 | "#{format('%04d', card_back_id)}" 296 | end 297 | 298 | def self.encode_goods_type(goods_type) 299 | return goods_type if /^\d+$/ =~ goods_type.to_s 300 | GOODS_TYPE[goods_type] 301 | end 302 | end 303 | -------------------------------------------------------------------------------- /lib/cmb_pay/merchant_code.rb: -------------------------------------------------------------------------------- 1 | module CmbPay 2 | module MerchantCode 3 | class << self 4 | attr_accessor :random_generator 5 | end 6 | @random_generator = Random.new 7 | 8 | # 产生商户校验码 9 | # 10 | # * +random+ - 随机数 11 | # * +strkey+ - 商户密钥 12 | # * +date+ - 订单日期 13 | # * +branch_id+ - 开户分行号 14 | # * +co_no+ - 商户号 15 | # * +bill_no+ - 订单号 16 | # * +amount+ - 订单金额 17 | # * +merchant_para+ - 商户自定义参数 18 | # * +merchant_url+ - 商户接受通知的URL 19 | # * +payer_id+ - 付款方用户标识。用来唯一标识商户的一个用户。长度限制为40字节以内。 20 | # * +payee_id+ - 收款方的用户标识。生成规则同上。不要求商户提供用户的注册名称,但需要保证一个用户对应一个UserID。 21 | # * +client_ips+ - 商户取得的客户端IP,如果有多个IP用逗号','分隔。长度限制为64字节。 22 | # * +goods_type+ - 商品类型编码,长度限制为8字节。 23 | # * +reserved+ - 保留字段,长度限制为1024字节。 24 | def self.generate(random: nil, strkey:, date:, branch_id:, co_no:, bill_no:, 25 | amount:, merchant_para:, merchant_url:, 26 | payer_id:, payee_id:, client_ips: nil, goods_type: nil, reserved: nil) 27 | random = random_generator.rand if random.nil? 28 | last_3 = optional_last_3(client_ips: client_ips, goods_type: goods_type, reserved: reserved) 29 | combine_part1 = pay_to_in_rc4(random: random, strkey: strkey, payer_id: payer_id, payee_id: payee_id, opt_last_3: last_3) 30 | combine_part2 = "#{strkey}#{combine_part1}#{date}#{branch_id}#{co_no}#{bill_no}#{amount}#{merchant_para}#{merchant_url}" 31 | "|#{combine_part1}|#{Sign.sha1_digest(combine_part2)}" 32 | end 33 | 34 | private_class_method 35 | 36 | def self.pay_to_in_rc4(random:, strkey:, payer_id:, payee_id:, opt_last_3:) 37 | in_data = "#{random}|#{payer_id}<$CmbSplitter$>#{payee_id}#{opt_last_3}" 38 | key_md5_digest = Sign.md5_key_digest(strkey) 39 | encrypted = Sign.rc4_encrypt(key_md5_digest, in_data) 40 | Sign.encode_base64(encrypted) 41 | end 42 | 43 | def self.optional_last_3(client_ips: nil, goods_type: nil, reserved: nil) 44 | r = '' 45 | r << "<$ClientIP$>#{client_ips}" unless client_ips.to_s == '' 46 | r << "<$GoodsType$>#{goods_type}" unless goods_type.to_s == '' 47 | r << "<$Reserved$>#{reserved}" unless reserved.to_s == '' 48 | r 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/cmb_pay/message/bill_records_message.rb: -------------------------------------------------------------------------------- 1 | require 'rexml/streamlistener' 2 | 3 | module CmbPay 4 | class BillRecordsMessage 5 | attr_reader :raw_http_response, :code, :error_message, 6 | :query_loop_flag, :query_loop_pos, :bill_records 7 | def initialize(http_response) 8 | @raw_http_response = http_response 9 | return unless http_response.code == 200 10 | @bill_records = [] 11 | 12 | REXML::Document.parse_stream(http_response.body, self) 13 | end 14 | 15 | def tag_start(name, _attributes) 16 | case name 17 | when 'Head' then @in_head = true 18 | when 'Body' then @in_body = true 19 | when 'BllRecord' then @current_bill_record = {} 20 | else 21 | @current_element_name = name 22 | end 23 | end 24 | 25 | def tag_end(name) 26 | case name 27 | when 'Head' then @in_head = false 28 | when 'Body' then @in_body = false 29 | when 'BllRecord' then @bill_records << @current_bill_record 30 | end 31 | end 32 | 33 | def text(text) 34 | if @in_head 35 | case @current_element_name 36 | when 'Code' then @code = text 37 | when 'ErrMsg' then @error_message = text 38 | end 39 | elsif @in_body 40 | case @current_element_name 41 | # 续传标记(采用多次通讯方式续传时使用) 默认值为’N’,表示没有后续数据包,’Y’表示仍有后续的通讯包 42 | when 'QryLopFlg' then @query_loop_flag = text 43 | # 续传包请求数据 44 | when 'QryLopBlk' then @query_loop_pos = text 45 | # 商户定单号 46 | when 'BillNo' then @current_bill_record[:bill_no] = text 47 | # 商户日期 48 | when 'MchDate' then @current_bill_record[:merchant_date] = text 49 | # 结算日期 50 | when 'StlDate' then @current_bill_record[:settled_date] = text 51 | # 订单状态 52 | when 'BillState' then @current_bill_record[:bill_state] = text 53 | # 订单金额 54 | when 'BillAmount' then @current_bill_record[:bill_amount] = text 55 | # 手续费 56 | when 'FeeAmount' then @current_bill_record[:fee_amount] = text 57 | # 卡类型 58 | when 'CardType' then @current_bill_record[:card_type] = text 59 | # 交易流水号 60 | when 'BillRfn' then @current_bill_record[:bill_ref_no] = text 61 | # 实扣金额 62 | when 'StlAmount' then @current_bill_record[:settled_amount] = text 63 | # 优惠金额 64 | when 'DecPayAmount' then @current_bill_record[:discount_pay_amount] = text 65 | # 订单类型:A表示二维码支付订单,B表示普通订单 66 | when 'BillType' then @current_bill_record[:bill_type] = text 67 | # 如果订单类型为A,下述字段才存在 68 | when 'Addressee' then @current_bill_record[:addressee] = text # 收货人姓名 69 | when 'Country' then @current_bill_record[:country] = text # 国家 70 | when 'Province' then @current_bill_record[:province] = text # 省份 71 | when 'City' then @current_bill_record[:city] = text # 城市 72 | when 'Address' then @current_bill_record[:address] = text # 街道地址 73 | when 'Mobile' then @current_bill_record[:mobile] = text # 手机号 74 | when 'Telephone' then @current_bill_record[:telephone] = text # 固定电话 75 | when 'ZipCode' then @current_bill_record[:zipcode] = text # 邮编 76 | when 'GoodsURL' then @current_bill_record[:goodsurl] = text # 商品详情链接 77 | end 78 | end 79 | end 80 | 81 | def succeed? 82 | code.nil? && error_message.nil? 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/cmb_pay/message/pay_message.rb: -------------------------------------------------------------------------------- 1 | module CmbPay 2 | class PayMessage 3 | attr_reader :succeed # 消息成功失败,成功为'Y',失败为'N' 4 | attr_reader :co_no # 商户号,6位长数字,由银行在商户开户时确定 5 | attr_reader :bill_no # 订单号(由支付命令送来); 6 | attr_reader :amount # 实际支付金额(由支付命令送来) 7 | attr_reader :date # 订单下单日期(由支付命令送来) 8 | attr_reader :merchant_para # 商户自定义传递参数(由支付命令送来) 9 | attr_reader :msg # 银行通知用户支付结构消息 10 | attr_reader :discount_flag # 当前订单是否有优惠,Y:有优惠 N:无优惠。 11 | attr_reader :discount_amt # 优惠金额,格式:xxxx.xx,当有优惠的时候, 12 | # 实际用户支付的是amount - discount_amt的金额到商户账号。 13 | 14 | attr_reader :branch_id # 分行号 15 | attr_reader :bank_date # 银行主机交易日期 16 | attr_reader :bank_serial_no # 银行流水号 17 | 18 | attr_reader :signature # 通知命令签名 19 | 20 | attr_reader :query_string # 原始的query_string 21 | 22 | def initialize(query_string) 23 | query_string = URI.encode_www_form(query_string) if query_string.is_a? Hash 24 | @query_string = query_string 25 | 26 | params = URI.decode_www_form(query_string).to_h 27 | 28 | @succeed = params['Succeed'] 29 | @co_no = params['CoNo'] 30 | @bill_no = params['BillNo'] 31 | @amount = params['Amount'] 32 | @date = params['Date'] 33 | @merchant_para = params['MerchantPara'] 34 | @msg = params['Msg'] 35 | @discount_flag = params['DiscountFlag'] 36 | @discount_amt = params['DiscountAmt'] 37 | 38 | # 银行通知用户的支付结果消息。信息的前38个字符格式为:4位分行号+6位商户号+8位银行接受交易的日期+20位银行流水号; 39 | # 可以利用交易日期+银行流水号+订单号对该订单进行结帐处理 40 | msg = params['Msg'][0..37] 41 | @branch_id = msg[0..3] 42 | @co_no ||= msg[4..9] 43 | @bank_date = msg[10..17] 44 | @bank_serial_no = msg[18..37] 45 | 46 | @signature = params['Signature'] 47 | end 48 | 49 | def valid? 50 | Sign::Sha1WithRsa.verify(query_string) 51 | end 52 | 53 | def succeed? 54 | succeed == 'Y' 55 | end 56 | 57 | def discount? 58 | discount_flag == 'Y' 59 | end 60 | 61 | def amount_cents 62 | (amount.to_f * 100).to_i 63 | end 64 | 65 | def discount_amount_cents 66 | (discount_amt.to_f * 100).to_i 67 | end 68 | 69 | def order_date 70 | Date.strptime(date, '%Y%m%d') 71 | end 72 | 73 | def payment_date 74 | Date.strptime(bank_date, '%Y%m%d') 75 | end 76 | 77 | def merchant_params 78 | URI.decode_www_form(merchant_para.tr('|', '&')).to_h 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/cmb_pay/message/refund_order_message.rb: -------------------------------------------------------------------------------- 1 | require 'rexml/document' 2 | 3 | module CmbPay 4 | class RefundOrderMessage 5 | attr_reader :raw_http_response, :code, :error_message, 6 | :refund_no, # 银行退款流水号 7 | :bank_seq_no, # 银行流水号 8 | :amount, # 退款金额 9 | :date, # 银行交易日期YYYYMMDD 10 | :time # 银行交易时间hhmmss 11 | 12 | def initialize(http_response) 13 | @raw_http_response = http_response 14 | return unless http_response.code == 200 15 | 16 | document_root = REXML::Document.new(http_response.body.to_s).root 17 | head = document_root.elements['Head'] 18 | @code = head.elements['Code'].text 19 | @error_message = head.elements['ErrMsg'].text 20 | return unless succeed? 21 | 22 | body = document_root.elements['Body'] 23 | @refund_no = body.elements['RefundNo'].text 24 | @bank_seq_no = body.elements['BankSeqNo'].text 25 | @amount = body.elements['Amount'].text 26 | @date = body.elements['Date'].text 27 | @time = body.elements['Time'].text 28 | end 29 | 30 | def succeed? 31 | code.nil? && error_message.nil? 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/cmb_pay/message/single_order_message.rb: -------------------------------------------------------------------------------- 1 | require 'rexml/document' 2 | 3 | module CmbPay 4 | class SingleOrderMessage 5 | attr_reader :raw_http_response, :code, :error_message, 6 | :bill_no, # 定单号 7 | :amount, # 实扣金额 8 | :accept_date, # 受理日期 9 | :accept_time, # 受理时间 10 | :bill_amount, # 定单金额 11 | :status, # 定单状态,0-已结帐,1-已撤销,2-部分结帐,4-未结帐,7-冻结交易-已经冻结金额已经全部结账 8-冻结交易,冻结金额只结帐了一部分 12 | :card_type, # 卡类型:02:一卡通 03:信用卡 13 | :fee, # 手续费 14 | :merchant_para, # 商户自定义参数。里面的特殊字符已经转码 15 | :card_no, # 卡号(部分数字用“*”掩盖) 16 | :bank_seq_no # 银行流水号 17 | def initialize(http_response) 18 | @raw_http_response = http_response 19 | return unless http_response.code == 200 20 | 21 | document_root = REXML::Document.new(http_response.body.to_s).root 22 | head = document_root.elements['Head'] 23 | @code = head.elements['Code'].text 24 | @error_message = head.elements['ErrMsg'].text 25 | return unless succeed? 26 | 27 | body = document_root.elements['Body'] 28 | @bill_no = body.elements['BillNo'].text 29 | @amount = body.elements['Amount'].text 30 | @accept_date = body.elements['AcceptDate'].text 31 | @accept_time = body.elements['AcceptTime'].text 32 | @bill_amount = body.elements['BillAmount'].text 33 | @status = body.elements['Status'].text 34 | @card_type = body.elements['CardType'].text 35 | @fee = body.elements['Fee'].text 36 | @merchant_para = body.elements['MerchantPara'].text 37 | @card_no = body.elements['CardNo'].text 38 | @bank_seq_no = body.elements['BankSeqNo'].text 39 | end 40 | 41 | def succeed? 42 | code.nil? && error_message.nil? 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/cmb_pay/public.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bayetech/cmb_pay/3f66cca10cf2c29a8331934e9f6e49b3e03c12ec/lib/cmb_pay/public.key -------------------------------------------------------------------------------- /lib/cmb_pay/service.rb: -------------------------------------------------------------------------------- 1 | module CmbPay 2 | module Service 3 | GATEWAY_URL = { 4 | production: { 5 | NP_BindCard: 'https://mobile.cmbchina.com/mobilehtml/DebitCard/M_NetPay/OneNetRegister/NP_BindCard.aspx', 6 | PrePayEUserP: 'https://netpay.cmbchina.com/netpayment/BaseHttp.dll?PrePayEUserP', 7 | PrePayC2: 'https://netpay.cmbchina.com/netpayment/BaseHttp.dll?PrePayC2', 8 | PrePayWAP: 'https://netpay.cmbchina.com/netpayment/BaseHttp.dll?PrePayWAP', 9 | DirectRequestX: 'https://payment.ebank.cmbchina.com/netpayment/basehttp.dll?DirectRequestX' 10 | }, 11 | test: { 12 | NP_BindCard: 'http://61.144.248.29:801/mobilehtml/DebitCard/M_NetPay/OneNetRegister/NP_BindCard.aspx', 13 | PrePayEUserP: 'http://61.144.248.29:801/netpayment/BaseHttp.dll?PrePayEUserP', 14 | PrePayC2: 'https://netpay.cmbchina.com/netpayment/BaseHttp.dll?TestPrePayC2', 15 | PrePayWAP: 'https://netpay.cmbchina.com/netpayment/BaseHttp.dll?TestPrePayWAP', 16 | DirectRequestX: 'http://218.17.27.197/netpayment/basehttp.dll?DirectRequestX' 17 | } 18 | }.freeze 19 | 20 | def self.request_gateway_url(api_action) 21 | GATEWAY_URL[CmbPay.environment.to_sym][api_action.to_sym] 22 | end 23 | 24 | def self.request_uri(api_action, params) 25 | uri = URI(request_gateway_url(api_action)) 26 | uri.query = if CmbPay.environment.to_sym == :test && api_action.to_sym == :PrePayC2 27 | "TestPrePayC2?#{URI.encode_www_form(params)}" 28 | elsif CmbPay.environment.to_sym == :test && api_action.to_sym == :PrePayWAP 29 | "TestPrePayWAP?#{URI.encode_www_form(params)}" 30 | else 31 | "#{uri.query}?#{URI.encode_www_form(params)}" 32 | end 33 | uri 34 | end 35 | 36 | def self.encode_merchant_para(para) 37 | if para.nil? 38 | '' 39 | else 40 | para.to_a.collect { |c| "#{c[0]}=#{c[1]}" }.join '|' 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/cmb_pay/sign.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | require 'digest/md5' 3 | require 'openssl' # For RC4/RSA/SHA1 4 | 5 | module CmbPay 6 | module Sign 7 | CMB_PUBLIC_KEY = File.read(File.expand_path('./public.key', __dir__)).freeze 8 | 9 | module Sha1WithRsa 10 | def self.verify(param_string) 11 | pub = OpenSSL::PKey::RSA.new(CMB_PUBLIC_KEY) 12 | pub.verify('sha1', signature(param_string), plain_text(param_string)) 13 | end 14 | 15 | private_class_method 16 | 17 | def self.plain_text(param_string) 18 | param_string[0, param_string.index('&Signature=')] 19 | end 20 | 21 | def self.signature(param_string) 22 | sign = param_string[param_string.index('&Signature=') + 11, param_string.length - 1] 23 | sign.split('|').map { |ascii_code| ascii_code.to_i.chr }.join('') 24 | end 25 | end 26 | 27 | # CMB replace '+' with '*' according to standard Base64 28 | def self.encode_base64(bin) 29 | ::Base64.strict_encode64(bin).tr('+', '*') 30 | end 31 | 32 | def self.md5_key_digest(str_key) 33 | Digest::MD5.hexdigest(str_key.encode('gb2312')).upcase 34 | end 35 | 36 | def self.rc4_encrypt(md5_hash, data) 37 | cipher = OpenSSL::Cipher.new('RC4') 38 | cipher.encrypt 39 | cipher.key = Util.hex_to_binary(md5_hash) 40 | cipher.update(data.encode('gb2312')) + cipher.final 41 | end 42 | 43 | def self.sha1_digest(str) 44 | OpenSSL::Digest::SHA1.hexdigest(str) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/cmb_pay/util.rb: -------------------------------------------------------------------------------- 1 | module CmbPay 2 | module Util 3 | MILLISECONDS_SINCE_UNIX_EPOCH = Time.new(2000, 1, 1).to_i * 1000 4 | 5 | def self.hex_to_binary(str) 6 | [str].pack('H*') 7 | end 8 | 9 | def self.binary_to_hex(s) 10 | s.unpack('H*')[0].upcase 11 | end 12 | 13 | def self.cmb_timestamp(t: nil) 14 | return t if t.is_a?(Integer) 15 | t = Time.now if t.nil? 16 | t.to_i * 1000 + (t.usec / 1000) - MILLISECONDS_SINCE_UNIX_EPOCH 17 | end 18 | 19 | # 招行定单号,6位或10位长数字,由商户系统生成,一天内不能重复 20 | def self.cmb_bill_no(bill_no) 21 | format('%010d', bill_no.to_i % 10_000_000_000) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/cmb_pay/version.rb: -------------------------------------------------------------------------------- 1 | module CmbPay 2 | VERSION = '1.2.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /spec/cmb_pay/merchant_code_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CmbPay::MerchantCode do 4 | subject do 5 | CmbPay::MerchantCode 6 | end 7 | 8 | describe '#generate' do 9 | specify 'test case from CMB document' do 10 | expect(subject.generate(random: '3.14', strkey: 'KeyString', date: '20081129', branch_id: '0755', co_no: '000354', bill_no: '0011223344', 11 | amount: '12.43', merchant_para: 'MerchantParaValue', merchant_url: 'http://www.abc.com/bankReciev', 12 | payer_id: 'User1', payee_id: 'User2', client_ips: '202.97.113.23', goods_type: '00000000')) 13 | .to eq '|aGpDsEcbmOuYcSeT5rQhnl0Z18OKkuAJlvDJXaex3KJXCn7KJ9XYfiw*UhIW6/a*YRTH1ImLwYPMybevPLIOUx1y6WdEnfv84loW9JF8nvw3Hsv/IWQpLd80SawuxobNab5OMOxpLg==|acdc1b7113a47324c2209626d11fa632e1210b1c' 14 | end 15 | 16 | specify 'test case using my birthday' do 17 | expect(subject.generate(random: '6.27', strkey: 'KeyString', date: '20081129', branch_id: '0755', co_no: '000354', bill_no: '0011223344', 18 | amount: '12.43', merchant_para: 'MerchantParaValue', merchant_url: 'http://www.abc.com/bankReciev', 19 | payer_id: 'User1', payee_id: 'User2', client_ips: '202.97.113.23', goods_type: '00000000')) 20 | .to eq '|bWpAs0cbmOuYcSeT5rQhnl0Z18OKkuAJlvDJXaex3KJXCn7KJ9XYfiw*UhIW6/a*YRTH1ImLwYPMybevPLIOUx1y6WdEnfv84loW9JF8nvw3Hsv/IWQpLd80SawuxobNab5OMOxpLg==|346a0d50df3f92fd0c973a7842f23f8fc8d858a2' 21 | end 22 | 23 | specify 'first test case with CMB bank' do 24 | expect(subject.generate(random: '3.14', strkey: '', date: '20160704', branch_id: '0755', co_no: '000257', bill_no: '0000002883', 25 | amount: '688.00', merchant_para: '', merchant_url: 'http://localhost:3000/mine/payments/ywt_callback', 26 | payer_id: '901', payee_id: '1', client_ips: '', goods_type: '', 27 | reserved: '9012883P0019844')) 28 | .to eq '|VkLiT8igMRUxu0xi25b3MXLjaJJk1crScPnm78NAQwGqeU5dyGY5FDgZVCSAwpyM4RkLZzFEvV3W53dc4*c4b5Lt3S6AN*DncXviElrnH6DLjUrorTpe0DWQoOI6jOQDjMN3x3*2LaKFmL9aJN7w9zvh/KaIZaGq5fo=|77629760e4b1c45051a39e1f151a18ed27f34f1b' 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/cmb_pay/service_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CmbPay::Service do 4 | subject do 5 | CmbPay.branch_id = 'bdzh' 6 | CmbPay.co_no = '123456' 7 | CmbPay::Service 8 | end 9 | 10 | describe '#request_gateway_url' do 11 | specify 'will get PrePayEUserP url' do 12 | expect(subject.request_gateway_url('PrePayEUserP')).to eq 'https://netpay.cmbchina.com/netpayment/BaseHttp.dll?PrePayEUserP' 13 | end 14 | 15 | specify 'will get PrePayWAP url' do 16 | CmbPay.environment = :test 17 | expect(subject.request_gateway_url('PrePayWAP')).to eq 'https://netpay.cmbchina.com/netpayment/BaseHttp.dll?TestPrePayWAP' 18 | end 19 | end 20 | 21 | describe '#encode_merchant_para' do 22 | specify 'will get encoded merchant para' do 23 | para_hash = { ReferenceNo: 12_345_678, 24 | Branch: '上海', 25 | SessionID: '20010901' } 26 | expect(subject.encode_merchant_para(para_hash)).to eq 'ReferenceNo=12345678|Branch=上海|SessionID=20010901' 27 | end 28 | 29 | specify 'will got empty string if input nil' do 30 | expect(subject.encode_merchant_para(nil)).to eq '' 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/cmb_pay/sign_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CmbPay::Sign do 4 | subject do 5 | CmbPay::Sign 6 | end 7 | 8 | describe '#encode_base64' do 9 | specify 'test encode64 have replace * with +' do 10 | rc4_result = CmbPay::Util.hex_to_binary('686A43B0471B98EB98712793E6B4219E5D19D7C38A92E00996F0C95DA7B1DCA2570A7ECA27D5D87E2C3E521216EBF6BE6114C7D4898BC183CCC9B7AF3CB20E531D72E967449DFBFCE25A16F4917C9EFC371ECBFF2164292DDF3449AC2EC686CD69BE4E30EC692E') 11 | expect(subject.encode_base64(rc4_result)) 12 | .to eq 'aGpDsEcbmOuYcSeT5rQhnl0Z18OKkuAJlvDJXaex3KJXCn7KJ9XYfiw*UhIW6/a*YRTH1ImLwYPMybevPLIOUx1y6WdEnfv84loW9JF8nvw3Hsv/IWQpLd80SawuxobNab5OMOxpLg==' 13 | end 14 | end 15 | 16 | describe '#md5_key_digest' do 17 | specify 'MD5 Key will got the right hashed value' do 18 | str_form_md5_hash = CmbPay::Util.hex_to_binary(subject.md5_key_digest('KeyString')) 19 | expect(CmbPay::Util.binary_to_hex(str_form_md5_hash)).to eq 'D04B908FFAF0D3213EE9B724E8B4FED8' 20 | end 21 | end 22 | 23 | describe '#rc4_encrypt' do 24 | specify 'RC4 encrypt should same as cmbJava.jar result' do 25 | strkey_md5_digest = 'D04B908FFAF0D3213EE9B724E8B4FED8' 26 | input = '3.14|User1<$CmbSplitter$>User2<$ClientIP$>202.97.113.23<$GoodsType$>00000000' 27 | encrypted = subject.rc4_encrypt(strkey_md5_digest, input) 28 | expect(CmbPay::Util.binary_to_hex(encrypted)).to eq '686A43B0471B98EB98712793E6B4219E5D19D7C38A92E00996F0C95DA7B1DCA2570A7ECA27D5D87E2C3E521216EBF6BE6114C7D4898BC183CCC9B7AF3CB20E531D72E967449DFBFCE25A16F4917C9EFC371ECBFF2164292DDF3449AC2EC686CD69BE4E30EC692E' 29 | expect(CmbPay::Sign.encode_base64(encrypted)).to eq 'aGpDsEcbmOuYcSeT5rQhnl0Z18OKkuAJlvDJXaex3KJXCn7KJ9XYfiw*UhIW6/a*YRTH1ImLwYPMybevPLIOUx1y6WdEnfv84loW9JF8nvw3Hsv/IWQpLd80SawuxobNab5OMOxpLg==' 30 | end 31 | end 32 | 33 | describe '#sha1_digest' do 34 | specify 'SHA1 will digest the right hashed value' do 35 | input = 'KeyStringaGpDsEcbmOuYcSeT5rQhnl0Z18OKkuAJlvDJXaex3KJXCn7KJ9XYfiw*UhIW6/a*YRTH1ImLwYPMybevPLIOUx1y6WdEnfv84loW9JF8nvw3Hsv/IWQpLd80SawuxobNab5OMOxpLg==200811290755000354001122334412.43MerchantParaValuehttp://www.abc.com/bankReciev' 36 | expect(subject.sha1_digest(input)).to eq 'acdc1b7113a47324c2209626d11fa632e1210b1c' 37 | end 38 | 39 | specify 'Another test from CMB bank' do 40 | input = 'abc' 41 | expect(subject.sha1_digest(input)).to eq 'a9993e364706816aba3e25717850c26c9cd0d89d' 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/cmb_pay/util_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CmbPay::Util do 4 | subject do 5 | CmbPay::Util 6 | end 7 | 8 | describe '#cmb_timestamp' do 9 | specify 'will got milli seconds since Y2K' do 10 | expect(subject.cmb_timestamp(t: Time.new(2014, 6, 27, 9, 38, 15))).to eq 457_177_095_000 11 | end 12 | 13 | specify 'will got correct timestamp from CMB case' do 14 | expect(subject.cmb_timestamp(t: Time.new(2016, 7, 28, 17, 38, 15))).to eq 523_042_695_000 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/cmb_pay_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CmbPay do 4 | subject do 5 | CmbPay.branch_id = '0755' 6 | CmbPay.co_no = '000257' 7 | CmbPay.co_key = '' 8 | CmbPay.mch_no = 'P0019844' 9 | CmbPay.default_payee_id = '1' 10 | CmbPay.environment = 'test' 11 | CmbPay 12 | end 13 | 14 | describe '#uri_of_pre_pay_euserp' do 15 | specify 'will return PrePayEUserP URI' do 16 | trade_date = Time.parse('July 6 2016').strftime('%Y%m%d') 17 | uri = subject.uri_of_pre_pay_euserp(payer_id: 1, bill_no: 654321, amount_in_cents: '12345', 18 | merchant_url: 'my_website_url', 19 | merchant_ret_url: 'browser_return_url', 20 | protocol: { 'PNo' => 1, 21 | 'Seq' => 12345, 22 | 'TS' => '20160704190627' }, 23 | options: { random: '3.14', trade_date: trade_date }) 24 | expect_result = 'http://61.144.248.29:801/netpayment/BaseHttp.dll?PrePayEUserP?BranchID=0755&CoNo=000257&BillNo=0000654321&Amount=123.45&Date=' \ 25 | + trade_date + '&ExpireTimeSpan=30&MerchantUrl=my_website_url&MerchantPara=&MerchantCode=%7CVkLiT8ioPQBO8m1cyanuKW%2FybtMowMjHHrjH78JTVBPrI1Yzhlk%2FFC8ZW3XQrO6zkUcJcVE77ky6%2FUtc7YRsKJzo1SKCMv*CJj3gAUPXSdLp0HKW8jU32DGVpfVD27Birp4jpkD6foWPiu4HKNHr5lWr3KaLLfiDlI2FrnMXX5DDdoI%2FtmTsiIpP7aWSifFOIOqLk*kJxBlFlCwNc6OW*5wnPZpwOq%2FtO0uR5OEVi3YyOSC4Q03QgE8aD15wkt5tYd0%3D%7C1d2b387aba971b0fa73bf3e39837a286e339b3f9&MerchantRetUrl=browser_return_url' 26 | expect(uri.to_s).to eq expect_result 27 | end 28 | end 29 | 30 | describe '#uri_of_pre_pay_c2' do 31 | specify 'will return PrePayC2 China Merchants Bank URI' do 32 | trade_date = Time.parse('July 7 2016').strftime('%Y%m%d') 33 | uri = subject.uri_of_pre_pay_c2(bill_no: 000000, amount_in_cents: 1, 34 | merchant_url: 'my_website_url', 35 | merchant_ret_url: 'browser_return_url', 36 | options: { random: '3.14', trade_date: trade_date }) 37 | expect_result = 'https://netpay.cmbchina.com/netpayment/BaseHttp.dll?TestPrePayC2?BranchID=0755&CoNo=000257&BillNo=0000000000&Amount=0.01&Date=' \ 38 | + trade_date + '&ExpireTimeSpan=30&MerchantUrl=my_website_url&MerchantPara=&MerchantCode=%7CVkLiT8ilJWdg%2FVx%2F1azzKX7lOMkq1bOMI7nH3slVVFPxKF5TxQd9SH1KGG*rk8*43CJMLmRRvw%3D%3D%7Ca2a17d2c32f678e13c32620aa1dc8b6c217b1569&MerchantRetUrl=browser_return_url' 39 | expect(uri.to_s).to eq expect_result 40 | end 41 | 42 | specify 'will return PrePayC2 Shanghai Bank URI but in id' do 43 | trade_date = Time.parse('July 7 2016').strftime('%Y%m%d') 44 | uri = subject.uri_of_pre_pay_c2(bill_no: 000000, amount_in_cents: 1, 45 | merchant_url: 'my_website_url', 46 | merchant_ret_url: 'browser_return_url', card_bank: 313, 47 | options: { random: '3.14', trade_date: trade_date }) 48 | expect_result = 'https://netpay.cmbchina.com/netpayment/BaseHttp.dll?TestPrePayC2?BranchID=0755&CoNo=000257&BillNo=0000000000&Amount=0.01&Date=' \ 49 | + trade_date + '&ExpireTimeSpan=30&MerchantUrl=my_website_url&MerchantPara=&MerchantCode=%7CVkLiT8ilJWdg%2FVx%2F1azzKX7lOMkq1bOMI7nH3slVVFPxKF5TxQd9SH1KGG*rk8*43CJMLmRRv06i*30RusYre8j72yrwar3QVmuyRSm0RN*XjzWb%2FHkDqmDK9%2FNLwKgfho4umlmhJvLU%7Cc05e7655018f36ba20df3b4b082184f723c3043d&MerchantRetUrl=browser_return_url' 50 | expect(uri.to_s).to eq expect_result 51 | end 52 | 53 | specify 'will return PrePayC2 Shanghai Bank URI but in Chinese' do 54 | trade_date = Time.parse('July 7 2016').strftime('%Y%m%d') 55 | uri = subject.uri_of_pre_pay_c2(bill_no: 000000, amount_in_cents: 1, 56 | merchant_url: 'my_website_url', 57 | merchant_ret_url: 'browser_return_url', card_bank: '上海银行', 58 | options: { random: '3.14', trade_date: trade_date }) 59 | expect_result = 'https://netpay.cmbchina.com/netpayment/BaseHttp.dll?TestPrePayC2?BranchID=0755&CoNo=000257&BillNo=0000000000&Amount=0.01&Date=' \ 60 | + trade_date + '&ExpireTimeSpan=30&MerchantUrl=my_website_url&MerchantPara=&MerchantCode=%7CVkLiT8ilJWdg%2FVx%2F1azzKX7lOMkq1bOMI7nH3slVVFPxKF5TxQd9SH1KGG*rk8*43CJMLmRRv06i*30RusYre8j72yrwar3QVmuyRSm0RN*XjzWb%2FHkDqmDK9%2FNLwKgfho4umlmhJvLU%7Cc05e7655018f36ba20df3b4b082184f723c3043d&MerchantRetUrl=browser_return_url' 61 | expect(uri.to_s).to eq expect_result 62 | end 63 | 64 | specify 'will return PrePayC2 Shanghai Bank URI but in English' do 65 | trade_date = Time.parse('July 7 2016').strftime('%Y%m%d') 66 | uri = subject.uri_of_pre_pay_c2(bill_no: 000000, amount_in_cents: 1, 67 | merchant_url: 'my_website_url', 68 | merchant_ret_url: 'browser_return_url', card_bank: 'Bank of Shanghai', 69 | options: { random: '3.14', trade_date: trade_date }) 70 | expect_result = 'https://netpay.cmbchina.com/netpayment/BaseHttp.dll?TestPrePayC2?BranchID=0755&CoNo=000257&BillNo=0000000000&Amount=0.01&Date=' \ 71 | + trade_date + '&ExpireTimeSpan=30&MerchantUrl=my_website_url&MerchantPara=&MerchantCode=%7CVkLiT8ilJWdg%2FVx%2F1azzKX7lOMkq1bOMI7nH3slVVFPxKF5TxQd9SH1KGG*rk8*43CJMLmRRv06i*30RusYre8j72yrwar3QVmuyRSm0RN*XjzWb%2FHkDqmDK9%2FNLwKgfho4umlmhJvLU%7Cc05e7655018f36ba20df3b4b082184f723c3043d&MerchantRetUrl=browser_return_url' 72 | expect(uri.to_s).to eq expect_result 73 | end 74 | end 75 | 76 | describe '#uri_of_pre_pay_wap' do 77 | specify 'will return PrePayWAP China Merchants Bank URI' do 78 | trade_date = Time.parse('July 11 2016').strftime('%Y%m%d') 79 | uri = subject.uri_of_pre_pay_wap(bill_no: 000000, amount_in_cents: 2, 80 | merchant_url: 'my_website_url', merchant_ret_url: 'browser_return_url', 81 | options: { random: '3.14', trade_date: trade_date }) 82 | expect_result = 'https://netpay.cmbchina.com/netpayment/BaseHttp.dll?TestPrePayWAP?BranchID=0755&CoNo=000257&BillNo=0000000000&Amount=0.02&Date=' \ 83 | + trade_date + '&ExpireTimeSpan=30&MerchantUrl=my_website_url&MerchantPara=&MerchantCode=%7CVkLiT8ilJWdg%2FVx%2F1azzKX7lOMk%3D%7Cf62b427aeb2be66e1de524bddeb42bd3f0c5f2dc&MerchantRetUrl=browser_return_url' 84 | expect(uri.to_s).to eq expect_result 85 | end 86 | end 87 | 88 | describe '#pay_message' do 89 | specify 'real CMB result with 2 value of merchant_para' do 90 | query_params = 'Succeed=Y&CoNo=000056&BillNo=000000&Amount=406.00&Date=20160710&MerchantPara=bill_no=fe5a64de823472dca4186a28759dd264|card_id=0104&Msg=07550000562016071000000000000000000000&Signature=163|81|102|123|242|141|88|91|206|112|113|27|202|119|101|133|231|160|194|14|60|221|40|81|233|0|150|225|72|150|248|74|248|184|183|118|130|213|18|232|100|123|173|74|60|248|142|143|184|14|236|43|248|235|95|38|93|182|253|113|236|212|159|255|' 91 | message = subject.pay_message(query_params) 92 | expect(message.valid?).to be_truthy 93 | expect(message.succeed?).to be_truthy 94 | expect(message.co_no).to eq '000056' 95 | expect(message.amount_cents).to eq 40600 96 | expect(message.order_date).to eq Date.new(2016, 7, 10) 97 | expect(message.payment_date).to eq Date.new(2016, 7, 10) 98 | expect(message.merchant_params['bill_no']).to eq 'fe5a64de823472dca4186a28759dd264' 99 | expect(message.merchant_params['card_id']).to eq '0104' 100 | end 101 | 102 | specify 'from a real CMB result should be valid' do 103 | query_params = 'Succeed=Y&CoNo=000056&BillNo=000000&Amount=0.01&Date=20160707&MerchantPara=&Msg=07550000562016070700000000000000000000&Signature=175|62|163|178|94|30|65|91|222|64|134|15|155|69|149|114|249|195|126|75|149|129|211|228|155|99|47|217|209|211|107|55|2|221|162|99|83|104|99|227|169|18|49|57|142|120|202|141|57|225|147|69|203|248|180|26|75|229|235|106|5|147|113|247|' 104 | message = subject.pay_message(query_params) 105 | expect(message.succeed?).to be_truthy 106 | expect(message.valid?).to be_truthy 107 | expect(message.discount?).to be_falsey 108 | expect(message.amount_cents).to eq 1 109 | expect(message.discount_amount_cents).to eq 0 110 | expect(message.branch_id).to eq '0755' 111 | expect(message.co_no).to eq '000056' 112 | end 113 | 114 | specify 'from a real CMB result should be valid too' do 115 | query_params = 'Succeed=Y&CoNo=000056&BillNo=000000&Amount=0.01&Date=20160708&MerchantPara=bill_no=3025&Msg=07550000562016070800000000000000000000&Signature=91|8|221|250|17|39|84|234|249|242|86|252|57|81|240|57|232|233|131|152|182|180|253|171|92|49|28|24|237|95|239|118|53|137|96|130|196|128|191|79|131|137|114|43|241|7|204|15|48|138|189|64|255|21|162|157|208|3|70|247|205|118|72|138|' 116 | message = subject.pay_message(query_params) 117 | expect(message.succeed?).to be_truthy 118 | expect(message.valid?).to be_truthy 119 | expect(message.amount_cents).to eq 1 120 | expect(message.branch_id).to eq '0755' 121 | expect(message.co_no).to eq '000056' 122 | expect(message.date).to eq '20160708' 123 | expect(message.merchant_para).to eq 'bill_no=3025' 124 | expect(message.merchant_params['bill_no']).to eq '3025' 125 | end 126 | 127 | specify 'from a real App Mobile callback' do 128 | query_params = 'Succeed=Y&CoNo=000257&BillNo=0000005577&Amount=138.00&Date=20160811&MerchantPara=bill_no=eea9259f0604d4b1d361742083e6f243&Msg=07550002572016081116281119400000000010&DiscountFlag=Y&DiscountAmt=10.00&Signature=169|151|118|188|238|233|243|159|208|186|118|137|23|244|237|207|68|205|240|29|182|168|151|203|61|183|202|255|33|204|134|211|70|22|143|173|96|33|118|193|144|250|255|223|166|73|102|166|231|238|190|63|48|60|175|153|208|38|178|137|209|118|128|205|' 129 | message = subject.pay_message(query_params) 130 | expect(message.succeed?).to be_truthy 131 | expect(message.valid?).to be_truthy 132 | expect(message.co_no).to eq '000257' 133 | expect(message.date).to eq '20160811' 134 | expect(message.merchant_params['bill_no']).to eq 'eea9259f0604d4b1d361742083e6f243' 135 | end 136 | end 137 | 138 | describe '#refund_no_dup' do 139 | specify 'will post refund_no_dup 5457, but not success' do 140 | request_xml = '07550002579999000257523735825625Refund_No_Dup2016080500000054570000000097149.99test7072816a910c151ae8737e55ae0b532d0a1cb3a7' 141 | expect_result_xml = 'NP2009NP2009.无效请求:当前商户不允许进行直连退款' 142 | expect(HTTP).to receive(:post).with(CmbPay::Service.request_gateway_url(:DirectRequestX), form: { 'Request' => request_xml }).and_return(HTTP::Response.new(status: 200, body: expect_result_xml, version: '1.0')) 143 | msg = subject.refund_no_dup(bill_no: 5457, refund_no: 97, refund_amount_in_cents: 14999, memo: 'test', 144 | bill_date: '20160805', operator_password: '000257', time_stamp: 523735825625) 145 | expect(msg.raw_http_response.body.to_s).to eq expect_result_xml 146 | expect(msg.succeed?).to be_falsey 147 | expect(msg.code).to eq 'NP2009' 148 | end 149 | 150 | specify 'post refund_no_dup 5457, after bank allow directly refund' do 151 | request_xml = '07550002579999000257523972537658Refund_No_Dup2016080500000054570000000097149.99test in Monday549265f99f4aa0a89405f2fcea0a59b8c3c72453' 152 | expect_result_xml = '0000000097149.992016080816280844600000000010' 153 | expect(HTTP).to receive(:post).with(CmbPay::Service.request_gateway_url(:DirectRequestX), form: { 'Request' => request_xml }).and_return(HTTP::Response.new(status: 200, body: expect_result_xml, version: '1.0')) 154 | msg = subject.refund_no_dup(bill_no: 5457, refund_no: 97, refund_amount_in_cents: 14999, memo: 'test in Monday', 155 | bill_date: '20160805', operator_password: '000257', time_stamp: 523972537658) 156 | expect(msg.raw_http_response.body.to_s).to eq expect_result_xml 157 | expect(msg.succeed?).to be_truthy 158 | expect(msg.bank_seq_no).to eq '16280844600000000010' 159 | end 160 | 161 | specify 'post refund_no_dup 5457, after refunded, refund again' do 162 | request_xml = '07550002579999000257523977519210Refund_No_Dup2016080500000054570000000097149.99test refund after refunded2c4f048e6ff9db3c6b799643427bdb24a8eea57f' 163 | expect_result_xml = 'DX4001DX4001.直连退款失败.WWT0506 - 退款金额超限 [DX214233]' 164 | expect(HTTP).to receive(:post).with(CmbPay::Service.request_gateway_url(:DirectRequestX), form: { 'Request' => request_xml }).and_return(HTTP::Response.new(status: 200, body: expect_result_xml, version: '1.0')) 165 | msg = subject.refund_no_dup(bill_no: 5457, refund_no: 97, refund_amount_in_cents: 14999, memo: 'test refund after refunded', 166 | bill_date: '20160805', operator_password: '000257', time_stamp: 523977519210) 167 | expect(msg.raw_http_response.body.to_s).to eq expect_result_xml 168 | expect(msg.succeed?).to be_falsey 169 | expect(msg.code).to eq 'DX4001' 170 | expect(msg.error_message).to eq 'DX4001.直连退款失败.WWT0506 - 退款金额超限 [DX214233]' 171 | end 172 | end 173 | 174 | describe '#query_single_order' do 175 | specify 'will post query_single_order 5456' do 176 | request_xml = '0755000257523726352486QuerySingleOrder201608050000005456348b863965127717e2822fd544bb6738773742a2' 177 | expect_result_xml = '00000054566.88201608051453546.880030.04bill_no%3d598331a5097eb572d7bd4ac96a1a6492622575******646016280531200000000020' 178 | expect(HTTP).to receive(:post).with(CmbPay::Service.request_gateway_url(:DirectRequestX), form: { 'Request' => request_xml }).and_return(HTTP::Response.new(status: 200, body: expect_result_xml, version: '1.0')) 179 | msg = subject.query_single_order(bill_no: 5456, trade_date: '20160805', time_stamp: 523726352486) 180 | expect(msg.raw_http_response.body.to_s).to eq expect_result_xml 181 | expect(msg.succeed?).to be_truthy 182 | end 183 | # bill_no 5457 exceed 100, so bill amount 159.99, and actual amount 149.99 184 | specify 'will post query_single_order 5457' do 185 | request_xml = '0755000257523726834219QuerySingleOrder2016080500000054574d951e34be8c10e9b2a29ee31cbd3a362cb46f81' 186 | expect_result_xml = '0000005457149.9920160805145441159.990030.90bill_no%3de943df978e8213715d21b5a12669e0d5622575******646016280567000000000030' 187 | expect(HTTP).to receive(:post).with(CmbPay::Service.request_gateway_url(:DirectRequestX), form: { 'Request' => request_xml }).and_return(HTTP::Response.new(status: 200, body: expect_result_xml, version: '1.0')) 188 | msg = subject.query_single_order(bill_no: 5457, trade_date: '20160805', time_stamp: 523726834219) 189 | expect(msg.raw_http_response.body.to_s).to eq expect_result_xml 190 | expect(msg.succeed?).to be_truthy 191 | expect(msg.bill_no).to eq '0000005457' 192 | expect(msg.amount).to eq '149.99' 193 | expect(msg.accept_date).to eq '20160805' 194 | expect(msg.accept_time).to eq '145441' 195 | expect(msg.bill_amount).to eq '159.99' 196 | expect(msg.status).to eq '0' 197 | expect(msg.card_type).to eq '03' 198 | expect(msg.fee).to eq '0.90' 199 | expect(msg.merchant_para).to eq 'bill_no%3de943df978e8213715d21b5a12669e0d5' 200 | expect(msg.card_no).to eq '622575******6460' 201 | expect(msg.bank_seq_no).to eq '16280567000000000030' 202 | end 203 | specify 'query a not exist order 54561' do 204 | request_xml = '0755000257523820622433QuerySingleOrder201608050000054561de36872ce482460d0d8993c9f41629432dddc4ea' 205 | expect_result_xml = 'DX4000DX4000.查询单笔订单失败.WWQ1111 - 找不到对应记录 [DX111354]' 206 | expect(HTTP).to receive(:post).with(CmbPay::Service.request_gateway_url(:DirectRequestX), form: { 'Request' => request_xml }).and_return(HTTP::Response.new(status: 200, body: expect_result_xml, version: '1.0')) 207 | msg = subject.query_single_order(bill_no: 54561, trade_date: '20160805', time_stamp: 523820622433) 208 | expect(msg.raw_http_response.body.to_s).to eq expect_result_xml 209 | expect(msg.succeed?).to be_falsey 210 | end 211 | end 212 | 213 | describe '#query_transact' do 214 | specify 'will post query_transact as direct request X' do 215 | request_xml = '0755000257523727813775QueryTransact2015091920150923299997d60537819b0e6db1223b422416eb7ed9d5d725f' 216 | expect_result_xml = 'Y20150919104757162722674000000000100000003305201607222015091900.980.0107162722058000000000100.980.000000003306201607222015091904.180.0307162722674000000000104.180.00' 217 | expect(HTTP).to receive(:post).with(CmbPay::Service.request_gateway_url(:DirectRequestX), form: { 'Request' => request_xml }).and_return(HTTP::Response.new(status: 200, body: expect_result_xml, version: '1.0')) 218 | msg = subject.query_transact(begin_date: '20150919', end_date: '20150923', count: 2, 219 | time_stamp: 523727813775) 220 | expect(msg.raw_http_response.body.to_s).to eq expect_result_xml 221 | expect(msg.succeed?).to be_truthy 222 | expect(msg.bill_records.count).to eq 2 223 | end 224 | 225 | specify 'will post query_transact as direct request X with pos' do 226 | request_xml = '0755000257523730558849QueryTransact2015091920150923299992015091910475716272267400000000010aff9f9c46455c2165c64b8380df079a398ca26c9' 227 | expect_result_xml = 'Y20150919105242162722627000000000100000003309201607222015091906.880.0407162722253000000000106.880.000000003310201607222015091906.880.0407162722627000000000106.880.00' 228 | expect(HTTP).to receive(:post).with(CmbPay::Service.request_gateway_url(:DirectRequestX), form: { 'Request' => request_xml }).and_return(HTTP::Response.new(status: 200, body: expect_result_xml, version: '1.0')) 229 | msg = subject.query_transact(begin_date: '20150919', end_date: '20150923', count: 2, pos: '2015091910475716272267400000000010', 230 | time_stamp: 523730558849) 231 | expect(msg.raw_http_response.body.to_s).to eq expect_result_xml 232 | expect(msg.bill_records.count).to eq 2 233 | end 234 | 235 | specify 'will post query_transact as direct request X as different account' do 236 | request_xml = '0731000005523042695395QueryTransact20160726201607271099995921609f50ff447c5f610fdf4607cb5416fe97ed' 237 | expect_result_xml = 'TTN00000000000000 ' 238 | expect(HTTP).to receive(:post).with(CmbPay::Service.request_gateway_url(:DirectRequestX), form: { 'Request' => request_xml }).and_return(HTTP::Response.new(status: 200, body: expect_result_xml, version: '1.0')) 239 | msg = subject.query_transact(begin_date: '20160726', end_date: '20160727', count: 10, 240 | branch_id: '0731', co_no: '000005', time_stamp: 523042695395) 241 | expect(msg.raw_http_response.body.to_s).to eq expect_result_xml 242 | expect(msg.bill_records.count).to eq 0 243 | end 244 | end 245 | 246 | describe '#query_settled_order_by_merchant_date' do 247 | specify 'will post query_settled_order_by_merchant_date as direct request X' do 248 | request_xml = '0755000257523731435008QuerySettledOrderByMerchantDate2016080520160805299993f33ffbbe9a54a9b86a0b18cd7226a51dc11f4f5' 249 | expect_result_xml = 'YHH000120160805154434162805662000000000200000005477201608052016080500.030.0003162805732000000000100.030.000000005474201608052016080500.030.0003162805662000000000200.030.00' 250 | expect(HTTP).to receive(:post).with(CmbPay::Service.request_gateway_url(:DirectRequestX), form: { 'Request' => request_xml }).and_return(HTTP::Response.new(status: 200, body: expect_result_xml, version: '1.0')) 251 | msg = subject.query_settled_order_by_merchant_date(begin_date: '20160805', end_date: '20160805', count: 2, 252 | time_stamp: 523731435008) 253 | expect(msg.raw_http_response.body.to_s).to eq expect_result_xml 254 | expect(msg.bill_records.count).to eq 2 255 | end 256 | 257 | specify 'will post query_settled_order_by_merchant_date with pos' do 258 | request_xml = '0755000257523731718930QuerySettledOrderByMerchantDate201608052016080539999HH00012016080515443416280566200000000020956b8f831c0490fdc7d35038a9b2ebc3ce450cda' 259 | expect_result_xml = 'YHH000120160805153019162805670000000000400000005472201608052016080500.030.0003162805251000000000100.030.000000005471201608052016080500.030.0003162805661000000000200.030.000000005469201608052016080500.030.0003162805670000000000400.030.00' 260 | expect(HTTP).to receive(:post).with(CmbPay::Service.request_gateway_url(:DirectRequestX), form: { 'Request' => request_xml }).and_return(HTTP::Response.new(status: 200, body: expect_result_xml, version: '1.0')) 261 | msg = subject.query_settled_order_by_merchant_date(begin_date: '20160805', end_date: '20160805', count: 3, pos: 'HH00012016080515443416280566200000000020', 262 | time_stamp: 523731718930) 263 | expect(msg.raw_http_response.body.to_s).to eq expect_result_xml 264 | expect(msg.bill_records.count).to eq 3 265 | end 266 | end 267 | 268 | describe '#query_settled_order_by_settled_date' do 269 | specify 'will post query_settled_order_by_settled_date as direct request X' do 270 | request_xml = '0755000257523732011805QuerySettledOrderBySettledDate201608052016080519999779a26d9beea763db5f4a7c783a329cc1f087caa' 271 | expect_result_xml = 'YHH000120160805155330162805732000000000100000005477201608052016080560.030.000316280573200000000010T0.030.00' 272 | expect(HTTP).to receive(:post).with(CmbPay::Service.request_gateway_url(:DirectRequestX), form: { 'Request' => request_xml }).and_return(HTTP::Response.new(status: 200, body: expect_result_xml, version: '1.0')) 273 | msg = subject.query_settled_order_by_settled_date(begin_date: '20160805', end_date: '20160805', count: 1, 274 | time_stamp: 523732011805) 275 | expect(msg.raw_http_response.body.to_s).to eq expect_result_xml 276 | expect(msg.bill_records.count).to eq 1 277 | end 278 | 279 | specify 'will post query_settled_order_by_settled_date with pos' do 280 | request_xml = '0755000257523732240132QuerySettledOrderBySettledDate201608052016080529999HH00012016080515533016280573200000000010845e207806d9d8b76e9767d7b3b70e3998368e56' 281 | expect_result_xml = 'YHH000120160805154153162805251000000000100000005474201608052016080560.030.0003162805662000000000200.030.000000005472201608052016080560.030.0003162805251000000000100.030.00' 282 | expect(HTTP).to receive(:post).with(CmbPay::Service.request_gateway_url(:DirectRequestX), form: { 'Request' => request_xml }).and_return(HTTP::Response.new(status: 200, body: expect_result_xml, version: '1.0')) 283 | msg = subject.query_settled_order_by_settled_date(begin_date: '20160805', end_date: '20160805', count: 2, pos: 'HH00012016080515533016280573200000000010', 284 | time_stamp: 523732240132) 285 | expect(msg.raw_http_response.body.to_s).to eq expect_result_xml 286 | expect(msg.query_loop_flag).to eq 'Y' 287 | expect(msg.query_loop_pos).to eq 'HH00012016080515415316280525100000000010' 288 | expect(msg.bill_records.count).to eq 2 289 | end 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 4 | require 'byebug' 5 | require 'cmb_pay' 6 | --------------------------------------------------------------------------------