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 ResourceSpecification {
pub id: String,
pub name: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub note: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub image: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub default_unit_of_resource: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub default_unit_of_effort: Option<String>,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))]
pub classified_as: Vec<String>,
pub substitutable: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl ResourceSpecification {
pub fn builder() -> ResourceSpecificationBuilder {
ResourceSpecificationBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct ResourceSpecificationBuilder {
id: Option<String>,
name: Option<String>,
note: Option<String>,
image: Option<String>,
default_unit_of_resource: Option<String>,
default_unit_of_effort: Option<String>,
classified_as: Vec<String>,
substitutable: bool,
}
impl ResourceSpecificationBuilder {
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
pub fn image(mut self, image: impl Into<String>) -> Self {
self.image = Some(image.into());
self
}
pub fn default_unit_of_resource(mut self, unit: impl Into<String>) -> Self {
self.default_unit_of_resource = Some(unit.into());
self
}
pub fn default_unit_of_effort(mut self, unit: impl Into<String>) -> Self {
self.default_unit_of_effort = Some(unit.into());
self
}
pub fn classified_as(mut self, classification: impl Into<String>) -> Self {
self.classified_as.push(classification.into());
self
}
pub fn substitutable(mut self, substitutable: bool) -> Self {
self.substitutable = substitutable;
self
}
pub fn build(self) -> Result<ResourceSpecification> {
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(ResourceSpecification {
id,
name,
note: self.note,
image: self.image,
default_unit_of_resource: self.default_unit_of_resource,
default_unit_of_effort: self.default_unit_of_effort,
classified_as: self.classified_as,
substitutable: self.substitutable,
created_at: now,
updated_at: now,
})
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct EconomicResource {
pub id: String,
pub name: String,
pub conforms_to: String,
pub primary_accountable: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub custodian: Option<String>,
pub accounting_quantity: Measure,
pub onhand_quantity: Measure,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub current_location: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub lot: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub tracking_identifier: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub contained_in: 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 note: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub image: Option<String>,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty"))]
pub classified_as: Vec<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl EconomicResource {
pub fn builder() -> EconomicResourceBuilder {
EconomicResourceBuilder::default()
}
pub fn has_accounting_quantity(&self) -> bool {
!self.accounting_quantity.is_zero()
}
pub fn has_onhand_quantity(&self) -> bool {
!self.onhand_quantity.is_zero()
}
pub fn increment_accounting(&mut self, amount: &Measure) -> Result<()> {
if !self.accounting_quantity.same_unit(amount) {
return Err(Error::UnitMismatch {
unit1: self.accounting_quantity.unit.to_string(),
unit2: amount.unit.to_string(),
});
}
self.accounting_quantity = self.accounting_quantity.add(amount).unwrap();
self.updated_at = Utc::now();
Ok(())
}
pub fn decrement_accounting(&mut self, amount: &Measure) -> Result<()> {
if !self.accounting_quantity.same_unit(amount) {
return Err(Error::UnitMismatch {
unit1: self.accounting_quantity.unit.to_string(),
unit2: amount.unit.to_string(),
});
}
self.accounting_quantity = self.accounting_quantity.sub(amount).unwrap();
self.updated_at = Utc::now();
Ok(())
}
pub fn increment_onhand(&mut self, amount: &Measure) -> Result<()> {
if !self.onhand_quantity.same_unit(amount) {
return Err(Error::UnitMismatch {
unit1: self.onhand_quantity.unit.to_string(),
unit2: amount.unit.to_string(),
});
}
self.onhand_quantity = self.onhand_quantity.add(amount).unwrap();
self.updated_at = Utc::now();
Ok(())
}
pub fn decrement_onhand(&mut self, amount: &Measure) -> Result<()> {
if !self.onhand_quantity.same_unit(amount) {
return Err(Error::UnitMismatch {
unit1: self.onhand_quantity.unit.to_string(),
unit2: amount.unit.to_string(),
});
}
self.onhand_quantity = self.onhand_quantity.sub(amount).unwrap();
self.updated_at = Utc::now();
Ok(())
}
pub fn set_location(&mut self, location: impl Into<String>) {
self.current_location = Some(location.into());
self.updated_at = Utc::now();
}
pub fn set_stage(&mut self, stage: impl Into<String>) {
self.stage = Some(stage.into());
self.updated_at = Utc::now();
}
pub fn set_state(&mut self, state: impl Into<String>) {
self.state = Some(state.into());
self.updated_at = Utc::now();
}
pub fn set_contained_in(&mut self, container: impl Into<String>) {
self.contained_in = Some(container.into());
self.updated_at = Utc::now();
}
pub fn remove_from_container(&mut self) {
self.contained_in = None;
self.updated_at = Utc::now();
}
pub fn transfer_accountable(&mut self, new_accountable: impl Into<String>) {
self.primary_accountable = new_accountable.into();
self.updated_at = Utc::now();
}
pub fn transfer_custody(&mut self, new_custodian: impl Into<String>) {
self.custodian = Some(new_custodian.into());
self.updated_at = Utc::now();
}
}
#[derive(Debug, Default)]
pub struct EconomicResourceBuilder {
id: Option<String>,
name: Option<String>,
conforms_to: Option<String>,
primary_accountable: Option<String>,
custodian: Option<String>,
accounting_quantity: Option<Measure>,
onhand_quantity: Option<Measure>,
current_location: Option<String>,
lot: Option<String>,
tracking_identifier: Option<String>,
contained_in: Option<String>,
stage: Option<String>,
state: Option<String>,
note: Option<String>,
image: Option<String>,
classified_as: Vec<String>,
}
impl EconomicResourceBuilder {
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn conforms_to(mut self, spec_id: impl Into<String>) -> Self {
self.conforms_to = Some(spec_id.into());
self
}
pub fn primary_accountable(mut self, agent_id: impl Into<String>) -> Self {
self.primary_accountable = Some(agent_id.into());
self
}
pub fn custodian(mut self, agent_id: impl Into<String>) -> Self {
self.custodian = Some(agent_id.into());
self
}
pub fn accounting_quantity(mut self, quantity: Measure) -> Self {
self.accounting_quantity = Some(quantity);
self
}
pub fn onhand_quantity(mut self, quantity: Measure) -> Self {
self.onhand_quantity = Some(quantity);
self
}
pub fn current_location(mut self, location: impl Into<String>) -> Self {
self.current_location = Some(location.into());
self
}
pub fn lot(mut self, lot: impl Into<String>) -> Self {
self.lot = Some(lot.into());
self
}
pub fn tracking_identifier(mut self, tracking_id: impl Into<String>) -> Self {
self.tracking_identifier = Some(tracking_id.into());
self
}
pub fn contained_in(mut self, container_id: impl Into<String>) -> Self {
self.contained_in = Some(container_id.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 note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
pub fn image(mut self, image: impl Into<String>) -> Self {
self.image = Some(image.into());
self
}
pub fn classified_as(mut self, classification: impl Into<String>) -> Self {
self.classified_as.push(classification.into());
self
}
pub fn build(self) -> Result<EconomicResource> {
let id = self.id.ok_or_else(|| Error::missing_field("id"))?;
let name = self.name.ok_or_else(|| Error::missing_field("name"))?;
let conforms_to = self
.conforms_to
.ok_or_else(|| Error::missing_field("conforms_to"))?;
let primary_accountable = self
.primary_accountable
.ok_or_else(|| Error::missing_field("primary_accountable"))?;
let accounting_quantity = self
.accounting_quantity
.ok_or_else(|| Error::missing_field("accounting_quantity"))?;
let onhand_quantity = self
.onhand_quantity
.unwrap_or_else(|| accounting_quantity.clone());
let now = Utc::now();
Ok(EconomicResource {
id,
name,
conforms_to,
primary_accountable,
custodian: self.custodian,
accounting_quantity,
onhand_quantity,
current_location: self.current_location,
lot: self.lot,
tracking_identifier: self.tracking_identifier,
contained_in: self.contained_in,
stage: self.stage,
state: self.state,
note: self.note,
image: self.image,
classified_as: self.classified_as,
created_at: now,
updated_at: now,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::measures::Unit;
#[test]
fn test_resource_specification_builder() {
let spec = ResourceSpecification::builder()
.id("spec-001")
.name("Organic Tomatoes")
.note("Fresh organic tomatoes")
.substitutable(true)
.build()
.unwrap();
assert_eq!(spec.id, "spec-001");
assert_eq!(spec.name, "Organic Tomatoes");
assert!(spec.substitutable);
}
#[test]
fn test_economic_resource_builder() {
let resource = EconomicResource::builder()
.id("resource-001")
.name("Tomato Batch #1")
.conforms_to("spec-001")
.primary_accountable("agent-001")
.accounting_quantity(Measure::new(100, Unit::Kilogram))
.build()
.unwrap();
assert_eq!(resource.id, "resource-001");
assert_eq!(resource.primary_accountable, "agent-001");
assert!(resource.has_accounting_quantity());
}
#[test]
fn test_resource_quantity_operations() {
let mut resource = EconomicResource::builder()
.id("resource-001")
.name("Test")
.conforms_to("spec-001")
.primary_accountable("agent-001")
.accounting_quantity(Measure::new(100, Unit::Each))
.build()
.unwrap();
resource
.increment_accounting(&Measure::new(50, Unit::Each))
.unwrap();
assert_eq!(resource.accounting_quantity.value, 150.into());
resource
.decrement_accounting(&Measure::new(30, Unit::Each))
.unwrap();
assert_eq!(resource.accounting_quantity.value, 120.into());
}
#[test]
fn test_resource_unit_mismatch() {
let mut resource = EconomicResource::builder()
.id("resource-001")
.name("Test")
.conforms_to("spec-001")
.primary_accountable("agent-001")
.accounting_quantity(Measure::new(100, Unit::Each))
.build()
.unwrap();
let result = resource.increment_accounting(&Measure::new(50, Unit::Kilogram));
assert!(result.is_err());
}
#[cfg(feature = "serde")]
#[test]
fn test_resource_serialization() {
let resource = EconomicResource::builder()
.id("resource-001")
.name("Test")
.conforms_to("spec-001")
.primary_accountable("agent-001")
.accounting_quantity(Measure::new(100, Unit::Each))
.build()
.unwrap();
let json = serde_json::to_string(&resource).unwrap();
let parsed: EconomicResource = serde_json::from_str(&json).unwrap();
assert_eq!(resource.id, parsed.id);
}
}