rvf 0.1.0

Rust implementation of the ValueFlows vocabulary for distributed economic networks
Documentation
//! Exchanges - reciprocal transfers between agents
//!
//! Exchanges represent reciprocal economic activity between agents,
//! such as buying and selling or barter.

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

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

/// Status of an exchange
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum ExchangeStatus {
    /// The exchange is proposed
    Proposed,
    /// The exchange is in progress
    InProgress,
    /// The exchange is complete
    Complete,
    /// The exchange was cancelled
    Cancelled,
}

impl Default for ExchangeStatus {
    fn default() -> Self {
        ExchangeStatus::Proposed
    }
}

/// An agreement between agents
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Agreement {
    /// Unique identifier
    pub id: String,
    /// Display name
    pub name: String,
    /// Optional note
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub note: Option<String>,
    /// When the agreement was created
    pub created_at: DateTime<Utc>,
}

impl Agreement {
    /// Create a new agreement
    pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
        Self {
            id: id.into(),
            name: name.into(),
            note: None,
            created_at: Utc::now(),
        }
    }

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

/// An exchange - reciprocal transfers between agents
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Exchange {
    /// Unique identifier
    pub id: String,
    /// Display name
    pub name: String,
    /// The agreement this exchange is part of
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub realized_in: Option<String>,
    /// IDs of transfers that are part of this exchange
    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))]
    pub transfers: Vec<String>,
    /// Status of the exchange
    pub status: ExchangeStatus,
    /// Scope (organization context)
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub in_scope_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 Exchange {
    /// Create a new exchange builder
    pub fn builder() -> ExchangeBuilder {
        ExchangeBuilder::default()
    }

    /// Add a transfer to this exchange
    pub fn add_transfer(&mut self, transfer_id: impl Into<String>) {
        self.transfers.push(transfer_id.into());
        self.updated_at = Utc::now();
    }

    /// Start the exchange
    pub fn start(&mut self) {
        if self.status == ExchangeStatus::Proposed {
            self.status = ExchangeStatus::InProgress;
            self.updated_at = Utc::now();
        }
    }

    /// Complete the exchange
    pub fn complete(&mut self) {
        if self.status == ExchangeStatus::InProgress {
            self.status = ExchangeStatus::Complete;
            self.updated_at = Utc::now();
        }
    }

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

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

/// Builder for Exchange
#[derive(Debug, Default)]
pub struct ExchangeBuilder {
    id: Option<String>,
    name: Option<String>,
    realized_in: Option<String>,
    transfers: Vec<String>,
    in_scope_of: Option<String>,
    note: Option<String>,
}

impl ExchangeBuilder {
    /// 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 agreement
    pub fn realized_in(mut self, agreement_id: impl Into<String>) -> Self {
        self.realized_in = Some(agreement_id.into());
        self
    }

    /// Add a transfer
    pub fn add_transfer(mut self, transfer_id: impl Into<String>) -> Self {
        self.transfers.push(transfer_id.into());
        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 a note
    pub fn note(mut self, note: impl Into<String>) -> Self {
        self.note = Some(note.into());
        self
    }

    /// Build the Exchange
    pub fn build(self) -> Result<Exchange> {
        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(Exchange {
            id,
            name,
            realized_in: self.realized_in,
            transfers: self.transfers,
            status: ExchangeStatus::Proposed,
            in_scope_of: self.in_scope_of,
            note: self.note,
            created_at: now,
            updated_at: now,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_agreement_creation() {
        let agreement = Agreement::new("agreement-001", "Sale Agreement")
            .with_note("Agreement for tomato sale");

        assert_eq!(agreement.id, "agreement-001");
        assert_eq!(agreement.name, "Sale Agreement");
        assert!(agreement.note.is_some());
    }

    #[test]
    fn test_exchange_builder() {
        let exchange = Exchange::builder()
            .id("exchange-001")
            .name("Tomato for Money")
            .realized_in("agreement-001")
            .build()
            .unwrap();

        assert_eq!(exchange.id, "exchange-001");
        assert_eq!(exchange.status, ExchangeStatus::Proposed);
    }

    #[test]
    fn test_exchange_lifecycle() {
        let mut exchange = Exchange::builder()
            .id("exchange-001")
            .name("Test Exchange")
            .build()
            .unwrap();

        assert_eq!(exchange.status, ExchangeStatus::Proposed);

        exchange.start();
        assert_eq!(exchange.status, ExchangeStatus::InProgress);

        exchange.add_transfer("transfer-001");
        exchange.add_transfer("transfer-002");

        exchange.complete();
        assert_eq!(exchange.status, ExchangeStatus::Complete);
        assert!(exchange.is_complete());
    }

    #[cfg(feature = "serde")]
    #[test]
    fn test_exchange_serialization() {
        let exchange = Exchange::builder()
            .id("exchange-001")
            .name("Test")
            .add_transfer("transfer-001")
            .build()
            .unwrap();

        let json = serde_json::to_string(&exchange).unwrap();
        let parsed: Exchange = serde_json::from_str(&json).unwrap();
        assert_eq!(exchange.id, parsed.id);
        assert_eq!(exchange.transfers, parsed.transfers);
    }
}