use std::collections::HashMap; use futures::StreamExt; use mongod::{Model, Sort}; use mongodb::bson::doc; use serde::{Deserialize, Serialize}; use serde_json::json; use crate::transaction::{Price, Transaction}; pub fn sort_by_timestamp() -> mongodb::bson::Document { doc! { "timestamp": mongod::Sort::Descending } } pub fn timestamp_range(year: i32, month: u32) -> (i64, i64) { let d = chrono::NaiveDate::from_ymd_opt(year, month, 0).unwrap(); let t = chrono::NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap(); let start = chrono::NaiveDateTime::new(d, t).and_utc().timestamp(); assert!(month <= 12); let end = if month == 12 { let d = chrono::NaiveDate::from_ymd_opt(year + 1, month, 0).unwrap(); let t = chrono::NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap(); chrono::NaiveDateTime::new(d, t).and_utc().timestamp() } else { let d = chrono::NaiveDate::from_ymd_opt(year, month + 1, 0).unwrap(); let t = chrono::NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap(); chrono::NaiveDateTime::new(d, t).and_utc().timestamp() }; (start, end) } /// Represents a specific instance of an item with potential variations. /// /// This struct is used to describe a particular variation or instance of an item /// in the real world. It may include attributes or properties that deviate from /// the standard definition of the item. For example, different colors, sizes, or /// configurations. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Variant { /// Associated Item pub item: String, /// Variant ID pub variant: String, /// Variant Name pub name: String, /// Minimum amount pub min: Option, /// Days until expiry pub expiry: Option, } impl Variant { /// Create variant from itemdb yaml pub fn from_yml(json: &serde_yaml::Value, variant: &str, item: &str) -> Self { Self { item: item.to_string(), variant: variant.to_string(), name: json .as_mapping() .unwrap() .get("name") .unwrap() .as_str() .unwrap() .to_string(), min: json .as_mapping() .unwrap() .get("min") .map(|x| x.as_i64().unwrap()), expiry: json .as_mapping() .unwrap() .get("expiry") .map(|x| x.as_i64().unwrap()), } } pub fn item_variant_id(&self) -> String { format!("{}::{}", self.item, self.variant) } /// Returns the IDs of Transactions from this Item Variant. pub async fn supply_log(&self) -> Vec { let filter = doc! { "item": &self.item, "variant": &self.variant }; let result = Transaction::find_partial(filter, json!({}), None, None) .await .unwrap(); let mut ret = Vec::new(); for doc in result { ret.push(doc._id); } ret } /// Returns the active Transaction of this Item Variant which are not yet consumed. pub async fn inventory(&self) -> Vec { let filter = doc! { "item": &self.item, "variant": &self.variant, "consumed": { "$not": { "$type": "object" } } }; Transaction::find(filter, None, None).await.unwrap() } /// Returns the IDs of the Transactions from this Item Variant which are consumed. pub async fn demand_log(&self, destination: Option<&str>) -> Vec { let filter = if let Some(dest) = destination { doc! { "item": &self.item, "variant": &self.variant, "consumed": { "destination": dest } } } else { doc! { "item": &self.item, "variant": &self.variant, "consumed": { "$type": "object" } } }; let result = Transaction::find_partial(filter, json!({}), None, None) .await .unwrap(); let mut ret = Vec::new(); for doc in result { ret.push(doc._id); } ret } pub async fn demand(uuid: &str, price: Price, destination: &str) -> Option { // check if transaction exists let mut t = Transaction::get(uuid).await?; t = t.consume(price, destination).await; Some(t) } /// Records a supply transaction in the database. /// /// # Arguments /// /// * `price` - The price of the supplied items. /// * `origin` - The origin or source of the supplied items. /// /// # Returns /// /// Returns a UUID string representing the transaction. pub async fn supply( &self, price: Price, origin: Option<&str>, location: Option<&str>, note: Option<&str>, ) -> Transaction { let t = Transaction::new(&self.item, &self.variant, price, origin, location, note).await; t.insert().await.unwrap(); t } /// Returns all Transactions of this Item Variant pub async fn get_all_transactions(&self) -> Vec { let filter = doc! { "item": &self.item, "variant": &self.variant }; Transaction::find(filter, None, Some(doc! { "timestamp": Sort::Descending })) .await .unwrap() } pub async fn get_transaction_timeslice(&self, year: i32, month: u32) -> Vec { let (start, end) = timestamp_range(year, month); Transaction::find( doc! { "timestamp": { "$gte": start, "$lte": end } }, None, Some(sort_by_timestamp()), ) .await .unwrap() } pub async fn get_unique_origins(&self) -> Vec { unique_flows( &Transaction::unique( doc! { "item": &self.item, "variant": &self.variant }, "origin", ) .await, ) } pub async fn get_unique_destinations(&self) -> Vec { unique_flows( &Transaction::unique( doc! { "item": &self.item, "variant": &self.variant }, "consumed.destination", ) .await, ) } pub async fn price_history_by_origin(&self, origin: &str, limit: Option) -> Vec { Transaction::find( doc! { "item": &self.item, "variant": &self.variant, "origin": origin }, limit, Some(sort_by_timestamp()), ) .await .unwrap() .into_iter() .map(|x| x.price) .collect() } pub async fn get_latest_price(&self, origin: Option) -> Price { let mut filter = doc! { "item": &self.item, "variant": &self.variant }; if let Some(origin) = origin { filter.insert("origin", origin); } Transaction::find(filter, Some(1), Some(sort_by_timestamp())) .await .unwrap() .first() .unwrap() .price .clone() } /// Check if item variant is below minimum. Returns if this is the case and the number needed to fulfill minimum pub async fn is_below_min(&self) -> (bool, i64) { if let Some(min) = self.min { let amount = self.inventory().await.len() as i64; if amount < min { return (true, min - amount); } } (false, 0) } pub async fn stat(&self, full: bool) -> serde_json::Value { let active_transactions = self.inventory().await; // fix : ignores currency let total_price: f64 = active_transactions.iter().map(|x| x.price.value).sum(); if !full { return json!({ "amount": active_transactions.len(), "total_price": total_price }); } let all_transactions = Transaction::find( doc! { "item": &self.item, "variant": &self.variant}, None, None, ) .await .unwrap(); let mut expired_count = 0.0; for t in &all_transactions { if t.is_expired().await { expired_count += 1.0; } } let expiry_rate = expired_count / all_transactions.len() as f64; let mut origin_stat = HashMap::new(); for origin in self.get_unique_origins().await { let transactions_from_origin = active_transactions .iter() .filter(|x| x.origin.as_ref().map(|x| *x == origin).unwrap_or(false)) .collect::>(); let prices = self .price_history_by_origin(&origin, None) .await .into_iter() .map(|x| x.value) .collect::>(); let prices_len = prices.len() as f64; let prices_summed = prices.into_iter().reduce(|acc, e| acc + e).unwrap_or(0.0); let stat_json = json!({ "average_price": prices_summed / prices_len, "inventory": transactions_from_origin.len() }); origin_stat.insert(origin, stat_json); } json!({ "amount": active_transactions.len(), "total_price": total_price, "expiry_rate": expiry_rate, "origins": origin_stat }) } pub fn api_json(&self) -> serde_json::Value { json!({ "item": self.item, "variant": self.variant, "name": self.name, "min": self.min, "expiry": self.expiry }) } } pub fn unique_flows(i: &[String]) -> Vec { let mut unique_vec: Vec = Vec::new(); for s in i { // Check if the string starts with "flow::" if let Some(suffix) = s.strip_prefix("flow::") { // Extract the part after "flow::" and split on "::" to get the kind (ignoring id) let parts: Vec<&str> = suffix.split("::").collect(); if let Some(kind) = parts.first() { // Build the common prefix "flow::kind" let common_prefix = format!("flow::{}", kind); if !unique_vec.contains(&common_prefix) { unique_vec.push(common_prefix); } } } else { // If the string doesn't start with "flow::", retain it // Assumption: Except "flow::" values, everything should be already unique unique_vec.push(s.to_string()); } } unique_vec }