spree_razorpay_checkout 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 210a8729bc5810c991d970ad880aa0a0ac8570e9eccfd0de194e13cb600d0136
4
- data.tar.gz: 2198af0c4bdf320756d3ca5a197a53b7f34892332f649e284ad98892355aeadd
3
+ metadata.gz: c4200b3ff85e0fea3d0c4412d81dd7af4cf4a66cdb5d582b871b88a8ac6bc2fc
4
+ data.tar.gz: d142c51d56ba5b2f1eb445780fc887012e1e199abb767f5edaf85f8badc8ce17
5
5
  SHA512:
6
- metadata.gz: 5fc643f93e86598dbf6586e333a54af34ceb300b523c3a6168bedb2164ed1112adb53707c9ac22637b54f1a1c77c1f44edbd1a96a81cda28de9147fbe7aee4eb
7
- data.tar.gz: 1df23f297c1a5629721d24d6db194a2b1ae2a8b6adf3b4eacd46ec658e8d4791637ec0d4282aff14876c9d1bf120621b238ad99f5cc618598f508e679cb4a497
6
+ metadata.gz: 48701e4dda07a8fa192f320981418ae79c5b744bff3e650935a4fac1485c66878bd19414bc4ca72d7411b5ad829bcbf2b0161ed214818057a459630e76cc267d
7
+ data.tar.gz: 800b0a224a91684f8f7072adc953665091702bda9aafba80f6898bc009cf466300567a579707f53fba9ab855218e76da543beafacc24c562c6a8f3dd6b77eee5
@@ -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,160 @@ 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
+ # Lock the order database row to prevent race conditions with Webhooks
166
+ payment_session.order.with_lock do
167
+
168
+ # Save the verified IDs into the session so the new Session Class can read them
169
+ session_data = payment_session.external_data || {}
170
+ session_data['razorpay_payment_id'] = rzp_payment_id
171
+ session_data['razorpay_signature'] = rzp_signature
172
+ payment_session.update!(external_data: session_data)
173
+
174
+ # Capture the payment
175
+ 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
+
180
+ # Process and Create Payment
181
+ payment_session.process! if payment_session.can_process?
182
+
183
+ # This safely calls your new Spree::PaymentSessions::Razorpay class!
184
+ payment = payment_session.find_or_create_payment!
185
+
186
+ if payment.present? && !payment.completed?
187
+ payment.started_processing! if payment.checkout?
188
+ payment.complete! if payment.can_complete?
189
+ end
190
+
191
+ payment_session.complete! if payment_session.can_complete?
192
+ end
193
+
194
+ rescue StandardError => e
195
+ Rails.logger.error("Razorpay 5.4 API Completion Failed: #{e.message}")
196
+ payment_session.fail! if payment_session.can_fail?
197
+ raise Spree::Core::GatewayError, e.message
198
+ end
199
+ end
200
+
201
+ def parse_webhook_event(raw_body, headers)
202
+ provider
203
+ signature = headers['HTTP_X_RAZORPAY_SIGNATURE'] || headers['X-Razorpay-Signature']
204
+
205
+ unless ::Razorpay::Utility.verify_webhook_signature(raw_body, signature, preferred_webhook_secret)
206
+ raise Spree::PaymentMethod::WebhookSignatureError
207
+ end
208
+
209
+ event = JSON.parse(raw_body)
210
+ payment_entity = event.dig('payload', 'payment', 'entity') || event.dig('payload', 'order', 'entity')
211
+
212
+ session = Spree::PaymentSession.find_by(external_id: payment_entity['order_id'])
213
+ return nil unless session
214
+
215
+ case event['event']
216
+ when 'payment.captured', 'payment.authorized'
217
+ { action: :captured, payment_session: session }
218
+ when 'payment.failed'
219
+ { action: :failed, payment_session: session }
220
+ else
221
+ nil
222
+ end
223
+ rescue ::Razorpay::Errors::SignatureVerificationError
224
+ raise Spree::PaymentMethod::WebhookSignatureError
225
+ end
226
+ end
227
+
87
228
  def purchase(_amount, source, _gateway_options = {})
88
229
  provider
89
230
 
@@ -92,14 +233,12 @@ module Spree
92
233
  return ActiveMerchant::Billing::Response.new(false, 'Payment was not completed. Please try again.', {}, test: preferred_test_mode)
93
234
  end
94
235
 
95
- # 1. Verify the signature
96
236
  ::Razorpay::Utility.verify_payment_signature(
97
237
  razorpay_order_id: source.razorpay_order_id,
98
238
  razorpay_payment_id: source.razorpay_payment_id,
99
239
  razorpay_signature: source.razorpay_signature
100
240
  )
101
241
 
102
- # 2. Safely ensure it is captured!
103
242
  rzp_payment = ::Razorpay::Payment.fetch(source.razorpay_payment_id)
104
243
  if rzp_payment.status == 'authorized'
105
244
  rzp_payment.capture({ amount: _amount })
@@ -107,13 +246,7 @@ module Spree
107
246
 
108
247
  source.update!(status: 'captured')
109
248
 
110
- ActiveMerchant::Billing::Response.new(
111
- true,
112
- 'Razorpay Payment Successful',
113
- {},
114
- test: preferred_test_mode,
115
- authorization: source.razorpay_payment_id
116
- )
249
+ ActiveMerchant::Billing::Response.new(true, 'Razorpay Payment Successful', {}, test: preferred_test_mode, authorization: source.razorpay_payment_id)
117
250
 
118
251
  rescue StandardError => e
119
252
  Rails.logger.error("Razorpay Verification/Capture Failed: #{e.message}")
@@ -121,64 +254,64 @@ module Spree
121
254
  end
122
255
  end
123
256
 
257
+ def resolve_razorpay_payment_id(response_code)
258
+ return nil if response_code.blank?
259
+
260
+ if response_code.to_s.start_with?('order_')
261
+ rzp_order = ::Razorpay::Order.fetch(response_code)
262
+ payments = rzp_order.payments
263
+
264
+ captured_payment = payments.items.find { |p| p.status == 'captured' } || payments.items.first
265
+
266
+ raise StandardError, "No captured payment found for Razorpay Order #{response_code}" unless captured_payment
267
+
268
+ captured_payment.id
269
+ else
270
+ response_code
271
+ end
272
+ end
273
+
124
274
  def capture(*args)
125
- # We already auto-capture via the frontend/webhook, so we return true to keep Spree happy
126
275
  ActiveMerchant::Billing::Response.new(true, 'Already Captured', {}, test: preferred_test_mode)
127
276
  end
128
277
 
129
- # -------------------------------------------------------------------------
130
- # REFUNDS & CANCELLATIONS
131
- # -------------------------------------------------------------------------
132
-
133
- # Triggered when you click "Refund" in the Spree Admin
134
278
  def credit(credit_cents, response_code, _gateway_options = {})
135
279
  provider
136
-
137
280
  begin
138
- # Fetch the original payment from Razorpay using the saved payment ID
139
- rzp_payment = ::Razorpay::Payment.fetch(response_code)
281
+ rzp_payment_id = resolve_razorpay_payment_id(response_code)
140
282
 
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
- )
283
+ if rzp_payment_id.blank?
284
+ raise StandardError, "Missing Razorpay Payment ID. Cannot process refund."
285
+ end
286
+
287
+ refund = ::Razorpay::Refund.create(payment_id: rzp_payment_id, amount: credit_cents.to_i)
288
+
289
+ ActiveMerchant::Billing::Response.new(true, 'Razorpay Refund Successful', { refund_id: refund.id }, test: preferred_test_mode, authorization: refund.id)
151
290
  rescue StandardError => e
152
291
  Rails.logger.error("Razorpay Refund Failed: #{e.message}")
153
292
  ActiveMerchant::Billing::Response.new(false, "Refund failed: #{e.message}", {}, test: preferred_test_mode)
154
293
  end
155
294
  end
156
295
 
157
- # Triggered if you explicitly "Void" a payment in Spree
158
296
  def void(response_code, _gateway_options = {})
159
297
  provider
160
-
161
298
  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
- )
299
+ rzp_payment_id = resolve_razorpay_payment_id(response_code)
300
+
301
+ if rzp_payment_id.blank?
302
+ raise StandardError, "Missing Razorpay Payment ID. Cannot process void."
303
+ end
304
+
305
+ refund = ::Razorpay::Refund.create(payment_id: rzp_payment_id)
306
+
307
+ ActiveMerchant::Billing::Response.new(true, 'Razorpay Void/Refund Successful', { refund_id: refund.id }, test: preferred_test_mode, authorization: refund.id)
174
308
  rescue StandardError => e
175
309
  Rails.logger.error("Razorpay Void Failed: #{e.message}")
176
310
  ActiveMerchant::Billing::Response.new(false, "Void failed: #{e.message}", {}, test: preferred_test_mode)
177
311
  end
178
312
  end
179
313
 
180
- # Triggered if the entire Order is Cancelled in the Spree Admin
181
- def cancel(response_code)
314
+ def cancel(response_code, _source = nil, _options = {})
182
315
  void(response_code)
183
316
  end
184
317
  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,53 @@
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,
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
11
+ end
12
+
13
+ # Required for Headless API V3 to properly create the source and prevent double-charges
14
+ def find_or_create_payment!(metadata = {})
15
+ return unless persisted?
16
+ return payment if payment.present?
17
+
18
+ # 1. Lock the order to prevent race conditions from Webhooks
19
+ order.with_lock do
20
+ rzp_payment_id = external_data['razorpay_payment_id']
21
+
22
+ # 2. Check if the webhook already created this payment
23
+ existing_payment = order.payments.where(
22
24
  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!(
25
+ response_code: rzp_payment_id || external_id
26
+ ).first
27
+
28
+ return existing_payment if existing_payment.present?
29
+
30
+ # 3. Create your custom RazorpayCheckout source record for Headless!
31
+ source = ::Spree::RazorpayCheckout.create!(
34
32
  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'
33
+ razorpay_payment_id: rzp_payment_id,
34
+ razorpay_order_id: external_id,
35
+ razorpay_signature: external_data['razorpay_signature'],
36
+ status: 'captured',
37
+ payment_method: payment_method.name
38
+ )
39
+
40
+ # 4. Create the actual Spree::Payment
41
+ order.payments.create!(
42
+ payment_method: payment_method,
43
+ amount: amount,
44
+ response_code: rzp_payment_id || external_id,
45
+ source: source,
46
+ skip_source_requirement: true
39
47
  )
40
48
  end
41
49
  end
50
+
42
51
  end
43
- end
52
+ end
53
+ 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.prefixed_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.1'.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.1
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