use super::{nonempty, owns, task_mut};
use crate::EngineError;
use chrono::{DateTime, Utc};
use dpm_model::{ActorId, ActorKind, ClaimRelease, Handoff, Plan, WorkItemId, WorkStatus};
pub(super) fn release(
plan: &mut Plan,
actor: &ActorId,
work: WorkItemId,
reason: &str,
at: DateTime<Utc>,
) -> Result<(), EngineError> {
nonempty(&work.to_string(), "release reason", reason)?;
let item = task_mut(plan, work)?;
owns(item, actor)?;
if item.start_event().is_some() {
return Err(EngineError::AlreadyStarted(work));
}
if item.execution.status != WorkStatus::Claimed || item.execution.owner.as_ref() != Some(actor)
{
return Err(EngineError::InvalidTransition {
work,
status: item.execution.status,
});
}
item.execution.owner = None;
item.execution.status = WorkStatus::Planned;
item.execution.events.claimed_at = None;
item.execution.releases.push(ClaimRelease {
actor: actor.clone(),
at,
reason: reason.into(),
});
Ok(())
}
pub(super) fn handoff(
plan: &mut Plan,
actor: &ActorId,
request: HandoffRequest<'_>,
at: DateTime<Utc>,
) -> Result<(), EngineError> {
let HandoffRequest {
work,
from,
to,
reason,
} = request;
nonempty(&work.to_string(), "handoff reason", reason)?;
nonempty(&work.to_string(), "new owner name", &to.name)?;
if actor.kind == ActorKind::Agent {
return Err(EngineError::ActorNotAllowed {
actor: actor.clone(),
action: "authorize a handoff; release an unstarted claim or ask a human",
});
}
let item = task_mut(plan, work)?;
let held = matches!(
item.execution.status,
WorkStatus::Claimed | WorkStatus::InProgress | WorkStatus::Blocked
);
let Some(owner) = item.execution.owner.clone().filter(|_| held) else {
return Err(EngineError::InvalidTransition {
work,
status: item.execution.status,
});
};
if owner != *from {
return Err(EngineError::OwnerMismatch {
work,
expected: from.clone(),
actual: owner,
});
}
if owner == *to {
return Err(EngineError::InvalidCommand {
entity: work.to_string(),
reason: format!("{to} already owns the work; hand it off to a different actor"),
});
}
item.execution.handoffs.push(Handoff {
from: owner,
to: to.clone(),
actor: actor.clone(),
at,
reason: reason.into(),
});
item.execution.owner = Some(to.clone());
if item.execution.events.claimed_at.is_some() {
item.execution.events.claimed_at = Some(at);
}
Ok(())
}
pub(super) struct HandoffRequest<'a> {
pub(super) work: WorkItemId,
pub(super) from: &'a ActorId,
pub(super) to: &'a ActorId,
pub(super) reason: &'a str,
}
#[cfg(test)]
mod tests;