├── .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}$ClientIP$>" unless client_ips.to_s == ''
46 | r << "<$GoodsType$>#{goods_type}$GoodsType$>" unless goods_type.to_s == ''
47 | r << "<$Reserved$>#{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$ClientIP$><$GoodsType$>00000000$GoodsType$>'
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 = 'NP2009
NP2009.无效请求:当前商户不允许进行直连退款'
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 = 'DX4001
DX4001.直连退款失败.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 = 'DX4000
DX4000.查询单笔订单失败.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 = 'T
TN00000000000000 '
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 |
--------------------------------------------------------------------------------