workarea-paypal 2.0.9 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc.json +2 -1
  3. data/.gitignore +16 -11
  4. data/CHANGELOG.md +79 -0
  5. data/README.md +105 -6
  6. data/Rakefile +4 -5
  7. data/UPGRADE.md +46 -0
  8. data/app/assets/javascripts/workarea/storefront/paypal/config.js.erb +44 -0
  9. data/app/assets/javascripts/workarea/storefront/paypal/modules/paypal_buttons.js +71 -0
  10. data/app/assets/javascripts/workarea/storefront/paypal/modules/paypal_hosted_fields.js +96 -0
  11. data/app/assets/javascripts/workarea/storefront/paypal/modules/update_checkout_submit_text.js +11 -5
  12. data/app/assets/javascripts/workarea/storefront/paypal/templates/paypal_fields.jst.ejs +43 -0
  13. data/app/controllers/workarea/storefront/checkout/place_order_controller.decorator +6 -4
  14. data/app/controllers/workarea/storefront/paypal_controller.rb +32 -26
  15. data/app/helpers/workarea/storefront/paypal_helper.rb +38 -0
  16. data/app/models/workarea/payment.decorator +17 -3
  17. data/app/models/workarea/payment/authorize/paypal.rb +13 -13
  18. data/app/models/workarea/payment/capture/paypal.rb +10 -20
  19. data/app/models/workarea/payment/null_address.rb +37 -0
  20. data/app/models/workarea/payment/purchase/paypal.rb +1 -25
  21. data/app/models/workarea/payment/refund/paypal.rb +3 -6
  22. data/app/models/workarea/payment/tender/paypal.rb +14 -2
  23. data/app/services/workarea/paypal/approve_order.rb +104 -0
  24. data/app/services/workarea/paypal/create_order.rb +177 -0
  25. data/app/services/workarea/paypal/update_order.rb +44 -0
  26. data/app/views/workarea/admin/orders/tenders/_paypal.html.haml +5 -2
  27. data/app/views/workarea/api/orders/tenders/_paypal.json.jbuilder +7 -1
  28. data/app/views/workarea/storefront/carts/_paypal_checkout.html.haml +6 -1
  29. data/app/views/workarea/storefront/checkouts/_paypal_payment.html.haml +16 -5
  30. data/app/views/workarea/storefront/order_mailer/tenders/_paypal.html.haml +5 -3
  31. data/app/views/workarea/storefront/orders/tenders/_paypal.html.haml +9 -2
  32. data/app/views/workarea/storefront/paypal/_paypal_sdk.html.haml +1 -0
  33. data/app/workers/workarea/paypal/handle_webhook_event.rb +64 -0
  34. data/config/initializers/append_points.rb +17 -5
  35. data/config/initializers/fields.rb +27 -0
  36. data/config/initializers/workarea.rb +41 -5
  37. data/config/locales/en.yml +14 -4
  38. data/config/routes.rb +3 -2
  39. data/lib/tasks/workarea/create_webhooks.rake +29 -0
  40. data/lib/workarea/paypal.rb +22 -18
  41. data/lib/workarea/paypal/engine.rb +4 -0
  42. data/lib/workarea/paypal/gateway.rb +200 -0
  43. data/lib/workarea/paypal/requests/create_webhook.rb +21 -0
  44. data/lib/workarea/paypal/requests/delete_webhook.rb +17 -0
  45. data/lib/workarea/paypal/requests/generate_token.rb +21 -0
  46. data/lib/workarea/paypal/requests/list_webhooks.rb +17 -0
  47. data/lib/workarea/paypal/version.rb +1 -1
  48. data/package.json +9 -0
  49. data/test/dummy/config/initializers/workarea.rb +1 -1
  50. data/test/factories/workarea/capture_completed_webhook.json +70 -0
  51. data/test/factories/workarea/capture_denied_webhook.json +68 -0
  52. data/test/factories/workarea/paypal.rb +34 -0
  53. data/test/helpers/workarea/storefront/paypal_helper_test.rb +35 -0
  54. data/test/integration/workarea/storefront/paypal_integration_test.rb +104 -294
  55. data/test/integration/workarea/storefront/paypal_place_order_integration_test.rb +42 -0
  56. data/test/lib/workarea/paypal/gateway_test.rb +236 -0
  57. data/test/models/workarea/payment/authorize/paypal_test.rb +57 -46
  58. data/test/models/workarea/payment/capture/paypal_test.rb +9 -51
  59. data/test/models/workarea/payment/null_address_test.rb +53 -0
  60. data/test/models/workarea/payment/refund/paypal_test.rb +39 -38
  61. data/test/models/workarea/paypal_payment_test.rb +65 -0
  62. data/test/models/workarea/search/paypal_order_text_test.rb +14 -0
  63. data/test/services/workarea/paypal/approve_order_test.rb +35 -0
  64. data/test/services/workarea/paypal/create_order_test.rb +127 -0
  65. data/test/services/workarea/paypal/update_order_test.rb +73 -0
  66. data/test/support/workarea/paypal_setup.rb +49 -0
  67. data/test/system/workarea/storefront/cart_system_test.decorator +1 -1
  68. data/test/system/workarea/storefront/logged_in_checkout_system_test.decorator +1 -1
  69. data/test/vcr_cassettes/paypal_approve_order.yml +106 -0
  70. data/test/vcr_cassettes/paypal_create_order.yml +110 -0
  71. data/test/vcr_cassettes/paypal_gateway_create_order.yml +105 -0
  72. data/test/vcr_cassettes/paypal_gateway_generate_token.yml +103 -0
  73. data/test/vcr_cassettes/paypal_gateway_get_order.yml +103 -0
  74. data/test/vcr_cassettes/paypal_gateway_update_order.yml +199 -0
  75. data/test/vcr_cassettes/paypal_gateway_webhooks.yml +403 -0
  76. data/test/vcr_cassettes/paypal_update_order.yml +204 -0
  77. data/test/workers/workarea/paypal/handle_webhook_event_test.rb +60 -0
  78. data/workarea-paypal.gemspec +2 -1
  79. metadata +65 -14
  80. data/app/services/workarea/paypal/setup.rb +0 -114
  81. data/app/services/workarea/paypal/update.rb +0 -69
  82. data/app/views/workarea/storefront/checkouts/_paypal_error.html.haml +0 -6
  83. data/app/views/workarea/storefront/order_mailer/tenders/_paypal.text.haml +0 -2
  84. data/test/dummy/config/initializers/session_store.rb +0 -3
  85. data/test/integration/workarea/storefront/place_order_integration_test.decorator +0 -11
  86. data/test/models/workarea/payment/purchase/paypal_test.rb +0 -94
  87. data/test/models/workarea/payment_test.decorator +0 -34
  88. data/test/models/workarea/search/admin/order_test.decorator +0 -32
  89. data/test/services/workarea/paypal/setup_test.rb +0 -120
  90. data/test/services/workarea/paypal/update_test.rb +0 -221
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @namespace WORKAREA.paypalHostedFields
3
+ */
4
+ WORKAREA.registerModule('paypalHostedFields', (function () {
5
+ 'use strict';
6
+
7
+ var requestWrapper = function (requestData) {
8
+ var deferred = $.Deferred();
9
+
10
+ $.ajax(requestData)
11
+ .done(function(data) {
12
+ deferred.resolve(data);
13
+ })
14
+ .fail(function(data, status, xhr) {
15
+ deferred.reject(xhr);
16
+ });
17
+
18
+ return deferred.promise();
19
+ },
20
+
21
+ createOrder = function() {
22
+ return requestWrapper({
23
+ url: WORKAREA.routes.storefront.paypalPath(),
24
+ type: 'post',
25
+ dataType: 'json'
26
+ }).then(function (data) {
27
+ return data.id;
28
+ });
29
+ },
30
+
31
+ onApprove = function($form, data) {
32
+ return requestWrapper({
33
+ url: WORKAREA.routes.storefront.paypalApprovedPath({ id: data.orderId }),
34
+ type: 'put',
35
+ dataType: 'json'
36
+ }).then(
37
+ function() {
38
+ $form
39
+ .off('submit')
40
+ .trigger('submit');
41
+ }
42
+ );
43
+ },
44
+
45
+ addSubmitListener = function($container, hostedFields) {
46
+ var $form = $container.parents('form');
47
+
48
+ $form.on('submit', function(event) {
49
+ if ($form.find('#payment_new_card').is(':checked')) {
50
+ event.preventDefault();
51
+ hostedFields.submit({
52
+ vault: $container.find('input[name="save_card"]').is(':checked')
53
+ }).then(_.partial(onApprove, $form));
54
+ }
55
+ });
56
+ },
57
+
58
+ getConfig = function () {
59
+ return _.assign({}, WORKAREA.config.paypalHostedFields, {
60
+ createOrder: createOrder
61
+ });
62
+ },
63
+
64
+
65
+ setup = function($placeholder, $scope) {
66
+ var template = JST['workarea/storefront/paypal/templates/paypal_fields'],
67
+ options = $placeholder.data('paypalHostedFields'),
68
+ $container = $('.checkout-payment__primary-method--new .checkout-payment__primary-method-edit', $scope);
69
+
70
+ $container.html(template(options));
71
+
72
+ paypal
73
+ .HostedFields
74
+ .render(getConfig())
75
+ .then(_.partial(addSubmitListener, $container));
76
+ },
77
+
78
+ /**
79
+ * @method
80
+ * @name init
81
+ * @memberof WORKAREA.paypalHostedFields
82
+ */
83
+ init = function ($scope) {
84
+ var $placeholder = $('[data-paypal-hosted-fields]', $scope);
85
+
86
+ if (_.isEmpty($placeholder)) { return; }
87
+ if (window.paypal === undefined) { return; }
88
+ if (!paypal.HostedFields.isEligible()) { return; }
89
+
90
+ setup($placeholder, $scope);
91
+ };
92
+
93
+ return {
94
+ init: init
95
+ };
96
+ }()));
@@ -4,9 +4,7 @@
4
4
  WORKAREA.registerModule('updateCheckoutSubmitText', (function () {
5
5
  'use strict';
6
6
 
7
- var submitButtonText = function ($selectedPaymentMethod) {
8
- var data = $selectedPaymentMethod.data('updateCheckoutSubmitText') || {};
9
-
7
+ var submitButtonText = function (data) {
10
8
  if (data.prevent) { return; }
11
9
 
12
10
  if (data.text) {
@@ -17,10 +15,17 @@ WORKAREA.registerModule('updateCheckoutSubmitText', (function () {
17
15
  },
18
16
 
19
17
  updateText = function($selectedPaymentMethod, $checkoutSubmit) {
20
- var text = submitButtonText($selectedPaymentMethod);
18
+ var data = $selectedPaymentMethod.data('updateCheckoutSubmitText') || {},
19
+ text = submitButtonText(data);
21
20
 
22
21
  if (_.isEmpty(text)) { return; }
23
22
 
23
+ if (data.disabled && !data.prevent) {
24
+ $checkoutSubmit.attr('disabled', 'disabled');
25
+ } else {
26
+ $checkoutSubmit.removeAttr('disabled');
27
+ }
28
+
24
29
  $checkoutSubmit.text(text);
25
30
  },
26
31
 
@@ -55,6 +60,7 @@ WORKAREA.registerModule('updateCheckoutSubmitText', (function () {
55
60
  };
56
61
 
57
62
  return {
58
- init: init
63
+ init: init,
64
+ updateText: updateText
59
65
  };
60
66
  }()));
@@ -0,0 +1,43 @@
1
+ <div class="grid grid--auto">
2
+ <div class="grid__cell">
3
+ <div class="property">
4
+ <label class="property__name" for="credit_card_number">
5
+ <span class="property__text">Card Number</span>
6
+ </label>
7
+ <div class="value">
8
+ <div class="text-box" id="paypal-credit-card-number" style="height: 30px;"></div>
9
+ </div>
10
+ </div>
11
+ </div>
12
+ <div class="grid__cell">
13
+ <div class="property">
14
+ <label class="property__name" for="credit_card_expiration">
15
+ <span>Expiration</span>
16
+ </label>
17
+ <div class="value">
18
+ <div class="text-box text-box--small" id="paypal-expiration-field" style="height: 30px;"></div>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ <div class="grid__cell">
23
+ <div class="property">
24
+ <label class="property__name" for="credit_card_cvv">
25
+ <span class="property__text">Security Code</span>
26
+ </label>
27
+ <div class="value">
28
+ <div class="text-box text-box--small" id="paypal-cvv-field" style="height: 30px;"></div>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ </div>
33
+
34
+ <% if (show_save_card) { %>
35
+ <div class="button-property">
36
+ <div class="value">
37
+ <input type="checkbox" name="save_card" id="save_card" value="true" />
38
+ </div>
39
+ <label class="button-property__name" for="save_card">
40
+ <span>Save card for faster checkout</span>
41
+ </label>
42
+ </div>
43
+ <% } %>
@@ -1,10 +1,12 @@
1
1
  module Workarea
2
2
  decorate Storefront::Checkout::PlaceOrderController, with: 'paypal' do
3
3
  def place_order
4
- if params[:payment] == 'paypal' && !current_checkout.payment.paypal? && params[:from_checkout].present?
5
- redirect_to start_paypal_path(from_checkout: 'from_checkout') and return
6
- elsif params[:payment] == 'paypal' && !current_checkout.payment.paypal?
7
- redirect_to start_paypal_path and return
4
+ if params[:payment] == 'paypal' && current_checkout.payment.paypal?
5
+ Paypal::UpdateOrder.new(current_checkout).perform
6
+ super
7
+ elsif params[:payment] == 'paypal'
8
+ flash['info'] = t('workarea.storefront.paypal.errors.place_order')
9
+ redirect_to checkout_payment_path
8
10
  else
9
11
  super
10
12
  end
@@ -2,10 +2,11 @@ module Workarea
2
2
  class Storefront::PaypalController < Storefront::ApplicationController
3
3
  include Storefront::CurrentCheckout
4
4
 
5
- before_action :validate_checkout
5
+ before_action :validate_checkout, except: :event
6
+ skip_before_action :verify_authenticity_token
6
7
 
7
- def start
8
- unless params[:from_checkout].present?
8
+ def create
9
+ unless current_order.checking_out?
9
10
  if logged_in?
10
11
  current_checkout.start_as(current_user)
11
12
  else
@@ -14,38 +15,43 @@ module Workarea
14
15
  end
15
16
 
16
17
  self.current_order = current_checkout.order
17
- Pricing.perform(current_order, current_shipping)
18
18
  check_inventory || (return)
19
19
 
20
- setup = Paypal::Setup.new(
21
- current_order,
22
- current_user,
23
- current_shipping,
24
- self
25
- )
20
+ if current_checkout.payment.paypal?
21
+ current_checkout.payment.paypal.update!(approved: false)
22
+ render json: { id: current_checkout.payment.paypal_id }
23
+ else
24
+ response = Paypal::CreateOrder.new(current_checkout).perform
25
+ render json: { id: response.id }
26
+ end
26
27
 
27
- redirect_to setup.redirect_url
28
+ rescue Paypal::Gateway::RequestError => e
29
+ Rails.logger.error(e)
30
+ flash[:error] = t('workarea.storefront.paypal.errors.request_failed')
31
+ head :internal_server_error
28
32
  end
29
33
 
30
- def complete
31
- self.current_order = Order.find(params[:order_id])
32
- current_order.user_id = current_user.try(:id)
33
- Pricing.perform(current_order, current_shipping)
34
+ def update
34
35
  check_inventory || (return)
35
36
 
36
- Paypal::Update.new(
37
- current_order,
38
- current_checkout.payment,
39
- current_shipping,
40
- params[:token]
41
- ).apply
37
+ Paypal::ApproveOrder.new(current_checkout, params[:id]).perform
42
38
 
43
- unless current_checkout.complete?
44
- flash[:error] = t('workarea.storefront.paypal.address_error')
45
- redirect_to(checkout_addresses_path) && (return)
46
- end
39
+ complete = current_checkout.complete?
40
+ flash[:error] = t('workarea.storefront.paypal.errors.order_incomplete') unless complete
41
+
42
+ render json: {
43
+ success: complete,
44
+ redirect_url: checkout_payment_path
45
+ }
46
+ end
47
+
48
+ def event
49
+ Paypal::HandleWebhookEvent.perform_async(
50
+ params[:event_type],
51
+ params[:resource].to_unsafe_h
52
+ )
47
53
 
48
- redirect_to checkout_payment_path
54
+ head :ok
49
55
  end
50
56
  end
51
57
  end
@@ -0,0 +1,38 @@
1
+ module Workarea
2
+ module Storefront
3
+ module PaypalHelper
4
+ def set_paypal_client_token
5
+ return unless Workarea::Paypal.gateway.configured?
6
+
7
+ @paypal_client_token =
8
+ if Workarea.config.use_paypal_hosted_fields
9
+ request = Workarea::Paypal.gateway.generate_token(user: current_user)
10
+ request.result.client_token
11
+ end
12
+ end
13
+
14
+ def include_paypal_javascript_tag(params: {}, data: {})
15
+ return unless Workarea::Paypal.gateway.configured?
16
+
17
+ params =
18
+ Workarea.config.paypal_sdk_params
19
+ .merge('client-id' => Workarea::Paypal.gateway.client_id)
20
+ .merge(params)
21
+ .compact
22
+
23
+ components = params['components'].to_s.split(',')
24
+ components << 'buttons'
25
+ components << 'hosted-fields' if Workarea.config.use_paypal_hosted_fields
26
+ params['components'] = components.compact.uniq.join(',')
27
+
28
+ javascript_include_tag(
29
+ "https://www.paypal.com/sdk/js?#{params.to_query}",
30
+ data: {
31
+ partner_attribution_id: 'Workarea_SP', # Do not change this
32
+ client_token: @paypal_client_token
33
+ }.merge(data)
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
@@ -2,26 +2,40 @@ module Workarea
2
2
  decorate Payment, with: :paypal do
3
3
  decorated do
4
4
  embeds_one :paypal, class_name: 'Workarea::Payment::Tender::Paypal'
5
+ delegate :paypal_id, to: :paypal, allow_nil: true
5
6
  end
6
7
 
7
8
  def set_paypal(attrs)
8
9
  build_paypal unless paypal
9
10
  paypal.attributes = attrs.slice(
10
- :token,
11
+ :paypal_id,
11
12
  :payer_id,
12
- :details
13
+ :details,
14
+ :approved,
15
+ :direct_payment
13
16
  )
14
17
 
15
18
  save
16
19
  end
17
20
 
18
21
  def paypal?
19
- paypal.present?
22
+ paypal.present? && paypal.approved?
20
23
  end
21
24
 
22
25
  def set_credit_card(*)
23
26
  self.paypal = nil
24
27
  super
25
28
  end
29
+
30
+ def address
31
+ addr = super
32
+ return addr if !paypal? || addr&.valid?
33
+
34
+ Payment::NullAddress.new.tap { |na| na.reference = addr || build_address }
35
+ end
36
+
37
+ def real_address
38
+ method(:address).super_method.call
39
+ end
26
40
  end
27
41
  end
@@ -3,27 +3,27 @@ module Workarea
3
3
  module Authorize
4
4
  class Paypal
5
5
  include OperationImplementation
6
- include CreditCardOperation
7
-
8
- delegate :gateway, to: Workarea::Paypal
9
6
 
10
7
  def complete!
11
- transaction.response = handle_active_merchant_errors do
12
- gateway.authorize(
13
- transaction.amount.cents,
14
- token: tender.token,
15
- payer_id: tender.payer_id,
16
- currency: transaction.amount.currency
17
- )
8
+ response = Workarea::Paypal.gateway.capture(tender.paypal_id)
9
+
10
+ if response.success? && response.params['status'] == 'PENDING'
11
+ transaction.action = 'authorize'
12
+ else
13
+ transaction.action = 'purchase'
18
14
  end
15
+
16
+ transaction.response = response
19
17
  end
20
18
 
21
19
  def cancel!
22
20
  return unless transaction.success?
23
21
 
24
- transaction.cancellation = handle_active_merchant_errors do
25
- gateway.void(transaction.response.params['transaction_id'])
26
- end
22
+ transaction.cancellation =
23
+ Workarea::Paypal.gateway.refund(
24
+ transaction.response.params['id'],
25
+ amount: transaction.amount
26
+ )
27
27
  end
28
28
  end
29
29
  end
@@ -3,31 +3,21 @@ module Workarea
3
3
  class Capture
4
4
  class Paypal
5
5
  include OperationImplementation
6
- include CreditCardOperation
7
-
8
- delegate :gateway, to: Workarea::Paypal
9
6
 
10
7
  def complete!
11
- validate_reference!
12
-
13
- transaction.response = handle_active_merchant_errors do
14
- gateway.capture(
15
- transaction.amount.cents,
16
- transaction.reference.response.params['transaction_id'],
17
- currency: transaction.amount.currency
18
- )
19
- end
8
+ # Capture can only happen if the initial capture was not completed
9
+ # immediately. Since we cannot force a pending capture to complete,
10
+ # this will only fail with an explanation that the capture will
11
+ # complete via webhook or never be completed.
12
+ #
13
+ transaction.response = ActiveMerchant::Billing::Response.new(
14
+ false,
15
+ I18n.t('workarea.payment.paypal_capture')
16
+ )
20
17
  end
21
18
 
22
19
  def cancel!
23
- return unless transaction.success?
24
-
25
- transaction.cancellation = handle_active_merchant_errors do
26
- gateway.refund(
27
- transaction.amount.cents,
28
- transaction.response.params['transaction_id']
29
- )
30
- end
20
+ # noop, nothing to cancel
31
21
  end
32
22
  end
33
23
  end
@@ -0,0 +1,37 @@
1
+ module Workarea
2
+ class Payment
3
+ class NullAddress
4
+ class NullCountry < OpenStruct
5
+ def to_s; end
6
+ end
7
+
8
+ include Mongoid::Document
9
+
10
+ FIELDS = Workarea::Payment::Address.fields.keys.tap { |k| k.delete('_id') }
11
+
12
+ attr_writer :reference
13
+ attr_reader *FIELDS, :region_name
14
+
15
+ delegate *FIELDS.map { |f| "#{f}=" }, :assign_attributes, :attributes=,
16
+ :allow_po_box?, to: :reference
17
+
18
+ def reference
19
+ @reference ||= Workarea::Payment::Address.new
20
+ end
21
+
22
+ def save(*args)
23
+ true
24
+ end
25
+
26
+ def country
27
+ NullCountry.new
28
+ end
29
+
30
+ def falsey(*args)
31
+ false
32
+ end
33
+ alias :po_box? :falsey
34
+ alias :address_eql? :falsey
35
+ end
36
+ end
37
+ end