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, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct EconomicEvent {
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_inventoried_as: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub to_resource_inventoried_as: Option<String>,
#[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 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 to_location: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub state: Option<String>,
pub has_point_in_time: 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 fulfills: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub satisfies: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub triggered_by: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub realized_in: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub corrects: Option<String>,
#[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 created_at: DateTime<Utc>,
}
impl EconomicEvent {
pub fn builder() -> EconomicEventBuilder {
EconomicEventBuilder::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_transfer(&self) -> bool {
self.action.is_transfer()
}
pub fn is_correction(&self) -> bool {
self.corrects.is_some()
}
pub fn effective_quantity(&self) -> Option<&Measure> {
self.resource_quantity
.as_ref()
.or(self.effort_quantity.as_ref())
}
}
#[derive(Debug, Default)]
pub struct EconomicEventBuilder {
id: Option<String>,
action: Option<ActionType>,
provider: Option<String>,
receiver: Option<String>,
resource_quantity: Option<Measure>,
effort_quantity: Option<Measure>,
resource_inventoried_as: Option<String>,
to_resource_inventoried_as: Option<String>,
resource_conforms_to: Option<String>,
input_of: Option<String>,
output_of: Option<String>,
at_location: Option<String>,
to_location: Option<String>,
state: Option<String>,
has_point_in_time: Option<DateTime<Utc>>,
has_beginning: Option<DateTime<Utc>>,
has_end: Option<DateTime<Utc>>,
fulfills: Option<String>,
satisfies: Option<String>,
triggered_by: Option<String>,
realized_in: Option<String>,
corrects: Option<String>,
in_scope_of: Option<String>,
note: Option<String>,
}
impl EconomicEventBuilder {
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_inventoried_as(mut self, resource_id: impl Into<String>) -> Self {
self.resource_inventoried_as = Some(resource_id.into());
self
}
pub fn to_resource_inventoried_as(mut self, resource_id: impl Into<String>) -> Self {
self.to_resource_inventoried_as = Some(resource_id.into());
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 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 to_location(mut self, location: impl Into<String>) -> Self {
self.to_location = Some(location.into());
self
}
pub fn state(mut self, state: impl Into<String>) -> Self {
self.state = Some(state.into());
self
}
pub fn has_point_in_time(mut self, time: DateTime<Utc>) -> Self {
self.has_point_in_time = Some(time);
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 fulfills(mut self, commitment_id: impl Into<String>) -> Self {
self.fulfills = Some(commitment_id.into());
self
}
pub fn satisfies(mut self, intent_id: impl Into<String>) -> Self {
self.satisfies = Some(intent_id.into());
self
}
pub fn triggered_by(mut self, event_id: impl Into<String>) -> Self {
self.triggered_by = Some(event_id.into());
self
}
pub fn realized_in(mut self, agreement_id: impl Into<String>) -> Self {
self.realized_in = Some(agreement_id.into());
self
}
pub fn corrects(mut self, event_id: impl Into<String>) -> Self {
self.corrects = Some(event_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<EconomicEvent> {
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"))?;
if self.resource_quantity.is_none() && self.effort_quantity.is_none() {
return Err(Error::validation(
"Either resource_quantity or effort_quantity must be provided",
));
}
let now = Utc::now();
let has_point_in_time = self.has_point_in_time.unwrap_or(now);
Ok(EconomicEvent {
id,
action,
provider,
receiver,
resource_quantity: self.resource_quantity,
effort_quantity: self.effort_quantity,
resource_inventoried_as: self.resource_inventoried_as,
to_resource_inventoried_as: self.to_resource_inventoried_as,
resource_conforms_to: self.resource_conforms_to,
input_of: self.input_of,
output_of: self.output_of,
at_location: self.at_location,
to_location: self.to_location,
state: self.state,
has_point_in_time,
has_beginning: self.has_beginning,
has_end: self.has_end,
fulfills: self.fulfills,
satisfies: self.satisfies,
triggered_by: self.triggered_by,
realized_in: self.realized_in,
corrects: self.corrects,
in_scope_of: self.in_scope_of,
note: self.note,
created_at: now,
})
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Fulfillment {
pub id: String,
pub fulfills: String,
pub fulfilled_by: 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 note: Option<String>,
}
impl Fulfillment {
pub fn new(
id: impl Into<String>,
commitment_id: impl Into<String>,
event_id: impl Into<String>,
) -> Self {
Self {
id: id.into(),
fulfills: commitment_id.into(),
fulfilled_by: event_id.into(),
resource_quantity: None,
effort_quantity: None,
note: None,
}
}
pub fn with_resource_quantity(mut self, quantity: Measure) -> Self {
self.resource_quantity = Some(quantity);
self
}
pub fn with_effort_quantity(mut self, quantity: Measure) -> Self {
self.effort_quantity = Some(quantity);
self
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Satisfaction {
pub id: String,
pub satisfies: String,
pub satisfied_by: 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 note: Option<String>,
}
impl Satisfaction {
pub fn new(
id: impl Into<String>,
intent_id: impl Into<String>,
satisfied_by: impl Into<String>,
) -> Self {
Self {
id: id.into(),
satisfies: intent_id.into(),
satisfied_by: satisfied_by.into(),
resource_quantity: None,
effort_quantity: None,
note: None,
}
}
pub fn with_resource_quantity(mut self, quantity: Measure) -> Self {
self.resource_quantity = Some(quantity);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::measures::Unit;
#[test]
fn test_economic_event_builder() {
let event = EconomicEvent::builder()
.id("event-001")
.action(ActionType::Produce)
.provider("agent-001")
.receiver("agent-001")
.resource_quantity(Measure::new(100, Unit::Kilogram))
.output_of("process-001")
.build()
.unwrap();
assert_eq!(event.id, "event-001");
assert_eq!(event.action, ActionType::Produce);
assert!(event.is_output());
assert!(!event.is_input());
}
#[test]
fn test_transfer_event() {
let event = EconomicEvent::builder()
.id("event-002")
.action(ActionType::Transfer)
.provider("agent-001")
.receiver("agent-002")
.resource_quantity(Measure::new(50, Unit::Each))
.resource_inventoried_as("resource-001")
.build()
.unwrap();
assert!(event.is_transfer());
}
#[test]
fn test_work_event() {
let event = EconomicEvent::builder()
.id("event-003")
.action(ActionType::Work)
.provider("agent-001")
.receiver("agent-002")
.effort_quantity(Measure::new(8, Unit::Hour))
.input_of("process-001")
.build()
.unwrap();
assert!(event.is_input());
assert_eq!(event.effective_quantity().unwrap().value, 8.into());
}
#[test]
fn test_event_missing_quantity() {
let result = EconomicEvent::builder()
.id("event-004")
.action(ActionType::Produce)
.provider("agent-001")
.receiver("agent-001")
.build();
assert!(result.is_err());
}
#[cfg(feature = "serde")]
#[test]
fn test_event_serialization() {
let event = EconomicEvent::builder()
.id("event-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(&event).unwrap();
let parsed: EconomicEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event.id, parsed.id);
assert_eq!(event.action, parsed.action);
}
}