use crate::error::{Error, Result};
use chrono::{DateTime, Utc};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum PlanStatus {
Draft,
Active,
Completed,
Cancelled,
}
impl Default for PlanStatus {
fn default() -> Self {
PlanStatus::Draft
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Plan {
pub id: String,
pub name: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub based_on: Option<String>,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))]
pub processes: Vec<String>,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))]
pub independent_commitments: Vec<String>,
pub status: PlanStatus,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub due: Option<DateTime<Utc>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub has_beginning: Option<DateTime<Utc>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub has_end: Option<DateTime<Utc>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub in_scope_of: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub refinement_of: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub note: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Plan {
pub fn builder() -> PlanBuilder {
PlanBuilder::default()
}
pub fn add_process(&mut self, process_id: impl Into<String>) {
self.processes.push(process_id.into());
self.updated_at = Utc::now();
}
pub fn add_commitment(&mut self, commitment_id: impl Into<String>) {
self.independent_commitments.push(commitment_id.into());
self.updated_at = Utc::now();
}
pub fn activate(&mut self) {
if self.status == PlanStatus::Draft {
self.status = PlanStatus::Active;
self.updated_at = Utc::now();
}
}
pub fn complete(&mut self) {
if self.status == PlanStatus::Active {
self.status = PlanStatus::Completed;
self.updated_at = Utc::now();
}
}
pub fn cancel(&mut self) {
if self.status != PlanStatus::Completed {
self.status = PlanStatus::Cancelled;
self.updated_at = Utc::now();
}
}
pub fn is_active(&self) -> bool {
matches!(self.status, PlanStatus::Active)
}
pub fn is_complete(&self) -> bool {
matches!(self.status, PlanStatus::Completed)
}
}
#[derive(Debug, Default)]
pub struct PlanBuilder {
id: Option<String>,
name: Option<String>,
based_on: Option<String>,
processes: Vec<String>,
independent_commitments: Vec<String>,
due: Option<DateTime<Utc>>,
has_beginning: Option<DateTime<Utc>>,
has_end: Option<DateTime<Utc>>,
in_scope_of: Option<String>,
refinement_of: Option<String>,
note: Option<String>,
}
impl PlanBuilder {
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn based_on(mut self, scenario_id: impl Into<String>) -> Self {
self.based_on = Some(scenario_id.into());
self
}
pub fn add_process(mut self, process_id: impl Into<String>) -> Self {
self.processes.push(process_id.into());
self
}
pub fn add_commitment(mut self, commitment_id: impl Into<String>) -> Self {
self.independent_commitments.push(commitment_id.into());
self
}
pub fn due(mut self, due: DateTime<Utc>) -> Self {
self.due = Some(due);
self
}
pub fn has_beginning(mut self, time: DateTime<Utc>) -> Self {
self.has_beginning = Some(time);
self
}
pub fn has_end(mut self, time: DateTime<Utc>) -> Self {
self.has_end = Some(time);
self
}
pub fn in_scope_of(mut self, scope: impl Into<String>) -> Self {
self.in_scope_of = Some(scope.into());
self
}
pub fn refinement_of(mut self, plan_id: impl Into<String>) -> Self {
self.refinement_of = Some(plan_id.into());
self
}
pub fn note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
pub fn build(self) -> Result<Plan> {
let id = self.id.ok_or_else(|| Error::missing_field("id"))?;
let name = self.name.ok_or_else(|| Error::missing_field("name"))?;
let now = Utc::now();
Ok(Plan {
id,
name,
based_on: self.based_on,
processes: self.processes,
independent_commitments: self.independent_commitments,
status: PlanStatus::Draft,
due: self.due,
has_beginning: self.has_beginning,
has_end: self.has_end,
in_scope_of: self.in_scope_of,
refinement_of: self.refinement_of,
note: self.note,
created_at: now,
updated_at: now,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plan_builder() {
let plan = Plan::builder()
.id("plan-001")
.name("Weekly Production Plan")
.add_process("process-001")
.add_process("process-002")
.build()
.unwrap();
assert_eq!(plan.id, "plan-001");
assert_eq!(plan.processes.len(), 2);
assert_eq!(plan.status, PlanStatus::Draft);
}
#[test]
fn test_plan_lifecycle() {
let mut plan = Plan::builder()
.id("plan-001")
.name("Test Plan")
.build()
.unwrap();
assert_eq!(plan.status, PlanStatus::Draft);
plan.activate();
assert!(plan.is_active());
plan.complete();
assert!(plan.is_complete());
}
#[test]
fn test_plan_cancel() {
let mut plan = Plan::builder()
.id("plan-001")
.name("Test Plan")
.build()
.unwrap();
plan.activate();
plan.cancel();
assert_eq!(plan.status, PlanStatus::Cancelled);
}
#[cfg(feature = "serde")]
#[test]
fn test_plan_serialization() {
let plan = Plan::builder()
.id("plan-001")
.name("Test Plan")
.add_process("process-001")
.build()
.unwrap();
let json = serde_json::to_string(&plan).unwrap();
let parsed: Plan = serde_json::from_str(&json).unwrap();
assert_eq!(plan.id, parsed.id);
assert_eq!(plan.processes, parsed.processes);
}
}