///| Review workflow turns machine findings into owned and auditable cases.
///|
pub(all) enum ReviewState {
Open
Investigating
Resolved
Dismissed
} derive(Debug, Eq)
///|
pub(all) enum ReviewDecision {
ConfirmedIssue
DataCorrection
BusinessException
FalsePositive
NoDecision
} derive(Debug, Eq)
///|
pub(all) struct ReviewCase {
case_id : String
finding : Finding
priority : Int
state : ReviewState
owner : String
opened_day : Int
updated_day : Int
decision : ReviewDecision
resolution_note : String
} derive(Debug, Eq)
///|
pub(all) struct ReviewQueue {
cases : Array[ReviewCase]
next_sequence : Int
} derive(Debug, Eq)
///|
pub(all) struct ReviewSummary {
open : Int
investigating : Int
resolved : Int
dismissed : Int
unassigned : Int
overdue : Int
} derive(Debug, Eq)
///|
pub(all) struct AssignmentRule {
code : FindingCode
owner : String
} derive(Debug, Eq)
///|
pub fn empty_review_queue() -> ReviewQueue {
{ cases: [], next_sequence: 1, }
}
///|
pub fn empty_review_summary() -> ReviewSummary {
{
open: 0,
investigating: 0,
resolved: 0,
dismissed: 0,
unassigned: 0,
overdue: 0,
}
}
///|
pub fn review_state_label(state : ReviewState) -> String {
match state {
Open => "open"
Investigating => "investigating"
Resolved => "resolved"
Dismissed => "dismissed"
}
}
///|
pub fn review_decision_label(decision : ReviewDecision) -> String {
match decision {
ConfirmedIssue => "confirmed_issue"
DataCorrection => "data_correction"
BusinessException => "business_exception"
FalsePositive => "false_positive"
NoDecision => "no_decision"
}
}
///|
pub fn priority_for_severity(severity : Severity) -> Int {
match severity {
Error => 1
Warning => 2
Info => 3
}
}
///|
pub fn priority_label(priority : Int) -> String {
if priority <= 1 {
"critical"
} else if priority == 2 {
"high"
} else if priority == 3 {
"normal"
} else {
"low"
}
}
///|
fn case_id(sequence : Int) -> String {
"CASE-" + sequence.to_string()
}
///|
pub fn new_review_case(
finding : Finding,
sequence : Int,
opened_day : Int,
) -> ReviewCase {
{
case_id: case_id(sequence),
finding,
priority: priority_for_severity(finding.severity),
state: Open,
owner: "",
opened_day,
updated_day: opened_day,
decision: NoDecision,
resolution_note: "",
}
}
///|
pub fn enqueue_finding(
queue : ReviewQueue,
finding : Finding,
opened_day : Int,
) -> ReviewQueue {
let cases = queue.cases.copy()
cases.push(new_review_case(finding, queue.next_sequence, opened_day))
{ cases, next_sequence: queue.next_sequence + 1, }
}
///|
pub fn enqueue_findings(
queue : ReviewQueue,
findings : Array[Finding],
opened_day : Int,
) -> ReviewQueue {
let mut next = queue
for finding in findings {
next = enqueue_finding(next, finding, opened_day)
}
next
}
///|
pub fn queue_from_result(
result : ReconciliationResult,
opened_day : Int,
) -> ReviewQueue {
enqueue_findings(
empty_review_queue(),
warning_or_higher_findings(result.findings),
opened_day,
)
}
///|
pub fn find_case_index(cases : Array[ReviewCase], wanted : String) -> Int {
for index = 0; index < cases.length(); index = index + 1 {
if cases[index].case_id == wanted {
return index
}
}
-1
}
///|
pub fn find_review_case(
cases : Array[ReviewCase],
wanted : String,
) -> ReviewCase? {
let index = find_case_index(cases, wanted)
if index < 0 {
None
} else {
Some(cases[index])
}
}
///|
pub fn is_terminal_state(state : ReviewState) -> Bool {
state == Resolved || state == Dismissed
}
///|
pub fn can_transition(from : ReviewState, to : ReviewState) -> Bool {
match from {
Open => to == Investigating || to == Resolved || to == Dismissed
Investigating => to == Open || to == Resolved || to == Dismissed
Resolved => to == Investigating
Dismissed => to == Investigating
}
}
///|
pub fn transition_case(
queue : ReviewQueue,
wanted : String,
target : ReviewState,
updated_day : Int,
) -> ReviewQueue {
let cases = queue.cases.copy()
let index = find_case_index(cases, wanted)
if index < 0 {
return queue
}
let current = cases[index]
if !can_transition(current.state, target) || updated_day < current.updated_day {
return queue
}
cases[index] = { ..current, state: target, updated_day, }
{ ..queue, cases, }
}
///|
pub fn assign_case(
queue : ReviewQueue,
wanted : String,
owner : String,
updated_day : Int,
) -> ReviewQueue {
let cases = queue.cases.copy()
let index = find_case_index(cases, wanted)
if index < 0 || is_blank(owner) {
return queue
}
let current = cases[index]
if updated_day < current.updated_day {
return queue
}
cases[index] = { ..current, owner, updated_day, }
{ ..queue, cases, }
}
///|
pub fn set_case_priority(
queue : ReviewQueue,
wanted : String,
priority : Int,
updated_day : Int,
) -> ReviewQueue {
let cases = queue.cases.copy()
let index = find_case_index(cases, wanted)
if index < 0 || priority < 1 || priority > 4 {
return queue
}
let current = cases[index]
if updated_day < current.updated_day {
return queue
}
cases[index] = { ..current, priority, updated_day, }
{ ..queue, cases, }
}
///|
pub fn decide_case(
queue : ReviewQueue,
wanted : String,
decision : ReviewDecision,
note : String,
updated_day : Int,
) -> ReviewQueue {
let cases = queue.cases.copy()
let index = find_case_index(cases, wanted)
if index < 0 || decision == NoDecision || is_blank(note) {
return queue
}
let current = cases[index]
if !is_terminal_state(current.state) || updated_day < current.updated_day {
return queue
}
cases[index] = { ..current, decision, resolution_note: note, updated_day, }
{ ..queue, cases, }
}
///|
pub fn reopen_case(
queue : ReviewQueue,
wanted : String,
updated_day : Int,
) -> ReviewQueue {
let transitioned = transition_case(queue, wanted, Investigating, updated_day)
let cases = transitioned.cases.copy()
let index = find_case_index(cases, wanted)
if index < 0 {
return transitioned
}
let current = cases[index]
if current.state != Investigating {
return transitioned
}
cases[index] = { ..current, decision: NoDecision, resolution_note: "", }
{ ..transitioned, cases, }
}
///|
pub fn auto_assign(
queue : ReviewQueue,
rules : Array[AssignmentRule],
updated_day : Int,
) -> ReviewQueue {
let mut next = queue
for case in queue.cases {
if !is_blank(case.owner) {
continue
}
for rule in rules {
if case.finding.code == rule.code {
next = assign_case(next, case.case_id, rule.owner, updated_day)
break
}
}
}
next
}
///|
pub fn cases_for_owner(
cases : Array[ReviewCase],
owner : String,
) -> Array[ReviewCase] {
let selected : Array[ReviewCase] = []
for case in cases {
if case.owner == owner {
selected.push(case)
}
}
selected
}
///|
pub fn cases_for_order(
cases : Array[ReviewCase],
order_id : String,
) -> Array[ReviewCase] {
let selected : Array[ReviewCase] = []
for case in cases {
if case.finding.order_id == order_id {
selected.push(case)
}
}
selected
}
///|
pub fn cases_for_state(
cases : Array[ReviewCase],
state : ReviewState,
) -> Array[ReviewCase] {
let selected : Array[ReviewCase] = []
for case in cases {
if case.state == state {
selected.push(case)
}
}
selected
}
///|
pub fn open_cases(cases : Array[ReviewCase]) -> Array[ReviewCase] {
let selected : Array[ReviewCase] = []
for case in cases {
if !is_terminal_state(case.state) {
selected.push(case)
}
}
selected
}
///|
pub fn case_age(case : ReviewCase, current_day : Int) -> Int {
if current_day <= case.opened_day {
0
} else {
current_day - case.opened_day
}
}
///|
pub fn is_overdue(
case : ReviewCase,
current_day : Int,
max_age_days : Int,
) -> Bool {
!is_terminal_state(case.state) && case_age(case, current_day) > max_age_days
}
///|
pub fn overdue_cases(
cases : Array[ReviewCase],
current_day : Int,
max_age_days : Int,
) -> Array[ReviewCase] {
let selected : Array[ReviewCase] = []
for case in cases {
if is_overdue(case, current_day, max_age_days) {
selected.push(case)
}
}
selected
}
///|
pub fn review_summary(
queue : ReviewQueue,
current_day : Int,
max_age_days : Int,
) -> ReviewSummary {
let mut summary = empty_review_summary()
for case in queue.cases {
match case.state {
Open => summary = { ..summary, open: summary.open + 1, }
Investigating =>
summary = { ..summary, investigating: summary.investigating + 1, }
Resolved => summary = { ..summary, resolved: summary.resolved + 1, }
Dismissed => summary = { ..summary, dismissed: summary.dismissed + 1, }
}
if is_blank(case.owner) && !is_terminal_state(case.state) {
summary = { ..summary, unassigned: summary.unassigned + 1, }
}
if is_overdue(case, current_day, max_age_days) {
summary = { ..summary, overdue: summary.overdue + 1, }
}
}
summary
}
///|
pub fn sort_cases_by_priority(cases : Array[ReviewCase]) -> Array[ReviewCase] {
let sorted = cases.copy()
for outer = 0; outer < sorted.length(); outer = outer + 1 {
for inner = 0; inner + 1 < sorted.length() - outer; inner = inner + 1 {
let left = sorted[inner]
let right = sorted[inner + 1]
if left.priority > right.priority ||
(left.priority == right.priority && left.opened_day > right.opened_day) {
sorted[inner] = right
sorted[inner + 1] = left
}
}
}
sorted
}
///|
pub fn render_review_case(case : ReviewCase) -> String {
"\{case.case_id} | \{priority_label(case.priority)} | \{review_state_label(case.state)} | \{case.owner} | \{finding_code_label(case.finding.code)} | \{case.finding.order_id}"
}
///|
pub fn render_review_queue(queue : ReviewQueue) -> String {
let mut output = "CASE | PRIORITY | STATE | OWNER | CODE | ORDER\n"
for case in sort_cases_by_priority(queue.cases) {
output = output + render_review_case(case) + "\n"
}
output
}
///|
pub fn review_queue_is_consistent(queue : ReviewQueue) -> Bool {
let ids : Array[String] = []
for case in queue.cases {
if is_blank(case.case_id) || contains_string(ids, case.case_id) {
return false
}
if case.updated_day < case.opened_day ||
case.priority < 1 ||
case.priority > 4 {
return false
}
if is_terminal_state(case.state) &&
case.decision == NoDecision &&
!is_blank(case.resolution_note) {
return false
}
ids.push(case.case_id)
}
true
}