build: turned_down_from, and uhhm's stages in words

A need may say which unfinished stages a case can still be turned
down from; the machine no longer offers the bad end from every stage.
uhhm's collaborators are declined before they start and depart after,
as the live site has it. The uhhm plan gets stages_text and three
desk buttons reworded: every uhhm desk scores full marks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-24 06:01:56 +02:00
co-authored by Claude Fable 5.1
parent aeff4c8cbe
commit 6e49694211
12 changed files with 1535 additions and 143 deletions
+25 -2
View File
@@ -171,6 +171,11 @@ pub struct PlanNeed {
/// were complete).
#[serde(default)]
pub skippable: Vec<String>,
/// The unfinished stages a case may still be turned down from;
/// empty means every one. An applicant is declined before they
/// start and not after, where they depart instead.
#[serde(default)]
pub turned_down_from: Vec<String>,
/// Who presses the button that moves a case INTO a stage, where it
/// is not `handled_by`: another group, several groups, or
/// `submitter` - the person who sent the form, through their own
@@ -351,6 +356,11 @@ impl Plan {
if need.watched_by.iter().any(|g| g == PUBLIC) && need.enters_as.is_some() {
bail!(at("a public view is for what the public sent; an internal need has its group's desk"));
}
for state in &need.turned_down_from {
if need.done_when.contains(state) || !need.stages.contains(state) {
bail!(at(&format!("turned_down_from names {state:?}, which is not one of its unfinished stages")));
}
}
for state in need.done_when.iter().chain(&need.skippable) {
if !need.stages.contains(state) {
bail!(at(&format!("{state:?} is not one of its stages")));
@@ -441,6 +451,7 @@ impl Plan {
|| other.attended_by != first.attended_by
|| other.mails != first.mails
|| other.stages_text != first.stages_text
|| other.turned_down_from != first.turned_down_from
{
bail!("needs {:?} and {:?} both land in {bucket:?} and must agree on stages, done_when, skippable, events, attended_by, mails and who moves and watches it", first.id, other.id);
}
@@ -516,7 +527,9 @@ pub fn machine(need: &PlanNeed) -> BTreeMap<String, Vec<String>> {
}
}
}
to.extend(ends_badly.iter().map(|s| (*s).clone()));
if need.turned_down_from.is_empty() || need.turned_down_from.contains(stage) {
to.extend(ends_badly.iter().map(|s| (*s).clone()));
}
moves.insert((*stage).clone(), to);
}
moves
@@ -1642,7 +1655,7 @@ mod tests {
lands_in: "b".into(), handled_by: "g".into(),
stages: v(stages), done_when: v(done), success: v(success), skippable: v(skippable),
moves_by: BTreeMap::new(), watched_by: vec![],
events: BTreeMap::new(), attended_by: None, mails: BTreeMap::new(), stages_text: BTreeMap::new(),
events: BTreeMap::new(), attended_by: None, mails: BTreeMap::new(), stages_text: BTreeMap::new(), turned_down_from: vec![],
collects: vec![], max_fields: 3,
}
}
@@ -1720,6 +1733,16 @@ mod tests {
assert!(!m.contains_key("won"));
}
#[test]
fn a_case_is_turned_down_only_where_the_plan_says() {
let mut n = need(&["open", "invited", "active", "departed", "declined"], &["departed", "declined"], &["departed"], &[]);
n.turned_down_from = vec!["open".into(), "invited".into()];
let m = machine(&n);
assert_eq!(m["open"], ["invited", "declined"]);
assert_eq!(m["invited"], ["active", "declined"]);
assert_eq!(m["active"], ["departed"]);
}
#[test]
fn a_skippable_stage_can_be_passed_over() {
let m = machine(&need(&["open", "awaiting_papers", "approved", "declined"], &["approved", "declined"], &["approved"], &["awaiting_papers"]));