solidus_paypal_braintree 0.1.0

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.
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