♻️ refactor
All checks were successful
ci/woodpecker/push/build Pipeline was successful

This commit is contained in:
JMARyA 2025-04-16 04:27:45 +02:00
parent 443b52bf38
commit 91d39a4d83
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
7 changed files with 29 additions and 7 deletions

189
src/core/flow.rs Normal file
View file

@ -0,0 +1,189 @@
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap;
use crate::routes::ApiError;
use crate::routes::item::{SupplyForm, item_does_not_exist_error, variant_does_not_exist_error};
use crate::{get_itemdb, get_pg};
use based::request::api::ToAPI;
use sqlx::FromRow;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowInfo {
#[serde(default)]
pub id: String,
pub name: String,
#[serde(default)]
pub depends: Vec<String>,
pub next: Option<String>,
pub produces: Option<Vec<String>>,
}
impl ToAPI for FlowInfo {
async fn api(&self) -> serde_json::Value {
json!({
"id": self.id,
"name": self.name,
"depends": self.depends,
"next": self.next,
"produces": self.produces
})
}
}
/// A production flow
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Flow {
/// ID
pub id: uuid::Uuid,
/// Tiemstamp when the flow was started
pub started: chrono::DateTime<chrono::Utc>,
/// Kind of flow; ID of the describing JSON
pub kind: String,
/// Input transactions
pub input: Option<Vec<uuid::Uuid>>,
/// Timestamp when the flow was ended
pub ended: Option<chrono::DateTime<chrono::Utc>>,
/// The flow succedding this one
pub next: Option<uuid::Uuid>,
/// Transactions this flow produced
pub produced: Option<Vec<uuid::Uuid>>,
}
impl ToAPI for Flow {
async fn api(&self) -> serde_json::Value {
let done = if self.ended.is_some() {
Some(json!({
"ended": self.ended.map(|x| x.timestamp()),
"next": self.next,
"produced": self.produced
}))
} else {
None
};
json!({
"id": self.id,
"started": self.started.timestamp(),
"kind": self.kind,
"input": self.input,
"done": done
})
}
}
impl Flow {
pub async fn get(id: &uuid::Uuid) -> Option<Self> {
sqlx::query_as("SELECT * FROM flows WHERE id = $1")
.bind(id)
.fetch_optional(get_pg!())
.await
.unwrap()
}
pub async fn create(kind: &str, input: Option<Vec<uuid::Uuid>>) -> Self {
sqlx::query_as("INSERT INTO flows (kind, input) VALUES ($1, $2) RETURNING *")
.bind(kind)
.bind(input)
.fetch_one(get_pg!())
.await
.unwrap()
}
pub async fn end(self) -> Self {
sqlx::query_as("UPDATE flows SET ended = current_timestamp RETURNING *")
.fetch_one(get_pg!())
.await
.unwrap()
}
pub async fn end_with_produce(
self,
produced: &[SupplyForm],
) -> Result<HashMap<String, Vec<uuid::Uuid>>, ApiError> {
let mut ret = HashMap::new();
let mut t_create = Vec::new();
let mut produced_ref = Vec::with_capacity(ret.len());
for prod in produced {
let t = get_itemdb!()
.get_item(&prod.item)
.ok_or_else(item_does_not_exist_error)?
.variant(&prod.variant)
.ok_or_else(variant_does_not_exist_error)?;
t_create.push((t, prod));
}
for (item, info) in t_create {
let t = item
.supply(
0.0,
Some(&format!("flow::{}::{}", self.kind, self.id)),
info.location.as_ref().map(|x| x.as_str()),
info.note.as_ref().map(|x| x.as_str()),
)
.await;
ret.entry(item.item_variant_id().clone())
.or_insert(Vec::new())
.push(t.id.clone());
produced_ref.push(t.id);
}
sqlx::query("UPDATE transactions SET consumed_timestamp = current_timestamp, produced = $1 WHERE id = $2")
.bind(produced_ref)
.bind(self.id)
.execute(get_pg!()).await.unwrap();
Ok(ret)
}
pub async fn continue_next(self, next_flow: &Flow) -> Self {
sqlx::query_as("UPDATE transactions SET consumed_timestamp = current_timestamp, \"next\" = $1 WHERE id = $2 RETURNING *")
.bind(next_flow.id)
.bind(&self.id)
.fetch_one(get_pg!()).await.unwrap()
}
}
/// A note for a Flow
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct FlowNote {
/// ID
pub id: uuid::Uuid,
/// Timestamp when the note was created
pub time: chrono::DateTime<chrono::Utc>,
/// Note Content
pub content: String,
/// Associated flow
pub on_flow: uuid::Uuid,
}
impl FlowNote {
pub async fn create(content: &str, flow: &uuid::Uuid) -> Self {
sqlx::query_as("INSERT INTO flow_notes (content, on_flow) VALUES ($1, $2) RETURNING *")
.bind(content)
.bind(flow)
.fetch_one(get_pg!())
.await
.unwrap()
}
pub async fn find_of(id: &str) -> Vec<Self> {
sqlx::query_as("SELECT * FROM flow_notes WHERE on_flow = $1 ORDER BY time DESC")
.bind(id)
.fetch_all(get_pg!())
.await
.unwrap()
}
}
impl ToAPI for FlowNote {
async fn api(&self) -> serde_json::Value {
json!({
"uuid": self.id,
"timestamp": self.time.timestamp(),
"content": self.content,
"on_flow": self.on_flow
})
}
}

166
src/core/item.rs Normal file
View file

@ -0,0 +1,166 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::get_pg;
use crate::transaction::Transaction;
use crate::variant::Variant;
// ITEM
// VARIANTS
// QUANTIZATION
// IMPORT / EXPORT
// DEPENDENCIES
// DEMAND STATS
// SEASONAL REVIEWS
/// Represents a single item in a collection of physical goods.
///
/// This struct serves as a blueprint for describing individual items within
/// a larger inventory or database of physical goods. It includes fields for
/// the name of the item and its category.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
/// The ID
pub id: String,
/// The name of the Item
pub name: String,
/// Category of the Item
pub category: Option<String>,
/// Image
pub image_path: Option<String>,
/// The variants of an Item.
/// Each key of the `HashMap<_>` is the ID of a variant and contains a `Variant`
pub variants: HashMap<String, Variant>,
}
/// Get the path of an image file in `path`
pub fn get_image(path: &std::path::Path) -> Option<String> {
let parent = path.parent()?;
let file_name = path.file_stem()?.to_str()?;
for ext in ["jpg", "jpeg", "webp", "png"] {
let mut img_file = parent.to_path_buf();
img_file.push(&format!("{file_name}.{ext}"));
if img_file.exists() {
return Some(img_file.display().to_string());
}
}
None
}
impl Item {
/// Creates a new `Item` from a parsed markdown document
pub fn new(doc: &mdq::Document) -> Self {
let path = std::path::Path::new(&doc.path);
let id = path.file_stem().unwrap().to_str().unwrap().to_lowercase();
let image_path = get_image(path);
let category = doc
.frontmatter
.as_mapping()
.unwrap()
.get("category")
.map(|x| x.as_str().unwrap().to_string());
let name = doc
.frontmatter
.as_mapping()
.unwrap()
.get("name")
.unwrap()
.as_str()
.unwrap()
.to_string();
let mut variants = HashMap::new();
let variants_lst = doc
.frontmatter
.as_mapping()
.unwrap()
.get("variants")
.map(|x| x.as_mapping().unwrap().clone())
.unwrap();
assert!(
!variants_lst.is_empty(),
"Item {id} needs at least one variant."
);
for (variant_id, variant) in variants_lst {
variants.insert(
variant_id.as_str().unwrap().to_string(),
Variant::from_yml(&variant, variant_id.as_str().unwrap(), &id),
);
}
Self {
id,
name,
category,
image_path,
variants,
}
}
}
impl Item {
/// Get this items variant names
pub fn get_variants(&self) -> Vec<String> {
self.variants.keys().cloned().collect()
}
/// Get a `Variant` of an `Item`
pub fn variant(&self, variant: &str) -> Option<Variant> {
self.variants.get(variant).cloned()
}
/// Get all active `Transaction`s of this `Item` not yet consumed
pub async fn inventory(&self) -> Vec<Transaction> {
sqlx::query_as("SELECT * FROM transactions WHERE item = $1 AND consumed_timestamp IS NULL ORDER BY created DESC")
.bind(&self.id)
.fetch_all(get_pg!()).await.unwrap()
}
/// Get all active `Transaction`s of this `Item` not yet consumed of `origin`
pub async fn inventory_by_origin(&self, origin: &str) -> Vec<Transaction> {
sqlx::query_as("SELECT * FROM transactions WHERE item = $1 AND consumed_timestamp IS NULL AND origin = $2 ORDER BY created DESC")
.bind(&self.id)
.bind(origin)
.fetch_all(get_pg!()).await.unwrap()
}
/// Get all active `Transaction`s of this `Item` not yet consumed of `destination`
pub async fn consumed_by_destination(&self, destination: &str) -> Vec<Transaction> {
sqlx::query_as("SELECT * FROM transactions WHERE item = $1 AND consumed_timestamp IS NOT NULL AND destination = $2 ORDER BY created DESC")
.bind(&self.id)
.bind(destination)
.fetch_all(get_pg!()).await.unwrap()
}
pub fn api_json(&self) -> serde_json::Value {
let variants: HashMap<String, serde_json::Value> = self
.variants
.iter()
.map(|(key, value)| (key.clone(), value.api_json()))
.collect();
json!({
"uuid": self.id,
"image": if self.image_path.is_some() {
Some(format!("/item/{}/image", self.id))
} else {
None
},
"name": self.name,
"category": self.category,
"variants": variants
})
}
}

110
src/core/location.rs Normal file
View file

@ -0,0 +1,110 @@
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::{config::get_config, get_locations};
use based::request::api::ToAPI;
/// A Storage Location
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Location {
/// UUID
#[serde(default)]
pub id: String,
/// Name
pub name: String,
/// Parent
pub parent: Option<String>,
/// Storage Conditions
pub conditions: Option<StorageConditions>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StorageConditions {
/// Median temperature
pub temperature: f64,
/// Accurate temperature sensor reading
pub temperature_sensor: Option<String>,
}
impl StorageConditions {
/// Get a accurate temperature from a sensor endpoint
pub async fn accurate_temperature(&self) -> Option<f64> {
let conf = get_config();
let client = reqwest::Client::new();
let res: serde_json::Value = client
.get(self.temperature_sensor.clone()?)
.bearer_auth(conf.home_assistant?)
.send()
.await
.unwrap()
.json()
.await
.unwrap();
res.as_object()?.get("state")?.as_f64()
}
}
impl Location {
/// Recursively get the conditions of a location. This inherits from parent locations.
pub fn conditions_rec(&self) -> Option<StorageConditions> {
let locations = get_locations!();
if let Some(cond) = &self.conditions {
return Some(cond.clone());
}
if let Some(parent) = &self.parent {
if let Some(parent_loc) = locations.get(parent) {
if let Some(cond) = parent_loc.conditions_rec() {
return Some(cond);
}
}
}
None
}
// Get direct children
pub fn children_direct(&self) -> Vec<Location> {
let mut ret = Vec::new();
let locations = get_locations!();
for loc in locations.keys() {
let loc = locations.get(loc).unwrap();
if let Some(parent) = &loc.parent {
if *parent == self.id {
ret.push(loc.clone());
}
}
}
ret
}
// Get all children locations
pub fn children_recursive(&self) -> Vec<Location> {
let mut all = Vec::new();
let direct = self.children_direct();
all.extend_from_slice(&direct);
for loc in direct {
let sub = loc.children_recursive();
all.extend_from_slice(&sub);
}
all
}
}
impl ToAPI for Location {
async fn api(&self) -> serde_json::Value {
json!({
"id": self.id,
"name": self.name,
"parent": self.parent,
"conditions": self.conditions_rec()
})
}
}

5
src/core/mod.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod flow;
pub mod item;
pub mod location;
pub mod transaction;
pub mod variant;

212
src/core/transaction.rs Normal file
View file

@ -0,0 +1,212 @@
use based::request::api::ToAPI;
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::prelude::FromRow;
use crate::{get_itemdb, get_locations, get_pg, item::Item, variant::Variant};
// 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: Option<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()
}
/// Get a reference to the `Item` of the `Transaction`
pub async fn item(&self) -> Option<&Item> {
get_itemdb!().get_item(&self.item)
}
/// Get a reference to the `Variant` of the `Item` of the `Transaction`
pub async fn item_variant(&self) -> Option<Variant> {
get_itemdb!().get_item(&self.item)?.variant(&self.variant)
}
/// Get a `Transaction` via `id`
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,
})
}
}

361
src/core/variant.rs Normal file
View file

@ -0,0 +1,361 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::{get_pg, location::StorageConditions, 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)]
pub struct Variant {
/// Associated Item
pub item: String,
/// Variant ID
pub variant: String,
/// Variant Name
pub name: String,
/// Minimum amount
pub min: Option<i64>,
/// Days until expiry
pub expiry: Option<i64>,
/// Associated barcodes
pub barcodes: Option<Vec<i64>>,
/// Variant Need Conditions
pub needs: Option<VariantNeedCondition>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VariantNeedCondition {
/// Required temperature range (min-max)
pub temperature: [f64; 2],
}
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()
}),
needs: json.as_mapping().unwrap().get("needs").map(|x| {
let temp_range = x
.as_mapping()
.unwrap()
.get("temperature")
.unwrap()
.as_sequence()
.unwrap();
VariantNeedCondition {
temperature: [
temp_range.get(0).unwrap().as_f64().unwrap(),
temp_range.get(1).unwrap().as_f64().unwrap(),
],
}
}),
}
}
/// Get a API id for this Item Variant.
/// The ID has the format: `<ITEM>::<VARIANT>`
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<String> {
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<Transaction> {
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<String> {
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<Transaction> {
// 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<Transaction> {
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()
}
/// Get Transactions within `year` and `month`
pub async fn get_transaction_timeslice(&self, year: i32, month: u32) -> Vec<Transaction> {
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()
}
/// Get all unique origins
pub async fn get_unique_origins(&self) -> Vec<String> {
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()
}
/// Get all unique destinations
pub async fn get_unique_destinations(&self) -> Vec<String> {
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()
}
/// Get the last `limit` price values for `origin`
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() })
)
.bind(&self.item)
.bind(&self.variant)
.bind(origin)
.fetch_all(get_pg!()).await.unwrap();
res.into_iter().map(|x| x.0).collect()
}
/// Get the latest price for `origin`
pub async fn get_latest_price(&self, origin: Option<String>) -> 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)
}
/// Check if the `Variant` satisfies `StorageConditions`
pub async fn satisfy_condition(&self, conditions: &StorageConditions) -> bool {
if let Some(needs) = &self.needs {
if let Some(room_temp) = conditions.accurate_temperature().await {
if needs.temperature[0] < room_temp && room_temp < needs.temperature[1] {
return true;
} else {
return false;
}
} else {
log::warn!("Could not get temperature for location");
}
}
true
}
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.unwrap_or_default())
.sum();
if !full {
return json!({
"amount": active_transactions.len(),
"total_price": total_price
});
}
let all_transactions: Vec<Transaction> = 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::<Vec<_>>();
let prices = self
.price_history_by_origin(&origin, None)
.await
.into_iter()
.map(|x| x)
.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);
}
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,
"needs": self.needs
})
}
}