spree_razorpay_checkout 0.3.1 → 0.3.2
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/controllers/spree/razorpay/_webhooks_controller.rb +78 -0
- data/app/models/spree/gateway/razorpay_gateway.rb +58 -18
- data/app/models/spree/payment_sessions/razorpay.rb +4 -7
- data/config/routes.rb +3 -6
- data/lib/spree_razorpay_checkout/version.rb +1 -1
- metadata +3 -3
- data/app/controllers/spree/razorpay/webhooks_controller.rb +0 -72
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 59ea8bc12e162b79eacdbefce1a1ee167cd9a4d4e7c2cd7cd3bd53ce47a8d716
|
|
4
|
+
data.tar.gz: a4b9a022d525bb254c0b932f415ebedf63dbfc4aac3be42d648546163b149d87
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc7bedce60a230b32703ff7e8613153f9eee91eee5554b51c42e7de2aa235799038825b5b103eddd4948e26e9ac8a13850d2353e1d240b514b839dd58c686fc2
|
|
7
|
+
data.tar.gz: eb72bd494b0f178939f8ad4463281ec3bb793f8bf6ef0bb0c751b8d1fa2c976ec1b42b1a9a81b774fff1680c411b783f2a2c31327f679c1d842a35dd63ce6bc1
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class RazorpayWebhooksController < ActionController::API
|
|
3
|
+
skip_before_action :verify_authenticity_token, raise: false
|
|
4
|
+
|
|
5
|
+
# Handles asynchronous background webhooks directly from Razorpay's servers
|
|
6
|
+
def create
|
|
7
|
+
payload = request.body.read
|
|
8
|
+
signature = request.headers['X-Razorpay-Signature'] || request.headers['HTTP_X_RAZORPAY_SIGNATURE']
|
|
9
|
+
|
|
10
|
+
gateway = Spree::Gateway::RazorpayGateway.active.first
|
|
11
|
+
return head :not_found unless gateway && gateway.preferred_webhook_secret.present?
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
# 1. Use the gateway's parser to verify signature and map the action
|
|
15
|
+
parsed_event = gateway.parse_webhook_event(payload, { 'HTTP_X_RAZORPAY_SIGNATURE' => signature })
|
|
16
|
+
|
|
17
|
+
# If it's an event we don't care about, acknowledge and return
|
|
18
|
+
return head :ok unless parsed_event
|
|
19
|
+
|
|
20
|
+
# 2. Extract Data
|
|
21
|
+
payment_session = parsed_event[:payment_session]
|
|
22
|
+
action = parsed_event[:action]
|
|
23
|
+
|
|
24
|
+
event_data = JSON.parse(payload)
|
|
25
|
+
metadata = event_data.dig('payload', 'payment', 'entity') || {}
|
|
26
|
+
|
|
27
|
+
# 3. Trigger the Core Spree 5.5 Webhook Handler Service
|
|
28
|
+
Spree::Payments::HandleWebhook.call(
|
|
29
|
+
payment_method: gateway,
|
|
30
|
+
action: action,
|
|
31
|
+
payment_session: payment_session,
|
|
32
|
+
metadata: metadata
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
rescue Spree::PaymentMethod::WebhookSignatureError => e
|
|
36
|
+
Rails.logger.error("Razorpay Webhook Verification Failed: #{e.message}")
|
|
37
|
+
return head :unauthorized
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
Rails.logger.error("Razorpay Webhook Processing Error: #{e.message}")
|
|
40
|
+
return head :unprocessable_entity
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
head :ok
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Handles synchronous frontend verification from Next.js Storefront
|
|
47
|
+
def verify
|
|
48
|
+
razorpay_order_id = params[:razorpay_order_id]
|
|
49
|
+
razorpay_payment_id = params[:razorpay_payment_id]
|
|
50
|
+
razorpay_signature = params[:razorpay_signature]
|
|
51
|
+
|
|
52
|
+
session = Spree::PaymentSession.find_by(external_id: razorpay_order_id)
|
|
53
|
+
return head :not_found unless session
|
|
54
|
+
|
|
55
|
+
gateway = Spree::Gateway::RazorpayGateway.active.first
|
|
56
|
+
return render json: { success: false, error: 'Gateway not configured' }, status: :internal_server_error unless gateway
|
|
57
|
+
gateway.provider
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
::Razorpay::Utility.verify_payment_signature(
|
|
61
|
+
razorpay_order_id: razorpay_order_id,
|
|
62
|
+
razorpay_payment_id: razorpay_payment_id,
|
|
63
|
+
razorpay_signature: razorpay_signature
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
session.external_data ||= {}
|
|
67
|
+
session.external_data['razorpay_payment_id'] = razorpay_payment_id
|
|
68
|
+
session.external_data['razorpay_signature'] = razorpay_signature
|
|
69
|
+
session.save!
|
|
70
|
+
|
|
71
|
+
render json: { success: true }
|
|
72
|
+
rescue ::Razorpay::Errors::SignatureVerificationError => e
|
|
73
|
+
Rails.logger.error("Razorpay Frontend Verification Failed: #{e.message}")
|
|
74
|
+
render json: { success: false, error: e.message }, status: :unprocessable_entity
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -14,6 +14,9 @@ module Spree
|
|
|
14
14
|
preference :merchant_address, :string, default: 'Razorpay, Bangalore, India'
|
|
15
15
|
preference :theme_color, :string, default: '#2e5bff'
|
|
16
16
|
preference :headless_api_mode, :boolean, default: false
|
|
17
|
+
|
|
18
|
+
# NEW: Allow manual capture vs auto-capture choice from Spree Admin Panel
|
|
19
|
+
preference :auto_capture, :boolean, default: true
|
|
17
20
|
|
|
18
21
|
def name
|
|
19
22
|
'Razorpay Secure (UPI, Wallets, Cards & Netbanking)'
|
|
@@ -52,7 +55,7 @@ module Spree
|
|
|
52
55
|
end
|
|
53
56
|
|
|
54
57
|
def auto_capture?
|
|
55
|
-
|
|
58
|
+
preferred_auto_capture
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
def request_type
|
|
@@ -95,7 +98,9 @@ module Spree
|
|
|
95
98
|
Spree::PaymentSessions::Razorpay if defined?(Spree::PaymentSession)
|
|
96
99
|
end
|
|
97
100
|
|
|
101
|
+
# =========================================================================
|
|
98
102
|
# SPREE 5.4+ HEADLESS API FLOW (Protected by defined? check)
|
|
103
|
+
# =========================================================================
|
|
99
104
|
|
|
100
105
|
if defined?(Spree::PaymentSession)
|
|
101
106
|
def create_payment_session(order:, amount: nil, external_data: {})
|
|
@@ -107,7 +112,7 @@ module Spree
|
|
|
107
112
|
amount: amount_in_cents,
|
|
108
113
|
currency: order.currency || 'INR',
|
|
109
114
|
receipt: order.number,
|
|
110
|
-
payment_capture: 1,
|
|
115
|
+
payment_capture: auto_capture? ? 1 : 0, # UPDATED: Respects Manual Capture
|
|
111
116
|
notes: { spree_order_number: order.number, email: order.email }
|
|
112
117
|
)
|
|
113
118
|
|
|
@@ -140,7 +145,7 @@ module Spree
|
|
|
140
145
|
amount: amount_in_cents,
|
|
141
146
|
currency: payment_session.currency,
|
|
142
147
|
receipt: payment_session.order.number,
|
|
143
|
-
payment_capture: 1
|
|
148
|
+
payment_capture: auto_capture? ? 1 : 0 # UPDATED: Respects Manual Capture
|
|
144
149
|
)
|
|
145
150
|
|
|
146
151
|
payment_session.update!(amount: amount, external_id: new_rzp_order.id)
|
|
@@ -151,48 +156,60 @@ module Spree
|
|
|
151
156
|
def complete_payment_session(payment_session:, params: {})
|
|
152
157
|
provider
|
|
153
158
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
# Robustly parse the JSON string sent from Next.js via the Spree API
|
|
160
|
+
result_data = {}
|
|
161
|
+
if params[:session_result].present?
|
|
162
|
+
begin
|
|
163
|
+
result_data = params[:session_result].is_a?(String) ? JSON.parse(params[:session_result]) : params[:session_result]
|
|
164
|
+
rescue JSON::ParserError
|
|
165
|
+
result_data = {}
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
rzp_payment_id = result_data['razorpay_payment_id'] || result_data[:razorpay_payment_id] || payment_session.external_data['razorpay_payment_id']
|
|
170
|
+
rzp_signature = result_data['razorpay_signature'] || result_data[:razorpay_signature] || payment_session.external_data['razorpay_signature']
|
|
157
171
|
|
|
158
172
|
begin
|
|
173
|
+
# Verify the Signature
|
|
159
174
|
::Razorpay::Utility.verify_payment_signature(
|
|
160
175
|
razorpay_order_id: payment_session.external_id,
|
|
161
176
|
razorpay_payment_id: rzp_payment_id,
|
|
162
177
|
razorpay_signature: rzp_signature
|
|
163
178
|
)
|
|
164
179
|
|
|
165
|
-
|
|
180
|
+
# Lock the order database row to prevent race conditions with Webhooks
|
|
166
181
|
payment_session.order.with_lock do
|
|
167
182
|
|
|
168
|
-
# Save the verified IDs into the session
|
|
183
|
+
# Save the verified IDs into the session
|
|
169
184
|
session_data = payment_session.external_data || {}
|
|
170
185
|
session_data['razorpay_payment_id'] = rzp_payment_id
|
|
171
186
|
session_data['razorpay_signature'] = rzp_signature
|
|
172
187
|
payment_session.update!(external_data: session_data)
|
|
173
188
|
|
|
174
|
-
#
|
|
189
|
+
# Fetch payment status directly from Razorpay for instant frontend resolution
|
|
175
190
|
rzp_payment = ::Razorpay::Payment.fetch(rzp_payment_id)
|
|
176
|
-
if rzp_payment.status == 'authorized'
|
|
177
|
-
rzp_payment.capture({ amount: (payment_session.amount.to_f * 100).to_i })
|
|
178
|
-
end
|
|
179
191
|
|
|
180
192
|
# Process and Create Payment
|
|
181
193
|
payment_session.process! if payment_session.can_process?
|
|
182
194
|
|
|
183
|
-
# This safely calls your new Spree::PaymentSessions::Razorpay class!
|
|
184
195
|
payment = payment_session.find_or_create_payment!
|
|
185
196
|
|
|
186
197
|
if payment.present? && !payment.completed?
|
|
187
198
|
payment.started_processing! if payment.checkout?
|
|
188
|
-
|
|
199
|
+
|
|
200
|
+
# SYNCHRONOUS FIX: Immediately transition payment state so Next.js can complete the order
|
|
201
|
+
if rzp_payment.status == 'captured'
|
|
202
|
+
payment.complete! if payment.can_complete?
|
|
203
|
+
elsif rzp_payment.status == 'authorized'
|
|
204
|
+
payment.pend! if payment.can_pend?
|
|
205
|
+
end
|
|
189
206
|
end
|
|
190
207
|
|
|
191
208
|
payment_session.complete! if payment_session.can_complete?
|
|
192
209
|
end
|
|
193
210
|
|
|
194
211
|
rescue StandardError => e
|
|
195
|
-
Rails.logger.error("Razorpay 5.4 API Completion Failed: #{e.message}")
|
|
212
|
+
Rails.logger.error("Razorpay 5.4+ API Completion Failed: #{e.message}")
|
|
196
213
|
payment_session.fail! if payment_session.can_fail?
|
|
197
214
|
raise Spree::Core::GatewayError, e.message
|
|
198
215
|
end
|
|
@@ -212,9 +229,12 @@ module Spree
|
|
|
212
229
|
session = Spree::PaymentSession.find_by(external_id: payment_entity['order_id'])
|
|
213
230
|
return nil unless session
|
|
214
231
|
|
|
232
|
+
# UPDATED: Split authorized vs captured to align with new Spree webhook architecture
|
|
215
233
|
case event['event']
|
|
216
|
-
when 'payment.captured', '
|
|
234
|
+
when 'payment.captured', 'order.paid'
|
|
217
235
|
{ action: :captured, payment_session: session }
|
|
236
|
+
when 'payment.authorized'
|
|
237
|
+
{ action: :authorized, payment_session: session }
|
|
218
238
|
when 'payment.failed'
|
|
219
239
|
{ action: :failed, payment_session: session }
|
|
220
240
|
else
|
|
@@ -225,6 +245,10 @@ module Spree
|
|
|
225
245
|
end
|
|
226
246
|
end
|
|
227
247
|
|
|
248
|
+
# =========================================================================
|
|
249
|
+
# LEGACY / ACTIVEMERCHANT FLOW
|
|
250
|
+
# =========================================================================
|
|
251
|
+
|
|
228
252
|
def purchase(_amount, source, _gateway_options = {})
|
|
229
253
|
provider
|
|
230
254
|
|
|
@@ -271,8 +295,24 @@ module Spree
|
|
|
271
295
|
end
|
|
272
296
|
end
|
|
273
297
|
|
|
274
|
-
|
|
275
|
-
|
|
298
|
+
# UPDATED: Fully implemented Capture method so you can click "Capture" inside Spree Admin
|
|
299
|
+
def capture(amount_in_cents, response_code, gateway_options = {})
|
|
300
|
+
provider
|
|
301
|
+
begin
|
|
302
|
+
rzp_payment_id = resolve_razorpay_payment_id(response_code)
|
|
303
|
+
|
|
304
|
+
raise StandardError, "Missing Razorpay Payment ID." if rzp_payment_id.blank?
|
|
305
|
+
|
|
306
|
+
rzp_payment = ::Razorpay::Payment.fetch(rzp_payment_id)
|
|
307
|
+
if rzp_payment.status == 'authorized'
|
|
308
|
+
rzp_payment.capture({ amount: amount_in_cents, currency: gateway_options[:currency] || 'INR' })
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
ActiveMerchant::Billing::Response.new(true, 'Razorpay Capture Successful', {}, test: preferred_test_mode, authorization: rzp_payment_id)
|
|
312
|
+
rescue StandardError => e
|
|
313
|
+
Rails.logger.error("Razorpay Capture Failed: #{e.message}")
|
|
314
|
+
ActiveMerchant::Billing::Response.new(false, "Capture failed: #{e.message}", {}, test: preferred_test_mode)
|
|
315
|
+
end
|
|
276
316
|
end
|
|
277
317
|
|
|
278
318
|
def credit(credit_cents, response_code, _gateway_options = {})
|
|
@@ -10,16 +10,13 @@ module Spree
|
|
|
10
10
|
external_id
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
# Required for Headless API V3 to properly create the source and prevent double-charges
|
|
14
13
|
def find_or_create_payment!(metadata = {})
|
|
15
14
|
return unless persisted?
|
|
16
15
|
return payment if payment.present?
|
|
17
16
|
|
|
18
|
-
# 1. Lock the order to prevent race conditions from Webhooks
|
|
19
17
|
order.with_lock do
|
|
20
|
-
rzp_payment_id = external_data['razorpay_payment_id']
|
|
18
|
+
rzp_payment_id = external_data['razorpay_payment_id'] || metadata['id']
|
|
21
19
|
|
|
22
|
-
# 2. Check if the webhook already created this payment
|
|
23
20
|
existing_payment = order.payments.where(
|
|
24
21
|
payment_method: payment_method,
|
|
25
22
|
response_code: rzp_payment_id || external_id
|
|
@@ -27,17 +24,17 @@ module Spree
|
|
|
27
24
|
|
|
28
25
|
return existing_payment if existing_payment.present?
|
|
29
26
|
|
|
30
|
-
|
|
27
|
+
rzp_status = metadata['status'] || 'pending'
|
|
28
|
+
|
|
31
29
|
source = ::Spree::RazorpayCheckout.create!(
|
|
32
30
|
order_id: order.id,
|
|
33
31
|
razorpay_payment_id: rzp_payment_id,
|
|
34
32
|
razorpay_order_id: external_id,
|
|
35
33
|
razorpay_signature: external_data['razorpay_signature'],
|
|
36
|
-
status:
|
|
34
|
+
status: rzp_status,
|
|
37
35
|
payment_method: payment_method.name
|
|
38
36
|
)
|
|
39
37
|
|
|
40
|
-
# 4. Create the actual Spree::Payment
|
|
41
38
|
order.payments.create!(
|
|
42
39
|
payment_method: payment_method,
|
|
43
40
|
amount: amount,
|
data/config/routes.rb
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
Spree::Core::Engine.add_routes do
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
post :verify, to: 'webhooks#verify'
|
|
6
|
-
end
|
|
7
|
-
end
|
|
2
|
+
post '/razorpay/webhooks', to: 'razorpay_webhooks#create'
|
|
3
|
+
post '/razorpay/verify', to: 'razorpay_webhooks#verify'
|
|
4
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_razorpay_checkout
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Umesh Ravani
|
|
@@ -79,7 +79,7 @@ dependencies:
|
|
|
79
79
|
- - ">="
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '0'
|
|
82
|
-
description: Seamless Razorpay checkout integration for Spree 5.
|
|
82
|
+
description: Seamless Razorpay checkout integration for Spree 5.5. Features include
|
|
83
83
|
Hotwire/Turbo compatibility, Zero Drop-off Webhook captures, native Spree Dashboard
|
|
84
84
|
refunds, and the Affordability Widget.
|
|
85
85
|
email:
|
|
@@ -95,7 +95,7 @@ files:
|
|
|
95
95
|
- app/assets/images/payment_icons/window_modal.svg
|
|
96
96
|
- app/controllers/concerns/spree/razor_pay.rb
|
|
97
97
|
- app/controllers/spree/products_controller_decorator.rb
|
|
98
|
-
- app/controllers/spree/razorpay/
|
|
98
|
+
- app/controllers/spree/razorpay/_webhooks_controller.rb
|
|
99
99
|
- app/controllers/spree/razorpay_controller.rb
|
|
100
100
|
- app/javascript/spree_razorpay/application.js
|
|
101
101
|
- app/javascript/spree_razorpay/controllers/checkout_razorpay_controller.js
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
module Spree
|
|
2
|
-
module Razorpay
|
|
3
|
-
class WebhooksController < ActionController::API
|
|
4
|
-
skip_before_action :verify_authenticity_token, raise: false
|
|
5
|
-
|
|
6
|
-
# Handles asynchronous background webhooks directly from Razorpay's servers
|
|
7
|
-
def create
|
|
8
|
-
payload = request.body.read
|
|
9
|
-
signature = request.headers['X-Razorpay-Signature']
|
|
10
|
-
|
|
11
|
-
gateway = Spree::Gateway::RazorpayGateway.active.first
|
|
12
|
-
return head :not_found unless gateway && gateway.preferred_webhook_secret.present?
|
|
13
|
-
|
|
14
|
-
begin
|
|
15
|
-
::Razorpay::Utility.verify_webhook_signature(
|
|
16
|
-
payload,
|
|
17
|
-
signature,
|
|
18
|
-
gateway.preferred_webhook_secret
|
|
19
|
-
)
|
|
20
|
-
rescue ::Razorpay::Errors::SignatureVerificationError => e
|
|
21
|
-
Rails.logger.error("Razorpay Webhook Verification Failed: #{e.message}")
|
|
22
|
-
return head :unauthorized
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
event = JSON.parse(payload)
|
|
26
|
-
|
|
27
|
-
# Listen for 'payment.authorized' which fires immediately after OTP!
|
|
28
|
-
if ['order.paid', 'payment.captured', 'payment.authorized'].include?(event['event'])
|
|
29
|
-
::SpreeRazorpayCheckout::HandleWebhookEventJob.perform_later(event)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
head :ok
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Handles synchronous frontend verification from Next.js Storefront
|
|
36
|
-
def verify
|
|
37
|
-
razorpay_order_id = params[:razorpay_order_id]
|
|
38
|
-
razorpay_payment_id = params[:razorpay_payment_id]
|
|
39
|
-
razorpay_signature = params[:razorpay_signature]
|
|
40
|
-
|
|
41
|
-
session = Spree::PaymentSession.find_by(external_id: razorpay_order_id)
|
|
42
|
-
return head :not_found unless session
|
|
43
|
-
|
|
44
|
-
# Initialize the Razorpay gem with your active API keys
|
|
45
|
-
gateway = Spree::Gateway::RazorpayGateway.active.first
|
|
46
|
-
return render json: { success: false, error: 'Gateway not configured' }, status: :internal_server_error unless gateway
|
|
47
|
-
gateway.provider
|
|
48
|
-
|
|
49
|
-
begin
|
|
50
|
-
# 1. Verify the signature securely on the server
|
|
51
|
-
::Razorpay::Utility.verify_payment_signature(
|
|
52
|
-
razorpay_order_id: razorpay_order_id,
|
|
53
|
-
razorpay_payment_id: razorpay_payment_id,
|
|
54
|
-
razorpay_signature: razorpay_signature
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
# 2. Inject the signatures into the session's external_data
|
|
58
|
-
session.external_data ||= {}
|
|
59
|
-
session.external_data['razorpay_payment_id'] = razorpay_payment_id
|
|
60
|
-
session.external_data['razorpay_signature'] = razorpay_signature
|
|
61
|
-
session.save!
|
|
62
|
-
|
|
63
|
-
render json: { success: true }
|
|
64
|
-
rescue ::Razorpay::Errors::SignatureVerificationError => e
|
|
65
|
-
Rails.logger.error("Razorpay Frontend Verification Failed: #{e.message}")
|
|
66
|
-
render json: { success: false, error: e.message }, status: :unprocessable_entity
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|