1use std::sync::Arc;
4
5use axum::Json;
6use axum::extract::{Extension, State};
7use axum::http::StatusCode;
8use serde_json::Value;
9
10use futu_auth::{CheckCtx, KeyRecord};
11
12use crate::adapter::{self, RestState};
13
14use super::card_num::{
15 extract_and_resolve_card_num_into_acc_id, normalize_and_resolve_card_num_for_route,
16};
17use super::validation::{
18 authorize_trade_write, read_handler_acc_id_check, validate_header_trd_env_present,
19 validate_header_trd_market, validate_header_trd_market_write,
20};
21use super::write_pipeline::limit_reject_response;
22use super::{ApiResult, RawApiResult};
23
24fn invalid_context(endpoint: &'static str) -> (StatusCode, Json<Value>) {
25 (
26 StatusCode::BAD_REQUEST,
27 Json(serde_json::json!({
28 "ret_type": -1,
29 "ret_msg": "algorithm operation or decimal limit context is invalid",
30 "endpoint": endpoint,
31 })),
32 )
33}
34
35fn authorize_contexts(
36 state: &RestState,
37 record: &KeyRecord,
38 endpoint: &'static str,
39 operation: &'static str,
40 acc_id: u64,
41 idempotency_key: Option<&str>,
42 contexts: &[CheckCtx],
43) -> Result<(), (StatusCode, Json<Value>)> {
44 let receipt_operation = if contexts.iter().all(|ctx| ctx.mutation_no_exposure) {
45 format!("{operation}:transport-preflight")
46 } else {
47 operation.to_string()
48 };
49 let receipt_id = futu_auth::group_write_receipt_id(
50 &receipt_operation,
51 Some(&record.id),
52 acc_id,
53 idempotency_key.unwrap_or("confirmation"),
54 );
55 if let Err(outcome) = state.counters.check_batch_limits(
56 &record.id,
57 record,
58 contexts,
59 chrono::Utc::now(),
60 false,
61 &receipt_id,
62 ) {
63 let reason = outcome.reason().unwrap_or_else(|| "rejected".into());
64 return Err(limit_reject_response(
65 endpoint,
66 record,
67 &reason,
68 outcome.http_status_code(),
69 ));
70 }
71 Ok(())
72}
73
74pub async fn place_algo_order(
75 State(state): State<RestState>,
76 rec: Option<Extension<Arc<KeyRecord>>>,
77 Json(mut body): Json<Value>,
78) -> ApiResult {
79 const PATH: &str = "/api/trd/place-algo-order";
80 crate::adapter::normalize_json_keys_snake_case(&mut body);
81 authorize_trade_write(
82 &state,
83 rec.as_deref().map(|record| record.as_ref()),
84 &body,
85 PATH,
86 )?;
87 extract_and_resolve_card_num_into_acc_id(
88 &state,
89 rec.as_deref().map(|record| record.as_ref()),
90 &mut body,
91 PATH,
92 )?;
93 validate_header_trd_market_write(&body, PATH)?;
94 let request = adapter::decode_json_request::<futu_proto::trd_place_algo_order::Request>(
95 futu_core::proto_id::TRD_PLACE_ALGO_ORDER,
96 Some(body.clone()),
97 adapter::JsonRequestMode::GenericRest,
98 )?;
99 if let Some(Extension(record)) = rec.as_ref() {
100 let contexts = futu_auth_pipeline::body_aware::place_algo_check_contexts(&request.c2s)
101 .ok_or_else(|| invalid_context(PATH))?;
102 let idempotency_key = match request.c2s.operation.as_ref() {
103 Some(futu_proto::trd_place_algo_order::c2s::Operation::Initial(initial)) => {
104 Some(initial.idempotency_key.as_str())
105 }
106 _ => None,
107 };
108 authorize_contexts(
109 &state,
110 record,
111 PATH,
112 "PlaceAlgoOrder",
113 request.c2s.header.acc_id,
114 idempotency_key,
115 &contexts,
116 )?;
117 }
118 adapter::proto_request_with_idempotency_and_caller::<
119 futu_proto::trd_place_algo_order::Request,
120 futu_proto::trd_place_algo_order::Response,
121 >(
122 &state,
123 futu_core::proto_id::TRD_PLACE_ALGO_ORDER,
124 Some(body),
125 None,
126 rec.as_ref().map(|Extension(record)| record.id.clone()),
127 )
128 .await
129}
130
131pub async fn modify_algo_order(
132 State(state): State<RestState>,
133 rec: Option<Extension<Arc<KeyRecord>>>,
134 Json(mut body): Json<Value>,
135) -> ApiResult {
136 const PATH: &str = "/api/trd/modify-algo-order";
137 crate::adapter::normalize_json_keys_snake_case(&mut body);
138 authorize_trade_write(
139 &state,
140 rec.as_deref().map(|record| record.as_ref()),
141 &body,
142 PATH,
143 )?;
144 extract_and_resolve_card_num_into_acc_id(
145 &state,
146 rec.as_deref().map(|record| record.as_ref()),
147 &mut body,
148 PATH,
149 )?;
150 validate_header_trd_market_write(&body, PATH)?;
151 let request = adapter::decode_json_request::<futu_proto::trd_modify_algo_order::Request>(
152 futu_core::proto_id::TRD_MODIFY_ALGO_ORDER,
153 Some(body.clone()),
154 adapter::JsonRequestMode::GenericRest,
155 )?;
156 if let Some(Extension(record)) = rec.as_ref() {
157 let contexts = futu_auth_pipeline::body_aware::modify_algo_check_contexts(&request.c2s)
158 .ok_or_else(|| invalid_context(PATH))?;
159 let idempotency_key = match request.c2s.operation.as_ref() {
160 Some(futu_proto::trd_modify_algo_order::c2s::Operation::Initial(initial)) => {
161 Some(initial.idempotency_key.as_str())
162 }
163 _ => None,
164 };
165 authorize_contexts(
166 &state,
167 record,
168 PATH,
169 "ModifyAlgoOrder",
170 request.c2s.header.acc_id,
171 idempotency_key,
172 &contexts,
173 )?;
174 }
175 adapter::proto_request_with_idempotency_and_caller::<
176 futu_proto::trd_modify_algo_order::Request,
177 futu_proto::trd_modify_algo_order::Response,
178 >(
179 &state,
180 futu_core::proto_id::TRD_MODIFY_ALGO_ORDER,
181 Some(body),
182 None,
183 rec.as_ref().map(|Extension(record)| record.id.clone()),
184 )
185 .await
186}
187
188pub async fn algo_order_logs(
189 State(state): State<RestState>,
190 rec: Option<Extension<Arc<KeyRecord>>>,
191 Json(mut body): Json<Value>,
192) -> RawApiResult {
193 const PATH: &str = "/api/trd/algo-order-logs";
194 normalize_and_resolve_card_num_for_route(&state, &rec, &mut body, PATH)?;
195 validate_header_trd_market(&body, PATH)?;
196 validate_header_trd_env_present(&body, PATH)?;
197 read_handler_acc_id_check(
198 &state,
199 rec.as_deref().map(|record| record.as_ref()),
200 &body,
201 PATH,
202 )?;
203 adapter::proto_request_raw::<
204 futu_proto::trd_get_algo_order_logs::Request,
205 futu_proto::trd_get_algo_order_logs::Response,
206 >(
207 &state,
208 futu_core::proto_id::TRD_GET_ALGO_ORDER_LOGS,
209 Some(body),
210 )
211 .await
212}