nordnet_api/resources/orders.rs
1//! Resource methods for the `orders` API group.
2//! # Operations
3//! | Method | Op | Path |
4//! |--------|----|------|
5//! | GET | `list_orders` | `/accounts/{accid}/orders` |
6//! | POST | `place_order` | `/accounts/{accid}/orders` |
7//! | PUT | `modify_order` | `/accounts/{accid}/orders/{order_id}` |
8//! | PUT | `activate_order` | `/accounts/{accid}/orders/{order_id}/activate` |
9//! | DELETE | `cancel_order` | `/accounts/{accid}/orders/{order_id}` |
10//!
11//! ## 204 No Content (`list_orders`)
12//! `GET /accounts/{accid}/orders` is documented to return 204 with no
13//! body when there are no orders. The base [`Client::get`] surfaces an
14//! empty body as [`Error::Decode`]; [`Client::list_orders`] maps that
15//! specific case to an empty `Vec`, mirroring the
16//! [`Client::get_tradable_info`] precedent.
17//!
18//! ## Body-less PUT (`activate_order`)
19//! `activate_order` has no documented request body — we use
20//! [`Client::put_empty`] so the wire request omits `Content-Type` and
21//! sends a zero-length payload (precedent: `login::refresh_session`).
22//!
23//! ## Multi-account / multi-order paths
24//! The Nordnet API path slots accept comma-separated lists of IDs (e.g.
25//! `/accounts/1,2,3/orders`). The typed surface here stays single-id by
26//! default — Phase 4 (or callers) can build comma lists into a `String`
27//! and supply it via a future helper if needed.
28
29use crate::client::Client;
30use crate::error::Error;
31use nordnet_model::ids::{AccountId, OrderId};
32use nordnet_model::models::orders::{ModifyOrderRequest, Order, OrderReply, PlaceOrderRequest};
33
34impl Client {
35 /// `GET /accounts/{accid}/orders` — Returns all orders belonging to
36 /// the given account.
37 ///
38 /// # Parameters
39 ///
40 /// - `accid` — the account identifier.
41 /// - `deleted` — optional. When `Some(true)`, the response includes
42 /// orders that were deleted today. Defaults to `false` server-side.
43 ///
44 /// Returns an empty `Vec` when the API responds with 204 No Content.
45 ///
46 /// # Errors
47 ///
48 /// Returns [`Error::BadRequest`] (400), [`Error::Unauthorized`] (401),
49 /// [`Error::Forbidden`] (403; documented with empty body),
50 /// [`Error::TooManyRequests`] (429), or
51 /// [`Error::ServiceUnavailable`] (503) per the docs.
52 #[doc(alias = "GET /accounts/{accid}/orders")]
53 pub async fn list_orders(
54 &self,
55 accid: AccountId,
56 deleted: Option<bool>,
57 ) -> Result<Vec<Order>, Error> {
58 let path = match deleted {
59 Some(d) => format!("/accounts/{accid}/orders?deleted={d}"),
60 None => format!("/accounts/{accid}/orders"),
61 };
62 match self.get::<Vec<Order>>(&path).await {
63 Ok(v) => Ok(v),
64 // 204 No Content — base client surfaces this as a Decode error
65 // over an empty body. Map it to an empty Vec.
66 Err(Error::Decode { ref body, .. }) if body.trim().is_empty() => Ok(vec![]),
67 Err(e) => Err(e),
68 }
69 }
70
71 /// `POST /accounts/{accid}/orders` — Enters a new order for the
72 /// tradable identified by the given market ID + tradable ID.
73 ///
74 /// The Nordnet docs mark every body parameter as Swagger 2.0
75 /// `FormData`, so the request is sent as
76 /// `application/x-www-form-urlencoded` via
77 /// [`Client::post_form`]. JSON bodies are silently rejected by the
78 /// live endpoint. See `` §"Locked decisions" item 9.
79 ///
80 /// # Errors
81 ///
82 /// Returns [`Error::BadRequest`] (400), [`Error::Unauthorized`] (401),
83 /// [`Error::Forbidden`] (403), [`Error::TooManyRequests`] (429), or
84 /// [`Error::ServiceUnavailable`] (503) per the docs.
85 #[doc(alias = "POST /accounts/{accid}/orders")]
86 pub async fn place_order(
87 &self,
88 accid: AccountId,
89 request: &PlaceOrderRequest,
90 ) -> Result<OrderReply, Error> {
91 let path = format!("/accounts/{accid}/orders");
92 self.post_form(&path, request).await
93 }
94
95 /// `PUT /accounts/{accid}/orders/{order_id}` — Modifies the price
96 /// and/or volume of an order.
97 ///
98 /// The Nordnet docs mark every body parameter as Swagger 2.0
99 /// `FormData`, so the request is sent as
100 /// `application/x-www-form-urlencoded` via [`Client::put_form`].
101 /// See `` §"Locked decisions" item 9.
102 ///
103 /// # Errors
104 ///
105 /// Returns [`Error::BadRequest`] (400), [`Error::Unauthorized`] (401),
106 /// [`Error::Forbidden`] (403),
107 /// [`Error::UnexpectedStatus`] (404; documented "Order not found"),
108 /// [`Error::TooManyRequests`] (429), or
109 /// [`Error::ServiceUnavailable`] (503) per the docs.
110 #[doc(alias = "PUT /accounts/{accid}/orders/{order_id}")]
111 pub async fn modify_order(
112 &self,
113 accid: AccountId,
114 order_id: OrderId,
115 request: &ModifyOrderRequest,
116 ) -> Result<OrderReply, Error> {
117 let path = format!("/accounts/{accid}/orders/{order_id}");
118 self.put_form(&path, request).await
119 }
120
121 /// `PUT /accounts/{accid}/orders/{order_id}/activate` — Activates
122 /// an inactive order. Sends a body-less `PUT` per the docs.
123 ///
124 /// The doc parameter table notes that `order_id` accepts a
125 /// comma-separated list and the response is therefore an array of
126 /// [`OrderReply`]. The single-id call still receives an array (of
127 /// length one).
128 ///
129 /// # Errors
130 ///
131 /// Returns [`Error::BadRequest`] (400), [`Error::Unauthorized`] (401),
132 /// [`Error::Forbidden`] (403), [`Error::TooManyRequests`] (429), or
133 /// [`Error::ServiceUnavailable`] (503) per the docs.
134 #[doc(alias = "PUT /accounts/{accid}/orders/{order_id}/activate")]
135 pub async fn activate_order(
136 &self,
137 accid: AccountId,
138 order_id: OrderId,
139 ) -> Result<Vec<OrderReply>, Error> {
140 let path = format!("/accounts/{accid}/orders/{order_id}/activate");
141 self.put_empty(&path).await
142 }
143
144 /// `DELETE /accounts/{accid}/orders/{order_id}` — Deletes an order.
145 ///
146 /// # Errors
147 ///
148 /// Returns [`Error::BadRequest`] (400), [`Error::Unauthorized`] (401),
149 /// [`Error::Forbidden`] (403), [`Error::TooManyRequests`] (429), or
150 /// [`Error::ServiceUnavailable`] (503) per the docs.
151 #[doc(alias = "DELETE /accounts/{accid}/orders/{order_id}")]
152 pub async fn cancel_order(
153 &self,
154 accid: AccountId,
155 order_id: OrderId,
156 ) -> Result<OrderReply, Error> {
157 let path = format!("/accounts/{accid}/orders/{order_id}");
158 self.delete(&path).await
159 }
160}