use std::collections::HashMap;
use crate::state::State;
pub const DEFAULT_NUDGE_AFTER: u32 = 3;
pub const DEFAULT_ESCALATE_AFTER: u32 = 6;
#[derive(Debug, Clone)]
pub struct RepairConfig {
pub nudge_after: u32,
pub escalate_after: u32,
}
impl Default for RepairConfig {
fn default() -> Self {
Self {
nudge_after: DEFAULT_NUDGE_AFTER,
escalate_after: DEFAULT_ESCALATE_AFTER,
}
}
}
impl RepairConfig {
pub fn new() -> Self {
Self::default()
}
pub fn nudge_after(mut self, n: u32) -> Self {
self.nudge_after = n;
self
}
pub fn escalate_after(mut self, n: u32) -> Self {
self.escalate_after = n;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RepairAction {
None,
Nudge {
unfulfilled: Vec<String>,
attempt: u32,
},
Escalate {
unfulfilled: Vec<String>,
},
}
pub struct NeedsFulfillment {
stall_count: HashMap<String, u32>,
config: RepairConfig,
}
impl NeedsFulfillment {
pub fn new(config: RepairConfig) -> Self {
Self {
stall_count: HashMap::new(),
config,
}
}
pub fn evaluate(&mut self, phase: &str, needs: &[String], state: &State) -> RepairAction {
let unfulfilled: Vec<String> = needs
.iter()
.filter(|key| state.get_raw(key).is_none())
.cloned()
.collect();
if unfulfilled.is_empty() {
self.stall_count.remove(phase);
return RepairAction::None;
}
let count = self.stall_count.entry(phase.to_string()).or_insert(0);
*count += 1;
if *count >= self.config.escalate_after {
RepairAction::Escalate { unfulfilled }
} else if *count >= self.config.nudge_after {
RepairAction::Nudge {
unfulfilled,
attempt: *count - self.config.nudge_after + 1,
}
} else {
RepairAction::None
}
}
pub fn reset(&mut self, phase: &str) {
self.stall_count.remove(phase);
}
pub fn reset_all(&mut self) {
self.stall_count.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_action_when_needs_fulfilled() {
let state = State::new();
state.set("customer_id", "C123");
state.set("account_number", "A456");
let mut nf = NeedsFulfillment::new(RepairConfig::default());
let action = nf.evaluate(
"gather",
&["customer_id".into(), "account_number".into()],
&state,
);
assert_eq!(action, RepairAction::None);
}
#[test]
fn no_action_before_threshold() {
let state = State::new();
let mut nf = NeedsFulfillment::new(RepairConfig::default());
for _ in 0..2 {
let action = nf.evaluate("gather", &["customer_id".into()], &state);
assert_eq!(action, RepairAction::None);
}
}
#[test]
fn nudge_at_threshold() {
let state = State::new();
let mut nf = NeedsFulfillment::new(RepairConfig::default());
for _ in 0..2 {
nf.evaluate("gather", &["customer_id".into()], &state);
}
let action = nf.evaluate("gather", &["customer_id".into()], &state);
assert!(matches!(action, RepairAction::Nudge { attempt: 1, .. }));
}
#[test]
fn escalation_at_threshold() {
let state = State::new();
let mut nf = NeedsFulfillment::new(RepairConfig::default());
for _ in 0..5 {
nf.evaluate("gather", &["customer_id".into()], &state);
}
let action = nf.evaluate("gather", &["customer_id".into()], &state);
assert!(matches!(action, RepairAction::Escalate { .. }));
}
#[test]
fn fulfilling_need_resets_counter() {
let state = State::new();
let mut nf = NeedsFulfillment::new(RepairConfig::default());
for _ in 0..2 {
nf.evaluate("gather", &["customer_id".into()], &state);
}
state.set("customer_id", "C123");
let action = nf.evaluate("gather", &["customer_id".into()], &state);
assert_eq!(action, RepairAction::None);
state.remove("customer_id");
let action = nf.evaluate("gather", &["customer_id".into()], &state);
assert_eq!(action, RepairAction::None); }
#[test]
fn custom_thresholds() {
let state = State::new();
let mut nf = NeedsFulfillment::new(RepairConfig::new().nudge_after(1).escalate_after(2));
let action = nf.evaluate("gather", &["x".into()], &state);
assert!(matches!(action, RepairAction::Nudge { attempt: 1, .. }));
let action = nf.evaluate("gather", &["x".into()], &state);
assert!(matches!(action, RepairAction::Escalate { .. }));
}
}