spree_razorpay_checkout 0.2.0 → 0.3.0

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: 210a8729bc5810c991d970ad880aa0a0ac8570e9eccfd0de194e13cb600d0136
4
- data.tar.gz: 2198af0c4bdf320756d3ca5a197a53b7f34892332f649e284ad98892355aeadd
3
+ metadata.gz: a2ae9eca9f07041baff888721f0da631374e73925e8237eeafe32ccfe5185298
4
+ data.tar.gz: b087fa0f6961cc60c77c38cef002566104a19f6a3a87519658d69c2eb0174ec0
5
5
  SHA512:
6
- metadata.gz: 5fc643f93e86598dbf6586e333a54af34ceb300b523c3a6168bedb2164ed1112adb53707c9ac22637b54f1a1c77c1f44edbd1a96a81cda28de9147fbe7aee4eb
7
- data.tar.gz: 1df23f297c1a5629721d24d6db194a2b1ae2a8b6adf3b4eacd46ec658e8d4791637ec0d4282aff14876c9d1bf120621b238ad99f5cc618598f508e679cb4a497
6
+ metadata.gz: 4262af60d12805b8d7ee60cae23b03f95d9211a806bc39d88d8a4cbe556e280428921d1c9c14b940f56d92128b1e952e7ea99f0d69436bb00071b16c425b83e5
7
+ data.tar.gz: bf60c0322601d7dfed075e214828ab5fabf98f55a57760ab3f7b73c9b40edc3c3da7a19ac04ac5ff267b23f8130171332c4a522da996533832d1568e23f0652d
@@ -1,17 +1,16 @@
1
1
  module Spree
2
- module ProductsControllerDecorator
3
- def self.prepended(base)
4
- base.before_action :force_razorpay_view_priority
5
- end
6
-
7
- private
8
-
9
- def force_razorpay_view_priority
10
- plugin_theme_path = SpreeRazorpayCheckout::Engine.root.join('app', 'views', 'themes', 'default')
11
- prepend_view_path(plugin_theme_path)
2
+ module ProductsControllerDecorator
3
+ def self.prepended(base)
4
+ base.before_action :force_razorpay_view_priority
5
+ end
6
+
7
+ private
12
8
 
13
- end
9
+ def force_razorpay_view_priority
10
+ plugin_theme_path = SpreeRazorpayCheckout::Engine.root.join('app', 'views', 'themes', 'default')
11
+ prepend_view_path(plugin_theme_path)
14
12
  end
15
13
  end
16
-
17
- ::Spree::ProductsController.prepend(Spree::ProductsControllerDecorator)
14
+ end
15
+
16
+ ::Spree::ProductsController.prepend(Spree::ProductsControllerDecorator) if defined?(::Spree::ProductsController)
@@ -1,35 +1,72 @@
1
1
  module Spree
2
- module Razorpay
3
- class WebhooksController < ActionController::API
4
- skip_before_action :verify_authenticity_token, raise: false
5
-
6
- def create
7
- payload = request.body.read
8
- signature = request.headers['X-Razorpay-Signature']
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
+ )
9
56
 
10
- gateway = Spree::Gateway::RazorpayGateway.active.first
11
- return head :not_found unless gateway && gateway.preferred_webhook_secret.present?
12
-
13
- begin
14
- ::Razorpay::Utility.verify_webhook_signature(
15
- payload,
16
- signature,
17
- gateway.preferred_webhook_secret
18
- )
19
- rescue ::Razorpay::Errors::SignatureVerificationError => e
20
- Rails.logger.error("Razorpay Webhook Verification Failed: #{e.message}")
21
- return head :unauthorized
22
- end
23
-
24
- event = JSON.parse(payload)
25
-
26
- # Listen for 'payment.authorized' which fires immediately after OTP!
27
- if ['order.paid', 'payment.captured', 'payment.authorized'].include?(event['event'])
28
- ::SpreeRazorpayCheckout::HandleWebhookEventJob.perform_later(event)
29
- end
30
-
31
- head :ok
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
32
67
  end
33
68
  end
69
+
34
70
  end
35
- end
71
+ end
72
+ end
@@ -9,4 +9,4 @@ if (typeof window.Stimulus === "undefined") {
9
9
  application = window.Stimulus
10
10
  }
11
11
 
12
- application.register('checkout-razorpay', CheckoutRazorpayController)
12
+ application.register('checkout-razorpay', CheckoutRazorpayController)
@@ -105,4 +105,4 @@ export default class extends Controller {
105
105
  this.submitBtn.innerHTML = 'Save and Continue';
106
106
  }
107
107
  }
108
- }
108
+ }
@@ -29,6 +29,7 @@ module SpreeRazorpayCheckout
29
29
  end
30
30
  rescue StandardError => e
31
31
  Rails.logger.error("Webhook Razorpay Capture Failed: #{e.message}")
32
+ return
32
33
  end
33
34
 
34
35
  checkout_record.update!(
@@ -61,4 +62,4 @@ module SpreeRazorpayCheckout
61
62
  end
62
63
  end
63
64
  end
64
- end
65
+ end
@@ -1,29 +1,19 @@
1
1
  require 'razorpay'
2
+ require 'active_merchant'
2
3
 
3
4
  module Spree
4
5
  class Gateway::RazorpayGateway < Gateway
5
- preference :webhook_secret, :password
6
- preference :key_id, :string
7
- preference :key_secret, :password
8
- preference :test_key_id, :string
9
- preference :test_key_secret, :password
6
+ preference :webhook_secret, :password, default: ''
7
+ preference :key_id, :string, default: ''
8
+ preference :key_secret, :password, default: ''
9
+ preference :test_key_id, :string, default: ''
10
+ preference :test_key_secret, :password, default: ''
10
11
  preference :test_mode, :boolean, default: false
11
12
  preference :merchant_name, :string, default: 'Razorpay'
12
13
  preference :merchant_description, :text, default: 'Razorpay Payment Gateway'
13
14
  preference :merchant_address, :string, default: 'Razorpay, Bangalore, India'
14
15
  preference :theme_color, :string, default: '#2e5bff'
15
-
16
- def supports?(_source)
17
- true
18
- end
19
-
20
- def source_required?
21
- true
22
- end
23
-
24
- def payment_source_class
25
- Spree::RazorpayCheckout
26
- end
16
+ preference :headless_api_mode, :boolean, default: false
27
17
 
28
18
  def name
29
19
  'Razorpay Secure (UPI, Wallets, Cards & Netbanking)'
@@ -81,9 +71,146 @@ module Spree
81
71
  payment.state != 'void'
82
72
  end
83
73
 
84
- # -------------------------------------------------------------------------
85
- # PURCHASE (Happy Path & Webhook Recovery)
86
- # -------------------------------------------------------------------------
74
+ def supports?(_source)
75
+ true
76
+ end
77
+
78
+ def session_required?
79
+ preferred_headless_api_mode
80
+ end
81
+
82
+ def source_required?
83
+ !preferred_headless_api_mode
84
+ end
85
+
86
+ def setup_session_supported?
87
+ false
88
+ end
89
+
90
+ def payment_source_class
91
+ Spree::RazorpayCheckout
92
+ end
93
+
94
+ def payment_session_class
95
+ Spree::PaymentSessions::Razorpay if defined?(Spree::PaymentSession)
96
+ end
97
+
98
+ # SPREE 5.4+ HEADLESS API FLOW (Protected by defined? check)
99
+
100
+ if defined?(Spree::PaymentSession)
101
+ def create_payment_session(order:, amount: nil, external_data: {})
102
+ provider
103
+ total = amount || order.total_minus_store_credits
104
+ amount_in_cents = (total.to_f * 100).to_i
105
+
106
+ rzp_order = ::Razorpay::Order.create(
107
+ amount: amount_in_cents,
108
+ currency: order.currency || 'INR',
109
+ receipt: order.number,
110
+ payment_capture: 1,
111
+ notes: { spree_order_number: order.number, email: order.email }
112
+ )
113
+
114
+ unless rzp_order && rzp_order.attributes.key?('id')
115
+ raise Spree::Core::GatewayError, 'Failed to create Razorpay session'
116
+ end
117
+
118
+ payment_sessions.create!(
119
+ type: 'Spree::PaymentSessions::Razorpay',
120
+ order: order,
121
+ amount: total,
122
+ currency: order.currency || 'INR',
123
+ external_id: rzp_order.id,
124
+ external_data: { client_key: current_key_id },
125
+ customer: order.user,
126
+ status: 'pending'
127
+ )
128
+ rescue StandardError => e
129
+ Rails.logger.error("Razorpay Session Creation Failed: #{e.message}")
130
+ raise Spree::Core::GatewayError, e.message
131
+ end
132
+
133
+ def update_payment_session(payment_session:, amount: nil, external_data: {})
134
+ provider
135
+
136
+ if amount.present? && payment_session.amount != amount
137
+ amount_in_cents = (amount.to_f * 100).to_i
138
+
139
+ new_rzp_order = ::Razorpay::Order.create(
140
+ amount: amount_in_cents,
141
+ currency: payment_session.currency,
142
+ receipt: payment_session.order.number,
143
+ payment_capture: 1
144
+ )
145
+
146
+ payment_session.update!(amount: amount, external_id: new_rzp_order.id)
147
+ end
148
+ payment_session
149
+ end
150
+
151
+ def complete_payment_session(payment_session:, params: {})
152
+ provider
153
+
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']
157
+
158
+ begin
159
+ ::Razorpay::Utility.verify_payment_signature(
160
+ razorpay_order_id: payment_session.external_id,
161
+ razorpay_payment_id: rzp_payment_id,
162
+ razorpay_signature: rzp_signature
163
+ )
164
+
165
+ rzp_payment = ::Razorpay::Payment.fetch(rzp_payment_id)
166
+ if rzp_payment.status == 'authorized'
167
+ rzp_payment.capture({ amount: (payment_session.amount.to_f * 100).to_i })
168
+ end
169
+
170
+ payment_session.process! if payment_session.can_process?
171
+ payment = payment_session.find_or_create_payment!
172
+
173
+ if payment.present? && !payment.completed?
174
+ payment.started_processing! if payment.checkout?
175
+ payment.complete! if payment.can_complete?
176
+ end
177
+
178
+ payment_session.complete! if payment_session.can_complete?
179
+
180
+ rescue StandardError => e
181
+ Rails.logger.error("Razorpay 5.4 API Completion Failed: #{e.message}")
182
+ payment_session.fail! if payment_session.can_fail?
183
+ raise Spree::Core::GatewayError, e.message
184
+ end
185
+ end
186
+
187
+ def parse_webhook_event(raw_body, headers)
188
+ provider
189
+ signature = headers['HTTP_X_RAZORPAY_SIGNATURE'] || headers['X-Razorpay-Signature']
190
+
191
+ unless ::Razorpay::Utility.verify_webhook_signature(raw_body, signature, preferred_webhook_secret)
192
+ raise Spree::PaymentMethod::WebhookSignatureError
193
+ end
194
+
195
+ event = JSON.parse(raw_body)
196
+ payment_entity = event.dig('payload', 'payment', 'entity') || event.dig('payload', 'order', 'entity')
197
+
198
+ session = Spree::PaymentSession.find_by(external_id: payment_entity['order_id'])
199
+ return nil unless session
200
+
201
+ case event['event']
202
+ when 'payment.captured', 'payment.authorized'
203
+ { action: :captured, payment_session: session }
204
+ when 'payment.failed'
205
+ { action: :failed, payment_session: session }
206
+ else
207
+ nil
208
+ end
209
+ rescue ::Razorpay::Errors::SignatureVerificationError
210
+ raise Spree::PaymentMethod::WebhookSignatureError
211
+ end
212
+ end
213
+
87
214
  def purchase(_amount, source, _gateway_options = {})
88
215
  provider
89
216
 
@@ -92,14 +219,12 @@ module Spree
92
219
  return ActiveMerchant::Billing::Response.new(false, 'Payment was not completed. Please try again.', {}, test: preferred_test_mode)
93
220
  end
94
221
 
95
- # 1. Verify the signature
96
222
  ::Razorpay::Utility.verify_payment_signature(
97
223
  razorpay_order_id: source.razorpay_order_id,
98
224
  razorpay_payment_id: source.razorpay_payment_id,
99
225
  razorpay_signature: source.razorpay_signature
100
226
  )
101
227
 
102
- # 2. Safely ensure it is captured!
103
228
  rzp_payment = ::Razorpay::Payment.fetch(source.razorpay_payment_id)
104
229
  if rzp_payment.status == 'authorized'
105
230
  rzp_payment.capture({ amount: _amount })
@@ -107,13 +232,7 @@ module Spree
107
232
 
108
233
  source.update!(status: 'captured')
109
234
 
110
- ActiveMerchant::Billing::Response.new(
111
- true,
112
- 'Razorpay Payment Successful',
113
- {},
114
- test: preferred_test_mode,
115
- authorization: source.razorpay_payment_id
116
- )
235
+ ActiveMerchant::Billing::Response.new(true, 'Razorpay Payment Successful', {}, test: preferred_test_mode, authorization: source.razorpay_payment_id)
117
236
 
118
237
  rescue StandardError => e
119
238
  Rails.logger.error("Razorpay Verification/Capture Failed: #{e.message}")
@@ -121,65 +240,65 @@ module Spree
121
240
  end
122
241
  end
123
242
 
243
+ def resolve_razorpay_payment_id(response_code)
244
+ return nil if response_code.blank?
245
+
246
+ if response_code.to_s.start_with?('order_')
247
+ rzp_order = ::Razorpay::Order.fetch(response_code)
248
+ payments = rzp_order.payments
249
+
250
+ captured_payment = payments.items.find { |p| p.status == 'captured' } || payments.items.first
251
+
252
+ raise StandardError, "No captured payment found for Razorpay Order #{response_code}" unless captured_payment
253
+
254
+ captured_payment.id
255
+ else
256
+ response_code
257
+ end
258
+ end
259
+
124
260
  def capture(*args)
125
- # We already auto-capture via the frontend/webhook, so we return true to keep Spree happy
126
261
  ActiveMerchant::Billing::Response.new(true, 'Already Captured', {}, test: preferred_test_mode)
127
262
  end
128
263
 
129
- # -------------------------------------------------------------------------
130
- # REFUNDS & CANCELLATIONS
131
- # -------------------------------------------------------------------------
132
-
133
- # Triggered when you click "Refund" in the Spree Admin
134
264
  def credit(credit_cents, response_code, _gateway_options = {})
135
265
  provider
136
-
137
266
  begin
138
- # Fetch the original payment from Razorpay using the saved payment ID
139
- rzp_payment = ::Razorpay::Payment.fetch(response_code)
267
+ rzp_payment_id = resolve_razorpay_payment_id(response_code)
140
268
 
141
- # Issue the refund via Razorpay API (amount must be in paise/cents)
142
- refund = rzp_payment.refund(amount: credit_cents)
143
-
144
- ActiveMerchant::Billing::Response.new(
145
- true,
146
- 'Razorpay Refund Successful',
147
- { refund_id: refund.id },
148
- test: preferred_test_mode,
149
- authorization: refund.id
150
- )
269
+ if rzp_payment_id.blank?
270
+ raise StandardError, "Missing Razorpay Payment ID. Cannot process refund."
271
+ end
272
+
273
+ refund = ::Razorpay::Refund.create(payment_id: rzp_payment_id, amount: credit_cents.to_i)
274
+
275
+ ActiveMerchant::Billing::Response.new(true, 'Razorpay Refund Successful', { refund_id: refund.id }, test: preferred_test_mode, authorization: refund.id)
151
276
  rescue StandardError => e
152
277
  Rails.logger.error("Razorpay Refund Failed: #{e.message}")
153
278
  ActiveMerchant::Billing::Response.new(false, "Refund failed: #{e.message}", {}, test: preferred_test_mode)
154
279
  end
155
280
  end
156
281
 
157
- # Triggered if you explicitly "Void" a payment in Spree
158
282
  def void(response_code, _gateway_options = {})
159
283
  provider
160
-
161
284
  begin
162
- # Razorpay doesn't have a concept of "Voiding" a captured payment,
163
- # so we just issue a full refund instead.
164
- rzp_payment = ::Razorpay::Payment.fetch(response_code)
165
- refund = rzp_payment.refund
166
-
167
- ActiveMerchant::Billing::Response.new(
168
- true,
169
- 'Razorpay Void/Refund Successful',
170
- { refund_id: refund.id },
171
- test: preferred_test_mode,
172
- authorization: refund.id
173
- )
285
+ rzp_payment_id = resolve_razorpay_payment_id(response_code)
286
+
287
+ if rzp_payment_id.blank?
288
+ raise StandardError, "Missing Razorpay Payment ID. Cannot process void."
289
+ end
290
+
291
+ refund = ::Razorpay::Refund.create(payment_id: rzp_payment_id)
292
+
293
+ ActiveMerchant::Billing::Response.new(true, 'Razorpay Void/Refund Successful', { refund_id: refund.id }, test: preferred_test_mode, authorization: refund.id)
174
294
  rescue StandardError => e
175
295
  Rails.logger.error("Razorpay Void Failed: #{e.message}")
176
296
  ActiveMerchant::Billing::Response.new(false, "Void failed: #{e.message}", {}, test: preferred_test_mode)
177
297
  end
178
298
  end
179
299
 
180
- # Triggered if the entire Order is Cancelled in the Spree Admin
181
- def cancel(response_code)
300
+ def cancel(response_code, _source = nil, _options = {})
182
301
  void(response_code)
183
302
  end
184
303
  end
185
- end
304
+ end
@@ -1,17 +1,15 @@
1
1
  module Spree
2
- module PageSections
3
- module ProductDetailsDecorator
4
- def default_blocks
5
- super + [Spree::PageBlocks::Products::RazorpayAffordability.new]
6
- end
7
-
8
- def available_blocks_to_add
9
- super + [Spree::PageBlocks::Products::RazorpayAffordability]
10
- end
2
+ module PageSections
3
+ module ProductDetailsDecorator
4
+ def default_blocks
5
+ super + [Spree::PageBlocks::Products::RazorpayAffordability.new]
6
+ end
7
+
8
+ def available_blocks_to_add
9
+ super + [Spree::PageBlocks::Products::RazorpayAffordability]
11
10
  end
12
11
  end
13
12
  end
14
-
15
- Spree::PageSections::ProductDetails.prepend(
16
- Spree::PageSections::ProductDetailsDecorator
17
- )
13
+ end
14
+
15
+ Spree::PageSections::ProductDetails.prepend(Spree::PageSections::ProductDetailsDecorator) if defined?(Spree::PageSections::ProductDetails)
@@ -1,43 +1,14 @@
1
1
  module Spree
2
- module PaymentSessions
3
- class Razorpay < PaymentSession
4
- # external_id will store the razorpay_order_id
5
- def razorpay_order_id
6
- external_id
7
- end
8
-
9
- # Helper to extract the signature if we need it later
10
- def razorpay_signature
11
- external_data&.dig('razorpay_signature')
12
- end
13
-
14
- # Mirrors Stripe's approach to safely generate the Spree::Payment
15
- def find_or_create_payment!(razorpay_payment_id)
16
- return unless persisted?
17
- return payment if payment.present?
18
-
19
- # Create the Spree::Payment record in the checkout state
20
- order.payments.create!(
21
- amount: amount,
22
- payment_method: payment_method,
23
- response_code: razorpay_payment_id,
24
- source: create_source(razorpay_payment_id), # Optional: if you still use a source model
25
- state: 'checkout'
26
- )
27
- end
28
-
29
- private
30
-
31
- # Optional: Only needed if you want to keep using your custom source model
32
- def create_source(payment_id)
33
- ::Spree::RazorpayCheckout.create!(
34
- order_id: order.id,
35
- razorpay_order_id: razorpay_order_id,
36
- razorpay_payment_id: payment_id,
37
- payment_method: payment_method.name,
38
- status: 'captured'
39
- )
40
- end
2
+ module PaymentSessions
3
+ class Razorpay < Spree::PaymentSession
4
+
5
+ def client_key
6
+ external_data['client_key']
7
+ end
8
+
9
+ def razorpay_order_id
10
+ external_id
41
11
  end
42
12
  end
43
- end
13
+ end
14
+ end
@@ -20,5 +20,10 @@ module Spree
20
20
  def order_id
21
21
  self.razorpay_order_id
22
22
  end
23
+
24
+ def gateway_payment_profile_id
25
+ self.razorpay_payment_id
26
+ end
27
+
23
28
  end
24
29
  end
@@ -2,6 +2,10 @@ module SpreeRazorpayCheckout
2
2
  module Spree
3
3
  module OrderDecorator
4
4
 
5
+ def self.prepended(base)
6
+ base.has_many :razorpay_checkouts, class_name: 'Spree::RazorpayCheckout', dependent: :destroy
7
+ end
8
+
5
9
  def inr_amt_in_paise
6
10
  payments.reload
7
11
 
@@ -53,8 +57,8 @@ module SpreeRazorpayCheckout
53
57
 
54
58
  payment
55
59
  end
56
-
57
- ::Spree::Order.prepend SpreeRazorpayCheckout::Spree::OrderDecorator
58
60
  end
59
61
  end
60
62
  end
63
+
64
+ ::Spree::Order.prepend SpreeRazorpayCheckout::Spree::OrderDecorator if defined?(::Spree::Order)
@@ -1,6 +1,104 @@
1
- <div class="alert alert-info">
2
- <p class="mb-0">
3
- To find your <strong>Key</strong> and <strong>Key Secret</strong>, go to the
4
- <%= external_link_to 'Razorpay dashboard', 'https://dashboard.razorpay.com/app/website-app-settings/api-keys', class: 'alert-link' %><br><%= external_link_to 'Create Webhook', 'https://dashboard.razorpay.com/app/website-app-settings/webhooks', class: 'alert-link' %> and mark all checkboxes in <strong>Active Events.</strong>
5
- </p>
1
+ <%
2
+ base_url = current_store.url_or_custom_domain.to_s
3
+ base_url = "https://#{base_url}" unless base_url.start_with?("http")
4
+ base_url = base_url.chomp('/')
5
+ %>
6
+
7
+ <style>
8
+ .razorpay-premium-ui .badge-event {
9
+ font-weight: 500;
10
+ background-color: #f3f4f6;
11
+ border: 1px solid #e5e7eb;
12
+ color: #4b5563;
13
+ padding: 0.35em 0.75em;
14
+ border-radius: 0.375rem;
15
+ font-size: 0.8rem;
16
+ }
17
+ </style>
18
+
19
+ <div class="razorpay-premium-ui">
20
+ <div class="card mb-3 border shadow-sm" style="border-radius: 0.5rem;">
21
+ <div class="card-body d-flex align-items-center py-3 px-4">
22
+ <div class="mr-3 text-muted d-flex align-items-center">
23
+ <%= icon 'key', width: 20, height: 20 %>
24
+ </div>
25
+ <p class="mb-0 text-dark" style="font-size: 0.95rem;">
26
+ Find your 'Key ID' and 'Key Secret' in the
27
+ <strong><%= external_link_to 'Razorpay Dashboard API Keys', 'https://dashboard.razorpay.com/app/website-app-settings/api-keys', target: '_blank', class: 'alert-link font-weight-bold text-primary' %></strong>
28
+ </p>
29
+ </div>
30
+ </div>
31
+
32
+ <% if @payment_method.persisted? %>
33
+ <div class="card mb-4 border shadow-sm" style="border-radius: 0.5rem;">
34
+
35
+ <div class="card-body d-flex align-items-center py-3 px-4 border-bottom">
36
+ <div class="mr-3 text-muted d-flex align-items-center">
37
+ <%= icon 'link', width: 20, height: 20 %>
38
+ </div>
39
+ <p class="mb-0 text-dark" style="font-size: 0.95rem;">
40
+ Add the following webhooks in your <strong><%= external_link_to 'Razorpay Settings', 'https://dashboard.razorpay.com/app/website-app-settings/webhooks', target: '_blank', class: 'alert-link text-primary font-weight-bold' %></strong>
41
+ </p>
42
+ </div>
43
+
44
+ <div class="card-body py-4 px-4">
45
+
46
+ <div class="mb-4 pb-2">
47
+ <h6 class="font-weight-bold text-dark mb-2" style="font-size: 0.95rem;">Next.js Storefront</h6>
48
+
49
+ <div class="input-group bg-gray-25 border border-gray-100 mb-3 d-flex align-items-center rounded" data-controller="clipboard" data-clipboard-success-content-value="Copied!">
50
+ <input type="text" readonly value="<%= base_url %>/api/v3/webhooks/payments/<%= @payment_method.id %>" class="text-gray-600 py-2 grow pl-3 border-0 bg-transparent shadow-none" style="font-family: monospace; font-size: 0.85rem; outline: none; min-width: 0;" data-clipboard-target="source">
51
+ <button type="button" class="btn btn-sm btn-light mr-1 d-flex align-items-center justify-content-center p-1" title="Copy to clipboard" data-action="clipboard#copy" data-clipboard-target="button">
52
+ <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24" class="text-muted">
53
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 4h3a1 1 0 0 1 1 1v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h3m0 3h6m-6 5h6m-6 4h6M10 3v4h4V3h-4Z"/>
54
+ </svg>
55
+ </button>
56
+ </div>
57
+
58
+ <div class="d-flex align-items-center flex-wrap">
59
+ <span class="text-dark font-weight-bold mr-3" style="font-size: 0.85rem;">Active Events:</span>
60
+ <span class="badge badge-event mr-2 mb-1">payment.authorized</span>
61
+ <span class="badge badge-event mr-2 mb-1">payment.captured</span>
62
+ <span class="badge badge-event mb-1">payment.failed</span>
63
+ </div>
64
+ </div>
65
+
66
+ <hr class="border-light my-4">
67
+
68
+ <div class="pt-2">
69
+ <h6 class="font-weight-bold text-dark mb-2" style="font-size: 0.95rem;">Legacy Rails Storefront</h6>
70
+
71
+ <div class="input-group bg-gray-25 border border-gray-100 mb-3 d-flex align-items-center rounded" data-controller="clipboard" data-clipboard-success-content-value="Copied!">
72
+ <input type="text" readonly value="<%= base_url %>/razorpay/webhooks" class="text-gray-600 py-2 grow pl-3 border-0 bg-transparent shadow-none" style="font-family: monospace; font-size: 0.85rem; outline: none; min-width: 0;" data-clipboard-target="source">
73
+ <button type="button" class="btn btn-sm btn-light mr-1 d-flex align-items-center justify-content-center p-1" title="Copy to clipboard" data-action="clipboard#copy" data-clipboard-target="button">
74
+ <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24" class="text-muted">
75
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 4h3a1 1 0 0 1 1 1v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h3m0 3h6m-6 5h6m-6 4h6M10 3v4h4V3h-4Z"/>
76
+ </svg>
77
+ </button>
78
+ </div>
79
+
80
+ <div class="d-flex align-items-center flex-wrap">
81
+ <span class="text-dark font-weight-bold mr-3" style="font-size: 0.85rem;">Active Events:</span>
82
+ <span class="badge badge-event mr-2 mb-1">payment.authorized</span>
83
+ <span class="badge badge-event mr-2 mb-1">payment.pending</span>
84
+ <span class="badge badge-event mr-2 mb-1">payment.captured</span>
85
+ <span class="badge badge-event mr-2 mb-1">payment.failed</span>
86
+ <span class="badge badge-event mb-1">order.paid</span>
87
+ </div>
88
+ </div>
89
+
90
+ </div>
91
+ </div>
92
+ <% else %>
93
+ <div class="card mb-4 border shadow-sm" style="border-radius: 0.5rem; border-left: 4px solid #0ea5e9 !important;">
94
+ <div class="card-body d-flex align-items-center py-3 px-4">
95
+ <div class="mr-3 text-info d-flex align-items-center">
96
+ <%= icon 'info-circle', width: 22, height: 22 %>
97
+ </div>
98
+ <p class="mb-0 text-dark" style="font-size: 0.95rem;">
99
+ <strong>Webhook Generation Pending:</strong> You must click <strong>Create</strong> to save this payment method before secure Webhook URLs can be generated.
100
+ </p>
101
+ </div>
102
+ </div>
103
+ <% end %>
6
104
  </div>
@@ -140,4 +140,4 @@
140
140
  <div class="alert alert-danger">
141
141
  Unable to initialize Razorpay session. Please try again.
142
142
  </div>
143
- <% end %>
143
+ <% end %>
data/config/routes.rb CHANGED
@@ -2,5 +2,6 @@ Spree::Core::Engine.add_routes do
2
2
 
3
3
  namespace :razorpay do
4
4
  post :webhooks, to: 'webhooks#create'
5
+ post :verify, to: 'webhooks#verify'
5
6
  end
6
- end
7
+ end
@@ -46,4 +46,4 @@ module SpreeRazorpayCheckout
46
46
  ::SpreeRazorpayCheckout::Engine.activate
47
47
  end
48
48
  end
49
- end
49
+ end
@@ -1,5 +1,5 @@
1
1
  module SpreeRazorpayCheckout
2
- VERSION = '0.2.0'.freeze
2
+ VERSION = '0.3.0'.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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Umesh Ravani
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '5.2'
32
+ version: '5.3'
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.2'
39
+ version: '5.3'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: spree_extension
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -65,7 +65,21 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
- description: Seamless Razorpay checkout integration for Spree 5.x. Features include
68
+ - !ruby/object:Gem::Dependency
69
+ name: activemerchant
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ description: Seamless Razorpay checkout integration for Spree 5.4. Features include
69
83
  Hotwire/Turbo compatibility, Zero Drop-off Webhook captures, native Spree Dashboard
70
84
  refunds, and the Affordability Widget.
71
85
  email:
@@ -93,7 +107,6 @@ files:
93
107
  - app/models/spree/razorpay_checkout.rb
94
108
  - app/models/spree_razorpay_checkout/configuration.rb
95
109
  - app/models/spree_razorpay_checkout/spree/order_decorator.rb
96
- - app/models/spree_razorpay_checkout/spree/refund_decorator.rb
97
110
  - app/services/razorpay/base.rb
98
111
  - app/services/razorpay/rp_order/api.rb
99
112
  - app/services/razorpay_refund.rb
@@ -134,5 +147,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
134
147
  requirements: []
135
148
  rubygems_version: 3.6.9
136
149
  specification_version: 4
137
- summary: Production-grade Razorpay integration for Spree Commerce 5.3+
150
+ summary: Production-grade Razorpay integration for Spree Commerce 5.4
138
151
  test_files: []
@@ -1,36 +0,0 @@
1
- module SpreeRazorpayCheckout
2
- module Spree
3
- module RefundDecorator
4
- def process!(credit_cents)
5
- response = if payment.payment_method.payment_profiles_supported?
6
- payment.payment_method.credit(
7
- credit_cents,
8
- payment.source,
9
- payment.transaction_id,
10
- originator: self
11
- )
12
- else
13
- razorpay_payment_id = payment.source&.razorpay_payment_id || payment.transaction_id
14
- payment.payment_method.credit(
15
- credit_cents,
16
- razorpay_payment_id,
17
- originator: self
18
- )
19
- end
20
-
21
- unless response.success?
22
- Rails.logger.error(Spree.t(:gateway_error) + " #{response.to_yaml}")
23
- text = response.params['message'] || response.params['response_reason_text'] || response.message
24
- raise Core::GatewayError, text
25
- end
26
-
27
- response
28
- rescue ActiveMerchant::ConnectionError => e
29
- Rails.logger.error(Spree.t(:gateway_error) + " #{e.inspect}")
30
- raise Core::GatewayError, Spree.t(:unable_to_connect_to_gateway)
31
- end
32
-
33
- ::Spree::Refund.prepend SpreeRazorpayCheckout::Spree::RefundDecorator
34
- end
35
- end
36
- end