DhanHQ 2.1.8 → 2.1.10

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.
@@ -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
82
241
  #
83
- # @param params [Hash] Order parameters
84
- # @return [Order]
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.
244
+ #
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,10 +330,39 @@ 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.
335
+ #
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.
112
354
  #
113
- # @param new_params [Hash]
114
- # @return [Order, nil]
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
 
@@ -138,9 +389,23 @@ module DhanHQ
138
389
  self
139
390
  end
140
391
 
141
- # Cancel the order
392
+ ##
393
+ # Cancels a pending order in the orderbook.
142
394
  #
143
- # @return [Boolean]
395
+ # Cancels an existing order using its order ID. Returns true if the cancellation
396
+ # was successful (order status becomes "CANCELLED").
397
+ #
398
+ # @return [Boolean] true if cancellation succeeds, false otherwise
399
+ #
400
+ # @example Cancel a pending order
401
+ # order = DhanHQ::Models::Order.find("112111182045")
402
+ # if order.cancel
403
+ # puts "Order cancelled successfully"
404
+ # else
405
+ # puts "Failed to cancel order"
406
+ # end
407
+ #
408
+ # @raise [RuntimeError] If order ID is missing
144
409
  def cancel
145
410
  raise "Order ID is required to cancel an order" unless id
146
411
 
@@ -148,9 +413,21 @@ module DhanHQ
148
413
  response["orderStatus"] == "CANCELLED"
149
414
  end
150
415
 
151
- # Fetch the latest details of the order
416
+ ##
417
+ # Fetches the latest details and status of the order.
152
418
  #
153
- # @return [Order, nil]
419
+ # Refreshes the order object with the most current information from the API,
420
+ # including updated status, trade information, and any other changes.
421
+ #
422
+ # @return [Order, nil] Updated Order object with latest details, nil if order not found
423
+ #
424
+ # @example Refresh order status
425
+ # order = DhanHQ::Models::Order.find("112111182198")
426
+ # order.refresh
427
+ # puts "Updated status: #{order.order_status}"
428
+ # puts "Filled: #{order.filled_qty}/#{order.quantity}"
429
+ #
430
+ # @raise [RuntimeError] If order ID is missing
154
431
  def refresh
155
432
  raise "Order ID is required to refresh an order" unless id
156
433
 
@@ -158,20 +435,56 @@ module DhanHQ
158
435
  end
159
436
 
160
437
  ##
161
- # This is how we figure out if it's an existing record or not:
438
+ # Checks if this is a new (unsaved) order record.
439
+ #
440
+ # Determines whether the order has been saved to the API or is a new instance
441
+ # that hasn't been placed yet. Used internally to decide between create and update operations.
442
+ #
443
+ # @return [Boolean] true if order_id is nil or empty (new record), false otherwise
444
+ #
445
+ # @api private
162
446
  def new_record?
163
447
  order_id.nil? || order_id.to_s.empty?
164
448
  end
165
449
 
166
450
  ##
167
- # The ID used for resource calls
451
+ # Returns the order ID used for resource calls.
452
+ #
453
+ # Provides a consistent identifier method for API operations. This is an alias
454
+ # for `order_id` that follows ActiveRecord conventions.
455
+ #
456
+ # @return [String, nil] Order ID if present, nil otherwise
457
+ #
458
+ # @api private
168
459
  def id
169
460
  order_id
170
461
  end
171
462
 
172
463
  ##
173
- # Save: If new_record?, do resource.create
174
- # else resource.update
464
+ # Saves the order (places new or modifies existing).
465
+ #
466
+ # For new records (no order_id), places a new order via the API.
467
+ # For existing records (has order_id), modifies the order via the API.
468
+ # The order's attributes are updated with the API response after successful save.
469
+ #
470
+ # @return [Boolean] true if save succeeds, false otherwise
471
+ #
472
+ # @example Save a new order
473
+ # order = DhanHQ::Models::Order.new(
474
+ # dhan_client_id: "1000000003",
475
+ # transaction_type: "BUY",
476
+ # exchange_segment: "NSE_EQ",
477
+ # product_type: "INTRADAY",
478
+ # order_type: "MARKET",
479
+ # validity: "DAY",
480
+ # security_id: "11536",
481
+ # quantity: 5
482
+ # )
483
+ # if order.save
484
+ # puts "Order placed: #{order.order_id}"
485
+ # end
486
+ #
487
+ # @note Validation is performed before save. Invalid orders will not be saved.
175
488
  def save
176
489
  return false unless valid?
177
490
 
@@ -200,7 +513,21 @@ module DhanHQ
200
513
  end
201
514
 
202
515
  ##
203
- # Cancel => calls resource.delete
516
+ # Cancels (destroys) the order.
517
+ #
518
+ # Cancels an existing order by calling the delete endpoint. This is an alias
519
+ # for the cancel operation following ActiveRecord conventions.
520
+ #
521
+ # @return [Boolean] true if cancellation succeeds (order status becomes "CANCELLED"),
522
+ # false otherwise
523
+ #
524
+ # @example Destroy an order
525
+ # order = DhanHQ::Models::Order.find("112111182045")
526
+ # if order.destroy
527
+ # puts "Order cancelled"
528
+ # end
529
+ #
530
+ # @note This method does nothing for new (unsaved) orders.
204
531
  def destroy
205
532
  return false if new_record?
206
533
 
@@ -215,8 +542,38 @@ module DhanHQ
215
542
  alias delete destroy
216
543
 
217
544
  ##
218
- # Slicing (optional)
219
- # If you want an AR approach:
545
+ # Slices an order into multiple legs to place orders over freeze limit quantity.
546
+ #
547
+ # This API helps you slice your order request into multiple orders to allow you
548
+ # to place over freeze limit quantity for F&O instruments. Returns an array of
549
+ # orders created from the slice operation.
550
+ #
551
+ # @param params [Hash{Symbol => String, Integer, Float, Boolean}] Order parameters for slicing.
552
+ # Same parameters as {place}, but quantity can exceed freeze limits as it will be
553
+ # automatically split into multiple orders.
554
+ #
555
+ # @return [Array<Hash>] Array of order objects created from the slice operation.
556
+ # Each hash contains:
557
+ # - **:order_id** [String] Order-specific identification generated by Dhan
558
+ # - **:order_status** [String] Order status. Valid values: "TRANSIT", "PENDING",
559
+ # "REJECTED", "CANCELLED", "TRADED", "EXPIRED", "CONFIRM"
560
+ #
561
+ # @example Slice a large order
562
+ # order = DhanHQ::Models::Order.find("112111182045")
563
+ # sliced_orders = order.slice_order(
564
+ # dhan_client_id: "1000000003",
565
+ # transaction_type: "BUY",
566
+ # exchange_segment: "NSE_FNO",
567
+ # product_type: "INTRADAY",
568
+ # order_type: "MARKET",
569
+ # validity: "DAY",
570
+ # security_id: "49081",
571
+ # quantity: 50000 # Will be split into multiple orders
572
+ # )
573
+ # puts "Created #{sliced_orders.count} orders"
574
+ #
575
+ # @raise [RuntimeError] If order ID is missing
576
+ # @raise [DhanHQ::ValidationError] If validation fails for any parameter
220
577
  def slice_order(params)
221
578
  raise "Order ID is required to slice an order" unless id
222
579
 
@@ -229,8 +586,16 @@ module DhanHQ
229
586
  end
230
587
 
231
588
  ##
232
- # Because we have two separate contracts: place vs. modify
233
- # We can do something like:
589
+ # Returns the appropriate validation contract based on order state.
590
+ #
591
+ # For new records, returns PlaceOrderContract. For existing records, returns
592
+ # ModifyOrderContract. This allows the same validation logic to be used for
593
+ # both creating and modifying orders.
594
+ #
595
+ # @return [DhanHQ::Contracts::PlaceOrderContract, DhanHQ::Contracts::ModifyOrderContract]
596
+ # The appropriate validation contract based on whether this is a new or existing order
597
+ #
598
+ # @api private
234
599
  def validation_contract
235
600
  new_record? ? DhanHQ::Contracts::PlaceOrderContract.new : DhanHQ::Contracts::ModifyOrderContract.new
236
601
  end