use crate::domain::model::issue::IssueView;
use crate::domain::model::status::StatusesConfig;
use super::helpers::{is_ongoing, is_stalled};
pub(super) fn compute_flow_efficiency(
filtered: &IssueView<'_>,
statuses: &StatusesConfig,
) -> Option<f64> {
let efficiencies: Vec<f64> = filtered
.iter()
.filter(|i| i.status.terminal)
.filter_map(|i| {
i.events
.flow_efficiency_pct(is_ongoing(statuses), is_stalled(statuses))
})
.collect();
if efficiencies.is_empty() {
None
} else {
Some(efficiencies.iter().sum::<f64>() / efficiencies.len() as f64)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::model::issue::Issue;
use crate::domain::model::status::StatusesConfig;
use crate::domain::usecases::issue::tests::{enrich_issue, feature, ir, IssueFixture};
#[test]
fn computes_active_over_active_plus_stalled() {
scenario_kanban()
.given(
feature("Story")
.status("done")
.with_timestamped_event("2026-03-01T00:00:00Z", "created", None, None)
.with_timestamped_event(
"2026-03-03T00:00:00Z",
"status_changed",
Some("backlog"),
Some("in-progress"),
)
.with_timestamped_event(
"2026-03-07T00:00:00Z",
"status_changed",
Some("in-progress"),
Some("blocked"),
)
.with_timestamped_event(
"2026-03-11T00:00:00Z",
"status_changed",
Some("blocked"),
Some("in-progress"),
)
.with_timestamped_event(
"2026-03-15T00:00:00Z",
"status_changed",
Some("in-progress"),
Some("done"),
),
)
.when_flow_efficiency()
.then_approximately(66.7, 1.0);
}
#[test]
fn returns_none_when_no_ongoing_transition() {
scenario()
.given(
feature("Task")
.status("closed")
.with_timestamped_event("2026-03-01T00:00:00Z", "created", None, None)
.with_timestamped_event(
"2026-03-09T00:00:00Z",
"status_changed",
Some("open"),
Some("closed"),
),
)
.when_flow_efficiency()
.then_none();
}
struct Scenario {
issues: Vec<Issue>,
config: StatusesConfig,
}
fn scenario() -> Scenario {
Scenario {
issues: vec![],
config: StatusesConfig::default_issue(),
}
}
fn scenario_kanban() -> Scenario {
Scenario {
issues: vec![],
config: StatusesConfig::preset_issue_kanban(),
}
}
impl Scenario {
fn given(mut self, fixture: IssueFixture) -> Self {
let id = self.issues.len() as u64 + 1;
let mut issue = fixture.build(ir(id));
enrich_issue(&mut issue, &self.config);
self.issues.push(issue);
self
}
fn when_flow_efficiency(self) -> FlowEfficiencyOutcome {
let refs = IssueView::from_slice(&self.issues);
let result = compute_flow_efficiency(&refs, &self.config);
FlowEfficiencyOutcome { result }
}
}
struct FlowEfficiencyOutcome {
result: Option<f64>,
}
impl FlowEfficiencyOutcome {
fn then_none(self) -> Self {
assert!(self.result.is_none(), "expected None");
self
}
fn then_approximately(self, expected: f64, tolerance: f64) -> Self {
let v = self.result.expect("expected Some");
assert!(
(v - expected).abs() < tolerance,
"expected ~{expected}%, got {v:.1}%"
);
self
}
}
}