spree-paypal_platform 5.1.3 → 5.1.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b451e198e6215856080bd410bfdcec699ad02835c638a02d7a33ee933f21f27
4
- data.tar.gz: 183989e5effcb3cc16461b8f7e0401a284d630d546e6304aa8d65730f3bce7be
3
+ metadata.gz: 4de54a54a0c7dd935c012bf20c72711b0bf78959b2556acebfacc3c0eb14e930
4
+ data.tar.gz: 94c506e594ce38d454fdff782d1dad610582b81e59e658d1bce232b29cf7344c
5
5
  SHA512:
6
- metadata.gz: 6a6c113d998c9538d07ebd9a6b483a37f1c26fa3502257dfa2855c94990ab307d8889a32f132cea4e29a0064812b9cbff221ae665ae0c06c9f101454cb5605b0
7
- data.tar.gz: 2dd052e117e6aff4facf62c72f63a0f00bedb7f0b7a466db880d49b48efba99bc058d9c3f1022a4cdbd71f552d73117edb0dd4ef7bfe9e1a14a53361d1ff6759
6
+ metadata.gz: abdce2d88ae3c2fb939cb6ce006bf1b6a654fadf98436df56ff3771c7f79b55c66e28fea31934bdc20310877b2f875c4e19fe0be873b4d15b1a05cfeee45af7c
7
+ data.tar.gz: 8c4d81e0b0f6e8d80da95c7febc3c6acaefda891d4dce1c5722ea0a9a0700da8f9beaeccb2dbe046ce0fd320b4dac0bccffa6a555cbd2e1356d719ce3d7077dc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.5
4
+
5
+ - Gift cards and store credit: the PayPal order is created for the amount
6
+ due (`total_minus_store_credits`), not the full order total. Before, PayPal
7
+ charged the full total while Spree recorded the gift card as paying part
8
+ of it, so the customer paid twice for that part. A partial charge is sent
9
+ as a plain amount, without the breakdown and items that sum to the full
10
+ total.
11
+ - Completing a session refuses to capture when the PayPal order amount no
12
+ longer matches the amount due (e.g. a gift card was applied, or the cart
13
+ changed, after the PayPal order was created). The session fails and the
14
+ storefront creates a new one.
15
+
16
+ ## 5.1.4
17
+
18
+ - `enable_card_fields` preference (checkbox, default on).
19
+ - Storefront sees `enable_apple_pay` and `enable_card_fields` on the payment
20
+ method (`public_preferences`) and on the payment session `external_data`.
21
+ - Client token is not requested when Card Fields are disabled.
22
+
3
23
  ## 5.1.3
4
24
 
5
25
  Remove the unused `apple_pay_domain` preference. Domain registration is
data/README.md CHANGED
@@ -48,7 +48,8 @@ Then add a **PayPal** payment method in admin and set:
48
48
  | `client_id` / `client_secret` | REST app credentials from the PayPal Developer Dashboard |
49
49
  | `webhook_secret` | PayPal webhook ID (used as the verify-webhook `webhook_id`) |
50
50
  | `test_mode` | Sandbox vs live |
51
- | `enable_apple_pay` | Advertise Apple Pay to headless storefronts (default on) |
51
+ | `enable_apple_pay` | Offer Apple Pay on the storefront (default on) |
52
+ | `enable_card_fields` | Offer Card Fields on the storefront (default on) |
52
53
 
53
54
  Webhook URL is the standard Spree v3 payments webhook:
54
55
 
@@ -42,13 +42,18 @@ module Spree
42
42
  return nil if total.zero?
43
43
 
44
44
  protect_from_error do
45
- order_presenter = OrderPresenter.new(order)
45
+ order_presenter = OrderPresenter.new(order, amount: total)
46
46
  paypal_response = client.orders.create_order(order_presenter.to_json)
47
47
 
48
48
  session_data = paypal_response.data.as_json
49
49
 
50
- client_token = generate_client_token
51
- session_data['client_token'] = client_token if client_token.present?
50
+ session_data['enable_apple_pay'] = apple_pay_enabled?
51
+ session_data['enable_card_fields'] = card_fields_enabled?
52
+
53
+ if card_fields_enabled?
54
+ client_token = generate_client_token
55
+ session_data['client_token'] = client_token if client_token.present?
56
+ end
52
57
 
53
58
  payment_session_class.create!(
54
59
  order: order,
@@ -96,6 +101,7 @@ module Spree
96
101
  if fetched.data.status == 'COMPLETED'
97
102
  fetched
98
103
  else
104
+ ensure_amount_matches!(payment_session, fetched)
99
105
  client.orders.capture_order(lookup)
100
106
  end
101
107
 
@@ -161,6 +167,26 @@ module Spree
161
167
 
162
168
  private
163
169
 
170
+ ##
171
+ # Refuses to capture a PayPal order whose amount no longer matches what
172
+ # is due — e.g. a gift card was applied, or the cart changed, after the
173
+ # PayPal order was created. Capturing would take the stale amount; the
174
+ # storefront recreates the session instead.
175
+ #
176
+ # @raise [Spree::Core::GatewayError]
177
+ #
178
+ def ensure_amount_matches!(payment_session, fetched)
179
+ value = fetched.data.as_json.dig('purchase_units', 0, 'amount', 'value')
180
+ return if value.blank?
181
+
182
+ due = payment_session.order.total_minus_store_credits
183
+ return if value.to_d == due.to_d
184
+
185
+ payment_session.fail if payment_session.can_fail?
186
+ raise Spree::Core::GatewayError,
187
+ "PayPal order amount #{value} does not match the amount due #{due}"
188
+ end
189
+
164
190
  def extract_order_id_from_webhook(event_type, resource)
165
191
  case event_type
166
192
  when /\ACHECKOUT\.ORDER\./
@@ -23,6 +23,7 @@ module Spree
23
23
  preference :webhook_secret, :string
24
24
  preference :test_mode, :boolean, default: true
25
25
  preference :enable_apple_pay, :boolean, default: true
26
+ preference :enable_card_fields, :boolean, default: true
26
27
 
27
28
  validates :preferred_client_id, :preferred_client_secret, presence: true
28
29
 
@@ -105,6 +106,15 @@ module Spree
105
106
  preferred_enable_apple_pay
106
107
  end
107
108
 
109
+ ##
110
+ # Whether the storefront should offer Card Fields for this method.
111
+ #
112
+ # @return [TrueClass, FalseClass]
113
+ #
114
+ def card_fields_enabled?
115
+ preferred_enable_card_fields
116
+ end
117
+
108
118
  ##
109
119
  # @return [String, NilClass]
110
120
  #
@@ -279,6 +289,12 @@ module Spree
279
289
  end
280
290
  end
281
291
 
292
+ protected
293
+
294
+ def public_preference_keys
295
+ %i[enable_apple_pay enable_card_fields]
296
+ end
297
+
282
298
  private
283
299
 
284
300
  def find_order(order_id)
@@ -18,9 +18,13 @@ module Spree
18
18
 
19
19
  ##
20
20
  # @param order [Spree::Order]
21
+ # @param amount [Numeric, NilClass] what PayPal should charge; defaults to
22
+ # the order total. Less than the total when a gift card or store credit
23
+ # pays part of the order.
21
24
  #
22
- def initialize(order)
25
+ def initialize(order, amount: nil)
23
26
  @order = order
27
+ @amount = amount
24
28
  end
25
29
 
26
30
  attr_reader :order
@@ -41,15 +45,29 @@ module Spree
41
45
  private
42
46
 
43
47
  def purchase_unit
44
- args = {
45
- amount: amount_with_breakdown,
46
- items: items
47
- }
48
+ # A gift card or store credit pays part of the order, so the breakdown
49
+ # and items (which always sum to the full order total) can't describe
50
+ # what PayPal charges. PayPal accepts an amount on its own.
51
+ args =
52
+ if partial_charge?
53
+ { amount: PaypalServerSdk::AmountWithBreakdown.new(currency_code: order.currency.upcase,
54
+ value: charge_amount.to_s) }
55
+ else
56
+ { amount: amount_with_breakdown, items: items }
57
+ end
48
58
  args[:shipping] = shipping_details if shipping_details
49
59
 
50
60
  PaypalServerSdk::PurchaseUnitRequest.new(**args)
51
61
  end
52
62
 
63
+ def charge_amount
64
+ @amount.presence || order.total
65
+ end
66
+
67
+ def partial_charge?
68
+ charge_amount.to_d != order.total.to_d
69
+ end
70
+
53
71
  # NOTE: the breakdown only sends additional (non-included) tax — sending
54
72
  # the full tax_total double-counts VAT on tax-inclusive markets and
55
73
  # trips AMOUNT_MISMATCH. Do not change to order.tax_total.
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Api
5
+ module V3
6
+ ##
7
+ # Exposes non-secret payment-method preferences to the storefront.
8
+ #
9
+ module PaymentMethodSerializerDecorator
10
+ def self.prepended(base)
11
+ base.attribute :public_preferences, &:public_preferences
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ Spree::Api::V3::PaymentMethodSerializer.prepend(
19
+ Spree::Api::V3::PaymentMethodSerializerDecorator
20
+ )
@@ -11,6 +11,7 @@
11
11
  <strong>Apple Pay</strong> is a funding source of this same PayPal method, not a second payment method.
12
12
  Register every storefront domain that will show the Apple Pay button in the
13
13
  <%= external_link_to 'PayPal Apple Pay domain registration', 'https://developer.paypal.com/docs/checkout/apm/apple-pay/', class: 'alert-link' %>
14
- dashboard. The button only renders in Safari over HTTPS.
14
+ dashboard. Use the checkboxes below to offer Card Fields and Apple Pay
15
+ on the storefront. The Apple Pay button only renders in Safari over HTTPS.
15
16
  </p>
16
17
  </div>
@@ -9,7 +9,8 @@ FactoryBot.define do
9
9
  client_id: ENV.fetch('PAYPAL_CLIENT_ID', 'client_id_test'),
10
10
  client_secret: ENV.fetch('PAYPAL_CLIENT_SECRET', 'client_secret_test'),
11
11
  test_mode: true,
12
- enable_apple_pay: true
12
+ enable_apple_pay: true,
13
+ enable_card_fields: true
13
14
  }
14
15
  end
15
16
  end
@@ -3,6 +3,6 @@
3
3
  module Spree
4
4
  module PaypalPlatform
5
5
  # Major version tracks Spree's major version: 5.x supports Spree 5.x.
6
- VERSION = '5.1.3'
6
+ VERSION = '5.1.5'
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree-paypal_platform
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.3
4
+ version: 5.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aypex
@@ -131,6 +131,7 @@ files:
131
131
  - app/models/spree/paypal_platform/payment_sources/paypal.rb
132
132
  - app/models/spree/paypal_platform/store_decorator.rb
133
133
  - app/presenters/spree/paypal_platform/order_presenter.rb
134
+ - app/serializers/spree/api/v3/payment_method_serializer_decorator.rb
134
135
  - app/services/spree/paypal_platform/capture_order.rb
135
136
  - app/services/spree/paypal_platform/create_payment.rb
136
137
  - app/services/spree/paypal_platform/create_source.rb
@@ -168,7 +169,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
168
169
  - !ruby/object:Gem::Version
169
170
  version: '0'
170
171
  requirements: []
171
- rubygems_version: 4.0.16
172
+ rubygems_version: 4.0.20
172
173
  specification_version: 4
173
174
  summary: PayPal Checkout for Spree, including Apple Pay and Card Fields
174
175
  test_files: []