1use anyhow::Context;
19use chrono::DateTime;
20use nautilus_core::{UUID4, UnixNanos, datetime::NANOSECONDS_IN_MILLISECOND};
21use nautilus_model::{
22 enums::AccountType,
23 events::AccountState,
24 identifiers::{AccountId, InstrumentId, Symbol},
25 instruments::{BettingInstrument, InstrumentAny},
26 types::{AccountBalance, Currency, Money, Price, Quantity},
27};
28use rust_decimal::{Decimal, prelude::ToPrimitive};
29use ustr::Ustr;
30
31use super::{
32 consts::{
33 BETFAIR_CUSTOMER_ORDER_REF_MAX_LEN, BETFAIR_PRICE_PRECISION, BETFAIR_QUANTITY_PRECISION,
34 BETFAIR_VENUE, DEFAULT_BETTING_TYPE, DEFAULT_MARKET_TYPE,
35 },
36 types::SelectionId,
37};
38use crate::{
39 http::models::{AccountFundsResponse, MarketCatalogue},
40 stream::messages::MarketDefinition,
41};
42
43#[must_use]
48pub fn make_symbol(market_id: &str, selection_id: u64, handicap: Decimal) -> Symbol {
49 if handicap == Decimal::ZERO {
50 Symbol::new(format!("{market_id}-{selection_id}"))
51 } else {
52 Symbol::new(format!("{market_id}-{selection_id}-{handicap}"))
53 }
54}
55
56#[must_use]
61pub fn make_instrument_id(market_id: &str, selection_id: u64, handicap: Decimal) -> InstrumentId {
62 let symbol = make_symbol(market_id, selection_id, handicap);
63 InstrumentId::new(symbol, *BETFAIR_VENUE)
64}
65
66pub fn parse_betfair_timestamp(s: &str) -> anyhow::Result<UnixNanos> {
79 let dt = DateTime::parse_from_rfc3339(s)
80 .or_else(|_| {
81 DateTime::parse_from_rfc3339(&s.replace(".000Z", "Z"))
83 })
84 .with_context(|| format!("invalid Betfair timestamp: {s}"))?;
85 Ok(UnixNanos::from(dt.timestamp_nanos_opt().unwrap() as u64))
86}
87
88#[must_use]
90pub fn parse_millis_timestamp(timestamp_ms: u64) -> UnixNanos {
91 UnixNanos::from(timestamp_ms * NANOSECONDS_IN_MILLISECOND)
92}
93
94pub fn parse_betfair_price(price: Decimal) -> anyhow::Result<Price> {
100 Price::from_decimal_dp(price, BETFAIR_PRICE_PRECISION).map_err(Into::into)
101}
102
103#[must_use]
105pub fn normalize_betfair_price(price: Decimal) -> Decimal {
106 parse_betfair_price(price).map_or(price, |price| price.as_decimal())
107}
108
109pub fn parse_betfair_quantity(quantity: Decimal) -> anyhow::Result<Quantity> {
115 Quantity::from_decimal_dp(quantity, BETFAIR_QUANTITY_PRECISION).map_err(Into::into)
116}
117
118#[must_use]
120pub fn normalize_betfair_quantity(quantity: Decimal) -> Decimal {
121 parse_betfair_quantity(quantity).map_or(quantity, |qty| qty.as_decimal())
122}
123
124#[must_use]
129pub fn make_customer_order_ref(client_order_id: &str) -> String {
130 let len = client_order_id.len();
131 if len <= BETFAIR_CUSTOMER_ORDER_REF_MAX_LEN {
132 client_order_id.to_string()
133 } else {
134 client_order_id[len - BETFAIR_CUSTOMER_ORDER_REF_MAX_LEN..].to_string()
135 }
136}
137
138#[must_use]
143pub fn make_customer_order_ref_legacy(client_order_id: &str) -> String {
144 let len = client_order_id.len();
145 if len <= BETFAIR_CUSTOMER_ORDER_REF_MAX_LEN {
146 client_order_id.to_string()
147 } else {
148 client_order_id[..BETFAIR_CUSTOMER_ORDER_REF_MAX_LEN].to_string()
149 }
150}
151
152pub fn parse_market_catalogue(
160 catalogue: &MarketCatalogue,
161 currency: Currency,
162 ts_init: UnixNanos,
163 min_notional: Option<Money>,
164) -> anyhow::Result<Vec<InstrumentAny>> {
165 let runners = catalogue
166 .runners
167 .as_ref()
168 .context("MarketCatalogue missing runners")?;
169
170 let market_id = &catalogue.market_id;
171
172 let (event_type_id, event_type_name) = match &catalogue.event_type {
173 Some(et) => (
174 et.id
175 .as_ref()
176 .and_then(|id| id.parse::<u64>().ok())
177 .unwrap_or(0),
178 Ustr::from(et.name.as_deref().unwrap_or("")),
179 ),
180 None => (0, Ustr::from("")),
181 };
182
183 let (competition_id, competition_name) = match &catalogue.competition {
184 Some(c) => (
185 c.id.as_ref()
186 .and_then(|id| id.parse::<u64>().ok())
187 .unwrap_or(0),
188 Ustr::from(c.name.as_deref().unwrap_or("")),
189 ),
190 None => (0, Ustr::from("")),
191 };
192
193 let (event_id, event_name, event_country_code, event_open_date) = match &catalogue.event {
194 Some(e) => {
195 let eid =
196 e.id.as_ref()
197 .and_then(|id| id.parse::<u64>().ok())
198 .unwrap_or(0);
199 let ename = Ustr::from(e.name.as_deref().unwrap_or(""));
200 let cc = e.country_code.unwrap_or_else(|| Ustr::from(""));
201 let open_date = e
202 .open_date
203 .as_deref()
204 .and_then(|d| parse_betfair_timestamp(d).ok())
205 .unwrap_or_default();
206 (eid, ename, cc, open_date)
207 }
208 None => (0, Ustr::from(""), Ustr::from(""), UnixNanos::default()),
209 };
210
211 let (betting_type, market_type, market_base_rate) = match &catalogue.description {
212 Some(desc) => (
213 Ustr::from(&format!("{}", desc.betting_type)),
214 desc.market_type,
215 desc.market_base_rate,
216 ),
217 None => (
218 Ustr::from(DEFAULT_BETTING_TYPE),
219 Ustr::from(DEFAULT_MARKET_TYPE),
220 Decimal::ZERO,
221 ),
222 };
223
224 let market_name = Ustr::from(&catalogue.market_name);
225 let market_start_time = catalogue
226 .market_start_time
227 .as_deref()
228 .and_then(|t| parse_betfair_timestamp(t).ok())
229 .unwrap_or_default();
230
231 let fee_rate = market_base_rate / Decimal::ONE_HUNDRED;
233
234 let tick = Decimal::new(1, 2); let price_increment = parse_betfair_price(tick)?;
236 let size_increment = parse_betfair_quantity(tick)?;
237
238 let mut instruments = Vec::with_capacity(runners.len());
239
240 for runner in runners {
241 let handicap = runner.handicap;
242 let instrument_id = make_instrument_id(market_id, runner.selection_id, handicap);
243 let raw_symbol = make_symbol(market_id, runner.selection_id, handicap);
244
245 let instrument = BettingInstrument::new_checked(
246 instrument_id,
247 raw_symbol,
248 event_type_id,
249 event_type_name,
250 competition_id,
251 competition_name,
252 event_id,
253 event_name,
254 event_country_code,
255 event_open_date,
256 betting_type,
257 Ustr::from(market_id.as_str()),
258 market_name,
259 market_type,
260 market_start_time,
261 runner.selection_id,
262 Ustr::from(&runner.runner_name),
263 handicap.to_f64().unwrap_or(0.0),
264 currency,
265 BETFAIR_PRICE_PRECISION,
266 BETFAIR_QUANTITY_PRECISION,
267 price_increment,
268 size_increment,
269 None, None, None, min_notional, None, None, Some(Decimal::ONE), Some(Decimal::ONE), Some(fee_rate), Some(fee_rate), None, None, ts_init, ts_init, )
284 .with_context(|| {
285 format!(
286 "failed to create BettingInstrument for {market_id}/{}/{}",
287 runner.selection_id, runner.runner_name
288 )
289 })?;
290
291 instruments.push(InstrumentAny::Betting(instrument));
292 }
293
294 Ok(instruments)
295}
296
297pub fn parse_market_definition(
307 market_id: &str,
308 def: &MarketDefinition,
309 currency: Currency,
310 ts_init: UnixNanos,
311 min_notional: Option<Money>,
312) -> anyhow::Result<Vec<InstrumentAny>> {
313 let runners = def
314 .runners
315 .as_ref()
316 .context("MarketDefinition missing runners")?;
317
318 let event_type_id = def
319 .event_type_id
320 .as_deref()
321 .and_then(|id| id.parse::<u64>().ok())
322 .unwrap_or(0);
323 let event_type_name = def.event_type_name.unwrap_or_else(|| Ustr::from(""));
324
325 let competition_id = def
326 .competition_id
327 .as_deref()
328 .and_then(|id| id.parse::<u64>().ok())
329 .unwrap_or(0);
330 let competition_name = Ustr::from(def.competition_name.as_deref().unwrap_or(""));
331
332 let event_id = def
333 .event_id
334 .as_deref()
335 .and_then(|id| id.parse::<u64>().ok())
336 .unwrap_or(0);
337 let event_name = Ustr::from(def.event_name.as_deref().unwrap_or(""));
338 let event_country_code = def.country_code.unwrap_or_else(|| Ustr::from(""));
339 let event_open_date = def
340 .open_date
341 .as_deref()
342 .and_then(|d| parse_betfair_timestamp(d).ok())
343 .unwrap_or_default();
344
345 let betting_type = match &def.betting_type {
346 Some(bt) => Ustr::from(&format!("{bt}")),
347 None => Ustr::from(DEFAULT_BETTING_TYPE),
348 };
349 let market_name = Ustr::from(def.market_name.as_deref().unwrap_or(""));
350 let market_type = def
351 .market_type
352 .unwrap_or_else(|| Ustr::from(DEFAULT_MARKET_TYPE));
353 let market_start_time = def
354 .market_time
355 .as_deref()
356 .and_then(|t| parse_betfair_timestamp(t).ok())
357 .unwrap_or_default();
358
359 let fee_rate = def
360 .market_base_rate
361 .map(|r| r / Decimal::ONE_HUNDRED)
362 .unwrap_or_default();
363
364 let tick = Decimal::new(1, 2); let price_increment = parse_betfair_price(tick)?;
366 let size_increment = parse_betfair_quantity(tick)?;
367
368 let market_id_ustr = Ustr::from(market_id);
369
370 let mut instruments = Vec::with_capacity(runners.len());
371
372 for runner in runners {
373 let handicap = runner.hc.unwrap_or(Decimal::ZERO);
374
375 let instrument_id = make_instrument_id(market_id, runner.id, handicap);
376 let raw_symbol = make_symbol(market_id, runner.id, handicap);
377 let runner_name = Ustr::from(runner.name.as_deref().unwrap_or(""));
378
379 let instrument = BettingInstrument::new_checked(
380 instrument_id,
381 raw_symbol,
382 event_type_id,
383 event_type_name,
384 competition_id,
385 competition_name,
386 event_id,
387 event_name,
388 event_country_code,
389 event_open_date,
390 betting_type,
391 market_id_ustr,
392 market_name,
393 market_type,
394 market_start_time,
395 runner.id,
396 runner_name,
397 handicap.to_f64().unwrap_or(0.0),
398 currency,
399 BETFAIR_PRICE_PRECISION,
400 BETFAIR_QUANTITY_PRECISION,
401 price_increment,
402 size_increment,
403 None, None, None, min_notional, None, None, Some(Decimal::ONE), Some(Decimal::ONE), Some(fee_rate), Some(fee_rate), None, None, ts_init, ts_init, )
418 .with_context(|| {
419 format!(
420 "failed to create BettingInstrument for {market_id}/{}",
421 runner.id
422 )
423 })?;
424
425 instruments.push(InstrumentAny::Betting(instrument));
426 }
427
428 Ok(instruments)
429}
430
431pub fn parse_account_state(
437 funds: &AccountFundsResponse,
438 account_id: AccountId,
439 currency: Currency,
440 ts_event: UnixNanos,
441 ts_init: UnixNanos,
442) -> anyhow::Result<AccountState> {
443 let available = funds.available_to_bet_balance.unwrap_or_default();
444 let exposure = funds.exposure.unwrap_or_default().abs();
445 let total = available + exposure;
446
447 let balance = AccountBalance::from_total_and_locked(total, exposure, currency)?;
448
449 Ok(AccountState::new(
450 account_id,
451 AccountType::Betting,
452 vec![balance],
453 vec![],
454 true,
455 UUID4::new(),
456 ts_event,
457 ts_init,
458 Some(currency),
459 ))
460}
461
462pub fn extract_market_id(instrument_id: &InstrumentId) -> anyhow::Result<String> {
471 let symbol = instrument_id.symbol.as_str();
472 let parts: Vec<&str> = symbol.splitn(3, '-').collect();
473 if parts.len() >= 2 {
474 Ok(parts[0].to_string())
475 } else {
476 anyhow::bail!("Cannot extract market ID from {instrument_id}")
477 }
478}
479
480pub fn extract_selection_id(
486 instrument_id: &InstrumentId,
487) -> anyhow::Result<(SelectionId, Decimal)> {
488 let symbol = instrument_id.symbol.as_str();
489 let parts: Vec<&str> = symbol.splitn(3, '-').collect();
490 if parts.len() < 2 {
491 anyhow::bail!("Cannot extract selection ID from {instrument_id}");
492 }
493
494 let selection_id: SelectionId = parts[1]
495 .parse()
496 .with_context(|| format!("invalid selection ID in {instrument_id}"))?;
497
498 let handicap = if parts.len() == 3 {
499 parts[2]
500 .parse::<Decimal>()
501 .with_context(|| format!("invalid handicap in {instrument_id}"))?
502 } else {
503 Decimal::ZERO
504 };
505
506 Ok((selection_id, handicap))
507}
508
509#[cfg(test)]
510mod tests {
511 use rstest::rstest;
512
513 use super::*;
514 use crate::{common::testing::load_test_json, stream::messages::StreamMessage};
515
516 #[rstest]
517 fn test_make_instrument_id_no_handicap() {
518 let id = make_instrument_id("1.180737206", 19248890, Decimal::ZERO);
519 assert_eq!(id.to_string(), "1.180737206-19248890.BETFAIR");
520 }
521
522 #[rstest]
523 fn test_make_instrument_id_with_handicap() {
524 let id = make_instrument_id("1.180737206", 19248890, Decimal::new(15, 1));
525 assert_eq!(id.to_string(), "1.180737206-19248890-1.5.BETFAIR");
526 }
527
528 #[rstest]
529 fn test_make_symbol_no_handicap() {
530 let sym = make_symbol("1.180737206", 19248890, Decimal::ZERO);
531 assert_eq!(sym.to_string(), "1.180737206-19248890");
532 }
533
534 #[rstest]
535 fn test_make_symbol_with_handicap() {
536 let sym = make_symbol("1.180737206", 19248890, Decimal::new(-5, 1));
537 assert_eq!(sym.to_string(), "1.180737206-19248890--0.5");
538 }
539
540 #[rstest]
541 fn test_parse_betfair_timestamp_utc() {
542 let ts = parse_betfair_timestamp("2023-11-27T05:43:00Z").unwrap();
543 assert!(ts.as_u64() > 0);
544 }
545
546 #[rstest]
547 fn test_parse_betfair_timestamp_with_offset() {
548 let ts = parse_betfair_timestamp("2021-03-19T12:07:00+10:00").unwrap();
549 assert!(ts.as_u64() > 0);
550 }
551
552 #[rstest]
553 fn test_parse_betfair_timestamp_with_millis() {
554 let ts = parse_betfair_timestamp("2021-03-19T08:50:00.000Z").unwrap();
555 assert!(ts.as_u64() > 0);
556 }
557
558 #[rstest]
559 fn test_parse_millis_timestamp() {
560 let ts = parse_millis_timestamp(1_471_370_159_007);
561 assert_eq!(ts.as_u64(), 1_471_370_159_007 * 1_000_000);
562 }
563
564 #[rstest]
565 #[case(Decimal::new(242, 2), Decimal::new(242, 2))]
566 #[case(Decimal::new(1, 0), Decimal::new(100, 2))]
567 #[case(Decimal::new(4_287_000_000_000_001, 14), Decimal::new(4287, 2))]
568 fn test_parse_betfair_price_uses_betfair_precision(
569 #[case] input: Decimal,
570 #[case] expected: Decimal,
571 ) {
572 let price = parse_betfair_price(input).unwrap();
573
574 assert_eq!(price.as_decimal(), expected);
575 assert_eq!(price.precision, BETFAIR_PRICE_PRECISION);
576 }
577
578 #[rstest]
579 #[case(Decimal::new(100, 0), Decimal::new(10000, 2))]
580 #[case(Decimal::ZERO, Decimal::ZERO)]
581 #[case(Decimal::new(4_287_000_000_000_001, 14), Decimal::new(4287, 2))]
582 fn test_parse_betfair_quantity_uses_betfair_precision(
583 #[case] input: Decimal,
584 #[case] expected: Decimal,
585 ) {
586 let quantity = parse_betfair_quantity(input).unwrap();
587
588 assert_eq!(quantity.as_decimal(), expected);
589 assert_eq!(quantity.precision, BETFAIR_QUANTITY_PRECISION);
590 }
591
592 #[rstest]
593 #[case(Decimal::new(-1, 0))]
594 #[case(Decimal::new(-1, 2))]
595 fn test_parse_betfair_quantity_rejects_negative(#[case] input: Decimal) {
596 let result = parse_betfair_quantity(input);
597
598 assert!(result.is_err());
599 }
600
601 #[rstest]
602 #[case(Decimal::new(4_287_000_000_000_001, 14), Decimal::new(4287, 2))]
603 #[case(Decimal::new(2555, 3), Decimal::new(256, 2))]
604 fn test_normalize_betfair_price_rounds_to_betfair_precision(
605 #[case] input: Decimal,
606 #[case] expected: Decimal,
607 ) {
608 let normalized = normalize_betfair_price(input);
609
610 assert_eq!(normalized, expected);
611 }
612
613 #[rstest]
614 #[case(Decimal::new(4_287_000_000_000_001, 14), Decimal::new(4287, 2))]
615 #[case(Decimal::new(2555, 3), Decimal::new(256, 2))]
616 fn test_normalize_betfair_quantity_rounds_to_betfair_precision(
617 #[case] input: Decimal,
618 #[case] expected: Decimal,
619 ) {
620 let normalized = normalize_betfair_quantity(input);
621
622 assert_eq!(normalized, expected);
623 }
624
625 #[rstest]
626 fn test_parse_market_catalogue() {
627 let data = load_test_json("rest/list_market_catalogue.json");
628 let catalogue: MarketCatalogue = serde_json::from_str(&data).unwrap();
629 let instruments =
630 parse_market_catalogue(&catalogue, Currency::GBP(), UnixNanos::default(), None)
631 .unwrap();
632
633 assert_eq!(instruments.len(), 3);
634
635 if let InstrumentAny::Betting(inst) = &instruments[0] {
637 assert_eq!(inst.market_id.as_str(), "1.221718403");
638 assert_eq!(inst.selection_id, 20075720);
639 assert_eq!(inst.selection_name.as_str(), "1. Searover");
640 assert_eq!(inst.event_type_name.as_str(), "Horse Racing");
641 assert_eq!(inst.event_name.as_str(), "Globe Derby (AUS) 27th Nov");
642 assert_eq!(inst.event_country_code.as_str(), "AU");
643 assert_eq!(inst.market_type.as_str(), "WIN");
644 assert_eq!(inst.betting_type.as_str(), "ODDS");
645 assert_eq!(inst.price_precision, 2);
646 assert_eq!(inst.size_precision, 2);
647 assert_eq!(inst.currency, Currency::GBP());
648 } else {
649 panic!("expected BettingInstrument");
650 }
651 }
652
653 #[rstest]
654 fn test_parse_market_catalogue_batch() {
655 let data = load_test_json("rest/betting_list_market_catalogue.json");
656 let catalogues: Vec<MarketCatalogue> = serde_json::from_str(&data).unwrap();
657
658 let mut total = 0;
659
660 for cat in &catalogues {
661 let instruments =
662 parse_market_catalogue(cat, Currency::GBP(), UnixNanos::default(), None).unwrap();
663 total += instruments.len();
664 }
665 assert!(total > 0);
666 }
667
668 #[rstest]
669 fn test_parse_market_definition_from_stream() {
670 let data = load_test_json("stream/mcm_SUB_IMAGE.json");
671 let msg: StreamMessage = serde_json::from_str(&data).unwrap();
672
673 if let StreamMessage::MarketChange(mcm) = msg {
674 let mc = mcm.mc.as_ref().expect("market changes");
675 let change = &mc[0];
676 let def = change
677 .market_definition
678 .as_ref()
679 .expect("market definition");
680
681 let instruments = parse_market_definition(
682 &change.id,
683 def,
684 Currency::GBP(),
685 parse_millis_timestamp(mcm.pt),
686 None,
687 )
688 .unwrap();
689
690 assert_eq!(instruments.len(), 7);
691
692 if let InstrumentAny::Betting(inst) = &instruments[0] {
693 assert_eq!(inst.market_id.as_str(), "1.180737206");
694 assert_eq!(inst.market_type.as_str(), "WIN");
695 } else {
696 panic!("expected BettingInstrument");
697 }
698 } else {
699 panic!("expected MarketChange message");
700 }
701 }
702
703 #[rstest]
704 fn test_parse_account_state() {
705 let data = load_test_json("rest/account_funds_with_exposure.json");
706 let funds: AccountFundsResponse = serde_json::from_str(&data).unwrap();
707
708 let state = parse_account_state(
709 &funds,
710 AccountId::from("BETFAIR-001"),
711 Currency::GBP(),
712 UnixNanos::default(),
713 UnixNanos::default(),
714 )
715 .unwrap();
716
717 assert_eq!(state.account_type, AccountType::Betting);
718 assert_eq!(state.balances.len(), 1);
719 assert!(state.is_reported);
720 assert_eq!(state.base_currency, Some(Currency::GBP()));
721 }
722
723 #[rstest]
724 fn test_extract_market_id_no_handicap() {
725 let instrument_id = make_instrument_id("1.180737206", 19248890, Decimal::ZERO);
726 let market_id = extract_market_id(&instrument_id).unwrap();
727 assert_eq!(market_id, "1.180737206");
728 }
729
730 #[rstest]
731 fn test_extract_market_id_with_handicap() {
732 let instrument_id = make_instrument_id("1.180737206", 19248890, Decimal::new(15, 1));
733 let market_id = extract_market_id(&instrument_id).unwrap();
734 assert_eq!(market_id, "1.180737206");
735 }
736
737 #[rstest]
738 fn test_extract_selection_id_no_handicap() {
739 let instrument_id = make_instrument_id("1.180737206", 19248890, Decimal::ZERO);
740 let (selection_id, handicap) = extract_selection_id(&instrument_id).unwrap();
741 assert_eq!(selection_id, 19248890);
742 assert_eq!(handicap, Decimal::ZERO);
743 }
744
745 #[rstest]
746 fn test_extract_selection_id_with_handicap() {
747 let instrument_id = make_instrument_id("1.180737206", 19248890, Decimal::new(15, 1));
748 let (selection_id, handicap) = extract_selection_id(&instrument_id).unwrap();
749 assert_eq!(selection_id, 19248890);
750 assert_eq!(handicap, Decimal::new(15, 1));
751 }
752
753 #[rstest]
754 fn test_make_customer_order_ref_short_id() {
755 let result = make_customer_order_ref("O-20240101-001");
756 assert_eq!(result, "O-20240101-001");
757 }
758
759 #[rstest]
760 fn test_make_customer_order_ref_exactly_32_chars() {
761 let id = "12345678901234567890123456789012";
762 assert_eq!(id.len(), 32);
763 let result = make_customer_order_ref(id);
764 assert_eq!(result, id);
765 }
766
767 #[rstest]
768 fn test_make_customer_order_ref_truncates_to_last_32() {
769 let id = "O-20240101-550e8400-e29b-41d4-a716-446655440000";
771 assert!(id.len() > 32);
772 let result = make_customer_order_ref(id);
773 assert_eq!(result.len(), 32);
774 assert_eq!(result, &id[id.len() - 32..]);
776 }
777
778 #[rstest]
779 fn test_make_customer_order_ref_legacy_short_id() {
780 let result = make_customer_order_ref_legacy("O-20240101-001");
781 assert_eq!(result, "O-20240101-001");
782 }
783
784 #[rstest]
785 fn test_make_customer_order_ref_legacy_truncates_to_first_32() {
786 let id = "O-20240101-550e8400-e29b-41d4-a716-446655440000";
787 assert!(id.len() > 32);
788 let result = make_customer_order_ref_legacy(id);
789 assert_eq!(result.len(), 32);
790 assert_eq!(result, &id[..32]);
791 }
792
793 #[rstest]
794 fn test_legacy_and_current_differ_for_long_ids() {
795 let id = "O-20240101-550e8400-e29b-41d4-a716-446655440000";
796 let current = make_customer_order_ref(id);
797 let legacy = make_customer_order_ref_legacy(id);
798 assert_ne!(current, legacy);
799 }
800
801 #[rstest]
802 fn test_legacy_and_current_same_for_short_ids() {
803 let id = "O-20240101-001";
804 let current = make_customer_order_ref(id);
805 let legacy = make_customer_order_ref_legacy(id);
806 assert_eq!(current, legacy);
807 }
808}