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.
- checksums.yaml +4 -4
- data/app/controllers/spree/products_controller_decorator.rb +12 -13
- data/app/controllers/spree/razorpay/webhooks_controller.rb +72 -0
- data/app/javascript/spree_razorpay/application.js +12 -0
- data/app/javascript/spree_razorpay/controllers/checkout_razorpay_controller.js +108 -0
- data/app/jobs/spree_razorpay_checkout/handle_webhook_event_job.rb +65 -0
- data/app/models/spree/gateway/razorpay_gateway.rb +234 -58
- data/app/models/spree/page_blocks/products/razorpay_affordability.rb +4 -4
- data/app/models/spree/page_sections/product_details_decorator.rb +11 -14
- data/app/models/spree/payment_sessions/razorpay.rb +53 -0
- data/app/models/spree/razorpay_checkout.rb +6 -0
- data/app/models/spree_razorpay_checkout/spree/order_decorator.rb +6 -2
- data/app/services/razorpay/rp_order/api.rb +9 -1
- data/app/views/spree/admin/payment_methods/configuration_guides/_razorpay.html.erb +103 -5
- data/app/views/spree/admin/payment_methods/descriptions/_razorpay.html.erb +1 -0
- data/app/views/spree/checkout/payment/_razorpay.html.erb +121 -186
- data/config/routes.rb +4 -3
- data/lib/generators/spree_razorpay_checkout/install/install_generator.rb +21 -3
- data/lib/spree_razorpay_checkout/engine.rb +12 -8
- data/lib/spree_razorpay_checkout/version.rb +1 -1
- metadata +19 -36
- data/README.md +0 -188
- data/app/assets/images/payment_icons/icon_razorpay.svg +0 -20
- data/app/assets/javascripts/spree/frontend/process_razorpay.js +0 -40
- data/app/assets/javascripts/spree/frontend/spree_razorpay.js +0 -33
- data/app/models/spree_razorpay_checkout/spree/refund_decorator.rb +0 -36
- data/app/views/themes/default/spree/page_sections/_product_details.html.erb +0 -88
- data/config/application.rb +0 -1
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)
|
|
@@ -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 :
|
|
6
|
-
preference :
|
|
7
|
-
preference :
|
|
8
|
-
preference :
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
89
|
-
|
|
82
|
+
def source_required?
|
|
83
|
+
!preferred_headless_api_mode
|
|
90
84
|
end
|
|
91
85
|
|
|
92
|
-
def
|
|
93
|
-
|
|
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
|
|
97
|
-
|
|
94
|
+
def payment_session_class
|
|
95
|
+
Spree::PaymentSessions::Razorpay if defined?(Spree::PaymentSession)
|
|
98
96
|
end
|
|
99
97
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
Razorpay.setup(current_key_id, current_key_secret)
|
|
228
|
+
def purchase(_amount, source, _gateway_options = {})
|
|
229
|
+
provider
|
|
121
230
|
|
|
122
231
|
begin
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
|
139
|
-
|
|
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 "
|
|
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 "
|
|
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 "
|
|
69
|
+
Rails.logger.error " Missing template: #{e.message}"
|
|
70
70
|
''
|
|
71
71
|
rescue => e
|
|
72
|
-
Rails.logger.error "
|
|
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
|