solidus_afterpay 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +41 -0
  3. data/.gem_release.yml +5 -0
  4. data/.github/stale.yml +17 -0
  5. data/.github_changelog_generator +2 -0
  6. data/.gitignore +20 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +14 -0
  9. data/CHANGELOG.md +1 -0
  10. data/Gemfile +33 -0
  11. data/LICENSE +202 -0
  12. data/README.md +175 -0
  13. data/Rakefile +6 -0
  14. data/app/assets/javascripts/solidus_afterpay/afterpay_checkout.js +131 -0
  15. data/app/assets/javascripts/spree/backend/solidus_afterpay.js +2 -0
  16. data/app/assets/javascripts/spree/frontend/solidus_afterpay.js +4 -0
  17. data/app/assets/stylesheets/spree/backend/solidus_afterpay.css +4 -0
  18. data/app/assets/stylesheets/spree/frontend/solidus_afterpay.css +4 -0
  19. data/app/controllers/solidus_afterpay/base_controller.rb +30 -0
  20. data/app/controllers/solidus_afterpay/callbacks_controller.rb +71 -0
  21. data/app/controllers/solidus_afterpay/checkouts_controller.rb +47 -0
  22. data/app/decorators/controllers/solidus_afterpay/spree/checkout_controller_decorator.rb +13 -0
  23. data/app/decorators/models/solidus_afterpay/spree/order_decorator.rb +15 -0
  24. data/app/helpers/solidus_afterpay/afterpay_helper.rb +15 -0
  25. data/app/models/solidus_afterpay/gateway.rb +157 -0
  26. data/app/models/solidus_afterpay/order_component_builder.rb +97 -0
  27. data/app/models/solidus_afterpay/payment_method.rb +56 -0
  28. data/app/models/solidus_afterpay/payment_source.rb +23 -0
  29. data/app/models/solidus_afterpay/user_agent_generator.rb +35 -0
  30. data/app/views/solidus_afterpay/_afterpay_javascript.html.erb +5 -0
  31. data/app/views/spree/admin/payments/source_forms/_afterpay.html.erb +1 -0
  32. data/app/views/spree/admin/payments/source_views/_afterpay.html.erb +0 -0
  33. data/app/views/spree/api/payments/source_views/_afterpay.json.jbuilder +3 -0
  34. data/app/views/spree/checkout/payment/_afterpay.html.erb +9 -0
  35. data/app/views/spree/shared/_afterpay_messaging.html.erb +13 -0
  36. data/bin/console +17 -0
  37. data/bin/rails +7 -0
  38. data/bin/rails-engine +13 -0
  39. data/bin/rails-sandbox +16 -0
  40. data/bin/rake +7 -0
  41. data/bin/sandbox +86 -0
  42. data/bin/setup +8 -0
  43. data/codecov.yml +2 -0
  44. data/config/locales/en.yml +12 -0
  45. data/config/routes.rb +7 -0
  46. data/db/migrate/20210813142725_create_solidus_afterpay_payment_sources.rb +12 -0
  47. data/lib/generators/solidus_afterpay/install/install_generator.rb +43 -0
  48. data/lib/generators/solidus_afterpay/install/templates/initializer.rb +5 -0
  49. data/lib/solidus_afterpay/configuration.rb +25 -0
  50. data/lib/solidus_afterpay/engine.rb +25 -0
  51. data/lib/solidus_afterpay/testing_support/factories.rb +20 -0
  52. data/lib/solidus_afterpay/version.rb +5 -0
  53. data/lib/solidus_afterpay.rb +5 -0
  54. data/solidus_afterpay.gemspec +38 -0
  55. data/spec/fixtures/vcr_casettes/create_checkout/invalid.yml +65 -0
  56. data/spec/fixtures/vcr_casettes/create_checkout/valid.yml +64 -0
  57. data/spec/fixtures/vcr_casettes/credit/invalid.yml +61 -0
  58. data/spec/fixtures/vcr_casettes/credit/valid.yml +63 -0
  59. data/spec/fixtures/vcr_casettes/deferred/authorize/declined_payment.yml +120 -0
  60. data/spec/fixtures/vcr_casettes/deferred/authorize/invalid.yml +61 -0
  61. data/spec/fixtures/vcr_casettes/deferred/authorize/valid.yml +120 -0
  62. data/spec/fixtures/vcr_casettes/deferred/capture/invalid.yml +61 -0
  63. data/spec/fixtures/vcr_casettes/deferred/capture/valid.yml +140 -0
  64. data/spec/fixtures/vcr_casettes/deferred/void/invalid.yml +61 -0
  65. data/spec/fixtures/vcr_casettes/deferred/void/valid.yml +137 -0
  66. data/spec/fixtures/vcr_casettes/find_payment/invalid.yml +61 -0
  67. data/spec/fixtures/vcr_casettes/find_payment/valid.yml +140 -0
  68. data/spec/fixtures/vcr_casettes/immediate/capture/declined_payment.yml +120 -0
  69. data/spec/fixtures/vcr_casettes/immediate/capture/invalid.yml +61 -0
  70. data/spec/fixtures/vcr_casettes/immediate/capture/valid.yml +134 -0
  71. data/spec/fixtures/vcr_casettes/retrieve_configuration/valid.yml +67 -0
  72. data/spec/helpers/solidus_afterpay/afterpay_helper_spec.rb +23 -0
  73. data/spec/models/solidus_afterpay/gateway_spec.rb +418 -0
  74. data/spec/models/solidus_afterpay/order_component_builder_spec.rb +137 -0
  75. data/spec/models/solidus_afterpay/payment_method_spec.rb +143 -0
  76. data/spec/models/solidus_afterpay/payment_source_spec.rb +61 -0
  77. data/spec/models/solidus_afterpay/user_agent_generator_spec.rb +22 -0
  78. data/spec/models/spree/order_spec.rb +158 -0
  79. data/spec/requests/solidus_afterpay/callbacks_controller_spec.rb +127 -0
  80. data/spec/requests/solidus_afterpay/checkouts_controller_spec.rb +190 -0
  81. data/spec/spec_helper.rb +31 -0
  82. data/spec/support/auth.rb +15 -0
  83. data/spec/support/preferences.rb +33 -0
  84. data/spec/support/vcr.rb +18 -0
  85. metadata +249 -0
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'afterpay'
4
+
5
+ module SolidusAfterpay
6
+ class OrderComponentBuilder
7
+ attr_reader :order, :redirect_confirm_url, :redirect_cancel_url
8
+
9
+ def initialize(order:, redirect_confirm_url:, redirect_cancel_url:)
10
+ @order = order
11
+ @redirect_confirm_url = redirect_confirm_url
12
+ @redirect_cancel_url = redirect_cancel_url
13
+ end
14
+
15
+ def call
16
+ ::Afterpay::Components::Order.new(
17
+ amount: amount,
18
+ consumer: consumer,
19
+ billing: address(order.billing_address),
20
+ shipping: address(order.shipping_address),
21
+ merchant: merchant,
22
+ items: items,
23
+ merchant_reference: order.number
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def address(address)
30
+ name = if SolidusSupport.combined_first_and_last_name_in_address?
31
+ address.name
32
+ else
33
+ [address.first_name, address.last_name].join(' ').strip
34
+ end
35
+
36
+ ::Afterpay::Components::Contact.new(
37
+ name: name,
38
+ line1: address.address1,
39
+ line2: address.address2,
40
+ area1: address.city,
41
+ region: address.state_text,
42
+ postcode: address.zipcode,
43
+ phone_number: address.phone
44
+ )
45
+ end
46
+
47
+ def consumer
48
+ if SolidusSupport.combined_first_and_last_name_in_address?
49
+ name_components = ::Spree::Address::Name.new(order.billing_address.name)
50
+ first_name = name_components.first_name
51
+ last_name = name_components.last_name
52
+ else
53
+ first_name = order.billing_address.first_name
54
+ last_name = order.billing_address.last_name
55
+ end
56
+
57
+ ::Afterpay::Components::Consumer.new(
58
+ email: order.user&.email || order.email,
59
+ given_names: first_name,
60
+ surname: last_name,
61
+ phone: order.billing_address.phone
62
+ )
63
+ end
64
+
65
+ def amount
66
+ ::Afterpay::Components::Money.new(
67
+ amount: order.total.to_s,
68
+ currency: order.currency
69
+ )
70
+ end
71
+
72
+ def merchant
73
+ ::Afterpay::Components::Merchant.new(
74
+ redirect_confirm_url: redirect_confirm_url,
75
+ redirect_cancel_url: redirect_cancel_url
76
+ )
77
+ end
78
+
79
+ def items
80
+ order.line_items.includes(variant: :product).map do |line_item|
81
+ item(line_item)
82
+ end
83
+ end
84
+
85
+ def item(line_item)
86
+ ::Afterpay::Components::Item.new(
87
+ name: line_item.name,
88
+ sku: line_item.sku,
89
+ quantity: line_item.quantity,
90
+ price: ::Afterpay::Components::Money.new(
91
+ amount: line_item.price.to_s,
92
+ currency: line_item.currency
93
+ )
94
+ )
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusAfterpay
4
+ class PaymentMethod < SolidusSupport.payment_method_parent_class
5
+ preference :merchant_id, :string
6
+ preference :secret_key, :string
7
+ preference :deferred, :boolean
8
+ preference :popup_window, :boolean
9
+ preference :minimum_amount, :decimal
10
+ preference :maximum_amount, :decimal
11
+ preference :currency, :string
12
+
13
+ def gateway_class
14
+ SolidusAfterpay::Gateway
15
+ end
16
+
17
+ def payment_source_class
18
+ SolidusAfterpay::PaymentSource
19
+ end
20
+
21
+ def partial_name
22
+ 'afterpay'
23
+ end
24
+
25
+ def try_void(payment)
26
+ return false unless payment.payment_source.can_void?(payment)
27
+
28
+ response = void(payment.response_code, { originator: payment, currency: payment.currency })
29
+
30
+ return false unless response.success?
31
+
32
+ response
33
+ end
34
+
35
+ def available_for_order?(order)
36
+ available_payment_currency == order.currency && available_payment_range.include?(order.total)
37
+ end
38
+
39
+ private
40
+
41
+ def available_payment_range
42
+ min = preferred_minimum_amount || configuration&.minimumAmount&.amount.to_f
43
+ max = preferred_maximum_amount || configuration&.maximumAmount&.amount.to_f
44
+
45
+ min..max
46
+ end
47
+
48
+ def available_payment_currency
49
+ preferred_currency || configuration&.maximumAmount&.currency
50
+ end
51
+
52
+ def configuration
53
+ @configuration ||= gateway.retrieve_configuration
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusAfterpay
4
+ class PaymentSource < SolidusSupport.payment_source_parent_class
5
+ self.table_name = 'solidus_afterpay_payment_sources'
6
+
7
+ validates :token, presence: true
8
+
9
+ def actions
10
+ %w[capture void credit]
11
+ end
12
+
13
+ def can_void?(payment)
14
+ payment_method = payment.payment_method
15
+
16
+ return false unless payment_method.preferred_deferred
17
+
18
+ payment_state = payment_method.gateway.find_payment(order_id: payment.response_code).try(:[], :paymentState)
19
+
20
+ ::SolidusAfterpay::Gateway::VOIDABLE_STATUSES.include?(payment_state)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusAfterpay
4
+ class UserAgentGenerator
5
+ def initialize(merchant_id:)
6
+ @merchant_id = merchant_id
7
+ end
8
+
9
+ def generate
10
+ "#{afterpay_plugin} (#{platform}; #{system_information}; #{merchant_id}) #{merchant_website_url}"
11
+ end
12
+
13
+ private
14
+
15
+ def afterpay_plugin
16
+ "SolidusAfterpay/#{SolidusAfterpay::VERSION}"
17
+ end
18
+
19
+ def platform
20
+ "Solidus/#{::Spree.solidus_gem_version}"
21
+ end
22
+
23
+ def system_information
24
+ "Ruby/#{RUBY_VERSION}"
25
+ end
26
+
27
+ def merchant_id
28
+ "Merchant/#{@merchant_id}"
29
+ end
30
+
31
+ def merchant_website_url
32
+ "https://#{::Spree::Store.default.url}"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ <% if payment_method.present? %>
2
+ <% content_for :head do %>
3
+ <%= include_afterpay_js(test_mode: payment_method.preferred_test_mode) %>
4
+ <% end %>
5
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= t('.not_supported') %>
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ json.nil!
@@ -0,0 +1,9 @@
1
+ <%= render partial: 'solidus_afterpay/afterpay_javascript', locals: { payment_method: payment_method } %>
2
+
3
+ <div
4
+ id="afterpay_checkout_payload"
5
+ class="hidden"
6
+ data-payment-method-id="<%= payment_method.id %>"
7
+ data-order-number="<%= @order.number %>"
8
+ data-popup-window="<%= payment_method.preferred_popup_window %>"
9
+ ></div>
@@ -0,0 +1,13 @@
1
+ <% content_for :head do %>
2
+ <script
3
+ src="https://js.afterpay.com/afterpay-1.x.js"
4
+ data-analytics-enabled
5
+ async
6
+ <% if max %>
7
+ data-min="<%= min %>"
8
+ data-max="<%= max %>"
9
+ <% end %>
10
+ ></script>
11
+ <% end %>
12
+
13
+ <%= content_tag("afterpay-placement", nil, data: data) %>
data/bin/console ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require "bundler/setup"
6
+ require "solidus_afterpay"
7
+
8
+ # You can add fixtures and/or initialization code here to make experimenting
9
+ # with your gem easier. You can also use a different console, if you like.
10
+ $LOAD_PATH.unshift(*Dir["#{__dir__}/../app/*"])
11
+
12
+ # (If you use this, don't forget to add pry to your Gemfile!)
13
+ # require "pry"
14
+ # Pry.start
15
+
16
+ require "irb"
17
+ IRB.start(__FILE__)
data/bin/rails ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ if %w[g generate].include? ARGV.first
4
+ exec "#{__dir__}/rails-engine", *ARGV
5
+ else
6
+ exec "#{__dir__}/rails-sandbox", *ARGV
7
+ end
data/bin/rails-engine ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path('..', __dir__)
6
+ ENGINE_PATH = File.expand_path('../lib/solidus_afterpay/engine', __dir__)
7
+
8
+ # Set up gems listed in the Gemfile.
9
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
10
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
11
+
12
+ require 'rails/all'
13
+ require 'rails/engine/commands'
data/bin/rails-sandbox ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ app_root = 'sandbox'
4
+
5
+ unless File.exist? "#{app_root}/bin/rails"
6
+ warn 'Creating the sandbox app...'
7
+ Dir.chdir "#{__dir__}/.." do
8
+ system "#{__dir__}/sandbox" or begin
9
+ warn 'Automatic creation of the sandbox app failed'
10
+ exit 1
11
+ end
12
+ end
13
+ end
14
+
15
+ Dir.chdir app_root
16
+ exec 'bin/rails', *ARGV
data/bin/rake ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rubygems"
5
+ require "bundler/setup"
6
+
7
+ load Gem.bin_path("rake", "rake")
data/bin/sandbox ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ case "$DB" in
6
+ postgres|postgresql)
7
+ RAILSDB="postgresql"
8
+ ;;
9
+ mysql)
10
+ RAILSDB="mysql"
11
+ ;;
12
+ sqlite|'')
13
+ RAILSDB="sqlite3"
14
+ ;;
15
+ *)
16
+ echo "Invalid DB specified: $DB"
17
+ exit 1
18
+ ;;
19
+ esac
20
+
21
+ if [ ! -z $SOLIDUS_BRANCH ]
22
+ then
23
+ BRANCH=$SOLIDUS_BRANCH
24
+ else
25
+ BRANCH="master"
26
+ fi
27
+
28
+ extension_name="solidus_afterpay"
29
+
30
+ # Stay away from the bundler env of the containing extension.
31
+ function unbundled {
32
+ ruby -rbundler -e'b = proc {system *ARGV}; Bundler.respond_to?(:with_unbundled_env) ? Bundler.with_unbundled_env(&b) : Bundler.with_clean_env(&b)' -- $@
33
+ }
34
+
35
+ rm -rf ./sandbox
36
+ unbundled bundle exec rails new sandbox --database="$RAILSDB" \
37
+ --skip-bundle \
38
+ --skip-git \
39
+ --skip-keeps \
40
+ --skip-rc \
41
+ --skip-spring \
42
+ --skip-test \
43
+ --skip-javascript
44
+
45
+ if [ ! -d "sandbox" ]; then
46
+ echo 'sandbox rails application failed'
47
+ exit 1
48
+ fi
49
+
50
+ cd ./sandbox
51
+ cat <<RUBY >> Gemfile
52
+ gem 'solidus', github: 'solidusio/solidus', branch: '$BRANCH'
53
+ gem 'solidus_auth_devise', '>= 2.1.0'
54
+ gem 'rails-i18n'
55
+ gem 'solidus_i18n'
56
+
57
+ gem '$extension_name', path: '..'
58
+
59
+ group :test, :development do
60
+ platforms :mri do
61
+ gem 'pry-byebug'
62
+ end
63
+ end
64
+ RUBY
65
+
66
+ unbundled bundle install --gemfile Gemfile
67
+
68
+ unbundled bundle exec rake db:drop db:create
69
+
70
+ unbundled bundle exec rails generate solidus:install \
71
+ --auto-accept \
72
+ --user_class=Spree::User \
73
+ --enforce_available_locales=true \
74
+ --with-authentication=false \
75
+ --payment-method=none \
76
+ $@
77
+
78
+ unbundled bundle exec rails generate solidus:auth:install
79
+ unbundled bundle exec rails generate ${extension_name}:install
80
+
81
+ echo
82
+ echo "🚀 Sandbox app successfully created for $extension_name!"
83
+ echo "🚀 Using $RAILSDB and Solidus $BRANCH"
84
+ echo "🚀 Use 'export DB=[postgres|mysql|sqlite]' to control the DB adapter"
85
+ echo "🚀 Use 'export SOLIDUS_BRANCH=<BRANCH-NAME>' to control the Solidus version"
86
+ echo "🚀 This app is intended for test purposes."
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ gem install bundler --conservative
7
+ bundle update
8
+ bin/rake clobber
data/codecov.yml ADDED
@@ -0,0 +1,2 @@
1
+ ignore:
2
+ - "lib/generators/solidus_afterpay/install/templates"
@@ -0,0 +1,12 @@
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
+ activerecord:
6
+ models:
7
+ solidus_afterpay/payment_method: Afterpay
8
+ solidus_afterpay:
9
+ payment_declined: Payment declined. Please contact the Afterpay Customer Service team for more information.
10
+ resource_not_found: The resource you were looking for could not be found.
11
+ order_token_not_found: Invalid order confirmation data passed in.
12
+ unauthorized: You are not authorized to perform that action.