DhanHQ 3.2.1 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 287ad8c77adb3623574c0ee14102cfc3e88695168f75e1550a25dbbff9eb8508
4
- data.tar.gz: a61b3f8e664f45f203b1afc3f840c9ed62a4e3c40e2f2d17fedc080ac682e029
3
+ metadata.gz: 065ac08985790dc4172816430025c482f539b506445c84181f69bdb56d30f510
4
+ data.tar.gz: 8f545f2ac8416dfadfe4fc0a5216b4203d06eaf05e1b1094d14ee571e84de342
5
5
  SHA512:
6
- metadata.gz: 2c9ad1a3df906b585b0308809fb75235cf475a23c4eaddfea3f699de2e577819a1b78315b60012b6c51bb6c398dcaa35daab31d9ae9452d81d84e3e76741f0a9
7
- data.tar.gz: 888e390276144b239a1bdd43afb9e77b58d21703587b898497b2426b8b78b47c7e03fd607bb3306fea49f9c2ae0e04a11ccb1b6968fc109fe61a8d2627b750a4
6
+ metadata.gz: 27d821b96f67997c64f1a523c2a366736dabcea5175e621014e22c9b691f35835f008f3bacaefe6b26229d2de92a78d44719f1b5ebff3e288960f156f7192331
7
+ data.tar.gz: 76558d36116bbc9c2b024aacc0c6cb1c30640b777cf2cd0832cb6d8da9b142fd6aca350318489a3d13f848136c1089bcd1ef014a2a975c8f66bde1e7c8131d10
data/CHANGELOG.md CHANGED
@@ -1,3 +1,51 @@
1
+ ## [3.3.0] - 2026-07-26
2
+
3
+ ### Added
4
+
5
+ - **Bang variants for every write method with a falsy failure contract** — `Order.place!`, `Order#modify!`/`#cancel!`/`#refresh!`, `SuperOrder.create!`/`#modify!`/`#cancel!`, `ForeverOrder.create!`, `IcebergOrder.create!`, `TwapOrder.create!`, `AlertOrder.create!`/`.modify!`, `PnlExit.configure!`/`.stop!`, `MultiOrder.place!`, `GlobalStocks::Order.place!`/`#modify!`/`#cancel!`.
6
+
7
+ Each raises `DhanHQ::OrderError` — which descends from `DhanHQ::Error`, so an existing `rescue DhanHQ::Error` handler still catches it — carrying whatever diagnostics the failure held. The non-bang methods are **untouched**: they return exactly what they returned before, so no existing caller changes behaviour.
8
+
9
+ This is step one of unifying the write return contracts. Today those contracts disagree: depending on the class and the failure, a rejected write comes back as `nil`, as `false`, or as a `DhanHQ::ErrorObject`, and `AlertOrder.modify` can return either of the first two from the same method. A caller cannot write one error branch. Unifying them outright would be a silent breaking change for dependent applications, because `nil` and `false` are falsy while `ErrorObject` is truthy — every `if result` failure branch in a dependent would quietly invert. So the migration is staged:
10
+
11
+ 1. **This release** — additive bang variants. Opt in per call site.
12
+ 2. **This release** — log a deprecation whenever a non-bang write returns a falsy failure, to find the remaining call sites from dependents' logs.
13
+ 3. **4.0.0** — non-bang methods return `ErrorObject` uniformly, gated on step 2 going quiet.
14
+
15
+ - **Deprecation notices for ambiguous write failures** (step two). When a non-bang write returns `nil`, `false` or a `DhanHQ::ErrorObject`, the SDK now logs once per call site:
16
+
17
+ ```
18
+ [DhanHQ] DEPRECATION: DhanHQ::Models::Order.place reported failure as nil. Write
19
+ methods return nil, false or a DhanHQ::ErrorObject inconsistently today and will all
20
+ return DhanHQ::ErrorObject in 4.0.0, which is truthy — an `if result` failure branch
21
+ will invert. Use DhanHQ::Models::Order.place! to get a DhanHQ::OrderError instead...
22
+ ```
23
+
24
+ The point is to surface the remaining call sites from dependent applications' logs before 4.0.0 changes any return value. Once per call site per process, not once per failure — a session that rejects hundreds of orders would otherwise produce noise that gets filtered out, defeating the purpose.
25
+
26
+ This layer only observes: it never alters a return value and never raises. It is silent on success, silent when reached through a bang variant (those callers have already migrated), and silent when you set `config.warn_on_ambiguous_write_failure = false` / `DHAN_WARN_AMBIGUOUS_WRITE_FAILURE=false`. Defaults to on, because a notice nobody sees finds nothing.
27
+
28
+ - **`DhanHQ::Concerns::TrackedWrites`** — installs the observation. Uses `prepend` rather than `include`, since the write methods are defined directly on the model classes and an included module would sit behind them in the ancestor chain and never be reached.
29
+ - **`DhanHQ::Deprecation`** — once-per-key notice registry, thread-safe, with `warned_keys` and `reset!` for tests.
30
+ - **`DhanHQ::WriteResult`** — puts the knowledge of what a write failure looks like in one place (`failure?`, `success?`, `unwrap!`). `BaseModel#save!` now uses it instead of duplicating the same three-way check inline.
31
+ - **`DhanHQ::Concerns::BangWrites`** — generates the bang variants by delegating to their non-bang counterparts, so the two cannot drift: no duplicated request building, validation or logging. Generated as a module, so a hand-written `place!` can still override and call `super`.
32
+
33
+ ### Fixed
34
+
35
+ - **`DhanHQ::MCP` was unreachable on a bare `require "dhan_hq"`.** Same failure as the `DhanHQ::AI` fix earlier in this release: `mcp.rb` defines `DhanHQ::MCP` while Zeitwerk's inflector expected `DhanHQ::Mcp` from the filename, so `DhanHQ::MCP::Server` raised `NameError` unless something had already loaded the file by hand — which only `exe/dhanhq-mcp` and `lib/dhan_hq/mcp.rb` did. Found the same way the `AI` bug was: cross-referencing every `DhanHQ::` constant named in the docs against what actually resolves. Guarded by a new `spec/dhan_hq/zeitwerk_autoload_spec.rb`, which shells out to a subprocess so the check can't be satisfied by another spec having already loaded the file by hand.
36
+ - **`lib/DhanHQ/configuration.rb`'s doc comment referenced `DhanHQ::Models::Order.find_by_correlation_id`, which does not exist** — the real method is `.find_by_correlation` (no `_id` suffix). Caught during the same audit.
37
+ - A documentation pass across the gem ahead of this release, cross-checking every code example and referenced class/method against the actual codebase:
38
+ - `docs/CONFIGURATION.md`'s resource table named `DhanHQ::Models::Fund` (real class: `Funds`) and `DhanHQ::Models::Ledger` (real class: `LedgerEntry`), and listed `fund_limit`/`margin_calculator` as `Funds` methods — neither exists; the real methods are `Funds.fetch`/`.balance` and `Margin.calculate`/`.calculate_multi`. The table now also covers the resources 3.2.0/3.3.0 added (Iceberg/TWAP/Alert orders, Multi Order, P&L Exit, eDIS, Global Stocks) and lists the `!` write variants.
39
+ - `docs/CONFIGURATION.md` was missing `LIVE_TRADING`, `DHAN_DRY_RUN`, `DHAN_RETRY_WRITES`, `DHAN_AUTO_CORRELATION_ID`, `DHAN_WARN_AMBIGUOUS_WRITE_FAILURE` and `DHAN_MARKET_DEPTH_LEVEL` entirely, and documented `DHAN_LOG_LEVEL` as if the library read it automatically — it doesn't; only the existing `## Logging` snippet wires it up.
40
+ - `skills/dhanhq-ruby/references/portfolio.md`'s eDIS section had the class name miscased (`EDIS` vs. `Edis`), called a nonexistent `.open_browser_for_tpin`, called `.inquiry` instead of `.inquire`, and accessed the (Hash) result via method calls instead of keys.
41
+ - `README.md` called `DhanHQ::Models::Fund.balance` — same class-name typo as above.
42
+ - `docs/RELEASE_GUIDE.md` claimed `Required Ruby: >= 3.1.0`; the gemspec has required `>= 3.2.0` since 3.0.0.
43
+ - `GUIDE.md` had no mention of the bang write variants added in this release; added a short section pointing to the README's fuller treatment.
44
+
45
+ ### Changed
46
+
47
+ - `BaseModel#save!`'s exception message is now `"<Class>#save failed: <details>"` rather than `"Failed to save the record: <details>"`. The exception class is unchanged (`DhanHQ::Error`), and nothing in the gem, specs or docs asserted the old text.
48
+
1
49
  ## [3.2.1] - 2026-07-26
2
50
 
3
51
  ### Fixed
data/GUIDE.md CHANGED
@@ -186,6 +186,22 @@ DhanHQ::Contracts::ModifyOrderContract.new.call(params).success?
186
186
  DhanHQ::Models::Order.resource.update("123", params)
187
187
  ```
188
188
 
189
+ ### Explicit Failures: Bang Variants
190
+
191
+ `place`, `#modify`, `#cancel` and their equivalents across every write model do not agree on
192
+ how they report failure — depending on the class, a rejected write comes back as `nil`,
193
+ `false`, or a `DhanHQ::ErrorObject`. Each has a `!` variant that raises `DhanHQ::OrderError`
194
+ instead, so one `rescue` handles every failure the same way:
195
+
196
+ ```ruby
197
+ order = DhanHQ::Models::Order.place!(params) # raises DhanHQ::OrderError on failure
198
+ order.cancel! # raises if the exchange did not cancel
199
+ ```
200
+
201
+ The non-bang methods are unchanged — adopting the bang variant is opt-in, per call site.
202
+ See the [README](README.md#explicit-failures-bang-variants) for the full list of `!` methods
203
+ and the deprecation notice that flags remaining non-bang call sites ahead of 4.0.0.
204
+
189
205
  ### Slicing Orders
190
206
 
191
207
  Use the same fields as placement, but the contract allows additional validity options (`GTC`, `GTD`). The model helper accepts snake_case parameters and handles camelCase conversion as part of validation:
data/README.md CHANGED
@@ -314,6 +314,59 @@ risk:
314
314
  DhanHQ.configure { |config| config.retry_non_idempotent_writes = true }
315
315
  ```
316
316
 
317
+ ### Explicit Failures: Bang Variants
318
+
319
+ Every write method has a `!` variant that raises instead of returning a falsy value:
320
+
321
+ ```ruby
322
+ order = DhanHQ::Models::Order.place!(params) # raises DhanHQ::OrderError on failure
323
+ order.cancel! # raises if the exchange did not cancel
324
+ ```
325
+
326
+ This exists because the non-bang methods do not agree on how to report failure — some
327
+ return `nil`, some `false`, some a `DhanHQ::ErrorObject` — so a caller cannot write one
328
+ error branch:
329
+
330
+ ```ruby
331
+ # Before: which falsy thing came back depends on which method and which failure
332
+ order = DhanHQ::Models::Order.place(params)
333
+ return unless order # nil on rejection
334
+
335
+ result = DhanHQ::Models::MultiOrder.place(legs)
336
+ return if result.is_a?(DhanHQ::ErrorObject) # truthy! `unless result` would not catch this
337
+ ```
338
+
339
+ `DhanHQ::OrderError` descends from `DhanHQ::Error`, so an existing `rescue DhanHQ::Error`
340
+ keeps working. The non-bang methods are unchanged, so adopting this is per call site:
341
+
342
+ ```ruby
343
+ begin
344
+ DhanHQ::Models::Order.place!(params)
345
+ rescue DhanHQ::OrderError => e
346
+ logger.error(e.message) # carries the API's diagnostics
347
+ end
348
+ ```
349
+
350
+ When a non-bang write reports failure, the SDK logs once per call site to help you find
351
+ what needs migrating before 4.0.0 changes the return value:
352
+
353
+ ```
354
+ [DhanHQ] DEPRECATION: DhanHQ::Models::Order.place reported failure as nil. ...
355
+ Use DhanHQ::Models::Order.place! to get a DhanHQ::OrderError instead, or set
356
+ config.warn_on_ambiguous_write_failure = false to silence this.
357
+ ```
358
+
359
+ It fires once per call site per process, never alters a return value, never raises, and
360
+ goes quiet once you switch that site to the bang variant. To silence it entirely:
361
+
362
+ ```ruby
363
+ DhanHQ.configure { |c| c.warn_on_ambiguous_write_failure = false } # or DHAN_WARN_AMBIGUOUS_WRITE_FAILURE=false
364
+ ```
365
+
366
+ Available on `Order`, `SuperOrder`, `ForeverOrder`, `IcebergOrder`, `TwapOrder`,
367
+ `AlertOrder`, `PnlExit`, `MultiOrder` and `GlobalStocks::Order`. The non-bang methods are
368
+ planned to converge on `ErrorObject` in 4.0.0 — see the CHANGELOG for the staged plan.
369
+
317
370
  ### Order Audit Logging
318
371
 
319
372
  Every order attempt (place, modify, slice) automatically logs a structured JSON line at WARN level:
@@ -374,7 +427,7 @@ order.cancel
374
427
  ```ruby
375
428
  DhanHQ::Models::Position.all
376
429
  DhanHQ::Models::Holding.all
377
- DhanHQ::Models::Fund.balance
430
+ DhanHQ::Models::Funds.balance
378
431
  ```
379
432
 
380
433
  ### Historical Data
@@ -24,7 +24,6 @@ Set these _before_ calling `configure_with_env` to override defaults:
24
24
 
25
25
  | Variable | Default | Description |
26
26
  | -------------------------------- | ---------- | ------------------------------------------------------- |
27
- | `DHAN_LOG_LEVEL` | `INFO` | Logger verbosity (`DEBUG`, `INFO`, `WARN`, `ERROR`) |
28
27
  | `DHAN_SANDBOX` | `false` | Set `"true"` to route REST calls to `https://sandbox.dhan.co/v2` instead of production. Note: Dhan's sandbox validates request/response plumbing only — it does not execute real order fills/matching. Placed orders stay `PENDING` indefinitely. See [ENDPOINTS_AND_SANDBOX.md](ENDPOINTS_AND_SANDBOX.md). |
29
28
  | `DHAN_BASE_URL` | Dhan prod | Point REST calls to a different API hostname. Takes precedence over `DHAN_SANDBOX` only when explicitly set to something other than the production default. |
30
29
  | `DHAN_WS_VERSION` | latest | Pin WebSocket connections to a specific API version |
@@ -32,12 +31,27 @@ Set these _before_ calling `configure_with_env` to override defaults:
32
31
  | `DHAN_WS_USER_TYPE` | `SELF` | Switch between `SELF` and `PARTNER` streaming modes |
33
32
  | `DHAN_PARTNER_ID` | — | Required when `DHAN_WS_USER_TYPE=PARTNER` |
34
33
  | `DHAN_PARTNER_SECRET` | — | Required when `DHAN_WS_USER_TYPE=PARTNER` |
34
+ | `DHAN_MARKET_DEPTH_LEVEL` | `20` | WebSocket market depth levels to subscribe to |
35
35
  | `DHAN_CONNECT_TIMEOUT` | `10` | Connection timeout in seconds |
36
36
  | `DHAN_READ_TIMEOUT` | `30` | Read timeout in seconds |
37
37
  | `DHAN_WRITE_TIMEOUT` | `30` | Write timeout in seconds |
38
38
  | `DHAN_WS_MAX_TRACKED_ORDERS` | `10000` | Maximum orders to track in WebSocket |
39
39
  | `DHAN_WS_MAX_ORDER_AGE` | `604800` | Maximum order age in seconds before cleanup (7 days) |
40
40
 
41
+ `DHAN_LOG_LEVEL` is **not** read automatically — see [Logging](#logging) below for the one-line snippet that wires it up.
42
+
43
+ ## Behavior Flags
44
+
45
+ These change what a write request *does*, not just where it goes. All default to the safe/conservative behavior and are off unless set.
46
+
47
+ | Variable | Config attribute | Default | Effect |
48
+ | ------------------------------------- | ------------------------------------ | ------- | ------ |
49
+ | `LIVE_TRADING=true` | — | unset | **Required to place, modify, or cancel any real order, position exit, kill switch, EDIS, or P&L exit.** Without it, every write-path resource raises `DhanHQ::LiveTradingDisabledError` before making the request. This is the primary safety gate — see [ARCHITECTURE.md](../ARCHITECTURE.md) and the risk pipeline. |
50
+ | `DHAN_DRY_RUN=true` | `config.dry_run` | `false` | Suppresses every state-changing request, logs the payload as `DHAN_DRY_RUN`, and answers order placements with a simulated `DRYRUN-…` id so caller code paths still run to completion. Reads still hit the API. |
51
+ | `DHAN_RETRY_WRITES=true` | `config.retry_non_idempotent_writes` | `false` | Auto-retries a non-idempotent write (order placement, modify, cancel) after a transient failure (429, 5xx, timeout). Off by default because the API has no idempotency key — a timed-out POST may have already reached the exchange, and retrying it can place a duplicate order. |
52
+ | `DHAN_AUTO_CORRELATION_ID=true` | `config.auto_correlation_id` | `false` | Fills in a `correlationId` (`dhq-<hex>`) on order placements that lack one, so a timed-out placement can be reconciled via `GET /v2/orders/external/{correlation-id}`. Off by default because it changes the request body; an explicit correlation id is always preserved. |
53
+ | `DHAN_WARN_AMBIGUOUS_WRITE_FAILURE=false` | `config.warn_on_ambiguous_write_failure` | `true` (on) | Logs a once-per-call-site deprecation notice when a non-bang write method (`Order.place`, `#modify`, …) reports failure as `nil`, `false`, or a `DhanHQ::ErrorObject` — these disagree today and unify on `ErrorObject` in 4.0.0. Use the `!` variant (`place!`, `#modify!`) to get a raised `DhanHQ::OrderError` instead of the ambiguous return value. |
54
+
41
55
  ## `.env` File Setup
42
56
 
43
57
  Create a `.env` file in your project root:
@@ -95,16 +109,26 @@ For detailed authentication flows, see [AUTHENTICATION.md](AUTHENTICATION.md).
95
109
 
96
110
  ## Available Resources
97
111
 
98
- | Resource | Model | Actions |
99
- | ------------------------ | -------------------------------------- | --------------------------------------------------- |
100
- | Orders | `DhanHQ::Models::Order` | `find`, `all`, `where`, `place`, `update`, `cancel` |
101
- | Trades | `DhanHQ::Models::Trade` | `all`, `find_by_order_id` |
102
- | Forever Orders | `DhanHQ::Models::ForeverOrder` | `create`, `find`, `modify`, `cancel`, `all` |
103
- | Holdings | `DhanHQ::Models::Holding` | `all` |
104
- | Positions | `DhanHQ::Models::Position` | `all`, `find`, `exit!` |
105
- | Funds & Margin | `DhanHQ::Models::Fund` | `fund_limit`, `margin_calculator` |
106
- | Ledger | `DhanHQ::Models::Ledger` | `all` |
107
- | Market Feeds | `DhanHQ::Models::MarketFeed` | `ltp`, `ohlc`, `quote` |
108
- | Historical Data (Charts) | `DhanHQ::Models::HistoricalData` | `daily`, `intraday` |
109
- | Option Chain | `DhanHQ::Models::OptionChain` | `fetch`, `fetch_expiry_list` |
110
- | Super Orders | `DhanHQ::Models::SuperOrder` | `create`, `modify`, `cancel`, `all` |
112
+ | Resource | Model | Actions |
113
+ | ------------------------ | ---------------------------------------- | ------------------------------------------------------------------------------ |
114
+ | Orders | `DhanHQ::Models::Order` | `place`, `find`, `all`, `where`, `#modify`, `#cancel`, `#destroy` (`!` variants raise) |
115
+ | Trades | `DhanHQ::Models::Trade` | `all`, `find_by_order_id` |
116
+ | Forever Orders | `DhanHQ::Models::ForeverOrder` | `create`, `find`, `all`, `#modify`, `#cancel` |
117
+ | Iceberg Orders | `DhanHQ::Models::IcebergOrder` | `create`, `find`, `all`, `#modify`, `#cancel` |
118
+ | TWAP Orders | `DhanHQ::Models::TwapOrder` | `create`, `find`, `all`, `#modify`, `#cancel` |
119
+ | Alert Orders | `DhanHQ::Models::AlertOrder` | `create`, `find`, `all`, `modify(alert_id, params)`, `#destroy` |
120
+ | Super Orders | `DhanHQ::Models::SuperOrder` | `create`, `all`, `#modify`, `#cancel(leg_name)` |
121
+ | Multi Order (basket) | `DhanHQ::Models::MultiOrder` | `place(orders, dhan_client_id:)` — up to 15 legs |
122
+ | P&L Exit | `DhanHQ::Models::PnlExit` | `configure`, `stop`, `status` |
123
+ | Holdings | `DhanHQ::Models::Holding` | `all` |
124
+ | Positions | `DhanHQ::Models::Position` | `all`, `active`, `#convert(params)`, `.exit_all!` |
125
+ | Funds | `DhanHQ::Models::Funds` | `fetch`, `balance` |
126
+ | Margin Calculator | `DhanHQ::Models::Margin` | `calculate(params)`, `calculate_multi(params)` |
127
+ | Ledger | `DhanHQ::Models::LedgerEntry` | `all(from_date:, to_date:)` |
128
+ | eDIS | `DhanHQ::Models::Edis` | `generate_tpin`, `generate_form`, `generate_bulk_form`, `inquire(isin:)` |
129
+ | Market Feeds | `DhanHQ::Models::MarketFeed` | `ltp`, `ohlc`, `quote` |
130
+ | Historical Data (Charts) | `DhanHQ::Models::HistoricalData` | `daily`, `intraday` |
131
+ | Option Chain | `DhanHQ::Models::OptionChain` | `fetch`, `fetch_expiry_list` |
132
+ | Global Stocks Orders | `DhanHQ::Models::GlobalStocks::Order` | `place`, `find`, `all`, `#cancel` — see [ARCHITECTURE.md](../ARCHITECTURE.md) |
133
+
134
+ `#method` denotes an instance method (called on a fetched or created record); everything else is a class method. Every write method above also has a `!` variant (`place!`, `#modify!`, `#cancel!`, …) that raises `DhanHQ::OrderError` instead of returning a falsy value or an `ErrorObject` — see the [CHANGELOG](../CHANGELOG.md) for the write-return-contract migration this is part of.
@@ -434,7 +434,7 @@ git push origin main v2.1.12
434
434
 
435
435
  - **Gem Name:** DhanHQ
436
436
  - **Current Version:** Check `lib/DhanHQ/version.rb`
437
- - **Required Ruby:** >= 3.1.0
437
+ - **Required Ruby:** >= 3.2.0
438
438
  - **License:** MIT
439
439
  - **Homepage:** https://github.com/shubhamtaywade82/dhanhq-client
440
440
 
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../write_result"
4
+
5
+ module DhanHQ
6
+ module Concerns
7
+ # Generates bang variants of write methods that raise instead of returning a falsy
8
+ # value or an {DhanHQ::ErrorObject}.
9
+ #
10
+ # Each generated method calls its non-bang counterpart and passes the result through
11
+ # {DhanHQ::WriteResult.unwrap!}, so the two cannot drift: there is no duplicated
12
+ # request building, validation or logging. The non-bang methods are untouched, which
13
+ # is what makes this additive — existing callers keep the return values they were
14
+ # written against.
15
+ #
16
+ # @example
17
+ # class Order < BaseModel
18
+ # extend DhanHQ::Concerns::BangWrites
19
+ #
20
+ # bang_class_writes :place # defines Order.place!
21
+ # bang_writes :modify, :cancel # defines #modify! and #cancel!
22
+ # end
23
+ #
24
+ # Order.place!(params) # => Order, or raises DhanHQ::OrderError
25
+ # order.cancel! # => true, or raises DhanHQ::OrderError
26
+ module BangWrites
27
+ # Defines `<name>!` instance methods.
28
+ #
29
+ # @param names [Array<Symbol>] Existing instance write methods to wrap.
30
+ # @param error_class [Class] Exception the generated methods raise.
31
+ # @return [void]
32
+ def bang_writes(*names, error_class: DhanHQ::OrderError)
33
+ names.each { |name| include bang_write_module(name, error_class) }
34
+ end
35
+
36
+ # Defines `<name>!` class methods.
37
+ #
38
+ # @param names [Array<Symbol>] Existing class write methods to wrap.
39
+ # @param error_class [Class] Exception the generated methods raise.
40
+ # @return [void]
41
+ def bang_class_writes(*names, error_class: DhanHQ::OrderError)
42
+ names.each { |name| singleton_class.include bang_write_module(name, error_class) }
43
+ end
44
+
45
+ private
46
+
47
+ # Built as a module rather than defined directly, so a hand-written `place!` in
48
+ # the class body still overrides the generated one and can call `super`.
49
+ def bang_write_module(name, error_class)
50
+ Module.new do
51
+ define_method(:"#{name}!") do |*args, **kwargs, &block|
52
+ # A caller already using the bang variant does not need a deprecation
53
+ # notice telling them to use the bang variant.
54
+ result = DhanHQ::WriteResult.suppressing_deprecation do
55
+ public_send(name, *args, **kwargs, &block)
56
+ end
57
+
58
+ DhanHQ::WriteResult.unwrap!(
59
+ result,
60
+ operation: DhanHQ::WriteResult.operation_label(self, name),
61
+ error_class: error_class,
62
+ errors: DhanHQ::WriteResult.errors_from(self)
63
+ )
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../write_result"
4
+
5
+ module DhanHQ
6
+ module Concerns
7
+ # Observes non-bang write methods and reports, once per call site, when one returns
8
+ # a failure shape that 4.0.0 will change.
9
+ #
10
+ # Uses +prepend+ rather than +include+ because these methods are defined directly on
11
+ # the model classes: an included module sits behind the class in the ancestor chain
12
+ # and would never be reached. A prepended one sits in front, so +super+ runs the real
13
+ # method and the result passes through untouched.
14
+ #
15
+ # This layer only observes. It never changes a return value, never raises, and goes
16
+ # quiet once a call site has been migrated to the bang variant.
17
+ #
18
+ # @example
19
+ # class Order < BaseModel
20
+ # extend DhanHQ::Concerns::TrackedWrites
21
+ #
22
+ # track_class_writes :place # warns when Order.place returns nil
23
+ # track_writes :modify, :cancel # warns when #cancel returns false
24
+ # end
25
+ module TrackedWrites
26
+ # Observes `<name>` instance methods.
27
+ #
28
+ # @param names [Array<Symbol>] Existing instance write methods to observe.
29
+ # @return [void]
30
+ def track_writes(*names)
31
+ names.each { |name| prepend tracking_module(name) }
32
+ end
33
+
34
+ # Observes `<name>` class methods.
35
+ #
36
+ # @param names [Array<Symbol>] Existing class write methods to observe.
37
+ # @return [void]
38
+ def track_class_writes(*names)
39
+ names.each { |name| singleton_class.prepend tracking_module(name) }
40
+ end
41
+
42
+ private
43
+
44
+ def tracking_module(name)
45
+ Module.new do
46
+ define_method(name) do |*args, **kwargs, &block|
47
+ DhanHQ::WriteResult.report_ambiguous_failure(
48
+ super(*args, **kwargs, &block),
49
+ operation: DhanHQ::WriteResult.operation_label(self, name)
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -60,7 +60,7 @@ module DhanHQ
60
60
  # /v2/orders that times out may well have reached the exchange, and retrying it
61
61
  # can place a second, duplicate order. With this off, the transient error is
62
62
  # raised to the caller, who can reconcile using the correlation id (see
63
- # {#auto_correlation_id} and +DhanHQ::Models::Order.find_by_correlation_id+)
63
+ # {#auto_correlation_id} and +DhanHQ::Models::Order.find_by_correlation+)
64
64
  # before deciding to resubmit.
65
65
  #
66
66
  # Read requests are always retried regardless of this setting.
@@ -69,6 +69,19 @@ module DhanHQ
69
69
  # @return [Boolean]
70
70
  attr_accessor :retry_non_idempotent_writes
71
71
 
72
+ # Whether to log a deprecation notice, once per call site, when a non-bang write
73
+ # method reports failure through +nil+, +false+ or a {DhanHQ::ErrorObject}.
74
+ #
75
+ # Those contracts disagree today and will unify on {DhanHQ::ErrorObject} in 4.0.0,
76
+ # which is truthy — so an `if result` failure branch written against +nil+ or
77
+ # +false+ will silently invert. The notice names the call sites that need moving to
78
+ # a bang variant before then.
79
+ #
80
+ # Defaults to +true+: a notice nobody sees finds nothing. Set to +false+ once the
81
+ # call sites are migrated, or via +DHAN_WARN_AMBIGUOUS_WRITE_FAILURE=false+.
82
+ # @return [Boolean]
83
+ attr_accessor :warn_on_ambiguous_write_failure
84
+
72
85
  # Whether to generate a +correlationId+ for order placements that do not carry
73
86
  # one. The correlation id is the only way to answer "did my order actually go
74
87
  # through?" after a timeout, via GET /v2/orders/external/{correlation-id}.
@@ -175,6 +188,11 @@ module DhanHQ
175
188
  @retry_non_idempotent_writes == true
176
189
  end
177
190
 
191
+ # @return [Boolean] True when ambiguous write failures should be reported.
192
+ def warn_on_ambiguous_write_failure?
193
+ @warn_on_ambiguous_write_failure == true
194
+ end
195
+
178
196
  # @return [Boolean] True when a correlation id should be generated for orders.
179
197
  def auto_correlation_id?
180
198
  @auto_correlation_id == true
@@ -193,6 +211,7 @@ module DhanHQ
193
211
  @dry_run = env_flag("DHAN_DRY_RUN", default: false)
194
212
  @retry_non_idempotent_writes = env_flag("DHAN_RETRY_WRITES", default: false)
195
213
  @auto_correlation_id = env_flag("DHAN_AUTO_CORRELATION_ID", default: false)
214
+ @warn_on_ambiguous_write_failure = env_flag("DHAN_WARN_AMBIGUOUS_WRITE_FAILURE", default: true)
196
215
  @base_url = ENV.fetch("DHAN_BASE_URL", nil)
197
216
  @ws_version = ENV.fetch("DHAN_WS_VERSION", 2).to_i
198
217
  @ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL", nil)
@@ -193,19 +193,12 @@ module DhanHQ
193
193
  # @return [DhanHQ::BaseModel]
194
194
  # @raise [DhanHQ::Error] When the record cannot be saved.
195
195
  def save!
196
- result = save
197
- return result unless result == false || result.nil? || result.is_a?(DhanHQ::ErrorObject)
198
-
199
- error_details =
200
- if result.is_a?(DhanHQ::ErrorObject)
201
- result.errors
202
- elsif @errors && !@errors.empty?
203
- @errors
204
- else
205
- "Unknown error"
206
- end
207
-
208
- raise DhanHQ::Error, "Failed to save the record: #{error_details}"
196
+ DhanHQ::WriteResult.unwrap!(
197
+ save,
198
+ operation: "#{self.class}#save",
199
+ error_class: DhanHQ::Error,
200
+ errors: @errors
201
+ )
209
202
  end
210
203
 
211
204
  # Delete the resource
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Emits a deprecation notice once per call site per process.
5
+ #
6
+ # A trading process can reject hundreds of orders in a session. A notice that fires
7
+ # on every one of them is noise that gets filtered out, which defeats the purpose —
8
+ # the point is for a maintainer to find the handful of call sites still relying on
9
+ # behaviour that is going to change, then fix them.
10
+ module Deprecation
11
+ @warned = {}
12
+ @mutex = Mutex.new
13
+
14
+ class << self
15
+ # Logs +message+ the first time this +key+ is seen in this process.
16
+ #
17
+ # A command, not a query: callers that need to know whether a notice was emitted
18
+ # should consult {.warned_keys}.
19
+ #
20
+ # @param key [String, Symbol] Identifies the call site, e.g. the operation name.
21
+ # @param message [String] What is deprecated and what to do instead.
22
+ # @return [void]
23
+ def warn_once(key, message)
24
+ return unless first_warning_for?(key)
25
+
26
+ DhanHQ.logger&.warn("[DhanHQ] DEPRECATION: #{message}")
27
+ nil
28
+ end
29
+
30
+ # Keys already warned about, for assertions and diagnostics.
31
+ #
32
+ # @return [Array<String>]
33
+ def warned_keys
34
+ @mutex.synchronize { @warned.keys }
35
+ end
36
+
37
+ # Forgets every emitted notice, so the next occurrence warns again.
38
+ # Intended for test isolation.
39
+ #
40
+ # @return [void]
41
+ def reset!
42
+ @mutex.synchronize { @warned.clear }
43
+ end
44
+
45
+ private
46
+
47
+ # Whether this is the first time +key+ has been seen, recording it either way.
48
+ #
49
+ # The check and the record happen under one lock so two threads hitting the same
50
+ # call site at once cannot both decide they are first.
51
+ #
52
+ # @return [Boolean]
53
+ def first_warning_for?(key)
54
+ @mutex.synchronize do
55
+ next false if @warned.key?(key.to_s)
56
+
57
+ @warned[key.to_s] = true
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -6,6 +6,13 @@ module DhanHQ
6
6
  module Models
7
7
  # Model for alert/conditional orders. CRUD via AlertOrders resource; validated by AlertOrderContract.
8
8
  class AlertOrder < BaseModel
9
+ extend DhanHQ::Concerns::BangWrites
10
+ extend DhanHQ::Concerns::TrackedWrites
11
+
12
+ track_class_writes :create, :modify
13
+
14
+ bang_class_writes :create, :modify
15
+
9
16
  include Concerns::ApiResponseHandler
10
17
 
11
18
  HTTP_PATH = "/v2/alerts/orders"
@@ -66,6 +66,15 @@ module DhanHQ
66
66
  # orders.each { |order| puts order.order_status }
67
67
  #
68
68
  class ForeverOrder < BaseModel
69
+ extend DhanHQ::Concerns::BangWrites
70
+ extend DhanHQ::Concerns::TrackedWrites
71
+
72
+ track_class_writes :create
73
+ track_writes :modify, :cancel
74
+
75
+ bang_class_writes :create
76
+ bang_writes :modify, :cancel
77
+
69
78
  include Concerns::ApiResponseHandler
70
79
 
71
80
  attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
@@ -36,6 +36,15 @@ module DhanHQ
36
36
  # end
37
37
  #
38
38
  class Order < BaseModel
39
+ extend DhanHQ::Concerns::BangWrites
40
+ extend DhanHQ::Concerns::TrackedWrites
41
+
42
+ track_class_writes :place
43
+ track_writes :modify, :cancel
44
+
45
+ bang_class_writes :place
46
+ bang_writes :modify, :cancel
47
+
39
48
  HTTP_PATH = "/v2/globalstocks/orders"
40
49
 
41
50
  attributes :dhan_client_id, :order_id, :exchange_order_id, :correlation_id,
@@ -48,6 +48,15 @@ module DhanHQ
48
48
  # orders = DhanHQ::Models::IcebergOrder.all
49
49
  #
50
50
  class IcebergOrder < BaseModel
51
+ extend DhanHQ::Concerns::BangWrites
52
+ extend DhanHQ::Concerns::TrackedWrites
53
+
54
+ track_class_writes :create
55
+ track_writes :modify, :cancel
56
+
57
+ bang_class_writes :create
58
+ bang_writes :modify, :cancel
59
+
51
60
  include Concerns::ApiResponseHandler
52
61
 
53
62
  attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
@@ -26,6 +26,13 @@ module DhanHQ
26
26
  # result.for_sequence("1").order_id
27
27
  #
28
28
  class MultiOrder < BaseModel
29
+ extend DhanHQ::Concerns::BangWrites
30
+ extend DhanHQ::Concerns::TrackedWrites
31
+
32
+ track_class_writes :place
33
+
34
+ bang_class_writes :place
35
+
29
36
  HTTP_PATH = "/v2/alerts/multi/orders"
30
37
 
31
38
  attributes :orders
@@ -46,6 +46,15 @@ module DhanHQ
46
46
  # puts "Pending orders: #{pending_orders.count}"
47
47
  #
48
48
  class Order < BaseModel
49
+ extend DhanHQ::Concerns::BangWrites
50
+ extend DhanHQ::Concerns::TrackedWrites
51
+
52
+ track_class_writes :place, :create
53
+ track_writes :modify, :cancel
54
+
55
+ bang_class_writes :place, :create
56
+ bang_writes :modify, :cancel, :refresh
57
+
49
58
  include Concerns::ApiResponseHandler
50
59
 
51
60
  # Attributes eligible for modification requests.
@@ -430,6 +439,25 @@ module DhanHQ
430
439
  order.save # calls resource create or update
431
440
  order
432
441
  end
442
+
443
+ # `create` always returns the built `Order`, even when `#save` returned
444
+ # `false` -- existing callers rely on that to inspect an unsaved order's
445
+ # `errors`, so `create` cannot change. The generated `create!` would
446
+ # therefore call `unwrap!` on an object that is truthy regardless of
447
+ # whether the placement actually succeeded, and never raise. This
448
+ # hand-written override supersedes it (see {BangWrites} for why a
449
+ # class-body definition takes precedence) and unwraps on `persisted?`
450
+ # instead, which reflects whether the API actually accepted the order.
451
+ def create!(params)
452
+ order = DhanHQ::WriteResult.suppressing_deprecation { create(params) }
453
+
454
+ DhanHQ::WriteResult.unwrap!(
455
+ order.persisted? ? order : false,
456
+ operation: DhanHQ::WriteResult.operation_label(self, :create),
457
+ error_class: DhanHQ::OrderError,
458
+ errors: DhanHQ::WriteResult.errors_from(order)
459
+ )
460
+ end
433
461
  end
434
462
 
435
463
  ##
@@ -32,6 +32,13 @@ module DhanHQ
32
32
  # puts response[:pnl_exit_status] # => "DISABLED"
33
33
  #
34
34
  class PnlExit < BaseModel
35
+ extend DhanHQ::Concerns::BangWrites
36
+ extend DhanHQ::Concerns::TrackedWrites
37
+
38
+ track_class_writes :configure, :stop
39
+
40
+ bang_class_writes :configure, :stop
41
+
35
42
  HTTP_PATH = "/v2/pnlExit"
36
43
 
37
44
  attributes :pnl_exit_status, :profit, :loss, :segments, :enable_kill_switch
@@ -48,6 +48,15 @@ module DhanHQ
48
48
  # order.cancel("STOP_LOSS_LEG")
49
49
  #
50
50
  class SuperOrder < BaseModel
51
+ extend DhanHQ::Concerns::BangWrites
52
+ extend DhanHQ::Concerns::TrackedWrites
53
+
54
+ track_class_writes :create
55
+ track_writes :modify, :cancel
56
+
57
+ bang_class_writes :create
58
+ bang_writes :modify, :cancel
59
+
51
60
  include Concerns::ApiResponseHandler
52
61
 
53
62
  attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
@@ -45,6 +45,15 @@ module DhanHQ
45
45
  # order.cancel
46
46
  #
47
47
  class TwapOrder < BaseModel
48
+ extend DhanHQ::Concerns::BangWrites
49
+ extend DhanHQ::Concerns::TrackedWrites
50
+
51
+ track_class_writes :create
52
+ track_writes :modify, :cancel
53
+
54
+ bang_class_writes :create
55
+ bang_writes :modify, :cancel
56
+
48
57
  include Concerns::ApiResponseHandler
49
58
 
50
59
  attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "3.2.1"
5
+ VERSION = "3.3.0"
6
6
  end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Interprets the several shapes a write currently uses to signal failure.
5
+ #
6
+ # The model write methods do not share a return contract. Depending on which class
7
+ # and which failure you hit, a rejected write comes back as +nil+, as +false+, or as
8
+ # a {DhanHQ::ErrorObject} — and {DhanHQ::Models::AlertOrder.modify} can return either
9
+ # of the first two from the same method. A caller cannot write one error branch.
10
+ #
11
+ # Unifying those contracts is a breaking change for the applications that depend on
12
+ # this gem, so it is staged (see CHANGELOG). This module is step one: it puts the
13
+ # knowledge of "what does failure look like" in exactly one place, and backs the bang
14
+ # variants (+place!+, +modify!+, +cancel!+, …) that give callers a single, explicit
15
+ # failure mode to rescue today.
16
+ #
17
+ # @example Opting a call site into explicit failures
18
+ # order = DhanHQ::Models::Order.place!(params) # raises DhanHQ::OrderError
19
+ # # instead of
20
+ # order = DhanHQ::Models::Order.place(params) # nil on failure
21
+ module WriteResult
22
+ # Thread-local flag set while a bang variant is calling through.
23
+ SUPPRESSION_KEY = :dhanhq_suppress_write_deprecation
24
+
25
+ module_function
26
+
27
+ # Whether a write result represents a rejected or failed operation.
28
+ #
29
+ # @param result [Object] Return value of a write method.
30
+ # @return [Boolean]
31
+ def failure?(result)
32
+ result.nil? || result == false || result.is_a?(DhanHQ::ErrorObject)
33
+ end
34
+
35
+ # @param result [Object] Return value of a write method.
36
+ # @return [Boolean]
37
+ def success?(result)
38
+ !failure?(result)
39
+ end
40
+
41
+ # Returns the result, or raises carrying whatever diagnostics the failure held.
42
+ #
43
+ # @param result [Object] Return value of a write method.
44
+ # @param operation [String] Human-readable operation name for the message,
45
+ # e.g. +"DhanHQ::Models::Order.place"+.
46
+ # @param error_class [Class] Exception to raise. Defaults to {DhanHQ::OrderError},
47
+ # which descends from {DhanHQ::Error}, so existing `rescue DhanHQ::Error`
48
+ # handlers still catch it.
49
+ # @param errors [Hash, nil] Validation errors to report when the result itself
50
+ # carries none — typically a model's +errors+ hash.
51
+ # @return [Object] The result, when it represents success.
52
+ # @raise [DhanHQ::Error] When the result represents failure.
53
+ def unwrap!(result, operation:, error_class: DhanHQ::OrderError, errors: nil)
54
+ return result if success?(result)
55
+
56
+ raise error_class, "#{operation} failed: #{describe(result, errors)}"
57
+ end
58
+
59
+ # Reports, once per call site, that a non-bang write signalled failure through a
60
+ # value whose shape is going to change in 4.0.0.
61
+ #
62
+ # Step two of the migration: the notice tells a maintainer which of their call
63
+ # sites still branch on the old return value, so they can move to the bang variant
64
+ # before the non-bang contract unifies on {DhanHQ::ErrorObject}. Returns the result
65
+ # untouched — this observes, it never alters behaviour.
66
+ #
67
+ # Silent when the write succeeded, when the caller opted out via
68
+ # +config.warn_on_ambiguous_write_failure = false+, or when reached through a bang
69
+ # variant (those callers have already migrated — see {.suppressing_deprecation}).
70
+ #
71
+ # @param result [Object] Return value of a non-bang write method.
72
+ # @param operation [String] Operation label from {.operation_label}.
73
+ # @return [Object] The result, unchanged.
74
+ def report_ambiguous_failure(result, operation:)
75
+ return result unless failure?(result)
76
+ return result if suppressed?
77
+ return result unless DhanHQ.configuration&.warn_on_ambiguous_write_failure?
78
+
79
+ DhanHQ::Deprecation.warn_once(
80
+ operation,
81
+ "#{operation} reported failure as #{shape_of(result)}. Write methods return " \
82
+ "nil, false or a DhanHQ::ErrorObject inconsistently today and will all return " \
83
+ "DhanHQ::ErrorObject in 4.0.0, which is truthy — an `if result` failure branch " \
84
+ "will invert. Use #{operation}! to get a DhanHQ::OrderError instead, or set " \
85
+ "config.warn_on_ambiguous_write_failure = false to silence this."
86
+ )
87
+
88
+ result
89
+ end
90
+
91
+ # Runs the block with {.report_ambiguous_failure} disabled on this thread.
92
+ #
93
+ # Used by the bang variants: they call the non-bang method to get its result, and a
94
+ # caller who has already moved to `place!` does not need telling to move to
95
+ # `place!`. Thread-local so a concurrent thread still gets its own notices.
96
+ #
97
+ # @return [Object] The block's value.
98
+ def suppressing_deprecation
99
+ previous = Thread.current[SUPPRESSION_KEY]
100
+ Thread.current[SUPPRESSION_KEY] = true
101
+ yield
102
+ ensure
103
+ Thread.current[SUPPRESSION_KEY] = previous
104
+ end
105
+
106
+ # @return [Boolean]
107
+ def suppressed?
108
+ Thread.current[SUPPRESSION_KEY] == true
109
+ end
110
+
111
+ # Names the shape a failure arrived in, for the notice.
112
+ #
113
+ # @return [String]
114
+ def shape_of(result)
115
+ return "nil" if result.nil?
116
+ return "false" if result == false
117
+
118
+ "a DhanHQ::ErrorObject"
119
+ end
120
+
121
+ # Label identifying the operation that failed, for the exception message.
122
+ #
123
+ # @param receiver [Object] The class (for a class method) or instance.
124
+ # @param name [Symbol] Method name.
125
+ # @return [String] e.g. +"DhanHQ::Models::Order.place"+ or +"…Order#modify"+.
126
+ def operation_label(receiver, name)
127
+ return "#{module_label(receiver)}.#{name}" if receiver.is_a?(Module)
128
+
129
+ "#{module_label(receiver.class)}##{name}"
130
+ end
131
+
132
+ # Reads a module's declared name, falling back to +to_s+ so an anonymous class
133
+ # still yields something readable rather than an object address.
134
+ #
135
+ # @return [String]
136
+ def module_label(mod)
137
+ mod.name || mod.to_s
138
+ end
139
+
140
+ # Validation errors carried by a model instance, when it has any.
141
+ #
142
+ # @param receiver [Object]
143
+ # @return [Hash, nil]
144
+ def errors_from(receiver)
145
+ return nil if receiver.is_a?(Module)
146
+ return nil unless receiver.respond_to?(:errors)
147
+
148
+ receiver.errors
149
+ end
150
+
151
+ # Best available explanation for a failure.
152
+ #
153
+ # @return [String]
154
+ def describe(result, errors = nil)
155
+ return result.errors.to_s if result.is_a?(DhanHQ::ErrorObject)
156
+ return errors.to_s if errors && !errors.empty?
157
+
158
+ # `nil` and `false` carry nothing, so say which of the two it was rather than
159
+ # inventing a cause.
160
+ result.nil? ? "the API returned no record" : "the API rejected the request"
161
+ end
162
+ end
163
+ end
data/lib/dhan_hq.rb CHANGED
@@ -35,6 +35,11 @@ module DhanHQ
35
35
  "base_api" => "BaseAPI",
36
36
  "ip_setup" => "IPSetup",
37
37
  "json_loader" => "JSONLoader",
38
+ # Same failure as "ai" above: mcp.rb defines DhanHQ::MCP while Zeitwerk expected
39
+ # DhanHQ::Mcp, so DhanHQ::MCP::Server raised NameError after a bare `require
40
+ # "dhan_hq"` — it only worked via exe/dhanhq-mcp and lib/dhan_hq/mcp.rb, which
41
+ # require_relative the file directly instead of going through the autoloader.
42
+ "mcp" => "MCP",
38
43
  "ws" => "WS"
39
44
  )
40
45
  LOADER.push_dir(File.join(__dir__, "DhanHQ"), namespace: self)
@@ -70,24 +70,31 @@ position.convert(
70
70
 
71
71
  ## eDIS Authorization
72
72
 
73
- For selling delivery holdings, authorization is handled via `DhanHQ::Models::EDIS`:
73
+ For selling delivery holdings, authorization is handled via `DhanHQ::Models::Edis`:
74
74
 
75
75
  ### Step 1: Generate TPIN
76
76
  ```ruby
77
- DhanHQ::Models::EDIS.generate_tpin
77
+ DhanHQ::Models::Edis.generate_tpin
78
78
  ```
79
79
 
80
- ### Step 2: Open Browser Authorization
80
+ Triggers a TPIN to the user's registered mobile/email. Returns `{status: "accepted"}` the API responds `202` for this async operation.
81
+
82
+ ### Step 2: Generate and Render the Authorization Form
81
83
  ```ruby
82
- DhanHQ::Models::EDIS.open_browser_for_tpin(
84
+ form = DhanHQ::Models::Edis.generate_form(
83
85
  isin: "INE002A01018",
84
86
  qty: 5,
85
- exchange: "NSE"
87
+ exchange: "NSE",
88
+ segment: "EQ"
86
89
  )
90
+ # form[:edisFormHtml] is a browser-postable HTML form; render or POST it so the
91
+ # user can complete authorization on Dhan's eDIS page.
87
92
  ```
88
93
 
89
- ### Step 3: Inquiry eDIS Approval
94
+ ### Step 3: Inquire eDIS Approval
90
95
  ```ruby
91
- inquiry = DhanHQ::Models::EDIS.inquiry(isin: "INE002A01018")
92
- puts "Approved Qty: #{inquiry.aprvd_qty}, Status: #{inquiry.status}"
96
+ status = DhanHQ::Models::Edis.inquire(isin: "INE002A01018") # or isin: "ALL"
97
+ puts "Approved Qty: #{status[:aprvdQty]}, Status: #{status[:status]}"
93
98
  ```
99
+
100
+ `inquire` returns the raw API response (a `HashWithIndifferentAccess`), not a model instance — key names match the API's camelCase (`aprvdQty`, `totalQty`), not the snake_case used elsewhere in this gem.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: DhanHQ
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.1
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shubham Taywade
@@ -229,7 +229,9 @@ files:
229
229
  - lib/DhanHQ/auth/token_manager.rb
230
230
  - lib/DhanHQ/auth/token_renewal.rb
231
231
  - lib/DhanHQ/client.rb
232
+ - lib/DhanHQ/concerns/bang_writes.rb
232
233
  - lib/DhanHQ/concerns/order_audit.rb
234
+ - lib/DhanHQ/concerns/tracked_writes.rb
233
235
  - lib/DhanHQ/configuration.rb
234
236
  - lib/DhanHQ/constants.rb
235
237
  - lib/DhanHQ/contracts/alert_order_contract.rb
@@ -266,6 +268,7 @@ files:
266
268
  - lib/DhanHQ/core/base_model.rb
267
269
  - lib/DhanHQ/core/base_resource.rb
268
270
  - lib/DhanHQ/core/error_handler.rb
271
+ - lib/DhanHQ/deprecation.rb
269
272
  - lib/DhanHQ/dry_run/ledger.rb
270
273
  - lib/DhanHQ/dry_run/simulator.rb
271
274
  - lib/DhanHQ/error_object.rb
@@ -391,6 +394,7 @@ files:
391
394
  - lib/DhanHQ/utils/network_inspector.rb
392
395
  - lib/DhanHQ/version.rb
393
396
  - lib/DhanHQ/write_paths.rb
397
+ - lib/DhanHQ/write_result.rb
394
398
  - lib/DhanHQ/ws.rb
395
399
  - lib/DhanHQ/ws/base_connection.rb
396
400
  - lib/DhanHQ/ws/client.rb