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"]));
+4
View File
@@ -70,6 +70,10 @@ needs:
done_when: [<end>, <end>] # every stage that counts as finished, good or bad
success: [<end>] # the finished stages that are a good outcome for the business
skippable: [<stage>] # stages a case may pass over (papers already complete)
turned_down_from: [open, <stage>]
# the unfinished stages a case can still be turned down from;
# omit for every one. An applicant is declined before they
# start, not after - after, they depart
moves_by: { <stage>: [<group>, ...] }
# only where someone other than handled_by moves a case INTO a
# stage: another group, or `submitter` (the person who sent the