204 lines
6 KiB
Rust
204 lines
6 KiB
Rust
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<String>,
|
|
/// The location of the Item
|
|
pub location: Option<String>,
|
|
/// Notes on Transaction
|
|
pub note: Option<String>,
|
|
/// Timestamp of the Transaction
|
|
pub created: chrono::DateTime<chrono::Utc>,
|
|
/// Destination of the Item or who consumed it
|
|
pub destination: Option<String>,
|
|
/// Price the Item was exported or consumed at
|
|
pub consumed_price: Option<f64>,
|
|
/// Timestamp of Consumption
|
|
pub consumed_timestamp: Option<chrono::DateTime<chrono::Utc>>,
|
|
}
|
|
|
|
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<Self> {
|
|
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<Self> {
|
|
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<Vec<Self>> {
|
|
// 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<i64>) -> Vec<Self> {
|
|
let items: Vec<Self> = 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
|
|
})
|
|
}
|
|
}
|