spree_paypal_checkout 0.7.1 → 6.0.3
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 +4 -4
- data/LICENSE +16 -4
- data/README.md +34 -52
- data/Rakefile +7 -17
- data/app/jobs/spree_paypal_checkout/base_job.rb +5 -0
- data/app/jobs/spree_paypal_checkout/register_webhook_job.rb +20 -0
- data/app/maintenance_tasks/spree_paypal_checkout/maintenance_tasks/migrate_payments.rb +100 -0
- data/app/models/spree/payment_sessions/paypal_checkout.rb +39 -57
- data/app/models/spree_paypal_checkout/amounts.rb +55 -0
- data/app/models/spree_paypal_checkout/gateway/payment_sessions.rb +232 -149
- data/app/models/spree_paypal_checkout/gateway/webhooks.rb +112 -0
- data/app/models/spree_paypal_checkout/gateway.rb +418 -128
- data/app/models/spree_paypal_checkout/notifications_client.rb +82 -0
- data/app/models/spree_paypal_checkout/payment_sources/paypal.rb +43 -3
- data/app/presenters/spree_paypal_checkout/order_presenter.rb +121 -49
- data/app/services/spree_paypal_checkout/register_webhook.rb +30 -0
- data/config/locales/en.yml +23 -4
- data/lib/generators/spree_paypal_checkout/install/install_generator.rb +12 -12
- data/lib/spree_paypal_checkout/engine.rb +6 -37
- data/lib/spree_paypal_checkout/factories.rb +1 -48
- data/lib/spree_paypal_checkout/testing_support/factories/gateway_factory.rb +18 -0
- data/lib/spree_paypal_checkout/testing_support/factories/payment_session_factory.rb +16 -0
- data/lib/spree_paypal_checkout.rb +11 -3
- data/lib/tasks/register_paypal_webhooks.rake +37 -0
- metadata +33 -104
- data/app/assets/config/spree_paypal_checkout_manifest.js +0 -2
- data/app/controllers/spree_paypal_checkout/store_controller_decorator.rb +0 -9
- data/app/helpers/spree_paypal_checkout/base_helper.rb +0 -11
- data/app/javascript/spree_paypal_checkout/application.js +0 -16
- data/app/javascript/spree_paypal_checkout/controllers/checkout_paypal_controller.js +0 -117
- data/app/models/spree/payment_setup_sessions/paypal_checkout.rb +0 -22
- data/app/models/spree_paypal_checkout/base.rb +0 -6
- data/app/models/spree_paypal_checkout/gateway/payment_setup_sessions.rb +0 -116
- data/app/models/spree_paypal_checkout/order.rb +0 -71
- data/app/models/spree_paypal_checkout/order_decorator.rb +0 -11
- data/app/models/spree_paypal_checkout/payment_method_decorator.rb +0 -15
- data/app/models/spree_paypal_checkout/store_decorator.rb +0 -9
- data/app/presenters/spree_paypal_checkout/setup_token_presenter.rb +0 -37
- data/app/services/spree_paypal_checkout/capture_order.rb +0 -104
- data/app/services/spree_paypal_checkout/create_payment.rb +0 -34
- data/app/services/spree_paypal_checkout/create_source.rb +0 -52
- data/app/views/spree/admin/payment_methods/configuration_guides/_spree_paypal_checkout.html.erb +0 -7
- data/app/views/spree/admin/payment_methods/descriptions/_spree_paypal_checkout.html.erb +0 -10
- data/app/views/spree/admin/payments/source_forms/_spree_paypal_checkout.html.erb +0 -7
- data/app/views/spree/checkout/payment/_spree_paypal_checkout.html.erb +0 -13
- data/app/views/spree/payment_sources/_paypal_checkout.html.erb +0 -16
- data/app/views/spree_paypal_checkout/_head.html.erb +0 -11
- data/config/importmap.rb +0 -6
- data/config/initializers/spree.rb +0 -7
- data/config/routes.rb +0 -16
- data/db/migrate/20250528095719_create_spree_paypal_checkout_orders.rb +0 -20
- data/lib/spree_api_v2/spree/api/v2/storefront/paypal_order_serializer.rb +0 -11
- data/lib/spree_api_v2/spree/api/v2/storefront/paypal_orders_controller.rb +0 -55
- data/lib/spree_paypal_checkout/configuration.rb +0 -5
- data/lib/spree_paypal_checkout/version.rb +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b41da8ff17667c478aa2a55cc31033ba2e6478cde897ee20afa80f35aec99ae1
|
|
4
|
+
data.tar.gz: 25feabb516457d5699b497c8bdadff0a3221c66fd7643f871ad872de872ed0e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a601b77f4a2f7cb3b0a78bc8d1dc619d048404ca706e150a1699f7fc78f8d5e25d20b75c4368c8d983f3611b9d6f3feab0ed43e25587450f40caf396b7899d45
|
|
7
|
+
data.tar.gz: 80cdce32765ca01bb72e1e50ccafdfae1d843e06a1b8c9f0a88b3d05750dc807744f7e9716366b273af4e9668625474c020b44ede67dc95b140c072364147a6f
|
data/LICENSE
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Vendo Connect Inc.
|
|
3
|
+
Copyright (c) 2026-present, Vendo Sp. z o.o., Vendo Connect Inc.
|
|
4
4
|
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
6
11
|
|
|
7
|
-
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
8
14
|
|
|
9
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
|
@@ -1,69 +1,51 @@
|
|
|
1
1
|
# Spree PayPal Checkout
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official [PayPal](https://www.paypal.com) payment gateway for
|
|
4
|
+
[Spree Commerce](https://spreecommerce.org), built on Spree's payment session
|
|
5
|
+
API.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
If you like what you see, consider giving this repo a GitHub star :star:
|
|
10
|
-
|
|
11
|
-
Thank you for supporting Spree open-source :heart:
|
|
7
|
+
Supports PayPal, Pay Later, Venmo, and card payments that buyers without a
|
|
8
|
+
PayPal account make in PayPal's own window. Honors the store's capture setting:
|
|
9
|
+
payments are captured at checkout, or authorized and captured later.
|
|
12
10
|
|
|
13
11
|
## Installation
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
bundle add spree_paypal_checkout
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
2. Run the install generator
|
|
22
|
-
|
|
23
|
-
```ruby
|
|
24
|
-
bundle exec rails g spree_paypal_checkout:install
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
3. Restart your server
|
|
28
|
-
|
|
29
|
-
If your server was running, restart it so that it can find the assets properly.
|
|
13
|
+
```bash
|
|
14
|
+
spree add paypal
|
|
15
|
+
```
|
|
30
16
|
|
|
31
|
-
|
|
17
|
+
Without the Spree CLI, add `gem 'spree_paypal_checkout'` to the `Gemfile` with
|
|
18
|
+
the same version requirement as your `spree` gem and run `bundle install`. The
|
|
19
|
+
gateway registers itself — there is nothing to generate and no migrations to
|
|
20
|
+
run.
|
|
32
21
|
|
|
33
|
-
|
|
22
|
+
## Configuration
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
Create a PayPal payment method in the admin dashboard and enter the client id
|
|
25
|
+
and secret of your PayPal REST app. Leave Test mode on for sandbox credentials
|
|
26
|
+
and turn it off for live ones. Saving queues a job that registers the webhook
|
|
27
|
+
with PayPal; until it has run, PayPal's notifications are refused, so if none
|
|
28
|
+
arrive, check the job queue for a failed `SpreePaypalCheckout::RegisterWebhookJob`.
|
|
39
29
|
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
Set `PAYPAL_LOG_LEVEL` (for example `debug`) to change how much the PayPal
|
|
31
|
+
client logs. Request and response bodies are never logged.
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
bundle exec rspec
|
|
45
|
-
```
|
|
33
|
+
## Upgrading from spree_paypal_checkout 0.x
|
|
46
34
|
|
|
47
|
-
|
|
48
|
-
|
|
35
|
+
See the PayPal section of the Spree 5.6 → 6.0 upgrade guide. Two upgrade
|
|
36
|
+
steps, `migrate_paypal_payments` and `register_paypal_webhooks`, carry existing
|
|
37
|
+
payments and webhooks over; run them before dropping the old
|
|
38
|
+
`spree_paypal_checkout_orders` table.
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
require 'spree_paypal_checkout/factories'
|
|
52
|
-
```
|
|
40
|
+
## Testing
|
|
53
41
|
|
|
54
|
-
|
|
42
|
+
The suite runs offline against recorded VCR cassettes:
|
|
55
43
|
|
|
56
|
-
```
|
|
57
|
-
bundle exec
|
|
58
|
-
bundle exec
|
|
44
|
+
```bash
|
|
45
|
+
bundle exec rake test_app # once
|
|
46
|
+
bundle exec rspec
|
|
59
47
|
```
|
|
60
48
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
If you'd like to contribute, please take a look at the
|
|
66
|
-
[instructions](CONTRIBUTING.md) for installing dependencies and crafting a good
|
|
67
|
-
pull request.
|
|
68
|
-
|
|
69
|
-
Copyright (c) 2025 Vendo Connect Inc, released under the AGPL-3.0 license
|
|
49
|
+
Record cassettes against the PayPal sandbox by adding `RECORD_VCR=1`,
|
|
50
|
+
`PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET` to the environment and running the
|
|
51
|
+
`vcr`-tagged examples.
|
data/Rakefile
CHANGED
|
@@ -1,25 +1,15 @@
|
|
|
1
|
-
require '
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'rake'
|
|
3
|
+
require 'rake/testtask'
|
|
4
4
|
require 'rspec/core/rake_task'
|
|
5
|
-
require 'spree/testing_support/
|
|
5
|
+
require 'spree/testing_support/common_rake'
|
|
6
6
|
|
|
7
7
|
RSpec::Core::RakeTask.new
|
|
8
8
|
|
|
9
|
-
task :
|
|
10
|
-
if Dir['spec/dummy'].empty?
|
|
11
|
-
Rake::Task[:test_app].invoke
|
|
12
|
-
Dir.chdir('../../')
|
|
13
|
-
end
|
|
14
|
-
Rake::Task[:spec].invoke
|
|
15
|
-
end
|
|
9
|
+
task default: :spec
|
|
16
10
|
|
|
17
|
-
desc
|
|
11
|
+
desc "Generates a dummy app for testing"
|
|
18
12
|
task :test_app do
|
|
19
13
|
ENV['LIB_NAME'] = 'spree_paypal_checkout'
|
|
20
|
-
Rake::Task['
|
|
21
|
-
install_storefront: true,
|
|
22
|
-
install_admin: true
|
|
23
|
-
)
|
|
24
|
-
system({ 'BUNDLE_GEMFILE' => File.expand_path('Gemfile', __dir__) }, 'rails g spree_legacy_api_v2:install')
|
|
14
|
+
Rake::Task['common:test_app'].invoke
|
|
25
15
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module SpreePaypalCheckout
|
|
2
|
+
class RegisterWebhookJob < BaseJob
|
|
3
|
+
# Safe to replay: registration finds the webhook for the URL before
|
|
4
|
+
# creating one.
|
|
5
|
+
retry_on StandardError, wait: :polynomially_longer, attempts: 5
|
|
6
|
+
# Must come after `retry_on StandardError` so DeserializationError lands in discard.
|
|
7
|
+
discard_on ActiveJob::DeserializationError
|
|
8
|
+
|
|
9
|
+
def perform(payment_method_id)
|
|
10
|
+
# Locked on load: two saves in a row queue two jobs, and both would
|
|
11
|
+
# otherwise create a webhook for the same URL.
|
|
12
|
+
Spree::PaymentMethod.transaction do
|
|
13
|
+
payment_method = Spree::PaymentMethod.lock.find_by(id: payment_method_id)
|
|
14
|
+
next unless payment_method.is_a?(SpreePaypalCheckout::Gateway)
|
|
15
|
+
|
|
16
|
+
payment_method.register_webhook
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module SpreePaypalCheckout
|
|
2
|
+
module MaintenanceTasks
|
|
3
|
+
# Gives PayPal payments taken under the 5.x gem the 6.0 shape: the PayPal
|
|
4
|
+
# order id as the response code, the key core links a session to its
|
|
5
|
+
# payment by, and the capture id the 5.x gem stored there moved to
|
|
6
|
+
# metadata['paypal_capture_id'], where refunds and the dashboard link read
|
|
7
|
+
# it.
|
|
8
|
+
#
|
|
9
|
+
# The order id is found two ways, both from the 5.x gem's own records: the
|
|
10
|
+
# session that took the payment holds the capture in its external data, and
|
|
11
|
+
# a payment from the older non-session flow is matched through
|
|
12
|
+
# spree_paypal_checkout_orders, which must therefore not be dropped first.
|
|
13
|
+
# A payment matched by neither is logged and left as it was.
|
|
14
|
+
class MigratePayments < Spree::MaintenanceTask
|
|
15
|
+
LEGACY_ORDERS_TABLE = 'spree_paypal_checkout_orders'.freeze
|
|
16
|
+
|
|
17
|
+
supports_dry_run
|
|
18
|
+
|
|
19
|
+
def collection
|
|
20
|
+
gateway_ids = Spree::PaymentMethod.unscoped.where(type: 'SpreePaypalCheckout::Gateway').select(:id)
|
|
21
|
+
Spree::Payment.unscoped.where(payment_method_id: gateway_ids).where.not(response_code: [nil, '']).order(:id)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def process(payment)
|
|
25
|
+
# Payments taken in 6.0, and those an earlier run converted, already
|
|
26
|
+
# carry what PayPal issued.
|
|
27
|
+
if payment.metadata['paypal_capture_id'].present? || payment.metadata['paypal_authorization_id'].present?
|
|
28
|
+
return tally(:already_converted)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
paypal_order_id = paypal_order_id_for(payment)
|
|
32
|
+
if paypal_order_id.nil?
|
|
33
|
+
Rails.logger.warn("[spree_paypal_checkout] migrate_paypal_payments: no PayPal order found for payment #{payment.id}")
|
|
34
|
+
return tally(:unmatched)
|
|
35
|
+
end
|
|
36
|
+
return tally(:would_convert) if dry_run?
|
|
37
|
+
|
|
38
|
+
payment.update_columns(
|
|
39
|
+
response_code: paypal_order_id,
|
|
40
|
+
metadata: payment.metadata.merge('paypal_capture_id' => payment.response_code),
|
|
41
|
+
updated_at: Time.current
|
|
42
|
+
)
|
|
43
|
+
tally(:converted)
|
|
44
|
+
rescue ActiveRecord::RecordNotUnique
|
|
45
|
+
# Another payment of the same order already carries this PayPal order.
|
|
46
|
+
Rails.logger.warn("[spree_paypal_checkout] migrate_paypal_payments: payment #{payment.id} duplicates another payment's PayPal order")
|
|
47
|
+
tally(:unmatched)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def paypal_order_id_for(payment)
|
|
53
|
+
capture_id = payment.response_code
|
|
54
|
+
session_order_id(payment, capture_id) || legacy_order_id(payment, capture_id)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The 5.x session stored PayPal's whole capture response as its external
|
|
58
|
+
# data, the order id as its external id.
|
|
59
|
+
# Looked up within the payment's own order or cart, so each payment reads
|
|
60
|
+
# a handful of rows.
|
|
61
|
+
def session_order_id(payment, capture_id)
|
|
62
|
+
owner = payment.order_id ? { order_id: payment.order_id } : { cart_id: payment.cart_id }
|
|
63
|
+
return if owner.values.first.nil?
|
|
64
|
+
|
|
65
|
+
sessions = Spree::PaymentSessions::PaypalCheckout.unscoped.where(payment_method_id: payment.payment_method_id, **owner)
|
|
66
|
+
sessions.find { |session| capture_id_in(session.external_data) == capture_id }&.external_id
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The older flow only ever paid for orders.
|
|
70
|
+
def legacy_order_id(payment, capture_id)
|
|
71
|
+
return if payment.order_id.nil? || !legacy_orders_table?
|
|
72
|
+
|
|
73
|
+
rows = legacy_order_model.where(payment_method_id: payment.payment_method_id, order_id: payment.order_id)
|
|
74
|
+
rows.find { |row| capture_id_in(row.data) == capture_id }&.paypal_id
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def capture_id_in(data)
|
|
78
|
+
data = JSON.parse(data) if data.is_a?(String)
|
|
79
|
+
return unless data.is_a?(Hash)
|
|
80
|
+
|
|
81
|
+
data.dig('purchase_units', 0, 'payments', 'captures', 0, 'id')
|
|
82
|
+
rescue JSON::ParserError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def legacy_orders_table?
|
|
87
|
+
@legacy_orders_table = ActiveRecord::Base.connection.table_exists?(LEGACY_ORDERS_TABLE) if @legacy_orders_table.nil?
|
|
88
|
+
@legacy_orders_table
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The 5.x model is gone, so the table is read through a throwaway one.
|
|
92
|
+
def legacy_order_model
|
|
93
|
+
@legacy_order_model ||= Class.new(ActiveRecord::Base) do
|
|
94
|
+
self.table_name = LEGACY_ORDERS_TABLE
|
|
95
|
+
self.inheritance_column = nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -1,76 +1,58 @@
|
|
|
1
1
|
module Spree
|
|
2
|
+
# Stored under this name in `type` columns since 5.x, so it is kept.
|
|
2
3
|
class PaymentSessions::PaypalCheckout < PaymentSession
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
# The order read at completion, or by prepare_for_settlement! on the
|
|
5
|
+
# webhook path, so settlement inside the lock does no PayPal I/O.
|
|
6
|
+
attr_writer :paypal_order
|
|
6
7
|
|
|
7
|
-
def
|
|
8
|
-
external_data&.dig('
|
|
8
|
+
def intent
|
|
9
|
+
external_data&.dig('intent')
|
|
9
10
|
end
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
# Moves only after a recoverable decline, so a buyer's next try reaches
|
|
13
|
+
# PayPal as a new request rather than a replay of the declined one.
|
|
14
|
+
#
|
|
15
|
+
# @return [Integer]
|
|
16
|
+
def attempt
|
|
17
|
+
(external_data&.dig('attempt') || 1).to_i
|
|
13
18
|
end
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
# Whether Complete ever asked PayPal to move money for this session.
|
|
21
|
+
def complete_attempted?
|
|
22
|
+
external_data.present? && (external_data.key?('attempt') || external_data.key?('paypal_result'))
|
|
17
23
|
end
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
# @return [Hash]
|
|
26
|
+
def paypal_order
|
|
27
|
+
@paypal_order ||= payment_method.paypal_order(external_id)
|
|
21
28
|
end
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
# Settlement reads the order only to create the payment, so a session that
|
|
31
|
+
# already has one needs no PayPal call.
|
|
32
|
+
def prepare_for_settlement!
|
|
33
|
+
paypal_order if payment.nil?
|
|
34
|
+
self
|
|
25
35
|
end
|
|
26
36
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
order.with_lock do
|
|
36
|
-
existing_payment = order.payments.where(
|
|
37
|
-
payment_method: payment_method,
|
|
38
|
-
response_code: paypal_capture_id
|
|
39
|
-
).first
|
|
40
|
-
|
|
41
|
-
return existing_payment if existing_payment.present?
|
|
42
|
-
|
|
43
|
-
source = create_payment_source!
|
|
44
|
-
|
|
45
|
-
order.payments.create!(
|
|
46
|
-
payment_method: payment_method,
|
|
47
|
-
amount: amount,
|
|
48
|
-
response_code: paypal_capture_id,
|
|
49
|
-
source: source,
|
|
50
|
-
skip_source_requirement: true,
|
|
51
|
-
private_metadata: metadata
|
|
52
|
-
)
|
|
53
|
-
end
|
|
37
|
+
# @return [SpreePaypalCheckout::PaymentSources::Paypal, nil]
|
|
38
|
+
def payment_source_for_settlement
|
|
39
|
+
SpreePaypalCheckout::PaymentSources::Paypal.for_payment_source(
|
|
40
|
+
paypal_order['payment_source'],
|
|
41
|
+
payment_method: payment_method,
|
|
42
|
+
customer: owner&.customer,
|
|
43
|
+
paypal_order_id: external_id
|
|
44
|
+
)
|
|
54
45
|
end
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def
|
|
59
|
-
|
|
60
|
-
return nil unless paypal_data
|
|
47
|
+
# Records what PayPal issued against the order, which refunds and the
|
|
48
|
+
# dashboard link cannot read from the order id alone.
|
|
49
|
+
def apply_settlement_metadata(payment, metadata)
|
|
50
|
+
super
|
|
61
51
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
source.update!(
|
|
67
|
-
user: order.user,
|
|
68
|
-
email: paypal_data['email_address'],
|
|
69
|
-
name: "#{paypal_data.dig('name', 'given_name')} #{paypal_data.dig('name', 'surname')}".strip,
|
|
70
|
-
account_id: paypal_data['account_id'],
|
|
71
|
-
account_status: paypal_data['account_status']
|
|
72
|
-
)
|
|
73
|
-
source
|
|
52
|
+
capture = payment_method.captures(paypal_order).first
|
|
53
|
+
authorization = payment_method.authorizations(paypal_order).first
|
|
54
|
+
payment.metadata['paypal_capture_id'] = capture['id'] if capture
|
|
55
|
+
payment.metadata['paypal_authorization_id'] = authorization['id'] if authorization
|
|
74
56
|
end
|
|
75
57
|
end
|
|
76
58
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module SpreePaypalCheckout
|
|
2
|
+
# PayPal's decimal-string amounts (https://developer.paypal.com/reference/currency-codes/).
|
|
3
|
+
# Written in the currency's ISO exponent, except that PayPal takes HUF, JPY
|
|
4
|
+
# and TWD only in whole units. An amount PayPal would refuse raises here,
|
|
5
|
+
# naming the rule, rather than being rounded to a sum Spree did not record.
|
|
6
|
+
module Amounts
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
SUPPORTED_CURRENCIES = %w[
|
|
10
|
+
AUD BRL CAD CHF CNY CZK DKK EUR GBP HKD HUF ILS JPY MXN MYR NOK NZD PHP PLN SEK SGD THB TWD USD
|
|
11
|
+
].freeze
|
|
12
|
+
WHOLE_UNITS = %w[HUF JPY TWD].freeze
|
|
13
|
+
|
|
14
|
+
class UnsupportedAmount < Spree::Core::GatewayError; end
|
|
15
|
+
|
|
16
|
+
# @param currency [String]
|
|
17
|
+
# @return [Boolean]
|
|
18
|
+
def supported_currency?(currency)
|
|
19
|
+
SUPPORTED_CURRENCIES.include?(currency.to_s.upcase)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Boolean] whether PayPal takes this amount in this currency
|
|
23
|
+
def payable?(amount, currency)
|
|
24
|
+
to_paypal(amount, currency)
|
|
25
|
+
true
|
|
26
|
+
rescue UnsupportedAmount
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param amount [BigDecimal, Spree::Money]
|
|
31
|
+
# @param currency [String]
|
|
32
|
+
# @return [String] e.g. "19.99", or "2000" for yen
|
|
33
|
+
# @raise [UnsupportedAmount] when PayPal would refuse the amount
|
|
34
|
+
def to_paypal(amount, currency)
|
|
35
|
+
amount = amount.to_d if amount.respond_to?(:to_d)
|
|
36
|
+
code = currency.to_s.upcase
|
|
37
|
+
raise UnsupportedAmount, "PayPal does not take #{code}" unless supported_currency?(code)
|
|
38
|
+
|
|
39
|
+
places = WHOLE_UNITS.include?(code) ? 0 : Spree::Money::Rounding.precision(code)
|
|
40
|
+
quantized = Spree::Money::Rounding.quantize(amount, places)
|
|
41
|
+
if quantized != amount
|
|
42
|
+
rule = places.zero? ? 'only in whole units' : "only to #{places} decimal places"
|
|
43
|
+
raise UnsupportedAmount, "PayPal takes #{code} #{rule}, got #{amount.to_s('F')}"
|
|
44
|
+
end
|
|
45
|
+
return quantized.to_i.to_s if places.zero?
|
|
46
|
+
|
|
47
|
+
Spree::Money::Rounding.format(quantized, code)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [PaypalServerSdk::Money]
|
|
51
|
+
def to_paypal_money(amount, currency)
|
|
52
|
+
PaypalServerSdk::Money.new(currency_code: currency.to_s.upcase, value: to_paypal(amount, currency))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|