DhanHQ 2.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.
Files changed (113) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +26 -0
  4. data/CHANGELOG.md +20 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/GUIDE.md +555 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +463 -0
  9. data/README1.md +521 -0
  10. data/Rakefile +12 -0
  11. data/TAGS +10 -0
  12. data/TODO-1.md +14 -0
  13. data/TODO.md +127 -0
  14. data/app/services/live/order_update_guard_support.rb +75 -0
  15. data/app/services/live/order_update_hub.rb +76 -0
  16. data/app/services/live/order_update_persistence_support.rb +68 -0
  17. data/config/initializers/order_update_hub.rb +16 -0
  18. data/diagram.html +184 -0
  19. data/diagram.md +34 -0
  20. data/docs/rails_integration.md +304 -0
  21. data/exe/DhanHQ +4 -0
  22. data/lib/DhanHQ/client.rb +116 -0
  23. data/lib/DhanHQ/config.rb +32 -0
  24. data/lib/DhanHQ/configuration.rb +72 -0
  25. data/lib/DhanHQ/constants.rb +170 -0
  26. data/lib/DhanHQ/contracts/base_contract.rb +15 -0
  27. data/lib/DhanHQ/contracts/historical_data_contract.rb +28 -0
  28. data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -0
  29. data/lib/DhanHQ/contracts/modify_order_contract copy.rb +100 -0
  30. data/lib/DhanHQ/contracts/modify_order_contract.rb +22 -0
  31. data/lib/DhanHQ/contracts/option_chain_contract.rb +31 -0
  32. data/lib/DhanHQ/contracts/order_contract.rb +102 -0
  33. data/lib/DhanHQ/contracts/place_order_contract.rb +119 -0
  34. data/lib/DhanHQ/contracts/position_conversion_contract.rb +24 -0
  35. data/lib/DhanHQ/contracts/slice_order_contract.rb +111 -0
  36. data/lib/DhanHQ/core/base_api.rb +105 -0
  37. data/lib/DhanHQ/core/base_model.rb +266 -0
  38. data/lib/DhanHQ/core/base_resource.rb +50 -0
  39. data/lib/DhanHQ/core/error_handler.rb +19 -0
  40. data/lib/DhanHQ/error_object.rb +49 -0
  41. data/lib/DhanHQ/errors.rb +45 -0
  42. data/lib/DhanHQ/helpers/api_helper.rb +17 -0
  43. data/lib/DhanHQ/helpers/attribute_helper.rb +72 -0
  44. data/lib/DhanHQ/helpers/model_helper.rb +7 -0
  45. data/lib/DhanHQ/helpers/request_helper.rb +69 -0
  46. data/lib/DhanHQ/helpers/response_helper.rb +98 -0
  47. data/lib/DhanHQ/helpers/validation_helper.rb +36 -0
  48. data/lib/DhanHQ/json_loader.rb +23 -0
  49. data/lib/DhanHQ/models/edis.rb +58 -0
  50. data/lib/DhanHQ/models/forever_order.rb +85 -0
  51. data/lib/DhanHQ/models/funds.rb +50 -0
  52. data/lib/DhanHQ/models/historical_data.rb +77 -0
  53. data/lib/DhanHQ/models/holding.rb +56 -0
  54. data/lib/DhanHQ/models/kill_switch.rb +49 -0
  55. data/lib/DhanHQ/models/ledger_entry.rb +60 -0
  56. data/lib/DhanHQ/models/margin.rb +54 -0
  57. data/lib/DhanHQ/models/market_feed.rb +41 -0
  58. data/lib/DhanHQ/models/option_chain.rb +79 -0
  59. data/lib/DhanHQ/models/order.rb +239 -0
  60. data/lib/DhanHQ/models/position.rb +60 -0
  61. data/lib/DhanHQ/models/profile.rb +44 -0
  62. data/lib/DhanHQ/models/super_order.rb +69 -0
  63. data/lib/DhanHQ/models/trade.rb +79 -0
  64. data/lib/DhanHQ/rate_limiter.rb +107 -0
  65. data/lib/DhanHQ/requests/optionchain/nifty.json +5 -0
  66. data/lib/DhanHQ/requests/optionchain/nifty_expiries.json +4 -0
  67. data/lib/DhanHQ/requests/orders/create.json +0 -0
  68. data/lib/DhanHQ/resources/edis.rb +44 -0
  69. data/lib/DhanHQ/resources/forever_orders.rb +53 -0
  70. data/lib/DhanHQ/resources/funds.rb +21 -0
  71. data/lib/DhanHQ/resources/historical_data.rb +34 -0
  72. data/lib/DhanHQ/resources/holdings.rb +21 -0
  73. data/lib/DhanHQ/resources/kill_switch.rb +21 -0
  74. data/lib/DhanHQ/resources/margin_calculator.rb +22 -0
  75. data/lib/DhanHQ/resources/market_feed.rb +56 -0
  76. data/lib/DhanHQ/resources/option_chain.rb +31 -0
  77. data/lib/DhanHQ/resources/orders.rb +70 -0
  78. data/lib/DhanHQ/resources/positions.rb +29 -0
  79. data/lib/DhanHQ/resources/profile.rb +25 -0
  80. data/lib/DhanHQ/resources/statements.rb +42 -0
  81. data/lib/DhanHQ/resources/super_orders.rb +46 -0
  82. data/lib/DhanHQ/resources/trades.rb +23 -0
  83. data/lib/DhanHQ/version.rb +6 -0
  84. data/lib/DhanHQ/ws/client.rb +182 -0
  85. data/lib/DhanHQ/ws/cmd_bus.rb +38 -0
  86. data/lib/DhanHQ/ws/connection.rb +240 -0
  87. data/lib/DhanHQ/ws/decoder.rb +83 -0
  88. data/lib/DhanHQ/ws/errors.rb +0 -0
  89. data/lib/DhanHQ/ws/orders/client.rb +59 -0
  90. data/lib/DhanHQ/ws/orders/connection.rb +148 -0
  91. data/lib/DhanHQ/ws/orders.rb +13 -0
  92. data/lib/DhanHQ/ws/packets/depth_delta_packet.rb +20 -0
  93. data/lib/DhanHQ/ws/packets/disconnect_packet.rb +15 -0
  94. data/lib/DhanHQ/ws/packets/full_packet.rb +40 -0
  95. data/lib/DhanHQ/ws/packets/header.rb +23 -0
  96. data/lib/DhanHQ/ws/packets/index_packet.rb +14 -0
  97. data/lib/DhanHQ/ws/packets/market_depth_level.rb +21 -0
  98. data/lib/DhanHQ/ws/packets/market_status_packet.rb +14 -0
  99. data/lib/DhanHQ/ws/packets/oi_packet.rb +15 -0
  100. data/lib/DhanHQ/ws/packets/prev_close_packet.rb +16 -0
  101. data/lib/DhanHQ/ws/packets/quote_packet.rb +26 -0
  102. data/lib/DhanHQ/ws/packets/ticker_packet.rb +16 -0
  103. data/lib/DhanHQ/ws/registry.rb +46 -0
  104. data/lib/DhanHQ/ws/segments.rb +75 -0
  105. data/lib/DhanHQ/ws/singleton_lock.rb +54 -0
  106. data/lib/DhanHQ/ws/sub_state.rb +59 -0
  107. data/lib/DhanHQ/ws/websocket_packet_parser.rb +165 -0
  108. data/lib/DhanHQ/ws.rb +37 -0
  109. data/lib/DhanHQ.rb +135 -0
  110. data/lib/ta/technical_analysis.rb +405 -0
  111. data/sig/DhanHQ.rbs +4 -0
  112. data/watchlist.csv +3 -0
  113. metadata +283 -0
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_contract"
4
+
5
+ module DhanHQ
6
+ module Contracts
7
+ # Validation contract for slicing an order into multiple parts for Dhanhq's API.
8
+ #
9
+ # This contract ensures all required parameters are provided and optional parameters
10
+ # meet the required constraints when they are specified. It validates:
11
+ # - Required fields for slicing orders.
12
+ # - Conditional logic for fields based on the provided values.
13
+ # - Constraints such as inclusion, numerical ranges, and string formats.
14
+ #
15
+ # Example usage:
16
+ # contract = Dhanhq::Contracts::SliceOrderContract.new
17
+ # result = contract.call(
18
+ # dhanClientId: "123456",
19
+ # transactionType: "BUY",
20
+ # exchangeSegment: "NSE_EQ",
21
+ # productType: "CNC",
22
+ # orderType: "LIMIT",
23
+ # validity: "DAY",
24
+ # securityId: "1001",
25
+ # quantity: 10
26
+ # )
27
+ # result.success? # => true or false
28
+ #
29
+ # @see https://dhanhq.co/docs/v2/ Dhanhq API Documentation
30
+ class SliceOrderContract < BaseContract
31
+ # Parameters and validation rules for the slicing order request.
32
+ #
33
+ # @!attribute [r] correlationId
34
+ # @return [String] Optional. Identifier for tracking, max length 25 characters.
35
+ # @!attribute [r] transactionType
36
+ # @return [String] Required. BUY or SELL.
37
+ # @!attribute [r] exchangeSegment
38
+ # @return [String] Required. The segment in which the order is placed.
39
+ # Must be one of: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM.
40
+ # @!attribute [r] productType
41
+ # @return [String] Required. Product type for the order.
42
+ # Must be one of: CNC, INTRADAY, MARGIN, MTF, CO, BO.
43
+ # @!attribute [r] orderType
44
+ # @return [String] Required. Type of order.
45
+ # Must be one of: LIMIT, MARKET, STOP_LOSS, STOP_LOSS_MARKET.
46
+ # @!attribute [r] validity
47
+ # @return [String] Required. Validity of the order.
48
+ # Must be one of: DAY, IOC, GTC, GTD.
49
+ # @!attribute [r] securityId
50
+ # @return [String] Required. Security identifier for the order.
51
+ # @!attribute [r] quantity
52
+ # @return [Integer] Required. Quantity of the order, must be greater than 0.
53
+ # @!attribute [r] disclosedQuantity
54
+ # @return [Integer] Optional. Disclosed quantity, must be >= 0 if provided.
55
+ # @!attribute [r] price
56
+ # @return [Float] Optional. Price for the order, must be > 0 if provided.
57
+ # @!attribute [r] triggerPrice
58
+ # @return [Float] Optional. Trigger price for stop-loss orders, must be > 0 if provided.
59
+ # @!attribute [r] afterMarketOrder
60
+ # @return [Boolean] Optional. Indicates if this is an after-market order.
61
+ # @!attribute [r] amoTime
62
+ # @return [String] Optional. Time for after-market orders. Must be one of: OPEN, OPEN_30, OPEN_60.
63
+ # @!attribute [r] boProfitValue
64
+ # @return [Float] Optional. Profit value for Bracket Orders, must be > 0 if provided.
65
+ # @!attribute [r] boStopLossValue
66
+ # @return [Float] Optional. Stop-loss value for Bracket Orders, must be > 0 if provided.
67
+ # @!attribute [r] drvExpiryDate
68
+ # @return [String] Optional. Expiry date for derivative contracts.
69
+ # @!attribute [r] drvOptionType
70
+ # @return [String] Optional. Option type for derivatives, must be one of: CALL, PUT, NA.
71
+ # @!attribute [r] drvStrikePrice
72
+ # @return [Float] Optional. Strike price for options, must be > 0 if provided.
73
+ params do
74
+ optional(:correlationId).maybe(:string, max_size?: 25)
75
+ required(:transactionType).filled(:string, included_in?: %w[BUY SELL])
76
+ required(:exchangeSegment).filled(:string,
77
+ included_in?: %w[NSE_EQ NSE_FNO NSE_CURRENCY BSE_EQ BSE_FNO BSE_CURRENCY
78
+ MCX_COMM])
79
+ required(:productType).filled(:string, included_in?: %w[CNC INTRADAY MARGIN MTF CO BO])
80
+ required(:orderType).filled(:string, included_in?: %w[LIMIT MARKET STOP_LOSS STOP_LOSS_MARKET])
81
+ required(:validity).filled(:string, included_in?: %w[DAY IOC GTC GTD])
82
+ required(:securityId).filled(:string)
83
+ required(:quantity).filled(:integer, gt?: 0)
84
+ optional(:disclosedQuantity).maybe(:integer, gteq?: 0)
85
+ optional(:price).maybe(:float, gt?: 0)
86
+ optional(:triggerPrice).maybe(:float, gt?: 0)
87
+ optional(:afterMarketOrder).maybe(:bool)
88
+ optional(:amoTime).maybe(:string, included_in?: %w[OPEN OPEN_30 OPEN_60])
89
+ optional(:boProfitValue).maybe(:float, gt?: 0)
90
+ optional(:boStopLossValue).maybe(:float, gt?: 0)
91
+ optional(:drvExpiryDate).maybe(:string)
92
+ optional(:drvOptionType).maybe(:string, included_in?: %w[CALL PUT NA])
93
+ optional(:drvStrikePrice).maybe(:float, gt?: 0)
94
+ end
95
+
96
+ # Custom validation for trigger price when the order type is STOP_LOSS or STOP_LOSS_MARKET.
97
+ rule(:triggerPrice, :orderType) do
98
+ if values[:orderType].start_with?("STOP_LOSS") && !values[:triggerPrice]
99
+ key(:triggerPrice).failure("is required for orderType STOP_LOSS or STOP_LOSS_MARKET")
100
+ end
101
+ end
102
+
103
+ # Custom validation for AMO time when the order is marked as after-market.
104
+ rule(:afterMarketOrder, :amoTime) do
105
+ if values[:afterMarketOrder] == true && !values[:amoTime]
106
+ key(:amoTime).failure("is required when afterMarketOrder is true")
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Base class for all API resource classes.
5
+ # Delegates HTTP requests to {DhanHQ::Client} and exposes helpers shared by
6
+ # resource wrappers.
7
+ class BaseAPI
8
+ include DhanHQ::APIHelper
9
+ include DhanHQ::AttributeHelper
10
+
11
+ # Default API type used when a subclass does not override {#initialize}.
12
+ API_TYPE = :non_trading_api
13
+ # Root path prepended to each endpoint segment.
14
+ HTTP_PATH = ""
15
+
16
+ attr_reader :client
17
+
18
+ # Initializes the BaseAPI with the appropriate Client instance
19
+ #
20
+ # @param api_type [Symbol] API type (`:order_api`, `:data_api`, `:non_trading_api`)
21
+ def initialize(api_type: self.class::API_TYPE)
22
+ @client = DhanHQ::Client.new(api_type: api_type)
23
+ end
24
+
25
+ # Perform a GET request via `Client`
26
+ #
27
+ # @param endpoint [String] API endpoint
28
+ # @param params [Hash] Query parameters
29
+ # @return [Hash, Array] The parsed API response
30
+ def get(endpoint, params: {})
31
+ formatted_params = format_params(endpoint, params)
32
+ handle_response(client.get(build_path(endpoint), formatted_params))
33
+ end
34
+
35
+ # Perform a POST request via `Client`
36
+ #
37
+ # @param endpoint [String] API endpoint
38
+ # @param params [Hash] Request body
39
+ # @return [Hash, Array] The parsed API response
40
+ def post(endpoint, params: {})
41
+ formatted_params = format_params(endpoint, params)
42
+ handle_response(client.post(build_path(endpoint), formatted_params))
43
+ end
44
+
45
+ # Perform a PUT request via `Client`
46
+ #
47
+ # @param endpoint [String] API endpoint
48
+ # @param params [Hash] Request body
49
+ # @return [Hash, Array] The parsed API response
50
+ def put(endpoint, params: {})
51
+ formatted_params = format_params(endpoint, params)
52
+ handle_response(client.put(build_path(endpoint), formatted_params))
53
+ end
54
+
55
+ # Perform a DELETE request via `Client`
56
+ #
57
+ # @param endpoint [String] API endpoint
58
+ # @return [Hash, Array] The parsed API response
59
+ def delete(endpoint)
60
+ formatted_params = format_params(endpoint, {})
61
+ handle_response(client.delete(build_path(endpoint), formatted_params))
62
+ end
63
+
64
+ private
65
+
66
+ # Performs an API request.
67
+ #
68
+ # @param method [Symbol] HTTP method (:get, :post, :put, :delete)
69
+ # @param endpoint [String] API endpoint
70
+ # @param params [Hash] Request parameters
71
+ # @return [Hash, Array] The parsed API response
72
+ # @raise [DhanHQ::Error] If an API error occurs.
73
+ def request(method, endpoint, params: {})
74
+ formatted_params = format_params(endpoint, params)
75
+
76
+ response = client.request(method, build_path(endpoint), formatted_params)
77
+
78
+ handle_response(response)
79
+ end
80
+
81
+ # Construct the complete API URL
82
+ #
83
+ # @param endpoint [String] API endpoint
84
+ # @return [String] Full API path
85
+ def build_path(endpoint)
86
+ "#{self.class::HTTP_PATH}#{endpoint}"
87
+ end
88
+
89
+ # Format parameters based on API endpoint
90
+ def format_params(endpoint, params)
91
+ return params if marketfeed_api?(endpoint) || params.empty?
92
+
93
+ optionchain_api?(endpoint) ? titleize_keys(params) : camelize_keys(params)
94
+ end
95
+
96
+ # Determines if the API endpoint is for Option Chain
97
+ def optionchain_api?(endpoint)
98
+ endpoint.include?("/optionchain")
99
+ end
100
+
101
+ def marketfeed_api?(endpoint)
102
+ endpoint.include?("/marketfeed")
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-validation"
4
+ require "active_support/core_ext/hash/indifferent_access"
5
+ require "active_support/inflector"
6
+
7
+ module DhanHQ
8
+ # Base class for resource objects
9
+ # Handles validation, attribute mapping, and response parsing
10
+ class BaseModel
11
+ # Extend & Include Modules
12
+ extend DhanHQ::APIHelper
13
+ extend DhanHQ::AttributeHelper
14
+ extend DhanHQ::ValidationHelper
15
+ extend DhanHQ::RequestHelper
16
+ extend DhanHQ::ResponseHelper
17
+
18
+ include DhanHQ::APIHelper
19
+ include DhanHQ::AttributeHelper
20
+ include DhanHQ::ValidationHelper
21
+ include DhanHQ::RequestHelper
22
+ include DhanHQ::ResponseHelper
23
+
24
+ # Attribute Accessors
25
+ attr_reader :attributes, :errors
26
+
27
+ # Initialize a new resource object
28
+ #
29
+ # @param attributes [Hash] The attributes of the resource
30
+ def initialize(attributes = {}, skip_validation: false)
31
+ @attributes = normalize_keys(attributes)
32
+ @errors = {}
33
+
34
+ validate! unless skip_validation
35
+ assign_attributes
36
+ end
37
+
38
+ # Class Methods
39
+ # Attributes set by child classes
40
+ class << self
41
+ attr_reader :defined_attributes
42
+
43
+ # Registers the set of attributes for this model
44
+ #
45
+ # @param args [Array<Symbol, String>] A list of attribute names
46
+ def attributes(*args)
47
+ @defined_attributes ||= []
48
+ @defined_attributes.concat(args.map(&:to_s))
49
+ end
50
+
51
+ # Provide a default API type, can be overridden by child classes
52
+ #
53
+ # e.g., def self.api_type; :data_api; end
54
+ #
55
+ # or override the `api` method entirely
56
+ def api_type
57
+ :order_api
58
+ end
59
+
60
+ # Provide a shared BaseAPI instance for this model
61
+ #
62
+ # For child classes, override `api_type` or `api` if needed
63
+ def api
64
+ @api ||= BaseAPI.new(api_type: api_type)
65
+ end
66
+
67
+ ##
68
+ # Returns the API resource used by collection methods.
69
+ #
70
+ # Subclasses may override this to return a specialized API class.
71
+ # By default it simply returns {#api}.
72
+ def resource
73
+ api
74
+ end
75
+
76
+ # Retrieve the resource path for the API
77
+ #
78
+ # @return [String] The resource path
79
+ def resource_path
80
+ self::HTTP_PATH
81
+ end
82
+
83
+ # Every model must either override this or set a Dry::Validation contract if they need validation
84
+ #
85
+ # @return [Dry::Validation::Contract] The validation contract
86
+ def validation_contract
87
+ raise NotImplementedError, "#{name} must implement `validation_contract`"
88
+ end
89
+
90
+ # Validate attributes before creating a new instance
91
+ def validate_attributes(attributes)
92
+ contract = validation_contract
93
+ result = contract.call(attributes)
94
+
95
+ raise ArgumentError, "Validation failed: #{result.errors.to_h}" if result.failure?
96
+ end
97
+
98
+ # == CRUD / Collection Methods
99
+
100
+ # Find all resources
101
+ #
102
+ # @return [Array<DhanHQ::BaseModel>, DhanHQ::ErrorObject] An array of resources or error object
103
+ def all
104
+ response = resource.get("")
105
+
106
+ parse_collection_response(response)
107
+ end
108
+
109
+ # Find a resource by ID
110
+ #
111
+ # @param id [String] The ID of the resource
112
+ # @return [DhanHQ::BaseModel, DhanHQ::ErrorObject] The resource or error object
113
+ def find(id)
114
+ response = resource.get("/#{id}")
115
+
116
+ payload = response.is_a?(Array) ? response.first : response
117
+ build_from_response(payload)
118
+ end
119
+
120
+ # Fetches records filtered by query parameters.
121
+ #
122
+ # @param params [Hash] Query parameters supported by the API.
123
+ # @return [Array<BaseModel>, BaseModel, DhanHQ::ErrorObject]
124
+ def where(params)
125
+ response = resource.get("", params: params)
126
+ build_from_response(response)
127
+ end
128
+
129
+ # Create a new resource
130
+ #
131
+ # @param attributes [Hash] The attributes of the resource
132
+ # @return [DhanHQ::BaseModel, DhanHQ::ErrorObject] The resource or error object
133
+ def create(attributes)
134
+ # validate_params!(attributes, validation_contract)
135
+
136
+ response = resource.post("", params: attributes)
137
+ build_from_response(response)
138
+ end
139
+
140
+ # Helper method to parse a collection response into model instances
141
+ #
142
+ # @param response [Object] The raw response from the API
143
+ # @return [Array<BaseModel>]
144
+ def parse_collection_response(response)
145
+ # Some endpoints return arrays, others might return a `[:data]` structure
146
+ return [] unless response.is_a?(Array) || (response.is_a?(Hash) && response[:data].is_a?(Array))
147
+
148
+ collection = response.is_a?(Array) ? response : response[:data]
149
+ collection.map { |record| new(record) }
150
+ end
151
+ end
152
+
153
+ # Instance Methods
154
+
155
+ # Update an existing resource
156
+ #
157
+ # @param attributes [Hash] Attributes to update
158
+ # @return [DhanHQ::BaseModel, DhanHQ::ErrorObject]
159
+ def update(attributes = {})
160
+ response = self.class.resource.put("/#{id}", params: attributes)
161
+
162
+ success_response?(response) ? self.class.build_from_response(response) : DhanHQ::ErrorObject.new(response)
163
+ end
164
+
165
+ # Persists the current resource by delegating to {#create} or {#update}.
166
+ #
167
+ # @return [DhanHQ::BaseModel, DhanHQ::ErrorObject, false]
168
+ def save
169
+ new_record? ? self.class.create(attributes) : update(attributes)
170
+ end
171
+
172
+ # Same as {#save} but raises {DhanHQ::Error} when persistence fails.
173
+ #
174
+ # @return [DhanHQ::BaseModel]
175
+ # @raise [DhanHQ::Error] When the record cannot be saved.
176
+ def save!
177
+ result = save
178
+ return result unless result == false || result.nil? || result.is_a?(DhanHQ::ErrorObject)
179
+
180
+ error_details =
181
+ if result.is_a?(DhanHQ::ErrorObject)
182
+ result.errors
183
+ elsif @errors && !@errors.empty?
184
+ @errors
185
+ else
186
+ "Unknown error"
187
+ end
188
+
189
+ raise DhanHQ::Error, "Failed to save the record: #{error_details}"
190
+ end
191
+
192
+ # Delete the resource
193
+ #
194
+ # @return [Boolean] True if deletion was successful
195
+ # Deletes the resource from the remote API.
196
+ #
197
+ # @return [Boolean] True when the server confirms deletion.
198
+ def delete
199
+ response = self.class.resource.delete("/#{id}")
200
+ success_response?(response)
201
+ rescue StandardError
202
+ false
203
+ end
204
+
205
+ # Alias for {#delete} maintained for ActiveModel familiarity.
206
+ #
207
+ # @return [Boolean]
208
+ def destroy
209
+ response = self.class.resource.delete("/#{id}")
210
+ success_response?(response)
211
+ rescue StandardError
212
+ false
213
+ end
214
+
215
+ def persisted?
216
+ !!id
217
+ end
218
+
219
+ def new_record?
220
+ !persisted?
221
+ end
222
+
223
+ # Format request parameters before sending to API
224
+ #
225
+ # @return [Hash] The camelCased attributes
226
+ def to_request_params
227
+ optionchain_api? ? titleize_keys(@attributes) : camelize_keys(@attributes)
228
+ end
229
+
230
+ # Identifier inferred from the loaded attributes.
231
+ #
232
+ # @return [String, Integer, nil]
233
+ def id
234
+ @attributes[:id] || @attributes[:order_id] || @attributes[:security_id]
235
+ end
236
+
237
+ # Dynamically assign attributes as methods
238
+ def assign_attributes
239
+ self.class.defined_attributes&.each do |attr|
240
+ instance_variable_set(:"@#{attr}", @attributes[attr])
241
+ define_singleton_method(attr) { instance_variable_get(:"@#{attr}") }
242
+ define_singleton_method(attr.to_s.camelize(:lower)) { instance_variable_get(:"@#{attr}") }
243
+ end
244
+ end
245
+
246
+ def optionchain_api?
247
+ self.class.name.include?("OptionChain")
248
+ end
249
+
250
+ # Validate attributes using contract
251
+ def valid?
252
+ contract_class = respond_to?(:validation_contract) ? validation_contract : self.class.validation_contract
253
+ return true unless contract_class
254
+
255
+ contract = contract_class.is_a?(Class) ? contract_class.new : contract_class
256
+ result = contract.call(@attributes)
257
+
258
+ if result.failure?
259
+ @errors = result.errors.to_h
260
+ return false
261
+ end
262
+
263
+ true
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Base wrapper exposing RESTful helpers used by resource classes.
5
+ class BaseResource < BaseAPI
6
+ def initialize(api_type: self.class::API_TYPE)
7
+ super(api_type: api_type) # rubocop:disable Style/SuperArguments
8
+ end
9
+
10
+ # Fetches all records for the resource.
11
+ #
12
+ # @return [Array<Hash>, Hash]
13
+ def all
14
+ get(self.class::HTTP_PATH)
15
+ end
16
+
17
+ # Retrieves a single resource by identifier.
18
+ #
19
+ # @param id [String, Integer]
20
+ # @return [Hash]
21
+ def find(id)
22
+ get("#{self.class::HTTP_PATH}/#{id}")
23
+ end
24
+
25
+ # Creates a new resource instance.
26
+ #
27
+ # @param params [Hash]
28
+ # @return [Hash]
29
+ def create(params)
30
+ post(self.class::HTTP_PATH, params: params)
31
+ end
32
+
33
+ # Updates an existing resource.
34
+ #
35
+ # @param id [String, Integer]
36
+ # @param params [Hash]
37
+ # @return [Hash]
38
+ def update(id, params)
39
+ put("#{self.class::HTTP_PATH}/#{id}", params: params)
40
+ end
41
+
42
+ # Deletes a resource by identifier.
43
+ #
44
+ # @param id [String, Integer]
45
+ # @return [Hash]
46
+ def delete(id)
47
+ super("#{self.class::HTTP_PATH}/#{id}")
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Provides a minimal shim for surfacing validation and runtime errors.
5
+ class ErrorHandler
6
+ # Normalises the exception raised for various error types.
7
+ #
8
+ # @param error [Dry::Validation::Result, StandardError]
9
+ # @raise [RuntimeError]
10
+ def self.handle(error)
11
+ case error
12
+ when Dry::Validation::Result
13
+ raise "Validation Error: #{error.errors.to_h}"
14
+ else
15
+ raise "Error: #{error.message}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Wrapper class for API error responses
5
+ class ErrorObject
6
+ # @return [Hash] Raw error response
7
+ attr_reader :response
8
+
9
+ # Initialize a new ErrorObject
10
+ #
11
+ # @param response [Hash] Parsed API response
12
+ def initialize(response)
13
+ @response =
14
+ if response.is_a?(Hash)
15
+ response.with_indifferent_access
16
+ else
17
+ { message: response.to_s }.with_indifferent_access
18
+ end
19
+ end
20
+
21
+ # Always returns false to mimic success? interface on resources
22
+ #
23
+ # @return [Boolean]
24
+ def success?
25
+ false
26
+ end
27
+
28
+ # Extracts the error message from the response
29
+ #
30
+ # @return [String]
31
+ def message
32
+ response[:errorMessage] || response[:message] || response[:error] || "Unknown error"
33
+ end
34
+
35
+ # Error code if present
36
+ #
37
+ # @return [String, nil]
38
+ def code
39
+ response[:errorCode]
40
+ end
41
+
42
+ # Alias for the raw response hash
43
+ #
44
+ # @return [Hash]
45
+ def errors
46
+ response
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Base error class for all DhanHQ API errors
5
+ class Error < StandardError; end
6
+
7
+ # Authentication and access errors
8
+ # DH-901
9
+ class InvalidAuthenticationError < Error; end
10
+ # DH-902
11
+ class InvalidAccessError < Error; end
12
+ # DH-903
13
+ class UserAccountError < Error; end
14
+ # DH-808
15
+ class AuthenticationFailedError < Error; end
16
+ # DH-807, DH-809
17
+ class InvalidTokenError < Error; end
18
+ # DH-810
19
+ class InvalidClientIDError < Error; end
20
+
21
+ # Rate limits and input validation errors
22
+ # DH-904, 805
23
+ class RateLimitError < Error; end
24
+ # DH-905
25
+ class InputExceptionError < Error; end
26
+ # DH-811, DH-812, DH-813, DH-814
27
+ class InvalidRequestError < Error; end
28
+
29
+ # Order and market data errors
30
+ class OrderError < Error; end
31
+ # Raised when the API signals an issue with the requested data payload.
32
+ class DataError < Error; end
33
+
34
+ # Server and network-related errors
35
+ # DH-908, 800
36
+ class InternalServerError < Error; end
37
+ # DH-1111
38
+ class NoHoldingsError < Error; end
39
+ # DH-909
40
+ class NetworkError < Error; end
41
+ # DH-910
42
+ class OtherError < Error; end
43
+ # 404
44
+ class NotFoundError < Error; end
45
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Helper mixin offering response validation behaviour for API wrappers.
5
+ module APIHelper
6
+ # Ensures the response is a structured payload before returning it.
7
+ #
8
+ # @param response [Hash, Array]
9
+ # @return [Hash, Array]
10
+ # @raise [DhanHQ::Error] When an unexpected payload type is received.
11
+ def handle_response(response)
12
+ return response if response.is_a?(Array) || response.is_a?(Hash)
13
+
14
+ raise DhanHQ::Error, "Unexpected API response format"
15
+ end
16
+ end
17
+ end