Skip to main content

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}