cashfree_pg 0.1.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 +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +342 -0
- data/lib/cashfree_pg/client.rb +160 -0
- data/lib/cashfree_pg/errors.rb +75 -0
- data/lib/cashfree_pg/money.rb +84 -0
- data/lib/cashfree_pg/orders.rb +42 -0
- data/lib/cashfree_pg/payment_links.rb +55 -0
- data/lib/cashfree_pg/payments.rb +110 -0
- data/lib/cashfree_pg/refunds.rb +38 -0
- data/lib/cashfree_pg/request.rb +137 -0
- data/lib/cashfree_pg/resource.rb +39 -0
- data/lib/cashfree_pg/response.rb +65 -0
- data/lib/cashfree_pg/version.rb +6 -0
- data/lib/cashfree_pg/webhook.rb +117 -0
- data/lib/cashfree_pg.rb +17 -0
- data/sig/cashfree_pg.rbs +125 -0
- metadata +89 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module CashfreePg
|
|
7
|
+
# Verifies a Cashfree webhook, then parses it.
|
|
8
|
+
#
|
|
9
|
+
# The signature is Base64(HMAC-SHA256(timestamp + raw body, client secret)).
|
|
10
|
+
# Pass the raw body string. A parsed Hash will not match.
|
|
11
|
+
# `tolerance` is a timestamp window in seconds. It is off unless you set it,
|
|
12
|
+
# because Cashfree retries reuse the original timestamp.
|
|
13
|
+
class Webhook
|
|
14
|
+
# A verified event. Unknown `type` values are returned as sent.
|
|
15
|
+
#
|
|
16
|
+
# @!attribute [r] type
|
|
17
|
+
# @return [String, nil]
|
|
18
|
+
# @!attribute [r] time
|
|
19
|
+
# @return [String, nil] Cashfree `event_time`
|
|
20
|
+
# @!attribute [r] data
|
|
21
|
+
# @return [Hash, nil]
|
|
22
|
+
# @!attribute [r] version
|
|
23
|
+
# @return [String, nil] `x-webhook-version`
|
|
24
|
+
Event = Data.define(:type, :time, :data, :version)
|
|
25
|
+
|
|
26
|
+
# @param raw_body [String] exact request body, not a parsed Hash
|
|
27
|
+
# @param headers [Hash] plain Hash, Rack env, or Rails headers
|
|
28
|
+
# @param secret [String] Cashfree client secret
|
|
29
|
+
# @param tolerance [Numeric, nil] seconds; nil skips the timestamp check
|
|
30
|
+
# @return [Event]
|
|
31
|
+
# @raise [SignatureVerificationError]
|
|
32
|
+
# @example
|
|
33
|
+
# CashfreePg::Webhook.construct_event(
|
|
34
|
+
# raw_body: request.raw_post,
|
|
35
|
+
# headers: request.headers,
|
|
36
|
+
# secret: ENV.fetch("CASHFREE_CLIENT_SECRET")
|
|
37
|
+
# )
|
|
38
|
+
def self.construct_event(raw_body:, headers:, secret:, tolerance: nil)
|
|
39
|
+
new(raw_body, headers, secret, tolerance).construct
|
|
40
|
+
end
|
|
41
|
+
private_class_method :new
|
|
42
|
+
|
|
43
|
+
# @api private
|
|
44
|
+
def initialize(raw_body, headers, secret, tolerance)
|
|
45
|
+
@raw_body = raw_body
|
|
46
|
+
@headers = headers
|
|
47
|
+
@secret = secret
|
|
48
|
+
@tolerance = tolerance
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @api private
|
|
52
|
+
def construct
|
|
53
|
+
check_input!
|
|
54
|
+
signature = present_header("x-webhook-signature")
|
|
55
|
+
timestamp = present_header("x-webhook-timestamp")
|
|
56
|
+
raise SignatureVerificationError, "webhook signature does not match" unless valid?(timestamp, signature)
|
|
57
|
+
|
|
58
|
+
check_window!(timestamp)
|
|
59
|
+
build_event(header("x-webhook-version"))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# A blank secret would let anyone sign with an empty key.
|
|
65
|
+
def check_input!
|
|
66
|
+
raise SignatureVerificationError, "webhook secret is missing" if @secret.to_s.strip.empty?
|
|
67
|
+
raise SignatureVerificationError, "webhook body must be a string" unless @raw_body.is_a?(String)
|
|
68
|
+
raise SignatureVerificationError, "webhook body is empty" if @raw_body.empty?
|
|
69
|
+
raise SignatureVerificationError, "webhook headers are missing" unless @headers.respond_to?(:each)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def valid?(timestamp, signature)
|
|
73
|
+
digest = OpenSSL::HMAC.digest("SHA256", @secret.to_s, "#{timestamp}#{@raw_body}")
|
|
74
|
+
expected = [digest].pack("m0")
|
|
75
|
+
signature.bytesize == expected.bytesize && OpenSSL.fixed_length_secure_compare(expected, signature)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def check_window!(timestamp)
|
|
79
|
+
return if @tolerance.nil? || within_window?(timestamp)
|
|
80
|
+
|
|
81
|
+
raise SignatureVerificationError, "webhook timestamp is outside the allowed window"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def within_window?(timestamp)
|
|
85
|
+
(Time.now.to_f - (Integer(timestamp, 10) / 1000.0)).abs <= @tolerance
|
|
86
|
+
rescue ArgumentError
|
|
87
|
+
false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def present_header(name)
|
|
91
|
+
value = header(name).to_s
|
|
92
|
+
raise SignatureVerificationError, "webhook #{name.delete_prefix("x-webhook-")} is missing" if value.strip.empty?
|
|
93
|
+
|
|
94
|
+
value
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def header(name)
|
|
98
|
+
@headers.each do |key, value|
|
|
99
|
+
return value if header_name(key) == name
|
|
100
|
+
end
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def header_name(key)
|
|
105
|
+
key.to_s.downcase.tr("_", "-").delete_prefix("http-")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_event(version)
|
|
109
|
+
payload = JSON.parse(@raw_body, decimal_class: BigDecimal)
|
|
110
|
+
raise SignatureVerificationError, "webhook body is not JSON" unless payload.is_a?(Hash)
|
|
111
|
+
|
|
112
|
+
Event.new(type: payload["type"], time: payload["event_time"], data: payload["data"], version: version)
|
|
113
|
+
rescue JSON::ParserError
|
|
114
|
+
raise SignatureVerificationError, "webhook body is not JSON"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/cashfree_pg.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cashfree_pg/version"
|
|
4
|
+
require_relative "cashfree_pg/errors"
|
|
5
|
+
require_relative "cashfree_pg/money"
|
|
6
|
+
require_relative "cashfree_pg/response"
|
|
7
|
+
require_relative "cashfree_pg/client"
|
|
8
|
+
require_relative "cashfree_pg/resource"
|
|
9
|
+
require_relative "cashfree_pg/orders"
|
|
10
|
+
require_relative "cashfree_pg/payments"
|
|
11
|
+
require_relative "cashfree_pg/refunds"
|
|
12
|
+
require_relative "cashfree_pg/payment_links"
|
|
13
|
+
require_relative "cashfree_pg/webhook"
|
|
14
|
+
|
|
15
|
+
# Ruby client for the Cashfree Payments API.
|
|
16
|
+
module CashfreePg
|
|
17
|
+
end
|
data/sig/cashfree_pg.rbs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
module CashfreePg
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
attr_reader status: Integer?
|
|
6
|
+
attr_reader code: String?
|
|
7
|
+
attr_reader type: String?
|
|
8
|
+
attr_reader request_id: String?
|
|
9
|
+
attr_reader raw_body: String?
|
|
10
|
+
|
|
11
|
+
def initialize: (?String? message, ?status: Integer?, ?code: String?, ?type: String?, ?request_id: String?, ?raw_body: String?) -> void
|
|
12
|
+
def inspect: () -> String
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class InvalidRequestError < Error
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class AuthenticationError < Error
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class PermissionError < Error
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class NotFoundError < Error
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class ConflictError < Error
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class ValidationError < Error
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class RateLimitError < Error
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class APIError < Error
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class ConnectionError < Error
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class TimeoutError < Error
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class SignatureVerificationError < Error
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class Response
|
|
49
|
+
attr_reader status: Integer
|
|
50
|
+
attr_reader request_id: String
|
|
51
|
+
|
|
52
|
+
def initialize: (Hash[String, untyped] | Array[untyped] data, status: Integer, request_id: String) -> void
|
|
53
|
+
def []: (String | Symbol key) -> untyped
|
|
54
|
+
def dig: (*String | Symbol keys) -> untyped
|
|
55
|
+
def to_h: () -> (Hash[String, untyped] | Array[untyped])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class Client
|
|
59
|
+
SANDBOX_URL: String
|
|
60
|
+
PRODUCTION_URL: String
|
|
61
|
+
DEFAULT_API_VERSION: String
|
|
62
|
+
|
|
63
|
+
attr_reader client_id: String
|
|
64
|
+
attr_reader client_secret: String
|
|
65
|
+
attr_reader environment: Symbol
|
|
66
|
+
attr_reader api_version: String
|
|
67
|
+
attr_reader base_url: String
|
|
68
|
+
|
|
69
|
+
def initialize: (client_id: String, client_secret: String, ?environment: Symbol, ?api_version: String, ?open_timeout: Numeric, ?read_timeout: Numeric, ?base_url: String?, ?allow_custom_base_url: bool) -> void
|
|
70
|
+
def get: (String path, ?skip_auth: bool) -> Response
|
|
71
|
+
def post: (String path, ?Hash[untyped, untyped]? body, ?skip_auth: bool, ?idempotency_key: String?) -> Response
|
|
72
|
+
def patch: (String path, Hash[untyped, untyped] body) -> Response
|
|
73
|
+
def orders: () -> Orders
|
|
74
|
+
def payments: () -> Payments
|
|
75
|
+
def refunds: () -> Refunds
|
|
76
|
+
def payment_links: () -> PaymentLinks
|
|
77
|
+
def inspect: () -> String
|
|
78
|
+
def to_s: () -> String
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class Resource
|
|
82
|
+
def initialize: (Client client) -> void
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class Orders < Resource
|
|
86
|
+
def create: (?idempotency_key: String?, **untyped) -> Response
|
|
87
|
+
def fetch: (String order_id) -> Response
|
|
88
|
+
def terminate: (String order_id) -> Response
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class Refunds < Resource
|
|
92
|
+
def create: (String order_id, ?idempotency_key: String?, **untyped) -> Response
|
|
93
|
+
def fetch: (String order_id, String refund_id) -> Response
|
|
94
|
+
def list: (String order_id) -> Response
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class PaymentLinks < Resource
|
|
98
|
+
def create: (?idempotency_key: String?, **untyped) -> Response
|
|
99
|
+
def fetch: (String link_id) -> Response
|
|
100
|
+
def cancel: (String link_id) -> Response
|
|
101
|
+
def orders: (String link_id, ?status: String?) -> Array[Response]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class Payments < Resource
|
|
105
|
+
CHANNELS: Hash[Symbol, String]
|
|
106
|
+
|
|
107
|
+
def list: (String order_id) -> Array[Response]
|
|
108
|
+
def fetch: (String order_id, String cf_payment_id) -> Response
|
|
109
|
+
def upi: (untyped order, channel: Symbol | String, ?upi_id: String?, **untyped) -> Response
|
|
110
|
+
def authorize: (String order_id, action: Symbol | String, ?amount: untyped?, ?idempotency_key: String?) -> Response
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class Webhook
|
|
114
|
+
class Event < ::Data
|
|
115
|
+
attr_reader type: String?
|
|
116
|
+
attr_reader time: String?
|
|
117
|
+
attr_reader data: untyped
|
|
118
|
+
attr_reader version: String?
|
|
119
|
+
|
|
120
|
+
def self.new: (type: String?, time: String?, data: untyped, version: String?) -> instance
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.construct_event: (raw_body: String, headers: untyped, secret: String, ?tolerance: Numeric?) -> Event
|
|
124
|
+
end
|
|
125
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cashfree_pg
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Muballigh Faraz
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-26 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '3'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '2.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3'
|
|
33
|
+
description: 'A small, well-tested Ruby client for Cashfree Payment Gateway APIs:
|
|
34
|
+
orders, payments, UPI, refunds, payment links and webhook verification. Instance-based,
|
|
35
|
+
thread-safe, with API version pinning, exact money handling and secrets redacted
|
|
36
|
+
by default. Not affiliated with or endorsed by Cashfree Payments.'
|
|
37
|
+
email:
|
|
38
|
+
- support@shadbox.com
|
|
39
|
+
executables: []
|
|
40
|
+
extensions: []
|
|
41
|
+
extra_rdoc_files: []
|
|
42
|
+
files:
|
|
43
|
+
- CHANGELOG.md
|
|
44
|
+
- LICENSE.txt
|
|
45
|
+
- README.md
|
|
46
|
+
- lib/cashfree_pg.rb
|
|
47
|
+
- lib/cashfree_pg/client.rb
|
|
48
|
+
- lib/cashfree_pg/errors.rb
|
|
49
|
+
- lib/cashfree_pg/money.rb
|
|
50
|
+
- lib/cashfree_pg/orders.rb
|
|
51
|
+
- lib/cashfree_pg/payment_links.rb
|
|
52
|
+
- lib/cashfree_pg/payments.rb
|
|
53
|
+
- lib/cashfree_pg/refunds.rb
|
|
54
|
+
- lib/cashfree_pg/request.rb
|
|
55
|
+
- lib/cashfree_pg/resource.rb
|
|
56
|
+
- lib/cashfree_pg/response.rb
|
|
57
|
+
- lib/cashfree_pg/version.rb
|
|
58
|
+
- lib/cashfree_pg/webhook.rb
|
|
59
|
+
- sig/cashfree_pg.rbs
|
|
60
|
+
homepage: https://github.com/Shadbox-Infosystem/cashfree_pg
|
|
61
|
+
licenses:
|
|
62
|
+
- MIT
|
|
63
|
+
metadata:
|
|
64
|
+
allowed_push_host: https://rubygems.org
|
|
65
|
+
homepage_uri: https://github.com/Shadbox-Infosystem/cashfree_pg
|
|
66
|
+
source_code_uri: https://github.com/Shadbox-Infosystem/cashfree_pg
|
|
67
|
+
changelog_uri: https://github.com/Shadbox-Infosystem/cashfree_pg/blob/main/CHANGELOG.md
|
|
68
|
+
bug_tracker_uri: https://github.com/Shadbox-Infosystem/cashfree_pg/issues
|
|
69
|
+
rubygems_mfa_required: 'true'
|
|
70
|
+
post_install_message:
|
|
71
|
+
rdoc_options: []
|
|
72
|
+
require_paths:
|
|
73
|
+
- lib
|
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: 3.3.0
|
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
84
|
+
requirements: []
|
|
85
|
+
rubygems_version: 3.5.22
|
|
86
|
+
signing_key:
|
|
87
|
+
specification_version: 4
|
|
88
|
+
summary: Unofficial Ruby client for the Cashfree Payments API.
|
|
89
|
+
test_files: []
|