cdb/src/variant.rs

307 lines
11 KiB
Rust
Raw Normal View History

2024-09-27 09:19:32 +02:00
use std::collections::HashMap;
2024-07-24 16:38:56 +02:00
use serde::{Deserialize, Serialize};
2024-06-22 02:05:22 +02:00
use serde_json::json;
2024-05-03 18:22:59 +02:00
2024-10-07 20:53:58 +02:00
use crate::{get_pg, transaction::Transaction};
2024-08-28 09:14:33 +02:00
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)
}
2024-05-03 18:22:59 +02:00
/// 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.
2024-07-24 16:38:56 +02:00
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2024-05-03 18:22:59 +02:00
pub struct Variant {
2024-05-10 11:59:05 +02:00
/// Associated Item
2024-05-03 18:22:59 +02:00
pub item: String,
2024-08-28 09:38:10 +02:00
/// Variant ID
2024-05-03 18:22:59 +02:00
pub variant: String,
2024-08-28 09:38:10 +02:00
/// Variant Name
pub name: String,
2024-08-30 14:13:56 +02:00
/// Minimum amount
pub min: Option<i64>,
/// Days until expiry
pub expiry: Option<i64>,
2024-10-08 10:42:21 +02:00
/// Associated barcodes
pub barcodes: Option<Vec<i64>>
2024-05-03 18:22:59 +02:00
}
impl Variant {
2024-05-10 11:59:05 +02:00
/// Create variant from itemdb yaml
2024-05-06 08:24:23 +02:00
pub fn from_yml(json: &serde_yaml::Value, variant: &str, item: &str) -> Self {
Self {
item: item.to_string(),
variant: variant.to_string(),
2024-08-28 09:38:10 +02:00
name: json
2024-05-06 08:24:23 +02:00
.as_mapping()
.unwrap()
2024-08-28 09:38:10 +02:00
.get("name")
.unwrap()
.as_str()
2024-05-10 10:56:07 +02:00
.unwrap()
2024-08-28 09:38:10 +02:00
.to_string(),
2024-08-30 14:13:56 +02:00
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()),
2024-10-08 10:42:21 +02:00
barcodes: json.as_mapping().unwrap().get("barcodes").map(|x| {
x.as_sequence().unwrap().into_iter().map(|x| x.as_i64().unwrap()).collect()
})
2024-05-06 08:24:23 +02:00
}
}
2024-09-13 14:17:55 +02:00
pub fn item_variant_id(&self) -> String {
format!("{}::{}", self.item, self.variant)
}
2024-09-12 13:11:44 +02:00
/// Returns the IDs of Transactions from this Item Variant.
2024-05-10 11:59:05 +02:00
pub async fn supply_log(&self) -> Vec<String> {
2024-10-07 20:53:58 +02:00
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();
2024-05-10 11:59:05 +02:00
2024-10-07 20:53:58 +02:00
res.into_iter().map(|x| x.0.to_string()).collect()
2024-05-10 11:59:05 +02:00
}
2024-09-12 13:11:44 +02:00
/// Returns the active Transaction of this Item Variant which are not yet consumed.
2024-08-13 04:13:38 +02:00
pub async fn inventory(&self) -> Vec<Transaction> {
2024-10-07 20:53:58 +02:00
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()
2024-08-13 04:13:38 +02:00
}
2024-09-12 13:11:44 +02:00
/// Returns the IDs of the Transactions from this Item Variant which are consumed.
pub async fn demand_log(&self, destination: Option<&str>) -> Vec<String> {
2024-10-07 20:53:58 +02:00
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()
2024-09-12 13:11:44 +02:00
} else {
2024-10-07 20:53:58 +02:00
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()
2024-05-10 11:59:05 +02:00
};
2024-10-07 20:53:58 +02:00
res.into_iter().map(|x| x.0.to_string()).collect()
2024-05-10 11:59:05 +02:00
}
2024-10-07 20:53:58 +02:00
pub async fn demand(uuid: &uuid::Uuid, price: f64, destination: &str) -> Option<Transaction> {
2024-07-24 16:38:56 +02:00
// check if transaction exists
2024-10-07 20:53:58 +02:00
let t = Transaction::get(uuid).await?;
Some(t.consume(price, destination).await)
2024-05-03 18:22:59 +02:00
}
/// 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.
2024-09-02 18:40:02 +02:00
pub async fn supply(
&self,
2024-10-07 20:53:58 +02:00
price: f64,
2024-09-02 18:40:02 +02:00
origin: Option<&str>,
location: Option<&str>,
2024-09-21 01:38:22 +02:00
note: Option<&str>,
2024-09-12 11:48:09 +02:00
) -> Transaction {
2024-10-07 20:53:58 +02:00
Transaction::new(&self.item, &self.variant, price, origin, location, note).await
2024-05-03 18:22:59 +02:00
}
2024-05-10 10:56:07 +02:00
2024-09-12 13:11:44 +02:00
/// Returns all Transactions of this Item Variant
2024-08-28 09:14:33 +02:00
pub async fn get_all_transactions(&self) -> Vec<Transaction> {
2024-10-07 20:53:58 +02:00
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()
2024-08-28 09:14:33 +02:00
}
pub async fn get_transaction_timeslice(&self, year: i32, month: u32) -> Vec<Transaction> {
let (start, end) = timestamp_range(year, month);
2024-10-07 20:53:58 +02:00
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()
2024-08-28 09:14:33 +02:00
}
pub async fn get_unique_origins(&self) -> Vec<String> {
2024-10-07 20:53:58 +02:00
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()
2024-08-28 09:14:33 +02:00
}
pub async fn get_unique_destinations(&self) -> Vec<String> {
2024-10-07 20:53:58 +02:00
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()
2024-08-28 09:14:33 +02:00
}
2024-10-07 20:53:58 +02:00
pub async fn price_history_by_origin(&self, origin: &str, limit: Option<i64>) -> Vec<f64> {
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() })
2024-08-30 14:13:56 +02:00
)
2024-10-07 20:53:58 +02:00
.bind(&self.item)
.bind(&self.variant)
.bind(origin)
.fetch_all(get_pg!()).await.unwrap();
res.into_iter().map(|x| x.0).collect()
2024-08-30 14:13:56 +02:00
}
2024-10-07 20:53:58 +02:00
pub async fn get_latest_price(&self, origin: Option<String>) -> f64 {
2024-08-28 09:14:33 +02:00
if let Some(origin) = origin {
2024-10-07 20:53:58 +02:00
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
2024-08-28 09:14:33 +02:00
}
}
2024-09-12 10:58:49 +02:00
/// 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)
}
2024-09-27 09:19:32 +02:00
pub async fn stat(&self, full: bool) -> serde_json::Value {
2024-08-27 23:33:09 +02:00
let active_transactions = self.inventory().await;
2024-10-07 20:53:58 +02:00
let total_price: f64 = active_transactions.iter().map(|x| x.price).sum();
2024-08-27 23:33:09 +02:00
2024-09-27 09:19:32 +02:00
if !full {
return json!({
"amount": active_transactions.len(),
"total_price": total_price
});
}
2024-10-07 20:53:58 +02:00
let all_transactions: Vec<Transaction> = sqlx::query_as(
"SELECT * FROM transactions WHERE item = $1 AND variant = $2 ORDER BY created DESC",
2024-09-27 09:19:32 +02:00
)
2024-10-07 20:53:58 +02:00
.bind(&self.item)
.bind(&self.variant)
.fetch_all(get_pg!())
2024-09-27 09:19:32 +02:00
.await
.unwrap();
2024-10-07 20:53:58 +02:00
2024-09-27 09:19:32 +02:00
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::<Vec<_>>();
let prices = self
.price_history_by_origin(&origin, None)
.await
.into_iter()
2024-10-07 20:53:58 +02:00
.map(|x| x)
2024-09-27 09:19:32 +02:00
.collect::<Vec<_>>();
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);
}
2024-08-27 23:33:09 +02:00
json!({
2024-09-27 09:19:32 +02:00
"amount": active_transactions.len(),
"total_price": total_price,
"expiry_rate": expiry_rate,
"origins": origin_stat
2024-08-27 23:33:09 +02:00
})
}
2024-06-22 02:05:22 +02:00
pub fn api_json(&self) -> serde_json::Value {
json!({
"item": self.item,
"variant": self.variant,
2024-08-30 14:13:56 +02:00
"name": self.name,
"min": self.min,
2024-10-08 10:42:21 +02:00
"expiry": self.expiry,
"barcodes": self.barcodes
2024-06-22 02:05:22 +02:00
})
}
2024-05-03 18:22:59 +02:00
}