1use ahash::AHashMap;
26use nautilus_core::serialization::{deserialize_decimal, deserialize_optional_decimal};
27use rust_decimal::Decimal;
28use serde::{Deserialize, Serialize};
29use ustr::Ustr;
30
31use crate::common::{
32 enums::{
33 BetDelayModel, BetStatus, BetTargetType, BetfairOrderStatus, BetfairOrderType, BetfairSide,
34 BetfairTimeInForce, CertLoginStatus, ExecutionReportErrorCode, ExecutionReportStatus,
35 GroupBy, InstructionReportErrorCode, InstructionReportStatus, MarketBettingType,
36 MarketProjection, MarketSort, OrderBy, OrderProjection, PersistenceType, PriceLadderType,
37 SortDir,
38 },
39 types::{
40 BetId, CompetitionId, CustomerOrderRef, CustomerStrategyRef, EventId, EventTypeId,
41 Handicap, MarketId, SelectionId, deserialize_optional_string_lenient,
42 deserialize_optional_u32_lenient,
43 },
44};
45
46#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
48#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
49pub enum LoginStatus {
50 Success,
51 LimitedAccess,
52 LoginRestricted,
53 Fail,
54}
55
56#[derive(Debug, Clone, Deserialize)]
58pub struct LoginResponse {
59 pub token: String,
60 pub product: String,
61 pub status: LoginStatus,
62 pub error: Option<String>,
63}
64
65#[derive(Debug, Clone, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct CertLoginResponse {
71 pub session_token: Option<String>,
72 pub login_status: CertLoginStatus,
73}
74
75#[derive(Debug, Clone, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub struct AccountDetailsResponse {
79 pub currency_code: Option<Ustr>,
80 pub first_name: Option<String>,
81 pub last_name: Option<String>,
82 pub locale_code: Option<Ustr>,
83 pub region: Option<Ustr>,
84 pub timezone: Option<Ustr>,
85 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
86 pub discount_rate: Option<Decimal>,
87 pub points_balance: Option<i64>,
88 pub country_code: Option<Ustr>,
89}
90
91#[derive(Debug, Clone, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct AccountFundsResponse {
95 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
96 pub available_to_bet_balance: Option<Decimal>,
97 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
98 pub exposure: Option<Decimal>,
99 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
100 pub retained_commission: Option<Decimal>,
101 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
102 pub exposure_limit: Option<Decimal>,
103 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
104 pub discount_rate: Option<Decimal>,
105 pub points_balance: Option<i64>,
106 pub wallet: Option<Ustr>,
107}
108
109#[derive(Debug, Clone, Default, Serialize, Deserialize)]
111pub struct TimeRange {
112 pub from: Option<String>,
113 pub to: Option<String>,
114}
115
116#[derive(Debug, Clone, Deserialize)]
118pub struct PriceSize {
119 #[serde(deserialize_with = "deserialize_decimal")]
120 pub price: Decimal,
121 #[serde(deserialize_with = "deserialize_decimal")]
122 pub size: Decimal,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct MarketVersion {
128 pub version: Option<i64>,
129}
130
131#[derive(Debug, Clone, Deserialize)]
133pub struct EventType {
134 #[serde(default, deserialize_with = "deserialize_optional_string_lenient")]
135 pub id: Option<EventTypeId>,
136 pub name: Option<Ustr>,
137}
138
139#[derive(Debug, Clone, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct Event {
143 #[serde(default, deserialize_with = "deserialize_optional_string_lenient")]
144 pub id: Option<EventId>,
145 pub name: Option<String>,
146 pub country_code: Option<Ustr>,
147 pub timezone: Option<Ustr>,
148 pub venue: Option<Ustr>,
149 pub open_date: Option<String>,
150}
151
152#[derive(Debug, Clone, Deserialize)]
154pub struct Competition {
155 #[serde(default, deserialize_with = "deserialize_optional_string_lenient")]
156 pub id: Option<CompetitionId>,
157 pub name: Option<String>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162#[serde(rename_all = "camelCase")]
163pub struct RunnerId {
164 pub market_id: MarketId,
165 pub selection_id: SelectionId,
166 pub handicap: Option<Handicap>,
167}
168
169#[derive(Debug, Clone, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct MarketCatalogue {
173 pub market_id: MarketId,
174 pub market_name: String,
175 pub market_start_time: Option<String>,
176 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
177 pub total_matched: Option<Decimal>,
178 pub event_type: Option<EventType>,
179 pub competition: Option<Competition>,
180 pub description: Option<MarketDescription>,
181 pub event: Option<Event>,
182 pub runners: Option<Vec<RunnerCatalog>>,
183}
184
185#[derive(Debug, Clone, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct MarketDescription {
189 pub betting_type: MarketBettingType,
190 pub bsp_market: bool,
191 pub clarifications: Option<String>,
192 pub discount_allowed: bool,
193 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
194 pub each_way_divisor: Option<Decimal>,
195 #[serde(deserialize_with = "deserialize_decimal")]
196 pub market_base_rate: Decimal,
197 pub market_time: String,
198 pub market_type: Ustr,
199 pub persistence_enabled: bool,
200 pub race_type: Option<Ustr>,
201 pub regulator: Ustr,
202 pub rules: Option<String>,
203 pub rules_has_date: Option<bool>,
204 pub settle_time: Option<String>,
205 pub suspend_time: String,
206 pub turn_in_play_enabled: bool,
207 pub wallet: Option<Ustr>,
208 pub bet_delay_models: Option<Vec<BetDelayModel>>,
209 pub line_range_info: Option<LineRangeInfo>,
210 pub price_ladder_description: Option<PriceLadderDescription>,
211}
212
213#[derive(Debug, Clone, Deserialize)]
215pub struct PriceLadderDescription {
216 #[serde(rename = "type")]
217 pub ladder_type: Option<PriceLadderType>,
218}
219
220#[derive(Debug, Clone, Deserialize)]
222#[serde(rename_all = "camelCase")]
223pub struct LineRangeInfo {
224 #[serde(deserialize_with = "deserialize_decimal")]
225 pub max_unit_value: Decimal,
226 #[serde(deserialize_with = "deserialize_decimal")]
227 pub min_unit_value: Decimal,
228 #[serde(deserialize_with = "deserialize_decimal")]
229 pub interval: Decimal,
230}
231
232#[derive(Debug, Clone, Deserialize)]
234#[serde(rename_all = "camelCase")]
235pub struct RunnerCatalog {
236 pub selection_id: SelectionId,
237 pub runner_name: String,
238 pub handicap: Handicap,
239 pub sort_priority: Option<u32>,
240 pub metadata: Option<AHashMap<String, serde_json::Value>>,
246}
247
248#[derive(Debug, Clone, Default, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct MarketFilter {
252 #[serde(skip_serializing_if = "Option::is_none")]
253 pub bsp_only: Option<bool>,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub competition_ids: Option<Vec<CompetitionId>>,
256 #[serde(skip_serializing_if = "Option::is_none")]
257 pub event_ids: Option<Vec<EventId>>,
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub event_type_ids: Option<Vec<EventTypeId>>,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub in_play_only: Option<bool>,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub market_betting_types: Option<Vec<MarketBettingType>>,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub market_countries: Option<Vec<Ustr>>,
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub market_ids: Option<Vec<MarketId>>,
268 #[serde(skip_serializing_if = "Option::is_none")]
269 pub market_start_time: Option<TimeRange>,
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub market_type_codes: Option<Vec<Ustr>>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub race_types: Option<Vec<Ustr>>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub text_query: Option<String>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub turn_in_play_enabled: Option<bool>,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub venues: Option<Vec<Ustr>>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub with_orders: Option<Vec<String>>,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
286#[serde(rename_all = "camelCase")]
287pub struct LimitOrder {
288 pub size: Decimal,
289 pub price: Decimal,
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub persistence_type: Option<PersistenceType>,
292 #[serde(skip_serializing_if = "Option::is_none")]
293 pub time_in_force: Option<BetfairTimeInForce>,
294 #[serde(skip_serializing_if = "Option::is_none")]
295 pub min_fill_size: Option<Decimal>,
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub bet_target_type: Option<BetTargetType>,
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub bet_target_size: Option<Decimal>,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct LimitOnCloseOrder {
305 pub liability: Decimal,
306 pub price: Decimal,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct MarketOnCloseOrder {
312 pub liability: Decimal,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317#[serde(rename_all = "camelCase")]
318pub struct PlaceInstruction {
319 pub order_type: BetfairOrderType,
320 pub selection_id: SelectionId,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub handicap: Option<Handicap>,
323 pub side: BetfairSide,
324 #[serde(skip_serializing_if = "Option::is_none")]
325 pub limit_order: Option<LimitOrder>,
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub limit_on_close_order: Option<LimitOnCloseOrder>,
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub market_on_close_order: Option<MarketOnCloseOrder>,
330 #[serde(skip_serializing_if = "Option::is_none")]
331 pub customer_order_ref: Option<String>,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct CancelInstruction {
338 pub bet_id: BetId,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub size_reduction: Option<Decimal>,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345#[serde(rename_all = "camelCase")]
346pub struct ReplaceInstruction {
347 pub bet_id: BetId,
348 pub new_price: Decimal,
349}
350
351#[derive(Debug, Clone, Serialize)]
353#[serde(rename_all = "camelCase")]
354pub struct PlaceOrdersParams {
355 pub market_id: MarketId,
356 pub instructions: Vec<PlaceInstruction>,
357 #[serde(skip_serializing_if = "Option::is_none")]
358 pub customer_ref: Option<String>,
359 #[serde(skip_serializing_if = "Option::is_none")]
360 pub market_version: Option<MarketVersion>,
361 #[serde(skip_serializing_if = "Option::is_none")]
362 pub customer_strategy_ref: Option<CustomerStrategyRef>,
363}
364
365#[derive(Debug, Clone, Serialize)]
367#[serde(rename_all = "camelCase")]
368pub struct CancelOrdersParams {
369 #[serde(skip_serializing_if = "Option::is_none")]
370 pub market_id: Option<MarketId>,
371 #[serde(skip_serializing_if = "Option::is_none")]
372 pub instructions: Option<Vec<CancelInstruction>>,
373 #[serde(skip_serializing_if = "Option::is_none")]
374 pub customer_ref: Option<String>,
375}
376
377#[derive(Debug, Clone, Serialize)]
379#[serde(rename_all = "camelCase")]
380pub struct ReplaceOrdersParams {
381 pub market_id: MarketId,
382 pub instructions: Vec<ReplaceInstruction>,
383 #[serde(skip_serializing_if = "Option::is_none")]
384 pub customer_ref: Option<String>,
385 #[serde(skip_serializing_if = "Option::is_none")]
386 pub market_version: Option<MarketVersion>,
387}
388
389#[derive(Debug, Clone, Serialize)]
391#[serde(rename_all = "camelCase")]
392pub struct ListMarketCatalogueParams {
393 pub filter: MarketFilter,
394 #[serde(skip_serializing_if = "Option::is_none")]
395 pub market_projection: Option<Vec<MarketProjection>>,
396 #[serde(skip_serializing_if = "Option::is_none")]
397 pub sort: Option<MarketSort>,
398 #[serde(skip_serializing_if = "Option::is_none")]
399 pub max_results: Option<u32>,
400 #[serde(skip_serializing_if = "Option::is_none")]
401 pub locale: Option<String>,
402}
403
404#[derive(Debug, Clone, Serialize)]
406#[serde(rename_all = "camelCase")]
407pub struct ListCurrentOrdersParams {
408 #[serde(skip_serializing_if = "Option::is_none")]
409 pub bet_ids: Option<Vec<BetId>>,
410 #[serde(skip_serializing_if = "Option::is_none")]
411 pub market_ids: Option<Vec<MarketId>>,
412 #[serde(skip_serializing_if = "Option::is_none")]
413 pub order_projection: Option<OrderProjection>,
414 #[serde(skip_serializing_if = "Option::is_none")]
415 pub customer_order_refs: Option<Vec<CustomerOrderRef>>,
416 #[serde(skip_serializing_if = "Option::is_none")]
417 pub customer_strategy_refs: Option<Vec<CustomerStrategyRef>>,
418 #[serde(skip_serializing_if = "Option::is_none")]
419 pub date_range: Option<TimeRange>,
420 #[serde(skip_serializing_if = "Option::is_none")]
421 pub order_by: Option<OrderBy>,
422 #[serde(skip_serializing_if = "Option::is_none")]
423 pub sort_dir: Option<SortDir>,
424 #[serde(skip_serializing_if = "Option::is_none")]
425 pub from_record: Option<u32>,
426 #[serde(skip_serializing_if = "Option::is_none")]
427 pub record_count: Option<u32>,
428}
429
430#[derive(Debug, Clone, Serialize)]
432#[serde(rename_all = "camelCase")]
433pub struct ListClearedOrdersParams {
434 pub bet_status: BetStatus,
435 #[serde(skip_serializing_if = "Option::is_none")]
436 pub event_type_ids: Option<Vec<EventTypeId>>,
437 #[serde(skip_serializing_if = "Option::is_none")]
438 pub event_ids: Option<Vec<EventId>>,
439 #[serde(skip_serializing_if = "Option::is_none")]
440 pub market_ids: Option<Vec<MarketId>>,
441 #[serde(skip_serializing_if = "Option::is_none")]
442 pub runner_ids: Option<Vec<RunnerId>>,
443 #[serde(skip_serializing_if = "Option::is_none")]
444 pub bet_ids: Option<Vec<BetId>>,
445 #[serde(skip_serializing_if = "Option::is_none")]
446 pub customer_order_refs: Option<Vec<CustomerOrderRef>>,
447 #[serde(skip_serializing_if = "Option::is_none")]
448 pub customer_strategy_refs: Option<Vec<CustomerStrategyRef>>,
449 #[serde(skip_serializing_if = "Option::is_none")]
450 pub side: Option<BetfairSide>,
451 #[serde(skip_serializing_if = "Option::is_none")]
452 pub settled_date_range: Option<TimeRange>,
453 #[serde(skip_serializing_if = "Option::is_none")]
454 pub group_by: Option<GroupBy>,
455 #[serde(skip_serializing_if = "Option::is_none")]
456 pub include_item_description: Option<bool>,
457 #[serde(skip_serializing_if = "Option::is_none")]
458 pub locale: Option<String>,
459 #[serde(skip_serializing_if = "Option::is_none")]
460 pub from_record: Option<u32>,
461 #[serde(skip_serializing_if = "Option::is_none")]
462 pub record_count: Option<u32>,
463}
464
465#[derive(Debug, Clone, Deserialize)]
467#[serde(rename_all = "camelCase")]
468pub struct PlaceExecutionReport {
469 pub customer_ref: Option<String>,
470 pub status: ExecutionReportStatus,
471 pub error_code: Option<ExecutionReportErrorCode>,
472 pub error_message: Option<String>,
473 pub market_id: Option<MarketId>,
474 pub instruction_reports: Option<Vec<PlaceInstructionReport>>,
475}
476
477#[derive(Debug, Clone, Deserialize)]
479#[serde(rename_all = "camelCase")]
480pub struct PlaceInstructionReport {
481 pub status: InstructionReportStatus,
482 pub error_code: Option<InstructionReportErrorCode>,
483 pub error_message: Option<String>,
484 pub order_status: Option<BetfairOrderStatus>,
485 pub instruction: Option<PlaceInstruction>,
486 pub bet_id: Option<BetId>,
487 pub placed_date: Option<String>,
488 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
489 pub average_price_matched: Option<Decimal>,
490 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
491 pub size_matched: Option<Decimal>,
492}
493
494#[derive(Debug, Clone, Deserialize)]
496#[serde(rename_all = "camelCase")]
497pub struct CancelExecutionReport {
498 pub customer_ref: Option<String>,
499 pub status: ExecutionReportStatus,
500 pub error_code: Option<ExecutionReportErrorCode>,
501 pub error_message: Option<String>,
502 pub market_id: Option<MarketId>,
503 pub instruction_reports: Option<Vec<CancelInstructionReport>>,
504}
505
506#[derive(Debug, Clone, Deserialize)]
508#[serde(rename_all = "camelCase")]
509pub struct CancelInstructionReport {
510 pub status: InstructionReportStatus,
511 pub error_code: Option<InstructionReportErrorCode>,
512 pub error_message: Option<String>,
513 pub instruction: Option<CancelInstruction>,
514 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
515 pub size_cancelled: Option<Decimal>,
516 pub cancelled_date: Option<String>,
517}
518
519#[derive(Debug, Clone, Deserialize)]
521#[serde(rename_all = "camelCase")]
522pub struct ReplaceExecutionReport {
523 pub customer_ref: Option<String>,
524 pub status: ExecutionReportStatus,
525 pub error_code: Option<ExecutionReportErrorCode>,
526 pub error_message: Option<String>,
527 pub market_id: Option<MarketId>,
528 pub instruction_reports: Option<Vec<ReplaceInstructionReport>>,
529}
530
531#[derive(Debug, Clone, Deserialize)]
533#[serde(rename_all = "camelCase")]
534pub struct ReplaceInstructionReport {
535 pub status: InstructionReportStatus,
536 pub error_code: Option<InstructionReportErrorCode>,
537 pub error_message: Option<String>,
538 pub cancel_instruction_report: Option<CancelInstructionReport>,
539 pub place_instruction_report: Option<PlaceInstructionReport>,
540}
541
542#[derive(Debug, Clone, Deserialize)]
544#[serde(rename_all = "camelCase")]
545pub struct CurrentOrderSummary {
546 pub bet_id: BetId,
547 pub market_id: MarketId,
548 pub selection_id: SelectionId,
549 pub handicap: Handicap,
550 pub price_size: PriceSize,
551 #[serde(deserialize_with = "deserialize_decimal")]
552 pub bsp_liability: Decimal,
553 pub side: BetfairSide,
554 pub status: BetfairOrderStatus,
555 pub persistence_type: PersistenceType,
556 pub order_type: BetfairOrderType,
557 pub placed_date: String,
558 pub matched_date: Option<String>,
559 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
560 pub average_price_matched: Option<Decimal>,
561 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
562 pub size_matched: Option<Decimal>,
563 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
564 pub size_remaining: Option<Decimal>,
565 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
566 pub size_lapsed: Option<Decimal>,
567 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
568 pub size_cancelled: Option<Decimal>,
569 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
570 pub size_voided: Option<Decimal>,
571 pub regulator_auth_code: Option<String>,
572 pub regulator_code: Option<String>,
573 pub customer_order_ref: Option<CustomerOrderRef>,
574 pub customer_strategy_ref: Option<CustomerStrategyRef>,
575}
576
577#[derive(Debug, Clone, Deserialize)]
579#[serde(rename_all = "camelCase")]
580pub struct CurrentOrderSummaryReport {
581 pub current_orders: Vec<CurrentOrderSummary>,
582 pub more_available: bool,
583}
584
585#[derive(Debug, Clone, Deserialize)]
587#[serde(rename_all = "camelCase")]
588pub struct ItemDescription {
589 pub event_type_desc: Option<String>,
590 pub event_desc: Option<String>,
591 pub market_desc: Option<String>,
592 pub market_type: Option<Ustr>,
593 pub market_start_time: Option<String>,
594 pub runner_desc: Option<String>,
595 pub number_of_winners: Option<u32>,
596 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
597 pub each_way_divisor: Option<Decimal>,
598}
599
600#[derive(Debug, Clone, Deserialize)]
602#[serde(rename_all = "camelCase")]
603pub struct ClearedOrderSummary {
604 pub event_type_id: Option<EventTypeId>,
605 pub event_id: Option<EventId>,
606 pub market_id: Option<MarketId>,
607 pub selection_id: Option<SelectionId>,
608 pub handicap: Option<Handicap>,
609 pub bet_id: Option<BetId>,
610 pub placed_date: Option<String>,
611 pub persistence_type: Option<PersistenceType>,
612 pub order_type: Option<BetfairOrderType>,
613 pub side: Option<BetfairSide>,
614 pub item_description: Option<ItemDescription>,
615 pub bet_outcome: Option<String>,
616 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
617 pub price_requested: Option<Decimal>,
618 pub settled_date: Option<String>,
619 pub last_matched_date: Option<String>,
620 pub bet_count: Option<u32>,
621 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
622 pub commission: Option<Decimal>,
623 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
624 pub price_matched: Option<Decimal>,
625 pub price_reduced: Option<bool>,
626 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
627 pub size_settled: Option<Decimal>,
628 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
629 pub profit: Option<Decimal>,
630 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
631 pub size_cancelled: Option<Decimal>,
632 pub customer_order_ref: Option<CustomerOrderRef>,
633 pub customer_strategy_ref: Option<CustomerStrategyRef>,
634}
635
636#[derive(Debug, Clone, Deserialize)]
638#[serde(rename_all = "camelCase")]
639pub struct ClearedOrderSummaryReport {
640 pub cleared_orders: Vec<ClearedOrderSummary>,
641 pub more_available: bool,
642}
643
644#[derive(Debug, Clone, Deserialize)]
646#[serde(rename_all = "camelCase")]
647pub struct NavigationMarket {
648 pub name: Option<String>,
649 pub id: Option<MarketId>,
650 pub exchange_id: Option<String>,
651 pub market_type: Option<Ustr>,
652 pub market_start_time: Option<String>,
653 #[serde(default, deserialize_with = "deserialize_optional_u32_lenient")]
654 pub number_of_winners: Option<u32>,
655}
656
657#[derive(Debug, Clone, Deserialize)]
659#[serde(rename_all = "camelCase")]
660pub struct NavigationRace {
661 pub name: Option<String>,
662 pub id: Option<String>,
663 pub venue: Option<Ustr>,
664 pub start_time: Option<String>,
665 pub race_number: Option<String>,
666 pub country_code: Option<Ustr>,
667 pub children: Option<Vec<NavigationChild>>,
668}
669
670#[derive(Debug, Clone, Deserialize)]
672#[serde(rename_all = "camelCase")]
673pub struct NavigationEvent {
674 pub name: Option<String>,
675 pub id: Option<EventId>,
676 pub country_code: Option<Ustr>,
677 pub children: Option<Vec<NavigationChild>>,
678}
679
680#[derive(Debug, Clone, Deserialize)]
682#[serde(rename_all = "camelCase")]
683pub struct NavigationGroup {
684 pub name: Option<String>,
685 pub id: Option<String>,
686 pub children: Option<Vec<NavigationChild>>,
687}
688
689#[derive(Debug, Clone, Deserialize)]
691#[serde(rename_all = "camelCase")]
692pub struct NavigationEventType {
693 pub name: Option<Ustr>,
694 pub id: Option<EventTypeId>,
695 pub children: Option<Vec<NavigationChild>>,
696}
697
698#[derive(Debug, Clone, Deserialize)]
700#[serde(tag = "type")]
701pub enum NavigationChild {
702 #[serde(rename = "EVENT_TYPE")]
703 EventType(NavigationEventType),
704 #[serde(rename = "GROUP")]
705 Group(NavigationGroup),
706 #[serde(rename = "EVENT")]
707 Event(NavigationEvent),
708 #[serde(rename = "RACE")]
709 Race(NavigationRace),
710 #[serde(rename = "MARKET")]
711 Market(NavigationMarket),
712}
713
714#[derive(Debug, Clone, Deserialize)]
716pub struct Navigation {
717 pub children: Option<Vec<NavigationChild>>,
718}
719
720#[derive(Debug, Clone)]
722pub struct FlattenedMarket {
723 pub event_type_id: Option<String>,
724 pub event_type_name: Option<Ustr>,
725 pub event_id: Option<String>,
726 pub event_name: Option<String>,
727 pub event_country_code: Option<Ustr>,
728 pub market_id: Option<MarketId>,
729 pub market_name: Option<String>,
730 pub market_type: Option<Ustr>,
731 pub market_start_time: Option<String>,
732 pub number_of_winners: Option<u32>,
733}
734
735#[cfg(test)]
736mod tests {
737 use rstest::rstest;
738
739 use super::*;
740 use crate::common::testing::{load_test_json, parse_jsonrpc};
741
742 #[rstest]
743 fn test_cert_login_response() {
744 let data = load_test_json("rest/cert_login.json");
745 let resp: CertLoginResponse = serde_json::from_str(&data).unwrap();
746 assert_eq!(resp.login_status, CertLoginStatus::Success);
747 assert!(resp.session_token.is_some());
748 }
749
750 #[rstest]
751 fn test_cert_login_error_response() {
752 let json = r#"{"loginStatus":"CERT_AUTH_REQUIRED"}"#;
753 let resp: CertLoginResponse = serde_json::from_str(json).unwrap();
754 assert_eq!(resp.login_status, CertLoginStatus::CertAuthRequired);
755 assert!(resp.session_token.is_none());
756 }
757
758 #[rstest]
759 fn test_cert_login_unknown_status_deserializes_to_other() {
760 let json = r#"{"loginStatus":"SOME_FUTURE_CODE"}"#;
761 let resp: CertLoginResponse = serde_json::from_str(json).unwrap();
762 assert_eq!(resp.login_status, CertLoginStatus::Other);
763 }
764
765 #[rstest]
766 fn test_interactive_login_response() {
767 let data = load_test_json("rest/login_success.json");
768 let resp: LoginResponse = serde_json::from_str(&data).unwrap();
769 assert_eq!(resp.status, LoginStatus::Success);
770 }
771
772 #[rstest]
773 fn test_interactive_login_failure() {
774 let data = load_test_json("rest/login_failure.json");
775 let resp: LoginResponse = serde_json::from_str(&data).unwrap();
776 assert_eq!(resp.status, LoginStatus::Fail);
777 }
778
779 #[rstest]
780 fn test_list_market_catalogue_with_runner_metadata() {
781 let data = load_test_json("rest/list_market_catalogue.json");
782 let catalogue: MarketCatalogue = serde_json::from_str(&data).unwrap();
783 let runners = catalogue.runners.expect("runners present");
784 let meta = runners[0].metadata.as_ref().expect("metadata present");
785 assert!(meta.contains_key("AGE"));
786 assert!(meta.contains_key("CLOTH_NUMBER"));
787 assert!(meta.contains_key("STALL_DRAW"));
788 }
789
790 #[rstest]
791 fn test_navigation_with_empty_number_of_winners() {
792 let data = load_test_json("rest/navigation_list_navigation.json");
793 let nav: Navigation = serde_json::from_str(&data).unwrap();
794 assert!(nav.children.is_some());
795 }
796
797 fn find_race_with_children(children: &[NavigationChild]) -> bool {
798 for child in children {
799 match child {
800 NavigationChild::Race(race) => {
801 if let Some(kids) = &race.children
802 && !kids.is_empty()
803 {
804 return true;
805 }
806 }
807 NavigationChild::EventType(et) => {
808 if let Some(kids) = &et.children
809 && find_race_with_children(kids)
810 {
811 return true;
812 }
813 }
814 NavigationChild::Group(g) => {
815 if let Some(kids) = &g.children
816 && find_race_with_children(kids)
817 {
818 return true;
819 }
820 }
821 NavigationChild::Event(e) => {
822 if let Some(kids) = &e.children
823 && find_race_with_children(kids)
824 {
825 return true;
826 }
827 }
828 NavigationChild::Market(_) => {}
829 }
830 }
831 false
832 }
833
834 #[rstest]
835 fn test_navigation_race_has_children() {
836 let data = load_test_json("rest/navigation_list_navigation.json");
837 let nav: Navigation = serde_json::from_str(&data).unwrap();
838 let children = nav.children.as_ref().unwrap();
839 assert!(
840 find_race_with_children(children),
841 "should find at least one RACE node with MARKET children"
842 );
843 }
844
845 #[rstest]
846 fn test_account_details() {
847 let data = load_test_json("rest/account_details.json");
848 let _resp: AccountDetailsResponse = serde_json::from_str(&data).unwrap();
849 }
850
851 #[rstest]
852 #[case("rest/account_funds_no_exposure.json")]
853 #[case("rest/account_funds_with_exposure.json")]
854 fn test_account_funds(#[case] fixture: &str) {
855 let data = load_test_json(fixture);
856 let _resp: AccountFundsResponse =
857 serde_json::from_str(&data).unwrap_or_else(|e| panic!("{fixture}: {e}"));
858 }
859
860 #[rstest]
861 #[case("rest/betting_place_order_success.json")]
862 #[case("rest/betting_place_order_error.json")]
863 #[case("rest/betting_place_order_batch_success.json")]
864 #[case("rest/betting_place_order_batch_partial_failure.json")]
865 fn test_place_order_responses(#[case] fixture: &str) {
866 let data = load_test_json(fixture);
867 let _resp: PlaceExecutionReport = parse_jsonrpc(&data);
868 }
869
870 #[rstest]
871 fn test_place_order_response_parses_instruction_error_message() {
872 let data = r#"
873 {
874 "jsonrpc": "2.0",
875 "result": {
876 "status": "FAILURE",
877 "instructionReports": [
878 {
879 "status": "FAILURE",
880 "errorCode": "ERROR_IN_ORDER",
881 "errorMessage": "Detailed Betfair validation message"
882 }
883 ]
884 }
885 }
886 "#;
887
888 let resp: PlaceExecutionReport = parse_jsonrpc(data);
889 let instruction_report = resp
890 .instruction_reports
891 .as_ref()
892 .and_then(|reports| reports.first())
893 .expect("instruction report");
894
895 assert_eq!(
896 instruction_report.error_message.as_deref(),
897 Some("Detailed Betfair validation message"),
898 );
899 }
900
901 #[rstest]
902 #[case("rest/betting_cancel_orders_success.json")]
903 #[case("rest/betting_cancel_orders_error.json")]
904 #[case("rest/betting_cancel_orders_batch_success.json")]
905 #[case("rest/betting_cancel_orders_batch_partial_failure.json")]
906 fn test_cancel_order_responses(#[case] fixture: &str) {
907 let data = load_test_json(fixture);
908 let _resp: CancelExecutionReport = parse_jsonrpc(&data);
909 }
910
911 #[rstest]
912 fn test_replace_order_responses() {
913 let data = load_test_json("rest/betting_replace_orders_success.json");
916 let _resp: ReplaceExecutionReport = parse_jsonrpc(&data);
917 }
918
919 #[rstest]
920 #[case("rest/list_current_orders_empty.json")]
921 #[case("rest/list_current_orders_single.json")]
922 #[case("rest/list_current_orders_executable.json")]
923 #[case("rest/list_current_orders_execution_complete.json")]
924 #[case("rest/list_current_orders_on_close_execution_complete.json")]
925 fn test_current_orders(#[case] fixture: &str) {
926 let data = load_test_json(fixture);
927 let _resp: CurrentOrderSummaryReport = parse_jsonrpc(&data);
928 }
929
930 #[rstest]
931 fn test_cleared_orders() {
932 let data = load_test_json("rest/list_cleared_orders.json");
933 let _resp: ClearedOrderSummaryReport = parse_jsonrpc(&data);
934 }
935
936 #[rstest]
937 fn test_betting_market_catalogue() {
938 let data = load_test_json("rest/betting_list_market_catalogue.json");
939 let catalogues: Vec<MarketCatalogue> = serde_json::from_str(&data).unwrap();
940 assert!(!catalogues.is_empty());
941 }
942}