headless_lms_credit_registration/use_cases/resolve_enrolments/persons/
decide.rs1use headless_lms_models::credit_registrations::{
4 CreditRegistrationErrorCode, CreditRegistrationState,
5};
6use headless_lms_models::library::credit_registration::outcomes::{
7 NextAttempt, Outcome, RowFacts, submit_error_outcome, unanswered_item_outcome,
8};
9
10use crate::registry::{BatchRequest, PersonLookup};
11use crate::use_cases::resolve_enrolments::Lookup;
12use crate::workflow::{ClaimedRegistration, Decision};
13
14pub(super) fn unanswered_person_decision(
16 state: CreditRegistrationState,
17 facts: &RowFacts,
18) -> Decision<'static> {
19 Decision::new(unanswered_item_outcome(
20 PersonLookup::OPERATION,
21 state,
22 facts,
23 ))
24 .with_message("Sisu did not answer for this item.")
25}
26
27pub(super) fn refused_person_decision(
29 code: CreditRegistrationErrorCode,
30 facts: &RowFacts,
31) -> Decision<'static> {
32 Decision::new(submit_error_outcome(PersonLookup::OPERATION, code, facts))
33}
34
35pub(super) enum PersonFill {
37 Filled,
38 LinkChanged,
40 HeldByAnotherLink,
42}
43
44impl PersonFill {
45 pub(super) fn decision(self, claim: &ClaimedRegistration) -> Decision<'static> {
49 match self {
50 Self::Filled => Decision::new(found_person_outcome(claim))
51 .with_message("Found the Sisu person the linked student number belongs to."),
52 Self::LinkChanged => Decision::new(Outcome::to(CreditRegistrationState::Pending))
53 .with_message(
54 "The linked student number changed while its Sisu person was looked up.",
55 ),
56 Self::HeldByAnotherLink => Decision::new(Outcome {
57 drop_verified_student_number: true,
58 ..Outcome::to(CreditRegistrationState::Pending)
59 })
60 .with_message(
61 "Another account's link already holds the Sisu person this student number belongs \
62 to.",
63 ),
64 }
65 }
66}
67
68fn found_person_outcome(claim: &ClaimedRegistration) -> Outcome {
71 let registration = claim.registration();
72 match Lookup::of(registration) {
73 Lookup::FirstResolve => Outcome::to(CreditRegistrationState::ReadyToSubmit),
74 Lookup::ParkedCheck => Outcome {
75 error_code: registration.error_code,
76 next: NextAttempt::At(registration.next_attempt_at),
77 keeps_enrolment_checked_at: true,
78 ..Outcome::to(CreditRegistrationState::NoUsableEnrolment)
79 },
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use chrono::TimeDelta;
86 use headless_lms_models::credit_registrations::CreditRegistration;
87 use headless_lms_models::credit_registrations::CreditRegistrationErrorCode as Code;
88 use headless_lms_models::credit_registrations::CreditRegistrationState as State;
89
90 use super::*;
91 use crate::test_fixtures::{now, registration};
92
93 fn parked_row() -> CreditRegistration {
94 CreditRegistration {
95 error_code: Some(Code::EnrolmentNotFound),
96 no_usable_enrolment_since: Some(now() - TimeDelta::days(3)),
97 next_attempt_at: now() + TimeDelta::days(1),
98 ..registration(State::NoUsableEnrolment)
99 }
100 }
101
102 fn unanswered(row: &CreditRegistration) -> Outcome {
103 unanswered_person_decision(row.state, &RowFacts::of(row, now())).outcome
104 }
105
106 #[test]
107 fn an_unanswered_first_resolve_retries_later() {
108 let outcome = unanswered(®istration(State::ReadyToSubmit));
109 assert_eq!(outcome.to_state, State::FailedRetryable);
110 assert_eq!(outcome.error_code, Some(Code::UnexpectedResponse));
111 }
112
113 #[test]
114 fn an_unanswered_parked_check_keeps_the_row_waiting_with_its_code() {
115 let outcome = unanswered(&parked_row());
116 assert_eq!(outcome.to_state, State::NoUsableEnrolment);
117 assert_eq!(outcome.error_code, Some(Code::EnrolmentNotFound));
118 assert!(outcome.keeps_enrolment_checked_at);
119 }
120
121 #[test]
122 fn person_not_found_drops_the_linked_number() {
123 let row = registration(State::ReadyToSubmit);
124 let decision = refused_person_decision(Code::PersonNotFound, &RowFacts::of(&row, now()));
125 let outcome = decision.outcome;
126 assert_eq!(outcome.to_state, State::Pending);
127 assert!(outcome.drop_verified_student_number);
128 }
129
130 fn fill(row: CreditRegistration, fill: PersonFill) -> Decision<'static> {
131 fill.decision(&crate::use_cases::resolve_enrolments::claim_for_lookup(row))
132 }
133
134 #[test]
135 fn a_filled_first_resolve_goes_on_to_the_enrolment_lookup() {
136 let decision = fill(registration(State::ReadyToSubmit), PersonFill::Filled);
137 assert_eq!(decision.outcome.to_state, State::ReadyToSubmit);
138 assert!(decision.message.as_deref().is_some());
139 }
140
141 #[test]
142 fn a_filled_parked_check_stays_parked_with_its_code_schedule_and_check_time() {
143 let row = parked_row();
144 let decision = fill(row.clone(), PersonFill::Filled);
145 let outcome = decision.outcome;
146 assert_eq!(outcome.to_state, State::NoUsableEnrolment);
147 assert_eq!(outcome.error_code, Some(Code::EnrolmentNotFound));
148 assert_eq!(outcome.next, NextAttempt::At(row.next_attempt_at));
149 assert!(outcome.keeps_enrolment_checked_at);
150 }
151
152 #[test]
153 fn a_link_that_changed_meanwhile_goes_back_to_pending_and_keeps_its_number() {
154 let decision = fill(registration(State::ReadyToSubmit), PersonFill::LinkChanged);
155 assert_eq!(decision.outcome.to_state, State::Pending);
156 assert!(!decision.outcome.drop_verified_student_number);
157 }
158
159 #[test]
160 fn a_person_held_by_another_link_drops_this_link() {
161 for row in [registration(State::ReadyToSubmit), parked_row()] {
162 let decision = fill(row, PersonFill::HeldByAnotherLink);
163 assert_eq!(decision.outcome.to_state, State::Pending);
164 assert!(decision.outcome.drop_verified_student_number);
165 }
166 }
167}