atpay_ruby 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/lib/atpay/card.rb ADDED
@@ -0,0 +1,6 @@
1
+ module AtPay
2
+ class Card
3
+ attr_accessor :token
4
+ # TODO: Accessors and utilities for fetching card details
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ module AtPay
2
+ class EmailAddress < Struct.new(:name, :address)
3
+ end
4
+ end
@@ -0,0 +1,15 @@
1
+ module AtPay
2
+ # TODO: differentiation on the transaction errors
3
+
4
+ class Error < RuntimeError; end;
5
+ class FatalError < Error; end;
6
+ class InvalidSignatureError < Error; end;
7
+ class TransactionError < Error; end;
8
+ class ProcessorError < TransactionError; end;
9
+ class EmailReservedError < TransactionError; end;
10
+ class EmailNotRegisteredError < TransactionError; end;
11
+ class AddressMismatch < TransactionError; end;
12
+ class OfferExpiredError < TransactionError; end;
13
+ class DuplicateTokenError < TransactionError; end;
14
+ class DuplicateGroupError < TransactionError; end;
15
+ end
data/lib/atpay/hook.rb ADDED
@@ -0,0 +1,34 @@
1
+ require 'openssl'
2
+ require 'multi_json'
3
+
4
+ module AtPay
5
+ class Hook
6
+ def initialize(session, params)
7
+ @session = session
8
+ @details = params['details']
9
+ @signature = params['signature']
10
+
11
+ verify_signature!
12
+ verify_success!
13
+ end
14
+
15
+ def details
16
+ MultiJson.load(@details)
17
+ end
18
+
19
+ private
20
+ def verify_signature!
21
+ unless OpenSSL::HMAC.hexdigest('sha1', @session.private_key, @details) == @signature
22
+ raise InvalidSignatureError
23
+ end
24
+ end
25
+
26
+ def verify_success!
27
+ if @details['type'] == 'error'
28
+ raise Error.new(@details['error'])
29
+ elsif @details['type'] == 'fatal'
30
+ raise FatalError.new(@details['error'])
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,6 @@
1
+ require 'rails'
2
+
3
+ module AtPay
4
+ class Railtie < Rails::Railtie
5
+ end
6
+ end
@@ -0,0 +1,17 @@
1
+ module AtPay
2
+ class Session < Struct.new(:partner_id, :public_key, :private_key)
3
+ attr_accessor :endpoint
4
+
5
+ def atpay_public_key=(atpay_public_key)
6
+ @atpay_public_key = Base64.decode64(atpay_public_key)
7
+ end
8
+
9
+ def atpay_public_key
10
+ @atpay_public_key || PUBLIC_KEY
11
+ end
12
+
13
+ def endpoint
14
+ @endpoint || "https://dashboard.atpay.com"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ require 'atpay/token/core'
2
+ require 'atpay/token/encoder'
3
+
4
+ module AtPay
5
+ module Token
6
+ class Bulk < Core
7
+ def initialize(session, amount, custom_data = {})
8
+ super
9
+
10
+ self.session = session
11
+ self.amount = amount
12
+ self.user_data.custom_data = custom_data
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,122 @@
1
+ require 'ostruct'
2
+ require 'multi_json'
3
+ require 'atpay/token/registration'
4
+
5
+ module AtPay
6
+ module Token
7
+ class Core
8
+ attr_accessor :session # AtPay::Session instance
9
+ attr_accessor :version # nil: Auth + Capture / 1: Validation (deprecated) / 2: Authorization Only
10
+ attr_accessor :amount # Dollar amount to capture
11
+ attr_accessor :url
12
+ attr_accessor :email_address
13
+ attr_accessor :user_data
14
+ attr_accessor :expires
15
+ attr_accessor :group
16
+
17
+ def initialize(*args)
18
+ self.version = nil
19
+ self.expires_in_seconds = (86400*14) # two weeks
20
+ self.user_data = OpenStruct.new
21
+ end
22
+
23
+ def email_address
24
+ if @email_address.is_a? String
25
+ email_address = EmailAddress.new
26
+ email_address.address = @email_address
27
+ email_address
28
+ else
29
+ @email_address
30
+ end
31
+ end
32
+
33
+ def name=(name)
34
+ self.user_data.name = name
35
+ end
36
+
37
+ def name
38
+ self.user_data.name
39
+ end
40
+
41
+ def url=(url)
42
+ self.user_data.url = url
43
+ @url = url
44
+ end
45
+
46
+ def expires_in_seconds=(seconds)
47
+ self.expires = Time.now.to_i + seconds
48
+ end
49
+
50
+ def estimated_fulfillment_days=(days)
51
+ self.auth_only!
52
+ self.user_data.fulfillment = days
53
+ end
54
+
55
+ def requires_shipping_address?
56
+ address_options.include?('shipping')
57
+ end
58
+
59
+ def requires_shipping_address=(v)
60
+ v ? add_address_option('shipping') : remove_address_option('shipping')
61
+ end
62
+
63
+ def requires_billing_address?
64
+ address_options.include?('billing')
65
+ end
66
+
67
+ def requires_billing_address=(v)
68
+ v ? add_address_option('billing') : remove_address_option('billing')
69
+ end
70
+
71
+ def custom_user_data=(str)
72
+ self.user_data.custom_user_data = str
73
+ end
74
+
75
+ def set_item_details=(item_details)
76
+ self.user_data.item_details = item_details
77
+ end
78
+
79
+ def set_item_quantity=(qty)
80
+ self.user_data.quantity = qty
81
+ end
82
+
83
+ def request_custom_data!(name, options={})
84
+ self.user_data.custom_fields ||= []
85
+ self.user_data.custom_fields << { name: name, required: !!options[:required] }
86
+ end
87
+
88
+ def auth_only!
89
+ self.version = 2
90
+ end
91
+
92
+ def register!
93
+ AtPay::Token::Registration.new(session, to_s)
94
+ end
95
+
96
+ def to_s
97
+ AtPay::Token::Encoder.new(session, version, amount, email_address, expires, url, encoded_user_data, group).email
98
+ end
99
+
100
+ private
101
+ def address_options
102
+ self.user_data.address.split(',')
103
+ rescue
104
+ []
105
+ end
106
+
107
+ def remove_address_option(address_option)
108
+ options = (address_options - [address_option])
109
+ self.user_data.address = options.uniq.join(',')
110
+ end
111
+
112
+ def add_address_option(address_option)
113
+ options = (address_options << address_option)
114
+ self.user_data.address = options.uniq.join(',')
115
+ end
116
+
117
+ def encoded_user_data
118
+ MultiJson.dump(user_data.to_h)
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,81 @@
1
+ require 'atpay'
2
+ require 'rbnacl'
3
+ require 'base64'
4
+ require 'securerandom'
5
+ require 'atpay/email_address'
6
+ require 'atpay/card'
7
+
8
+ module AtPay
9
+ module Token
10
+ class Encoder < Struct.new(:session, :version, :amount, :target, :expires, :url, :user_data, :group)
11
+ def email
12
+ version_and_encode(nonce, partner_frame, body_frame)
13
+ end
14
+
15
+ def site(remote_addr, headers)
16
+ version_and_encode(nonce, partner_frame, site_frame(remote_addr, headers), body_frame)
17
+ end
18
+
19
+ private
20
+ def version_and_encode(*frames)
21
+ "@#{version_tag}#{Base64.urlsafe_encode64(frames.join)}@"
22
+ ensure
23
+ @nonce = nil
24
+ end
25
+
26
+ def version_tag
27
+ version ? (Base64.urlsafe_encode64([version].pack("Q>")) + '~') : nil
28
+ end
29
+
30
+ def site_frame(remote_addr, headers)
31
+ message = boxer.box(nonce, Digest::SHA1.hexdigest([
32
+ headers["HTTP_USER_AGENT"],
33
+ headers["HTTP_ACCEPT_LANGUAGE"],
34
+ headers["HTTP_ACCEPT_CHARSET"],
35
+ remote_addr
36
+ ].join))
37
+
38
+ [[message.length].pack("l>"), message,
39
+ [remote_addr.length].pack("l>"), remote_addr].join
40
+ end
41
+
42
+ def partner_frame
43
+ [session.partner_id].pack("Q>")
44
+ end
45
+
46
+ def body_frame
47
+ boxer.box(nonce, crypted_frame)
48
+ end
49
+
50
+ def crypted_frame
51
+ [target_tag, options_group, '/', options_frame, '/', user_data].flatten.compact.join
52
+ end
53
+
54
+ def options_frame
55
+ [amount, expires].pack("g l>")
56
+ end
57
+
58
+ def options_group
59
+ ":#{group || SecureRandom.hex(5)}"
60
+ end
61
+
62
+ def target_tag
63
+ if target.is_a? EmailAddress
64
+ "email<#{target.address}>"
65
+ elsif target.is_a? Card
66
+ "card<#{target.token}>"
67
+ else
68
+ "url<#{self.url}>"
69
+ end
70
+ end
71
+
72
+ def boxer
73
+ RbNaCl::Box.new(session.atpay_public_key, Base64.decode64(session.private_key))
74
+ end
75
+
76
+ def nonce
77
+ @nonce ||= SecureRandom.random_bytes(24)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,17 @@
1
+ require 'atpay/token/core'
2
+ require 'atpay/token/encoder'
3
+
4
+ module AtPay
5
+ module Token
6
+ class Invoice < Core
7
+ def initialize(session, amount, email_address, custom_data={})
8
+ super
9
+
10
+ self.session = session
11
+ self.amount = amount
12
+ self.email_address = email_address
13
+ self.user_data.custom_data = custom_data
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ require 'httpi'
2
+
3
+ module AtPay
4
+ module Token
5
+ class Registration < Struct.new(:session, :token)
6
+ def initialize(*args)
7
+ super(*args)
8
+ registration # The registration should occur even if we don't access a url or id
9
+ end
10
+
11
+ def url
12
+ registration['url']
13
+ end
14
+
15
+ def id
16
+ registration['id']
17
+ end
18
+
19
+ def short
20
+ "atpay://#{id}"
21
+ end
22
+
23
+ private
24
+ def registration
25
+ @registration ||= (
26
+ request = HTTPI::Request.new("#{session.endpoint}/token/registrations")
27
+ request.body = { token: self.token }
28
+
29
+ response = HTTPI.post(request)
30
+ MultiJson.load(response.body)
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+ require 'atpay/button'
3
+
4
+ describe AtPay::Button do
5
+ let(:token) { 'xyz' }
6
+ let(:amount) { 20.00 }
7
+ let(:merchant_name) { 'Toys for Code' }
8
+ let(:yahoo_providers) { %w(test@yahoo.com test@ymail.com test@rocketmail.com) }
9
+
10
+ it 'renders without exception' do
11
+ button = AtPay::Button.new(token, amount, merchant_name)
12
+ expect(button.render).to_not be_nil
13
+ end
14
+
15
+ context 'when using a yahoo provider email address' do
16
+ it 'renders the wrap_yahoo template with the wrap' do
17
+ button = AtPay::Button.new(token, amount, merchant_name, wrap:true)
18
+ allow(button).to receive(:provider).and_return(:yahoo)
19
+ expect(button.send(:template_name)).to eq('wrap_yahoo.liquid')
20
+ end
21
+
22
+ it 'renders the yahoo template without the wrap' do
23
+ button = AtPay::Button.new(token, amount, merchant_name, wrap:false)
24
+ allow(button).to receive(:provider).and_return(:yahoo)
25
+ expect(button.send(:template_name)).to eq('yahoo.liquid')
26
+ end
27
+ end
28
+
29
+ context 'when using a standard email address' do
30
+ it 'renders the wrap_yahoo template with the wrap' do
31
+ button = AtPay::Button.new(token, amount, merchant_name, wrap:true)
32
+ allow(button).to receive(:provider).and_return(:default)
33
+ expect(button.send(:template_name)).to eq('wrap_default.liquid')
34
+ end
35
+
36
+ it 'renders the yahoo template without the wrap' do
37
+ button = AtPay::Button.new(token, amount, merchant_name, wrap:false)
38
+ allow(button).to receive(:provider).and_return(:default)
39
+ expect(button.send(:template_name)).to eq('default.liquid')
40
+ end
41
+ end
42
+ end
data/spec/hook_spec.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+ require 'atpay'
3
+ require 'multi_json'
4
+ require 'securerandom'
5
+
6
+ describe AtPay::Hook do
7
+ let(:partner_private_key) { '1ED8952DAA4B863DA9EECDFBE8F1FA' }
8
+ let(:params) { { 'details' => details, 'signature' => signature } }
9
+ let(:details) { '{"type":"charge.sale","transaction":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","partner":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","balance":0.0,"unit_price":50.0,"quantity":1,"date":1373388625,"user":null,"card":"Yjc0YzA1ZDkxZFHbuNyMpQA=","email":"johnsmith@example.com","name":"John Smith","user_data":null,"referrer_context":"my-data-ref-50"}' }
10
+ let(:signature) { '28b91cf0482304c6bfe36fc2b84d2c50867a3e05' }
11
+
12
+ context 'when the signature is invalid' do
13
+ it 'should raise an exception' do
14
+ session = AtPay::Session.new('', '', '1234BADKEY')
15
+
16
+ expect {
17
+ AtPay::Hook.new(session, params)
18
+ }.to raise_error(AtPay::InvalidSignatureError)
19
+ end
20
+ end
21
+
22
+ context 'when the signature is valid' do
23
+ it 'make the details available' do
24
+ session = AtPay::Session.new('', '', partner_private_key)
25
+ hook = AtPay::Hook.new(session, params)
26
+ expect(hook.details).to be_a_kind_of(Hash)
27
+ end
28
+ end
29
+ end