spree_razorpay_checkout 0.1.3 → 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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/spree/products_controller_decorator.rb +12 -13
  3. data/app/controllers/spree/razorpay/webhooks_controller.rb +72 -0
  4. data/app/javascript/spree_razorpay/application.js +12 -0
  5. data/app/javascript/spree_razorpay/controllers/checkout_razorpay_controller.js +108 -0
  6. data/app/jobs/spree_razorpay_checkout/handle_webhook_event_job.rb +65 -0
  7. data/app/models/spree/gateway/razorpay_gateway.rb +234 -58
  8. data/app/models/spree/page_blocks/products/razorpay_affordability.rb +4 -4
  9. data/app/models/spree/page_sections/product_details_decorator.rb +11 -14
  10. data/app/models/spree/payment_sessions/razorpay.rb +53 -0
  11. data/app/models/spree/razorpay_checkout.rb +6 -0
  12. data/app/models/spree_razorpay_checkout/spree/order_decorator.rb +6 -2
  13. data/app/services/razorpay/rp_order/api.rb +9 -1
  14. data/app/views/spree/admin/payment_methods/configuration_guides/_razorpay.html.erb +103 -5
  15. data/app/views/spree/admin/payment_methods/descriptions/_razorpay.html.erb +1 -0
  16. data/app/views/spree/checkout/payment/_razorpay.html.erb +121 -186
  17. data/config/routes.rb +4 -3
  18. data/lib/generators/spree_razorpay_checkout/install/install_generator.rb +21 -3
  19. data/lib/spree_razorpay_checkout/engine.rb +12 -8
  20. data/lib/spree_razorpay_checkout/version.rb +1 -1
  21. metadata +19 -36
  22. data/README.md +0 -188
  23. data/app/assets/images/payment_icons/icon_razorpay.svg +0 -20
  24. data/app/assets/javascripts/spree/frontend/process_razorpay.js +0 -40
  25. data/app/assets/javascripts/spree/frontend/spree_razorpay.js +0 -33
  26. data/app/models/spree_razorpay_checkout/spree/refund_decorator.rb +0 -36
  27. data/app/views/themes/default/spree/page_sections/_product_details.html.erb +0 -88
  28. data/config/application.rb +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fa91a572f8ba303d47dd90b0142eff75dc21478b85c9d85a9d2ca842318a4b9
4
- data.tar.gz: 889b39cf1a9076a0b88d6d9a08d06971d4890f9fb08dbd94b4e9ba170f2d92a2
3
+ metadata.gz: c4200b3ff85e0fea3d0c4412d81dd7af4cf4a66cdb5d582b871b88a8ac6bc2fc
4
+ data.tar.gz: d142c51d56ba5b2f1eb445780fc887012e1e199abb767f5edaf85f8badc8ce17
5
5
  SHA512:
6
- metadata.gz: f11e14acd2e4325710adc68337f10fd3d8ab3736dc5afb9d1dc91d91af5991ac3e9feaf296f337bad37ff5f469d2d4778c9dc33ecfac8314daf95766ebb9684e
7
- data.tar.gz: 21c4b3840513e8a19987a9901d5afe1d32f18832869e876c0bc03098165ab703a55a95e101a2092403458c4e1a5314de46d2474ea786c8b69e14a3fb190ceb91
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)
@@ -0,0 +1,72 @@
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
@@ -0,0 +1,12 @@
1
+ import { Application } from '@hotwired/stimulus'
2
+ import CheckoutRazorpayController from './controllers/checkout_razorpay_controller'
3
+
4
+ let application;
5
+ if (typeof window.Stimulus === "undefined") {
6
+ application = Application.start()
7
+ window.Stimulus = application
8
+ } else {
9
+ application = window.Stimulus
10
+ }
11
+
12
+ application.register('checkout-razorpay', CheckoutRazorpayController)
@@ -0,0 +1,108 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ paymentMethodId: String,
6
+ keyId: String,
7
+ orderId: String,
8
+ amount: Number,
9
+ currency: String,
10
+ merchantName: String,
11
+ merchantDesc: String,
12
+ themeColor: String,
13
+ userName: String,
14
+ userEmail: String,
15
+ userContact: String
16
+ }
17
+
18
+ static targets = [
19
+ 'paymentId',
20
+ 'orderId',
21
+ 'signature'
22
+ ]
23
+
24
+ connect() {
25
+ this.form = document.querySelector("#checkout_form_payment")
26
+ this.submitBtn = document.getElementById("checkout-payment-submit")
27
+
28
+ // Bind to the form submit event, NOT the button click!
29
+ // This perfectly intercepts Spree 5.3's native validation flows.
30
+ this.submitHandler = this.submit.bind(this)
31
+ this.form.addEventListener('submit', this.submitHandler)
32
+ }
33
+
34
+ disconnect() {
35
+ if (this.form) {
36
+ this.form.removeEventListener('submit', this.submitHandler)
37
+ }
38
+ }
39
+
40
+ submit(e) {
41
+ // Check if Razorpay is the currently selected radio button
42
+ const selectedRadio = document.querySelector('input[name="order[payments_attributes][][payment_method_id]"]:checked');
43
+ const isRazorpay = selectedRadio && selectedRadio.value === this.paymentMethodIdValue;
44
+
45
+ if (!isRazorpay) return; // If it's a different gateway, let Spree handle it normally
46
+
47
+ // If we already have the Razorpay signature populated, let the form submit natively to the backend!
48
+ if (this.paymentIdTarget.value && this.signatureTarget.value) {
49
+ return true;
50
+ }
51
+
52
+ // Otherwise, STOP the form submission and open Razorpay
53
+ e.preventDefault();
54
+ e.stopImmediatePropagation();
55
+
56
+ this.setLoading(true);
57
+
58
+ const options = {
59
+ key: this.keyIdValue,
60
+ order_id: this.orderIdValue,
61
+ amount: this.amountValue,
62
+ currency: this.currencyValue,
63
+ name: this.merchantNameValue,
64
+ description: this.merchantDescValue,
65
+ handler: this.handleSuccess.bind(this),
66
+ modal: {
67
+ ondismiss: this.handleDismiss.bind(this)
68
+ },
69
+ prefill: {
70
+ name: this.userNameValue,
71
+ email: this.userEmailValue,
72
+ contact: this.userContactValue
73
+ },
74
+ theme: {
75
+ color: this.themeColorValue
76
+ }
77
+ };
78
+
79
+ const rzp = new window.Razorpay(options);
80
+ rzp.open();
81
+ }
82
+
83
+ handleSuccess(response) {
84
+ // 1. Populate the hidden fields with the secure response
85
+ this.paymentIdTarget.value = response.razorpay_payment_id;
86
+ this.orderIdTarget.value = response.razorpay_order_id;
87
+ this.signatureTarget.value = response.razorpay_signature;
88
+
89
+ // 2. Submit the form natively to Spree's backend!
90
+ // requestSubmit() ensures Turbo and Spree's event listeners fire correctly
91
+ this.form.requestSubmit();
92
+ }
93
+
94
+ handleDismiss() {
95
+ this.setLoading(false);
96
+ }
97
+
98
+ setLoading(isLoading) {
99
+ if (isLoading) {
100
+ this.submitBtn.disabled = true;
101
+ this.submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Processing...';
102
+ } else {
103
+ this.submitBtn.disabled = false;
104
+ // Reset Spree button text depending on what it usually says
105
+ this.submitBtn.innerHTML = 'Save and Continue';
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,65 @@
1
+ module SpreeRazorpayCheckout
2
+ class HandleWebhookEventJob < ActiveJob::Base
3
+ queue_as :default
4
+
5
+ def perform(event)
6
+ payload = event['payload']
7
+ payment_entity = payload.dig('payment', 'entity') || payload.dig('order', 'entity')
8
+
9
+ # Razorpay might send the ID in slightly different places depending on the event
10
+ razorpay_order_id = payment_entity['order_id'] || payload.dig('order', 'entity', 'id')
11
+ razorpay_payment_id = payment_entity['id']
12
+
13
+ checkout_record = Spree::RazorpayCheckout.find_by(razorpay_order_id: razorpay_order_id)
14
+ return unless checkout_record
15
+
16
+ order = checkout_record.order
17
+ return if order.completed? || order.canceled?
18
+
19
+ gateway = Spree::Gateway::RazorpayGateway.active.first
20
+ return unless gateway
21
+
22
+ order.with_lock do
23
+ # 1. Capture the abandoned payment via API
24
+ begin
25
+ rzp_payment = ::Razorpay::Payment.fetch(razorpay_payment_id)
26
+ if rzp_payment.status == 'authorized'
27
+ amount_in_cents = (order.total_minus_store_credits.to_f * 100).to_i
28
+ rzp_payment.capture({ amount: amount_in_cents })
29
+ end
30
+ rescue StandardError => e
31
+ Rails.logger.error("Webhook Razorpay Capture Failed: #{e.message}")
32
+ return
33
+ end
34
+
35
+ checkout_record.update!(
36
+ razorpay_payment_id: razorpay_payment_id,
37
+ status: 'captured'
38
+ )
39
+
40
+ # 2. Create the Spree::Payment record natively
41
+ payment = order.payments.find_or_create_by!(
42
+ response_code: razorpay_payment_id,
43
+ payment_method_id: gateway.id
44
+ ) do |p|
45
+ p.amount = order.total
46
+ p.source = checkout_record
47
+ p.state = 'checkout'
48
+ end
49
+
50
+ # 3. Mark the payment as completed
51
+ payment.process! if payment.checkout?
52
+ payment.complete! if payment.pending? || payment.processing?
53
+
54
+ # Tell Spree to recalculate the payment state
55
+ order.updater.update_payment_state
56
+
57
+ # Loop through the checkout steps (Payment -> Confirm -> Complete)
58
+ until order.completed? || order.state == 'complete'
59
+ order.next!
60
+ end
61
+
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,24 +1,19 @@
1
1
  require 'razorpay'
2
+ require 'active_merchant'
2
3
 
3
4
  module Spree
4
5
  class Gateway::RazorpayGateway < Gateway
5
- preference :key_id, :string
6
- preference :key_secret, :string
7
- preference :test_key_id, :string
8
- preference :test_key_secret, :string
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: ''
9
11
  preference :test_mode, :boolean, default: false
10
12
  preference :merchant_name, :string, default: 'Razorpay'
11
13
  preference :merchant_description, :text, default: 'Razorpay Payment Gateway'
12
14
  preference :merchant_address, :string, default: 'Razorpay, Bangalore, India'
13
15
  preference :theme_color, :string, default: '#2e5bff'
14
-
15
- def supports?(_source)
16
- true
17
- end
18
-
19
- def source_required?
20
- false
21
- end
16
+ preference :headless_api_mode, :boolean, default: false
22
17
 
23
18
  def name
24
19
  'Razorpay Secure (UPI, Wallets, Cards & Netbanking)'
@@ -28,10 +23,6 @@ module Spree
28
23
  'razorpay'
29
24
  end
30
25
 
31
- def payment_source_class
32
- 'razorpay'
33
- end
34
-
35
26
  def payment_icon_name
36
27
  'razorpay'
37
28
  end
@@ -49,7 +40,7 @@ module Spree
49
40
  end
50
41
 
51
42
  def provider
52
- Razorpay.setup(current_key_id, current_key_secret)
43
+ ::Razorpay.setup(current_key_id, current_key_secret)
53
44
  end
54
45
 
55
46
  def current_key_id
@@ -69,7 +60,7 @@ module Spree
69
60
  end
70
61
 
71
62
  def actions
72
- %w[capture void]
63
+ %w[capture void credit]
73
64
  end
74
65
 
75
66
  def can_capture?(payment)
@@ -80,63 +71,248 @@ module Spree
80
71
  payment.state != 'void'
81
72
  end
82
73
 
83
- # Not used directly (we use custom flow), but kept it for compatibility
84
- def purchase(_amount, _transaction_details, _gateway_options = {})
85
- ActiveMerchant::Billing::Response.new(true, 'Razorpay success')
74
+ def supports?(_source)
75
+ true
76
+ end
77
+
78
+ def session_required?
79
+ preferred_headless_api_mode
86
80
  end
87
81
 
88
- def capture(*args)
89
- simulated_successful_billing_response
82
+ def source_required?
83
+ !preferred_headless_api_mode
90
84
  end
91
85
 
92
- def void(*)
93
- simulated_successful_billing_response
86
+ def setup_session_supported?
87
+ false
88
+ end
89
+
90
+ def payment_source_class
91
+ Spree::RazorpayCheckout
94
92
  end
95
93
 
96
- def credit(_credit_cents, _payment_id, _options)
97
- ActiveMerchant::Billing::Response.new(true, 'Refund successful')
94
+ def payment_session_class
95
+ Spree::PaymentSessions::Razorpay if defined?(Spree::PaymentSession)
98
96
  end
99
97
 
100
- def cancel(payment, _options = {})
101
- # If `payment` is a Spree::Payment, use its source
102
- source = if payment.respond_to?(:source)
103
- payment.source else payment end
104
- payment.void! if payment.respond_to?(:void!)
105
- if source.respond_to?(:razorpay_payment_id)
106
- # Uncomment if you want to actually trigger refund
107
- # Razorpay::Payment.fetch(source.razorpay_payment_id).refund
108
- OpenStruct.new(success?: true, authorization: source.razorpay_payment_id)
109
- else
110
- # fallback for string/unknown source
111
- OpenStruct.new(success?: true, authorization: nil)
112
- end
113
- rescue => e
114
- Rails.logger.error("Razorpay cancel failed: #{e.message}")
115
- OpenStruct.new(success?: false, message: e.message)
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
116
226
  end
117
227
 
118
- # Verify signature, fetch payment and capture if required. Returns Razorpay::Payment object.
119
- def verify_and_capture_razorpay_payment(order, razorpay_payment_id)
120
- Razorpay.setup(current_key_id, current_key_secret)
228
+ def purchase(_amount, source, _gateway_options = {})
229
+ provider
121
230
 
122
231
  begin
123
- payment = Razorpay::Payment.fetch(razorpay_payment_id)
124
- # If payment is not captured and auto_capture set true, capture it
125
- if payment.status == 'authorized'
126
- amount = order.inr_amt_in_paise
127
- payment = payment.capture(amount: amount)
232
+ if source.razorpay_payment_id.blank? || source.razorpay_signature.blank?
233
+ return ActiveMerchant::Billing::Response.new(false, 'Payment was not completed. Please try again.', {}, test: preferred_test_mode)
128
234
  end
129
235
 
130
- payment
131
- rescue Razorpay::Error => e
132
- raise Spree::Core::GatewayError, "Razorpay error: #{e.message}"
236
+ ::Razorpay::Utility.verify_payment_signature(
237
+ razorpay_order_id: source.razorpay_order_id,
238
+ razorpay_payment_id: source.razorpay_payment_id,
239
+ razorpay_signature: source.razorpay_signature
240
+ )
241
+
242
+ rzp_payment = ::Razorpay::Payment.fetch(source.razorpay_payment_id)
243
+ if rzp_payment.status == 'authorized'
244
+ rzp_payment.capture({ amount: _amount })
245
+ end
246
+
247
+ source.update!(status: 'captured')
248
+
249
+ ActiveMerchant::Billing::Response.new(true, 'Razorpay Payment Successful', {}, test: preferred_test_mode, authorization: source.razorpay_payment_id)
250
+
251
+ rescue StandardError => e
252
+ Rails.logger.error("Razorpay Verification/Capture Failed: #{e.message}")
253
+ ActiveMerchant::Billing::Response.new(false, 'Payment verification failed.', {}, test: preferred_test_mode)
254
+ end
255
+ end
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
133
271
  end
134
272
  end
135
273
 
136
- private
274
+ def capture(*args)
275
+ ActiveMerchant::Billing::Response.new(true, 'Already Captured', {}, test: preferred_test_mode)
276
+ end
277
+
278
+ def credit(credit_cents, response_code, _gateway_options = {})
279
+ provider
280
+ begin
281
+ rzp_payment_id = resolve_razorpay_payment_id(response_code)
282
+
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)
290
+ rescue StandardError => e
291
+ Rails.logger.error("Razorpay Refund Failed: #{e.message}")
292
+ ActiveMerchant::Billing::Response.new(false, "Refund failed: #{e.message}", {}, test: preferred_test_mode)
293
+ end
294
+ end
295
+
296
+ def void(response_code, _gateway_options = {})
297
+ provider
298
+ begin
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)
308
+ rescue StandardError => e
309
+ Rails.logger.error("Razorpay Void Failed: #{e.message}")
310
+ ActiveMerchant::Billing::Response.new(false, "Void failed: #{e.message}", {}, test: preferred_test_mode)
311
+ end
312
+ end
137
313
 
138
- def simulated_successful_billing_response
139
- ActiveMerchant::Billing::Response.new(true, '', {}, {})
314
+ def cancel(response_code, _source = nil, _options = {})
315
+ void(response_code)
140
316
  end
141
317
  end
142
- end
318
+ end
@@ -55,7 +55,7 @@ module Spree
55
55
  is_available = available?(locals)
56
56
  Rails.logger.info " Available check: #{is_available}"
57
57
  unless is_available
58
- Rails.logger.warn " ⚠️ Block marked as not available, but rendering anyway"
58
+ Rails.logger.warn " Block marked as not available, but rendering anyway"
59
59
  end
60
60
  end
61
61
 
@@ -63,13 +63,13 @@ module Spree
63
63
  Rails.logger.info " Rendering partial: spree/page_blocks/products/razorpay_affordability/razorpay_affordability"
64
64
  result = view_context.render partial: 'spree/page_blocks/products/razorpay_affordability/razorpay_affordability',
65
65
  locals: locals.merge(block: self, page_block: self)
66
- Rails.logger.info " Render successful, output length: #{result.to_s.length}"
66
+ Rails.logger.info " Render successful, output length: #{result.to_s.length}"
67
67
  result
68
68
  rescue ActionView::MissingTemplate => e
69
- Rails.logger.error " Missing template: #{e.message}"
69
+ Rails.logger.error " Missing template: #{e.message}"
70
70
  ''
71
71
  rescue => e
72
- Rails.logger.error " Error rendering Razorpay Affordability block: #{e.message}"
72
+ Rails.logger.error " Error rendering Razorpay Affordability block: #{e.message}"
73
73
  "<div class='razorpay-affordability-error'>Error loading Razorpay Affordability Widget</div>".html_safe
74
74
  end
75
75
  end