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 +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 +197 -64
- data/app/models/spree/page_sections/product_details_decorator.rb +11 -13
- data/app/models/spree/payment_sessions/razorpay.rb +46 -36
- 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: c4200b3ff85e0fea3d0c4412d81dd7af4cf4a66cdb5d582b871b88a8ac6bc2fc
|
|
4
|
+
data.tar.gz: d142c51d56ba5b2f1eb445780fc887012e1e199abb767f5edaf85f8badc8ce17
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48701e4dda07a8fa192f320981418ae79c5b744bff3e650935a4fac1485c66878bd19414bc4ca72d7411b5ad829bcbf2b0161ed214818057a459630e76cc267d
|
|
7
|
+
data.tar.gz: 800b0a224a91684f8f7072adc953665091702bda9aafba80f6898bc009cf466300567a579707f53fba9ab855218e76da543beafacc24c562c6a8f3dd6b77eee5
|
|
@@ -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,160 @@ 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
|
+
# 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
|
-
|
|
139
|
-
rzp_payment = ::Razorpay::Payment.fetch(response_code)
|
|
281
|
+
rzp_payment_id = resolve_razorpay_payment_id(response_code)
|
|
140
282
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,53 @@
|
|
|
1
1
|
module Spree
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
@@ -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.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>
|
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.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.
|
|
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
|