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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4200b3ff85e0fea3d0c4412d81dd7af4cf4a66cdb5d582b871b88a8ac6bc2fc
4
- data.tar.gz: d142c51d56ba5b2f1eb445780fc887012e1e199abb767f5edaf85f8badc8ce17
3
+ metadata.gz: 59ea8bc12e162b79eacdbefce1a1ee167cd9a4d4e7c2cd7cd3bd53ce47a8d716
4
+ data.tar.gz: a4b9a022d525bb254c0b932f415ebedf63dbfc4aac3be42d648546163b149d87
5
5
  SHA512:
6
- metadata.gz: 48701e4dda07a8fa192f320981418ae79c5b744bff3e650935a4fac1485c66878bd19414bc4ca72d7411b5ad829bcbf2b0161ed214818057a459630e76cc267d
7
- data.tar.gz: 800b0a224a91684f8f7072adc953665091702bda9aafba80f6898bc009cf466300567a579707f53fba9ab855218e76da543beafacc24c562c6a8f3dd6b77eee5
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
- true
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
- ext_data = params[:external_data] || params['external_data'] || {}
155
- rzp_payment_id = ext_data[:razorpay_payment_id] || ext_data['razorpay_payment_id'] || payment_session.external_data['razorpay_payment_id']
156
- rzp_signature = ext_data[:razorpay_signature] || ext_data['razorpay_signature'] || payment_session.external_data['razorpay_signature']
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
- # Lock the order database row to prevent race conditions with Webhooks
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 so the new Session Class can read them
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
- # Capture the payment
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
- payment.complete! if payment.can_complete?
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', 'payment.authorized'
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
- def capture(*args)
275
- ActiveMerchant::Billing::Response.new(true, 'Already Captured', {}, test: preferred_test_mode)
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
- # 3. Create your custom RazorpayCheckout source record for Headless!
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: 'captured',
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
- namespace :razorpay do
4
- post :webhooks, to: 'webhooks#create'
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
@@ -1,5 +1,5 @@
1
1
  module SpreeRazorpayCheckout
2
- VERSION = '0.3.1'.freeze
2
+ VERSION = '0.3.2'.freeze
3
3
 
4
4
  module_function
5
5
 
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.1
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.4. Features include
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/webhooks_controller.rb
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