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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +2 -0
- data/README.md +277 -0
- data/assets/button/templates/default.liquid +72 -0
- data/assets/button/templates/mailto_body.liquid +8 -0
- data/assets/button/templates/wrap_default.liquid +130 -0
- data/assets/button/templates/wrap_yahoo.liquid +175 -0
- data/assets/button/templates/yahoo.liquid +111 -0
- data/assets/imgs/sample_button.png +0 -0
- data/atpay_ruby.gemspec +27 -0
- data/bin/atpay +58 -0
- data/lib/atpay.rb +14 -0
- data/lib/atpay/button.rb +125 -0
- data/lib/atpay/card.rb +6 -0
- data/lib/atpay/email_address.rb +4 -0
- data/lib/atpay/error.rb +15 -0
- data/lib/atpay/hook.rb +34 -0
- data/lib/atpay/railtie.rb +6 -0
- data/lib/atpay/session.rb +17 -0
- data/lib/atpay/token/bulk.rb +16 -0
- data/lib/atpay/token/core.rb +122 -0
- data/lib/atpay/token/encoder.rb +81 -0
- data/lib/atpay/token/invoice.rb +17 -0
- data/lib/atpay/token/registration.rb +35 -0
- data/spec/button_spec.rb +42 -0
- data/spec/hook_spec.rb +29 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/token/bulk_spec.rb +28 -0
- data/spec/token/core_spec.rb +62 -0
- data/spec/token/encoder_spec.rb +86 -0
- data/spec/token/invoice_spec.rb +20 -0
- data/spec/token/registration.rb +28 -0
- metadata +198 -0
data/lib/atpay/card.rb
ADDED
data/lib/atpay/error.rb
ADDED
@@ -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,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
|
data/spec/button_spec.rb
ADDED
@@ -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
|