1use std::sync::Arc;
4
5use axum::extract::{Extension, Json, State};
6use axum::http::{HeaderMap, StatusCode};
7use serde_json::Value;
8
9use futu_auth::{CheckCtx, KeyRecord};
10use futu_core::proto_id;
11use futu_proto::trd_modify_order;
12use futu_proto::trd_place_combo_order;
13use futu_proto::trd_place_order;
14use futu_proto::trd_reconfirm_order;
15
16use super::ApiResult;
17use super::card_num::{
18 extract_and_resolve_card_num_into_acc_id, normalize_and_resolve_card_num_for_route,
19};
20use super::validation::{
21 read_handler_acc_id_check, rest_handler_limit_check, trd_market_str,
22 validate_header_trd_env_present, validate_header_trd_market_write,
23};
24use crate::adapter::{self, RestState};
25
26pub async fn place_order(
33 State(state): State<RestState>,
34 rec: Option<Extension<Arc<KeyRecord>>>,
35 headers: HeaderMap,
36 Json(mut body): Json<Value>,
37) -> ApiResult {
38 crate::adapter::normalize_json_keys_snake_case(&mut body);
40 let rec_ref_for_card_num = rec.as_ref().map(|Extension(r)| r.as_ref());
50 extract_and_resolve_card_num_into_acc_id(
51 &state,
52 rec_ref_for_card_num,
53 &mut body,
54 "/api/order",
55 )?;
56 validate_header_trd_market_write(&body, "/api/order")?;
59 validate_header_trd_env_present(&body, "/api/order")?;
61 if let Some(Extension(rec)) = rec {
62 match serde_json::from_value::<trd_place_order::Request>(body.clone()) {
64 Ok(parsed) => rest_handler_limit_check(&state, &rec, &parsed)?,
65 Err(_) => {
66 }
69 }
70 }
71 let idem_key = headers
74 .get("idempotency-key")
75 .and_then(|v| v.to_str().ok())
76 .map(|s| s.to_string());
77 adapter::proto_request_with_idempotency::<trd_place_order::Request, trd_place_order::Response>(
78 &state,
79 proto_id::TRD_PLACE_ORDER,
80 Some(body),
81 idem_key,
82 )
83 .await
84}
85
86pub async fn place_combo_order(
88 State(state): State<RestState>,
89 rec: Option<Extension<Arc<KeyRecord>>>,
90 headers: HeaderMap,
91 Json(mut body): Json<Value>,
92) -> ApiResult {
93 crate::adapter::normalize_json_keys_snake_case(&mut body);
94 let rec_ref_for_card_num = rec.as_ref().map(|Extension(r)| r.as_ref());
95 extract_and_resolve_card_num_into_acc_id(
96 &state,
97 rec_ref_for_card_num,
98 &mut body,
99 "/api/combo-order",
100 )?;
101 validate_header_trd_market_write(&body, "/api/combo-order")?;
102 validate_header_trd_env_present(&body, "/api/combo-order")?;
103 if let Some(Extension(rec)) = rec
104 && let Ok(parsed) = serde_json::from_value::<trd_place_combo_order::Request>(body.clone())
105 {
106 let market = trd_market_str(parsed.c2s.header.trd_market);
107 let symbol = parsed
108 .c2s
109 .combo_legs
110 .first()
111 .map(|leg| leg.security.code.as_str())
112 .filter(|code| !market.is_empty() && !code.is_empty())
113 .map(|code| format!("{market}.{code}"))
114 .unwrap_or_default();
115 let ctx = CheckCtx {
116 market: market.to_string(),
117 symbol,
118 order_value: parsed.c2s.price.map(|price| price * parsed.c2s.qty),
119 trd_side: None,
120 acc_id: Some(parsed.c2s.header.acc_id),
121 mutation_no_exposure: false,
122 currency: futu_auth::market_to_currency(market).map(String::from),
123 };
124 let now = chrono::Utc::now();
125 let outcome = state
126 .counters
127 .check_full_skip_rate(&rec.id, &rec.limits(), &ctx, now);
128 if let Some(reason) = outcome.reason() {
129 futu_auth::audit::reject(
130 "rest",
131 "/api/combo-order",
132 &rec.id,
133 &format!("limit: {reason}"),
134 );
135 let status = StatusCode::from_u16(outcome.http_status_code())
136 .unwrap_or(StatusCode::TOO_MANY_REQUESTS);
137 return Err((
138 status,
139 Json(serde_json::json!({
140 "error": format!("limit check failed: {reason}")
141 })),
142 ));
143 }
144 }
145 let idem_key = headers
146 .get("idempotency-key")
147 .and_then(|v| v.to_str().ok())
148 .map(|s| s.to_string());
149 adapter::proto_request_with_idempotency::<
150 trd_place_combo_order::Request,
151 trd_place_combo_order::Response,
152 >(
153 &state,
154 proto_id::TRD_PLACE_COMBO_ORDER,
155 Some(body),
156 idem_key,
157 )
158 .await
159}
160
161pub async fn modify_order(
181 State(state): State<RestState>,
182 rec: Option<Extension<Arc<KeyRecord>>>,
183 headers: HeaderMap,
184 Json(mut body): Json<Value>,
185) -> ApiResult {
186 crate::adapter::normalize_json_keys_snake_case(&mut body);
188 let rec_ref_for_card_num = rec.as_ref().map(|Extension(r)| r.as_ref());
192 extract_and_resolve_card_num_into_acc_id(
193 &state,
194 rec_ref_for_card_num,
195 &mut body,
196 "/api/modify-order",
197 )?;
198 validate_header_trd_market_write(&body, "/api/modify-order")?;
203 validate_header_trd_env_present(&body, "/api/modify-order")?;
205 if let Some(Extension(rec)) = rec
206 && let Ok(parsed) = serde_json::from_value::<trd_modify_order::Request>(body.clone())
207 {
208 let market = trd_market_str(parsed.c2s.header.trd_market);
209 const MODIFY_OP_NORMAL: i32 = 1;
213 let (order_value, mutation_no_exposure) = if parsed.c2s.modify_order_op == MODIFY_OP_NORMAL
214 {
215 let v = match (parsed.c2s.qty, parsed.c2s.price) {
216 (Some(q), Some(pr)) => Some(q * pr),
217 _ => None, };
219 (v, false)
220 } else {
221 (None, true)
222 };
223 let ctx = CheckCtx {
224 market: market.to_string(),
225 symbol: String::new(),
226 order_value,
227 trd_side: None,
228 acc_id: Some(parsed.c2s.header.acc_id), mutation_no_exposure,
230 currency: futu_auth::market_to_currency(market).map(String::from),
232 };
233 let now = chrono::Utc::now();
234 let outcome = state
236 .counters
237 .check_full_skip_rate(&rec.id, &rec.limits(), &ctx, now);
238 if let Some(reason) = outcome.reason() {
239 futu_auth::audit::reject(
240 "rest",
241 "/api/modify-order",
242 &rec.id,
243 &format!("limit: {reason}"),
244 );
245 let status = StatusCode::from_u16(outcome.http_status_code())
246 .unwrap_or(StatusCode::TOO_MANY_REQUESTS);
247 return Err((
248 status,
249 Json(serde_json::json!({
250 "error": format!("limit check failed: {reason}")
251 })),
252 ));
253 }
254 }
255 let idem_key = headers
256 .get("idempotency-key")
257 .and_then(|v| v.to_str().ok())
258 .map(|s| s.to_string());
259 adapter::proto_request_with_idempotency::<trd_modify_order::Request, trd_modify_order::Response>(
260 &state,
261 proto_id::TRD_MODIFY_ORDER,
262 Some(body),
263 idem_key,
264 )
265 .await
266}
267
268pub async fn reconfirm_order(
276 State(state): State<RestState>,
277 rec: Option<Extension<Arc<KeyRecord>>>,
278 Json(mut body): Json<Value>,
279) -> ApiResult {
280 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, "/api/reconfirm-order")?;
281 read_handler_acc_id_check(
282 &state,
283 rec.as_deref().map(|r| r.as_ref()),
284 &body,
285 "/api/reconfirm-order",
286 )?;
287 validate_header_trd_market_write(&body, "/api/reconfirm-order")?;
290 validate_header_trd_env_present(&body, "/api/reconfirm-order")?;
292 adapter::proto_request::<trd_reconfirm_order::Request, trd_reconfirm_order::Response>(
293 &state,
294 proto_id::TRD_RECONFIRM_ORDER,
295 Some(body),
296 )
297 .await
298}