DhanHQ 2.1.8 → 2.2.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.rubocop_todo.yml +143 -118
  4. data/CHANGELOG.md +177 -0
  5. data/CODE_REVIEW_ISSUES.md +397 -0
  6. data/FIXES_APPLIED.md +373 -0
  7. data/GUIDE.md +41 -0
  8. data/README.md +55 -0
  9. data/RELEASING.md +60 -0
  10. data/REVIEW_SUMMARY.md +120 -0
  11. data/VERSION_UPDATE.md +82 -0
  12. data/core +0 -0
  13. data/docs/AUTHENTICATION.md +63 -0
  14. data/docs/DATA_API_PARAMETERS.md +278 -0
  15. data/docs/PR_2.2.0.md +48 -0
  16. data/docs/RELEASE_GUIDE.md +492 -0
  17. data/docs/TESTING_GUIDE.md +1514 -0
  18. data/docs/live_order_updates.md +25 -1
  19. data/docs/rails_integration.md +29 -0
  20. data/docs/websocket_integration.md +22 -0
  21. data/lib/DhanHQ/client.rb +65 -9
  22. data/lib/DhanHQ/configuration.rb +26 -0
  23. data/lib/DhanHQ/constants.rb +1 -1
  24. data/lib/DhanHQ/contracts/expired_options_data_contract.rb +6 -6
  25. data/lib/DhanHQ/contracts/place_order_contract.rb +51 -0
  26. data/lib/DhanHQ/core/base_model.rb +26 -11
  27. data/lib/DhanHQ/errors.rb +4 -0
  28. data/lib/DhanHQ/helpers/request_helper.rb +17 -2
  29. data/lib/DhanHQ/helpers/response_helper.rb +34 -13
  30. data/lib/DhanHQ/models/edis.rb +150 -14
  31. data/lib/DhanHQ/models/expired_options_data.rb +307 -88
  32. data/lib/DhanHQ/models/forever_order.rb +261 -22
  33. data/lib/DhanHQ/models/funds.rb +76 -10
  34. data/lib/DhanHQ/models/historical_data.rb +148 -31
  35. data/lib/DhanHQ/models/holding.rb +82 -6
  36. data/lib/DhanHQ/models/instrument_helpers.rb +4 -4
  37. data/lib/DhanHQ/models/kill_switch.rb +113 -11
  38. data/lib/DhanHQ/models/ledger_entry.rb +101 -13
  39. data/lib/DhanHQ/models/margin.rb +133 -8
  40. data/lib/DhanHQ/models/market_feed.rb +181 -17
  41. data/lib/DhanHQ/models/option_chain.rb +184 -12
  42. data/lib/DhanHQ/models/order.rb +418 -36
  43. data/lib/DhanHQ/models/order_update.rb +0 -4
  44. data/lib/DhanHQ/models/position.rb +161 -10
  45. data/lib/DhanHQ/models/profile.rb +103 -7
  46. data/lib/DhanHQ/models/super_order.rb +275 -15
  47. data/lib/DhanHQ/models/trade.rb +279 -26
  48. data/lib/DhanHQ/rate_limiter.rb +40 -6
  49. data/lib/DhanHQ/resources/expired_options_data.rb +1 -1
  50. data/lib/DhanHQ/version.rb +1 -1
  51. data/lib/DhanHQ/ws/client.rb +11 -5
  52. data/lib/DhanHQ/ws/connection.rb +16 -2
  53. data/lib/DhanHQ/ws/market_depth/client.rb +2 -1
  54. data/lib/DhanHQ/ws/market_depth.rb +12 -12
  55. data/lib/DhanHQ/ws/orders/client.rb +78 -12
  56. data/lib/DhanHQ/ws/orders/connection.rb +2 -1
  57. data/lib/DhanHQ/ws/orders.rb +2 -1
  58. metadata +18 -5
  59. data/lib/DhanHQ/contracts/modify_order_contract_copy.rb +0 -100
@@ -6,7 +6,45 @@ require_relative "../contracts/modify_order_contract"
6
6
  module DhanHQ
7
7
  # ActiveRecord-style models built on top of the REST resources.
8
8
  module Models
9
- # Representation of an order as returned by the REST APIs.
9
+ ##
10
+ # Model for managing equity and F&O orders.
11
+ #
12
+ # The Order Management API lets you place new orders, cancel or modify pending orders,
13
+ # and retrieve order status, trade status, order book, and trade book. This model provides
14
+ # an ActiveRecord-style interface for order management operations.
15
+ #
16
+ # @note **Static IP Whitelisting**: Order placement, modification, and cancellation APIs
17
+ # require Static IP whitelisting. Ensure your IP is whitelisted before using these endpoints.
18
+ #
19
+ # @example Place a new market order
20
+ # order = DhanHQ::Models::Order.place(
21
+ # dhan_client_id: "1000000003",
22
+ # transaction_type: "BUY",
23
+ # exchange_segment: "NSE_EQ",
24
+ # product_type: "INTRADAY",
25
+ # order_type: "MARKET",
26
+ # validity: "DAY",
27
+ # security_id: "11536",
28
+ # quantity: 5
29
+ # )
30
+ # puts "Order placed: #{order.order_id} - #{order.order_status}"
31
+ #
32
+ # @example Modify an existing order
33
+ # order = DhanHQ::Models::Order.find("112111182045")
34
+ # order.modify(price: 3345.8, quantity: 40)
35
+ # puts "Order modified: #{order.order_status}"
36
+ #
37
+ # @example Cancel an order
38
+ # order = DhanHQ::Models::Order.find("112111182045")
39
+ # if order.cancel
40
+ # puts "Order cancelled successfully"
41
+ # end
42
+ #
43
+ # @example Fetch all orders for the day
44
+ # orders = DhanHQ::Models::Order.all
45
+ # pending_orders = orders.select { |o| o.order_status == "PENDING" }
46
+ # puts "Pending orders: #{pending_orders.count}"
47
+ #
10
48
  class Order < BaseModel
11
49
  # Attributes eligible for modification requests.
12
50
  MODIFIABLE_FIELDS = %i[
@@ -35,17 +73,36 @@ module DhanHQ
35
73
 
36
74
  class << self
37
75
  ##
38
- # Provides a **shared instance** of the `Orders` resource
76
+ # Provides a shared instance of the Orders resource.
39
77
  #
40
- # @return [DhanHQ::Resources::Orders]
78
+ # @return [DhanHQ::Resources::Orders] The Orders resource client instance
41
79
  def resource
42
80
  @resource ||= DhanHQ::Resources::Orders.new
43
81
  end
44
82
 
45
83
  ##
46
- # Fetch all orders for the day.
84
+ # Retrieves all orders placed during the current trading day.
85
+ #
86
+ # Fetches an array of all orders requested in a day with their last updated status.
87
+ # Returns empty array if no orders are found or if the response is not in the expected format.
88
+ #
89
+ # @return [Array<Order>] Array of Order objects. Returns empty array if no orders exist.
90
+ # Each Order object contains comprehensive order details including status, timestamps,
91
+ # quantities, prices, and error information if applicable.
92
+ #
93
+ # @example Fetch all orders for the day
94
+ # orders = DhanHQ::Models::Order.all
95
+ # puts "Total orders: #{orders.count}"
96
+ # orders.each do |order|
97
+ # puts "#{order.order_id}: #{order.order_status} - #{order.transaction_type} #{order.quantity}"
98
+ # end
99
+ #
100
+ # @example Filter orders by status
101
+ # orders = DhanHQ::Models::Order.all
102
+ # pending = orders.select { |o| o.order_status == "PENDING" }
103
+ # traded = orders.select { |o| o.order_status == "TRADED" }
104
+ # puts "Pending: #{pending.count}, Traded: #{traded.count}"
47
105
  #
48
- # @return [Array<Order>]
49
106
  def all
50
107
  response = resource.all
51
108
  return [] unless response.is_a?(Array)
@@ -54,10 +111,61 @@ module DhanHQ
54
111
  end
55
112
 
56
113
  ##
57
- # Fetch a specific order by ID.
114
+ # Retrieves the details and status of a specific order by order ID.
115
+ #
116
+ # Fetches comprehensive order information including all order parameters, status,
117
+ # timestamps, trade information, and error details if the order was rejected.
118
+ #
119
+ # @param order_id [String] Order-specific identification generated by Dhan
120
+ #
121
+ # @return [Order, nil] Order object with complete details if found, nil otherwise.
122
+ # Response structure includes (keys normalized to snake_case):
123
+ # - **:dhan_client_id** [String] User-specific identification generated by Dhan
124
+ # - **:order_id** [String] Order-specific identification generated by Dhan
125
+ # - **:correlation_id** [String] User/partner generated ID for tracking
126
+ # - **:order_status** [String] Last updated status of the order.
127
+ # Valid values: "TRANSIT", "PENDING", "REJECTED", "CANCELLED", "PART_TRADED",
128
+ # "TRADED", "EXPIRED"
129
+ # - **:transaction_type** [String] The trading side of transaction. "BUY" or "SELL"
130
+ # - **:exchange_segment** [String] Exchange segment of instrument
131
+ # - **:product_type** [String] Product type. Valid values: "CNC", "INTRADAY",
132
+ # "MARGIN", "MTF", "CO", "BO"
133
+ # - **:order_type** [String] Order type. Valid values: "LIMIT", "MARKET",
134
+ # "STOP_LOSS", "STOP_LOSS_MARKET"
135
+ # - **:validity** [String] Validity of order. "DAY" or "IOC"
136
+ # - **:trading_symbol** [String] Trading symbol of the instrument
137
+ # - **:security_id** [String] Exchange standard ID for each scrip
138
+ # - **:quantity** [Integer] Number of shares for the order
139
+ # - **:disclosed_quantity** [Integer] Number of shares visible
140
+ # - **:price** [Float] Price at which order is placed
141
+ # - **:trigger_price** [Float] Price at which order is triggered (for SL-M, SL-L, CO & BO)
142
+ # - **:after_market_order** [Boolean] Whether the order placed is an AMO
143
+ # - **:bo_profit_value** [Float] Bracket Order Target Price change
144
+ # - **:bo_stop_loss_value** [Float] Bracket Order Stop Loss Price change
145
+ # - **:leg_name** [String] Leg identification in case of BO.
146
+ # Valid values: "ENTRY_LEG", "TARGET_LEG", "STOP_LOSS_LEG"
147
+ # - **:create_time** [String] Time at which the order is created
148
+ # - **:update_time** [String] Time at which the last activity happened
149
+ # - **:exchange_time** [String] Time at which order reached at exchange
150
+ # - **:drv_expiry_date** [String, nil] For F&O, expiry date of contract
151
+ # - **:drv_option_type** [String, nil] Type of Option. "CALL" or "PUT"
152
+ # - **:drv_strike_price** [Float] For Options, Strike Price
153
+ # - **:oms_error_code** [String, nil] Error code if the order is rejected or failed
154
+ # - **:oms_error_description** [String, nil] Description of error if the order is rejected
155
+ # - **:algo_id** [String] Exchange Algo ID for Dhan
156
+ # - **:remaining_quantity** [Integer] Quantity pending at the exchange to be traded
157
+ # (quantity - filled_qty)
158
+ # - **:average_traded_price** [Float] Average price at which order is traded
159
+ # - **:filled_qty** [Integer] Quantity of order traded on Exchange
160
+ #
161
+ # @example Fetch order details
162
+ # order = DhanHQ::Models::Order.find("112111182198")
163
+ # if order
164
+ # puts "Status: #{order.order_status}"
165
+ # puts "Quantity: #{order.quantity}, Filled: #{order.filled_qty}"
166
+ # puts "Remaining: #{order.remaining_quantity}"
167
+ # end
58
168
  #
59
- # @param order_id [String]
60
- # @return [Order, nil]
61
169
  def find(order_id)
62
170
  response = resource.find(order_id)
63
171
  return nil unless response.is_a?(Hash) || (response.is_a?(Array) && response.any?)
@@ -67,10 +175,24 @@ module DhanHQ
67
175
  end
68
176
 
69
177
  ##
70
- # Fetch a specific order by correlation ID.
178
+ # Retrieves the status of an order using a user-supplied correlation ID.
179
+ #
180
+ # Useful when you need to track orders using your own correlation ID instead of
181
+ # the Dhan-generated order ID. This is helpful if you missed the order ID due to
182
+ # unforeseen reasons but have your correlation ID stored.
183
+ #
184
+ # @param correlation_id [String] The user/partner generated ID for tracking
185
+ #
186
+ # @return [Order, nil] Order object with complete details if found, nil otherwise.
187
+ # Response structure is the same as {find}.
188
+ #
189
+ # @example Fetch order by correlation ID
190
+ # order = DhanHQ::Models::Order.find_by_correlation("123abc678")
191
+ # if order
192
+ # puts "Order ID: #{order.order_id}"
193
+ # puts "Status: #{order.order_status}"
194
+ # end
71
195
  #
72
- # @param correlation_id [String]
73
- # @return [Order, nil]
74
196
  def find_by_correlation(correlation_id)
75
197
  response = resource.by_correlation(correlation_id)
76
198
  return nil unless response[:status] == "success"
@@ -78,12 +200,94 @@ module DhanHQ
78
200
  new(response, skip_validation: true)
79
201
  end
80
202
 
81
- # Place a new order
203
+ ##
204
+ # Places a new order.
205
+ #
206
+ # Validates order parameters and places a new order via the API. After successful
207
+ # placement, fetches and returns the complete order details including the generated
208
+ # order ID and current status.
209
+ #
210
+ # @param params [Hash{Symbol => String, Integer, Float, Boolean}] Order placement parameters
211
+ # @option params [String] :dhan_client_id (required) User-specific identification generated by Dhan.
212
+ # Must be explicitly provided in the params hash
213
+ # @option params [String] :correlation_id (optional) User/partner generated ID for tracking.
214
+ # Max length: 25 characters
215
+ # @option params [String] :transaction_type (required) The trading side of transaction.
216
+ # Valid values: "BUY", "SELL"
217
+ # @option params [String] :exchange_segment (required) Exchange and segment of instrument.
218
+ # Valid values: See {DhanHQ::Constants::EXCHANGE_SEGMENTS}
219
+ # @option params [String] :product_type (required) Product type.
220
+ # Valid values: "CNC", "INTRADAY", "MARGIN", "MTF", "CO", "BO"
221
+ # @option params [String] :order_type (required) Order type.
222
+ # Valid values: "LIMIT", "MARKET", "STOP_LOSS", "STOP_LOSS_MARKET"
223
+ # @option params [String] :validity (required) Validity of order for execution.
224
+ # Valid values: "DAY", "IOC"
225
+ # @option params [String] :security_id (required) Exchange standard ID for each scrip
226
+ # @option params [Integer] :quantity (required) Number of shares for the order. Must be greater than 0
227
+ # @option params [Integer] :disclosed_quantity (optional) Number of shares visible to market.
228
+ # Keep more than 30% of quantity if providing. Must be >= 0 if provided
229
+ # @option params [Float] :price (conditionally required) Price at which order is placed.
230
+ # Required for LIMIT orders. Must be > 0 if provided
231
+ # @option params [Float] :trigger_price (conditionally required) Price at which the order is triggered.
232
+ # Required for STOP_LOSS and STOP_LOSS_MARKET order types. Must be > 0 if provided
233
+ # @option params [Boolean] :after_market_order (optional) Flag for orders placed after market hours
234
+ # @option params [String] :amo_time (conditionally required) Timing to place the after market order.
235
+ # Required when after_market_order is true.
236
+ # Valid values: "PRE_OPEN", "OPEN", "OPEN_30", "OPEN_60"
237
+ # @option params [Float] :bo_profit_value (conditionally required) Bracket Order Target Price change.
238
+ # Required for BO product type. Must be > 0 if provided
239
+ # @option params [Float] :bo_stop_loss_value (conditionally required) Bracket Order Stop Loss Price change.
240
+ # Required for BO product type. Must be > 0 if provided
241
+ #
242
+ # @return [Order, nil] Order object with complete details if placement succeeds, nil otherwise.
243
+ # The order object includes the generated :order_id and current :order_status.
82
244
  #
83
- # @param params [Hash] Order parameters
84
- # @return [Order]
245
+ # @example Place a market order
246
+ # order = DhanHQ::Models::Order.place(
247
+ # dhan_client_id: "1000000003",
248
+ # transaction_type: "BUY",
249
+ # exchange_segment: "NSE_EQ",
250
+ # product_type: "INTRADAY",
251
+ # order_type: "MARKET",
252
+ # validity: "DAY",
253
+ # security_id: "11536",
254
+ # quantity: 5
255
+ # )
256
+ # puts "Order ID: #{order.order_id}"
257
+ # puts "Status: #{order.order_status}"
258
+ #
259
+ # @example Place a limit order
260
+ # order = DhanHQ::Models::Order.place(
261
+ # dhan_client_id: "1000000003",
262
+ # transaction_type: "BUY",
263
+ # exchange_segment: "NSE_EQ",
264
+ # product_type: "CNC",
265
+ # order_type: "LIMIT",
266
+ # validity: "DAY",
267
+ # security_id: "11536",
268
+ # quantity: 10,
269
+ # price: 1500.0
270
+ # )
271
+ #
272
+ # @example Place a stop-loss order
273
+ # order = DhanHQ::Models::Order.place(
274
+ # dhan_client_id: "1000000003",
275
+ # transaction_type: "SELL",
276
+ # exchange_segment: "NSE_EQ",
277
+ # product_type: "INTRADAY",
278
+ # order_type: "STOP_LOSS",
279
+ # validity: "DAY",
280
+ # security_id: "11536",
281
+ # quantity: 5,
282
+ # price: 1480.0,
283
+ # trigger_price: 1490.0
284
+ # )
285
+ #
286
+ # @raise [DhanHQ::ValidationError] If validation fails for any parameter
85
287
  def place(params)
86
288
  normalized_params = snake_case(params)
289
+ # Auto-inject dhan_client_id from configuration if not provided
290
+ normalized_params[:dhan_client_id] ||= DhanHQ.configuration.client_id if DhanHQ.configuration.client_id
87
291
  validate_params!(normalized_params, DhanHQ::Contracts::PlaceOrderContract)
88
292
 
89
293
  response = resource.create(camelize_keys(normalized_params))
@@ -94,11 +298,29 @@ module DhanHQ
94
298
  end
95
299
 
96
300
  ##
97
- # AR-like create: new => valid? => save => resource.create
98
- # But we can also define a class method if we want direct:
99
- # Order.create(order_params)
301
+ # ActiveRecord-style create method.
302
+ #
303
+ # Creates a new Order instance, validates it, and saves it. This provides an
304
+ # ActiveRecord-like interface: `Order.create(params)` or `Order.new(params).save`.
305
+ #
306
+ # @param params [Hash{Symbol => String, Integer, Float, Boolean}] Order creation parameters.
307
+ # See {place} for parameter details.
308
+ #
309
+ # @return [Order] Order instance. Note that validation failures may result in an
310
+ # invalid order that couldn't be saved.
311
+ #
312
+ # @example Create order using AR-style method
313
+ # order = DhanHQ::Models::Order.create(
314
+ # dhan_client_id: "1000000003",
315
+ # transaction_type: "BUY",
316
+ # exchange_segment: "NSE_EQ",
317
+ # product_type: "INTRADAY",
318
+ # order_type: "MARKET",
319
+ # validity: "DAY",
320
+ # security_id: "11536",
321
+ # quantity: 5
322
+ # )
100
323
  #
101
- # For the typical usage "Order.new(...).save", we rely on #save below.
102
324
  def create(params)
103
325
  order = new(params) # build it
104
326
  return order unless order.valid? # run place order contract?
@@ -108,13 +330,48 @@ module DhanHQ
108
330
  end
109
331
  end
110
332
 
111
- # Modify the order while preserving existing attributes
333
+ ##
334
+ # Modifies a pending order in the orderbook.
112
335
  #
113
- # @param new_params [Hash]
114
- # @return [Order, nil]
336
+ # Allows modification of price, quantity, order type, and validity for pending orders.
337
+ # The method preserves existing attributes and only updates the fields specified in
338
+ # `new_params`. Only modifiable fields are included in the update request.
339
+ #
340
+ # @param new_params [Hash{Symbol => String, Integer, Float}] Fields to modify.
341
+ # Only modifiable fields are accepted:
342
+ # @option new_params [String] :order_type (optional) New order type.
343
+ # Valid values: "LIMIT", "MARKET", "STOP_LOSS", "STOP_LOSS_MARKET"
344
+ # @option new_params [Integer] :quantity (optional) New quantity to modify
345
+ # @option new_params [Float] :price (optional) New price to modify
346
+ # @option new_params [Float] :trigger_price (optional) New trigger price for SL-M and SL-L orders
347
+ # @option new_params [Integer] :disclosed_quantity (optional) New disclosed quantity
348
+ # @option new_params [String] :validity (optional) New validity. "DAY" or "IOC"
349
+ # @option new_params [String] :leg_name (optional) Leg identification for BO & CO orders.
350
+ # Valid values: "ENTRY_LEG", "TARGET_LEG", "STOP_LOSS_LEG"
351
+ #
352
+ # @return [Order, ErrorObject] Updated Order object on success, ErrorObject on failure.
353
+ # The order's attributes are updated with the latest values from the API response.
354
+ #
355
+ # @example Modify order price and quantity
356
+ # order = DhanHQ::Models::Order.find("112111182045")
357
+ # order.modify(price: 3345.8, quantity: 40)
358
+ # puts "Modified: #{order.order_status}"
359
+ #
360
+ # @example Modify order validity
361
+ # order = DhanHQ::Models::Order.find("112111182045")
362
+ # order.modify(validity: "IOC")
363
+ #
364
+ # @raise [RuntimeError] If order ID is missing
365
+ # @raise [DhanHQ::ValidationError] If validation fails for any parameter
115
366
  def modify(new_params)
116
367
  raise "Order ID is required to modify an order" unless id
117
368
 
369
+ # Log warning for invalid states but still attempt API call (let API handle validation)
370
+ # This maintains backward compatibility - API will return appropriate error
371
+ if order_status && %w[TRADED CANCELLED EXPIRED CLOSED].include?(order_status)
372
+ DhanHQ.logger&.warn("[DhanHQ::Models::Order] Attempting to modify order #{id} in #{order_status} state - API will reject")
373
+ end
374
+
118
375
  base_payload = attributes.merge(new_params)
119
376
  normalized_payload = snake_case(base_payload).merge(order_id: id)
120
377
  filtered_payload = normalized_payload.each_with_object({}) do |(key, value), memo|
@@ -122,7 +379,7 @@ module DhanHQ
122
379
  memo[symbolized_key] = value if MODIFIABLE_FIELDS.include?(symbolized_key)
123
380
  end
124
381
  filtered_payload[:order_id] ||= id
125
- filtered_payload[:dhan_client_id] ||= attributes[:dhan_client_id]
382
+ filtered_payload[:dhan_client_id] ||= attributes[:dhan_client_id] || DhanHQ.configuration&.client_id
126
383
 
127
384
  cleaned_payload = filtered_payload.compact
128
385
  formatted_payload = camelize_keys(cleaned_payload)
@@ -138,9 +395,23 @@ module DhanHQ
138
395
  self
139
396
  end
140
397
 
141
- # Cancel the order
398
+ ##
399
+ # Cancels a pending order in the orderbook.
400
+ #
401
+ # Cancels an existing order using its order ID. Returns true if the cancellation
402
+ # was successful (order status becomes "CANCELLED").
403
+ #
404
+ # @return [Boolean] true if cancellation succeeds, false otherwise
405
+ #
406
+ # @example Cancel a pending order
407
+ # order = DhanHQ::Models::Order.find("112111182045")
408
+ # if order.cancel
409
+ # puts "Order cancelled successfully"
410
+ # else
411
+ # puts "Failed to cancel order"
412
+ # end
142
413
  #
143
- # @return [Boolean]
414
+ # @raise [RuntimeError] If order ID is missing
144
415
  def cancel
145
416
  raise "Order ID is required to cancel an order" unless id
146
417
 
@@ -148,9 +419,21 @@ module DhanHQ
148
419
  response["orderStatus"] == "CANCELLED"
149
420
  end
150
421
 
151
- # Fetch the latest details of the order
422
+ ##
423
+ # Fetches the latest details and status of the order.
424
+ #
425
+ # Refreshes the order object with the most current information from the API,
426
+ # including updated status, trade information, and any other changes.
427
+ #
428
+ # @return [Order, nil] Updated Order object with latest details, nil if order not found
429
+ #
430
+ # @example Refresh order status
431
+ # order = DhanHQ::Models::Order.find("112111182198")
432
+ # order.refresh
433
+ # puts "Updated status: #{order.order_status}"
434
+ # puts "Filled: #{order.filled_qty}/#{order.quantity}"
152
435
  #
153
- # @return [Order, nil]
436
+ # @raise [RuntimeError] If order ID is missing
154
437
  def refresh
155
438
  raise "Order ID is required to refresh an order" unless id
156
439
 
@@ -158,49 +441,110 @@ module DhanHQ
158
441
  end
159
442
 
160
443
  ##
161
- # This is how we figure out if it's an existing record or not:
444
+ # Checks if this is a new (unsaved) order record.
445
+ #
446
+ # Determines whether the order has been saved to the API or is a new instance
447
+ # that hasn't been placed yet. Used internally to decide between create and update operations.
448
+ #
449
+ # @return [Boolean] true if order_id is nil or empty (new record), false otherwise
450
+ #
451
+ # @api private
162
452
  def new_record?
163
453
  order_id.nil? || order_id.to_s.empty?
164
454
  end
165
455
 
166
456
  ##
167
- # The ID used for resource calls
457
+ # Returns the order ID used for resource calls.
458
+ #
459
+ # Provides a consistent identifier method for API operations. This is an alias
460
+ # for `order_id` that follows ActiveRecord conventions.
461
+ #
462
+ # @return [String, nil] Order ID if present, nil otherwise
463
+ #
464
+ # @api private
168
465
  def id
169
466
  order_id
170
467
  end
171
468
 
172
469
  ##
173
- # Save: If new_record?, do resource.create
174
- # else resource.update
470
+ # Saves the order (places new or modifies existing).
471
+ #
472
+ # For new records (no order_id), places a new order via the API.
473
+ # For existing records (has order_id), modifies the order via the API.
474
+ # The order's attributes are updated with the API response after successful save.
475
+ #
476
+ # @return [Boolean] true if save succeeds, false otherwise
477
+ #
478
+ # @example Save a new order
479
+ # order = DhanHQ::Models::Order.new(
480
+ # dhan_client_id: "1000000003",
481
+ # transaction_type: "BUY",
482
+ # exchange_segment: "NSE_EQ",
483
+ # product_type: "INTRADAY",
484
+ # order_type: "MARKET",
485
+ # validity: "DAY",
486
+ # security_id: "11536",
487
+ # quantity: 5
488
+ # )
489
+ # if order.save
490
+ # puts "Order placed: #{order.order_id}"
491
+ # end
492
+ #
493
+ # @note Validation is performed before save. Invalid orders will not be saved.
175
494
  def save
176
495
  return false unless valid?
177
496
 
178
497
  if new_record?
179
498
  # PLACE ORDER
499
+ DhanHQ.logger&.info("[DhanHQ::Models::Order] Placing order: #{attributes.slice(:transaction_type,
500
+ :exchange_segment, :security_id, :quantity, :price).inspect}")
180
501
  response = self.class.resource.create(to_request_params)
181
502
  if success_response?(response) && response["orderId"]
182
503
  @attributes.merge!(normalize_keys(response))
183
504
  assign_attributes
505
+ DhanHQ.logger&.info("[DhanHQ::Models::Order] Order placed successfully: #{response["orderId"]}")
184
506
  true
185
507
  else
186
- # maybe store errors?
508
+ error_msg = response.is_a?(Hash) ? response[:errorMessage] || response[:message] || "Unknown error" : "Invalid response format"
509
+ DhanHQ.logger&.error("[DhanHQ::Models::Order] Order placement failed: #{error_msg}")
510
+ @errors = response if response.is_a?(Hash)
187
511
  false
188
512
  end
189
513
  else
190
514
  # MODIFY ORDER
515
+ DhanHQ.logger&.info("[DhanHQ::Models::Order] Modifying order #{id}: #{attributes.slice(:price, :quantity,
516
+ :order_type).inspect}")
191
517
  response = self.class.resource.update(id, to_request_params)
192
518
  if success_response?(response) && response["orderStatus"]
193
519
  @attributes.merge!(normalize_keys(response))
194
520
  assign_attributes
521
+ DhanHQ.logger&.info("[DhanHQ::Models::Order] Order modified successfully: #{id}")
195
522
  true
196
523
  else
524
+ error_msg = response.is_a?(Hash) ? response[:errorMessage] || response[:message] || "Unknown error" : "Invalid response format"
525
+ DhanHQ.logger&.error("[DhanHQ::Models::Order] Order modification failed for #{id}: #{error_msg}")
526
+ @errors = response if response.is_a?(Hash)
197
527
  false
198
528
  end
199
529
  end
200
530
  end
201
531
 
202
532
  ##
203
- # Cancel => calls resource.delete
533
+ # Cancels (destroys) the order.
534
+ #
535
+ # Cancels an existing order by calling the delete endpoint. This is an alias
536
+ # for the cancel operation following ActiveRecord conventions.
537
+ #
538
+ # @return [Boolean] true if cancellation succeeds (order status becomes "CANCELLED"),
539
+ # false otherwise
540
+ #
541
+ # @example Destroy an order
542
+ # order = DhanHQ::Models::Order.find("112111182045")
543
+ # if order.destroy
544
+ # puts "Order cancelled"
545
+ # end
546
+ #
547
+ # @note This method does nothing for new (unsaved) orders.
204
548
  def destroy
205
549
  return false if new_record?
206
550
 
@@ -215,8 +559,38 @@ module DhanHQ
215
559
  alias delete destroy
216
560
 
217
561
  ##
218
- # Slicing (optional)
219
- # If you want an AR approach:
562
+ # Slices an order into multiple legs to place orders over freeze limit quantity.
563
+ #
564
+ # This API helps you slice your order request into multiple orders to allow you
565
+ # to place over freeze limit quantity for F&O instruments. Returns an array of
566
+ # orders created from the slice operation.
567
+ #
568
+ # @param params [Hash{Symbol => String, Integer, Float, Boolean}] Order parameters for slicing.
569
+ # Same parameters as {place}, but quantity can exceed freeze limits as it will be
570
+ # automatically split into multiple orders.
571
+ #
572
+ # @return [Array<Hash>] Array of order objects created from the slice operation.
573
+ # Each hash contains:
574
+ # - **:order_id** [String] Order-specific identification generated by Dhan
575
+ # - **:order_status** [String] Order status. Valid values: "TRANSIT", "PENDING",
576
+ # "REJECTED", "CANCELLED", "TRADED", "EXPIRED", "CONFIRM"
577
+ #
578
+ # @example Slice a large order
579
+ # order = DhanHQ::Models::Order.find("112111182045")
580
+ # sliced_orders = order.slice_order(
581
+ # dhan_client_id: "1000000003",
582
+ # transaction_type: "BUY",
583
+ # exchange_segment: "NSE_FNO",
584
+ # product_type: "INTRADAY",
585
+ # order_type: "MARKET",
586
+ # validity: "DAY",
587
+ # security_id: "49081",
588
+ # quantity: 50000 # Will be split into multiple orders
589
+ # )
590
+ # puts "Created #{sliced_orders.count} orders"
591
+ #
592
+ # @raise [RuntimeError] If order ID is missing
593
+ # @raise [DhanHQ::ValidationError] If validation fails for any parameter
220
594
  def slice_order(params)
221
595
  raise "Order ID is required to slice an order" unless id
222
596
 
@@ -229,8 +603,16 @@ module DhanHQ
229
603
  end
230
604
 
231
605
  ##
232
- # Because we have two separate contracts: place vs. modify
233
- # We can do something like:
606
+ # Returns the appropriate validation contract based on order state.
607
+ #
608
+ # For new records, returns PlaceOrderContract. For existing records, returns
609
+ # ModifyOrderContract. This allows the same validation logic to be used for
610
+ # both creating and modifying orders.
611
+ #
612
+ # @return [DhanHQ::Contracts::PlaceOrderContract, DhanHQ::Contracts::ModifyOrderContract]
613
+ # The appropriate validation contract based on whether this is a new or existing order
614
+ #
615
+ # @api private
234
616
  def validation_contract
235
617
  new_record? ? DhanHQ::Contracts::PlaceOrderContract.new : DhanHQ::Contracts::ModifyOrderContract.new
236
618
  end
@@ -5,7 +5,6 @@ module DhanHQ
5
5
  ##
6
6
  # Represents a real-time order update received via WebSocket
7
7
  # Parses and provides access to all order update fields as per DhanHQ API documentation
8
- # rubocop:disable Metrics/ClassLength
9
8
  class OrderUpdate < BaseModel
10
9
  # All order update attributes as per API documentation
11
10
  attributes :exchange, :segment, :source, :security_id, :client_id,
@@ -206,7 +205,6 @@ module DhanHQ
206
205
 
207
206
  ##
208
207
  # Status summary for logging/debugging
209
- # rubocop:disable Metrics/MethodLength
210
208
  def status_summary
211
209
  {
212
210
  order_no: order_no,
@@ -222,7 +220,6 @@ module DhanHQ
222
220
  super_order: super_order?
223
221
  }
224
222
  end
225
- # rubocop:enable Metrics/MethodLength
226
223
 
227
224
  ##
228
225
  # Convert to hash for serialization
@@ -230,6 +227,5 @@ module DhanHQ
230
227
  @attributes.dup
231
228
  end
232
229
  end
233
- # rubocop:enable Metrics/ClassLength
234
230
  end
235
231
  end