use crate::actions::ActionType;
use crate::error::{Error, Result};
use crate::measures::Measure;
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 CommitmentStatus {
Pending,
InProgress,
Fulfilled,
PartiallyFulfilled,
Cancelled,
Overdue,
}
impl Default for CommitmentStatus {
fn default() -> Self {
CommitmentStatus::Pending
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Commitment {
pub id: String,
pub action: ActionType,
pub provider: String,
pub receiver: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub resource_quantity: Option<Measure>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub effort_quantity: Option<Measure>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub resource_conforms_to: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub resource_inventoried_as: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub input_of: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub output_of: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub at_location: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub stage: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub state: Option<String>,
#[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 has_point_in_time: Option<DateTime<Utc>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub plan: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub clause_of: Option<String>,
pub finished: bool,
#[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 note: Option<String>,
pub status: CommitmentStatus,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub fulfilled_quantity: Option<Measure>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Commitment {
pub fn builder() -> CommitmentBuilder {
CommitmentBuilder::default()
}
pub fn is_input(&self) -> bool {
self.input_of.is_some()
}
pub fn is_output(&self) -> bool {
self.output_of.is_some()
}
pub fn is_overdue(&self) -> bool {
if let Some(due) = self.due {
!self.finished && due < Utc::now()
} else {
false
}
}
pub fn is_fulfilled(&self) -> bool {
matches!(self.status, CommitmentStatus::Fulfilled)
}
pub fn unfulfilled_quantity(&self) -> Option<Measure> {
match (&self.resource_quantity, &self.fulfilled_quantity) {
(Some(committed), Some(fulfilled)) => committed.sub(fulfilled),
(Some(committed), None) => Some(committed.clone()),
_ => None,
}
}
pub fn record_fulfillment(&mut self, quantity: &Measure) -> Result<()> {
if let Some(ref mut fulfilled) = self.fulfilled_quantity {
if !fulfilled.same_unit(quantity) {
return Err(Error::UnitMismatch {
unit1: fulfilled.unit.to_string(),
unit2: quantity.unit.to_string(),
});
}
*fulfilled = fulfilled.add(quantity).unwrap();
} else {
self.fulfilled_quantity = Some(quantity.clone());
}
if let Some(ref committed) = self.resource_quantity {
if let Some(ref fulfilled) = self.fulfilled_quantity {
if fulfilled.value >= committed.value {
self.status = CommitmentStatus::Fulfilled;
self.finished = true;
} else {
self.status = CommitmentStatus::PartiallyFulfilled;
}
}
}
self.updated_at = Utc::now();
Ok(())
}
pub fn cancel(&mut self) {
self.status = CommitmentStatus::Cancelled;
self.finished = true;
self.updated_at = Utc::now();
}
pub fn start(&mut self) {
if self.status == CommitmentStatus::Pending {
self.status = CommitmentStatus::InProgress;
self.updated_at = Utc::now();
}
}
}
#[derive(Debug, Default)]
pub struct CommitmentBuilder {
id: Option<String>,
action: Option<ActionType>,
provider: Option<String>,
receiver: Option<String>,
resource_quantity: Option<Measure>,
effort_quantity: Option<Measure>,
resource_conforms_to: Option<String>,
resource_inventoried_as: Option<String>,
input_of: Option<String>,
output_of: Option<String>,
at_location: Option<String>,
stage: Option<String>,
state: Option<String>,
due: Option<DateTime<Utc>>,
has_beginning: Option<DateTime<Utc>>,
has_end: Option<DateTime<Utc>>,
has_point_in_time: Option<DateTime<Utc>>,
plan: Option<String>,
clause_of: Option<String>,
in_scope_of: Option<String>,
note: Option<String>,
}
impl CommitmentBuilder {
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn action(mut self, action: ActionType) -> Self {
self.action = Some(action);
self
}
pub fn provider(mut self, agent_id: impl Into<String>) -> Self {
self.provider = Some(agent_id.into());
self
}
pub fn receiver(mut self, agent_id: impl Into<String>) -> Self {
self.receiver = Some(agent_id.into());
self
}
pub fn resource_quantity(mut self, quantity: Measure) -> Self {
self.resource_quantity = Some(quantity);
self
}
pub fn effort_quantity(mut self, quantity: Measure) -> Self {
self.effort_quantity = Some(quantity);
self
}
pub fn resource_conforms_to(mut self, spec_id: impl Into<String>) -> Self {
self.resource_conforms_to = Some(spec_id.into());
self
}
pub fn resource_inventoried_as(mut self, resource_id: impl Into<String>) -> Self {
self.resource_inventoried_as = Some(resource_id.into());
self
}
pub fn input_of(mut self, process_id: impl Into<String>) -> Self {
self.input_of = Some(process_id.into());
self
}
pub fn output_of(mut self, process_id: impl Into<String>) -> Self {
self.output_of = Some(process_id.into());
self
}
pub fn at_location(mut self, location: impl Into<String>) -> Self {
self.at_location = Some(location.into());
self
}
pub fn stage(mut self, stage: impl Into<String>) -> Self {
self.stage = Some(stage.into());
self
}
pub fn state(mut self, state: impl Into<String>) -> Self {
self.state = Some(state.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 has_point_in_time(mut self, time: DateTime<Utc>) -> Self {
self.has_point_in_time = Some(time);
self
}
pub fn plan(mut self, plan_id: impl Into<String>) -> Self {
self.plan = Some(plan_id.into());
self
}
pub fn clause_of(mut self, agreement_id: impl Into<String>) -> Self {
self.clause_of = Some(agreement_id.into());
self
}
pub fn in_scope_of(mut self, scope: impl Into<String>) -> Self {
self.in_scope_of = Some(scope.into());
self
}
pub fn note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
pub fn build(self) -> Result<Commitment> {
let id = self.id.ok_or_else(|| Error::missing_field("id"))?;
let action = self.action.ok_or_else(|| Error::missing_field("action"))?;
let provider = self
.provider
.ok_or_else(|| Error::missing_field("provider"))?;
let receiver = self
.receiver
.ok_or_else(|| Error::missing_field("receiver"))?;
let now = Utc::now();
Ok(Commitment {
id,
action,
provider,
receiver,
resource_quantity: self.resource_quantity,
effort_quantity: self.effort_quantity,
resource_conforms_to: self.resource_conforms_to,
resource_inventoried_as: self.resource_inventoried_as,
input_of: self.input_of,
output_of: self.output_of,
at_location: self.at_location,
stage: self.stage,
state: self.state,
due: self.due,
has_beginning: self.has_beginning,
has_end: self.has_end,
has_point_in_time: self.has_point_in_time,
plan: self.plan,
clause_of: self.clause_of,
finished: false,
in_scope_of: self.in_scope_of,
note: self.note,
status: CommitmentStatus::Pending,
fulfilled_quantity: None,
created_at: now,
updated_at: now,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::measures::Unit;
#[test]
fn test_commitment_builder() {
let commitment = Commitment::builder()
.id("commitment-001")
.action(ActionType::Produce)
.provider("agent-001")
.receiver("agent-001")
.resource_quantity(Measure::new(100, Unit::Kilogram))
.build()
.unwrap();
assert_eq!(commitment.id, "commitment-001");
assert_eq!(commitment.status, CommitmentStatus::Pending);
assert!(!commitment.finished);
}
#[test]
fn test_commitment_fulfillment() {
let mut commitment = Commitment::builder()
.id("commitment-001")
.action(ActionType::Produce)
.provider("agent-001")
.receiver("agent-001")
.resource_quantity(Measure::new(100, Unit::Kilogram))
.build()
.unwrap();
commitment
.record_fulfillment(&Measure::new(50, Unit::Kilogram))
.unwrap();
assert_eq!(commitment.status, CommitmentStatus::PartiallyFulfilled);
commitment
.record_fulfillment(&Measure::new(50, Unit::Kilogram))
.unwrap();
assert_eq!(commitment.status, CommitmentStatus::Fulfilled);
assert!(commitment.finished);
}
#[test]
fn test_unfulfilled_quantity() {
let mut commitment = Commitment::builder()
.id("commitment-001")
.action(ActionType::Produce)
.provider("agent-001")
.receiver("agent-001")
.resource_quantity(Measure::new(100, Unit::Each))
.build()
.unwrap();
commitment
.record_fulfillment(&Measure::new(30, Unit::Each))
.unwrap();
let unfulfilled = commitment.unfulfilled_quantity().unwrap();
assert_eq!(unfulfilled.value, 70.into());
}
#[cfg(feature = "serde")]
#[test]
fn test_commitment_serialization() {
let commitment = Commitment::builder()
.id("commitment-001")
.action(ActionType::Transfer)
.provider("agent-001")
.receiver("agent-002")
.resource_quantity(Measure::new(100, Unit::Each))
.build()
.unwrap();
let json = serde_json::to_string(&commitment).unwrap();
let parsed: Commitment = serde_json::from_str(&json).unwrap();
assert_eq!(commitment.id, parsed.id);
}
}