solidus_paypal_braintree 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +26 -0
  3. data/README.md +196 -0
  4. data/Rakefile +30 -0
  5. data/app/assets/javascripts/spree/backend/solidus_paypal_braintree.js +66 -0
  6. data/app/assets/javascripts/spree/braintree_hosted_form.js +98 -0
  7. data/app/assets/javascripts/spree/checkout/braintree.js +60 -0
  8. data/app/assets/javascripts/spree/frontend/paypal_button.js +182 -0
  9. data/app/assets/javascripts/spree/frontend/solidus_paypal_braintree.js +188 -0
  10. data/app/assets/stylesheets/spree/backend/solidus_paypal_braintree.scss +28 -0
  11. data/app/assets/stylesheets/spree/frontend/solidus_paypal_braintree.css +16 -0
  12. data/app/controllers/solidus_paypal_braintree/client_tokens_controller.rb +21 -0
  13. data/app/helpers/braintree_admin_helper.rb +18 -0
  14. data/app/models/application_record.rb +3 -0
  15. data/app/models/solidus_paypal_braintree/configuration.rb +5 -0
  16. data/app/models/solidus_paypal_braintree/customer.rb +4 -0
  17. data/app/models/solidus_paypal_braintree/gateway.rb +323 -0
  18. data/app/models/solidus_paypal_braintree/response.rb +52 -0
  19. data/app/models/solidus_paypal_braintree/source.rb +73 -0
  20. data/app/models/solidus_paypal_braintree/transaction.rb +30 -0
  21. data/app/models/solidus_paypal_braintree/transaction_address.rb +66 -0
  22. data/app/models/solidus_paypal_braintree/transaction_import.rb +92 -0
  23. data/app/models/spree/store_decorator.rb +11 -0
  24. data/app/overrides/admin_navigation_menu.rb +6 -0
  25. data/app/views/spree/shared/_braintree_hosted_fields.html.erb +26 -0
  26. data/config/initializers/braintree.rb +1 -0
  27. data/config/locales/en.yml +30 -0
  28. data/config/routes.rb +12 -0
  29. data/db/migrate/20160830061749_create_solidus_paypal_braintree_sources.rb +16 -0
  30. data/db/migrate/20160906201711_create_solidus_paypal_braintree_customers.rb +11 -0
  31. data/db/migrate/20161114231422_create_solidus_paypal_braintree_configurations.rb +11 -0
  32. data/db/migrate/20161125172005_add_braintree_configuration_to_stores.rb +9 -0
  33. data/db/migrate/20170203191030_add_credit_card_to_braintree_configuration.rb +6 -0
  34. data/db/migrate/20170505193712_add_null_constraint_to_sources.rb +30 -0
  35. data/db/migrate/20170508085402_add_not_null_constraint_to_sources_payment_type.rb +11 -0
  36. data/lib/controllers/backend/solidus_paypal_braintree/configurations_controller.rb +30 -0
  37. data/lib/controllers/frontend/solidus_paypal_braintree/checkouts_controller.rb +27 -0
  38. data/lib/controllers/frontend/solidus_paypal_braintree/transactions_controller.rb +61 -0
  39. data/lib/generators/solidus_paypal_braintree/install/install_generator.rb +37 -0
  40. data/lib/solidus_paypal_braintree.rb +10 -0
  41. data/lib/solidus_paypal_braintree/country_mapper.rb +35 -0
  42. data/lib/solidus_paypal_braintree/engine.rb +53 -0
  43. data/lib/solidus_paypal_braintree/factories.rb +18 -0
  44. data/lib/solidus_paypal_braintree/version.rb +3 -0
  45. data/lib/views/backend/solidus_paypal_braintree/configurations/_admin_tab.html.erb +3 -0
  46. data/lib/views/backend/solidus_paypal_braintree/configurations/list.html.erb +30 -0
  47. data/lib/views/backend/spree/admin/payments/source_forms/_paypal_braintree.html.erb +16 -0
  48. data/lib/views/backend/spree/admin/payments/source_views/_paypal_braintree.html.erb +34 -0
  49. data/lib/views/backend_v1.2/spree/admin/payments/source_forms/_paypal_braintree.html.erb +16 -0
  50. data/lib/views/frontend/spree/checkout/payment/_paypal_braintree.html.erb +52 -0
  51. metadata +350 -0
@@ -0,0 +1,30 @@
1
+ require 'active_model'
2
+
3
+ module SolidusPaypalBraintree
4
+ class Transaction
5
+ include ActiveModel::Model
6
+
7
+ attr_accessor :nonce, :payment_method, :payment_type, :address, :email, :phone
8
+
9
+ validates :nonce, presence: true
10
+ validates :payment_method, presence: true
11
+ validates :payment_type, presence: true
12
+ validates :phone, presence: true
13
+ validates :email, presence: true
14
+
15
+ validate do
16
+ unless payment_method.is_a? SolidusPaypalBraintree::Gateway
17
+ errors.add(:payment_method, 'Must be braintree')
18
+ end
19
+ if address && !address.valid?
20
+ address.errors.each do |field, error|
21
+ errors.add(:address, "#{field} #{error}")
22
+ end
23
+ end
24
+ end
25
+
26
+ def address_attributes=(attributes)
27
+ self.address = TransactionAddress.new attributes
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,66 @@
1
+ require 'active_model'
2
+
3
+ module SolidusPaypalBraintree
4
+ class TransactionAddress
5
+ include ActiveModel::Model
6
+ include ActiveModel::Validations::Callbacks
7
+ include SolidusPaypalBraintree::CountryMapper
8
+
9
+ attr_accessor :country_code, :last_name, :first_name,
10
+ :city, :zip, :state_code, :address_line_1, :address_line_2
11
+
12
+ validates :first_name, :last_name, :address_line_1, :city, :zip,
13
+ :state_code, :country_code, presence: true
14
+
15
+ before_validation do
16
+ self.country_code = country_code.presence || "us"
17
+ end
18
+
19
+ validates :spree_country, presence: true
20
+ validates :spree_state, presence: true, if: :should_match_state_model?
21
+
22
+ def initialize(attributes = {})
23
+ country_name = attributes.delete(:country_name) || ""
24
+ if attributes[:country_code].blank?
25
+ attributes[:country_code] = iso_from_name(country_name)
26
+ end
27
+
28
+ super(attributes)
29
+ end
30
+
31
+ def spree_country
32
+ country_code && (@country ||= Spree::Country.find_by(iso: country_code.upcase))
33
+ end
34
+
35
+ def spree_state
36
+ spree_country && state_code && ( @state ||= spree_country.states.where(
37
+ Spree::State.arel_table[:name].matches(state_code).or(
38
+ Spree::State.arel_table[:abbr].matches(state_code)
39
+ )
40
+ ).first )
41
+ end
42
+
43
+ def to_spree_address
44
+ address = Spree::Address.new first_name: first_name,
45
+ last_name: last_name,
46
+ city: city,
47
+ country: spree_country,
48
+ address1: address_line_1,
49
+ address2: address_line_2,
50
+ zipcode: zip
51
+
52
+ if spree_state
53
+ address.state = spree_state
54
+ else
55
+ address.state_name = state_code
56
+ end
57
+ address
58
+ end
59
+
60
+ # Check to see if this address should match to a state
61
+ # model in the database.
62
+ def should_match_state_model?
63
+ spree_country.present? && spree_country.states.any?
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,92 @@
1
+ require 'active_model'
2
+
3
+ module SolidusPaypalBraintree
4
+ class TransactionImport
5
+ class InvalidImportError < StandardError; end
6
+
7
+ include ActiveModel::Model
8
+
9
+ validate do
10
+ errors.add("Address", "is invalid") if address && !address.valid?
11
+
12
+ if !transaction.valid?
13
+ transaction.errors.each do |field, error|
14
+ errors.add(field, error)
15
+ end
16
+ end
17
+ errors.none?
18
+ end
19
+
20
+ attr_reader :transaction, :order
21
+
22
+ def initialize(order, transaction)
23
+ @order = order
24
+ @transaction = transaction
25
+ end
26
+
27
+ def source
28
+ SolidusPaypalBraintree::Source.new nonce: transaction.nonce,
29
+ payment_type: transaction.payment_type,
30
+ payment_method: transaction.payment_method,
31
+ user: user
32
+ end
33
+
34
+ def user
35
+ order.user
36
+ end
37
+
38
+ def import!(end_state)
39
+ if valid?
40
+ order.email = user.try!(:email) || transaction.email
41
+
42
+ if address
43
+ order.shipping_address = order.billing_address = address
44
+ # work around a bug in most solidus versions
45
+ # about tax zone cachine between address changes
46
+ order.instance_variable_set("@tax_zone", nil)
47
+ end
48
+
49
+ payment = order.payments.new source: source,
50
+ payment_method: transaction.payment_method,
51
+ amount: order.total
52
+
53
+ order.save!
54
+ advance_order(payment, end_state)
55
+ else
56
+ raise InvalidImportError,
57
+ "Validation failed: #{errors.full_messages.join(', ')}"
58
+ end
59
+ end
60
+
61
+ def address
62
+ transaction.address && transaction.address.to_spree_address.tap do |address|
63
+ address.phone = transaction.phone
64
+ end
65
+ end
66
+
67
+ def state_before_current?(state)
68
+ steps = order.checkout_steps
69
+ steps.index(state) < (steps.index(order.state) || 0)
70
+ end
71
+
72
+ protected
73
+
74
+ def advance_order(payment, end_state)
75
+ return if state_before_current?(end_state)
76
+
77
+ until order.state == end_state
78
+ order.next!
79
+ update_payment_total(payment) if order.payment?
80
+ end
81
+ end
82
+
83
+ def update_payment_total(payment)
84
+ payment_total = order.payments.where(state: %w[checkout pending]).sum(:amount)
85
+ remaining_total = order.outstanding_balance - payment_total
86
+
87
+ if remaining_total > 0
88
+ payment.update!(amount: payment.amount + remaining_total)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,11 @@
1
+ Spree::Store.class_eval do
2
+ has_one :braintree_configuration, class_name: "SolidusPaypalBraintree::Configuration", dependent: :destroy
3
+
4
+ before_create :build_default_configuration
5
+
6
+ private
7
+
8
+ def build_default_configuration
9
+ build_braintree_configuration
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ Deface::Override.new(
2
+ virtual_path: "spree/admin/shared/_settings_sub_menu",
3
+ name: "solidus_paypal_braintree_admin_navigation_configuration",
4
+ insert_bottom: "[data-hook='admin_settings_sub_tabs']",
5
+ partial: "solidus_paypal_braintree/configurations/admin_tab"
6
+ )
@@ -0,0 +1,26 @@
1
+ <% prefix = "payment_source[#{id}]" %>
2
+
3
+ <div class="hosted-fields">
4
+ <div class="field" data-hook="card_number">
5
+ <%= label_tag "card_number#{id}", Spree::CreditCard.human_attribute_name(:number), class: "required" %>
6
+ <div class="input" id="card_number<%= id %>"></div>
7
+ </div>
8
+
9
+ <div class="field" data-hook="card_expiration">
10
+ <%= label_tag "card_expiry#{id}", Spree::CreditCard.human_attribute_name(:expiration), class: "required" %>
11
+ <div class="input" id="card_expiry<%= id %>"></div>
12
+ </div>
13
+
14
+ <div class="field" data-hook="card_code">
15
+ <%= label_tag "card_code#{id}", Spree::CreditCard.human_attribute_name(:card_code), class: "required" %>
16
+ <div class="input" id="card_code<%= id %>"></div>
17
+
18
+ <a href="/content/cvv" class="info cvvLink" target="_blank">
19
+ (<%= Spree.t(:what_is_this) %>)
20
+ </a>
21
+ </div>
22
+
23
+ <div class="clear"></div>
24
+ <input type="hidden" name="<%= prefix %>[payment_type]" value="<%= SolidusPaypalBraintree::Source::CREDIT_CARD %>">
25
+ <input type="hidden" id="payment_method_nonce" name="<%= prefix %>[nonce]">
26
+ </div>
@@ -0,0 +1 @@
1
+ Spree::Admin::PaymentsController.helper :braintree_admin
@@ -0,0 +1,30 @@
1
+ en:
2
+ activerecord:
3
+ models:
4
+ solidus_paypal_braintree/gateway: Braintree
5
+ spree:
6
+ admin:
7
+ tab:
8
+ braintree: Braintree
9
+ client_sdk_enabled: Client SDK enabled?
10
+ environment: Environment
11
+ merchant_currency_map: Merchant currency map
12
+ merchant_id: Merchant ID
13
+ paypal_payee_email_map: Paypal payee email map
14
+ private_key: Private key
15
+ public_key: Public key
16
+ token_generation_enabled: Token generation enabled?
17
+ solidus_paypal_braintree:
18
+ braintree: Braintree
19
+ nonce: Nonce
20
+ token: Token
21
+ payment_type:
22
+ label: Payment Type
23
+ apple_pay_card: Apple Pay
24
+ credit_card: Credit Card
25
+ pay_pal_account: PayPal
26
+ configurations:
27
+ title: Braintree Configurations
28
+ tab: Braintree
29
+ update_success: Successfully updated Braintree configurations.
30
+ update_error: An error occurred while updating Braintree configurations.
data/config/routes.rb ADDED
@@ -0,0 +1,12 @@
1
+ SolidusPaypalBraintree::Engine.routes.draw do
2
+ resource :checkout, only: [:update, :edit]
3
+ resource :client_token, only: [:create], format: :json
4
+ resource :transactions, only: [:create]
5
+
6
+ resources :configurations do
7
+ collection do
8
+ get :list
9
+ post :update
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ class CreateSolidusPaypalBraintreeSources < SolidusSupport::Migration[4.2]
2
+ def change
3
+ create_table :solidus_paypal_braintree_sources do |t|
4
+ t.string :nonce
5
+ t.string :token
6
+ t.string :payment_type
7
+ t.integer :user_id, index: true
8
+ t.references :customer, index: true
9
+ t.references :payment_method, index: true
10
+
11
+ t.timestamps null: false
12
+ end
13
+
14
+ add_foreign_key :solidus_paypal_braintree_sources, :spree_payment_methods, column: :payment_method_id
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ class CreateSolidusPaypalBraintreeCustomers < SolidusSupport::Migration[4.2]
2
+ def change
3
+ create_table :solidus_paypal_braintree_customers do |t|
4
+ t.references :user
5
+ t.string :braintree_customer_id
6
+ end
7
+
8
+ add_index :solidus_paypal_braintree_customers, :user_id, unique: true, name: "index_braintree_customers_on_user_id"
9
+ add_index :solidus_paypal_braintree_customers, :braintree_customer_id, unique: true, name: "index_braintree_customers_on_braintree_customer_id"
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ class CreateSolidusPaypalBraintreeConfigurations < SolidusSupport::Migration[4.2]
2
+ def change
3
+ create_table :solidus_paypal_braintree_configurations do |t|
4
+ t.boolean :paypal, null: false, default: false
5
+ t.boolean :apple_pay, null: false, default: false
6
+ t.integer :store_id, null: false, index: true, foreign_key: { references: :spree_stores }
7
+
8
+ t.timestamps null: false
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ class AddBraintreeConfigurationToStores < SolidusSupport::Migration[4.2]
2
+ def up
3
+ Spree::Store.all.each(&:create_braintree_configuration)
4
+ end
5
+
6
+ def down
7
+ SolidusPaypalBraintree::Configuration.joins(:store).destroy_all
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ class AddCreditCardToBraintreeConfiguration < SolidusSupport::Migration[4.2]
2
+ def change
3
+ add_column :solidus_paypal_braintree_configurations, :credit_card,
4
+ :boolean, null: false, default: false
5
+ end
6
+ end
@@ -0,0 +1,30 @@
1
+ class AddNullConstraintToSources < SolidusSupport::Migration[4.2]
2
+ def up
3
+ payments = Spree::Payment.arel_table
4
+ sources = SolidusPaypalBraintree::Source.arel_table
5
+ join_sources = payments.join(sources).on(
6
+ payments[:source_id].eq(sources[:id]).and(
7
+ payments[:source_type].eq("SolidusPaypalBraintree::Source")
8
+ ).and(
9
+ sources[:payment_method_id].eq(nil)
10
+ )
11
+ ).join_sources
12
+
13
+ count = Spree::Payment.joins(join_sources).count
14
+ Rails.logger.info("Updating #{count} problematic sources")
15
+
16
+ Spree::Payment.joins(join_sources).find_each do |payment|
17
+ Rails.logger.info("Updating source #{payment.source_id} with payment method id #{payment.payment_method_id}")
18
+ SolidusPaypalBraintree::Source.where(id: payment.source_id).update_all(payment_method_id: payment.payment_method_id)
19
+ end
20
+
21
+ # We use a foreign key constraint on the model,
22
+ # but it doesnt make sense to have this model exist without a payment method
23
+ # as two of its methods delegate to the payment method.
24
+ change_column_null(:solidus_paypal_braintree_sources, :payment_method_id, false)
25
+ end
26
+
27
+ def down
28
+ change_column_null(:solidus_paypal_braintree_sources, :payment_method_id, true)
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ class AddNotNullConstraintToSourcesPaymentType < SolidusSupport::Migration[4.2]
2
+ def change
3
+ reversible do |dir|
4
+ dir.up do
5
+ SolidusPaypalBraintree::Source.where(payment_type: nil).
6
+ update_all(payment_type: 'CreditCard')
7
+ end
8
+ end
9
+ change_column_null(:solidus_paypal_braintree_sources, :payment_type, false)
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ module SolidusPaypalBraintree
2
+ class ConfigurationsController < Spree::Admin::BaseController
3
+ helper Spree::Core::Engine.routes.url_helpers
4
+
5
+ def list
6
+ authorize! :list, SolidusPaypalBraintree::Configuration
7
+
8
+ @configurations = Spree::Store.all.map(&:braintree_configuration)
9
+ end
10
+
11
+ def update
12
+ authorize! :update, SolidusPaypalBraintree::Configuration
13
+
14
+ params = configurations_params[:configuration_fields]
15
+ if SolidusPaypalBraintree::Configuration.update(params.keys, params.values)
16
+ flash[:success] = t('update_success', scope: 'solidus_paypal_braintree.configurations')
17
+ else
18
+ flash[:error] = t('update_error', scope: 'solidus_paypal_braintree.configurations')
19
+ end
20
+ redirect_to action: :list
21
+ end
22
+
23
+ private
24
+
25
+ def configurations_params
26
+ params.require(:configurations).
27
+ permit(configuration_fields: [:paypal, :apple_pay, :credit_card])
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ class SolidusPaypalBraintree::CheckoutsController < Spree::CheckoutController
2
+ PERMITTED_PAYMENT_PARAMS = [
3
+ :payment_method_id,
4
+ source_attributes: [
5
+ :nonce,
6
+ :payment_type
7
+ ]
8
+ ].freeze
9
+
10
+ def update
11
+ @payment = Spree::PaymentCreate.new(@order, payment_params).build
12
+
13
+ if @payment.save
14
+ render plain: "ok"
15
+ else
16
+ render plain: "not-ok"
17
+ end
18
+ end
19
+
20
+ def payment_params
21
+ params.
22
+ require(:order).
23
+ require(:payments_attributes).
24
+ first.
25
+ permit(PERMITTED_PAYMENT_PARAMS)
26
+ end
27
+ end