1use serde::{Deserialize, Serialize};
25
26use crate::{
27 error::{ExoError, Result},
28 hash::hash_structured,
29 hlc::HybridClock,
30 types::{CorrelationId, Did, Hash256, Timestamp},
31};
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
39pub enum BctsState {
40 Draft,
41 Submitted,
42 IdentityResolved,
43 ConsentValidated,
44 Deliberated,
45 Verified,
46 Governed,
47 Approved,
48 Executed,
49 Recorded,
50 Closed,
51 Denied,
52 Escalated,
53 Remediated,
54}
55
56impl BctsState {
57 #[must_use]
59 pub const fn as_str(self) -> &'static str {
60 match self {
61 Self::Draft => "Draft",
62 Self::Submitted => "Submitted",
63 Self::IdentityResolved => "IdentityResolved",
64 Self::ConsentValidated => "ConsentValidated",
65 Self::Deliberated => "Deliberated",
66 Self::Verified => "Verified",
67 Self::Governed => "Governed",
68 Self::Approved => "Approved",
69 Self::Executed => "Executed",
70 Self::Recorded => "Recorded",
71 Self::Closed => "Closed",
72 Self::Denied => "Denied",
73 Self::Escalated => "Escalated",
74 Self::Remediated => "Remediated",
75 }
76 }
77
78 #[must_use]
80 pub fn valid_transitions(self) -> &'static [BctsState] {
81 use BctsState::*;
82 match self {
83 Draft => &[Submitted],
84 Submitted => &[IdentityResolved, Denied],
85 IdentityResolved => &[ConsentValidated, Denied],
86 ConsentValidated => &[Deliberated, Denied],
87 Deliberated => &[Verified, Denied, Escalated],
88 Verified => &[Governed, Denied, Escalated],
89 Governed => &[Approved, Denied, Escalated],
90 Approved => &[Executed, Denied],
91 Executed => &[Recorded, Escalated],
92 Recorded => &[Closed, Escalated],
93 Closed => &[],
94 Denied => &[Remediated],
95 Escalated => &[Deliberated, Denied, Remediated],
96 Remediated => &[Submitted],
97 }
98 }
99
100 #[must_use]
102 pub fn can_transition_to(self, target: BctsState) -> bool {
103 self.valid_transitions().contains(&target)
104 }
105}
106
107impl core::fmt::Display for BctsState {
108 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
109 f.write_str(self.as_str())
110 }
111}
112
113#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
119pub struct BctsTransition {
120 pub from_state: BctsState,
121 pub to_state: BctsState,
122 pub timestamp: Timestamp,
123 pub receipt_hash: Hash256,
124 pub actor_did: Did,
125}
126
127#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
130pub struct BctsTransitionRequest {
131 pub correlation_id: CorrelationId,
132 pub from_state: BctsState,
133 pub to_state: BctsState,
134 pub actor_did: Did,
135 pub prior_receipt_hash: Hash256,
136}
137
138pub trait BctsTransitionAdjudicator {
140 fn adjudicate_transition(&self, request: &BctsTransitionRequest) -> Result<()>;
147}
148
149impl<F> BctsTransitionAdjudicator for F
150where
151 F: Fn(&BctsTransitionRequest) -> Result<()>,
152{
153 fn adjudicate_transition(&self, request: &BctsTransitionRequest) -> Result<()> {
154 self(request)
155 }
156}
157
158pub trait BailmentTransaction {
164 fn state(&self) -> BctsState;
166
167 fn transition(
174 &mut self,
175 to: BctsState,
176 actor: &Did,
177 clock: &mut HybridClock,
178 adjudicator: &dyn BctsTransitionAdjudicator,
179 ) -> Result<BctsTransition>;
180
181 fn receipt_chain(&self) -> &[Hash256];
183
184 fn correlation_id(&self) -> &CorrelationId;
186}
187
188#[derive(Clone, Debug, Serialize, Deserialize)]
194pub struct Transaction {
195 correlation_id: CorrelationId,
196 current_state: BctsState,
197 receipt_chain: Vec<Hash256>,
198 transitions: Vec<BctsTransition>,
199}
200
201impl Transaction {
202 #[must_use]
204 pub fn new(correlation_id: CorrelationId) -> Self {
205 Self {
206 correlation_id,
207 current_state: BctsState::Draft,
208 receipt_chain: Vec::new(),
209 transitions: Vec::new(),
210 }
211 }
212
213 #[must_use]
215 pub fn transitions(&self) -> &[BctsTransition] {
216 &self.transitions
217 }
218
219 fn compute_receipt(
221 &self,
222 from: BctsState,
223 to: BctsState,
224 timestamp: &Timestamp,
225 actor: &Did,
226 ) -> Result<Hash256> {
227 #[derive(Serialize)]
229 struct ReceiptInput<'a> {
230 from: BctsState,
231 to: BctsState,
232 timestamp: &'a Timestamp,
233 actor: &'a str,
234 prev_hash: Hash256,
235 }
236 let prev = self.receipt_chain.last().copied().unwrap_or(Hash256::ZERO);
237 let input = ReceiptInput {
238 from,
239 to,
240 timestamp,
241 actor: actor.as_str(),
242 prev_hash: prev,
243 };
244 hash_structured(&input)
245 }
246
247 pub fn verify_receipt_chain(&self) -> Result<()> {
252 let mut prev = Hash256::ZERO;
253 for (i, transition) in self.transitions.iter().enumerate() {
254 #[derive(Serialize)]
255 struct ReceiptInput<'a> {
256 from: BctsState,
257 to: BctsState,
258 timestamp: &'a Timestamp,
259 actor: &'a str,
260 prev_hash: Hash256,
261 }
262 let input = ReceiptInput {
263 from: transition.from_state,
264 to: transition.to_state,
265 timestamp: &transition.timestamp,
266 actor: transition.actor_did.as_str(),
267 prev_hash: prev,
268 };
269 let computed = hash_structured(&input)?;
270 if computed != transition.receipt_hash {
271 return Err(ExoError::ReceiptChainBroken { index: i });
272 }
273 if i < self.receipt_chain.len() && self.receipt_chain[i] != computed {
274 return Err(ExoError::ReceiptChainBroken { index: i });
275 }
276 prev = computed;
277 }
278 Ok(())
279 }
280}
281
282impl BailmentTransaction for Transaction {
283 fn state(&self) -> BctsState {
284 self.current_state
285 }
286
287 fn transition(
288 &mut self,
289 to: BctsState,
290 actor: &Did,
291 clock: &mut HybridClock,
292 adjudicator: &dyn BctsTransitionAdjudicator,
293 ) -> Result<BctsTransition> {
294 let from = self.current_state;
295 if !from.can_transition_to(to) {
296 return Err(ExoError::InvalidTransition {
297 from: from.to_string(),
298 to: to.to_string(),
299 });
300 }
301
302 let prior_receipt_hash = self.receipt_chain.last().copied().unwrap_or(Hash256::ZERO);
303 adjudicator.adjudicate_transition(&BctsTransitionRequest {
304 correlation_id: self.correlation_id,
305 from_state: from,
306 to_state: to,
307 actor_did: actor.clone(),
308 prior_receipt_hash,
309 })?;
310
311 let timestamp = clock.now()?;
312 let receipt_hash = self.compute_receipt(from, to, ×tamp, actor)?;
313
314 let transition = BctsTransition {
315 from_state: from,
316 to_state: to,
317 timestamp,
318 receipt_hash,
319 actor_did: actor.clone(),
320 };
321
322 self.current_state = to;
323 self.receipt_chain.push(receipt_hash);
324 self.transitions.push(transition.clone());
325
326 Ok(transition)
327 }
328
329 fn receipt_chain(&self) -> &[Hash256] {
330 &self.receipt_chain
331 }
332
333 fn correlation_id(&self) -> &CorrelationId {
334 &self.correlation_id
335 }
336}
337
338#[cfg(test)]
343mod tests {
344 use super::*;
345
346 macro_rules! correlation_id {
347 () => {
348 CorrelationId::from_uuid(uuid::Uuid::from_u128(u128::from(line!())))
349 };
350 }
351
352 fn test_clock() -> HybridClock {
353 let counter = std::sync::atomic::AtomicU64::new(1000);
354 HybridClock::with_wall_clock(move || {
355 counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
356 })
357 }
358
359 fn test_did() -> Did {
360 Did::new("did:exo:test-actor").expect("valid")
361 }
362
363 struct AllowAllAdjudicator;
364
365 impl BctsTransitionAdjudicator for AllowAllAdjudicator {
366 fn adjudicate_transition(&self, _request: &BctsTransitionRequest) -> Result<()> {
367 Ok(())
368 }
369 }
370
371 #[test]
374 fn state_display() {
375 assert_eq!(BctsState::Draft.to_string(), "Draft");
376 assert_eq!(BctsState::Closed.to_string(), "Closed");
377 }
378
379 #[test]
380 fn draft_can_only_go_to_submitted() {
381 assert!(BctsState::Draft.can_transition_to(BctsState::Submitted));
382 assert!(!BctsState::Draft.can_transition_to(BctsState::Closed));
383 assert!(!BctsState::Draft.can_transition_to(BctsState::Draft));
384 }
385
386 #[test]
387 fn submitted_transitions() {
388 assert!(BctsState::Submitted.can_transition_to(BctsState::IdentityResolved));
389 assert!(BctsState::Submitted.can_transition_to(BctsState::Denied));
390 assert!(!BctsState::Submitted.can_transition_to(BctsState::Closed));
391 }
392
393 #[test]
394 fn identity_resolved_transitions() {
395 assert!(BctsState::IdentityResolved.can_transition_to(BctsState::ConsentValidated));
396 assert!(BctsState::IdentityResolved.can_transition_to(BctsState::Denied));
397 assert!(!BctsState::IdentityResolved.can_transition_to(BctsState::Submitted));
398 }
399
400 #[test]
401 fn consent_validated_transitions() {
402 assert!(BctsState::ConsentValidated.can_transition_to(BctsState::Deliberated));
403 assert!(BctsState::ConsentValidated.can_transition_to(BctsState::Denied));
404 assert!(!BctsState::ConsentValidated.can_transition_to(BctsState::Executed));
405 }
406
407 #[test]
408 fn deliberated_transitions() {
409 assert!(BctsState::Deliberated.can_transition_to(BctsState::Verified));
410 assert!(BctsState::Deliberated.can_transition_to(BctsState::Denied));
411 assert!(BctsState::Deliberated.can_transition_to(BctsState::Escalated));
412 assert!(!BctsState::Deliberated.can_transition_to(BctsState::Closed));
413 }
414
415 #[test]
416 fn verified_transitions() {
417 assert!(BctsState::Verified.can_transition_to(BctsState::Governed));
418 assert!(BctsState::Verified.can_transition_to(BctsState::Denied));
419 assert!(BctsState::Verified.can_transition_to(BctsState::Escalated));
420 }
421
422 #[test]
423 fn governed_transitions() {
424 assert!(BctsState::Governed.can_transition_to(BctsState::Approved));
425 assert!(BctsState::Governed.can_transition_to(BctsState::Denied));
426 assert!(BctsState::Governed.can_transition_to(BctsState::Escalated));
427 }
428
429 #[test]
430 fn approved_transitions() {
431 assert!(BctsState::Approved.can_transition_to(BctsState::Executed));
432 assert!(BctsState::Approved.can_transition_to(BctsState::Denied));
433 assert!(!BctsState::Approved.can_transition_to(BctsState::Escalated));
434 }
435
436 #[test]
437 fn executed_transitions() {
438 assert!(BctsState::Executed.can_transition_to(BctsState::Recorded));
439 assert!(BctsState::Executed.can_transition_to(BctsState::Escalated));
440 assert!(!BctsState::Executed.can_transition_to(BctsState::Denied));
441 }
442
443 #[test]
444 fn recorded_transitions() {
445 assert!(BctsState::Recorded.can_transition_to(BctsState::Closed));
446 assert!(BctsState::Recorded.can_transition_to(BctsState::Escalated));
447 assert!(!BctsState::Recorded.can_transition_to(BctsState::Denied));
448 }
449
450 #[test]
451 fn closed_is_terminal() {
452 assert!(BctsState::Closed.valid_transitions().is_empty());
453 assert!(!BctsState::Closed.can_transition_to(BctsState::Draft));
454 }
455
456 #[test]
457 fn denied_transitions() {
458 assert!(BctsState::Denied.can_transition_to(BctsState::Remediated));
459 assert!(!BctsState::Denied.can_transition_to(BctsState::Closed));
460 }
461
462 #[test]
463 fn escalated_transitions() {
464 assert!(BctsState::Escalated.can_transition_to(BctsState::Deliberated));
465 assert!(BctsState::Escalated.can_transition_to(BctsState::Denied));
466 assert!(BctsState::Escalated.can_transition_to(BctsState::Remediated));
467 }
468
469 #[test]
470 fn remediated_transitions() {
471 assert!(BctsState::Remediated.can_transition_to(BctsState::Submitted));
472 assert!(!BctsState::Remediated.can_transition_to(BctsState::Closed));
473 }
474
475 #[test]
476 fn state_serde_roundtrip() {
477 let s = BctsState::Governed;
478 let json = serde_json::to_string(&s).expect("ser");
479 let s2: BctsState = serde_json::from_str(&json).expect("de");
480 assert_eq!(s, s2);
481 }
482
483 #[test]
484 fn state_ord() {
485 let mut states = vec![BctsState::Closed, BctsState::Draft, BctsState::Executed];
487 states.sort();
488 let mut states2 = vec![BctsState::Closed, BctsState::Draft, BctsState::Executed];
490 states2.sort();
491 assert_eq!(states, states2);
492 }
493
494 #[test]
497 fn new_transaction_is_draft() {
498 let cid = correlation_id!();
499 let tx = Transaction::new(cid);
500 assert_eq!(tx.state(), BctsState::Draft);
501 assert!(tx.receipt_chain().is_empty());
502 assert!(tx.transitions().is_empty());
503 assert_eq!(*tx.correlation_id(), cid);
504 }
505
506 #[test]
507 fn transition_invokes_adjudicator_before_state_mutation() {
508 struct DenyingAdjudicator;
509
510 impl BctsTransitionAdjudicator for DenyingAdjudicator {
511 fn adjudicate_transition(&self, _request: &BctsTransitionRequest) -> Result<()> {
512 Err(ExoError::InvariantViolation {
513 description: "test denial".into(),
514 })
515 }
516 }
517
518 let mut clock = test_clock();
519 let actor = test_did();
520 let mut tx = Transaction::new(correlation_id!());
521
522 let err = tx
523 .transition(
524 BctsState::Submitted,
525 &actor,
526 &mut clock,
527 &DenyingAdjudicator,
528 )
529 .expect_err("transition must fail before mutation when adjudication denies");
530
531 assert!(matches!(err, ExoError::InvariantViolation { .. }));
532 assert_eq!(tx.state(), BctsState::Draft);
533 assert!(tx.receipt_chain().is_empty());
534 assert!(tx.transitions().is_empty());
535 }
536
537 #[test]
538 fn transition_supplies_canonical_request_to_adjudicator() {
539 use std::cell::RefCell;
540
541 struct RecordingAdjudicator {
542 request: RefCell<Option<BctsTransitionRequest>>,
543 }
544
545 impl BctsTransitionAdjudicator for RecordingAdjudicator {
546 fn adjudicate_transition(&self, request: &BctsTransitionRequest) -> Result<()> {
547 self.request.replace(Some(request.clone()));
548 Ok(())
549 }
550 }
551
552 let mut clock = test_clock();
553 let actor = test_did();
554 let correlation_id = correlation_id!();
555 let mut tx = Transaction::new(correlation_id);
556 let adjudicator = RecordingAdjudicator {
557 request: RefCell::new(None),
558 };
559
560 tx.transition(BctsState::Submitted, &actor, &mut clock, &adjudicator)
561 .expect("transition ok");
562
563 let request = adjudicator
564 .request
565 .take()
566 .expect("adjudicator received request");
567 assert_eq!(request.correlation_id, correlation_id);
568 assert_eq!(request.from_state, BctsState::Draft);
569 assert_eq!(request.to_state, BctsState::Submitted);
570 assert_eq!(request.actor_did, actor);
571 assert_eq!(request.prior_receipt_hash, Hash256::ZERO);
572 }
573
574 #[test]
575 fn transition_source_invokes_adjudicator_before_hlc_and_mutation() {
576 let source = include_str!("bcts.rs");
577 let implementation = source
578 .split("impl BailmentTransaction for Transaction")
579 .nth(1)
580 .expect("transaction impl");
581 let adjudicator_call = implementation
582 .find("adjudicator.adjudicate_transition")
583 .expect("adjudicator call");
584 let hlc_tick = implementation.find("clock.now()").expect("HLC tick");
585 let mutation = implementation
586 .find("self.current_state = to")
587 .expect("state mutation");
588
589 assert!(
590 adjudicator_call < hlc_tick,
591 "BCTS must adjudicate before consuming an HLC tick"
592 );
593 assert!(
594 adjudicator_call < mutation,
595 "BCTS must adjudicate before mutating state"
596 );
597 }
598
599 #[test]
600 fn happy_path_full_lifecycle() {
601 let mut clock = test_clock();
602 let actor = test_did();
603 let mut tx = Transaction::new(correlation_id!());
604
605 let steps = [
606 BctsState::Submitted,
607 BctsState::IdentityResolved,
608 BctsState::ConsentValidated,
609 BctsState::Deliberated,
610 BctsState::Verified,
611 BctsState::Governed,
612 BctsState::Approved,
613 BctsState::Executed,
614 BctsState::Recorded,
615 BctsState::Closed,
616 ];
617
618 for (i, &target) in steps.iter().enumerate() {
619 let t = tx
620 .transition(target, &actor, &mut clock, &AllowAllAdjudicator)
621 .expect("transition ok");
622 assert_eq!(t.to_state, target);
623 assert_eq!(tx.state(), target);
624 assert_eq!(tx.receipt_chain().len(), i + 1);
625 }
626
627 tx.verify_receipt_chain().expect("chain valid");
629 }
630
631 #[test]
632 fn invalid_transition_from_draft_to_closed() {
633 let mut clock = test_clock();
634 let actor = test_did();
635 let mut tx = Transaction::new(correlation_id!());
636
637 let err = tx
638 .transition(BctsState::Closed, &actor, &mut clock, &AllowAllAdjudicator)
639 .unwrap_err();
640 assert!(matches!(err, ExoError::InvalidTransition { .. }));
641 assert_eq!(tx.state(), BctsState::Draft);
643 }
644
645 #[test]
646 fn invalid_transition_from_closed() {
647 let mut clock = test_clock();
648 let actor = test_did();
649 let mut tx = Transaction::new(correlation_id!());
650
651 for &s in &[
653 BctsState::Submitted,
654 BctsState::IdentityResolved,
655 BctsState::ConsentValidated,
656 BctsState::Deliberated,
657 BctsState::Verified,
658 BctsState::Governed,
659 BctsState::Approved,
660 BctsState::Executed,
661 BctsState::Recorded,
662 BctsState::Closed,
663 ] {
664 tx.transition(s, &actor, &mut clock, &AllowAllAdjudicator)
665 .expect("ok");
666 }
667
668 let err = tx
670 .transition(BctsState::Draft, &actor, &mut clock, &AllowAllAdjudicator)
671 .unwrap_err();
672 assert!(matches!(err, ExoError::InvalidTransition { .. }));
673 }
674
675 #[test]
676 fn denial_path() {
677 let mut clock = test_clock();
678 let actor = test_did();
679 let mut tx = Transaction::new(correlation_id!());
680
681 tx.transition(
682 BctsState::Submitted,
683 &actor,
684 &mut clock,
685 &AllowAllAdjudicator,
686 )
687 .expect("ok");
688 tx.transition(BctsState::Denied, &actor, &mut clock, &AllowAllAdjudicator)
689 .expect("ok");
690 assert_eq!(tx.state(), BctsState::Denied);
691 tx.verify_receipt_chain().expect("chain valid");
692 }
693
694 #[test]
695 fn escalation_path() {
696 let mut clock = test_clock();
697 let actor = test_did();
698 let mut tx = Transaction::new(correlation_id!());
699
700 for &s in &[
701 BctsState::Submitted,
702 BctsState::IdentityResolved,
703 BctsState::ConsentValidated,
704 BctsState::Deliberated,
705 BctsState::Escalated,
706 BctsState::Deliberated,
707 BctsState::Verified,
708 ] {
709 tx.transition(s, &actor, &mut clock, &AllowAllAdjudicator)
710 .expect("ok");
711 }
712 assert_eq!(tx.state(), BctsState::Verified);
713 tx.verify_receipt_chain().expect("chain valid");
714 }
715
716 #[test]
717 fn remediation_path() {
718 let mut clock = test_clock();
719 let actor = test_did();
720 let mut tx = Transaction::new(correlation_id!());
721
722 tx.transition(
723 BctsState::Submitted,
724 &actor,
725 &mut clock,
726 &AllowAllAdjudicator,
727 )
728 .expect("ok");
729 tx.transition(BctsState::Denied, &actor, &mut clock, &AllowAllAdjudicator)
730 .expect("ok");
731 tx.transition(
732 BctsState::Remediated,
733 &actor,
734 &mut clock,
735 &AllowAllAdjudicator,
736 )
737 .expect("ok");
738 tx.transition(
739 BctsState::Submitted,
740 &actor,
741 &mut clock,
742 &AllowAllAdjudicator,
743 )
744 .expect("ok");
745 assert_eq!(tx.state(), BctsState::Submitted);
746 tx.verify_receipt_chain().expect("chain valid");
747 }
748
749 #[test]
750 fn receipt_chain_grows_monotonically() {
751 let mut clock = test_clock();
752 let actor = test_did();
753 let mut tx = Transaction::new(correlation_id!());
754
755 tx.transition(
756 BctsState::Submitted,
757 &actor,
758 &mut clock,
759 &AllowAllAdjudicator,
760 )
761 .expect("ok");
762 assert_eq!(tx.receipt_chain().len(), 1);
763
764 tx.transition(
765 BctsState::IdentityResolved,
766 &actor,
767 &mut clock,
768 &AllowAllAdjudicator,
769 )
770 .expect("ok");
771 assert_eq!(tx.receipt_chain().len(), 2);
772
773 assert_ne!(tx.receipt_chain()[0], tx.receipt_chain()[1]);
775 }
776
777 #[test]
778 fn transition_records_correct_actor() {
779 let mut clock = test_clock();
780 let actor = Did::new("did:exo:alice").expect("valid");
781 let mut tx = Transaction::new(correlation_id!());
782
783 let t = tx
784 .transition(
785 BctsState::Submitted,
786 &actor,
787 &mut clock,
788 &AllowAllAdjudicator,
789 )
790 .expect("ok");
791 assert_eq!(t.actor_did, actor);
792 }
793
794 #[test]
795 fn transition_timestamps_are_monotonic() {
796 let mut clock = test_clock();
797 let actor = test_did();
798 let mut tx = Transaction::new(correlation_id!());
799
800 tx.transition(
801 BctsState::Submitted,
802 &actor,
803 &mut clock,
804 &AllowAllAdjudicator,
805 )
806 .expect("ok");
807 tx.transition(
808 BctsState::IdentityResolved,
809 &actor,
810 &mut clock,
811 &AllowAllAdjudicator,
812 )
813 .expect("ok");
814
815 let ts = &tx.transitions();
816 assert!(ts[0].timestamp < ts[1].timestamp);
817 }
818
819 #[test]
820 fn verify_receipt_chain_detects_tampering() {
821 let mut clock = test_clock();
822 let actor = test_did();
823 let mut tx = Transaction::new(correlation_id!());
824
825 tx.transition(
826 BctsState::Submitted,
827 &actor,
828 &mut clock,
829 &AllowAllAdjudicator,
830 )
831 .expect("ok");
832 tx.transition(
833 BctsState::IdentityResolved,
834 &actor,
835 &mut clock,
836 &AllowAllAdjudicator,
837 )
838 .expect("ok");
839
840 tx.receipt_chain[0] = Hash256::ZERO;
842 let err = tx.verify_receipt_chain().unwrap_err();
843 assert!(matches!(err, ExoError::ReceiptChainBroken { index: 0 }));
844 }
845
846 #[test]
847 fn transaction_serde_roundtrip() {
848 let mut clock = test_clock();
849 let actor = test_did();
850 let mut tx = Transaction::new(correlation_id!());
851 tx.transition(
852 BctsState::Submitted,
853 &actor,
854 &mut clock,
855 &AllowAllAdjudicator,
856 )
857 .expect("ok");
858
859 let json = serde_json::to_string(&tx).expect("ser");
860 let tx2: Transaction = serde_json::from_str(&json).expect("de");
861 assert_eq!(tx.state(), tx2.state());
862 assert_eq!(tx.receipt_chain(), tx2.receipt_chain());
863 assert_eq!(tx.correlation_id(), tx2.correlation_id());
864 }
865
866 #[test]
867 fn every_invalid_transition_from_each_state() {
868 let all_states = [
870 BctsState::Draft,
871 BctsState::Submitted,
872 BctsState::IdentityResolved,
873 BctsState::ConsentValidated,
874 BctsState::Deliberated,
875 BctsState::Verified,
876 BctsState::Governed,
877 BctsState::Approved,
878 BctsState::Executed,
879 BctsState::Recorded,
880 BctsState::Closed,
881 BctsState::Denied,
882 BctsState::Escalated,
883 BctsState::Remediated,
884 ];
885
886 for &from in &all_states {
887 let valid = from.valid_transitions();
888 for &to in &all_states {
889 if valid.contains(&to) {
890 assert!(
891 from.can_transition_to(to),
892 "{from} should be able to transition to {to}"
893 );
894 } else {
895 assert!(
896 !from.can_transition_to(to),
897 "{from} should NOT be able to transition to {to}"
898 );
899 }
900 }
901 }
902 }
903
904 #[test]
905 fn escalated_to_denied_to_remediated_to_submitted() {
906 let mut clock = test_clock();
907 let actor = test_did();
908 let mut tx = Transaction::new(correlation_id!());
909
910 for &s in &[
911 BctsState::Submitted,
912 BctsState::IdentityResolved,
913 BctsState::ConsentValidated,
914 BctsState::Deliberated,
915 BctsState::Escalated,
916 BctsState::Denied,
917 BctsState::Remediated,
918 BctsState::Submitted,
919 ] {
920 tx.transition(s, &actor, &mut clock, &AllowAllAdjudicator)
921 .expect("ok");
922 }
923 assert_eq!(tx.state(), BctsState::Submitted);
924 tx.verify_receipt_chain().expect("chain valid");
925 }
926
927 #[test]
928 fn escalated_to_remediated() {
929 let mut clock = test_clock();
930 let actor = test_did();
931 let mut tx = Transaction::new(correlation_id!());
932
933 for &s in &[
934 BctsState::Submitted,
935 BctsState::IdentityResolved,
936 BctsState::ConsentValidated,
937 BctsState::Deliberated,
938 BctsState::Escalated,
939 BctsState::Remediated,
940 BctsState::Submitted,
941 ] {
942 tx.transition(s, &actor, &mut clock, &AllowAllAdjudicator)
943 .expect("ok");
944 }
945 tx.verify_receipt_chain().expect("chain valid");
946 }
947
948 #[test]
949 fn bcts_state_labels_do_not_depend_on_debug_formatting() {
950 assert_eq!(BctsState::Submitted.as_str(), "Submitted");
951 assert_eq!(BctsState::Submitted.to_string(), "Submitted");
952
953 let source = include_str!("bcts.rs");
954 let production = source
955 .split("#[cfg(test)]")
956 .next()
957 .expect("production section");
958 assert!(
959 !production.contains("write!(f, \"{self:?}\")"),
960 "BCTS display output must use explicit stable labels"
961 );
962 }
963}