aph_core/
human_present_flow.rs1#[derive(
15 std::fmt::Debug,
16 std::clone::Clone,
17 std::marker::Copy,
18 std::cmp::PartialEq,
19 std::cmp::Eq,
20 std::hash::Hash,
21 serde::Serialize,
22 serde::Deserialize,
23)]
24#[serde(rename_all = "camelCase")]
25pub enum HumanPresentNotarizationState {
26 Drafted,
28 PendingDecision,
30 Approved,
32 MandateIssued,
34 EnvelopeIssued,
36 Delivered,
38 Denied,
40}
41
42impl HumanPresentNotarizationState {
43 pub fn is_terminal(&self) -> bool {
45 matches!(self, Self::Delivered | Self::Denied)
46 }
47
48 pub fn valid_transitions(&self) -> &'static [Self] {
50 match self {
51 Self::Drafted => &[Self::PendingDecision],
52 Self::PendingDecision => &[Self::Approved, Self::Denied],
53 Self::Approved => &[Self::MandateIssued],
54 Self::MandateIssued => &[Self::EnvelopeIssued],
55 Self::EnvelopeIssued => &[Self::Delivered],
56 Self::Delivered => &[],
57 Self::Denied => &[],
58 }
59 }
60
61 pub fn can_transition_to(&self, next: Self) -> bool {
63 self.valid_transitions().contains(&next)
64 }
65}
66
67#[derive(std::fmt::Debug, std::clone::Clone, std::cmp::PartialEq, std::cmp::Eq, serde::Serialize, serde::Deserialize)]
69pub struct HumanPresentNotarizationFlow {
70 state: HumanPresentNotarizationState,
71 communication_mandate_id: String,
72 envelope_id: std::option::Option<String>,
73}
74
75impl HumanPresentNotarizationFlow {
76 pub fn new(communication_mandate_id: impl std::convert::Into<String>) -> Self {
79 Self {
80 state: HumanPresentNotarizationState::Drafted,
81 communication_mandate_id: communication_mandate_id.into(),
82 envelope_id: std::option::Option::None,
83 }
84 }
85
86 pub fn state(&self) -> HumanPresentNotarizationState {
88 self.state
89 }
90
91 pub fn communication_mandate_id(&self) -> &str {
93 &self.communication_mandate_id
94 }
95
96 pub fn envelope_id(&self) -> std::option::Option<&str> {
98 self.envelope_id.as_deref()
99 }
100
101 pub fn set_envelope_id(&mut self, id: impl std::convert::Into<String>) {
103 self.envelope_id = std::option::Option::Some(id.into());
104 }
105
106 pub fn transition_to(
109 &mut self,
110 next: HumanPresentNotarizationState,
111 ) -> std::result::Result<(), super::errors::AphError> {
112 if self.state.can_transition_to(next) {
113 self.state = next;
114 std::result::Result::Ok(())
115 } else {
116 std::result::Result::Err(super::errors::AphError::invalid_flow_transition(
117 format!("{:?}", self.state),
118 format!("{:?}", next),
119 ))
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 #[test]
127 fn terminal_states() {
128 std::assert!(super::HumanPresentNotarizationState::Delivered.is_terminal());
132 std::assert!(super::HumanPresentNotarizationState::Denied.is_terminal());
133 std::assert!(!super::HumanPresentNotarizationState::Drafted.is_terminal());
134 std::assert!(!super::HumanPresentNotarizationState::PendingDecision.is_terminal());
135 std::assert!(!super::HumanPresentNotarizationState::Approved.is_terminal());
136 std::assert!(!super::HumanPresentNotarizationState::MandateIssued.is_terminal());
137 std::assert!(!super::HumanPresentNotarizationState::EnvelopeIssued.is_terminal());
138 }
139
140 #[test]
141 fn full_transition_walk_approved_path() {
142 let mut flow = super::HumanPresentNotarizationFlow::new("cm-1");
146 std::assert_eq!(flow.state(), super::HumanPresentNotarizationState::Drafted);
147
148 flow
149 .transition_to(super::HumanPresentNotarizationState::PendingDecision)
150 .unwrap();
151 flow
152 .transition_to(super::HumanPresentNotarizationState::Approved)
153 .unwrap();
154 flow
155 .transition_to(super::HumanPresentNotarizationState::MandateIssued)
156 .unwrap();
157 flow
158 .transition_to(super::HumanPresentNotarizationState::EnvelopeIssued)
159 .unwrap();
160 flow
161 .transition_to(super::HumanPresentNotarizationState::Delivered)
162 .unwrap();
163
164 std::assert_eq!(
165 flow.state(),
166 super::HumanPresentNotarizationState::Delivered
167 );
168 std::assert!(flow.state().is_terminal());
169 std::assert_eq!(flow.communication_mandate_id(), "cm-1");
170 }
171
172 #[test]
173 fn denial_path() {
174 let mut flow = super::HumanPresentNotarizationFlow::new("cm-deny");
178 flow
179 .transition_to(super::HumanPresentNotarizationState::PendingDecision)
180 .unwrap();
181 flow
182 .transition_to(super::HumanPresentNotarizationState::Denied)
183 .unwrap();
184
185 std::assert_eq!(flow.state(), super::HumanPresentNotarizationState::Denied);
186 std::assert!(flow.state().is_terminal());
187 }
188
189 #[test]
190 fn terminal_no_transitions() {
191 std::assert!(
195 super::HumanPresentNotarizationState::Delivered
196 .valid_transitions()
197 .is_empty()
198 );
199 std::assert!(
200 super::HumanPresentNotarizationState::Denied
201 .valid_transitions()
202 .is_empty()
203 );
204 }
205
206 #[test]
207 fn invalid_transition_returns_error() {
208 let mut flow = super::HumanPresentNotarizationFlow::new("cm-bad");
211 let err = flow
212 .transition_to(super::HumanPresentNotarizationState::Delivered)
213 .unwrap_err();
214 std::assert_eq!(err.code(), "APH_E002");
215 }
216
217 #[test]
218 fn skip_state_rejected() {
219 let mut flow = super::HumanPresentNotarizationFlow::new("cm-skip");
223 let err = flow
224 .transition_to(super::HumanPresentNotarizationState::MandateIssued)
225 .unwrap_err();
226 std::assert_eq!(err.code(), "APH_E002");
227 std::assert_eq!(flow.state(), super::HumanPresentNotarizationState::Drafted);
228 }
229
230 #[test]
231 fn backward_transition_rejected() {
232 let mut flow = super::HumanPresentNotarizationFlow::new("cm-back");
235 flow
236 .transition_to(super::HumanPresentNotarizationState::PendingDecision)
237 .unwrap();
238 flow
239 .transition_to(super::HumanPresentNotarizationState::Approved)
240 .unwrap();
241 flow
242 .transition_to(super::HumanPresentNotarizationState::MandateIssued)
243 .unwrap();
244 let err = flow
245 .transition_to(super::HumanPresentNotarizationState::Drafted)
246 .unwrap_err();
247 std::assert_eq!(err.code(), "APH_E002");
248 std::assert_eq!(
249 flow.state(),
250 super::HumanPresentNotarizationState::MandateIssued
251 );
252 }
253
254 #[test]
255 fn set_envelope_id_works() {
256 let mut flow = super::HumanPresentNotarizationFlow::new("cm-env");
260 std::assert!(flow.envelope_id().is_none());
261 flow.set_envelope_id("env-42");
262 std::assert_eq!(flow.envelope_id(), std::option::Option::Some("env-42"));
263 }
264
265 #[test]
266 fn state_serde_roundtrip() {
267 let variants = [
271 super::HumanPresentNotarizationState::Drafted,
272 super::HumanPresentNotarizationState::PendingDecision,
273 super::HumanPresentNotarizationState::Approved,
274 super::HumanPresentNotarizationState::MandateIssued,
275 super::HumanPresentNotarizationState::EnvelopeIssued,
276 super::HumanPresentNotarizationState::Delivered,
277 super::HumanPresentNotarizationState::Denied,
278 ];
279 for v in &variants {
280 let json = serde_json::to_string(v).unwrap();
281 let back: super::HumanPresentNotarizationState = serde_json::from_str(&json).unwrap();
282 std::assert_eq!(*v, back, "round-trip mismatch for {:?}", v);
283 }
284 }
285}