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}; /// 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, /// 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>, ) -> 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 }, 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(&self) -> bool { let current_time = chrono::Utc::now().timestamp(); 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) < current_time; } false } pub async fn in_location(l: &str) -> Vec { Self::find(doc! { "location": l}, None, None).await.unwrap() } pub async fn in_location_recursive(l: &str) -> Vec { let locations = Location::find(doc! { "parent": l}, None, None) .await .unwrap(); let mut transactions = Self::find(doc! { "location": l}, None, None).await.unwrap(); for loc in locations { transactions.extend( Self::find(doc! { "location": loc.id() }, None, None) .await .unwrap(), ); } transactions } /// Get all Transactions which are not consumed and are expired pub async fn active_expired() -> 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 { if item.is_expired().await { Some(item) } else { None } }) .collect() .await; expired_items } } impl mongod::ToAPI for Transaction { async fn api(&self) -> serde_json::Value { json!({ "uuid": self._id, "item": self.item, "variant": self.variant, "price": self.price, "origin": self.origin, "timestamp": self.timestamp, "consumed": self.consumed, "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(), } } 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(()) } }