atpay_ruby 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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