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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +12 -0
  3. data/README.md +69 -0
  4. data/Rakefile +21 -0
  5. data/app/assets/config/spree_paypal_checkout_manifest.js +2 -0
  6. data/app/controllers/spree/api/v2/storefront/paypal_orders_controller.rb +55 -0
  7. data/app/controllers/spree_paypal_checkout/store_controller_decorator.rb +9 -0
  8. data/app/helpers/spree_paypal_checkout/base_helper.rb +11 -0
  9. data/app/javascript/spree_paypal_checkout/application.js +16 -0
  10. data/app/javascript/spree_paypal_checkout/controllers/checkout_paypal_controller.js +99 -0
  11. data/app/models/spree_paypal_checkout/base.rb +6 -0
  12. data/app/models/spree_paypal_checkout/gateway.rb +185 -0
  13. data/app/models/spree_paypal_checkout/order.rb +71 -0
  14. data/app/models/spree_paypal_checkout/order_decorator.rb +11 -0
  15. data/app/models/spree_paypal_checkout/payment_method_decorator.rb +15 -0
  16. data/app/models/spree_paypal_checkout/payment_sources/paypal.rb +15 -0
  17. data/app/models/spree_paypal_checkout/store_decorator.rb +9 -0
  18. data/app/presenters/spree_paypal_checkout/order_presenter.rb +59 -0
  19. data/app/serializers/spree/api/v2/storefront/paypal_order_serializer.rb +11 -0
  20. data/app/services/spree_paypal_checkout/capture_order.rb +104 -0
  21. data/app/services/spree_paypal_checkout/create_payment.rb +34 -0
  22. data/app/services/spree_paypal_checkout/create_source.rb +52 -0
  23. data/app/views/spree/admin/payment_methods/configuration_guides/_spree_paypal_checkout.html.erb +6 -0
  24. data/app/views/spree/admin/payment_methods/descriptions/_spree_paypal_checkout.html.erb +10 -0
  25. data/app/views/spree/checkout/payment/_spree_paypal_checkout.html.erb +13 -0
  26. data/app/views/spree/payment_sources/_paypal_checkout.html.erb +16 -0
  27. data/app/views/spree_paypal_checkout/_head.html.erb +10 -0
  28. data/config/importmap.rb +6 -0
  29. data/config/initializers/spree.rb +7 -0
  30. data/config/locales/en.yml +5 -0
  31. data/config/routes.rb +13 -0
  32. data/db/migrate/20250528095719_create_spree_paypal_checkout_orders.rb +20 -0
  33. data/lib/generators/spree_paypal_checkout/install/install_generator.rb +20 -0
  34. data/lib/spree_paypal_checkout/configuration.rb +13 -0
  35. data/lib/spree_paypal_checkout/engine.rb +35 -0
  36. data/lib/spree_paypal_checkout/factories.rb +33 -0
  37. data/lib/spree_paypal_checkout/version.rb +7 -0
  38. data/lib/spree_paypal_checkout.rb +7 -0
  39. 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,2 @@
1
+ //= link spree_paypal_checkout/application.js
2
+ //= link_tree ../../javascript/spree_paypal_checkout/controllers .js
@@ -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,6 @@
1
+ module SpreePaypalCheckout
2
+ class Base < ::Spree.base_class
3
+ self.abstract_class = true
4
+ self.table_name_prefix = 'spree_paypal_checkout_'
5
+ end
6
+ end
@@ -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,9 @@
1
+ module SpreePaypalCheckout
2
+ module StoreDecorator
3
+ def paypal_checkout_gateway
4
+ @paypal_checkout_gateway ||= payment_methods.paypal_checkout.active.last
5
+ end
6
+ end
7
+ end
8
+
9
+ Spree::Store.prepend(SpreePaypalCheckout::StoreDecorator)
@@ -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