use betex::{
book::protocol::command::{Command, CommandKind, Persistence, Side, TimeInForce},
book::protocol::reject::RejectReason,
book::{Book, BookEvent},
types::{AccountId, CorrelationId, MarketId, Money, OddsX10000, RunnerId},
};
fn place_order_cmd(
market_id: MarketId,
correlation_id: u64,
account_id: u64,
runner_id: u32,
) -> Command {
Command {
correlation_id: Some(CorrelationId(correlation_id.to_string())),
metadata: None,
market_id,
kind: CommandKind::PlaceOrder {
runner_id: RunnerId(runner_id),
account_id: AccountId::from(account_id),
client_order_id: None,
side: Side::Yes,
odds: OddsX10000(20000),
stake: Money(100),
persistence: Persistence::Persist,
time_in_force: TimeInForce::Gtc,
},
}
}
fn batch_cancel_cmd(market_id: MarketId, metadata: Option<serde_json::Value>) -> Command {
Command {
correlation_id: Some(CorrelationId(9001.to_string())),
metadata,
market_id,
kind: CommandKind::BatchCancelOrders {
from_created_at_inclusive: None,
to_created_at_inclusive: None,
account_id: None,
runner_id: None,
reason: "BET_CANCEL".to_string(),
},
}
}
fn continue_batch_cancel_cmd(market_id: MarketId) -> Command {
Command {
correlation_id: Some(CorrelationId(9002.to_string())),
metadata: None,
market_id,
kind: CommandKind::ContinueBatchProcess,
}
}
#[test]
fn batch_cancel_no_orders_still_emits_final_event() {
let market_id = MarketId(101);
let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);
let (events, _) = book
.handle(&batch_cancel_cmd(market_id, None))
.expect("batch cancel should be accepted");
assert_eq!(events.len(), 2);
assert!(matches!(
events[0].event,
BookEvent::BatchProcessStarted { .. }
));
assert!(matches!(
events[1].event,
BookEvent::BatchProcessCompleted { .. }
));
}
#[test]
fn batch_cancel_metadata_attaches_only_to_first_event() {
let market_id = MarketId(102);
let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);
for i in 0..3 {
let (events, _) = book
.handle(&place_order_cmd(market_id, i + 1, 10_000 + i, 1))
.expect("place should succeed");
book.apply_all_events(&events);
}
let metadata = serde_json::json!({"source":"bet-cancel","trace":"t-1"});
book.set_close_batch_max_events(2);
let (start_events, _) = book
.handle(&batch_cancel_cmd(market_id, Some(metadata.clone())))
.expect("batch start should succeed");
assert_eq!(start_events.len(), 2);
assert!(matches!(
start_events[0].event,
BookEvent::BatchProcessStarted { .. }
));
match &start_events[1].event {
BookEvent::OrderCancelledBatched { .. } => {}
_ => panic!("expected OrderCancelledBatched"),
}
assert_eq!(start_events[0].metadata, Some(metadata));
assert!(start_events[1].metadata.is_none());
book.apply_all_events(&start_events);
loop {
let (events, _) = book
.handle(&continue_batch_cancel_cmd(market_id))
.expect("continue should succeed");
assert!(events.len() == 1 || events.len() == 2);
assert!(events.iter().all(|event| event.metadata.is_none()));
match (
events.first().map(|e| &e.event),
events.get(1).map(|e| &e.event),
) {
(
Some(BookEvent::OrderCancelledBatched { .. }),
Some(BookEvent::BatchProcessCompleted { .. }),
) => break,
(Some(BookEvent::OrderCancelledBatched { .. }), _) => {}
_ => panic!("expected OrderCancelledBatched"),
}
book.apply_all_events(&events);
}
}
#[test]
fn batch_cancel_blocks_manual_cancel_while_processing() {
let market_id = MarketId(103);
let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);
for correlation_id in [1_u64, 2] {
let (events, _) = book
.handle(&place_order_cmd(
market_id,
correlation_id,
99 + correlation_id,
1,
))
.expect("place should succeed");
book.apply_all_events(&events);
}
let (events, _) = book
.handle(&place_order_cmd(market_id, 3, 102, 1))
.expect("place should succeed");
let oid = match events[0].event {
BookEvent::OrderAccepted { order_id, .. } => order_id,
_ => panic!("expected OrderAccepted"),
};
book.apply_all_events(&events);
book.set_close_batch_max_events(2);
let (start_events, _) = book
.handle(&batch_cancel_cmd(market_id, None))
.expect("batch start should succeed");
book.apply_all_events(&start_events);
let err = book
.handle(&Command {
correlation_id: Some(CorrelationId(77.to_string())),
metadata: None,
market_id,
kind: CommandKind::CancelOrder {
account_id: AccountId::from(102),
order_id: oid,
},
})
.expect_err("manual cancel should be blocked while batch-cancel is in progress");
assert_eq!(err.reason, RejectReason::MarketBatchCancelling);
}
#[test]
fn batch_cancel_is_allowed_when_market_halted() {
let market_id = MarketId(104);
let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);
let (events, _) = book
.handle(&Command {
correlation_id: Some(CorrelationId(55.to_string())),
metadata: None,
market_id,
kind: CommandKind::HaltMarket {
reason: "TEST_HALT".to_string(),
},
})
.expect("halt should succeed");
book.apply_all_events(&events);
let (events, _) = book
.handle(&batch_cancel_cmd(market_id, None))
.expect("batch-cancel should be accepted while halted");
assert_eq!(events.len(), 2);
assert!(matches!(
events[0].event,
BookEvent::BatchProcessStarted { .. }
));
assert!(matches!(
events[1].event,
BookEvent::BatchProcessCompleted { .. }
));
}