spree_affirm 0.2.19 → 0.2.20

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +15 -0
  3. data/Gemfile +19 -0
  4. data/LICENSE +26 -0
  5. data/README.md +47 -0
  6. data/Rakefile +15 -0
  7. data/Versionfile +5 -0
  8. data/app/assets/javascripts/spree/backend/spree_affirm.js +3 -0
  9. data/app/assets/javascripts/spree/frontend/spree_affirm.js +2 -0
  10. data/app/assets/stylesheets/spree/backend/spree_affirm.css +4 -0
  11. data/app/assets/stylesheets/spree/frontend/spree_affirm.css +4 -0
  12. data/app/controllers/spree/affirm_controller.rb +127 -0
  13. data/app/models/affirm/address_validator.rb +25 -0
  14. data/app/models/spree/affirm_checkout.rb +147 -0
  15. data/app/models/spree/gateway/affirm.rb +58 -0
  16. data/app/views/spree/admin/log_entries/_affirm.html.erb +4 -0
  17. data/app/views/spree/admin/log_entries/index.html.erb +28 -0
  18. data/app/views/spree/admin/payments/source_forms/_affirm.html.erb +19 -0
  19. data/app/views/spree/admin/payments/source_views/_affirm.html.erb +12 -0
  20. data/app/views/spree/checkout/affirm/_learn_more.html.erb +0 -0
  21. data/app/views/spree/checkout/payment/_affirm.html.erb +190 -0
  22. data/bin/rails +7 -0
  23. data/config/locales/en.yml +31 -0
  24. data/config/routes.rb +5 -0
  25. data/db/migrate/20140514194315_create_affirm_checkout.rb +10 -0
  26. data/lib/active_merchant/billing/affirm.rb +161 -0
  27. data/lib/generators/spree_affirm/install/install_generator.rb +21 -0
  28. data/lib/spree_affirm.rb +2 -0
  29. data/lib/spree_affirm/engine.rb +27 -0
  30. data/lib/spree_affirm/factories.rb +5 -0
  31. data/lib/spree_affirm/factories/affirm_checkout_factory.rb +219 -0
  32. data/lib/spree_affirm/factories/affirm_payment_method_factory.rb +8 -0
  33. data/lib/spree_affirm/factories/payment_factory.rb +10 -0
  34. data/lib/spree_affirm/version.rb +3 -0
  35. data/spec/controllers/affirm_controller_spec.rb +138 -0
  36. data/spec/lib/active_merchant/billing/affirm_spec.rb +294 -0
  37. data/spec/models/affirm_address_validator_spec.rb +13 -0
  38. data/spec/models/spree_affirm_checkout_spec.rb +349 -0
  39. data/spec/models/spree_gateway_affirm_spec.rb +137 -0
  40. data/spec/spec_helper.rb +98 -0
  41. data/spree_affirm.gemspec +30 -0
  42. metadata +53 -7
@@ -0,0 +1,58 @@
1
+ module Spree
2
+ class Gateway::Affirm < Gateway
3
+ preference :api_key, :string
4
+ preference :secret_key, :string
5
+ preference :server, :string, default: 'www.affirm.com'
6
+ preference :product_key, :string
7
+
8
+ def provider_class
9
+ ActiveMerchant::Billing::Affirm
10
+ end
11
+
12
+ def payment_source_class
13
+ Spree::AffirmCheckout
14
+ end
15
+
16
+ def source_required?
17
+ true
18
+ end
19
+
20
+ def method_type
21
+ 'affirm'
22
+ end
23
+
24
+ def actions
25
+ %w{capture void credit}
26
+ end
27
+
28
+ def supports?(source)
29
+ source.is_a? payment_source_class
30
+ end
31
+
32
+ def self.version
33
+ Gem::Specification.find_by_name('spree_affirm').version.to_s
34
+ end
35
+
36
+ def cancel(charge_ari)
37
+ _payment = Spree::Payment.valid.where(
38
+ response_code: charge_ari,
39
+ source_type: "#{payment_source_class}"
40
+ ).first
41
+
42
+ return if _payment.nil?
43
+
44
+ if _payment.pending?
45
+ _payment.void_transaction!
46
+
47
+ elsif _payment.completed? and _payment.can_credit?
48
+
49
+ # create adjustment
50
+ _payment.order.adjustments.create label: "Refund - Canceled Order", amount: -_payment.credit_allowed.to_f
51
+ _payment.order.update!
52
+
53
+ _payment.credit!
54
+
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,4 @@
1
+ <tr>
2
+ <td><%= Spree.t(:message, :scope => [:log_entry, :affirm]) %></td>
3
+ <td><%= entry.parsed_details.message %></td>
4
+ </tr>
@@ -0,0 +1,28 @@
1
+ <%= render :partial => 'spree/admin/shared/order_tabs', locals: { current: 'Payments' }%>
2
+
3
+ <% content_for :page_title do %>
4
+ <i class="icon-arrow-right"></i>
5
+ <%= I18n.t(:one, scope: "activerecord.models.spree/payment") %>
6
+ <i class="icon-arrow-right"></i>
7
+ <%= Spree.t(:log_entries) %>
8
+ <% end %>
9
+
10
+ <% content_for :page_actions do %>
11
+ <li><%= button_link_to Spree.t(:logs), spree.admin_order_payment_log_entries_url(@order, @payment), :icon => 'icon-archive' %></li>
12
+ <li><%= button_link_to Spree.t(:back_to_payment), spree.admin_order_payment_url(@order, @payment), :icon => 'icon-arrow-left' %></li>
13
+ <% end %>
14
+
15
+ <table class='index' id='listing_log_entries'>
16
+ <% @log_entries.each do |entry| %>
17
+ <thead>
18
+ <tr class="log_entry <%= entry.parsed_details.success? ? 'success' : 'fail' %>">
19
+ <td colspan='2'>
20
+ <h4><i class='icon icon-<%= entry.parsed_details.success? ? 'ok-circle' : 'remove-sign' %>'></i> <%= pretty_time(entry.created_at) %></h4>
21
+ </td>
22
+ </tr>
23
+ </thead>
24
+ <tbody>
25
+ <%= render "spree/admin/log_entries/#{@payment.payment_method.method_type.gsub(' ', '').underscore}", entry: entry rescue render "spree/admin/log_entries/#{@payment.payment_method.name.gsub(' ', '').underscore}", entry: entry %>
26
+ </tbody>
27
+ <% end %>
28
+ </table>
@@ -0,0 +1,19 @@
1
+ <script>
2
+ function showAffirmError(method_id) {
3
+ if (method_id === AffirmPaymentMethodID) {
4
+ $('.payment-method-settings').prepend("<strong id=\"affirm-warning\">You cannot charge Affirm accounts through the admin backend at this time.</strong>");
5
+ } else {
6
+ $('#affirm-warning').remove();
7
+ }
8
+ }
9
+ AffirmPaymentMethodID = "<%= payment_method.id %>"
10
+
11
+ $(function() {
12
+ var checked_method_id = $('[data-hook="payment_method_field"] input[type="radio"]:checked').val();
13
+ showAffirmError(checked_method_id);
14
+
15
+ $('[data-hook="payment_method_field"] input[type="radio"]').click(function (e) {
16
+ showAffirmError($(e.target).val());
17
+ });
18
+ })
19
+ </script>
@@ -0,0 +1,12 @@
1
+ <fieldset data-hook="credit_card">
2
+ <legend align="center">Affirm Payment</legend>
3
+
4
+ <div class="row">
5
+ <div class="alpha six columns">
6
+ <dl>
7
+ <dt>Charge Id:</dt>
8
+ <dd><%= payment.response_code %></dd>
9
+ </dl>
10
+ </div>
11
+ </div>
12
+ </fieldset>
@@ -0,0 +1,190 @@
1
+ <div class="affirm-learn-more-section">
2
+ <%= render partial: 'spree/checkout/affirm/learn_more' %>
3
+ </div>
4
+
5
+ <script>
6
+ (function(){
7
+ /* only include this setup once */
8
+ if (!window.AffirmPaymentMethods) {
9
+
10
+ /*****************************************************\
11
+ Include the affirm js snippet
12
+ \*****************************************************/
13
+ var _affirm_config = {
14
+ public_api_key: "<%= payment_method.preferred_api_key %>",
15
+ script: "https://<%= payment_method.preferred_server %>/js/v2/affirm.js"
16
+ };
17
+ (function(l,g,m,e,a,f,b){var d,c=l[m]||{},h=document.createElement(f),n=document.getElementsByTagName(f)[0],k=function(a,b,c){return function(){a[b]._.push([c,arguments])}};c[e]=k(c,e,"set");d=c[e];c[a]={};c[a]._=[];d._=[];c[a][b]=k(c,a,b);a=0;for(b="set add save post open empty reset on off trigger ready setProduct".split(" ");a<b.length;a++)d[b[a]]=k(c,e,b[a]);a=0;for(b=["get","token","url","items"];a<b.length;a++)d[b[a]]=function(){};h.async=!0;h.src=g[f];n.parentNode.insertBefore(h,n);delete g[f];d(g);l[m]=c})(window,_affirm_config,"affirm","checkout","ui","script","ready");
18
+
19
+ /*****************************************************\
20
+ set the shared checkout data
21
+ \*****************************************************/
22
+ affirm.checkout({
23
+ total: <%= (@order.total * 100).to_i %>,
24
+ currency: "USD",
25
+ tax_amount: <%= (@order.additional_tax_total * 100).to_i %>,
26
+ checkout_id: "<%= @order.number %>",
27
+ discount_code: "<%= @order.coupon_code %>",
28
+ shipping_type: "<%= @order.shipments.first.shipping_method.name if @order.shipments %>",
29
+ shipping_amount: <%= (@order.shipment_total * 100).to_i %>,
30
+
31
+ shipping: {
32
+ name: {
33
+ full: "<%= @order.ship_address.full_name %>",
34
+ },
35
+ address: {
36
+ line1: "<%= @order.ship_address.address1 %>",
37
+ line2: "<%= @order.ship_address.address2 %>",
38
+ city: "<%= @order.ship_address.city %>",
39
+ state: "<%= @order.ship_address.state_text %>",
40
+ country: "<%= @order.ship_address.country.iso %>",
41
+ zipcode: "<%= @order.ship_address.zipcode %>",
42
+ }
43
+ },
44
+
45
+ billing: {
46
+ email: "<%= @order.email %>",
47
+ name: {
48
+ full: "<%= @order.bill_address.full_name %>"
49
+ },
50
+ address: {
51
+ line1: "<%= @order.bill_address.address1 %>",
52
+ line2: "<%= @order.bill_address.address2 %>",
53
+ city: "<%= @order.bill_address.city %>",
54
+ state: "<%= @order.bill_address.state_text %>",
55
+ country: "<%= @order.bill_address.country.iso %>",
56
+ zipcode: "<%= @order.bill_address.zipcode %>",
57
+ }
58
+ },
59
+
60
+
61
+ meta: {
62
+ source: {
63
+ client_name: "spree_affirm",
64
+ version: "<%= Spree::Gateway::Affirm.version %>",
65
+ data: {
66
+ <% if spree_current_user %>
67
+ order_count: "<%= spree_current_user.orders.complete.count %>",
68
+ account_created: "<%= spree_current_user.created_at %>",
69
+
70
+ <% if spree_current_user.orders.complete.any? %>
71
+ last_order_date: "<%= spree_current_user.orders.complete.last.completed_at %>",
72
+ <% end %>
73
+
74
+ <% end %>
75
+ is_logged_in: <%= !!spree_current_user %>,
76
+ spree_version: "<%= Spree.version %>"
77
+ }
78
+ }
79
+ },
80
+
81
+ merchant: {
82
+ user_confirmation_url: "<%= confirm_affirm_url(:payment_method_id => payment_method.id) %>",
83
+ user_cancel_url: "<%= cancel_affirm_url(:payment_method_id => payment_method.id) %>",
84
+ },
85
+
86
+ config: {
87
+ required_billing_fields: "name,address,email",
88
+ },
89
+
90
+
91
+ <% if @order.promotions.any? %>
92
+ discounts: {
93
+ <% @order.promotions.each do |promo| %>
94
+ "<%= promo.code %>": {
95
+ discount_amount: <%= (0-promo.credits.sum(:amount)*100).to_i %>,
96
+ discount_display_name: "<%= promo.name %>"
97
+ },
98
+ <% end %>
99
+ },
100
+ <% end %>
101
+
102
+ items: [
103
+ <% @order.line_items.each do |item| %>
104
+ {
105
+ <% if item.variant.images.any? %>
106
+ item_image_url: "<%= URI.join(root_url, item.variant.images.first.attachment.url(:large)) %>",
107
+ <% elsif item.variant.product.images.any? %>
108
+ item_image_url: "<%= URI.join(root_url, item.variant.product.images.first.attachment.url(:large)) %>",
109
+ <% end %>
110
+
111
+ qty: <%= item.quantity %>,
112
+ sku: "<%= item.variant.sku %>",
113
+ item_url: "<%= product_url(item.product) %>",
114
+ unit_price: <%= item.price * 100 %>,
115
+ display_name: "<%= raw(item.variant.product.name) %>"
116
+ },
117
+ <% end %>
118
+ ]
119
+ });
120
+
121
+
122
+ /* wait for the DOM to be ready */
123
+ affirm.ui.ready(function(){
124
+ $(function() {
125
+
126
+ /*****************************************************\
127
+ setup loading and cancel events for the form
128
+ \*****************************************************/
129
+ affirm.checkout.on("cancel", function(){
130
+ $("#checkout_form_payment input.disabled")
131
+ .attr("disabled", false)
132
+ .removeClass("disabled");
133
+ });
134
+
135
+ var button_text = $("#checkout_form_payment input").val();
136
+
137
+ $("#checkout_form_payment input[type='submit']").on("loading", function(){
138
+ button_text = $(this).val();
139
+ $(this).val("Loading...");
140
+ })
141
+
142
+ .on("done_loading", function(){
143
+ $(this).val(button_text);
144
+ });
145
+
146
+
147
+
148
+ /*****************************************************\
149
+ handle continue button clicks with .open()
150
+ \*****************************************************/
151
+ $('#checkout_form_payment').submit(function(e){
152
+ var checkedPaymentMethod = $('div[data-hook="checkout_payment_step"] input[type="radio"]:checked').val();
153
+
154
+ if (window.AffirmPaymentMethods[checkedPaymentMethod]) {
155
+ var $submit_button = $(this).find("input[type='submit']");
156
+
157
+ // update with checkout method details
158
+ affirm.checkout(window.AffirmPaymentMethods[checkedPaymentMethod]);
159
+
160
+ // show the loading message
161
+ $submit_button.trigger("loading");
162
+
163
+ // submit the checkout
164
+ affirm.checkout.open({
165
+ target: $submit_button
166
+ });
167
+
168
+ e.preventDefault();
169
+ return false;
170
+ }
171
+ });
172
+ });
173
+ });
174
+
175
+ window.AffirmPaymentMethods = {};
176
+ }
177
+
178
+
179
+ /*****************************************************\
180
+ set the product/button specific data to be
181
+ used when the continue button is directly
182
+ clicked
183
+ \*****************************************************/
184
+ window.AffirmPaymentMethods["<%= payment_method.id %>"] = {
185
+ public_api_key: "<%= payment_method.preferred_api_key %>",
186
+ financial_product_key: "<%= payment_method.preferred_product_key %>"
187
+ };
188
+
189
+ }());
190
+ </script>
data/bin/rails ADDED
@@ -0,0 +1,7 @@
1
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
2
+
3
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
4
+ ENGINE_PATH = File.expand_path('../../lib/spree_affirm/engine', __FILE__)
5
+
6
+ require 'rails/all'
7
+ require 'rails/engine/commands'
@@ -0,0 +1,31 @@
1
+ # Sample localization file for English. Add more files in this directory for other locales.
2
+ # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3
+
4
+ en:
5
+ spree:
6
+ affirm_checkout: "Affirm Checkout"
7
+ activerecord:
8
+ models:
9
+ "spree/affirm_checkout":
10
+ "Order size mismatch": "Order size mismatch"
11
+ "Line Item not in checkout details": "Line Item not in checkout details"
12
+ "Quantity mismatch": "Quantity mismatch"
13
+ "Price mismatch": "Price mismatch"
14
+ "Billing email mismatch": "Billing email mismatch"
15
+ "Product key mismatch": "Product key mismatch"
16
+ "city mismatch": "city mismatch"
17
+ "street1 mismatch": "street1 mismatch"
18
+ "street2 mismatch": "street2 mismatch"
19
+ "postal_code mismatch": "postal_code mismatch"
20
+ "region1_code mismatch": "region1_code mismatch"
21
+ "First/Last name mismatch": "First/Last name mismatch"
22
+ "Full name mismatch": "Full name mismatch"
23
+
24
+ attributes:
25
+ "spree/affirm_checkout":
26
+ line_items: "items"
27
+ billing_address: "billing address"
28
+ shipping_address: "shipping address"
29
+ billing_email: "email"
30
+ financial_product_key: "Affirm Financial Product"
31
+
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ Spree::Core::Engine.routes.draw do
2
+ # Add your extension routes here
3
+ post '/affirm/confirm', :to => "affirm#confirm", :as => :confirm_affirm
4
+ get '/affirm/cancel', :to => "affirm#cancel", :as => :cancel_affirm
5
+ end
@@ -0,0 +1,10 @@
1
+ class CreateAffirmCheckout < ActiveRecord::Migration
2
+ def change
3
+ create_table :spree_affirm_checkouts do |t|
4
+ t.string :token
5
+ t.references :order
6
+ t.references :payment_method
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,161 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ class Affirm < Gateway
4
+ self.supported_countries = %w(US)
5
+ self.default_currency = 'USD'
6
+ self.money_format = :cents
7
+
8
+ def initialize(options = {})
9
+ requires!(options, :api_key, :secret_key, :server)
10
+ @api_key = options[:api_key]
11
+ @secret_key = options[:secret_key]
12
+ super
13
+ end
14
+
15
+ def set_charge(charge_id)
16
+ @charge_id = charge_id
17
+ end
18
+
19
+ def authorize(money, affirm_source, options = {})
20
+ result = commit(:post, "", {"checkout_token"=>affirm_source.token}, options, true)
21
+ return result unless result.success?
22
+
23
+ if amount(money).to_i != result.params["amount"].to_i
24
+ return Response.new(false,
25
+ "Auth amount does not match charge amount",
26
+ result.params
27
+ )
28
+ elsif result.params["pending"].to_s != "true"
29
+ return Response.new(false,
30
+ "There was an error authorizing this Charge",
31
+ result.params
32
+ )
33
+ end
34
+ result
35
+ end
36
+
37
+ # To create a charge on a card or a token, call
38
+ #
39
+ # purchase(money, card_hash_or_token, { ... })
40
+ #
41
+ # To create a charge on a customer, call
42
+ #
43
+ # purchase(money, nil, { :customer => id, ... })
44
+ def purchase(money, affirm_source, options = {})
45
+ result = authorize(money, affirm_source, options)
46
+ return result unless result.success?
47
+ capture(money, @charge_id, options)
48
+ end
49
+
50
+ def capture(money, charge_source, options = {})
51
+ post = {:amount => amount(money)}
52
+ set_charge(charge_source)
53
+ result = commit(:post, "#{@charge_id}/capture", post, options)
54
+ return result unless result.success?
55
+
56
+ if amount(money).to_i != result.params["amount"].to_i
57
+ return Response.new(false,
58
+ "Capture amount does not match charge amount",
59
+ result.params
60
+ )
61
+ end
62
+ result
63
+ end
64
+
65
+ def void(charge_source, options = {})
66
+ set_charge(charge_source)
67
+ commit(:post, "#{@charge_id}/void", {}, options)
68
+ end
69
+
70
+ def refund(money, charge_source, options = {})
71
+ post = {:amount => amount(money)}
72
+ set_charge(charge_source)
73
+ commit(:post, "#{@charge_id}/refund", post, options)
74
+ end
75
+
76
+ def credit(money, charge_source, options = {})
77
+ set_charge(charge_source)
78
+ return Response.new(true ,
79
+ "Credited Zero amount",
80
+ {},
81
+ :authorization => @charge_id,
82
+ ) unless money > 0
83
+ refund(money, charge_source, options)
84
+ end
85
+
86
+ def root_url
87
+ "#{root_api_url}charges/"
88
+ end
89
+
90
+ def root_api_url
91
+ "https://#{@options[:server]}/api/v2/"
92
+ end
93
+
94
+ def headers
95
+ {
96
+ "Content-Type" => "application/json",
97
+ "Authorization" => "Basic " + Base64.encode64(@api_key.to_s + ":" + @secret_key.to_s).gsub(/\n/, '').strip,
98
+ "User-Agent" => "Affirm/v1 ActiveMerchantBindings",
99
+ }
100
+ end
101
+
102
+ def parse(body)
103
+ JSON.parse(body)
104
+ end
105
+
106
+ def post_data(params)
107
+ return nil unless params
108
+ params.to_json
109
+ end
110
+
111
+ def response_error(raw_response)
112
+ begin
113
+ parse(raw_response)
114
+ rescue JSON::ParserError
115
+ json_error(raw_response)
116
+ end
117
+ end
118
+
119
+ def json_error(raw_response)
120
+ msg = 'Invalid response. Please contact affirm if you continue to receive this message.'
121
+ msg += " (The raw response returned by the API was #{raw_response.inspect})"
122
+ {
123
+ "error" => {
124
+ "message" => msg
125
+ }
126
+ }
127
+ end
128
+
129
+ def get_checkout(checkout_token)
130
+ _url = root_api_url + "checkout/#{checkout_token}"
131
+ _raw_response = ssl_request :get, _url, nil, headers
132
+
133
+ parse(_raw_response)
134
+ end
135
+
136
+ def commit(method, url, parameters=nil, options = {}, ret_charge=false)
137
+ raw_response = response = nil
138
+ success = false
139
+ begin
140
+ raw_response = ssl_request(method, root_url + url, post_data(parameters), headers)
141
+ response = parse(raw_response)
142
+ success = !response.key?("status_code") && (!ret_charge || response.key?("id"))
143
+ rescue ResponseError => e
144
+ raw_response = e.response.body
145
+ response = response_error(raw_response)
146
+ rescue JSON::ParserError
147
+ response = json_error(raw_response)
148
+ end
149
+
150
+ if success && ret_charge
151
+ @charge_id = response["id"]
152
+ end
153
+ Response.new(success,
154
+ success ? "Transaction approved" : response["message"],
155
+ response,
156
+ :authorization => @charge_id,
157
+ )
158
+ end
159
+ end
160
+ end
161
+ end