samurai 0.2.4 → 0.2.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/app/views/application/_errors.html.erb +15 -0
- data/app/views/application/_payment_method_form.html.erb +1 -1
- data/app/views/application/_transaction.html.erb +1 -10
- data/app/views/application/_transaction_form.html.erb +1 -1
- data/lib/samurai.rb +3 -1
- data/lib/samurai/base.rb +7 -0
- data/lib/samurai/payment_method.rb +30 -5
- data/lib/samurai/rails.rb +7 -0
- data/lib/samurai/rails/helpers.rb +28 -0
- data/lib/samurai/rails/views.rb +24 -0
- data/lib/samurai/transaction.rb +26 -0
- data/lib/samurai/version.rb +1 -1
- data/{test/spec → spec/lib}/authorization_spec.rb +3 -10
- data/spec/lib/generate_docs_spec.rb +207 -0
- data/{test/spec → spec/lib}/processor_spec.rb +8 -10
- data/{test/spec → spec/lib}/purchase_spec.rb +14 -6
- data/spec/spec_helper.rb +34 -0
- data/spec/support/http_proxy.rb +40 -0
- data/spec/support/response_logger.rb +71 -0
- data/spec/support/transparent_redirect_helper.rb +26 -0
- metadata +24 -16
- data/test/spec_helper.rb +0 -91
@@ -0,0 +1,15 @@
|
|
1
|
+
<% transaction ||= Samurai::Transaction.new %>
|
2
|
+
<% payment_method ||= Samurai::PaymentMethod.new %>
|
3
|
+
<% if transaction.errors.any? || payment_method.errors.any? %>
|
4
|
+
<div id="error_explanation">
|
5
|
+
<h4>This transaction could not be processed:</h4>
|
6
|
+
<ul>
|
7
|
+
<% transaction.errors.full_messages.each do |msg| %>
|
8
|
+
<li><%= msg %></li>
|
9
|
+
<% end %>
|
10
|
+
<% payment_method.errors.full_messages.each do |msg| %>
|
11
|
+
<li><%= msg %></li>
|
12
|
+
<% end %>
|
13
|
+
</ul>
|
14
|
+
</div>
|
15
|
+
<% end %>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<% sandbox ||= false %>
|
2
|
-
<% payment_method ||=
|
2
|
+
<% payment_method ||= Samurai::PaymentMethod.new %>
|
3
3
|
<form action="<%= Samurai.site %>/payment_methods" method="POST">
|
4
4
|
<fieldset>
|
5
5
|
<input name="redirect_url" type="hidden" value="<%= redirect_url %>" />
|
@@ -1,16 +1,7 @@
|
|
1
1
|
<h3><%= transaction.errors.empty? ? 'Successful' : 'Failed' %> <%= transaction.transaction_type.titleize %> Transaction</h3>
|
2
2
|
<h4>Reference ID: <%= transaction.reference_id %></h4>
|
3
3
|
|
4
|
-
|
5
|
-
<div id="error_explanation">
|
6
|
-
<h4>This transaction could not be processed:</h4>
|
7
|
-
<ul>
|
8
|
-
<% transaction.errors.full_messages.each do |msg| %>
|
9
|
-
<li><%= msg %></li>
|
10
|
-
<% end %>
|
11
|
-
</ul>
|
12
|
-
</div>
|
13
|
-
<% end %>
|
4
|
+
<%= render Samurai::Views.errors :transaction=>transaction %>
|
14
5
|
|
15
6
|
<p>
|
16
7
|
<strong>Amount:</strong>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<% authenticity_token ||= '' %>
|
2
|
-
<% transaction ||=
|
2
|
+
<% transaction ||= Samurai::Transaction.new %>
|
3
3
|
<form accept-charset="UTF-8" action="<%= post_url %>" class="new_transaction" id="new_transaction" method="post">
|
4
4
|
<%= authenticity_token %>
|
5
5
|
<input id="processor_token" name="processor_token" type="hidden" value="<%= processor_token %>"/>
|
data/lib/samurai.rb
CHANGED
@@ -30,7 +30,7 @@ module Samurai
|
|
30
30
|
@@options = (value || {}).reverse_merge(DEFAULT_OPTIONS)
|
31
31
|
Samurai::Base.setup_site!
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
end
|
35
35
|
|
36
36
|
require 'samurai/cacheable_by_token'
|
@@ -40,3 +40,5 @@ require 'samurai/payment_method'
|
|
40
40
|
require 'samurai/transaction'
|
41
41
|
require 'samurai/message'
|
42
42
|
require 'samurai/processor_response'
|
43
|
+
|
44
|
+
require 'samurai/rails'
|
data/lib/samurai/base.rb
CHANGED
@@ -11,11 +11,18 @@ class Samurai::Base < ActiveResource::Base
|
|
11
11
|
self.password = Samurai.merchant_password
|
12
12
|
end
|
13
13
|
|
14
|
+
def has_errors?
|
15
|
+
respond_to?(:errors) && !errors.empty?
|
16
|
+
end
|
17
|
+
|
14
18
|
protected
|
15
19
|
|
16
20
|
def load_attributes_from_response(response)
|
17
21
|
super.tap { |instance| instance.process_response_errors }
|
18
22
|
end
|
23
|
+
def self.instantiate_record(record, prefix_options = {})
|
24
|
+
super.tap { |instance| instance.send :process_response_errors }
|
25
|
+
end
|
19
26
|
|
20
27
|
def process_response_errors
|
21
28
|
# Do nothing by default, subclasses may override this to process specific error messages
|
@@ -27,13 +27,38 @@ class Samurai::PaymentMethod < Samurai::Base
|
|
27
27
|
@custom_data ||= self.custom && (JSON.parse(self.custom) rescue {}).symbolize_keys
|
28
28
|
end
|
29
29
|
|
30
|
+
def process_response_errors
|
31
|
+
if respond_to?(:messages) && self.messages
|
32
|
+
self.messages.each do |message|
|
33
|
+
#if (message.respond_to?(:subclass) && message.subclass == 'error')
|
34
|
+
self.errors.add message.context.gsub(/\./, ' '), message.key
|
35
|
+
#end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
protected :process_response_errors
|
30
40
|
|
31
|
-
|
32
|
-
|
33
|
-
|
41
|
+
# Initialize the known attributes from the schema as empty strings, so that they can be accessed via method-missing
|
42
|
+
KNOWN_ATTRIBUTES = [
|
43
|
+
:first_name, :last_name, :address_1, :address_2, :city, :state, :zip,
|
44
|
+
:card_number, :cvv, :expiry_month, :expiry_year
|
45
|
+
]
|
46
|
+
EMPTY_ATTRIBUTES = KNOWN_ATTRIBUTES.inject({}) {|h, k| h[k] = ''; h}
|
47
|
+
def initialize(attrs={})
|
48
|
+
super(EMPTY_ATTRIBUTES.merge(attrs))
|
34
49
|
end
|
35
|
-
|
36
|
-
|
50
|
+
|
51
|
+
# Prepare a new PaymentMethod for use with a transparent redirect form
|
52
|
+
def self.for_transparent_redirect(params)
|
53
|
+
if params[:payment_method_token].blank?
|
54
|
+
Samurai::PaymentMethod.new(params)
|
55
|
+
else
|
56
|
+
Samurai::PaymentMethod.find(params[:payment_method_token]).tap do |pm|
|
57
|
+
pm.card_number = "************#{pm.last_four_digits}"
|
58
|
+
pm.cvv = "***"
|
59
|
+
pm.errors[:base] << 'The card number or CVV are not valid.' if !pm.is_sensitive_data_valid
|
60
|
+
end
|
61
|
+
end
|
37
62
|
end
|
38
63
|
|
39
64
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Samurai::Rails
|
4
|
+
module Helpers
|
5
|
+
|
6
|
+
def setup_for_transparent_redirect(params)
|
7
|
+
@transaction = Samurai::Transaction.find params[:reference_id] unless params[:reference_id].blank?
|
8
|
+
@payment_method = Samurai::PaymentMethod.for_transparent_redirect(params)
|
9
|
+
end
|
10
|
+
|
11
|
+
def load_and_verify_payment_method(params)
|
12
|
+
if params[:payment_method_token].blank?
|
13
|
+
@payment_method = Samurai::PaymentMethod.new :is_sensitive_data_valid=>false
|
14
|
+
else
|
15
|
+
@payment_method = Samurai::PaymentMethod.find params[:payment_method_token]
|
16
|
+
@payment_method = nil if @payment_method && !@payment_method.is_sensitive_data_valid?
|
17
|
+
end
|
18
|
+
@payment_method
|
19
|
+
end
|
20
|
+
|
21
|
+
def payment_method_params
|
22
|
+
{ :payment_method_token => params[:payment_method_token] }.tap do |_params|
|
23
|
+
_params[:reference_id] = @transaction.reference_id if @transaction
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Samurai::Rails
|
4
|
+
class Views
|
5
|
+
class << self
|
6
|
+
|
7
|
+
PARTIALS = [ 'payment_method_form', 'transaction_form', 'errors', 'transaction' ]
|
8
|
+
|
9
|
+
PARTIALS.each do |partial|
|
10
|
+
define_method partial do |attrs|
|
11
|
+
attrs ||= {}
|
12
|
+
{:file=>send("#{partial}_file"), :locals=>attrs}
|
13
|
+
end
|
14
|
+
define_method "#{partial}_file" do
|
15
|
+
Pathname.new(__FILE__).dirname.join('../../..', 'app', 'views', 'application', "_#{partial}.html.erb").to_s
|
16
|
+
end
|
17
|
+
define_method "#{partial}_html" do
|
18
|
+
File.read(send partial)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/samurai/transaction.rb
CHANGED
@@ -27,6 +27,21 @@ class Samurai::Transaction < Samurai::Base
|
|
27
27
|
execute(:credit, {:amount => amount || self.amount}.reverse_merge(options))
|
28
28
|
end
|
29
29
|
|
30
|
+
# Reverse this transaction. First, tries a void.
|
31
|
+
# If a void is unsuccessful, (because the transaction has already settled) perform a credit for the full amount.
|
32
|
+
def reverse(options = {})
|
33
|
+
transaction = void(options)
|
34
|
+
return transaction if transaction.processor_response.success
|
35
|
+
return credit(nil, options)
|
36
|
+
end
|
37
|
+
|
38
|
+
def success?
|
39
|
+
respond_to?(:processor_response) && processor_response.success
|
40
|
+
end
|
41
|
+
def failed?
|
42
|
+
!success?
|
43
|
+
end
|
44
|
+
|
30
45
|
private
|
31
46
|
|
32
47
|
def execute(action, options = {})
|
@@ -62,6 +77,17 @@ class Samurai::Transaction < Samurai::Base
|
|
62
77
|
to_xml(:skip_instruct => true, :root => 'transaction', :dasherize => false)
|
63
78
|
end
|
64
79
|
|
80
|
+
# Initialize the known attributes from the schema as empty strings, so that they can be accessed via method-missing
|
81
|
+
KNOWN_ATTRIBUTES = [
|
82
|
+
:amount, :type, :payment_method_token, :currency_code,
|
83
|
+
:descriptor, :custom, :customer_reference, :billing_reference
|
84
|
+
]
|
85
|
+
EMPTY_ATTRIBUTES = KNOWN_ATTRIBUTES.inject({}) {|h, k| h[k] = ''; h}
|
86
|
+
def initialize(attrs={})
|
87
|
+
super(EMPTY_ATTRIBUTES.merge(attrs))
|
88
|
+
end
|
89
|
+
|
90
|
+
|
65
91
|
require 'pathname'
|
66
92
|
def self.form_html
|
67
93
|
File.read(form_partial_path)
|
data/lib/samurai/version.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
require '
|
1
|
+
require 'spec_helper'
|
2
2
|
|
3
3
|
describe "processing authorizations" do
|
4
4
|
|
5
5
|
before :each do
|
6
|
-
|
7
|
-
@authorization = Samurai::Processor.authorize(
|
6
|
+
payment_method_token = PAYMENT_METHOD_TOKENS[:success]
|
7
|
+
@authorization = Samurai::Processor.authorize(payment_method_token, @@seed)
|
8
8
|
end
|
9
9
|
|
10
10
|
it "should create a new authorization transaction" do
|
@@ -12,39 +12,33 @@ describe "processing authorizations" do
|
|
12
12
|
end
|
13
13
|
|
14
14
|
it "should find the authorization" do
|
15
|
-
register_transaction_response(:method => :get, :path => "transactions/#{@authorization.reference_id}", :type => 'authorize')
|
16
15
|
transaction = Samurai::Transaction.find(@authorization.reference_id)
|
17
16
|
transaction.reference_id.intern.should be_equal(@authorization.reference_id.intern)
|
18
17
|
end
|
19
18
|
|
20
19
|
it "should successfully capture" do
|
21
|
-
register_transaction_response(:method => :post, :path => "transactions/#{@authorization.id}/capture", :type => 'capture')
|
22
20
|
capture = @authorization.capture(@@seed)
|
23
21
|
capture.processor_response.success.should be_true
|
24
22
|
end
|
25
23
|
|
26
24
|
it "should capture an authorization without specifying an amount" do
|
27
|
-
register_transaction_response(:method => :post, :path => "transactions/#{@authorization.id}/capture", :type => 'capture', :amount => @@seed)
|
28
25
|
capture = @authorization.capture
|
29
26
|
capture.amount.intern.should be_equal "#{@@seed}".intern
|
30
27
|
capture.processor_response.success.should be_true
|
31
28
|
end
|
32
29
|
|
33
30
|
it "should partially capture an authorization" do
|
34
|
-
register_transaction_response(:method => :post, :path => "transactions/#{@authorization.id}/capture", :type => 'capture', :amount => @@seed - 1.0)
|
35
31
|
capture = @authorization.capture(@@seed - 1.0)
|
36
32
|
capture.amount.intern.should be_equal "#{@@seed - 1.0}".intern
|
37
33
|
capture.processor_response.success.should be_true
|
38
34
|
end
|
39
35
|
|
40
36
|
it "should void an authorization" do
|
41
|
-
register_transaction_response(:method => :post, :path => "transactions/#{@authorization.id}/void", :type => 'void', :amount => @@seed)
|
42
37
|
void = @authorization.void
|
43
38
|
void.processor_response.success.should be_true
|
44
39
|
end
|
45
40
|
|
46
41
|
it "should credit an authorization for the full amount by default" do
|
47
|
-
register_transaction_response(:method => :post, :path => "transactions/#{@authorization.id}/credit", :type => 'credit', :amount => @@seed, :success => 'false')
|
48
42
|
credit = @authorization.credit
|
49
43
|
credit.amount.intern.should be_equal "#{@@seed}".intern
|
50
44
|
pending "the response is not successful since the authorization hasn't settled" do
|
@@ -53,7 +47,6 @@ describe "processing authorizations" do
|
|
53
47
|
end
|
54
48
|
|
55
49
|
it "should partially credit an authorization" do
|
56
|
-
register_transaction_response(:method => :post, :path => "transactions/#{@authorization.id}/credit", :type => 'credit', :amount => @@seed - 1.0, :success => 'false')
|
57
50
|
credit = @authorization.credit(@@seed - 1.0)
|
58
51
|
credit.amount.intern.should be_equal "#{@@seed - 1.0}".intern
|
59
52
|
pending "the response is not successful since the authorization hasn't settled" do
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "generate documentation" do
|
4
|
+
include TransparentRedirectHelper
|
5
|
+
include ResponseLoggerHelper
|
6
|
+
|
7
|
+
before(:all) do
|
8
|
+
@logger = ResponseLogger.new(File.open('response_log.html', 'w'))
|
9
|
+
end
|
10
|
+
after(:all) do
|
11
|
+
@logger.close!
|
12
|
+
end
|
13
|
+
|
14
|
+
before do
|
15
|
+
@logger.begin_section example.full_description.sub(/generate documentation/, '').titleize
|
16
|
+
|
17
|
+
@params = {
|
18
|
+
'redirect_url' => 'http://test.host',
|
19
|
+
'merchant_key' => Samurai.merchant_key,
|
20
|
+
'custom' => 'custom',
|
21
|
+
'credit_card[first_name]' => 'FirstName',
|
22
|
+
'credit_card[last_name]' => 'LastName',
|
23
|
+
'credit_card[address_1]' => '123 Main St',
|
24
|
+
'credit_card[address_2]' => '',
|
25
|
+
'credit_card[city]' => 'Chicago',
|
26
|
+
'credit_card[state]' => 'IL',
|
27
|
+
'credit_card[zip]' => '60610',
|
28
|
+
'credit_card[card_number]' => '4222222222222',
|
29
|
+
'credit_card[cvv]' => '123',
|
30
|
+
'credit_card[expiry_month]' => '05',
|
31
|
+
'credit_card[expiry_year]' => '2014',
|
32
|
+
}
|
33
|
+
|
34
|
+
Samurai::Base.instance_eval do
|
35
|
+
def connection(refresh = false)
|
36
|
+
if defined?(@connection) || superclass == ActiveResource::Base
|
37
|
+
@connection ||= begin
|
38
|
+
c = HttpProxyConnection.new(site, format)
|
39
|
+
c.proxy = proxy if proxy
|
40
|
+
c.user = user if user
|
41
|
+
c.password = password if password
|
42
|
+
c.auth_type = auth_type if auth_type
|
43
|
+
c.timeout = timeout if timeout
|
44
|
+
c.ssl_options = ssl_options if ssl_options
|
45
|
+
c
|
46
|
+
end
|
47
|
+
else
|
48
|
+
superclass.connection
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
Samurai::Base.connection
|
53
|
+
end
|
54
|
+
|
55
|
+
after do
|
56
|
+
@logger.end_section
|
57
|
+
end
|
58
|
+
|
59
|
+
describe 'with an invalid payment method' do
|
60
|
+
it 'should not create the payment method with missing card_number' do
|
61
|
+
@params.delete 'credit_card[card_number]'
|
62
|
+
data = create_payment_method(@params)
|
63
|
+
log_request_response! data[:request], data[:response]
|
64
|
+
data[:payment_method_token].should be_nil
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should create the payment method with invalid card_number format' do
|
68
|
+
@params['credit_card[card_number]'] = '12345'
|
69
|
+
data = create_payment_method(@params)
|
70
|
+
log_request_response! data[:request], data[:response]
|
71
|
+
data[:payment_method_token].should =~ /^[0-9a-z]{24}$/
|
72
|
+
Samurai::PaymentMethod.find data[:payment_method_token]
|
73
|
+
log_http!
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should create the payment method with blank cvv' do
|
77
|
+
@params['credit_card[cvv]'] = ''
|
78
|
+
data = create_payment_method(@params)
|
79
|
+
log_request_response! data[:request], data[:response]
|
80
|
+
data[:payment_method_token].should =~ /^[0-9a-z]{24}$/
|
81
|
+
Samurai::PaymentMethod.find data[:payment_method_token]
|
82
|
+
log_http!
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'with a valid payment method' do
|
87
|
+
before do
|
88
|
+
@amount = '1.00' # response code: card number is declined
|
89
|
+
@data = create_payment_method(@params)
|
90
|
+
end
|
91
|
+
it 'should create the payment method' do
|
92
|
+
log_request_response! @data[:request], @data[:response]
|
93
|
+
@data[:payment_method_token].should =~ /^[0-9a-z]{24}$/
|
94
|
+
Samurai::PaymentMethod.find @data[:payment_method_token]
|
95
|
+
log_http!
|
96
|
+
end
|
97
|
+
it 'should create a valid transaction' do
|
98
|
+
purchase = Samurai::Processor.purchase(@data[:payment_method_token], @amount)
|
99
|
+
log_http!
|
100
|
+
purchase.processor_response.success.should be_true
|
101
|
+
end
|
102
|
+
it 'should create an invalid transaction' do
|
103
|
+
lambda do
|
104
|
+
Samurai::Processor.purchase(@data[:payment_method_token], '')
|
105
|
+
end.should raise_error(ActiveResource::ServerError)
|
106
|
+
log_http!
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe 'with a declined card' do
|
111
|
+
before do
|
112
|
+
@amount = '3.00' # response code: card number is declined
|
113
|
+
@data = create_payment_method(@params)
|
114
|
+
end
|
115
|
+
it 'should create the payment method' do
|
116
|
+
log_request_response! @data[:request], @data[:response]
|
117
|
+
@data[:payment_method_token].should =~ /^[0-9a-z]{24}$/
|
118
|
+
Samurai::PaymentMethod.find @data[:payment_method_token]
|
119
|
+
log_http!
|
120
|
+
end
|
121
|
+
it 'should create a valid transaction' do
|
122
|
+
purchase = Samurai::Processor.purchase(@data[:payment_method_token], @amount)
|
123
|
+
log_http!
|
124
|
+
purchase.processor_response.success.should be_false
|
125
|
+
end
|
126
|
+
it 'should create an invalid transaction' do
|
127
|
+
lambda do
|
128
|
+
Samurai::Processor.purchase(@data[:payment_method_token], '')
|
129
|
+
end.should raise_error(ActiveResource::ServerError)
|
130
|
+
log_http!
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe 'with an expired card' do
|
135
|
+
before do
|
136
|
+
@amount = '8.00' # response code: card is expired
|
137
|
+
@data = create_payment_method(@params)
|
138
|
+
end
|
139
|
+
it 'should create the payment method' do
|
140
|
+
log_request_response! @data[:request], @data[:response]
|
141
|
+
@data[:payment_method_token].should =~ /^[0-9a-z]{24}$/
|
142
|
+
Samurai::PaymentMethod.find @data[:payment_method_token]
|
143
|
+
log_http!
|
144
|
+
end
|
145
|
+
it 'should create a valid transaction' do
|
146
|
+
purchase = Samurai::Processor.purchase(@data[:payment_method_token], @amount)
|
147
|
+
log_http!
|
148
|
+
purchase.processor_response.success.should be_false
|
149
|
+
end
|
150
|
+
it 'should create an invalid transaction' do
|
151
|
+
lambda do
|
152
|
+
Samurai::Processor.purchase(@data[:payment_method_token], '')
|
153
|
+
end.should raise_error(ActiveResource::ServerError)
|
154
|
+
log_http!
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
describe 'with a card with incorrect cvv' do
|
159
|
+
before do
|
160
|
+
@amount = '6.00' # response code: card number is invalid
|
161
|
+
@data = create_payment_method(@params)
|
162
|
+
end
|
163
|
+
it 'should create the payment method' do
|
164
|
+
log_request_response! @data[:request], @data[:response]
|
165
|
+
@data[:payment_method_token].should =~ /^[0-9a-z]{24}$/
|
166
|
+
Samurai::PaymentMethod.find @data[:payment_method_token]
|
167
|
+
log_http!
|
168
|
+
end
|
169
|
+
it 'should create a valid transaction' do
|
170
|
+
purchase = Samurai::Processor.purchase(@data[:payment_method_token], @amount)
|
171
|
+
log_http!
|
172
|
+
purchase.processor_response.success.should be_false
|
173
|
+
end
|
174
|
+
it 'should create an invalid transaction' do
|
175
|
+
lambda do
|
176
|
+
Samurai::Processor.purchase(@data[:payment_method_token], '')
|
177
|
+
end.should raise_error(ActiveResource::ServerError)
|
178
|
+
log_http!
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
describe 'with an nonexistant card' do
|
183
|
+
before do
|
184
|
+
@params['credit_card[card_number]'] = '4222222222222'
|
185
|
+
@amount = '6.00' # response code: card number is invalid
|
186
|
+
@data = create_payment_method(@params)
|
187
|
+
end
|
188
|
+
it 'should create the payment method' do
|
189
|
+
log_request_response! @data[:request], @data[:response]
|
190
|
+
@data[:payment_method_token].should =~ /^[0-9a-z]{24}$/
|
191
|
+
Samurai::PaymentMethod.find @data[:payment_method_token]
|
192
|
+
log_http!
|
193
|
+
end
|
194
|
+
it 'should create a valid transaction' do
|
195
|
+
purchase = Samurai::Processor.purchase(@data[:payment_method_token], @amount)
|
196
|
+
log_http!
|
197
|
+
purchase.processor_response.success.should be_false
|
198
|
+
end
|
199
|
+
it 'should create an invalid transaction' do
|
200
|
+
lambda do
|
201
|
+
Samurai::Processor.purchase(@data[:payment_method_token], '')
|
202
|
+
end.should raise_error(ActiveResource::ServerError)
|
203
|
+
log_http!
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
@@ -1,24 +1,23 @@
|
|
1
|
-
require '
|
1
|
+
require 'spec_helper'
|
2
2
|
|
3
3
|
describe "Processor actions" do
|
4
|
-
|
4
|
+
before :each do
|
5
|
+
@payment_method_token = PAYMENT_METHOD_TOKENS[:success]
|
6
|
+
end
|
7
|
+
|
5
8
|
it "should return an empty processor" do
|
6
9
|
processor = Samurai::Processor.the_processor
|
7
10
|
processor.should_not be_nil
|
8
11
|
end
|
9
12
|
|
10
13
|
it "should create a new purchase" do
|
11
|
-
|
12
|
-
|
13
|
-
purchase = Samurai::Processor.purchase(PAYMENT_METHOD_TOKEN, @@seed)
|
14
|
+
purchase = Samurai::Processor.purchase(@payment_method_token, @@seed)
|
14
15
|
purchase.processor_response.success.should be_true
|
15
16
|
# FakeWeb.last_request
|
16
17
|
end
|
17
18
|
|
18
19
|
it "should create a new purchase with tracking data" do
|
19
|
-
|
20
|
-
|
21
|
-
purchase = Samurai::Processor.purchase(PAYMENT_METHOD_TOKEN, @@seed, {
|
20
|
+
purchase = Samurai::Processor.purchase(@payment_method_token, @@seed, {
|
22
21
|
:descriptor => "A test purchase",
|
23
22
|
:custom => "some optional custom data",
|
24
23
|
:billing_reference => "ABC123",
|
@@ -29,8 +28,7 @@ describe "Processor actions" do
|
|
29
28
|
end
|
30
29
|
|
31
30
|
it "should create a non-new authorization" do
|
32
|
-
|
33
|
-
authorization = Samurai::Processor.authorize(PAYMENT_METHOD_TOKEN, @@seed)
|
31
|
+
authorization = Samurai::Processor.authorize(@payment_method_token, @@seed)
|
34
32
|
authorization.processor_response.success.should be_true
|
35
33
|
end
|
36
34
|
|
@@ -1,10 +1,10 @@
|
|
1
|
-
require '
|
1
|
+
require 'spec_helper'
|
2
2
|
|
3
3
|
describe "processing purchases" do
|
4
4
|
|
5
5
|
before :each do
|
6
|
-
|
7
|
-
@purchase = Samurai::Processor.purchase(
|
6
|
+
payment_method_token = PAYMENT_METHOD_TOKENS[:success]
|
7
|
+
@purchase = Samurai::Processor.purchase(payment_method_token, @@seed)
|
8
8
|
end
|
9
9
|
|
10
10
|
it "should process successfully" do
|
@@ -12,20 +12,28 @@ describe "processing purchases" do
|
|
12
12
|
end
|
13
13
|
|
14
14
|
it "should be able to void a recent purchase" do
|
15
|
-
register_transaction_response(:method => :post, :path => "transactions/#{@purchase.id}/void", :type => 'void', :success => 'false')
|
16
15
|
void = @purchase.void
|
17
16
|
void.processor_response.success.should be_true
|
18
17
|
end
|
19
18
|
|
20
19
|
it "should not be able to credit a recent purchase" do
|
21
|
-
register_transaction_response(:method => :post, :path => "transactions/#{@purchase.id}/credit", :type => 'void', :success => 'false')
|
22
20
|
credit = @purchase.credit
|
23
21
|
credit.processor_response.success.should be_false
|
24
22
|
end
|
25
23
|
|
24
|
+
it "should be able to reverse a recent purchase" do
|
25
|
+
reverse = @purchase.reverse
|
26
|
+
reverse.processor_response.success.should be_true
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should be able to reverse a settled purchase" do
|
30
|
+
pending "currently we cannot force settle a purchase, so can't test this properly"
|
31
|
+
reverse = @purchase.reverse
|
32
|
+
reverse.processor_response.success.should be_true
|
33
|
+
end
|
34
|
+
|
26
35
|
it "should be able to credit a settled purchase" do
|
27
36
|
pending "currently we cannot force settle a purchase, so can't test this properly" do
|
28
|
-
register_transaction_response(:method => :post, :path => "transactions/#{@purchase.id}/credit", :type => 'void', :success => 'false')
|
29
37
|
credit = @purchase.credit
|
30
38
|
credit.processor_response.success.should be_true
|
31
39
|
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'ruby-debug'
|
3
|
+
require 'pp'
|
4
|
+
Debugger.start
|
5
|
+
Debugger.settings[:autoeval] = true
|
6
|
+
Debugger.settings[:autolist] = 5
|
7
|
+
Debugger.settings[:reload_source_on_change] = true
|
8
|
+
|
9
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
10
|
+
|
11
|
+
SITE = ENV['site'] || 'https://samurai.feefighters.com/v1/'
|
12
|
+
USE_MOCK = !ENV['site']
|
13
|
+
|
14
|
+
PAYMENT_METHOD_TOKENS = {
|
15
|
+
:success => 'b7c966452702282b32a4c65d'
|
16
|
+
}
|
17
|
+
|
18
|
+
RSpec.configure do |c|
|
19
|
+
c.before :all do
|
20
|
+
@@seed = rand(1000).to_f / 100.0
|
21
|
+
end
|
22
|
+
c.before :each do
|
23
|
+
@@seed += 1.0
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'samurai'
|
28
|
+
Samurai.options = {
|
29
|
+
:site => SITE,
|
30
|
+
:merchant_key => ENV['merchant_key'] || 'f4b17359f267915e705fdcb6',
|
31
|
+
:merchant_password => ENV['merchant_password'] || 'd7bf19a8aa1051335b83b349',
|
32
|
+
:processor_token => ENV['processor_token'] || 'c5823b5f1616ed6c0891d167'
|
33
|
+
}
|
34
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'active_resource/connection'
|
2
|
+
|
3
|
+
class HttpProxy
|
4
|
+
attr_accessor :request, :response
|
5
|
+
|
6
|
+
def initialize(http, options={})
|
7
|
+
@http = http
|
8
|
+
@response = nil
|
9
|
+
@request = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing(meth, *args, &block)
|
13
|
+
case meth
|
14
|
+
when :post, :put
|
15
|
+
@request = {
|
16
|
+
:method => meth,
|
17
|
+
:path => args[0],
|
18
|
+
:body => args[1],
|
19
|
+
:headers => args[2],
|
20
|
+
}
|
21
|
+
when :get, :delete
|
22
|
+
@request = {
|
23
|
+
:method => meth,
|
24
|
+
:path => args[0],
|
25
|
+
:headers => args[1],
|
26
|
+
}
|
27
|
+
end
|
28
|
+
@response = @http.send(meth, *args, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def respond_to?(meth)
|
32
|
+
super || @http.respond_to?(meth)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class HttpProxyConnection < ActiveResource::Connection
|
37
|
+
def http
|
38
|
+
@http ||= HttpProxy.new(configure_http(new_http))
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "erb"
|
2
|
+
|
3
|
+
class ResponseLogger
|
4
|
+
include ERB::Util
|
5
|
+
|
6
|
+
def initialize(io)
|
7
|
+
@sections = []
|
8
|
+
@io = io
|
9
|
+
@io.puts '<div class="wrapper">'
|
10
|
+
end
|
11
|
+
|
12
|
+
def close!
|
13
|
+
@io.puts '</div>'
|
14
|
+
@sections.each do |section|
|
15
|
+
@io.puts "<a href='##{section[:id]}'>#{section[:name]}</a><br>"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def begin_section(name)
|
20
|
+
@sections << {:name=>name, :id=>h(name.parameterize)}
|
21
|
+
@io.puts "<article class='example span-8' id='#{h name.parameterize}'>"
|
22
|
+
@io.puts " <h3>#{h name}</h3>"
|
23
|
+
end
|
24
|
+
|
25
|
+
def end_section
|
26
|
+
@io.puts "</article>"
|
27
|
+
@io.puts "<hr>"
|
28
|
+
end
|
29
|
+
|
30
|
+
def log(request, response, options={})
|
31
|
+
@io.puts ' <div class="code http-request"><em class="lang">HTTP Request</em>'
|
32
|
+
@io.puts " <pre><strong>#{h request[:method].to_s.upcase} #{h request[:path]}</strong><br>"
|
33
|
+
@io.puts "Headers: #{h request[:headers].inject({}) {|h, (k,v)| h[k] = (v.is_a?(Array) ? v.first : v); h }}</pre>"
|
34
|
+
@io.puts ' </div>'
|
35
|
+
if request[:body]
|
36
|
+
@io.puts ' <div class="code xml">'
|
37
|
+
@io.puts ' <em class="lang">XML Payload</em>'
|
38
|
+
@io.puts " <pre class='prettyprint lang-xml'>#{h request[:body]}</pre>"
|
39
|
+
@io.puts ' </div>'
|
40
|
+
end
|
41
|
+
@io.puts ' <div class="code http-response">'
|
42
|
+
@io.puts " <em class='lang'>HTTP Response: #{h response.code}</em>"
|
43
|
+
@io.puts " <pre class='prettyprint lang-xml'><code>#{h response.body}</code></pre>"
|
44
|
+
@io.puts " </div>"
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
module ResponseLoggerHelper
|
50
|
+
def log_http! options={}
|
51
|
+
@logger.log Samurai::Base.connection.http.request,
|
52
|
+
Samurai::Base.connection.http.response,
|
53
|
+
options
|
54
|
+
end
|
55
|
+
|
56
|
+
def log_request_response! request, response, options={}
|
57
|
+
def request.[](v)
|
58
|
+
case v
|
59
|
+
when :method
|
60
|
+
return self.method
|
61
|
+
when :path
|
62
|
+
return self.path
|
63
|
+
when :headers
|
64
|
+
return self.to_hash
|
65
|
+
else
|
66
|
+
super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
@logger.log request, response, options
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module TransparentRedirectHelper
|
5
|
+
|
6
|
+
def create_payment_method(params = {}, options={})
|
7
|
+
url = Samurai.site + 'payment_methods'
|
8
|
+
url.sub! %r{https://}, "https://#{Samurai.merchant_key}:#{Samurai.merchant_password}@"
|
9
|
+
|
10
|
+
uri = URI.parse url
|
11
|
+
req = Net::HTTP::Post.new uri.path
|
12
|
+
req.set_form_data params
|
13
|
+
req.basic_auth uri.user, uri.password
|
14
|
+
|
15
|
+
res = Net::HTTP.new(uri.host, uri.port)
|
16
|
+
res.use_ssl = true
|
17
|
+
|
18
|
+
response = res.start {|http| http.request(req) }
|
19
|
+
{
|
20
|
+
:payment_method_token => response['Location'] && response['Location'].sub(%r{#{Regexp.escape params['redirect_url']}\?payment_method_token=}, ''),
|
21
|
+
:response => response,
|
22
|
+
:request => req,
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: samurai
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,11 +10,11 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2011-08-
|
13
|
+
date: 2011-08-18 00:00:00.000000000Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activeresource
|
17
|
-
requirement: &
|
17
|
+
requirement: &70227589695120 !ruby/object:Gem::Requirement
|
18
18
|
none: false
|
19
19
|
requirements:
|
20
20
|
- - ! '>='
|
@@ -22,10 +22,10 @@ dependencies:
|
|
22
22
|
version: 2.2.2
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
|
-
version_requirements: *
|
25
|
+
version_requirements: *70227589695120
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: bundler
|
28
|
-
requirement: &
|
28
|
+
requirement: &70227589694660 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
31
31
|
- - ! '>='
|
@@ -33,10 +33,10 @@ dependencies:
|
|
33
33
|
version: 1.0.0
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
|
-
version_requirements: *
|
36
|
+
version_requirements: *70227589694660
|
37
37
|
- !ruby/object:Gem::Dependency
|
38
38
|
name: rspec
|
39
|
-
requirement: &
|
39
|
+
requirement: &70227589694200 !ruby/object:Gem::Requirement
|
40
40
|
none: false
|
41
41
|
requirements:
|
42
42
|
- - ! '>='
|
@@ -44,10 +44,10 @@ dependencies:
|
|
44
44
|
version: 2.6.0
|
45
45
|
type: :development
|
46
46
|
prerelease: false
|
47
|
-
version_requirements: *
|
47
|
+
version_requirements: *70227589694200
|
48
48
|
- !ruby/object:Gem::Dependency
|
49
49
|
name: fakeweb
|
50
|
-
requirement: &
|
50
|
+
requirement: &70227589693820 !ruby/object:Gem::Requirement
|
51
51
|
none: false
|
52
52
|
requirements:
|
53
53
|
- - ! '>='
|
@@ -55,10 +55,10 @@ dependencies:
|
|
55
55
|
version: '0'
|
56
56
|
type: :development
|
57
57
|
prerelease: false
|
58
|
-
version_requirements: *
|
58
|
+
version_requirements: *70227589693820
|
59
59
|
- !ruby/object:Gem::Dependency
|
60
60
|
name: ruby-debug19
|
61
|
-
requirement: &
|
61
|
+
requirement: &70227589693320 !ruby/object:Gem::Requirement
|
62
62
|
none: false
|
63
63
|
requirements:
|
64
64
|
- - ! '>='
|
@@ -66,7 +66,7 @@ dependencies:
|
|
66
66
|
version: '0'
|
67
67
|
type: :development
|
68
68
|
prerelease: false
|
69
|
-
version_requirements: *
|
69
|
+
version_requirements: *70227589693320
|
70
70
|
description: If you are an online merchant and using samurai.feefighters.com, this
|
71
71
|
gem will make your life easy. Integrate with the samurai.feefighters.com portal
|
72
72
|
and process transaction.
|
@@ -83,6 +83,7 @@ files:
|
|
83
83
|
- Gemfile.lock
|
84
84
|
- README.markdown
|
85
85
|
- Rakefile
|
86
|
+
- app/views/application/_errors.html.erb
|
86
87
|
- app/views/application/_payment_method_form.html.erb
|
87
88
|
- app/views/application/_transaction.html.erb
|
88
89
|
- app/views/application/_transaction_form.html.erb
|
@@ -93,13 +94,20 @@ files:
|
|
93
94
|
- lib/samurai/payment_method.rb
|
94
95
|
- lib/samurai/processor.rb
|
95
96
|
- lib/samurai/processor_response.rb
|
97
|
+
- lib/samurai/rails.rb
|
98
|
+
- lib/samurai/rails/helpers.rb
|
99
|
+
- lib/samurai/rails/views.rb
|
96
100
|
- lib/samurai/transaction.rb
|
97
101
|
- lib/samurai/version.rb
|
98
102
|
- samurai.gemspec
|
99
|
-
-
|
100
|
-
-
|
101
|
-
-
|
102
|
-
-
|
103
|
+
- spec/lib/authorization_spec.rb
|
104
|
+
- spec/lib/generate_docs_spec.rb
|
105
|
+
- spec/lib/processor_spec.rb
|
106
|
+
- spec/lib/purchase_spec.rb
|
107
|
+
- spec/spec_helper.rb
|
108
|
+
- spec/support/http_proxy.rb
|
109
|
+
- spec/support/response_logger.rb
|
110
|
+
- spec/support/transparent_redirect_helper.rb
|
103
111
|
homepage: http://rubygems.org/gems/samurai
|
104
112
|
licenses: []
|
105
113
|
post_install_message:
|
data/test/spec_helper.rb
DELETED
@@ -1,91 +0,0 @@
|
|
1
|
-
require 'ruby-debug'
|
2
|
-
Debugger.start
|
3
|
-
Debugger.settings[:autoeval] = true
|
4
|
-
Debugger.settings[:autolist] = 5
|
5
|
-
Debugger.settings[:reload_source_on_change] = true
|
6
|
-
|
7
|
-
SITE = ENV['site'] || 'http://localhost:3002/v1/'
|
8
|
-
USE_MOCK = !ENV['site']
|
9
|
-
PAYMENT_METHOD_TOKEN = ENV['payment_method_token'] || 'asdf'
|
10
|
-
|
11
|
-
RSpec.configure do |c|
|
12
|
-
c.before :all do
|
13
|
-
@@seed = rand(1000).to_f / 100.0
|
14
|
-
end
|
15
|
-
c.before :each do
|
16
|
-
@@seed += 1.0
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
require 'fakeweb'
|
21
|
-
FakeWeb.allow_net_connect = !USE_MOCK
|
22
|
-
|
23
|
-
require 'samurai'
|
24
|
-
Samurai.options = {
|
25
|
-
:site => SITE,
|
26
|
-
:merchant_key => ENV['merchant_key'] || 'e62c5a006cdd9908234193bc',
|
27
|
-
:merchant_password => ENV['merchant_password'] || '18e87d97b3a44b56fe07497e4812f14555db69df9e6ca16f',
|
28
|
-
:processor_token => ENV['processor_token'] || 'af762c3499f77c5f181650a7'
|
29
|
-
}
|
30
|
-
|
31
|
-
def register_transaction_response(options)
|
32
|
-
return unless USE_MOCK
|
33
|
-
|
34
|
-
options.symbolize_keys!
|
35
|
-
|
36
|
-
method = options[:method] && options[:method].to_sym || :post
|
37
|
-
type = options[:type]
|
38
|
-
path = options[:path] || "processors/af762c3499f77c5f181650a7/#{type}"
|
39
|
-
payment_method_token = options[:payment_method_token] || PAYMENT_METHOD_TOKEN
|
40
|
-
amount = options[:amount] || 15.00
|
41
|
-
success = options[:success].blank? ? true : options[:success]
|
42
|
-
|
43
|
-
FakeWeb.register_uri(method,
|
44
|
-
"http://e62c5a006cdd9908234193bc:18e87d97b3a44b56fe07497e4812f14555db69df9e6ca16f@localhost:3002/v1/#{path}.xml",
|
45
|
-
:body => <<-EOF
|
46
|
-
<transaction>
|
47
|
-
<reference_id>3dcFjTC7LDjIjTY3nkKjBVZ8qkZ</reference_id>
|
48
|
-
<transaction_token>53VFyQKYBmN9vKfA9mHCTs79L9a</transaction_token>
|
49
|
-
<created_at type="datetime">2011-04-22T17:57:56Z</created_at>
|
50
|
-
<descriptor>Custom descriptor here if your processor supports it.</descriptor>
|
51
|
-
<custom>Any value you like.</custom>
|
52
|
-
<transaction_type>#{type}</transaction_type>
|
53
|
-
<amount>#{amount}</amount>
|
54
|
-
<currency_code>USD</currency_code>
|
55
|
-
<processor_token>af762c3499f77c5f181650a7</processor_token>
|
56
|
-
<processor_response>
|
57
|
-
<success type="boolean">#{success}</success>
|
58
|
-
<messages type="array">
|
59
|
-
<message class="error" context="processor.avs" key="country_not_supported" />
|
60
|
-
<message class="error" context="input.cvv" key="too_short" />
|
61
|
-
</messages>
|
62
|
-
</processor_response>
|
63
|
-
<payment_method>
|
64
|
-
<payment_method_token>#{payment_method_token}</payment_method_token>
|
65
|
-
<created_at type="datetime">2011-02-12T20:20:46Z</created_at>
|
66
|
-
<updated_at type="datetime">2011-04-22T17:57:30Z</updated_at>
|
67
|
-
<custom>Any value you want us to save with this payment method.</custom>
|
68
|
-
<is_retained type="boolean">true</is_retained>
|
69
|
-
<is_redacted type="boolean">false</is_redacted>
|
70
|
-
<is_sensitive_data_valid type="boolean">true</is_sensitive_data_valid>
|
71
|
-
<messages type="array">
|
72
|
-
<message class="error" context="input.cvv" key="too_long" />
|
73
|
-
<message class="error" context="input.card_number" key="failed_checksum" />
|
74
|
-
</messages>
|
75
|
-
<last_four_digits>1111</last_four_digits>
|
76
|
-
<card_type>visa</card_type>
|
77
|
-
<first_name>Bob</first_name>
|
78
|
-
<last_name>Smith</last_name>
|
79
|
-
<expiry_month type="integer">1</expiry_month>
|
80
|
-
<expiry_year type="integer">2020</expiry_year>
|
81
|
-
<address_1 nil="true"></address_1>
|
82
|
-
<address_2 nil="true"></address_2>
|
83
|
-
<city nil="true"></city>
|
84
|
-
<state nil="true"></state>
|
85
|
-
<zip nil="true"></zip>
|
86
|
-
<country nil="true"></country>
|
87
|
-
</payment_method>
|
88
|
-
</transaction>
|
89
|
-
EOF
|
90
|
-
)
|
91
|
-
end
|