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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 601ea757a5ae5b368fb3c3937d12f3281ac35f2f8ab4128d288919cf81208f03
4
+ data.tar.gz: 87e14bc708bc3d5def1eba63ecd2291d46a8c76d5f9c5dc8212bdb09c5a0de3e
5
+ SHA512:
6
+ metadata.gz: c64873b92386ffcf036a7c63e599aa3c05885fa8d53ee18ecd8ae3b679bc26cd84bd3129bae8651bd3a762eb9a32e90ae4c7e3db42e51433c29191c0a708eda1
7
+ data.tar.gz: 595b54f8b913642cc8d2a4c91a790ad781607060c49d12e9aa5caa0f254244a98860d8df8587ddaa364aa17c55bb53f4bb2c5443b472f85ec226c540b91d2c91
data/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] - 2026-10-26
8
+
9
+ ### Added
10
+
11
+ - `CashfreePg::Client`: one instance per merchant, sandbox or production, pinned API version `2025-01-01`, client secret hidden in `inspect`, `to_s`, `pp` and error messages.
12
+ - HTTPS only, TLS verified, no redirects, no request logging and no automatic retries. A timed-out POST raises `TimeoutError` telling you to fetch before retrying.
13
+ - Typed errors for HTTP 400, 401, 403, 404, 409, 422, 429 and 5xx, plus `ConnectionError` and `TimeoutError`, each with the Cashfree request id.
14
+ - Exact amounts: String, Integer or BigDecimal in, two-decimal JSON numbers out, BigDecimal back. A Float amount warns.
15
+ - Orders: create (with an optional idempotency key), fetch and terminate.
16
+ - Payments: list, fetch, and capture or void a pre-authorised payment.
17
+ - UPI on the order session: intent, QR and collect, without sending the client secret.
18
+ - Refunds: create with your own `refund_id`, fetch and list.
19
+ - Payment links: create, fetch, cancel, and list the orders a link created (needed to refund a link payment).
20
+ - `docs/api.md`: every method with its Cashfree endpoint and reference link.
21
+ - No telemetry: the gem sends nothing except the Cashfree calls you make.
22
+ - Webhook verification with `CashfreePg::Webhook.construct_event`: raw body, constant-time signature check, optional timestamp window, and `SignatureVerificationError` for anything that cannot be trusted, including a missing secret.
23
+ - Ruby 3.3 or newer. Faraday 2 is the only runtime dependency.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Muballigh Faraz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,342 @@
1
+ <div align="center">
2
+
3
+ <img src="docs/assets/logo.png" alt="cashfree_pg: Unofficial Ruby Client for Cashfree Payments" width="360">
4
+
5
+ **Orders, payments, UPI, refunds, payment links and webhook verification<br>for the Cashfree Payment Gateway, from plain Ruby or Rails.**
6
+
7
+ [![CI](https://github.com/Shadbox-Infosystem/cashfree_pg/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Shadbox-Infosystem/cashfree_pg/actions/workflows/ci.yml)
8
+ [![Ruby](https://img.shields.io/badge/ruby-%E2%89%A5%203.3-CC342D?logo=ruby&logoColor=white)](cashfree_pg.gemspec)
9
+ [![Cashfree API](https://img.shields.io/badge/x--api--version-2025--01--01-2F3B52)](docs/api.md)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
11
+
12
+ [Quick start](#-quick-start) · [Payment flow](#-payment-flow) · [Rails](#-rails-integration) · [API reference](docs/api.md) · [Webhooks](docs/webhooks.md) · [Sandbox testing](#-sandbox-testing)
13
+
14
+ </div>
15
+
16
+ > [!IMPORTANT]
17
+ > **Community project, not an official SDK.** `cashfree_pg` is built and maintained independently. It is not affiliated with, endorsed by, or supported by Cashfree Payments.
18
+
19
+ ## ⚡ Overview
20
+
21
+ `cashfree_pg` wraps the Cashfree Payment Gateway (PG) REST API in a small, instance-based Ruby interface. It keeps Cashfree's own field names, so Cashfree's docs still apply, and it handles the parts that are easy to get wrong in a payment integration.
22
+
23
+ > **You write:** `client.orders.create(order_id: "order_42", order_amount: "499.00", ...)`<br>
24
+ > **You get:** a `Response` with Cashfree's fields, amounts as `BigDecimal`, the `request_id` for support, and a typed exception for every failure. No raw HTTP, no `Float` money, no secrets in your logs.
25
+
26
+ The gem talks only to Cashfree over HTTPS. It never handles card data, and it has no telemetry, request logging or automatic retries. Faraday is its only runtime dependency.
27
+
28
+ <table>
29
+ <tr>
30
+ <td width="33%" valign="top"><h4>🧾 Orders</h4>Create, fetch and terminate, with an optional idempotency key.</td>
31
+ <td width="33%" valign="top"><h4>💳 Payments</h4>List and fetch, plus capture or void pre-authorised payments.</td>
32
+ <td width="33%" valign="top"><h4>📱 UPI</h4>Intent, QR and collect on the order session, without sending your secret.</td>
33
+ </tr>
34
+ <tr>
35
+ <td valign="top"><h4>↩️ Refunds</h4>Create with your own <code>refund_id</code>, then fetch or list.</td>
36
+ <td valign="top"><h4>🔗 Payment Links</h4>Create, fetch and cancel, and find the orders a paid link created.</td>
37
+ <td valign="top"><h4>🔐 Webhooks</h4>Constant-time HMAC-SHA256 check, optional timestamp window.</td>
38
+ </tr>
39
+ <tr>
40
+ <td valign="top"><h4>🧯 Typed Errors</h4>One exception per HTTP status, with Cashfree's <code>code</code> and <code>request_id</code>.</td>
41
+ <td valign="top"><h4>💰 Exact Money</h4>Two-decimal JSON amounts out, <code>BigDecimal</code> back in.</td>
42
+ <td valign="top"><h4>🛡️ Safe Defaults</h4>HTTPS only, TLS verified, secret redacted from <code>inspect</code> and errors.</td>
43
+ </tr>
44
+ </table>
45
+
46
+ ## 🔄 Payment flow
47
+
48
+ How one payment moves through your app, `cashfree_pg` and Cashfree:
49
+
50
+ <p align="center">
51
+ <picture>
52
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/payment-flow-dark.svg">
53
+ <img src="docs/assets/payment-flow-light.svg" width="100%" alt="Payment flow: 1. create the order with orders.create on your server, 2. the customer pays on Cashfree checkout or UPI, 3. verify the webhook with Webhook.construct_event, 4. confirm with orders.fetch that order_status is PAID, 5. optionally refund with refunds.create">
54
+ </picture>
55
+ </p>
56
+
57
+ - 🔁 **Retries are yours.** The gem never retries. Reuse your own `order_id` or `refund_id` and the call is safe to repeat.
58
+ - 🔔 **Verify, then confirm.** Check the webhook signature, deduplicate repeats, and fetch the order before you fulfil it.
59
+ - 🧾 **Log the `request_id`.** Every response and error carries one, and it is what Cashfree support asks for.
60
+
61
+ ## 🚀 Quick start
62
+
63
+ | Requirement | Version |
64
+ | --- | --- |
65
+ | Ruby | 3.3 or newer |
66
+ | Cashfree account | PG API credentials, sandbox or production |
67
+
68
+ **1. Install.** Add the gem to your `Gemfile` and run `bundle install`:
69
+
70
+ ```ruby
71
+ gem "cashfree_pg", "~> 0.1.0"
72
+ ```
73
+
74
+ **2. Create a client.** There is no global configuration: credentials live on the instance, one client per merchant account. The gem reads nothing from `ENV` itself.
75
+
76
+ ```ruby
77
+ require "cashfree_pg"
78
+
79
+ client = CashfreePg::Client.new(
80
+ client_id: ENV.fetch("CASHFREE_CLIENT_ID"),
81
+ client_secret: ENV.fetch("CASHFREE_CLIENT_SECRET"),
82
+ environment: :sandbox # or :production, with production credentials
83
+ )
84
+ ```
85
+
86
+ **3. Take a payment.**
87
+
88
+ ```ruby
89
+ order = client.orders.create(
90
+ order_id: "order_#{SecureRandom.hex(6)}",
91
+ order_amount: "499.00", # String, Integer or BigDecimal; never Float
92
+ order_currency: "INR",
93
+ customer_details: { customer_id: "cust_42", customer_phone: "9999999999" }
94
+ )
95
+
96
+ order.payment_session_id # hand this to Cashfree checkout on the frontend
97
+ client.orders.fetch(order.order_id).order_status # => "ACTIVE", "PAID", ...
98
+ ```
99
+
100
+ <details>
101
+ <summary><b>All client options</b></summary>
102
+
103
+ <br>
104
+
105
+ | Option | Default | Notes |
106
+ | --- | --- | --- |
107
+ | `client_id` | required | Your Cashfree PG app id. |
108
+ | `client_secret` | required | Never shown by `inspect`, `to_s` or `pp`. Also the webhook signing secret. |
109
+ | `environment` | `:sandbox` | `:sandbox` (`sandbox.cashfree.com/pg`) or `:production` (`api.cashfree.com/pg`). Must be a Symbol. |
110
+ | `api_version` | `"2025-01-01"` | Sent as `x-api-version` on every call. |
111
+ | `open_timeout` | `30` | Seconds to wait for a connection. |
112
+ | `read_timeout` | `60` | Seconds to wait for a response. |
113
+
114
+ [`.env.example`](.env.example) lists the variable names this repository's tooling uses. Never commit real credentials; `.env` is git-ignored.
115
+
116
+ To try unreleased changes, install from GitHub: `gem "cashfree_pg", github: "Shadbox-Infosystem/cashfree_pg", branch: "main"`.
117
+
118
+ </details>
119
+
120
+ ## 📦 Usage
121
+
122
+ Single objects come back as a `CashfreePg::Response`, with Cashfree's field names exactly:
123
+
124
+ ```ruby
125
+ order.order_status # field as a method
126
+ order["order_amount"] # => BigDecimal("499.0")
127
+ order.dig(:customer_details, :customer_id) # use [] or dig for keys like hash or class
128
+ order.request_id # quote this to Cashfree support
129
+ ```
130
+
131
+ `payments.list` and `payment_links.orders` return an Array of `Response`. Every method and its Cashfree endpoint is listed in [docs/api.md](docs/api.md).
132
+
133
+ <details>
134
+ <summary><b>UPI: intent, QR and collect</b></summary>
135
+
136
+ <br>
137
+
138
+ ```ruby
139
+ client.payments.upi(order, channel: :intent) # sent as Cashfree's "link"
140
+ client.payments.upi(order, channel: :qr) # sent as "qrcode"
141
+ client.payments.upi(order, channel: :collect, upi_id: "testsuccess@gocash")
142
+ client.payments.upi(order.payment_session_id, channel: :qr, upi_expiry_minutes: 10)
143
+ ```
144
+
145
+ Calling Order Pay from a server needs Cashfree's S2S feature on your account. Without it, Cashfree returns 403 and the gem raises `CashfreePg::PermissionError` saying so.
146
+
147
+ </details>
148
+
149
+ <details>
150
+ <summary><b>Payments and pre-authorisation</b></summary>
151
+
152
+ <br>
153
+
154
+ ```ruby
155
+ payments = client.payments.list(order.order_id)
156
+ client.payments.fetch(order.order_id, payments.first.cf_payment_id)
157
+
158
+ client.payments.authorize(order.order_id, action: :capture, amount: "499.00", idempotency_key: "cap_1")
159
+ client.payments.authorize(order.order_id, action: :void, idempotency_key: "void_1")
160
+ ```
161
+
162
+ </details>
163
+
164
+ <details>
165
+ <summary><b>Refunds</b></summary>
166
+
167
+ <br>
168
+
169
+ ```ruby
170
+ client.refunds.create(order.order_id, refund_amount: "499.00", refund_id: "refund_00912")
171
+ client.refunds.fetch(order.order_id, "refund_00912")
172
+ client.refunds.list(order.order_id)
173
+ ```
174
+
175
+ Always pass your own `refund_id`. If a create times out, fetch that id before trying again; a duplicate raises `CashfreePg::ConflictError`.
176
+
177
+ </details>
178
+
179
+ <details>
180
+ <summary><b>Payment links</b></summary>
181
+
182
+ <br>
183
+
184
+ ```ruby
185
+ link = client.payment_links.create(
186
+ link_id: "link_123", link_amount: "499.00", link_currency: "INR", link_purpose: "Invoice 1042",
187
+ customer_details: { customer_phone: "9999999999" }
188
+ )
189
+ link.link_url # send this to the customer
190
+ client.payment_links.cancel(link.link_id) # ACTIVE links only
191
+
192
+ paid = client.payment_links.orders(link.link_id) # Cashfree creates the order when the link is paid
193
+ client.refunds.create(paid.first.order_id, refund_amount: "499.00", refund_id: "refund_link_1")
194
+ ```
195
+
196
+ </details>
197
+
198
+ <details>
199
+ <summary><b>Terminating orders and calling other endpoints</b></summary>
200
+
201
+ <br>
202
+
203
+ ```ruby
204
+ client.orders.terminate(order.order_id).order_status # => "TERMINATED" or "TERMINATION_REQUESTED"
205
+ client.get("/orders/order_123") # any PG endpoint, same auth headers, errors and Response
206
+ ```
207
+
208
+ `client.get`, `client.post` and `client.patch` take paths relative to `/pg`.
209
+
210
+ </details>
211
+
212
+ ## 🧯 Error handling
213
+
214
+ Every exception inherits from `CashfreePg::Error` and exposes `status`, `code`, `type` and `request_id`. Messages never contain the client secret.
215
+
216
+ ```ruby
217
+ begin
218
+ order = client.orders.create(order_id: "order_123", order_amount: "499.00", order_currency: "INR",
219
+ customer_details: { customer_id: "cust_42", customer_phone: "9999999999" })
220
+ rescue CashfreePg::ConflictError, CashfreePg::TimeoutError
221
+ order = client.orders.fetch("order_123") # already exists, or outcome unknown: check, don't recreate
222
+ rescue CashfreePg::Error => e
223
+ warn "Cashfree #{e.class}: status=#{e.status} code=#{e.code} request_id=#{e.request_id}"
224
+ raise
225
+ end
226
+ ```
227
+
228
+ HTTP 400, 401, 403, 404, 409, 422 and 429 each have their own class; 5xx raises `APIError`, and a missing response raises `ConnectionError` or `TimeoutError`. The full mapping is in [docs/api.md](docs/api.md#errors). Bad local input, such as a blank id or an unknown UPI channel, raises `ArgumentError` before any request is sent.
229
+
230
+ ## 🚂 Rails integration
231
+
232
+ No Railtie needed: an initializer, a service object, and a controller for webhooks.
233
+
234
+ ```ruby
235
+ # config/initializers/cashfree.rb
236
+ CASHFREE = CashfreePg::Client.new(
237
+ client_id: ENV.fetch("CASHFREE_CLIENT_ID"),
238
+ client_secret: ENV.fetch("CASHFREE_CLIENT_SECRET"),
239
+ environment: ENV.fetch("CASHFREE_ENVIRONMENT", "sandbox").to_sym
240
+ )
241
+
242
+ # app/services/cashfree_checkout.rb
243
+ class CashfreeCheckout
244
+ def self.call(order_id:, amount:, customer_id:, phone:)
245
+ CASHFREE.orders.create(
246
+ order_id: order_id, order_amount: amount.to_s, order_currency: "INR",
247
+ customer_details: { customer_id: customer_id, customer_phone: phone }
248
+ ).payment_session_id
249
+ end
250
+ end
251
+
252
+ # app/controllers/cashfree_webhooks_controller.rb (route: post "/webhooks/cashfree")
253
+ class CashfreeWebhooksController < ActionController::API
254
+ def create
255
+ event = CashfreePg::Webhook.construct_event(
256
+ raw_body: request.raw_post,
257
+ headers: request.headers,
258
+ secret: ENV.fetch("CASHFREE_CLIENT_SECRET")
259
+ )
260
+ CashfreeEventJob.perform_later(event.type, event.data) # your job; deduplicate there
261
+ head :ok
262
+ rescue CashfreePg::SignatureVerificationError
263
+ head :bad_request
264
+ end
265
+ end
266
+ ```
267
+
268
+ > [!WARNING]
269
+ > Pass `request.raw_post`, never parsed params. Re-encoding changes amounts like `10.10` and the signature will not match. See [docs/webhooks.md](docs/webhooks.md) for deduplication, the optional `tolerance:` window and Cashfree's IP allow-list.
270
+
271
+ ## 🧪 Sandbox testing
272
+
273
+ **Unit specs run on every push.** RSpec with WebMock (real network blocked) covers every resource method, headers, money handling, each HTTP status mapping, timeouts, secret redaction and webhook signatures. SimpleCov fails the run below 90% line coverage, and [CI](.github/workflows/ci.yml) runs on Ruby 3.3, 3.4 and latest, with `bundler-audit` and a `gitleaks` secret scan.
274
+
275
+ **Integration specs hit the real sandbox.** [`spec/integration/sandbox_spec.rb`](spec/integration/sandbox_spec.rb) runs against `sandbox.cashfree.com/pg` when you provide credentials. It skips without them, refuses to run with `CASHFREE_ENVIRONMENT=production`, and does not run in CI.
276
+
277
+ | Flow | Verified against the sandbox |
278
+ | --- | --- |
279
+ | Order → UPI intent → payments | `payment_session_id` returned, intent channel is `link`, payments list is an Array |
280
+ | Terminate | Unpaid order becomes `TERMINATED` or `TERMINATION_REQUESTED` |
281
+ | Payment link lifecycle | Created, fetched, orders listed, cancelled |
282
+ | Refund | Created on a paid order and fetched by `refund_id` (needs `CASHFREE_PAID_ORDER_ID`) |
283
+
284
+ ```bash
285
+ cp .env.example .env # sandbox credentials only
286
+ set -a; source .env; set +a
287
+ bundle exec rspec spec/integration
288
+ bin/console # IRB with the gem loaded, sandbox only
289
+ ```
290
+
291
+ <details>
292
+ <summary><b>Sandbox test values</b></summary>
293
+
294
+ <br>
295
+
296
+ | Value | Effect in sandbox |
297
+ | --- | --- |
298
+ | `upi_id: "testsuccess@gocash"` | Successful UPI collect |
299
+ | `refund_note: "SUCCESS"`, `"FAILED"`, `"PENDING"`, `"ACTIVE"` | Simulates that refund status |
300
+ | `link_notify: { send_email: true }` | Sandbox sends email; SMS is production only |
301
+
302
+ </details>
303
+
304
+ ## 🧭 Coverage and roadmap
305
+
306
+ | Status | Scope |
307
+ | :---: | --- |
308
+ | ✅ | Orders, payments, pre-authorisation, UPI (intent, QR, collect), refunds, payment links |
309
+ | ✅ | Webhook verification, typed errors, exact money, RBS signatures, idempotency keys |
310
+ | 🚧 | First RubyGems release (v0.1.0) through the trusted-publishing [release workflow](.github/workflows/release.yml) |
311
+ | 📋 | Stabilise the public API for v1.0 |
312
+ | 📋 | More PG endpoints, such as Order Pay for cards and netbanking, settlements and subscriptions |
313
+
314
+ Until v1.0, a minor release may include breaking changes, listed in the [changelog](CHANGELOG.md). Pin with `~> 0.1.0` to get only patch updates. Missing something? [Open an issue](https://github.com/Shadbox-Infosystem/cashfree_pg/issues) before starting larger work.
315
+
316
+ ## 🤝 Contributing
317
+
318
+ Bug reports and pull requests are welcome. This gem sits in payment flows, so correctness and security come before new features. Read [CONTRIBUTING.md](CONTRIBUTING.md) first.
319
+
320
+ ```bash
321
+ git checkout dev && git checkout -b fix/short-description
322
+ bin/setup # install dependencies
323
+ bundle exec rake # specs, RuboCop, RBS validation and gem contents check
324
+ ```
325
+
326
+ Match new endpoints to Cashfree's current API reference, cover them with RSpec and WebMock, never use `Float` for amounts, add a line under `[Unreleased]` in [CHANGELOG.md](CHANGELOG.md), and open the pull request against `dev`. Never put real credentials or customer data in code, fixtures or screenshots. Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md). Everyone taking part follows the [code of conduct](CODE_OF_CONDUCT.md).
327
+
328
+ ## 📄 License
329
+
330
+ Released under the [MIT License](LICENSE.txt). Copyright © 2026 Muballigh Faraz.
331
+
332
+ "Cashfree" and "Cashfree Payments" are names of their respective owner, used here only to describe which API this library talks to. For official SDKs and support, see [cashfree.com](https://www.cashfree.com).
333
+
334
+ <br>
335
+
336
+ <div align="center">
337
+
338
+ **Built for the Ruby community ❤️**
339
+
340
+ <sub><code>cashfree_pg</code> · Unofficial Ruby Client for Cashfree Payments</sub>
341
+
342
+ </div>
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require_relative "request"
5
+
6
+ module CashfreePg
7
+ # One merchant's credentials and the calls made with them.
8
+ #
9
+ # Checkout, refunds and payment links are available on this client.
10
+ #
11
+ # @example
12
+ # client = CashfreePg::Client.new(
13
+ # client_id: ENV.fetch("CASHFREE_CLIENT_ID"),
14
+ # client_secret: ENV.fetch("CASHFREE_CLIENT_SECRET"),
15
+ # environment: :sandbox
16
+ # )
17
+ class Client
18
+ SANDBOX_URL = "https://sandbox.cashfree.com/pg"
19
+ PRODUCTION_URL = "https://api.cashfree.com/pg"
20
+ DEFAULT_API_VERSION = "2025-01-01"
21
+
22
+ # @return [String]
23
+ attr_reader :client_id
24
+
25
+ # @return [String]
26
+ attr_reader :client_secret
27
+
28
+ # @return [Symbol]
29
+ attr_reader :environment
30
+
31
+ # @return [String]
32
+ attr_reader :api_version
33
+
34
+ # @return [String]
35
+ attr_reader :base_url
36
+
37
+ # @param client_id [String]
38
+ # @param client_secret [String]
39
+ # @param environment [Symbol] `:sandbox` or `:production`
40
+ # @param api_version [String]
41
+ # @param open_timeout [Numeric]
42
+ # @param read_timeout [Numeric]
43
+ # @param base_url [String, nil] test-only override; must be https
44
+ # @param allow_custom_base_url [Boolean]
45
+ def initialize(client_id:, client_secret:, environment: :sandbox, api_version: DEFAULT_API_VERSION,
46
+ open_timeout: 30, read_timeout: 60, base_url: nil, allow_custom_base_url: false)
47
+ @client_id = required_string(client_id, "client_id")
48
+ @client_secret = required_string(client_secret, "client_secret")
49
+ @environment = environment
50
+ @api_version = required_string(api_version, "api_version")
51
+ @base_url = resolve_base_url(base_url, allow_custom_base_url)
52
+ @connection = build_connection(open_timeout, read_timeout)
53
+ end
54
+
55
+ # @param path [String] absolute path on the Cashfree host, such as `/orders`
56
+ # @param skip_auth [Boolean] omit client id and secret; `x-api-version` is still sent
57
+ # @return [Response]
58
+ # @example
59
+ # client.get("/orders/order_123")
60
+ def get(path, skip_auth: false)
61
+ call(:get, path, skip_auth: skip_auth)
62
+ end
63
+
64
+ # @param path [String]
65
+ # @param body [Hash, nil]
66
+ # @param skip_auth [Boolean]
67
+ # @param idempotency_key [String, nil] sent as `x-idempotency-key`; the gem does not retry
68
+ # @return [Response]
69
+ # @example
70
+ # client.post("/orders", { order_amount: "10.10", order_currency: "INR" })
71
+ def post(path, body = nil, skip_auth: false, idempotency_key: nil)
72
+ call(:post, path, body: body, skip_auth: skip_auth, idempotency_key: idempotency_key)
73
+ end
74
+
75
+ # @param path [String]
76
+ # @param body [Hash]
77
+ # @return [Response]
78
+ # @example
79
+ # client.patch("/orders/order_123", { order_status: "TERMINATED" })
80
+ def patch(path, body)
81
+ call(:patch, path, body: body)
82
+ end
83
+
84
+ # @return [Refunds]
85
+ # @example
86
+ # client.refunds.create("order_123", refund_amount: "10.10", refund_id: "refund_00912")
87
+ def refunds
88
+ @refunds ||= Refunds.new(self)
89
+ end
90
+
91
+ # @return [PaymentLinks]
92
+ # @example
93
+ # client.payment_links.fetch("link_123")
94
+ def payment_links
95
+ @payment_links ||= PaymentLinks.new(self)
96
+ end
97
+
98
+ # @return [Orders]
99
+ # @example
100
+ # client.orders.create(order_amount: "10.10", order_currency: "INR")
101
+ def orders
102
+ @orders ||= Orders.new(self)
103
+ end
104
+
105
+ # @return [Payments]
106
+ # @example
107
+ # client.payments.upi(order, channel: :intent)
108
+ def payments
109
+ @payments ||= Payments.new(self)
110
+ end
111
+
112
+ # `pp` uses this too, so the secret stays hidden there as well.
113
+ #
114
+ # @return [String] client secret is always `[FILTERED]`
115
+ def inspect
116
+ "#<CashfreePg::Client environment=#{environment.inspect} api_version=#{api_version.inspect} " \
117
+ "client_id=#{client_id.inspect} client_secret=[FILTERED]>"
118
+ end
119
+ alias to_s inspect
120
+
121
+ private
122
+
123
+ def call(method, path, **)
124
+ Request.new(self, @connection).perform(method, path, **)
125
+ end
126
+
127
+ def required_string(value, name)
128
+ string = value.to_s
129
+ raise ArgumentError, "#{name} is required" if string.strip.empty?
130
+
131
+ string
132
+ end
133
+
134
+ def resolve_base_url(custom, allowed)
135
+ return environment_url if custom.nil?
136
+ raise ArgumentError, "base_url is only allowed when allow_custom_base_url: true" unless allowed
137
+ raise ArgumentError, "base_url must use https" unless custom.start_with?("https://")
138
+
139
+ custom.chomp("/")
140
+ end
141
+
142
+ def environment_url
143
+ case environment
144
+ when :sandbox then SANDBOX_URL
145
+ when :production then PRODUCTION_URL
146
+ else
147
+ raise ArgumentError, "environment must be :sandbox or :production"
148
+ end
149
+ end
150
+
151
+ def build_connection(open_timeout, read_timeout)
152
+ Faraday.new(url: base_url) do |faraday|
153
+ faraday.options.open_timeout = open_timeout
154
+ faraday.options.timeout = read_timeout
155
+ faraday.ssl.verify = true
156
+ faraday.adapter Faraday.default_adapter
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CashfreePg
4
+ # Base error. The message never includes the raw body or the client secret.
5
+ class Error < StandardError
6
+ # @return [Integer, nil] HTTP status, when the server responded
7
+ attr_reader :status
8
+
9
+ # @return [String, nil] Cashfree error code
10
+ attr_reader :code
11
+
12
+ # @return [String, nil] Cashfree error type
13
+ attr_reader :type
14
+
15
+ # @return [String, nil] x-request-id for this call
16
+ attr_reader :request_id
17
+
18
+ # @return [String, nil] response body, for callers who need it
19
+ attr_reader :raw_body
20
+
21
+ # @param message [String, nil]
22
+ # @param status [Integer, nil]
23
+ # @param code [String, nil]
24
+ # @param type [String, nil]
25
+ # @param request_id [String, nil]
26
+ # @param raw_body [String, nil]
27
+ def initialize(message = nil, status: nil, code: nil, type: nil, request_id: nil, raw_body: nil)
28
+ @status = status
29
+ @code = code
30
+ @type = type
31
+ @request_id = request_id
32
+ @raw_body = raw_body
33
+ super(message)
34
+ end
35
+
36
+ # @return [String]
37
+ def inspect
38
+ "#<#{self.class.name} status=#{status.inspect} code=#{code.inspect} " \
39
+ "type=#{type.inspect} request_id=#{request_id.inspect} message=#{message.inspect} raw_body=[FILTERED]>"
40
+ end
41
+ end
42
+
43
+ # HTTP 400.
44
+ class InvalidRequestError < Error; end
45
+
46
+ # HTTP 401.
47
+ class AuthenticationError < Error; end
48
+
49
+ # HTTP 403. Product not enabled, or the caller is not allowed.
50
+ class PermissionError < Error; end
51
+
52
+ # HTTP 404.
53
+ class NotFoundError < Error; end
54
+
55
+ # HTTP 409. A duplicate order, refund, or link id.
56
+ class ConflictError < Error; end
57
+
58
+ # HTTP 422.
59
+ class ValidationError < Error; end
60
+
61
+ # HTTP 429.
62
+ class RateLimitError < Error; end
63
+
64
+ # HTTP 5xx and any other unexpected status.
65
+ class APIError < Error; end
66
+
67
+ # No response was received (DNS, connection refused, TLS).
68
+ class ConnectionError < Error; end
69
+
70
+ # The call timed out and was not retried. For POST, fetch the resource before trying again.
71
+ class TimeoutError < Error; end
72
+
73
+ # The webhook signature, headers, or body could not be trusted.
74
+ class SignatureVerificationError < Error; end
75
+ end