use futures::StreamExt; use mongod::{ assert_reference_of, derive::{Model, Referencable}, reference_of, Model, Referencable, Reference, Sort, Validate, }; use mongodb::bson::doc; use serde::{Deserialize, Serialize}; use serde_json::json; use crate::{item::Item, location::Location}; // todo : produced / consumed by flow field? /// A Transaction of an Item Variant #[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)] pub struct Transaction { /// UUID pub _id: String, /// Associated Item pub item: String, /// Associated Variant pub variant: String, /// Price of obtaining the Item pub price: Price, /// Origin of the Item pub origin: Option, /// The location of the Item pub location: Option, /// Info on consumption of the Item pub consumed: Option, /// Notes on Transaction pub note: Option, /// Timestamp of the Transaction pub timestamp: i64, } impl Validate for Transaction { async fn validate(&self) -> Result<(), String> { if let Some(location) = &self.location { assert_reference_of!(location, Location); } Ok(()) } } /// Information about consumed Items #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Consumed { /// Destination of the Item or who consumed it pub destination: String, /// Price the Item was exported or consumed at pub price: Price, /// Timestamp of Consumption pub timestamp: i64, } impl Transaction { pub async fn new( item: &str, variant: &str, price: Price, origin: Option<&str>, location: Option<&str>, note: Option<&str>, ) -> Self { Self { _id: uuid::Uuid::new_v4().to_string(), item: item.to_string(), variant: variant.to_string(), price, consumed: None, origin: origin.map(std::string::ToString::to_string), location: if let Some(location) = location { reference_of!(Location, location) } else { None }, note: note.map(|x| x.to_string()), timestamp: chrono::Utc::now().timestamp(), } } /// Consumes the Item with `price` and `destination` pub async fn consume(self, price: Price, destination: &str) -> Self { self.change() .consumed(Some(Consumed { destination: destination.to_string(), price, timestamp: chrono::Utc::now().timestamp(), })) .update() .await .unwrap() } pub async fn is_expired_at(&self, time: i64) -> bool { if let Some(expiry) = Item::get(&self.item) .await .unwrap() .variant(&self.variant) .unwrap() .expiry { let date_added = self.timestamp; let expiration_ts = expiry * 24 * 60 * 60; return (date_added + expiration_ts) < time; } false } pub async fn is_expired_in_days(&self, days: i64) -> bool { let current_time = chrono::Utc::now().timestamp(); self.is_expired_at(current_time + (days * 24 * 60 * 60)).await } pub async fn is_expired(&self) -> bool { let current_time = chrono::Utc::now().timestamp(); self.is_expired_at(current_time).await } pub async fn in_location(l: &str) -> Option> { let l = reference_of!(Location, l)?; Some( Self::find( doc! { "location": l, "consumed": { "$not": { "$type": "object" } }}, None, None, ) .await .unwrap(), ) } pub async fn in_location_recursive(l: &str) -> Option> { // get the children of this location let locations = Location::get(l).await?.children_recursive().await; let l = reference_of!(Location, l)?; let mut transactions = Self::find( doc! { "location": l, "consumed": { "$not": { "$type": "object" } },}, None, None, ) .await .unwrap(); for loc in locations { transactions.extend( Self::find(doc! { "location": loc.reference(), "consumed": { "$not": { "$type": "object" } }}, None, None) .await .unwrap(), ); } Some(transactions) } /// Get all Transactions which are not consumed and are expired pub async fn active_expired(days: Option) -> Vec { let items = Self::find( doc! { "consumed": { "$not": { "$type": "object" } } }, None, Some(doc! { "timestamp": Sort::Descending }), ) .await .unwrap(); let expired_items: Vec<_> = futures::stream::iter(items) .filter_map(|item| async move { let expired = if let Some(days) = days { item.is_expired_in_days(days).await } else { item.is_expired().await }; if expired { Some(item) } else { None } }) .collect() .await; expired_items } } impl mongod::ToAPI for Transaction { async fn api(&self) -> serde_json::Value { let location = if let Some(loc) = &self.location { Some(loc.get::().await.api().await) } else { None }; json!({ "uuid": self._id, "item": self.item, "variant": self.variant, "price": self.price, "origin": self.origin, "location": location, "timestamp": self.timestamp, "consumed": self.consumed, "note": self.note, "expired": self.is_expired().await }) } } /// Economic Price #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Price { /// Value of the currency pub value: f64, /// Kind of currency pub currency: String, } impl Price { pub fn new(value: f64, currency: &str) -> Self { Self { value, currency: currency.to_string(), } } pub fn zero() -> Self { Self { value: 0.00, currency: String::new(), } } fn parse(price: &str) -> Option { let (value, currency) = price.split_once(' ')?; Some(Self { value: value.parse().ok()?, currency: currency.to_string(), }) } } impl TryFrom for Price { type Error = (); fn try_from(value: String) -> Result { Self::parse(&value).ok_or(()) } }