use std::collections::HashMap; use serde::{Deserialize, Serialize}; use serde_json::json; use crate::{get_pg, transaction::Transaction}; 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, /// Associated barcodes pub barcodes: 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()), barcodes: json.as_mapping().unwrap().get("barcodes").map(|x| { x.as_sequence().unwrap().into_iter().map(|x| x.as_i64().unwrap()).collect() }) } } 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 res: Vec<(uuid::Uuid,)> = sqlx::query_as( "SELECT id FROM transactions WHERE item = $1 AND variant = $2 ORDER BY created DESC", ) .bind(&self.item) .bind(&self.variant) .fetch_all(get_pg!()) .await .unwrap(); res.into_iter().map(|x| x.0.to_string()).collect() } /// Returns the active Transaction of this Item Variant which are not yet consumed. pub async fn inventory(&self) -> Vec { sqlx::query_as("SELECT * FROM transactions WHERE item = $1 AND variant = $2 AND consumed_timestamp IS NULL ORDER BY created DESC") .bind(&self.item) .bind(&self.variant) .fetch_all(get_pg!()).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 res: Vec<(uuid::Uuid,)> = if let Some(destination) = destination { sqlx::query_as( "SELECT id FROM transactions WHERE item = $1 AND variant = $2 AND consumed_timestamp IS NOT NULL AND destination = $3 ORDER BY created DESC" ) .bind(&self.item) .bind(&self.variant) .bind(destination) .fetch_all(get_pg!()).await.unwrap() } else { sqlx::query_as( "SELECT id FROM transactions WHERE item = $1 AND variant = $2 AND consumed_timestamp IS NOT NULL ORDER BY created DESC" ) .bind(&self.item) .bind(&self.variant) .fetch_all(get_pg!()).await.unwrap() }; res.into_iter().map(|x| x.0.to_string()).collect() } pub async fn demand(uuid: &uuid::Uuid, price: f64, destination: &str) -> Option { // check if transaction exists let t = Transaction::get(uuid).await?; Some(t.consume(price, destination).await) } /// 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: f64, origin: Option<&str>, location: Option<&str>, note: Option<&str>, ) -> Transaction { Transaction::new(&self.item, &self.variant, price, origin, location, note).await } /// Returns all Transactions of this Item Variant pub async fn get_all_transactions(&self) -> Vec { sqlx::query_as( "SELECT * FROM transactions WHERE item = $1 AND variant = $2 ORDER BY created DESC", ) .bind(&self.item) .bind(&self.variant) .fetch_all(get_pg!()) .await .unwrap() } pub async fn get_transaction_timeslice(&self, year: i32, month: u32) -> Vec { let (start, end) = timestamp_range(year, month); sqlx::query_as("SELECT * FROM transactions WHERE created BETWEEN to_timestamp($1) AND to_timestamp($2) ORDER BY created DESC") .bind(start) .bind(end) .fetch_all(get_pg!()).await.unwrap() } pub async fn get_unique_origins(&self) -> Vec { let res: Vec<(String,)> = sqlx::query_as("SELECT DISTINCT(origin) FROM transactions WHERE origin NOT LIKE 'flow::%' AND item = $1 AND variant = $2") .bind(&self.item) .bind(&self.variant) .fetch_all(get_pg!()).await.unwrap(); res.into_iter().map(|x| x.0).collect() } pub async fn get_unique_destinations(&self) -> Vec { let res: Vec<(String,)> = sqlx::query_as("SELECT DISTINCT(destination) FROM transactions WHERE destination NOT LIKE 'flow::%' AND item = $1 AND variant = $2") .bind(&self.item) .bind(&self.variant) .fetch_all(get_pg!()).await.unwrap(); res.into_iter().map(|x| x.0).collect() } pub async fn price_history_by_origin(&self, origin: &str, limit: Option) -> Vec { let res: Vec<(f64,)> = sqlx::query_as( &format!("SELECT price FROM transactions WHERE item = $1 AND variant = $2 AND origin = $3 ORDER BY created DESC {}", if let Some(limit) = limit { format!("LIMIT {limit}") } else { String::new() }) ) .bind(&self.item) .bind(&self.variant) .bind(origin) .fetch_all(get_pg!()).await.unwrap(); res.into_iter().map(|x| x.0).collect() } pub async fn get_latest_price(&self, origin: Option) -> f64 { if let Some(origin) = origin { let res: (f64,) = sqlx::query_as("SELECT price FROM transactions WHERE item = $1 AND variant = $2 AND origin = $3 ORDER BY created DESC LIMIT 1") .bind(&self.item) .bind(&self.variant) .bind(origin) .fetch_one(get_pg!()).await.unwrap(); res.0 } else { let res: (f64,) = sqlx::query_as("SELECT price FROM transactions WHERE item = $1 AND variant = $2 ORDER BY created DESC LIMIT 1") .bind(&self.item) .bind(&self.variant) .bind(origin) .fetch_one(get_pg!()).await.unwrap(); res.0 } } /// 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; let total_price: f64 = active_transactions.iter().map(|x| x.price).sum(); if !full { return json!({ "amount": active_transactions.len(), "total_price": total_price }); } let all_transactions: Vec = sqlx::query_as( "SELECT * FROM transactions WHERE item = $1 AND variant = $2 ORDER BY created DESC", ) .bind(&self.item) .bind(&self.variant) .fetch_all(get_pg!()) .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) .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, "barcodes": self.barcodes }) } }