spree_paypal_checkout 0.6.0 → 0.7.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 +4 -4
- data/app/models/spree/payment_sessions/paypal_checkout.rb +76 -0
- data/app/models/spree/payment_setup_sessions/paypal_checkout.rb +22 -0
- data/app/models/spree_paypal_checkout/gateway/payment_sessions.rb +204 -0
- data/app/models/spree_paypal_checkout/gateway/payment_setup_sessions.rb +116 -0
- data/app/models/spree_paypal_checkout/gateway.rb +9 -0
- data/app/models/spree_paypal_checkout/payment_sources/paypal.rb +1 -1
- data/app/presenters/spree_paypal_checkout/setup_token_presenter.rb +37 -0
- data/app/services/spree_paypal_checkout/create_source.rb +1 -1
- data/app/views/spree/admin/payment_methods/configuration_guides/_spree_paypal_checkout.html.erb +2 -1
- data/config/routes.rb +9 -6
- data/lib/spree_paypal_checkout/configuration.rb +1 -9
- data/lib/spree_paypal_checkout/engine.rb +6 -0
- data/lib/spree_paypal_checkout/factories.rb +15 -0
- data/lib/spree_paypal_checkout/version.rb +1 -1
- metadata +14 -9
- /data/{app/serializers → lib/spree_api_v2}/spree/api/v2/storefront/paypal_order_serializer.rb +0 -0
- /data/{app/controllers → lib/spree_api_v2}/spree/api/v2/storefront/paypal_orders_controller.rb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bfb11878efce5e64446b6e83c9f16a69b64203ff47ce9ca601f1404b6f0ed017
|
|
4
|
+
data.tar.gz: 86705e3ed5b1c0314c2518c793edc022d5149e64a32d710919455b0e3ef4fe42
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fb11160ee57ed3ab5275c2e32c8d13463b74149002ed2297cd95045f53dc3101c8a1f97fff3b8424c4d0ae6e5ba5003c623ad2d12d4226183dbb30aa8c5a4c95
|
|
7
|
+
data.tar.gz: 74a7e4a0cdd3f55e976bb948ee8bc8a9799b55ec076319c353720342ffb0b80e087f0229e1046e0fc6f4b74dae88ff019ad1231f6a8f127ddbdfff1cc73dba23
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class PaymentSessions::PaypalCheckout < PaymentSession
|
|
3
|
+
def paypal_order_id
|
|
4
|
+
external_id
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def paypal_capture_id
|
|
8
|
+
external_data&.dig('purchase_units', 0, 'payments', 'captures', 0, 'id')
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def paypal_payer
|
|
12
|
+
external_data&.dig('payer')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def paypal_payment_source
|
|
16
|
+
external_data&.dig('payment_source')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def accepted?
|
|
20
|
+
external_data&.dig('status') == 'COMPLETED'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def successful?
|
|
24
|
+
accepted?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Creates or finds the Spree::Payment for this session.
|
|
28
|
+
# Defers creation until paypal_capture_id is present so response_code
|
|
29
|
+
# is always the capture ID (required for refunds via Gateway#credit).
|
|
30
|
+
def find_or_create_payment!(metadata = {})
|
|
31
|
+
return unless persisted?
|
|
32
|
+
return payment if payment.present?
|
|
33
|
+
return unless paypal_capture_id
|
|
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
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def create_payment_source!
|
|
59
|
+
paypal_data = paypal_payment_source&.dig('paypal')
|
|
60
|
+
return nil unless paypal_data
|
|
61
|
+
|
|
62
|
+
source = SpreePaypalCheckout::PaymentSources::Paypal.find_or_initialize_by(
|
|
63
|
+
payment_method: payment_method,
|
|
64
|
+
gateway_payment_profile_id: paypal_data['account_id']
|
|
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
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class PaymentSetupSessions::PaypalCheckout < PaymentSetupSession
|
|
3
|
+
# PayPal's Vault API uses setup tokens (temporary) that get exchanged for
|
|
4
|
+
# permanent payment tokens once the buyer approves on PayPal.
|
|
5
|
+
def paypal_setup_token_id
|
|
6
|
+
external_id
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def paypal_payment_token_id
|
|
10
|
+
external_data&.dig('payment_token', 'id')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The approve link the storefront redirects the buyer to.
|
|
14
|
+
def approve_link
|
|
15
|
+
Array(external_data&.dig('links')).find { |l| l['rel'] == 'approve' }&.dig('href')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def successful?
|
|
19
|
+
status == 'completed'
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
|
|
3
|
+
module SpreePaypalCheckout
|
|
4
|
+
class Gateway < ::Spree::Gateway
|
|
5
|
+
module PaymentSessions
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
def session_required?
|
|
9
|
+
!SpreePaypalCheckout::Config[:use_legacy_api]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def payment_session_class
|
|
13
|
+
Spree::PaymentSessions::PaypalCheckout
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Creates a PayPal order via the PayPal Orders API and persists
|
|
17
|
+
# a Spree::PaymentSessions::PaypalCheckout record.
|
|
18
|
+
def create_payment_session(order:, amount: nil, external_data: {})
|
|
19
|
+
total = amount.presence || order.total_minus_store_credits
|
|
20
|
+
|
|
21
|
+
return nil if total.zero?
|
|
22
|
+
|
|
23
|
+
protect_from_error do
|
|
24
|
+
order_presenter = SpreePaypalCheckout::OrderPresenter.new(order)
|
|
25
|
+
paypal_response = client.orders.create_order(order_presenter.to_json)
|
|
26
|
+
|
|
27
|
+
payment_session_class.create!(
|
|
28
|
+
order: order,
|
|
29
|
+
payment_method: self,
|
|
30
|
+
amount: total,
|
|
31
|
+
currency: order.currency,
|
|
32
|
+
status: 'pending',
|
|
33
|
+
external_id: paypal_response.data.id,
|
|
34
|
+
customer: order.user,
|
|
35
|
+
external_data: paypal_response.data.as_json
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def update_payment_session(payment_session:, amount: nil, external_data: {})
|
|
41
|
+
attrs = {}
|
|
42
|
+
attrs[:amount] = amount if amount.present?
|
|
43
|
+
|
|
44
|
+
if external_data.present?
|
|
45
|
+
attrs[:external_data] = (payment_session.external_data || {}).merge(external_data.stringify_keys)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
payment_session.update!(attrs) if attrs.any?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Completes a payment session by capturing the PayPal order,
|
|
52
|
+
# creating the Payment record, and transitioning the session.
|
|
53
|
+
#
|
|
54
|
+
# Does NOT complete the order -- that is handled by Carts::Complete
|
|
55
|
+
# (called by the storefront or by the webhook handler).
|
|
56
|
+
def complete_payment_session(payment_session:, params: {})
|
|
57
|
+
paypal_order_id = payment_session.external_id
|
|
58
|
+
|
|
59
|
+
response = client.orders.capture_order({
|
|
60
|
+
'id' => paypal_order_id,
|
|
61
|
+
'prefer' => 'return=representation'
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
# Persist capture data outside the lock so it survives if post-capture bookkeeping fails
|
|
65
|
+
payment_session.update!(external_data: response.data.as_json)
|
|
66
|
+
|
|
67
|
+
payment_session.order.with_lock do
|
|
68
|
+
if response.data.status == 'COMPLETED'
|
|
69
|
+
payment_session.process if payment_session.can_process?
|
|
70
|
+
|
|
71
|
+
payment = payment_session.find_or_create_payment!
|
|
72
|
+
|
|
73
|
+
if payment.present? && !payment.completed?
|
|
74
|
+
payment.started_processing! if payment.checkout?
|
|
75
|
+
payment.complete! if payment.can_complete?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
create_profile(payment) if payment&.source.present?
|
|
79
|
+
|
|
80
|
+
payment_session.complete unless payment_session.completed?
|
|
81
|
+
else
|
|
82
|
+
payment_session.fail if payment_session.can_fail?
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
rescue PaypalServerSdk::APIException => e
|
|
86
|
+
payment_session.fail if payment_session.can_fail?
|
|
87
|
+
raise Spree::Core::GatewayError, "PayPal API error: #{e.message}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Parses incoming PayPal webhook events.
|
|
91
|
+
def parse_webhook_event(raw_body, headers)
|
|
92
|
+
verify_webhook_signature!(raw_body, headers)
|
|
93
|
+
|
|
94
|
+
event = JSON.parse(raw_body).with_indifferent_access
|
|
95
|
+
event_type = event[:event_type]
|
|
96
|
+
resource = event[:resource] || {}
|
|
97
|
+
|
|
98
|
+
paypal_order_id = extract_order_id_from_webhook(event_type, resource)
|
|
99
|
+
return nil unless paypal_order_id
|
|
100
|
+
|
|
101
|
+
payment_session = Spree::PaymentSessions::PaypalCheckout.find_by(
|
|
102
|
+
payment_method: self,
|
|
103
|
+
external_id: paypal_order_id
|
|
104
|
+
)
|
|
105
|
+
return nil unless payment_session
|
|
106
|
+
|
|
107
|
+
case event_type
|
|
108
|
+
when 'CHECKOUT.ORDER.APPROVED'
|
|
109
|
+
{ action: :authorized, payment_session: payment_session, metadata: { paypal_event: event } }
|
|
110
|
+
when 'PAYMENT.CAPTURE.COMPLETED'
|
|
111
|
+
{ action: :captured, payment_session: payment_session, metadata: { paypal_event: event } }
|
|
112
|
+
when 'PAYMENT.CAPTURE.DENIED', 'PAYMENT.CAPTURE.DECLINED'
|
|
113
|
+
{ action: :failed, payment_session: payment_session, metadata: { paypal_event: event } }
|
|
114
|
+
when 'PAYMENT.CAPTURE.REVERSED', 'PAYMENT.CAPTURE.REFUNDED'
|
|
115
|
+
{ action: :canceled, payment_session: payment_session, metadata: { paypal_event: event } }
|
|
116
|
+
else
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def extract_order_id_from_webhook(event_type, resource)
|
|
124
|
+
case event_type
|
|
125
|
+
when /\ACHECKOUT\.ORDER\./
|
|
126
|
+
resource['id']
|
|
127
|
+
when /\APAYMENT\.CAPTURE\./
|
|
128
|
+
resource.dig('supplementary_data', 'related_ids', 'order_id')
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def verify_webhook_signature!(raw_body, headers)
|
|
133
|
+
if preferred_webhook_secret.blank?
|
|
134
|
+
return if Rails.env.development? || Rails.env.test?
|
|
135
|
+
|
|
136
|
+
raise Spree::PaymentMethod::WebhookSignatureError,
|
|
137
|
+
'PayPal webhook_secret is not configured'
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
transmission_id = headers['HTTP_PAYPAL_TRANSMISSION_ID'] || headers['PAYPAL-TRANSMISSION-ID']
|
|
141
|
+
transmission_time = headers['HTTP_PAYPAL_TRANSMISSION_TIME'] || headers['PAYPAL-TRANSMISSION-TIME']
|
|
142
|
+
cert_url = headers['HTTP_PAYPAL_CERT_URL'] || headers['PAYPAL-CERT-URL']
|
|
143
|
+
auth_algo = headers['HTTP_PAYPAL_AUTH_ALGO'] || headers['PAYPAL-AUTH-ALGO']
|
|
144
|
+
transmission_sig = headers['HTTP_PAYPAL_TRANSMISSION_SIG'] || headers['PAYPAL-TRANSMISSION-SIG']
|
|
145
|
+
|
|
146
|
+
unless transmission_id && transmission_sig
|
|
147
|
+
raise Spree::PaymentMethod::WebhookSignatureError, 'Missing PayPal webhook headers'
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
token = obtain_access_token
|
|
151
|
+
|
|
152
|
+
api_base = preferred_test_mode ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com'
|
|
153
|
+
uri = URI("#{api_base}/v1/notifications/verify-webhook-signature")
|
|
154
|
+
|
|
155
|
+
payload = {
|
|
156
|
+
auth_algo: auth_algo,
|
|
157
|
+
cert_url: cert_url,
|
|
158
|
+
transmission_id: transmission_id,
|
|
159
|
+
transmission_sig: transmission_sig,
|
|
160
|
+
transmission_time: transmission_time,
|
|
161
|
+
webhook_id: preferred_webhook_secret,
|
|
162
|
+
webhook_event: JSON.parse(raw_body)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
166
|
+
http.use_ssl = true
|
|
167
|
+
http.open_timeout = 5
|
|
168
|
+
http.read_timeout = 10
|
|
169
|
+
request = Net::HTTP::Post.new(uri.path, {
|
|
170
|
+
'Content-Type' => 'application/json',
|
|
171
|
+
'Authorization' => "Bearer #{token}"
|
|
172
|
+
})
|
|
173
|
+
request.body = payload.to_json
|
|
174
|
+
|
|
175
|
+
response = http.request(request)
|
|
176
|
+
result = JSON.parse(response.body)
|
|
177
|
+
|
|
178
|
+
unless result['verification_status'] == 'SUCCESS'
|
|
179
|
+
raise Spree::PaymentMethod::WebhookSignatureError, 'Invalid webhook signature'
|
|
180
|
+
end
|
|
181
|
+
rescue Spree::PaymentMethod::WebhookSignatureError
|
|
182
|
+
raise
|
|
183
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, SocketError, JSON::ParserError, Errno::ECONNREFUSED => e
|
|
184
|
+
raise Spree::PaymentMethod::WebhookSignatureError, "Webhook verification failed: #{e.message}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def obtain_access_token
|
|
188
|
+
api_base = preferred_test_mode ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com'
|
|
189
|
+
uri = URI("#{api_base}/v1/oauth2/token")
|
|
190
|
+
|
|
191
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
192
|
+
http.use_ssl = true
|
|
193
|
+
http.open_timeout = 5
|
|
194
|
+
http.read_timeout = 10
|
|
195
|
+
request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/x-www-form-urlencoded' })
|
|
196
|
+
request.basic_auth(preferred_client_id, preferred_client_secret)
|
|
197
|
+
request.body = 'grant_type=client_credentials'
|
|
198
|
+
|
|
199
|
+
response = http.request(request)
|
|
200
|
+
JSON.parse(response.body)['access_token']
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module SpreePaypalCheckout
|
|
2
|
+
class Gateway < ::Spree::Gateway
|
|
3
|
+
module PaymentSetupSessions
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def setup_session_supported?
|
|
7
|
+
true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def payment_setup_session_class
|
|
11
|
+
Spree::PaymentSetupSessions::PaypalCheckout
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Creates a PayPal Vault setup token (temporary, awaiting buyer approval)
|
|
15
|
+
# and persists a Spree::PaymentSetupSessions::PaypalCheckout record.
|
|
16
|
+
#
|
|
17
|
+
# @param customer [Spree::User] the customer to vault the PayPal account for
|
|
18
|
+
# @param external_data [Hash] optional :return_url and :cancel_url for the
|
|
19
|
+
# PayPal redirect flow
|
|
20
|
+
# @return [Spree::PaymentSetupSessions::PaypalCheckout]
|
|
21
|
+
def create_payment_setup_session(customer:, external_data: {})
|
|
22
|
+
return_url = external_data[:return_url] || external_data['return_url'] || default_setup_return_url
|
|
23
|
+
cancel_url = external_data[:cancel_url] || external_data['cancel_url'] || default_setup_cancel_url
|
|
24
|
+
|
|
25
|
+
protect_from_error do
|
|
26
|
+
response = client.vault.create_setup_token(
|
|
27
|
+
SpreePaypalCheckout::SetupTokenPresenter.new(
|
|
28
|
+
customer: customer,
|
|
29
|
+
return_url: return_url,
|
|
30
|
+
cancel_url: cancel_url
|
|
31
|
+
).to_h
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
payment_setup_session_class.create!(
|
|
35
|
+
customer: customer,
|
|
36
|
+
payment_method: self,
|
|
37
|
+
status: 'pending',
|
|
38
|
+
external_id: response.data.id,
|
|
39
|
+
external_data: response.data.as_json.merge(
|
|
40
|
+
'return_url' => return_url,
|
|
41
|
+
'cancel_url' => cancel_url
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Completes a setup session by exchanging the approved setup token for a
|
|
48
|
+
# permanent payment token, creates a Spree::PaymentSource representing the
|
|
49
|
+
# vaulted PayPal account, and transitions the session.
|
|
50
|
+
#
|
|
51
|
+
# @param setup_session [Spree::PaymentSetupSessions::PaypalCheckout]
|
|
52
|
+
# @param params [Hash] unused — PayPal needs no params beyond the setup_session
|
|
53
|
+
def complete_payment_setup_session(setup_session:, params: {})
|
|
54
|
+
protect_from_error do
|
|
55
|
+
response = client.vault.create_payment_token(
|
|
56
|
+
'body' => {
|
|
57
|
+
'payment_source' => {
|
|
58
|
+
'token' => {
|
|
59
|
+
'id' => setup_session.external_id,
|
|
60
|
+
'type' => 'SETUP_TOKEN'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
payment_token = response.data
|
|
67
|
+
|
|
68
|
+
setup_session.payment_source = build_payment_source_from_token(setup_session, payment_token)
|
|
69
|
+
setup_session.update!(
|
|
70
|
+
external_data: setup_session.external_data.to_h.merge('payment_token' => payment_token.as_json)
|
|
71
|
+
)
|
|
72
|
+
setup_session.complete if setup_session.can_complete?
|
|
73
|
+
rescue PaypalServerSdk::APIException => e
|
|
74
|
+
setup_session.fail if setup_session.can_fail?
|
|
75
|
+
raise Spree::Core::GatewayError, "PayPal Vault error: #{e.message}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
setup_session
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def build_payment_source_from_token(setup_session, payment_token)
|
|
84
|
+
paypal = payment_token.as_json.dig('payment_source', 'paypal') || {}
|
|
85
|
+
account_id = paypal['email_address'] || payment_token.id
|
|
86
|
+
|
|
87
|
+
source = SpreePaypalCheckout::PaymentSources::Paypal.find_or_initialize_by(
|
|
88
|
+
payment_method: self,
|
|
89
|
+
gateway_payment_profile_id: payment_token.id
|
|
90
|
+
)
|
|
91
|
+
source.update!(
|
|
92
|
+
user: setup_session.customer,
|
|
93
|
+
email: paypal['email_address'],
|
|
94
|
+
name: [paypal.dig('name', 'given_name'), paypal.dig('name', 'surname')].compact.join(' ').strip.presence,
|
|
95
|
+
account_id: account_id,
|
|
96
|
+
account_status: paypal['account_status']
|
|
97
|
+
)
|
|
98
|
+
source
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def default_setup_return_url
|
|
102
|
+
store = stores.first
|
|
103
|
+
return nil unless store
|
|
104
|
+
|
|
105
|
+
"#{store.storefront_url}/paypal/payment_setup_sessions/return"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def default_setup_cancel_url
|
|
109
|
+
store = stores.first
|
|
110
|
+
return nil unless store
|
|
111
|
+
|
|
112
|
+
"#{store.storefront_url}/paypal/payment_setup_sessions/cancel"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module SpreePaypalCheckout
|
|
2
2
|
class Gateway < ::Spree::Gateway
|
|
3
3
|
include PaypalServerSdk
|
|
4
|
+
include PaymentSessions
|
|
4
5
|
|
|
5
6
|
GatewayResponse = Struct.new(:success, :message, :params, :authorization) do
|
|
6
7
|
alias_method :success?, :success
|
|
@@ -11,6 +12,7 @@ module SpreePaypalCheckout
|
|
|
11
12
|
#
|
|
12
13
|
preference :client_id, :password
|
|
13
14
|
preference :client_secret, :password
|
|
15
|
+
preference :webhook_secret, :string
|
|
14
16
|
preference :test_mode, :boolean, default: true
|
|
15
17
|
|
|
16
18
|
#
|
|
@@ -54,6 +56,13 @@ module SpreePaypalCheckout
|
|
|
54
56
|
'paypal_checkout'
|
|
55
57
|
end
|
|
56
58
|
|
|
59
|
+
def webhook_url
|
|
60
|
+
store = stores.first
|
|
61
|
+
return nil unless store
|
|
62
|
+
|
|
63
|
+
"#{store.formatted_url}/api/v3/webhooks/payments/#{prefixed_id}"
|
|
64
|
+
end
|
|
65
|
+
|
|
57
66
|
def create_profile(payment)
|
|
58
67
|
user = payment.order.user
|
|
59
68
|
return if user.blank?
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module SpreePaypalCheckout
|
|
2
2
|
module PaymentSources
|
|
3
3
|
class Paypal < ::Spree::PaymentSource
|
|
4
|
-
store_accessor :
|
|
4
|
+
store_accessor :private_metadata, :email, :name, :account_status, :account_id
|
|
5
5
|
|
|
6
6
|
def actions
|
|
7
7
|
%w[credit void]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module SpreePaypalCheckout
|
|
2
|
+
# Builds the body of a PayPal Vault /v3/vault/setup-tokens request for tokenizing
|
|
3
|
+
# a buyer's PayPal account without an immediate purchase.
|
|
4
|
+
class SetupTokenPresenter
|
|
5
|
+
def initialize(customer:, return_url:, cancel_url:)
|
|
6
|
+
@customer = customer
|
|
7
|
+
@return_url = return_url
|
|
8
|
+
@cancel_url = cancel_url
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_h
|
|
12
|
+
{
|
|
13
|
+
'body' => {
|
|
14
|
+
'payment_source' => {
|
|
15
|
+
'paypal' => {
|
|
16
|
+
'usage_type' => 'MERCHANT',
|
|
17
|
+
'experience_context' => {
|
|
18
|
+
'return_url' => return_url,
|
|
19
|
+
'cancel_url' => cancel_url
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}.merge(customer_payload)
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :customer, :return_url, :cancel_url
|
|
30
|
+
|
|
31
|
+
def customer_payload
|
|
32
|
+
return {} unless customer&.id
|
|
33
|
+
|
|
34
|
+
{ 'customer' => { 'id' => "customer_#{customer.id}" } }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -37,10 +37,10 @@ module SpreePaypalCheckout
|
|
|
37
37
|
def create_paypal_source
|
|
38
38
|
source = SpreePaypalCheckout::PaymentSources::Paypal.find_or_create_by!(
|
|
39
39
|
payment_method: gateway,
|
|
40
|
-
user: user,
|
|
41
40
|
gateway_payment_profile_id: paypal_payment_source['paypal']['account_id']
|
|
42
41
|
)
|
|
43
42
|
source.update!(
|
|
43
|
+
user: user,
|
|
44
44
|
email: paypal_payment_source['paypal']['email_address'],
|
|
45
45
|
name: "#{paypal_payment_source['paypal']['name']['given_name']} #{paypal_payment_source['paypal']['name']['surname']}".strip,
|
|
46
46
|
account_id: paypal_payment_source['paypal']['account_id'],
|
data/app/views/spree/admin/payment_methods/configuration_guides/_spree_paypal_checkout.html.erb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<div class="alert alert-info">
|
|
2
2
|
<p class="mb-0">
|
|
3
3
|
To find your <strong>Client ID</strong> and <strong>Client Secret</strong>, go to the
|
|
4
|
-
<%= external_link_to 'PayPal
|
|
4
|
+
<%= external_link_to 'PayPal Developer Dashboard', 'https://developer.paypal.com/dashboard/', class: 'alert-link' %>.
|
|
5
|
+
For more information, see the <%= external_link_to 'PayPal REST API documentation', 'https://developer.paypal.com/api/rest/', class: 'alert-link' %>.
|
|
5
6
|
</p>
|
|
6
7
|
</div>
|
data/config/routes.rb
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
Spree::Core::Engine.add_routes do
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
# Storefront API v2 (only when spree_legacy_api_v2 gem is available)
|
|
3
|
+
if defined?(SpreeLegacyApiV2::Engine)
|
|
4
|
+
namespace :api, defaults: { format: 'json' } do
|
|
5
|
+
namespace :v2 do
|
|
6
|
+
namespace :storefront do
|
|
7
|
+
resources :paypal_orders, only: [:create] do
|
|
8
|
+
member do
|
|
9
|
+
put :capture
|
|
10
|
+
end
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
module SpreePaypalCheckout
|
|
2
2
|
class Configuration < Spree::Preferences::Configuration
|
|
3
|
-
|
|
4
|
-
# Some example preferences are shown below, for more information visit:
|
|
5
|
-
# https://docs.spreecommerce.org/developer/contributing/creating-an-extension
|
|
6
|
-
|
|
7
|
-
# preference :enabled, :boolean, default: true
|
|
8
|
-
# preference :dark_chocolate, :boolean, default: true
|
|
9
|
-
# preference :color, :string, default: 'Red'
|
|
10
|
-
# preference :favorite_number, :integer
|
|
11
|
-
# preference :supported_locales, :array, default: [:en]
|
|
3
|
+
preference :use_legacy_api, :boolean, default: false
|
|
12
4
|
end
|
|
13
5
|
end
|
|
@@ -4,6 +4,12 @@ module SpreePaypalCheckout
|
|
|
4
4
|
isolate_namespace Spree
|
|
5
5
|
engine_name 'spree_paypal_checkout'
|
|
6
6
|
|
|
7
|
+
# Only load API v2 controllers and serializers when spree_legacy_api_v2 gem is available
|
|
8
|
+
if defined?(SpreeLegacyApiV2::Engine)
|
|
9
|
+
config.autoload_paths << root.join('lib', 'spree_api_v2')
|
|
10
|
+
config.eager_load_paths << root.join('lib', 'spree_api_v2')
|
|
11
|
+
end
|
|
12
|
+
|
|
7
13
|
# use rspec for tests
|
|
8
14
|
config.generators do |g|
|
|
9
15
|
g.test_framework :rspec
|
|
@@ -20,6 +20,21 @@ FactoryBot.define do
|
|
|
20
20
|
gateway_customer_profile_id { 'PAY-CUSTOMER-ID' }
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
factory :paypal_checkout_payment_session, class: 'Spree::PaymentSessions::PaypalCheckout' do
|
|
24
|
+
association :order, factory: :order
|
|
25
|
+
association :payment_method, factory: :paypal_checkout_gateway
|
|
26
|
+
amount { order.total }
|
|
27
|
+
currency { order.currency }
|
|
28
|
+
status { 'pending' }
|
|
29
|
+
external_id { "PAYPAL-ORDER-#{SecureRandom.hex(8).upcase}" }
|
|
30
|
+
external_data { JSON.parse(File.read(SpreePaypalCheckout::Engine.root.join('spec', 'fixtures', 'paypal_order.json'))) }
|
|
31
|
+
|
|
32
|
+
factory :completed_paypal_checkout_payment_session do
|
|
33
|
+
status { 'completed' }
|
|
34
|
+
external_data { JSON.parse(File.read(SpreePaypalCheckout::Engine.root.join('spec', 'fixtures', 'captured_paypal_order.json'))) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
23
38
|
factory :paypal_checkout_order, class: 'SpreePaypalCheckout::Order' do
|
|
24
39
|
paypal_id { 'PAY-ORDER-ID' }
|
|
25
40
|
order { create(:order) }
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_paypal_checkout
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vendo Connect Inc.
|
|
@@ -15,28 +15,28 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 5.
|
|
18
|
+
version: 5.4.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 5.
|
|
25
|
+
version: 5.4.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: spree_admin
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 5.
|
|
32
|
+
version: 5.4.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 5.
|
|
39
|
+
version: 5.4.0
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: paypal-server-sdk
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -116,20 +116,23 @@ files:
|
|
|
116
116
|
- README.md
|
|
117
117
|
- Rakefile
|
|
118
118
|
- app/assets/config/spree_paypal_checkout_manifest.js
|
|
119
|
-
- app/controllers/spree/api/v2/storefront/paypal_orders_controller.rb
|
|
120
119
|
- app/controllers/spree_paypal_checkout/store_controller_decorator.rb
|
|
121
120
|
- app/helpers/spree_paypal_checkout/base_helper.rb
|
|
122
121
|
- app/javascript/spree_paypal_checkout/application.js
|
|
123
122
|
- app/javascript/spree_paypal_checkout/controllers/checkout_paypal_controller.js
|
|
123
|
+
- app/models/spree/payment_sessions/paypal_checkout.rb
|
|
124
|
+
- app/models/spree/payment_setup_sessions/paypal_checkout.rb
|
|
124
125
|
- app/models/spree_paypal_checkout/base.rb
|
|
125
126
|
- app/models/spree_paypal_checkout/gateway.rb
|
|
127
|
+
- app/models/spree_paypal_checkout/gateway/payment_sessions.rb
|
|
128
|
+
- app/models/spree_paypal_checkout/gateway/payment_setup_sessions.rb
|
|
126
129
|
- app/models/spree_paypal_checkout/order.rb
|
|
127
130
|
- app/models/spree_paypal_checkout/order_decorator.rb
|
|
128
131
|
- app/models/spree_paypal_checkout/payment_method_decorator.rb
|
|
129
132
|
- app/models/spree_paypal_checkout/payment_sources/paypal.rb
|
|
130
133
|
- app/models/spree_paypal_checkout/store_decorator.rb
|
|
131
134
|
- app/presenters/spree_paypal_checkout/order_presenter.rb
|
|
132
|
-
- app/
|
|
135
|
+
- app/presenters/spree_paypal_checkout/setup_token_presenter.rb
|
|
133
136
|
- app/services/spree_paypal_checkout/capture_order.rb
|
|
134
137
|
- app/services/spree_paypal_checkout/create_payment.rb
|
|
135
138
|
- app/services/spree_paypal_checkout/create_source.rb
|
|
@@ -145,6 +148,8 @@ files:
|
|
|
145
148
|
- config/routes.rb
|
|
146
149
|
- db/migrate/20250528095719_create_spree_paypal_checkout_orders.rb
|
|
147
150
|
- lib/generators/spree_paypal_checkout/install/install_generator.rb
|
|
151
|
+
- lib/spree_api_v2/spree/api/v2/storefront/paypal_order_serializer.rb
|
|
152
|
+
- lib/spree_api_v2/spree/api/v2/storefront/paypal_orders_controller.rb
|
|
148
153
|
- lib/spree_paypal_checkout.rb
|
|
149
154
|
- lib/spree_paypal_checkout/configuration.rb
|
|
150
155
|
- lib/spree_paypal_checkout/engine.rb
|
|
@@ -155,9 +160,9 @@ licenses:
|
|
|
155
160
|
- MIT
|
|
156
161
|
metadata:
|
|
157
162
|
bug_tracker_uri: https://github.com/spree/spree_paypal_checkout/issues
|
|
158
|
-
changelog_uri: https://github.com/spree/spree_paypal_checkout/releases/tag/v0.
|
|
163
|
+
changelog_uri: https://github.com/spree/spree_paypal_checkout/releases/tag/v0.7.1
|
|
159
164
|
documentation_uri: https://docs.spreecommerce.org/
|
|
160
|
-
source_code_uri: https://github.com/spree/spree_paypal_checkout/tree/v0.
|
|
165
|
+
source_code_uri: https://github.com/spree/spree_paypal_checkout/tree/v0.7.1
|
|
161
166
|
rdoc_options: []
|
|
162
167
|
require_paths:
|
|
163
168
|
- lib
|
/data/{app/serializers → lib/spree_api_v2}/spree/api/v2/storefront/paypal_order_serializer.rb
RENAMED
|
File without changes
|
/data/{app/controllers → lib/spree_api_v2}/spree/api/v2/storefront/paypal_orders_controller.rb
RENAMED
|
File without changes
|