use futures::StreamExt; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::prelude::FromRow; use crate::{get_itemdb, get_locations, get_pg, routes::ToAPI}; // todo : produced / consumed by flow field? /// A Transaction of an Item Variant #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Transaction { /// UUID pub id: uuid::Uuid, /// Associated Item pub item: String, /// Associated Variant pub variant: String, /// Price of obtaining the Item pub price: f64, /// Origin of the Item pub origin: Option, /// The location of the Item pub location: Option, /// Notes on Transaction pub note: Option, /// Timestamp of the Transaction pub created: chrono::DateTime, /// Destination of the Item or who consumed it pub destination: Option, /// Price the Item was exported or consumed at pub consumed_price: Option, /// Timestamp of Consumption pub consumed_timestamp: Option>, } impl Transaction { pub async fn new( item: &str, variant: &str, price: f64, origin: Option<&str>, location: Option<&str>, note: Option<&str>, ) -> Self { sqlx::query_as("INSERT INTO transactions (item, variant, price, origin, location, note) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *") .bind(item) .bind(variant) .bind(price) .bind(origin) .bind(location) .bind(note) .fetch_one(get_pg!()).await.unwrap() } pub async fn get(id: &uuid::Uuid) -> Option { sqlx::query_as("SELECT * FROM transactions WHERE id = $1") .bind(id) .fetch_optional(get_pg!()) .await .unwrap() } /// Consumes the Item with `price` and `destination` pub async fn consume(self, price: f64, destination: &str) -> Self { sqlx::query_as( "UPDATE transactions SET consumed_timestamp = current_timestamp, consumed_price = $1, destination = $2 WHERE id = $3 RETURNING *") .bind(price) .bind(destination) .bind(&self.id) .fetch_one(get_pg!()).await.unwrap() } pub async fn is_expired_at(&self, time: i64) -> bool { if let Some(expiry) = get_itemdb!() .get_item(&self.item) .unwrap() .variant(&self.variant) .unwrap() .expiry { let date_added = self.created.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 { if self.consumed_timestamp.is_some() { if let Some(expiry) = get_itemdb!() .get_item(&self.item) .unwrap() .variant(&self.variant) .unwrap() .expiry { let time_around = self.created.timestamp() - self.consumed_timestamp.unwrap().timestamp(); let expiration_ts = expiry * 24 * 60 * 60; return time_around > expiration_ts; } else { return false; } } let current_time = chrono::Utc::now().timestamp(); self.is_expired_at(current_time).await } pub async fn in_location(l: &str) -> Vec { sqlx::query_as( "SELECT * FROM transactions WHERE location = $1 AND consumed_timestamp IS NULL", ) .bind(l) .fetch_all(get_pg!()) .await .unwrap() } pub async fn in_location_recursive(l: &str) -> Option> { // get the children of this location let locations = get_locations!().get(l)?.children_recursive(); let mut transactions = Self::in_location(l).await; for loc in locations { transactions.extend(Self::in_location(&loc.id).await); } Some(transactions) } /// Get all Transactions which are not consumed and are expired pub async fn active_expired(days: Option) -> Vec { let items: Vec = sqlx::query_as( "SELECT * FROM transactions WHERE consumed_timestamp IS NULL ORDER BY created DESC", ) .fetch_all(get_pg!()) .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 ToAPI for Transaction { async fn api(&self) -> serde_json::Value { let location = if let Some(loc) = &self.location { Some(get_locations!().get(loc).unwrap().api().await) } else { None }; let consumed = if self.consumed_timestamp.is_some() { Some(json!({ "destination": self.destination, "price": self.consumed_price, "timestamp": self.consumed_timestamp.unwrap().timestamp() })) } else { None }; json!({ "uuid": self.id, "item": self.item, "variant": self.variant, "price": self.price, "origin": self.origin, "location": location, "timestamp": self.created.timestamp(), "consumed": consumed, "note": self.note, "expired": self.is_expired().await }) } }