├── lib ├── active_merchant_allpay │ └── version.rb ├── active_merchant_allpay.rb └── offsite_payments │ └── integrations │ ├── allpay.rb │ └── allpay │ ├── notification.rb │ └── helper.rb ├── Gemfile ├── .gitignore ├── test ├── unit │ └── integrations │ │ ├── allpay_module_test.rb │ │ ├── allpay_notification_test.rb │ │ └── allpay_helper_test.rb └── test_helper.rb ├── Rakefile ├── LICENSE.txt ├── active_merchant_allpay.gemspec └── README.md /lib/active_merchant_allpay/version.rb: -------------------------------------------------------------------------------- 1 | module ActiveMerchantAllpay 2 | VERSION = "0.1.5" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in active_merchant_allpay.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | .idea/ 19 | .idea/workspace.xml 20 | -------------------------------------------------------------------------------- /test/unit/integrations/allpay_module_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AllpayModuleTest < Test::Unit::TestCase 4 | include OffsitePayments::Integrations 5 | 6 | def test_notification_method 7 | assert_instance_of Allpay::Notification, Allpay.notification('name=cody') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/active_merchant_allpay.rb: -------------------------------------------------------------------------------- 1 | require 'action_view' 2 | require 'active_merchant_allpay/version' 3 | require 'active_merchant' 4 | require 'money' 5 | require 'offsite_payments' 6 | module OffsitePayments 7 | module Integrations 8 | autoload :Allpay, 'offsite_payments/integrations/allpay' 9 | end 10 | end 11 | 12 | ActionView::Base.send(:include, OffsitePayments::ActionViewHelper) 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require 'rake/testtask' 4 | 5 | desc "Run the unit test suite" 6 | task :default => 'test:units' 7 | 8 | task :test => 'test:units' 9 | 10 | namespace :test do 11 | 12 | Rake::TestTask.new(:units) do |t| 13 | t.pattern = 'test/unit/**/*_test.rb' 14 | t.ruby_opts << '-rubygems' 15 | t.libs << 'test' 16 | t.verbose = true 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 xwaynec 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/unit/integrations/allpay_notification_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AllpayNotificationTest < Test::Unit::TestCase 4 | include OffsitePayments::Integrations 5 | 6 | def setup 7 | OffsitePayments::Integrations::Allpay.hash_key = '5294y06JbISpM5x9' 8 | OffsitePayments::Integrations::Allpay.hash_iv = 'v77hoKGq4kWxNNIS' 9 | @allpay = Allpay::Notification.new(http_raw_data) 10 | end 11 | 12 | def test_params 13 | p = @allpay.params 14 | 15 | assert_equal 12, p.size 16 | assert_equal 'Credit_CreditCard', p['PaymentType'] 17 | assert_equal 'BC586977559ED305BEC4C334DFDC881D', p['CheckMacValue'] 18 | assert_equal '2014/04/15 15:39:38', p['PaymentDate'] 19 | end 20 | 21 | def test_complete? 22 | assert @allpay.complete? 23 | end 24 | 25 | def test_checksum_ok? 26 | assert @allpay.checksum_ok? 27 | 28 | # Should preserve mac value 29 | assert @allpay.params['CheckMacValue'].present? 30 | end 31 | 32 | private 33 | 34 | def http_raw_data 35 | # Sample notification from test environment 36 | "TradeAmt=2760&RtnMsg=付款成功&MerchantTradeNo=81397545579&PaymentType=Credit_CreditCard&TradeNo=1404151506342901&SimulatePaid=1&MerchantID=2000132&TradeDate=2014-04-15 15:06:34&PaymentDate=2014/04/15 15:39:38&PaymentTypeChargeFee=0&CheckMacValue=BC586977559ED305BEC4C334DFDC881D&RtnCode=1" 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /active_merchant_allpay.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'active_merchant_allpay/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "active_merchant_allpay" 8 | spec.version = ActiveMerchantAllpay::VERSION 9 | spec.authors = ["xwaynec"] 10 | spec.email = ["xwaynec@gmail.com"] 11 | spec.description = %q{A rails plugin to add active_merchant patch for Taiwan payment} 12 | spec.summary = %q{A rails plugin to add active_merchant patch for Taiwan payment} 13 | spec.homepage = "https://github.com/xwaynec/active_merchant_allpay" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency 'activemerchant' 22 | spec.add_dependency 'offsite_payments' 23 | spec.add_dependency 'money' 24 | spec.add_development_dependency('test-unit', '~> 2.5.5') 25 | 26 | spec.add_development_dependency('rake') 27 | spec.add_development_dependency('mocha', '~> 0.13.0') 28 | spec.add_development_dependency('rails', '>= 2.3.14') 29 | spec.add_development_dependency('thor') 30 | end 31 | -------------------------------------------------------------------------------- /lib/offsite_payments/integrations/allpay.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/allpay/helper.rb' 2 | require File.dirname(__FILE__) + '/allpay/notification.rb' 3 | 4 | module OffsitePayments #:nodoc: 5 | module Integrations #:nodoc: 6 | module Allpay 7 | PAYMENT_CREDIT_CARD = 'Credit' 8 | PAYMENT_ATM = 'ATM' 9 | PAYMENT_CVS = 'CVS' 10 | PAYMENT_ALIPAY = 'Alipay' 11 | PAYMENT_BARCODE = 'BARCODE' 12 | 13 | SUBPAYMENT_ATM_TAISHIN = 'TAISHIN' 14 | SUBPAYMENT_ATM_ESUN = 'ESUN' 15 | SUBPAYMENT_ATM_HUANAN = 'HUANAN' 16 | SUBPAYMENT_ATM_BOT = 'BOT' 17 | SUBPAYMENT_ATM_FUBON = 'FUBON' 18 | SUBPAYMENT_ATM_CHINATRUST = 'CHINATRUST' 19 | SUBPAYMENT_ATM_FIRST = 'FIRST' 20 | 21 | SUBPAYMENT_CVS_CVS = 'CVS' 22 | SUBPAYMENT_CVS_OK = 'OK' 23 | SUBPAYMENT_CVS_FAMILY = 'FAMILY' 24 | SUBPAYMENT_CVS_HILIFE = 'HILIFE' 25 | SUBPAYMENT_CVS_IBON = 'IBON' 26 | 27 | PAYMENT_TYPE = 'aio' 28 | 29 | mattr_accessor :service_url 30 | mattr_accessor :merchant_id 31 | mattr_accessor :hash_key 32 | mattr_accessor :hash_iv 33 | mattr_accessor :debug 34 | 35 | def self.service_url 36 | mode = ActiveMerchant::Billing::Base.mode 37 | case mode 38 | when :production 39 | 'https://payment.allpay.com.tw/Cashier/AioCheckOut' 40 | when :development 41 | 'http://payment-stage.allpay.com.tw/Cashier/AioCheckOut' 42 | when :test 43 | 'http://payment-stage.allpay.com.tw/Cashier/AioCheckOut' 44 | else 45 | raise StandardError, "Integration mode set to an invalid value: #{mode}" 46 | end 47 | end 48 | 49 | def self.notification(post) 50 | Notification.new(post) 51 | end 52 | 53 | def self.setup 54 | yield(self) 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/unit/integrations/allpay_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AllpayHelperTest < Test::Unit::TestCase 4 | include OffsitePayments::Integrations 5 | 6 | def setup 7 | end 8 | 9 | def test_check_mac_value 10 | @helper = Allpay::Helper.new 'sdfasdfa', '12345678' 11 | @helper.add_field 'ItemName', 'sdfasdfa' 12 | @helper.add_field 'MerchantID', '12345678' 13 | @helper.add_field 'MerchantTradeDate', '2013/03/12 15:30:23' 14 | @helper.add_field 'MerchantTradeNo','allpay_1234' 15 | @helper.add_field 'PaymentType', 'allpay' 16 | @helper.add_field 'ReturnURL', 'http:sdfasdfa' 17 | @helper.add_field 'TotalAmount', '500' 18 | @helper.add_field 'TradeDesc', 'dafsdfaff' 19 | 20 | OffsitePayments::Integrations::Allpay.hash_key = 'xdfaefasdfasdfa32d' 21 | OffsitePayments::Integrations::Allpay.hash_iv = 'sdfxfafaeafwexfe' 22 | 23 | @helper.encrypted_data 24 | 25 | assert_equal '40D9A6C00A4A78A300ED458237071BDA', @helper.fields['CheckMacValue'] 26 | end 27 | 28 | def test_check_mac_value_with_special_characters 29 | @helper = Allpay::Helper.new 'R435729525344', '2000132' 30 | @helper.add_field 'ItemName', 'Guava' 31 | @helper.add_field 'MerchantID', '2000132' 32 | @helper.add_field 'MerchantTradeDate', '2014/08/07 17:17:33' 33 | @helper.add_field 'MerchantTradeNo','R435729525344' 34 | @helper.add_field 'PaymentType', 'aio' 35 | @helper.add_field 'ReturnURL', 'http://example.com/notify' 36 | @helper.add_field 'TotalAmount', '1000' 37 | @helper.add_field 'TradeDesc', ( '~`@#$%*^()_-+={}[]|\\"\'>,.?/:;' + "\t" ) 38 | @helper.add_field 'ChoosePayment', 'Credit' 39 | 40 | OffsitePayments::Integrations::Allpay.hash_key = '5294y06JbISpM5x9' 41 | OffsitePayments::Integrations::Allpay.hash_iv = 'v77hoKGq4kWxNNIS' 42 | 43 | @helper.encrypted_data 44 | 45 | assert_equal 'DF1186B7B8651F44380BEF8AB8A5727B', @helper.fields['CheckMacValue'] 46 | end 47 | 48 | def test_url_encoding 49 | encoded = Allpay::Helper.url_encode('-_.!~*() @#$%^&=+;?/\\><`[]{}:\'",|') 50 | assert_equal encoded, '-_.!%7e*()+%40%23%24%25%5e%26%3d%2b%3b%3f%2f%5c%3e%3c%60%5b%5d%7b%7d%3a%27%22%2c%7c' 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $:.unshift File.expand_path('../../lib', __FILE__) 3 | 4 | begin 5 | require 'rubygems' 6 | require 'bundler' 7 | Bundler.setup 8 | rescue LoadError => e 9 | puts "Error loading bundler (#{e.message}): \"gem install bundler\" for bundler support." 10 | end 11 | 12 | require 'active_merchant_allpay' 13 | 14 | require 'test/unit' 15 | require 'money' 16 | 17 | ActiveMerchant::Billing::Base.mode = :test 18 | 19 | module ActiveMerchant 20 | module Assertions 21 | AssertionClass = Test::Unit::AssertionFailedError 22 | 23 | def assert_field(field, value) 24 | clean_backtrace do 25 | assert_equal value, @helper.fields[field] 26 | end 27 | end 28 | 29 | # Allows the testing of you to check for negative assertions: 30 | # 31 | # # Instead of 32 | # assert !something_that_is_false 33 | # 34 | # # Do this 35 | # assert_false something_that_should_be_false 36 | # 37 | # An optional +msg+ parameter is available to help you debug. 38 | def assert_false(boolean, message = nil) 39 | message = build_message message, ' is not false or nil.', boolean 40 | 41 | clean_backtrace do 42 | assert_block message do 43 | not boolean 44 | end 45 | end 46 | end 47 | 48 | # A handy little assertion to check for a successful response: 49 | # 50 | # # Instead of 51 | # assert_success response 52 | # 53 | # # DRY that up with 54 | # assert_success response 55 | # 56 | # A message will automatically show the inspection of the response 57 | # object if things go afoul. 58 | def assert_success(response) 59 | clean_backtrace do 60 | assert response.success?, "Response failed: #{response.inspect}" 61 | end 62 | end 63 | 64 | # The negative of +assert_success+ 65 | def assert_failure(response) 66 | clean_backtrace do 67 | assert_false response.success?, "Response expected to fail: #{response.inspect}" 68 | end 69 | end 70 | 71 | def assert_valid(validateable) 72 | clean_backtrace do 73 | assert validateable.valid?, "Expected to be valid" 74 | end 75 | end 76 | 77 | def assert_not_valid(validateable) 78 | clean_backtrace do 79 | assert_false validateable.valid?, "Expected to not be valid" 80 | end 81 | end 82 | 83 | def assert_deprecation_warning(message, target) 84 | target.expects(:deprecated).with(message) 85 | yield 86 | end 87 | 88 | def assert_no_deprecation_warning(target) 89 | target.expects(:deprecated).never 90 | yield 91 | end 92 | 93 | private 94 | def clean_backtrace(&block) 95 | yield 96 | rescue AssertionClass => e 97 | path = File.expand_path(__FILE__) 98 | raise AssertionClass, e.message, e.backtrace.reject { |line| File.expand_path(line) =~ /#{path}/ } 99 | end 100 | end 101 | end 102 | 103 | Test::Unit::TestCase.class_eval do 104 | include ActiveMerchant::Assertions 105 | end 106 | -------------------------------------------------------------------------------- /lib/offsite_payments/integrations/allpay/notification.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | 3 | module OffsitePayments #:nodoc: 4 | module Integrations #:nodoc: 5 | module Allpay 6 | class Notification < OffsitePayments::Notification 7 | 8 | def status 9 | if rtn_code == '1' 10 | true 11 | else 12 | false 13 | end 14 | end 15 | 16 | # TODO 使用查詢功能實作 acknowledge 17 | # Allpay 沒有遠端驗證功能, 18 | # 而以 checksum_ok? 代替 19 | def acknowledge 20 | checksum_ok? 21 | end 22 | 23 | def complete? 24 | case @params['RtnCode'] 25 | when '1' #付款成功 26 | true 27 | when '2' # ATM 取號成功 28 | true 29 | when '10100073' # CVS 或 BARCODE 取號成功 30 | true 31 | when '800' #貨到付款訂單建立成功 32 | true 33 | else 34 | false 35 | end 36 | end 37 | 38 | def hash_key=(key) 39 | @hash_key = key 40 | end 41 | 42 | def hash_iv=(iv) 43 | @hash_iv = iv 44 | end 45 | 46 | def hash_key 47 | @hash_key || OffsitePayments::Integrations::Allpay.hash_key 48 | end 49 | 50 | def hash_iv 51 | @hash_iv || OffsitePayments::Integrations::Allpay.hash_iv 52 | end 53 | 54 | def checksum_ok? 55 | params_copy = @params.clone 56 | 57 | checksum = params_copy.delete('CheckMacValue') 58 | 59 | # 把 params 轉成 query string 前必須先依照 hash key 做 sort 60 | # 依照英文字母排序,由 A 到 Z 且大小寫不敏感 61 | raw_data = params_copy.sort_by{ |k,v| k.downcase }.map do |x, y| 62 | "#{x}=#{y}" 63 | end.join('&') 64 | 65 | hash_raw_data = "HashKey=#{hash_key}&#{raw_data}&HashIV=#{hash_iv}" 66 | 67 | url_encode_data = OffsitePayments::Integrations::Allpay::Helper.url_encode(hash_raw_data) 68 | url_encode_data.downcase! 69 | 70 | (Digest::MD5.hexdigest(url_encode_data) == checksum.to_s.downcase) 71 | end 72 | 73 | def rtn_code 74 | @params['RtnCode'] 75 | end 76 | 77 | def merchant_id 78 | @params['MerchantID'] 79 | end 80 | 81 | # 廠商交易編號 82 | def merchant_trade_no 83 | @params['MerchantTradeNo'] 84 | end 85 | alias :item_id :merchant_trade_no 86 | 87 | def rtn_msg 88 | @params['RtnMsg'] 89 | end 90 | 91 | # AllPay 的交易編號 92 | def trade_no 93 | @params['TradeNo'] 94 | end 95 | alias :transaction_id :trade_no 96 | 97 | def trade_amt 98 | @params['TradeAmt'] 99 | end 100 | def gross 101 | ::Money.new(@params['TradeAmt'].to_i * 100, currency) 102 | end 103 | 104 | def payment_date 105 | @params['PaymentDate'] 106 | end 107 | 108 | def payment_type 109 | @params['PaymentType'] 110 | end 111 | 112 | def payment_type_charge_fee 113 | @params['PaymentTypeChargeFee'] 114 | end 115 | 116 | def trade_date 117 | @params['TradeDate'] 118 | end 119 | 120 | def simulate_paid 121 | @params['SimulatePaid'] 122 | end 123 | 124 | def check_mac_value 125 | @params['CheckMacValue'] 126 | end 127 | 128 | # for ATM 129 | def bank_code 130 | @params['BankCode'] 131 | end 132 | 133 | def v_account 134 | @params['vAccount'] 135 | end 136 | 137 | def expire_date 138 | @params['ExpireDate'] 139 | end 140 | 141 | def card4no 142 | @params['card4no'] 143 | end 144 | 145 | # for CVS 146 | def payment_no 147 | @params['PaymentNo'] 148 | end 149 | 150 | def currency 151 | 'TWD' 152 | end 153 | 154 | # for BARCODE 155 | def barcode1 156 | @params['Barcode1'] 157 | end 158 | 159 | def barcode2 160 | @params['Barcode2'] 161 | end 162 | 163 | def barcode3 164 | @params['Barcode3'] 165 | end 166 | end 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveMerchantAllpay 2 | 3 | This plugin is an active_merchant patch for Allpay(歐付寶) online payment in Taiwan. 4 | Now it supports: 5 | - Credit card(信用卡) 6 | - ATM(虛擬ATM) 7 | - Alipay(支付寶) 8 | - CVS(超商繳費) 9 | - BARCODE(超商條碼). 10 | 11 | It has been tested on Rails 4.2 successfully. 12 | 13 | ## Installation 14 | 15 | Add this line to your application's Gemfile: 16 | ``` Gemfile 17 | gem 'active_merchant_allpay', github: 'imgarylai/active_merchant_allpay' 18 | ``` 19 | And then execute: 20 | ```sh 21 | $ bundle install 22 | ``` 23 | Or install it yourself as: 24 | ``` 25 | $ gem install active_merchant_allpay 26 | ``` 27 | 28 | ## Usage 29 | 30 | You can get Payment API and SPEC in [Allpay API](https://www.allpay.com.tw/Service/API_Help). 31 | Then create an initializer, like initializers/allpay.rb. Add the following configurations depends on your settings. 32 | 33 | ``` ruby 34 | # config/environments/development.rb 35 | config.after_initialize do 36 | ActiveMerchant::Billing::Base.mode = :development 37 | end 38 | 39 | # config/environments/production.rb 40 | config.after_initialize do 41 | ActiveMerchant::Billing::Base.mode = :production 42 | end 43 | 44 | ``` 45 | 46 | ``` ruby 47 | # initializers/allpay.rb 48 | OffsitePayments::Integrations::Allpay.setup do |allpay| 49 | if Rails.env.development? 50 | # default setting for stage test 51 | allpay.merchant_id = '2000132' 52 | allpay.hash_key = '5294y06JbISpM5x9' 53 | allpay.hash_iv = 'v77hoKGq4kWxNNIS' 54 | else 55 | # change to yours 56 | allpay.merchant_id = '7788520' 57 | allpay.hash_key = 'adfas123412343j' 58 | allpay.hash_iv = '123ddewqerasdfas' 59 | end 60 | end 61 | ``` 62 | 63 | 64 | ## Example Usage 65 | 66 | Now support three payment methods: 67 | 68 | ``` rb 69 | # 1. Credit card 70 | OffsitePayments::Integrations::Allpay::PAYMENT_CREDIT_CARD 71 | 72 | # 2. ATM 73 | OffsitePayments::Integrations::Allpay::PAYMENT_ATM 74 | 75 | # 3. CVS (convenience store) 76 | OffsitePayments::Integrations::Allpay::PAYMENT_CVS 77 | 78 | # 4. Alipay 79 | OffsitePayments::Integrations::Allpay::PAYMENT_ALIPAY 80 | 81 | # 5. BARCODE 82 | OffsitePayments::Integrations::Allpay::PAYMENT_BARCODE 83 | ``` 84 | 85 | Once you’ve configured ActiveMerchantAllpay, you need a checkout form; it looks like: 86 | 87 | ``` erb 88 | <% payment_service_for @order.id, 89 | @order.user.email, 90 | :service => :allpay, 91 | :html => { :id => 'allpay-atm-form', :method => :post } do |service| %> 92 | <% service.merchant_trade_no "#{@order.id}T#{Time.zone.now}" %> 93 | <% service.merchant_trade_date @order.created_at %> 94 | <% service.total_amount @order.total.to_i %> 95 | <% service.description @order.id %> 96 | <% service.item_name @order.id %> 97 | <% service.choose_payment OffsitePayments::Integrations::Allpay::PAYMENT_ATM %> 98 | <% service.return_url root_path %> 99 | <% service.notify_url allpay_atm_return_url %> 100 | <% service.encrypted_data %> 101 | <%= submit_tag 'Buy!' %> 102 | <% end %> 103 | ``` 104 | 105 | Also need a notification action when Allpay service notifies your server; it looks like: 106 | 107 | ``` ruby 108 | def notify 109 | notification = OffsitePayments::Integrations::Allpay::Notification.new(request.raw_post) 110 | 111 | order = Order.find_by_number(notification.merchant_trade_no) 112 | 113 | if notification.status && notification.checksum_ok? 114 | # payment is compeleted 115 | else 116 | # payment is failed 117 | end 118 | 119 | render text: '1|OK', status: 200 120 | end 121 | ``` 122 | 123 | ## Multiple Merchant Id (dynamic merchant id) 124 | 125 | You don't need to setup `merchant_id` `hash_key` `hash_iv` in `initializers/*.rb`. setup those data in form helper like below: 126 | 127 | ``` erb 128 | <% payment_service_for @order.id, 129 | @order.user.email, 130 | :service => :allpay, 131 | :html => { :id => 'allpay-atm-form', :method => :post } do |service| %> 132 | <% service.merchant_trade_no "#{@order.id}T#{Time.zone.now}" %> 133 | <% service.merchant_id "YOUR MERCHANT_ID HERE" %> 134 | 138 | <% service.hash_key "YOUR HASH KEY HERE" %> 139 | <% service.hash_iv "YOUR HASH IV HERE" %> 140 | <% service.total_amount @order.total.to_i %> 141 | <% service.description @order.id %> 142 | <% service.item_name @order.id %> 143 | <% service.choose_payment OffsitePayments::Integrations::Allpay::PAYMENT_ATM %> 144 | <% service.return_url root_path %> 145 | <% service.notify_url allpay_atm_return_url %> 146 | <% service.encrypted_data %> 147 | <%= submit_tag 'Buy!' %> 148 | <% end %> 149 | ``` 150 | 151 | ## Troublechooting 152 | If you get a error "undefined method \`payment\_service\_for\`", you can add following configurations to initializers/allpay.rb. 153 | ``` ruby 154 | require "offsite_payments/action_view_helper" 155 | ActionView::Base.send(:include, OffsitePayments::ActionViewHelper) 156 | ``` 157 | 158 | Some allpay error due to CSRF token ("authenticity_token is not in spec"), you can add following scripts to remove them manually. 159 | 160 | ``` js 161 | 165 | ``` 166 | 167 | It's caused from payment\_service\_for helper function when generating by [offsite_payments](https://github.com/Shopify/offsite_payments) gem (offsite\_payments/lib/offsite\_payments/action\_view\_helper.rb) 168 | 169 | ## Upgrade Notes 170 | 171 | When upgrading from 0.1.3 and below to any higher versions, you need to make the following changes: 172 | 173 | - the notification initialize with raw post string (instead of a hash of params) 174 | - `return_url()` should be renamed to `notify_url()` (server-side callback url). 175 | 176 | ## Contributing 177 | 178 | 1. Fork it 179 | 2. Create your feature branch (`git checkout -b my-new-feature`) 180 | 3. Commit your changes (`git commit -am 'Add some feature'`) 181 | 4. Push to the branch (`git push origin my-new-feature`) 182 | 5. Create new Pull Request 183 | -------------------------------------------------------------------------------- /lib/offsite_payments/integrations/allpay/helper.rb: -------------------------------------------------------------------------------- 1 | require 'digest/md5' 2 | 3 | module OffsitePayments #:nodoc: 4 | module Integrations #:nodoc: 5 | module Allpay 6 | class Helper < OffsitePayments::Helper 7 | ### 預設介面 8 | 9 | # 廠商編號(由 allpay 提供) 10 | # type: Varchar(10) 11 | # presense: true 12 | # example: 2000132 13 | # description: 14 | mapping :merchant_id, 'MerchantID' 15 | mapping :account, 'MerchantID' # AM common 16 | 17 | # 廠商交易編號 18 | # type: Varchar(20) 19 | # presense: true 20 | # example: allpay1234 21 | # description: 廠商交易編號不可重覆。英數字大小寫混合 22 | mapping :merchant_trade_no, 'MerchantTradeNo' 23 | mapping :order, 'MerchantTradeNo' # AM common 24 | 25 | # 廠商交易時間 26 | # type: Varchar(20) 27 | # presense: true 28 | # example: 2012/03/21 15:40:18 29 | # description: 格式為:yyyy/MM/dd HH:mm:ss 30 | mapping :merchant_trade_date, 'MerchantTradeDate' 31 | 32 | # 交易類型 33 | # type: Varchar(20) 34 | # presense: true 35 | # example: aio 36 | # description: 請帶 aio 37 | mapping :payment_type, 'PaymentType' 38 | 39 | # 交易金額 40 | # type: Varchar(20) 41 | # presense: true 42 | # example: 43 | # description: 44 | mapping :total_amount, 'TotalAmount' 45 | mapping :amount, 'TotalAmount' # AM common 46 | 47 | # 交易描述 48 | # type: Varchar(20) 49 | # presense: true 50 | # example: 51 | # description: 52 | mapping :description, 'TradeDesc' 53 | 54 | # 商品名稱 55 | # type: Varchar(20) 56 | # presense: true 57 | # example: 58 | # description: 59 | mapping :item_name, 'ItemName' 60 | 61 | # 付款完成通知回傳網址 62 | # type: Varchar(20) 63 | # presense: true 64 | # example: 65 | # description: 66 | mapping :notify_url, 'ReturnURL' # AM common 67 | 68 | # 選擇預設付款方式 69 | # type: Varchar(20) 70 | # presense: true 71 | # example: 72 | # description: 73 | mapping :choose_payment, 'ChoosePayment' 74 | 75 | # 檢查碼 76 | # type: Varchar(20) 77 | # presense: true 78 | # example: 79 | # description: 80 | mapping :check_mac_value, 'CheckMacValue' 81 | 82 | # Client 端返回廠商網址 83 | # type: Varchar(20) 84 | # presense: true 85 | # example: 86 | # description: 87 | mapping :client_back_url, 'ClientBackURL' 88 | mapping :return_url, 'ClientBackURL' # AM common 89 | 90 | # 商品銷售網址 91 | mapping :item_url, 'ItemURL' 92 | 93 | # 備註欄位。 94 | mapping :remark, 'Remark' 95 | 96 | # 選擇預設付款子項目 97 | mapping :choose_sub_payment, 'ChooseSubPayment' 98 | 99 | # 付款完成 redirect 的網址 100 | mapping :redirect_url, 'OrderResultURL' 101 | 102 | # 是否需要額外的付款資訊, defalut: N 103 | mapping :need_extra_paid_info, 'NeedExtraPaidInfo' 104 | 105 | # 裝置來源, defalut: P 106 | mapping :devise_source, 'DeviceSource' 107 | 108 | # 忽略的付款方式 109 | mapping :ignore_payment, 'IgnorePayment' 110 | 111 | # 特約合作平台商代號(由allpay提供) 112 | mapping :platform_id, 'PlatformID' 113 | 114 | # 電子發票開註記 115 | mapping :invoice_mark, 'InvoiceMark' 116 | 117 | # 是否延遲撥款, defalut: 0 118 | mapping :hold_trade_amt, 'HoldTradeAMT' 119 | 120 | # CheckMacValue 加密類型, defalut: 0 121 | mapping :encrypt_type, 'EncryptType' 122 | 123 | ### ChoosePayment 為 ATM/CVS/BARCODE 124 | 125 | # ATM, CVS 序號回傳網址 (Server Side) 126 | mapping :payment_info_url, 'PaymentInfoURL' 127 | # ATM, CVS 序號頁面回傳網址 (Client Side) 128 | mapping :client_redirect_url, 'ClientRedirectURL' 129 | mapping :payment_redirect_url, 'ClientRedirectURL' 130 | 131 | ### ATM 132 | 133 | # ATM 允許繳費有效天數 134 | mapping :expire_date, 'ExpireDate' 135 | 136 | ### CVS/BARCODE 137 | 138 | # 超商繳費截止時間 139 | mapping :store_expire_date, 'StoreExpireDate' 140 | # 交易描述1 141 | mapping :desc_1, 'Desc_1' 142 | # 交易描述2 143 | mapping :desc_2, 'Desc_2' 144 | # 交易描述3 145 | mapping :desc_3, 'Desc_3' 146 | # 交易描述4 147 | mapping :desc_4, 'Desc_4' 148 | 149 | ### Alipay 150 | mapping :alipay_item_name, 'AlipayItemName' 151 | mapping :alipay_item_counts, 'AlipayItemCounts' 152 | mapping :alipay_item_price, 'AlipayItemPrice' 153 | mapping :email, 'Email' 154 | mapping :phone_no, 'PhoneNo' 155 | mapping :uder_name, 'UserName' 156 | mapping :expire_time, 'ExpireTime' 157 | 158 | def initialize(order, account, options = {}) 159 | super 160 | add_field 'MerchantID', OffsitePayments::Integrations::Allpay.merchant_id 161 | add_field 'PaymentType', OffsitePayments::Integrations::Allpay::PAYMENT_TYPE 162 | end 163 | 164 | def merchant_trade_date(date) 165 | add_field 'MerchantTradeDate', date.strftime('%Y/%m/%d %H:%M:%S') 166 | end 167 | 168 | def hash_key(key) 169 | @key = key 170 | end 171 | 172 | def hash_iv(iv) 173 | @iv = iv 174 | end 175 | 176 | def merchant_hash_key 177 | @key || OffsitePayments::Integrations::Allpay.hash_key 178 | end 179 | 180 | def merchant_hash_iv 181 | @iv || OffsitePayments::Integrations::Allpay.hash_iv 182 | end 183 | 184 | def encrypted_data 185 | 186 | raw_data = @fields.sort.map{|field, value| 187 | # utf8, authenticity_token, commit are generated from form helper, needed to skip 188 | "#{field}=#{value}" if field!='utf8' && field!='authenticity_token' && field!='commit' 189 | }.join('&') 190 | 191 | hash_raw_data = "HashKey=#{merchant_hash_key}&#{raw_data}&HashIV=#{merchant_hash_iv}" 192 | url_encode_data = self.class.url_encode(hash_raw_data) 193 | url_encode_data.downcase! 194 | 195 | binding.pry if OffsitePayments::Integrations::Allpay.debug 196 | 197 | add_field 'CheckMacValue', Digest::MD5.hexdigest(url_encode_data).upcase 198 | end 199 | 200 | # Allpay .NET url encoding 201 | # Code based from CGI.escape() 202 | # Some special characters (e.g. "()*!") are not escaped on Allpay server when they generate their check sum value, causing CheckMacValue Error. 203 | # 204 | # TODO: The following characters still cause CheckMacValue error: 205 | # '<', "\n", "\r", '&' 206 | def self.url_encode(text) 207 | text = text.dup 208 | text.gsub!(/([^ a-zA-Z0-9\(\)\!\*_.-]+)/) do 209 | '%' + $1.unpack('H2' * $1.bytesize).join('%') 210 | end 211 | text.tr!(' ', '+') 212 | text 213 | end 214 | end 215 | end 216 | end 217 | end 218 | --------------------------------------------------------------------------------