spree_paypal_checkout 0.5.1
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 +7 -0
- data/LICENSE.md +12 -0
- data/README.md +69 -0
- data/Rakefile +21 -0
- data/app/assets/config/spree_paypal_checkout_manifest.js +2 -0
- data/app/controllers/spree/api/v2/storefront/paypal_orders_controller.rb +55 -0
- data/app/controllers/spree_paypal_checkout/store_controller_decorator.rb +9 -0
- data/app/helpers/spree_paypal_checkout/base_helper.rb +11 -0
- data/app/javascript/spree_paypal_checkout/application.js +16 -0
- data/app/javascript/spree_paypal_checkout/controllers/checkout_paypal_controller.js +99 -0
- data/app/models/spree_paypal_checkout/base.rb +6 -0
- data/app/models/spree_paypal_checkout/gateway.rb +185 -0
- data/app/models/spree_paypal_checkout/order.rb +71 -0
- data/app/models/spree_paypal_checkout/order_decorator.rb +11 -0
- data/app/models/spree_paypal_checkout/payment_method_decorator.rb +15 -0
- data/app/models/spree_paypal_checkout/payment_sources/paypal.rb +15 -0
- data/app/models/spree_paypal_checkout/store_decorator.rb +9 -0
- data/app/presenters/spree_paypal_checkout/order_presenter.rb +59 -0
- data/app/serializers/spree/api/v2/storefront/paypal_order_serializer.rb +11 -0
- data/app/services/spree_paypal_checkout/capture_order.rb +104 -0
- data/app/services/spree_paypal_checkout/create_payment.rb +34 -0
- data/app/services/spree_paypal_checkout/create_source.rb +52 -0
- data/app/views/spree/admin/payment_methods/configuration_guides/_spree_paypal_checkout.html.erb +6 -0
- data/app/views/spree/admin/payment_methods/descriptions/_spree_paypal_checkout.html.erb +10 -0
- data/app/views/spree/checkout/payment/_spree_paypal_checkout.html.erb +13 -0
- data/app/views/spree/payment_sources/_paypal_checkout.html.erb +16 -0
- data/app/views/spree_paypal_checkout/_head.html.erb +10 -0
- data/config/importmap.rb +6 -0
- data/config/initializers/spree.rb +7 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20250528095719_create_spree_paypal_checkout_orders.rb +20 -0
- data/lib/generators/spree_paypal_checkout/install/install_generator.rb +20 -0
- data/lib/spree_paypal_checkout/configuration.rb +13 -0
- data/lib/spree_paypal_checkout/engine.rb +35 -0
- data/lib/spree_paypal_checkout/factories.rb +33 -0
- data/lib/spree_paypal_checkout/version.rb +7 -0
- data/lib/spree_paypal_checkout.rb +7 -0
- metadata +206 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2e2d9fa4598b56c85022a9703e132d1addabaa9318fc41199c1ed8eb590dfe2a
|
4
|
+
data.tar.gz: f8e04b13cd445993ad93f30cbea36dbe5a1bfedc1bace9e0e8d0598736ecf89a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '079a5d90edfc6816fe8100caf2286c1b9d39151b1ac28a6c8985915b93bdd7630f60eaf0fbb897f4af6b4ec4cea92a6f5a84ded4723c35d726ada1c97257f966'
|
7
|
+
data.tar.gz: '09a9f5f86659326ed5b1461d5cec9e1c4cea0ebe8c2f44b025b323556f3da97d0e04aa7a4cf18d1354dd8da09700c8150eb45287b0fe4c8d46cc366bf4a9c0ff'
|
data/LICENSE.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Copyright (c) 2025 Vendo Connect Inc.
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
5
|
+
|
6
|
+
This program is distributed in the hope that it will be useful,
|
7
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
8
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
9
|
+
GNU Affero General Public License for more details.
|
10
|
+
|
11
|
+
You should have received a copy of the GNU Affero General Public License
|
12
|
+
along with this program. If not, see [https://www.gnu.org/licenses/](https://www.gnu.org/licenses/).
|
data/README.md
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# Spree PayPal Checkout
|
2
|
+
|
3
|
+
This is the official PayPal Checkout extension for [Spree Commerce](https://spreecommerce.org).
|
4
|
+
|
5
|
+
This PayPal Checkout integration is bundled in the [Spree Starter](https://github.com/spree/spree_starter/) for your development convenience.
|
6
|
+
|
7
|
+
Or you could follow the [installation instructions](https://spreecommerce.org/docs/integrations/payments/paypal).
|
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:
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
1. Add this extension to your Gemfile with this line:
|
16
|
+
|
17
|
+
```ruby
|
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.
|
30
|
+
|
31
|
+
## Developing
|
32
|
+
|
33
|
+
1. Create a dummy app
|
34
|
+
|
35
|
+
```bash
|
36
|
+
bundle update
|
37
|
+
bundle exec rake test_app
|
38
|
+
```
|
39
|
+
|
40
|
+
2. Add your new code
|
41
|
+
3. Run tests
|
42
|
+
|
43
|
+
```bash
|
44
|
+
bundle exec rspec
|
45
|
+
```
|
46
|
+
|
47
|
+
When testing your applications integration with this extension you may use it's factories.
|
48
|
+
Simply add this require statement to your spec_helper:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
require 'spree_paypal_checkout/factories'
|
52
|
+
```
|
53
|
+
|
54
|
+
## Releasing a new version
|
55
|
+
|
56
|
+
```shell
|
57
|
+
bundle exec gem bump -p -t
|
58
|
+
bundle exec gem release
|
59
|
+
```
|
60
|
+
|
61
|
+
For more options please see [gem-release README](https://github.com/svenfuchs/gem-release)
|
62
|
+
|
63
|
+
## Contributing
|
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
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'spree/testing_support/extension_rake'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new
|
8
|
+
|
9
|
+
task :default do
|
10
|
+
if Dir['spec/dummy'].empty?
|
11
|
+
Rake::Task[:test_app].invoke
|
12
|
+
Dir.chdir('../../')
|
13
|
+
end
|
14
|
+
Rake::Task[:spec].invoke
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Generates a dummy app for testing'
|
18
|
+
task :test_app do
|
19
|
+
ENV['LIB_NAME'] = 'spree_paypal_checkout'
|
20
|
+
Rake::Task['extension:test_app'].invoke
|
21
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Spree
|
2
|
+
module Api
|
3
|
+
module V2
|
4
|
+
module Storefront
|
5
|
+
class PaypalOrdersController < ::Spree::Api::V2::BaseController
|
6
|
+
include Spree::Api::V2::Storefront::OrderConcern
|
7
|
+
|
8
|
+
before_action :require_paypal_checkout_gateway
|
9
|
+
|
10
|
+
# POST /api/v2/storefront/paypal_orders
|
11
|
+
def create
|
12
|
+
order_presenter = SpreePaypalCheckout::OrderPresenter.new(spree_current_order)
|
13
|
+
|
14
|
+
paypal_response = paypal_client.orders.create_order(order_presenter.to_json)
|
15
|
+
|
16
|
+
paypal_order = spree_current_order.paypal_checkout_orders.create!(
|
17
|
+
paypal_id: paypal_response.data.id,
|
18
|
+
data: paypal_response.data.as_json,
|
19
|
+
amount: spree_current_order.total,
|
20
|
+
payment_method: current_store.paypal_checkout_gateway
|
21
|
+
)
|
22
|
+
|
23
|
+
render_serialized_payload { serialize_resource(paypal_order) }
|
24
|
+
rescue PaypalServerSdk::ErrorException => e
|
25
|
+
render_error_payload(e.message)
|
26
|
+
end
|
27
|
+
|
28
|
+
# PUT /api/v2/storefront/paypal_orders/:id/capture
|
29
|
+
def capture
|
30
|
+
paypal_order = spree_current_order.paypal_checkout_orders.find_by!(paypal_id: params[:id])
|
31
|
+
paypal_order.capture!
|
32
|
+
|
33
|
+
render_serialized_payload { serialize_resource(paypal_order) }
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def resource_serializer
|
39
|
+
Spree::Api::V2::Storefront::PaypalOrderSerializer
|
40
|
+
end
|
41
|
+
|
42
|
+
def paypal_client
|
43
|
+
@paypal_client ||= current_store.paypal_checkout_gateway.client
|
44
|
+
end
|
45
|
+
|
46
|
+
def require_paypal_checkout_gateway
|
47
|
+
return if current_store.paypal_checkout_gateway.present?
|
48
|
+
|
49
|
+
render_error_payload('Paypal checkout gateway not found', :not_found) && return
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module SpreePaypalCheckout
|
2
|
+
module StoreControllerDecorator
|
3
|
+
def self.prepended(base)
|
4
|
+
base.helper SpreePaypalCheckout::BaseHelper
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
Spree::StoreController.prepend(SpreePaypalCheckout::StoreControllerDecorator) if defined?(Spree::StoreController)
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module SpreePaypalCheckout
|
2
|
+
module BaseHelper
|
3
|
+
def current_paypal_checkout_gateway
|
4
|
+
@current_paypal_checkout_gateway ||= current_store.paypal_checkout_gateway
|
5
|
+
end
|
6
|
+
|
7
|
+
def paypal_checkout_enabled?
|
8
|
+
current_paypal_checkout_gateway.present?
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import '@hotwired/turbo-rails'
|
2
|
+
import { Application } from '@hotwired/stimulus'
|
3
|
+
|
4
|
+
let application
|
5
|
+
|
6
|
+
if (typeof window.Stimulus === "undefined") {
|
7
|
+
application = Application.start()
|
8
|
+
application.debug = false
|
9
|
+
window.Stimulus = application
|
10
|
+
} else {
|
11
|
+
application = window.Stimulus
|
12
|
+
}
|
13
|
+
|
14
|
+
import CheckoutPaypalController from 'spree_paypal_checkout/controllers/checkout_paypal_controller'
|
15
|
+
|
16
|
+
application.register('checkout-paypal', CheckoutPaypalController)
|
@@ -0,0 +1,99 @@
|
|
1
|
+
import { Controller } from '@hotwired/stimulus'
|
2
|
+
import showFlashMessage from 'spree/storefront/helpers/show_flash_message'
|
3
|
+
import { post, put } from '@rails/request.js'
|
4
|
+
|
5
|
+
export default class extends Controller {
|
6
|
+
static values = {
|
7
|
+
clientKey: String,
|
8
|
+
orderNumber: String,
|
9
|
+
orderToken: String,
|
10
|
+
currency: String,
|
11
|
+
amount: Number,
|
12
|
+
apiCreateOrderPath: String,
|
13
|
+
apiCaptureOrderPath: String,
|
14
|
+
apiCheckoutUpdatePath: String,
|
15
|
+
returnUrl: String,
|
16
|
+
}
|
17
|
+
|
18
|
+
connect() {
|
19
|
+
this.initPayPal();
|
20
|
+
|
21
|
+
this.submitTarget = document.querySelector('#checkout-payment-submit')
|
22
|
+
this.billingAddressCheckbox = document.querySelector('#order_use_shipping')
|
23
|
+
this.billingAddressForm = document.querySelector('form.edit_order')
|
24
|
+
|
25
|
+
// hide submit button
|
26
|
+
this.submitTarget.style.display = 'none'
|
27
|
+
}
|
28
|
+
|
29
|
+
disconnect() {
|
30
|
+
this.submitTarget.style.display = 'block'
|
31
|
+
}
|
32
|
+
|
33
|
+
initPayPal() {
|
34
|
+
const paypalButtons = window.paypal.Buttons({
|
35
|
+
style: {
|
36
|
+
shape: "rect",
|
37
|
+
layout: "vertical",
|
38
|
+
color: "gold",
|
39
|
+
label: "paypal",
|
40
|
+
},
|
41
|
+
message: {
|
42
|
+
amount: this.amountValue,
|
43
|
+
},
|
44
|
+
createOrder: async () => {
|
45
|
+
const response = await post(this.apiCreateOrderPathValue, {
|
46
|
+
headers: {
|
47
|
+
"X-Spree-Order-Token": this.orderTokenValue,
|
48
|
+
}
|
49
|
+
});
|
50
|
+
|
51
|
+
if (response.ok) {
|
52
|
+
const paypalOrder = await response.json;
|
53
|
+
|
54
|
+
if (!paypalOrder.data) {
|
55
|
+
console.error('No data in PayPal order response:', paypalOrder);
|
56
|
+
throw new Error(paypalOrder.error || 'Failed to create PayPal order');
|
57
|
+
}
|
58
|
+
|
59
|
+
const orderId = paypalOrder.data.attributes.paypal_id;
|
60
|
+
console.log('PayPal order ID to return:', orderId);
|
61
|
+
|
62
|
+
if (!orderId) {
|
63
|
+
console.error('No paypal_id found in response data:', paypalOrder.data);
|
64
|
+
throw new Error('No PayPal order ID found in response');
|
65
|
+
}
|
66
|
+
|
67
|
+
// Make sure we're returning a string
|
68
|
+
return String(orderId);
|
69
|
+
} else {
|
70
|
+
console.error('Failed to create PayPal order:', response.error);
|
71
|
+
showFlashMessage('error', `Sorry, your transaction could not be processed...<br><br>${response.error}`);
|
72
|
+
throw new Error(response.error || 'Failed to create PayPal order');
|
73
|
+
}
|
74
|
+
},
|
75
|
+
onApprove: async (data, actions) => {
|
76
|
+
const response = await put(
|
77
|
+
this.apiCaptureOrderPathValue.replace(this.orderNumberValue, data.orderID),
|
78
|
+
{
|
79
|
+
headers: {
|
80
|
+
"X-Spree-Order-Token": this.orderTokenValue,
|
81
|
+
},
|
82
|
+
}
|
83
|
+
);
|
84
|
+
|
85
|
+
if (response.ok) {
|
86
|
+
window.location.href = this.returnUrlValue;
|
87
|
+
} else {
|
88
|
+
console.error('Failed to capture PayPal order:', response.error);
|
89
|
+
showFlashMessage('error', `Sorry, your transaction could not be processed...<br><br>${response.error}`);
|
90
|
+
}
|
91
|
+
},
|
92
|
+
onError: (err) => {
|
93
|
+
showFlashMessage('error', `Your transaction was cancelled...<br><br>${err}`);
|
94
|
+
}
|
95
|
+
});
|
96
|
+
|
97
|
+
paypalButtons.render(this.element);
|
98
|
+
}
|
99
|
+
}
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module SpreePaypalCheckout
|
2
|
+
class Gateway < ::Spree::Gateway
|
3
|
+
include PaypalServerSdk
|
4
|
+
|
5
|
+
#
|
6
|
+
# Preferences
|
7
|
+
#
|
8
|
+
preference :client_id, :password
|
9
|
+
preference :client_secret, :password
|
10
|
+
preference :test_mode, :boolean, default: true
|
11
|
+
|
12
|
+
#
|
13
|
+
# Validations
|
14
|
+
#
|
15
|
+
validates :preferred_client_id, :preferred_client_secret, presence: true
|
16
|
+
|
17
|
+
def provider_class
|
18
|
+
self.class
|
19
|
+
end
|
20
|
+
|
21
|
+
def payment_source_class
|
22
|
+
SpreePaypalCheckout::PaymentSources::Paypal
|
23
|
+
end
|
24
|
+
|
25
|
+
def payment_profiles_supported?
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_name
|
30
|
+
'PayPal'
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_type
|
34
|
+
'spree_paypal_checkout'
|
35
|
+
end
|
36
|
+
|
37
|
+
def payment_icon_name
|
38
|
+
'paypal'
|
39
|
+
end
|
40
|
+
|
41
|
+
def description_partial_name
|
42
|
+
'spree_paypal_checkout'
|
43
|
+
end
|
44
|
+
|
45
|
+
def configuration_guide_partial_name
|
46
|
+
'spree_paypal_checkout'
|
47
|
+
end
|
48
|
+
|
49
|
+
def source_partial_name
|
50
|
+
'paypal_checkout'
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_profile(payment)
|
54
|
+
user = payment.order.user
|
55
|
+
return if user.blank?
|
56
|
+
|
57
|
+
return if payment.source.blank?
|
58
|
+
return unless payment.source.is_a?(SpreePaypalCheckout::PaymentSources::Paypal)
|
59
|
+
|
60
|
+
paypal_account_id = payment.source.account_id
|
61
|
+
|
62
|
+
return if paypal_account_id.blank?
|
63
|
+
|
64
|
+
payment.payment_method.gateway_customers.find_or_create_by(user: user, profile_id: paypal_account_id)
|
65
|
+
end
|
66
|
+
|
67
|
+
def client
|
68
|
+
@client ||= Client.new(
|
69
|
+
client_credentials_auth_credentials: ClientCredentialsAuthCredentials.new(
|
70
|
+
o_auth_client_id: preferred_client_id,
|
71
|
+
o_auth_client_secret: preferred_client_secret
|
72
|
+
),
|
73
|
+
environment: preferred_test_mode ? Environment::SANDBOX : Environment::PRODUCTION,
|
74
|
+
logging_configuration: LoggingConfiguration.new(
|
75
|
+
log_level: Logger::INFO,
|
76
|
+
request_logging_config: RequestLoggingConfiguration.new(
|
77
|
+
log_body: true
|
78
|
+
),
|
79
|
+
response_logging_config: ResponseLoggingConfiguration.new(
|
80
|
+
log_headers: true
|
81
|
+
)
|
82
|
+
)
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
def authorize(amount_in_cents, payment_source, gateway_options = {})
|
87
|
+
raise 'Not implemented'
|
88
|
+
end
|
89
|
+
|
90
|
+
# Purchase is the same as authorize + capture in one step
|
91
|
+
def purchase(amount_in_cents, payment_source, gateway_options = {})
|
92
|
+
capture(amount_in_cents, payment_source.paypal_id, gateway_options)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Capture a previously authorized payment
|
96
|
+
# @param amount_in_cents [Integer] the amount in cents to capture
|
97
|
+
# @param paypal_id [String] the PayPal Order ID
|
98
|
+
# @param gateway_options [Hash] this is an instance of Spree::Payment::GatewayOptions.to_hash
|
99
|
+
def capture(amount_in_cents, paypal_id, gateway_options = {})
|
100
|
+
protect_from_error do
|
101
|
+
order = find_order(gateway_options[:order_id])
|
102
|
+
return failure('Order not found') unless order
|
103
|
+
|
104
|
+
response = client.orders.capture_order({
|
105
|
+
'id' => paypal_id,
|
106
|
+
'prefer' => 'return=representation'
|
107
|
+
})
|
108
|
+
|
109
|
+
if response.data.status == 'COMPLETED'
|
110
|
+
success(response.data.id, response.data.as_json)
|
111
|
+
else
|
112
|
+
failure('Failed to capture PayPal payment', response.data)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def void(authorization, source, gateway_options = {})
|
118
|
+
raise 'Not implemented'
|
119
|
+
end
|
120
|
+
|
121
|
+
def credit(amount_in_cents, _payment_source, paypal_payment_id, gateway_options = {})
|
122
|
+
refund_originator = gateway_options[:originator]
|
123
|
+
order = refund_originator.respond_to?(:order) ? refund_originator.order : refund_originator
|
124
|
+
|
125
|
+
return failure('Order not found') unless order
|
126
|
+
|
127
|
+
protect_from_error do
|
128
|
+
payload = {
|
129
|
+
capture_id: paypal_payment_id,
|
130
|
+
amount: {
|
131
|
+
value: (amount_in_cents / 100.0).to_s,
|
132
|
+
currency_code: order.currency.upcase
|
133
|
+
}
|
134
|
+
}.deep_stringify_keys
|
135
|
+
|
136
|
+
response = client.payments.refund_captured_payment(payload)
|
137
|
+
|
138
|
+
success(response.data.id, response.data.as_json)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def cancel(authorization, payment = nil)
|
143
|
+
raise 'Not implemented'
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def find_order(order_id)
|
149
|
+
return nil unless order_id
|
150
|
+
|
151
|
+
order_number, _payment_number = order_id.split('-')
|
152
|
+
Spree::Order.find_by(number: order_number)
|
153
|
+
end
|
154
|
+
|
155
|
+
def find_capture_id(order_data)
|
156
|
+
return nil unless order_data
|
157
|
+
|
158
|
+
# Navigate through the order data to find the capture ID
|
159
|
+
order_data.dig('purchase_units', 0, 'payments', 'captures', 0, 'id')
|
160
|
+
end
|
161
|
+
|
162
|
+
def protect_from_error
|
163
|
+
yield
|
164
|
+
rescue PaypalServerSdk::APIException => e
|
165
|
+
raise Spree::Core::GatewayError, "PayPal API error: #{e.message}"
|
166
|
+
end
|
167
|
+
|
168
|
+
def success(authorization, response)
|
169
|
+
ActiveMerchant::Billing::Response.new(
|
170
|
+
true,
|
171
|
+
'Transaction successful',
|
172
|
+
response,
|
173
|
+
authorization: authorization
|
174
|
+
)
|
175
|
+
end
|
176
|
+
|
177
|
+
def failure(message, response = {})
|
178
|
+
ActiveMerchant::Billing::Response.new(
|
179
|
+
false,
|
180
|
+
message,
|
181
|
+
response
|
182
|
+
)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module SpreePaypalCheckout
|
2
|
+
class Order < Base
|
3
|
+
class NotCapturedError < StandardError; end
|
4
|
+
class AlreadyCapturedError < StandardError; end
|
5
|
+
|
6
|
+
#
|
7
|
+
# Associations
|
8
|
+
#
|
9
|
+
belongs_to :order, class_name: 'Spree::Order'
|
10
|
+
belongs_to :payment_method, class_name: 'Spree::PaymentMethod'
|
11
|
+
alias gateway payment_method
|
12
|
+
|
13
|
+
#
|
14
|
+
# Callbacks
|
15
|
+
#
|
16
|
+
before_validation :set_amount_from_order, on: :create
|
17
|
+
|
18
|
+
#
|
19
|
+
# Validations
|
20
|
+
#
|
21
|
+
validates :paypal_id, presence: true, uniqueness: true
|
22
|
+
validates :order, :payment_method, :data, presence: true
|
23
|
+
validates :amount, numericality: { greater_than: 0 }, presence: true
|
24
|
+
|
25
|
+
store_accessor :data, :payer, :purchase_units, :payment_source, :status
|
26
|
+
|
27
|
+
# Create a Spree::Payment record for this PayPal order
|
28
|
+
# @return [Spree::Payment]
|
29
|
+
def create_payment!
|
30
|
+
raise NotCapturedError unless completed?
|
31
|
+
|
32
|
+
SpreePaypalCheckout::CreatePayment.new(
|
33
|
+
order: order,
|
34
|
+
paypal_order: self,
|
35
|
+
gateway: payment_method,
|
36
|
+
amount: amount
|
37
|
+
).call
|
38
|
+
end
|
39
|
+
|
40
|
+
# Capture the PayPal order in PayPal API
|
41
|
+
# @return [SpreePaypalCheckout::Order]
|
42
|
+
def capture!
|
43
|
+
raise AlreadyCapturedError if completed?
|
44
|
+
|
45
|
+
CaptureOrder.new(paypal_order: self).call
|
46
|
+
end
|
47
|
+
|
48
|
+
# gets the PayPal payment ID from the PayPal order
|
49
|
+
# only available for authorized or captured orders
|
50
|
+
# @return [String]
|
51
|
+
def paypal_payment_id
|
52
|
+
@paypal_payment_id ||= data.dig('purchase_units', 0, 'payments', 'captures', 0, 'id')
|
53
|
+
end
|
54
|
+
|
55
|
+
# gets the PayPal order from PayPal API
|
56
|
+
# @return [PaypalServerSdk::ApiResponse]
|
57
|
+
def paypal_order
|
58
|
+
@paypal_order ||= gateway.client.orders.get_order({ 'id' => paypal_id })
|
59
|
+
end
|
60
|
+
|
61
|
+
def completed?
|
62
|
+
status == 'COMPLETED'
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def set_amount_from_order
|
68
|
+
self.amount ||= order&.total
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module SpreePaypalCheckout
|
2
|
+
module OrderDecorator
|
3
|
+
def self.prepended(base)
|
4
|
+
base.store_accessor :private_metadata, :paypal_id
|
5
|
+
|
6
|
+
base.has_many :paypal_checkout_orders, class_name: 'SpreePaypalCheckout::Order', dependent: :destroy, foreign_key: :order_id
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
Spree::Order.prepend(SpreePaypalCheckout::OrderDecorator)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SpreePaypalCheckout
|
2
|
+
module PaymentMethodDecorator
|
3
|
+
PAYPAL_CHECKOUT_TYPE = 'SpreePaypalCheckout::Gateway'.freeze
|
4
|
+
|
5
|
+
def self.prepended(base)
|
6
|
+
base.scope :paypal_checkout, -> { where(type: PAYPAL_CHECKOUT_TYPE) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def paypal_checkout?
|
10
|
+
type == PAYPAL_CHECKOUT_TYPE
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Spree::PaymentMethod.prepend(SpreePaypalCheckout::PaymentMethodDecorator)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SpreePaypalCheckout
|
2
|
+
module PaymentSources
|
3
|
+
class Paypal < ::Spree::PaymentSource
|
4
|
+
store_accessor :public_metadata, :email, :name, :account_status, :account_id
|
5
|
+
|
6
|
+
def actions
|
7
|
+
%w[credit void]
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.display_name
|
11
|
+
'PayPal'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'paypal_server_sdk'
|
2
|
+
|
3
|
+
module SpreePaypalCheckout
|
4
|
+
class OrderPresenter
|
5
|
+
include PaypalServerSdk
|
6
|
+
|
7
|
+
def initialize(order)
|
8
|
+
@order = order
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :order
|
12
|
+
|
13
|
+
def to_json
|
14
|
+
{
|
15
|
+
'body' => OrderRequest.new(
|
16
|
+
intent: CheckoutPaymentIntent::CAPTURE,
|
17
|
+
purchase_units: [
|
18
|
+
PurchaseUnitRequest.new(
|
19
|
+
amount: AmountWithBreakdown.new(
|
20
|
+
currency_code: order.currency.upcase,
|
21
|
+
value: order.total.to_s,
|
22
|
+
breakdown: AmountBreakdown.new(
|
23
|
+
item_total: Money.new(
|
24
|
+
currency_code: order.currency.upcase,
|
25
|
+
value: order.item_total.to_s
|
26
|
+
),
|
27
|
+
shipping: Money.new(
|
28
|
+
currency_code: order.currency.upcase,
|
29
|
+
value: order.ship_total.to_s
|
30
|
+
),
|
31
|
+
tax_total: Money.new(
|
32
|
+
currency_code: order.currency.upcase,
|
33
|
+
value: order.tax_total.to_s
|
34
|
+
),
|
35
|
+
discount: Money.new(
|
36
|
+
currency_code: order.currency.upcase,
|
37
|
+
value: order.promo_total.to_s
|
38
|
+
)
|
39
|
+
)
|
40
|
+
),
|
41
|
+
items: order.line_items.map do |line_item|
|
42
|
+
Item.new(
|
43
|
+
name: line_item.name,
|
44
|
+
unit_amount: Money.new(
|
45
|
+
currency_code: order.currency.upcase,
|
46
|
+
value: line_item.price.to_s
|
47
|
+
),
|
48
|
+
quantity: line_item.quantity.to_s,
|
49
|
+
sku: line_item.sku,
|
50
|
+
category: line_item.variant.digital? ? ItemCategory::DIGITAL_GOODS : ItemCategory::PHYSICAL_GOODS
|
51
|
+
)
|
52
|
+
end
|
53
|
+
)
|
54
|
+
]
|
55
|
+
)
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|