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 +4 -4
- data/app/controllers/spree/products_controller_decorator.rb +12 -13
- data/app/controllers/spree/razorpay/webhooks_controller.rb +67 -30
- data/app/javascript/spree_razorpay/application.js +1 -1
- data/app/javascript/spree_razorpay/controllers/checkout_razorpay_controller.js +1 -1
- data/app/jobs/spree_razorpay_checkout/handle_webhook_event_job.rb +2 -1
- data/app/models/spree/gateway/razorpay_gateway.rb +184 -65
- data/app/models/spree/page_sections/product_details_decorator.rb +11 -13
- data/app/models/spree/payment_sessions/razorpay.rb +11 -40
- data/app/models/spree/razorpay_checkout.rb +5 -0
- data/app/models/spree_razorpay_checkout/spree/order_decorator.rb +6 -2
- data/app/views/spree/admin/payment_methods/configuration_guides/_razorpay.html.erb +103 -5
- data/app/views/spree/checkout/payment/_razorpay.html.erb +1 -1
- data/config/routes.rb +2 -1
- data/lib/spree_razorpay_checkout/engine.rb +1 -1
- data/lib/spree_razorpay_checkout/version.rb +1 -1
- metadata +19 -6
- data/app/models/spree_razorpay_checkout/spree/refund_decorator.rb +0 -36
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2ae9eca9f07041baff888721f0da631374e73925e8237eeafe32ccfe5185298
|
|
4
|
+
data.tar.gz: b087fa0f6961cc60c77c38cef002566104a19f6a3a87519658d69c2eb0174ec0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4262af60d12805b8d7ee60cae23b03f95d9211a806bc39d88d8a4cbe556e280428921d1c9c14b940f56d92128b1e952e7ea99f0d69436bb00071b16c425b83e5
|
|
7
|
+
data.tar.gz: bf60c0322601d7dfed075e214828ab5fabf98f55a57760ab3f7b73c9b40edc3c3da7a19ac04ac5ff267b23f8130171332c4a522da996533832d1568e23f0652d
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
module Spree
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
::Spree::ProductsController.prepend(Spree::ProductsControllerDecorator) if defined?(::Spree::ProductsController)
|
|
@@ -1,35 +1,72 @@
|
|
|
1
1
|
module Spree
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
139
|
-
rzp_payment = ::Razorpay::Payment.fetch(response_code)
|
|
267
|
+
rzp_payment_id = resolve_razorpay_payment_id(response_code)
|
|
140
268
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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>
|
data/config/routes.rb
CHANGED
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|