Skip to content
Browse files

Implement payRequest

  • Loading branch information...
1 parent 7195e60 commit 118b9c75d9fa18f673c67adcfbf312074cd894fa @exviva committed
View
1 .gitignore
@@ -0,0 +1 @@
+/spec/support/clickand_buy.yml
View
3 .rspec
@@ -0,0 +1,3 @@
+--color
+--format progress
+--tag ~remote
View
47 Gemfile.lock
@@ -0,0 +1,47 @@
+PATH
+ remote: .
+ specs:
+ activemerchant-clickandbuy (0.1.0)
+ activemerchant
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ active_utils (1.0.5)
+ activesupport (>= 2.3.11)
+ i18n
+ activemerchant (1.28.0)
+ active_utils (>= 1.0.2)
+ activesupport (>= 2.3.11)
+ builder (>= 2.0.0)
+ i18n
+ json (>= 1.5.1)
+ money
+ nokogiri
+ activesupport (3.2.8)
+ i18n (~> 0.6)
+ multi_json (~> 1.0)
+ builder (3.1.3)
+ diff-lcs (1.1.3)
+ i18n (0.6.1)
+ json (1.7.5)
+ money (5.0.0)
+ i18n (~> 0.4)
+ json
+ multi_json (1.3.6)
+ nokogiri (1.5.5)
+ rspec (2.11.0)
+ rspec-core (~> 2.11.0)
+ rspec-expectations (~> 2.11.0)
+ rspec-mocks (~> 2.11.0)
+ rspec-core (2.11.1)
+ rspec-expectations (2.11.3)
+ diff-lcs (~> 1.1.3)
+ rspec-mocks (2.11.3)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ activemerchant-clickandbuy!
+ rspec
View
34 README.md
@@ -18,7 +18,39 @@ Or install it yourself as:
## Usage
-TODO
+To use the gateway, you'll need to provide your merchant ID, project ID and secret key:
+
+ auth = {merchant_id: 'foo', project_id: 'bar', secret_key: 'baz'}
+ gateway = ActiveMerchant::Billing::ClickandBuyGateway.new(auth)
+
+Now you can initiate a transaction at ClickandBuy:
+
+ amount = Money.new(1000, 'EUR')
+ options = {
+ success_url: 'https://www.your-site.com/callback/success', # where to redirect the user on success
+ failure_url: 'https://www.your-site.com/callback/failure', # where to redirect the user on failure
+ order_id: 123, # your unique order identifier
+ ip: '1.2.3.4', # user's IP address
+ order_description: 'ACME Earthquake Pills', # what the user is buying
+ locale: 'en' # user's locale ('en' or 'de')
+ }
+ response = gateway.setup_purchase(amount, options)
+
+The `response` hash will contain amongst other keys two important ones: `transactionID` and `redirectURL`.
+The `transactionID` identifies the transaction at ClickandBuy, make sure you store it together with your order.
+The `redirectURL` is where you redirect the user so that they can confirm the purchase.
+
+## Running specs
+
+Copy `spec/support/clickand_buy.yml.example` to `spec/support/clickand_buy.yml`. Now run:
+
+ bundle exec rspec
+
+By default, only "fast" specs will be running (the API will not be hit). In order to be able
+to execute remote specs, populate the `clickand_buy.yml` file with your staging account's
+authentication credentials. To run the remote specs, run the following:
+
+ bundle exec rspec --tag remote
## Contributing
View
5 activemerchant-clickandbuy.gemspec
@@ -1,4 +1,3 @@
-# encoding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'active_merchant/clickand_buy/version'
@@ -15,4 +14,8 @@ Gem::Specification.new do |gem|
gem.files = `git ls-files`.split($/)
gem.test_files = gem.files.grep(%r{^spec/})
gem.require_paths = ['lib']
+
+ gem.add_runtime_dependency 'activemerchant'
+
+ gem.add_development_dependency 'rspec'
end
View
31 lib/active_merchant/billing/clickand_buy_gateway.rb
@@ -0,0 +1,31 @@
+require 'active_merchant/clickand_buy/request/pay_request'
+
+module ActiveMerchant
+ module Billing
+ class ClickandBuyGateway < Gateway
+ def initialize(auth)
+ requires!(auth, :project_id, :merchant_id, :secret_key)
+ @auth = auth
+ end
+
+ def setup_purchase(amount, options)
+ requires!(options, :success_url, :failure_url, :order_id, :ip, :order_description, :locale)
+ perform(ClickandBuy::Request::PayRequest.new(@auth, amount, options))
+ end
+
+ private
+ def perform(request)
+ response_string = ssl_post(endpoint_url, request.body, headers)
+ request.handle_response(response_string)
+ end
+
+ def headers
+ {'Content-Type' => 'text/xml; charset=UTF-8'}
+ end
+
+ def endpoint_url
+ 'https://api.clickandbuy-s1.com/webservices/soap/pay_1_1_0'
+ end
+ end
+ end
+end
View
60 lib/active_merchant/clickand_buy/request/pay_request.rb
@@ -0,0 +1,60 @@
+module ActiveMerchant
+ module ClickandBuy
+ module Request
+ class PayRequest
+ def initialize(auth, amount, options)
+ @auth = auth
+ @amount = amount
+ @options = options
+ end
+
+ def body
+ xml = Builder::XmlMarkup.new
+ xml.instruct!
+
+ xml.SOAP :Envelope, 'xmlns:SOAP' => 'http://schemas.xmlsoap.org/soap/envelope/' do
+ xml.SOAP :Body do
+ xml.payRequest_Request xmlns: 'http://api.clickandbuy.com/webservices/pay_1_1_0/' do
+ xml.authentication do
+ xml.merchantID @auth[:merchant_id]
+ xml.projectID @auth[:project_id]
+ xml.token token
+ end
+
+ xml.details do
+ xml.amount do
+ xml.amount sprintf('%.2f', @amount.to_f)
+ xml.currency @amount.currency_as_string
+ end
+ xml.orderDetails do
+ xml.text @options[:order_description]
+ end
+ xml.successURL @options[:success_url]
+ xml.failureURL @options[:failure_url]
+ xml.externalID @options[:order_id]
+ xml.consumerIPAddress @options[:ip]
+ xml.consumerLanguage @options[:locale]
+ end
+ end
+ end
+ end
+
+ xml.target!
+ end
+
+ def handle_response(response_string)
+ response = Hash.from_xml(response_string)
+ response['Envelope']['Body']['payRequest_Response']['transaction']
+ end
+
+ private
+ def token
+ timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
+ hash_input = [@auth[:project_id], @auth[:secret_key], timestamp].join('::')
+ hash = Digest::SHA1.hexdigest(hash_input)
+ [timestamp, hash].join('::')
+ end
+ end
+ end
+ end
+end
View
2 lib/activemerchant-clickandbuy.rb
@@ -1 +1,3 @@
+require 'active_merchant'
require 'active_merchant/clickand_buy/version'
+require 'active_merchant/billing/clickand_buy_gateway'
View
46 spec/active_merchant/billing/clickand_buy_gateway_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe ActiveMerchant::Billing::ClickandBuyGateway do
+ let(:auth) { YAML.load_file(File.expand_path('../../../support/clickand_buy.yml', __FILE__)) }
+ let(:gateway) { described_class.new(auth) }
+
+ describe '#initialize' do
+ [:project_id, :merchant_id, :secret_key].each do |key|
+ it "requires #{key} in auth" do
+ expect { described_class.new(auth.except(key)) }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ describe '#setup_purchase' do
+ let(:amount) { Money.new(1000, 'EUR') }
+ let(:order_id) { Time.now.to_i }
+ let(:options) { {success_url: 'http://example.com', failure_url: 'http://example.com', order_id: order_id, ip: '1.2.3.4', order_description: '', locale: 'en'} }
+ subject { gateway.setup_purchase(amount, options) }
+
+ [:success_url, :failure_url, :order_id, :ip, :order_description, :locale].each do |key|
+ context "without #{key} in options" do
+ subject { gateway.setup_purchase(amount, options.except(key)) }
+
+ it 'raises an argument error' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ it 'returns a hash with transaction details', :remote do
+ subject.should be_a(Hash)
+
+ subject['transactionID'].should match(/\d+/)
+ subject['transactionStatus'].should eq('CREATED')
+ subject['transactionType'].should eq('PAY')
+ subject['externalID'].should eq(order_id.to_s)
+
+ URI.parse(subject['redirectURL']).tap do |redirect_url|
+ redirect_url.scheme.should eq('https')
+ redirect_url.host.should eq('checkout.clickandbuy-s1.com')
+ redirect_url.path.should eq('/frontend/secure/checkout')
+ end
+ end
+ end
+end
View
73 spec/active_merchant/clickand_buy/request/pay_request_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe ActiveMerchant::ClickandBuy::Request::PayRequest do
+ let(:url) { proc {|variation| "http://example.com/callback/#{variation}" } }
+ let(:success_url) { url[:success] }
+ let(:failure_url) { url[:failure] }
+ let(:order_id) { Time.now.to_i }
+ let(:ip) { '1.2.3.4' }
+ let(:locale) { 'en' }
+ let(:order_description) { 'Foo bar baz' }
+ let(:success_url) { url[:success] }
+
+ let(:auth) { {merchant_id: 123, project_id: 456, secret_key: 'foo'} }
+ let(:amount) { Money.new(1000, 'EUR') }
+ let(:options) { {success_url: success_url, failure_url: failure_url, order_id: order_id, ip: ip, order_description: order_description, locale: locale} }
+
+ let(:request) { described_class.new(auth, amount, options) }
+
+ describe '#body' do
+ let(:body) { request.body }
+ let(:body_hash) { Hash.from_xml(request.body) }
+ let(:request_hash) { body_hash['Envelope']['Body']['payRequest_Request'] }
+ let(:details) { request_hash['details'] }
+
+ it 'uses the auth argument to provide authentication' do
+ authentication = request_hash['authentication']
+ authentication['merchantID'].should eq('123')
+ authentication['projectID'].should eq('456')
+ authentication['token'].should match(/\A\d{14}::\w{40}\z/)
+ end
+
+ it 'uses the amount argument to provide amount' do
+ amount_tag = details['amount']
+ amount_tag['amount'].should eq(sprintf('%.2f', amount.to_f))
+ amount_tag['currency'].should eq(amount.currency_as_string)
+ end
+
+ it 'uses the order_description option for order details text' do
+ text = details['orderDetails']['text']
+ text.should eq(order_description)
+ end
+
+ it 'uses the success_url option for successURL' do
+ details['successURL'].should eq(success_url)
+ end
+
+ it 'uses the failure_url option for failureURL' do
+ details['failureURL'].should eq(failure_url)
+ end
+
+ it 'uses the order_id option for externalID' do
+ details['externalID'].should eq(order_id.to_s)
+ end
+
+ it 'uses the ip option for consumerIPAddress' do
+ details['consumerIPAddress'].should eq(ip)
+ end
+
+ it 'uses the locale option for consumerLanguage' do
+ details['consumerLanguage'].should eq(locale)
+ end
+ end
+
+ describe '#handle_response' do
+ let(:response_string) { File.read(File.expand_path('../../../../support/requests/pay_request_response.xml', __FILE__)) }
+ subject { request.handle_response(response_string) }
+
+ it 'returns the transaction hash' do
+ subject['transactionID'].should eq('1397737001')
+ subject['redirectURL'].should eq('https://checkout.clickandbuy-s1.com/frontend/secure/checkout?tx=1397737001&s=988809699F35D642&h=09FBBC928EDAA067207D4E69B5EA9CAD441D4812')
+ end
+ end
+end
View
21 spec/spec_helper.rb
@@ -0,0 +1,21 @@
+# This file was generated by the `rspec --init` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# Require this file using `require "spec_helper"` to ensure that it is only
+# loaded once.
+
+require 'activemerchant-clickandbuy'
+require 'money'
+require 'active_support/core_ext/hash/except'
+
+# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+ config.treat_symbols_as_metadata_keys_with_true_values = true
+ config.run_all_when_everything_filtered = true
+ config.filter_run :focus
+
+ # Run specs in random order to surface order dependencies. If you find an
+ # order dependency and want to debug it, you can fix the order by providing
+ # the seed, which is printed after each run.
+ # --seed 1234
+ config.order = 'random'
+end
View
3 spec/support/clickand_buy.yml.example
@@ -0,0 +1,3 @@
+:project_id: 'your project ID'
+:merchant_id: 'your merchant ID'
+:secret_key: 'your secret key'
View
1 spec/support/requests/pay_request_response.xml
@@ -0,0 +1 @@
+<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/><SOAP-ENV:Body><ns2:payRequest_Response xmlns:ns2="http://api.clickandbuy.com/webservices/pay_1_1_0/"><ns2:requestTrackingID>3a121134#m1-s1#20120925165757</ns2:requestTrackingID><ns2:transaction><ns2:transactionID>1397737001</ns2:transactionID><ns2:externalID>1348592270</ns2:externalID><ns2:transactionStatus>CREATED</ns2:transactionStatus><ns2:transactionType>PAY</ns2:transactionType><ns2:redirectURL>https://checkout.clickandbuy-s1.com/frontend/secure/checkout?tx=1397737001&amp;s=988809699F35D642&amp;h=09FBBC928EDAA067207D4E69B5EA9CAD441D4812</ns2:redirectURL></ns2:transaction></ns2:payRequest_Response></SOAP-ENV:Body></SOAP-ENV:Envelope>

0 comments on commit 118b9c7

Please sign in to comment.
Something went wrong with that request. Please try again.