headless_lms_credit_registration/workflow/
counts.rs1use uuid::Uuid;
4
5use super::decision::Applied;
6
7#[derive(Debug, Default)]
12pub(crate) struct Counts {
13 processed: i32,
14 waiting: i32,
16 failed: i32,
17 moved_on: i32,
19 finding: Option<String>,
20}
21
22impl Counts {
23 pub(crate) fn processed(count: i64) -> Self {
26 Self {
27 processed: count.try_into().unwrap_or(i32::MAX),
28 ..Self::default()
29 }
30 }
31
32 pub(crate) fn processed_with_failures(processed: i32, failed: i32) -> Self {
34 debug_assert!(failed <= processed, "{failed} failed of {processed}");
35 Self {
36 processed,
37 failed,
38 ..Self::default()
39 }
40 }
41
42 pub(crate) fn all_failed(count: i32) -> Self {
43 Self::processed_with_failures(count, count)
44 }
45
46 pub(crate) fn with_finding(self, finding: Option<String>) -> Self {
49 Self { finding, ..self }
50 }
51
52 pub(crate) fn record_decided(&mut self, is_failure: bool) {
54 self.record_verdict(is_failure, false);
55 }
56
57 fn record_verdict(&mut self, is_failure: bool, is_waiting: bool) {
58 self.processed += 1;
59 self.failed += i32::from(is_failure);
60 self.waiting += i32::from(is_waiting);
61 }
62
63 pub(crate) fn record_applied(&mut self, registration_id: Uuid, applied: Applied) {
66 match applied {
67 Applied::Written {
68 is_failure,
69 is_waiting,
70 } => self.record_verdict(is_failure, is_waiting),
71 Applied::MovedOn { found } => {
72 self.moved_on += 1;
73 debug!(
74 credit_registration_id = %registration_id,
75 found_state = ?found,
76 "Credit registration moved on while the study registry answered; leaving it"
77 );
78 }
79 }
80 }
81
82 pub(crate) fn processed_count(&self) -> i32 {
84 self.processed
85 }
86
87 pub(crate) fn waiting_count(&self) -> i32 {
89 self.waiting
90 }
91
92 pub(crate) fn failed_count(&self) -> i32 {
94 self.failed
95 }
96
97 pub(crate) fn moved_on_count(&self) -> i32 {
98 self.moved_on
99 }
100
101 pub(crate) fn into_finding(self) -> Option<String> {
102 self.finding
103 }
104}
105
106impl std::ops::AddAssign for Counts {
107 fn add_assign(&mut self, other: Self) {
108 self.processed += other.processed;
109 self.waiting += other.waiting;
110 self.failed += other.failed;
111 self.moved_on += other.moved_on;
112 self.finding = self.finding.take().or(other.finding);
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use headless_lms_models::credit_registrations::CreditRegistrationState as State;
119
120 use super::*;
121
122 fn written(is_failure: bool) -> Applied {
123 Applied::Written {
124 is_failure,
125 is_waiting: false,
126 }
127 }
128
129 const MOVED_ON: Applied = Applied::MovedOn {
130 found: State::Registered,
131 };
132
133 #[test]
134 fn a_moved_on_row_is_counted_apart_from_the_processed_ones() {
135 let mut counts = Counts::default();
136 for applied in [written(false), written(true), MOVED_ON, MOVED_ON] {
137 counts.record_applied(Uuid::new_v4(), applied);
138 }
139 assert_eq!(counts.processed_count(), 2);
140 assert_eq!(counts.failed_count(), 1);
141 assert_eq!(counts.moved_on_count(), 2);
142 }
143
144 #[test]
145 fn failures_never_outnumber_processed_rows() {
146 let steps = [Some(true), None, Some(false), Some(true), None, Some(true)];
147 let mut counts = Counts::default();
148 for step in steps {
149 match step {
150 Some(is_failure) => counts.record_decided(is_failure),
151 None => counts.record_applied(Uuid::new_v4(), MOVED_ON),
152 }
153 assert!(counts.failed_count() <= counts.processed_count());
154 }
155 assert_eq!(counts.processed_count(), 4);
156 assert_eq!(counts.failed_count(), 3);
157 }
158
159 #[test]
160 fn adding_counts_sums_them_and_keeps_the_first_finding() {
161 let mut counts = Counts::processed_with_failures(3, 1).with_finding(None);
162 let mut moved = Counts::all_failed(2).with_finding(Some("second".to_string()));
163 moved.record_applied(Uuid::new_v4(), MOVED_ON);
164 counts += moved;
165 counts += Counts::processed(4).with_finding(Some("third".to_string()));
166 assert_eq!(counts.processed_count(), 9);
167 assert_eq!(counts.failed_count(), 3);
168 assert_eq!(counts.moved_on_count(), 1);
169 assert_eq!(counts.into_finding().as_deref(), Some("second"));
170 }
171
172 #[test]
173 fn an_oversized_sweep_saturates_instead_of_going_negative() {
174 assert_eq!(Counts::processed(i64::MAX).processed_count(), i32::MAX);
175 }
176}