rvf 0.1.0

Rust implementation of the ValueFlows vocabulary for distributed economic networks
Documentation
//! Plans - operational planning for economic activity
//!
//! Plans are sets of commitments and processes that represent
//! scheduled future economic activity.

use crate::error::{Error, Result};
use chrono::{DateTime, Utc};

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// Status of a plan
#[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 {
    /// The plan is being created
    Draft,
    /// The plan is active
    Active,
    /// The plan has been completed
    Completed,
    /// The plan was cancelled
    Cancelled,
}

impl Default for PlanStatus {
    fn default() -> Self {
        PlanStatus::Draft
    }
}

/// A plan for future economic activity
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Plan {
    /// Unique identifier
    pub id: String,
    /// Display name
    pub name: String,
    /// Created from a scenario
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub based_on: Option<String>,
    /// IDs of processes in this plan
    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))]
    pub processes: Vec<String>,
    /// IDs of independent commitments in this plan
    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))]
    pub independent_commitments: Vec<String>,
    /// Status of the plan
    pub status: PlanStatus,
    /// When the plan is due to be complete
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub due: Option<DateTime<Utc>>,
    /// When the plan period starts
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub has_beginning: Option<DateTime<Utc>>,
    /// When the plan period ends
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub has_end: Option<DateTime<Utc>>,
    /// Scope (organization context)
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub in_scope_of: Option<String>,
    /// Refinement of another plan
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub refinement_of: Option<String>,
    /// Optional note
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub note: Option<String>,
    /// When this was created
    pub created_at: DateTime<Utc>,
    /// When this was last updated
    pub updated_at: DateTime<Utc>,
}

impl Plan {
    /// Create a new plan builder
    pub fn builder() -> PlanBuilder {
        PlanBuilder::default()
    }

    /// Add a process to this plan
    pub fn add_process(&mut self, process_id: impl Into<String>) {
        self.processes.push(process_id.into());
        self.updated_at = Utc::now();
    }

    /// Add an independent commitment
    pub fn add_commitment(&mut self, commitment_id: impl Into<String>) {
        self.independent_commitments.push(commitment_id.into());
        self.updated_at = Utc::now();
    }

    /// Activate the plan
    pub fn activate(&mut self) {
        if self.status == PlanStatus::Draft {
            self.status = PlanStatus::Active;
            self.updated_at = Utc::now();
        }
    }

    /// Complete the plan
    pub fn complete(&mut self) {
        if self.status == PlanStatus::Active {
            self.status = PlanStatus::Completed;
            self.updated_at = Utc::now();
        }
    }

    /// Cancel the plan
    pub fn cancel(&mut self) {
        if self.status != PlanStatus::Completed {
            self.status = PlanStatus::Cancelled;
            self.updated_at = Utc::now();
        }
    }

    /// Check if the plan is active
    pub fn is_active(&self) -> bool {
        matches!(self.status, PlanStatus::Active)
    }

    /// Check if the plan is complete
    pub fn is_complete(&self) -> bool {
        matches!(self.status, PlanStatus::Completed)
    }
}

/// Builder for Plan
#[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 {
    /// Set the ID
    pub fn id(mut self, id: impl Into<String>) -> Self {
        self.id = Some(id.into());
        self
    }

    /// Set the name
    pub fn name(mut self, name: impl Into<String>) -> Self {
        self.name = Some(name.into());
        self
    }

    /// Set the scenario this is based on
    pub fn based_on(mut self, scenario_id: impl Into<String>) -> Self {
        self.based_on = Some(scenario_id.into());
        self
    }

    /// Add a process
    pub fn add_process(mut self, process_id: impl Into<String>) -> Self {
        self.processes.push(process_id.into());
        self
    }

    /// Add a commitment
    pub fn add_commitment(mut self, commitment_id: impl Into<String>) -> Self {
        self.independent_commitments.push(commitment_id.into());
        self
    }

    /// Set the due date
    pub fn due(mut self, due: DateTime<Utc>) -> Self {
        self.due = Some(due);
        self
    }

    /// Set the beginning time
    pub fn has_beginning(mut self, time: DateTime<Utc>) -> Self {
        self.has_beginning = Some(time);
        self
    }

    /// Set the end time
    pub fn has_end(mut self, time: DateTime<Utc>) -> Self {
        self.has_end = Some(time);
        self
    }

    /// Set the scope
    pub fn in_scope_of(mut self, scope: impl Into<String>) -> Self {
        self.in_scope_of = Some(scope.into());
        self
    }

    /// Set the parent plan
    pub fn refinement_of(mut self, plan_id: impl Into<String>) -> Self {
        self.refinement_of = Some(plan_id.into());
        self
    }

    /// Set a note
    pub fn note(mut self, note: impl Into<String>) -> Self {
        self.note = Some(note.into());
        self
    }

    /// Build the Plan
    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);
    }
}