rvf 0.1.0

Rust implementation of the ValueFlows vocabulary for distributed economic networks
Documentation
//! Transfers - moving resources between agents
//!
//! Transfers represent the movement of resources between agents,
//! either physically (custody) or in terms of rights.

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

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

/// Type of transfer
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum TransferType {
    /// Transfer of all rights (ownership/stewardship)
    TransferAllRights,
    /// Transfer of custody (physical possession)
    TransferCustody,
    /// Transfer of both rights and custody
    Transfer,
}

impl Default for TransferType {
    fn default() -> Self {
        TransferType::Transfer
    }
}

/// A transfer - movement of a resource between agents
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Transfer {
    /// Unique identifier
    pub id: String,
    /// Display name
    pub name: String,
    /// Type of transfer
    pub transfer_type: TransferType,
    /// IDs of the events that make up this transfer
    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))]
    pub realized_by: Vec<String>,
    /// IDs of the commitments that make up this transfer
    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))]
    pub planned_by: Vec<String>,
    /// Part of an exchange
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub part_of: Option<String>,
    /// Related agreement
    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
    pub realized_in: Option<String>,
    /// 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>,
}

impl Transfer {
    /// Create a new transfer builder
    pub fn builder() -> TransferBuilder {
        TransferBuilder::default()
    }

    /// Add an event to this transfer
    pub fn add_event(&mut self, event_id: impl Into<String>) {
        self.realized_by.push(event_id.into());
    }

    /// Add a commitment to this transfer
    pub fn add_commitment(&mut self, commitment_id: impl Into<String>) {
        self.planned_by.push(commitment_id.into());
    }

    /// Check if this transfer has been realized
    pub fn is_realized(&self) -> bool {
        !self.realized_by.is_empty()
    }

    /// Check if this transfer is planned
    pub fn is_planned(&self) -> bool {
        !self.planned_by.is_empty()
    }
}

/// Builder for Transfer
#[derive(Debug, Default)]
pub struct TransferBuilder {
    id: Option<String>,
    name: Option<String>,
    transfer_type: Option<TransferType>,
    realized_by: Vec<String>,
    planned_by: Vec<String>,
    part_of: Option<String>,
    realized_in: Option<String>,
    in_scope_of: Option<String>,
    note: Option<String>,
}

impl TransferBuilder {
    /// 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 transfer type
    pub fn transfer_type(mut self, transfer_type: TransferType) -> Self {
        self.transfer_type = Some(transfer_type);
        self
    }

    /// Add an event
    pub fn realized_by(mut self, event_id: impl Into<String>) -> Self {
        self.realized_by.push(event_id.into());
        self
    }

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

    /// Set the exchange this is part of
    pub fn part_of(mut self, exchange_id: impl Into<String>) -> Self {
        self.part_of = Some(exchange_id.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
    }

    /// 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 Transfer
    pub fn build(self) -> Result<Transfer> {
        let id = self.id.ok_or_else(|| Error::missing_field("id"))?;
        let name = self.name.ok_or_else(|| Error::missing_field("name"))?;
        let transfer_type = self.transfer_type.unwrap_or_default();

        Ok(Transfer {
            id,
            name,
            transfer_type,
            realized_by: self.realized_by,
            planned_by: self.planned_by,
            part_of: self.part_of,
            realized_in: self.realized_in,
            in_scope_of: self.in_scope_of,
            note: self.note,
            created_at: Utc::now(),
        })
    }
}

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

    #[test]
    fn test_transfer_builder() {
        let transfer = Transfer::builder()
            .id("transfer-001")
            .name("Sell tomatoes")
            .transfer_type(TransferType::Transfer)
            .build()
            .unwrap();

        assert_eq!(transfer.id, "transfer-001");
        assert_eq!(transfer.transfer_type, TransferType::Transfer);
    }

    #[test]
    fn test_transfer_with_events() {
        let mut transfer = Transfer::builder()
            .id("transfer-001")
            .name("Test Transfer")
            .build()
            .unwrap();

        transfer.add_event("event-001");
        transfer.add_event("event-002");

        assert!(transfer.is_realized());
        assert_eq!(transfer.realized_by.len(), 2);
    }

    #[cfg(feature = "serde")]
    #[test]
    fn test_transfer_serialization() {
        let transfer = Transfer::builder()
            .id("transfer-001")
            .name("Test")
            .transfer_type(TransferType::TransferCustody)
            .build()
            .unwrap();

        let json = serde_json::to_string(&transfer).unwrap();
        let parsed: Transfer = serde_json::from_str(&json).unwrap();
        assert_eq!(transfer.id, parsed.id);
        assert_eq!(transfer.transfer_type, parsed.transfer_type);
    }
}