♻️ refactor + location conditions

This commit is contained in:
JMARyA 2025-04-16 04:05:29 +02:00
parent ce9cdc4256
commit 3076ebe6a0
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
19 changed files with 1228 additions and 520 deletions

1429
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,13 @@
[package] [package]
name = "cdb" name = "cdb"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
futures = "0.3.30" futures = "0.3.30"
log = "0.4.20" log = "0.4.20"
mdq = { git = "https://git.hydrar.de/mdtools/mdq" } mdq = { git = "https://git.hydrar.de/mdtools/mdq" }
based = { git = "https://git.hydrar.de/jmarya/based" }
rocket = { version = "0.5.1", features = ["json"] } rocket = { version = "0.5.1", features = ["json"] }
rocket_cors = "0.6.0" rocket_cors = "0.6.0"
serde = { version = "1.0.195", features = ["derive"] } serde = { version = "1.0.195", features = ["derive"] }

View file

@ -1,6 +1,8 @@
# Only these tokens are authorized to make requests # Only these tokens are authorized to make requests
allowed_tokens = ["password"] allowed_tokens = ["password"]
home_assistant = "TOKEN"
# Webhook Notifications # Webhook Notifications
[webhook] [webhook]
# Item Variants inventory goes below the minimum # Item Variants inventory goes below the minimum

View file

@ -75,3 +75,15 @@ variants:
name: "Regular Water" name: "Regular Water"
barcodes: [12345678] barcodes: [12345678]
``` ```
### Need Conditions
Some items prefer or need to be stored under some conditions. These conditions can be configured and warn you when locations dont match the required conditions.
```yml
name: "Water"
variants:
regular:
name: "Regular Water"
needs:
temperature: [5.0, 10.0]
```

View file

@ -50,7 +50,20 @@ Example: Freezer
{ {
"name": "Freezer", "name": "Freezer",
"conditions": { "conditions": {
"temperature": -10 "temperature": -10.0
}
}
```
#### Fetch temperature from Home Assistant
You can fetch the temperature from a sensor via Home Assistant.
You need to setup a API Token for Home Assistant. After that you can enter the sensors API route in the location definition:
```json
{
"name": "Freezer",
"conditions": {
"temperature_sensor": "https://homeassistant.local/api/states/sensor.freezer_temperature"
} }
} }
``` ```

View file

@ -6,6 +6,8 @@ pub struct Config {
pub allowed_tokens: Vec<String>, pub allowed_tokens: Vec<String>,
/// Webhook Config /// Webhook Config
pub webhook: Option<Webhook>, pub webhook: Option<Webhook>,
/// Home Assistant Token
pub home_assistant: Option<String>,
} }
pub fn get_config() -> Config { pub fn get_config() -> Config {

View file

@ -2,9 +2,10 @@ use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use std::collections::HashMap; use std::collections::HashMap;
use crate::routes::item::{item_does_not_exist_error, variant_does_not_exist_error, SupplyForm}; use crate::routes::ApiError;
use crate::routes::{ApiError, ToAPI}; use crate::routes::item::{SupplyForm, item_does_not_exist_error, variant_does_not_exist_error};
use crate::{get_itemdb, get_pg}; use crate::{get_itemdb, get_pg};
use based::request::api::ToAPI;
use sqlx::FromRow; use sqlx::FromRow;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View file

@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use crate::{get_locations, routes::ToAPI}; use crate::{config::get_config, get_locations};
use based::request::api::ToAPI;
/// A Storage Location /// A Storage Location
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -17,10 +18,31 @@ pub struct Location {
pub conditions: Option<StorageConditions>, pub conditions: Option<StorageConditions>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StorageConditions { pub struct StorageConditions {
/// Median temperature /// Median temperature
pub temperature: i64, 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 { impl Location {

View file

@ -167,6 +167,7 @@ async fn rocket() -> _ {
routes::flow::end_flow_route, routes::flow::end_flow_route,
routes::flow::continue_flow_route, routes::flow::continue_flow_route,
routes::flow::create_flow_route, routes::flow::create_flow_route,
routes::item::location_condition_warn,
routes::item::move_transaction_route, routes::item::move_transaction_route,
routes::item::variant_price_latest_by_origin, routes::item::variant_price_latest_by_origin,
routes::item::item_stat_route, routes::item::item_stat_route,

View file

@ -1,19 +1,19 @@
use rocket::{get, post, serde::json::Json, State};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::{collections::HashMap, str::FromStr};
use crate::{ use crate::{
check_auth, check_auth,
config::Config, config::Config,
flow::{Flow, FlowInfo, FlowNote}, flow::{Flow, FlowInfo, FlowNote},
get_pg, get_pg,
json_store::JSONStore, json_store::JSONStore,
routes::{api_error, vec_to_api, FallibleApiResponse, ToAPI, Token}, routes::{FallibleApiResponse, Token, api_error},
transaction::Transaction, transaction::Transaction,
}; };
use based::request::api::{ToAPI, vec_to_api};
use rocket::{State, get, post, serde::json::Json};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::{collections::HashMap, str::FromStr};
use super::{item::SupplyForm, ApiError}; use super::{ApiError, item::SupplyForm};
#[get("/flow/<id>/info")] #[get("/flow/<id>/info")]
pub async fn flow_info( pub async fn flow_info(

View file

@ -1,18 +1,18 @@
use std::str::FromStr; use std::str::FromStr;
use rocket::serde::json::Json;
use rocket::{get, post, State};
use serde::Deserialize;
use serde_json::json;
use crate::config::{Config, Webhook}; use crate::config::{Config, Webhook};
use crate::routes::{ToAPI, Token}; use crate::routes::Token;
use crate::variant::Variant; use crate::variant::Variant;
use crate::{check_auth, get_itemdb}; use crate::{check_auth, get_itemdb};
use crate::{ use crate::{
db::ItemDB, db::ItemDB,
routes::{api_error, FallibleApiResponse}, routes::{FallibleApiResponse, api_error},
}; };
use based::request::api::ToAPI;
use rocket::serde::json::Json;
use rocket::{State, get, post};
use serde::Deserialize;
use serde_json::json;
use super::{item_does_not_exist_error, variant_does_not_exist_error}; use super::{item_does_not_exist_error, variant_does_not_exist_error};
@ -23,8 +23,8 @@ pub struct DemandForm {
price: f64, price: f64,
} }
/// Consumes a Transaction with Price and Destination
#[post("/demand", data = "<f>")] #[post("/demand", data = "<f>")]
/// Consumes a Transaction with Price and Destination
pub async fn demand_route(f: Json<DemandForm>, t: Token, c: &State<Config>) -> FallibleApiResponse { pub async fn demand_route(f: Json<DemandForm>, t: Token, c: &State<Config>) -> FallibleApiResponse {
check_auth!(t, c); check_auth!(t, c);
@ -66,8 +66,8 @@ pub async fn demand_route(f: Json<DemandForm>, t: Token, c: &State<Config>) -> F
Ok(json!({"ok": 1})) Ok(json!({"ok": 1}))
} }
/// Returns all consumed transactions for Item Variant
#[get("/item/<item_id>/<variant_id>/demand?<destination>")] #[get("/item/<item_id>/<variant_id>/demand?<destination>")]
/// Returns all consumed transactions for Item Variant
pub async fn demand_log_route( pub async fn demand_log_route(
item_id: &str, item_id: &str,
variant_id: &str, variant_id: &str,

View file

@ -1,4 +1,4 @@
use crate::routes::{api_error, ApiError}; use crate::routes::{ApiError, api_error};
pub fn item_does_not_exist_error() -> ApiError { pub fn item_does_not_exist_error() -> ApiError {
api_error("The item does not exist") api_error("The item does not exist")

View file

@ -1,16 +1,16 @@
use std::collections::HashMap; use std::collections::HashMap;
use rocket::{get, State};
use serde_json::json;
use crate::{ use crate::{
check_auth, check_auth,
config::Config, config::Config,
json_store::JSONStore, json_store::JSONStore,
location::Location, location::Location,
routes::{api_error, vec_to_api, FallibleApiResponse, ToAPI, Token}, routes::{FallibleApiResponse, Token, api_error},
transaction::Transaction, transaction::Transaction,
}; };
use based::request::api::{ToAPI, vec_to_api};
use rocket::{State, get};
use serde_json::json;
#[get("/location/<id>")] #[get("/location/<id>")]
pub async fn location_info( pub async fn location_info(
@ -27,6 +27,42 @@ pub async fn location_info(
Ok(loc.api().await) Ok(loc.api().await)
} }
#[get("/location/<id>/warn")]
/// API route returning affected items on a condition mismatch
pub async fn location_condition_warn(
id: &str,
locations: &State<JSONStore<Location>>,
t: Token,
c: &State<Config>,
) -> FallibleApiResponse {
check_auth!(t, c);
let loc = locations
.get(id)
.ok_or_else(|| api_error("No location with that ID"))?;
let transactions = Transaction::in_location(&loc.id).await;
let mut ret = Vec::new();
for t in transactions {
if let Some(item) = t.item_variant().await {
if !item
.satisfy_condition(
loc.conditions
.as_ref()
.ok_or(api_error("Location has no conditions"))?,
)
.await
{
ret.push(t);
}
}
}
Ok(json!(vec_to_api(&ret).await))
}
/// Resolve locations children recursively /// Resolve locations children recursively
async fn location_api_resolve( async fn location_api_resolve(
id: &str, id: &str,

View file

@ -6,6 +6,7 @@ mod supply;
use std::str::FromStr; use std::str::FromStr;
use based::request::api::ToAPI;
pub use demand::*; pub use demand::*;
pub use error::*; pub use error::*;
pub use location::*; pub use location::*;
@ -17,25 +18,24 @@ use serde::Serialize;
pub use stat::*; pub use stat::*;
pub use supply::*; pub use supply::*;
use rocket::get;
use rocket::State; use rocket::State;
use rocket::get;
use serde_json::json; use serde_json::json;
use crate::check_auth; use crate::check_auth;
use crate::config::Config; use crate::config::Config;
use crate::db::get_items_without_min_satisfied;
use crate::db::ItemDB; use crate::db::ItemDB;
use crate::db::get_items_without_min_satisfied;
use crate::get_locations; use crate::get_locations;
use crate::get_pg; use crate::get_pg;
use crate::routes::ToAPI;
use crate::routes::Token; use crate::routes::Token;
use crate::transaction::Transaction; use crate::transaction::Transaction;
use super::api_error; use super::api_error;
use crate::routes::FallibleApiResponse; use crate::routes::FallibleApiResponse;
/// Returns a JSON response with all items in the database.
#[get("/items")] #[get("/items")]
/// Returns a JSON response with all items in the database.
pub fn get_items_route(itemdb: &State<ItemDB>, t: Token, c: &State<Config>) -> FallibleApiResponse { pub fn get_items_route(itemdb: &State<ItemDB>, t: Token, c: &State<Config>) -> FallibleApiResponse {
check_auth!(t, c); check_auth!(t, c);
@ -47,8 +47,8 @@ pub fn get_items_route(itemdb: &State<ItemDB>, t: Token, c: &State<Config>) -> F
Ok(json!({"items": items})) Ok(json!({"items": items}))
} }
/// Return an API Response for an `Item`
#[get("/item/<item_id>")] #[get("/item/<item_id>")]
/// Return an API Response for an `Item`
pub fn item_route( pub fn item_route(
item_id: &str, item_id: &str,
itemdb: &State<ItemDB>, itemdb: &State<ItemDB>,
@ -74,8 +74,8 @@ pub async fn item_image_route(item_id: &str, itemdb: &State<ItemDB>) -> Option<N
None None
} }
/// Returns all variants of an Item
#[get("/item/<item_id>/variants")] #[get("/item/<item_id>/variants")]
/// Returns all variants of an Item
pub fn item_variants_page( pub fn item_variants_page(
item_id: &str, item_id: &str,
itemdb: &State<ItemDB>, itemdb: &State<ItemDB>,
@ -112,8 +112,8 @@ pub async fn transaction_route(
Ok(t.api().await) Ok(t.api().await)
} }
/// Returns unique values for a field
#[get("/item/<item_id>/<variant_id>/unique?<field>")] #[get("/item/<item_id>/<variant_id>/unique?<field>")]
/// Returns unique values for a field
pub async fn unique_field_route( pub async fn unique_field_route(
item_id: &str, item_id: &str,
variant_id: &str, variant_id: &str,
@ -131,23 +131,28 @@ pub async fn unique_field_route(
.ok_or_else(variant_does_not_exist_error)?; .ok_or_else(variant_does_not_exist_error)?;
match field { match field {
"origin" => Ok(json!(variant "origin" => Ok(json!(
.get_unique_origins() variant
.await .get_unique_origins()
.into_iter() .await
.filter(|x| !x.starts_with("flow::")) .into_iter()
.collect::<Vec<_>>())), .filter(|x| !x.starts_with("flow::"))
"destination" => Ok(json!(variant .collect::<Vec<_>>()
.get_unique_destinations() )),
.await "destination" => Ok(json!(
.into_iter() variant
.filter(|x| !x.starts_with("flow::")) .get_unique_destinations()
.collect::<Vec<_>>())), .await
.into_iter()
.filter(|x| !x.starts_with("flow::"))
.collect::<Vec<_>>()
)),
_ => Err(api_error("Unknown field")), _ => Err(api_error("Unknown field")),
} }
} }
#[get("/items/expired?<days>")] #[get("/items/expired?<days>")]
/// Get all expired transactions
pub async fn expired_items_route( pub async fn expired_items_route(
days: Option<&str>, days: Option<&str>,
t: Token, t: Token,
@ -169,6 +174,7 @@ pub async fn expired_items_route(
} }
#[get("/items/min")] #[get("/items/min")]
/// Return all items without minimum satisfied
pub async fn min_items_route( pub async fn min_items_route(
itemdb: &State<ItemDB>, itemdb: &State<ItemDB>,
t: Token, t: Token,
@ -197,6 +203,7 @@ pub struct MoveTransaction {
} }
#[post("/transaction/<id>/move", data = "<form>")] #[post("/transaction/<id>/move", data = "<form>")]
/// Move a transaction to a new location
pub async fn move_transaction_route(id: &str, form: Json<MoveTransaction>) -> FallibleApiResponse { pub async fn move_transaction_route(id: &str, form: Json<MoveTransaction>) -> FallibleApiResponse {
let new_loc = &form.to; let new_loc = &form.to;
let locations = get_locations!(); let locations = get_locations!();

View file

@ -1,4 +1,4 @@
use rocket::{get, State}; use rocket::{State, get};
use serde_json::json; use serde_json::json;
use crate::check_auth; use crate::check_auth;
@ -6,7 +6,7 @@ use crate::config::Config;
use crate::routes::Token; use crate::routes::Token;
use crate::{ use crate::{
db::ItemDB, db::ItemDB,
routes::{api_error, FallibleApiResponse}, routes::{FallibleApiResponse, api_error},
}; };
use super::{item_does_not_exist_error, variant_does_not_exist_error}; use super::{item_does_not_exist_error, variant_does_not_exist_error};
@ -48,11 +48,13 @@ pub async fn variant_price_latest_by_origin(
.variant(variant_id) .variant(variant_id)
.ok_or_else(variant_does_not_exist_error)?; .ok_or_else(variant_does_not_exist_error)?;
Ok(json!(variant Ok(json!(
.price_history_by_origin(origin, Some(1)) variant
.await .price_history_by_origin(origin, Some(1))
.first() .await
.unwrap())) .first()
.unwrap()
))
} }
#[get("/items/stat")] #[get("/items/stat")]
@ -73,7 +75,7 @@ pub async fn item_stat_route(
let item_var = itemdb.get_item(&item).unwrap().variant(var).unwrap(); let item_var = itemdb.get_item(&item).unwrap().variant(var).unwrap();
for t in item_var.inventory().await { for t in item_var.inventory().await {
transaction_count += 1; transaction_count += 1;
total_price += t.price; total_price += t.price.unwrap_or_default();
} }
} }
} }

View file

@ -1,15 +1,15 @@
use rocket::serde::json::Json;
use rocket::{get, post, State};
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::check_auth; use crate::check_auth;
use crate::config::{Config, Webhook}; use crate::config::{Config, Webhook};
use crate::routes::{vec_to_api, ToAPI, Token}; use crate::routes::Token;
use crate::{ use crate::{
db::ItemDB, db::ItemDB,
routes::{api_error, FallibleApiResponse}, routes::{FallibleApiResponse, api_error},
}; };
use based::request::api::{ToAPI, vec_to_api};
use rocket::serde::json::Json;
use rocket::{State, get, post};
use serde::{Deserialize, Serialize};
use serde_json::json;
use super::{item_does_not_exist_error, variant_does_not_exist_error}; use super::{item_does_not_exist_error, variant_does_not_exist_error};
@ -23,8 +23,8 @@ pub struct SupplyForm {
pub note: Option<String>, pub note: Option<String>,
} }
/// Route for supply action. Creates a new Transaction for the specified Item Variant.
#[post("/supply", data = "<form>")] #[post("/supply", data = "<form>")]
/// Route for supply action. Creates a new Transaction for the specified Item Variant.
pub async fn supply_route( pub async fn supply_route(
form: Json<SupplyForm>, form: Json<SupplyForm>,
itemdb: &State<ItemDB>, itemdb: &State<ItemDB>,
@ -57,8 +57,8 @@ pub async fn supply_route(
Ok(json!({"uuid": transaction.id})) Ok(json!({"uuid": transaction.id}))
} }
/// Returns a list of Transaction UUIDs for the Item Variant
#[get("/item/<item_id>/<variant_id>/supply")] #[get("/item/<item_id>/<variant_id>/supply")]
/// Returns a list of Transaction UUIDs for the Item Variant
pub async fn supply_log_route( pub async fn supply_log_route(
item_id: &str, item_id: &str,
variant_id: &str, variant_id: &str,
@ -79,8 +79,8 @@ pub async fn supply_log_route(
Ok(json!(transactions)) Ok(json!(transactions))
} }
/// Returns current active Transactions for Item
#[get("/item/<item_id>/inventory?<origin>")] #[get("/item/<item_id>/inventory?<origin>")]
/// Returns current active Transactions for Item
pub async fn inventory_route( pub async fn inventory_route(
item_id: &str, item_id: &str,
itemdb: &State<ItemDB>, itemdb: &State<ItemDB>,
@ -103,8 +103,8 @@ pub async fn inventory_route(
Ok(json!(vec_to_api(&transactions).await)) Ok(json!(vec_to_api(&transactions).await))
} }
/// Returns current active Transactions for Item Variant
#[get("/item/<item_id>/<variant_id>/inventory")] #[get("/item/<item_id>/<variant_id>/inventory")]
/// Returns current active Transactions for Item Variant
pub async fn inventory_route_variant( pub async fn inventory_route_variant(
item_id: &str, item_id: &str,
variant_id: &str, variant_id: &str,
@ -125,8 +125,8 @@ pub async fn inventory_route_variant(
Ok(json!(vec_to_api(&transactions).await)) Ok(json!(vec_to_api(&transactions).await))
} }
/// Returns statistics for the Item Variant
#[get("/item/<item_id>/<variant_id>/stat?<full>")] #[get("/item/<item_id>/<variant_id>/stat?<full>")]
/// Returns statistics for the Item Variant
pub async fn variant_stat_route( pub async fn variant_stat_route(
item_id: &str, item_id: &str,
variant_id: &str, variant_id: &str,

View file

@ -1,5 +1,5 @@
use rocket::{ use rocket::{
http::Status, outcome::Outcome, request::FromRequest, response::status::BadRequest, Request, Request, http::Status, outcome::Outcome, request::FromRequest, response::status::BadRequest,
}; };
use serde_json::json; use serde_json::json;
@ -37,20 +37,3 @@ impl<'r> FromRequest<'r> for Token {
} }
} }
} }
/// A trait to generate a Model API representation in JSON format.
pub trait ToAPI: Sized {
/// Generate public API JSON
fn api(&self) -> impl std::future::Future<Output = serde_json::Value>;
}
/// Converts a slice of items implementing the `ToAPI` trait into a `Vec` of JSON values.
pub async fn vec_to_api(items: &[impl ToAPI]) -> Vec<serde_json::Value> {
let mut ret = Vec::with_capacity(items.len());
for e in items {
ret.push(e.api().await);
}
ret
}

View file

@ -1,9 +1,10 @@
use based::request::api::ToAPI;
use futures::StreamExt; use futures::StreamExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use sqlx::prelude::FromRow; use sqlx::prelude::FromRow;
use crate::{get_itemdb, get_locations, get_pg, routes::ToAPI}; use crate::{get_itemdb, get_locations, get_pg, item::Item, variant::Variant};
// todo : produced / consumed by flow field? // todo : produced / consumed by flow field?
@ -17,7 +18,7 @@ pub struct Transaction {
/// Associated Variant /// Associated Variant
pub variant: String, pub variant: String,
/// Price of obtaining the Item /// Price of obtaining the Item
pub price: f64, pub price: Option<f64>,
/// Origin of the Item /// Origin of the Item
pub origin: Option<String>, pub origin: Option<String>,
/// The location of the Item /// The location of the Item
@ -53,6 +54,14 @@ impl Transaction {
.fetch_one(get_pg!()).await.unwrap() .fetch_one(get_pg!()).await.unwrap()
} }
pub async fn item(&self) -> Option<&Item> {
get_itemdb!().get_item(&self.item)
}
pub async fn item_variant(&self) -> Option<Variant> {
get_itemdb!().get_item(&self.item)?.variant(&self.variant)
}
pub async fn get(id: &uuid::Uuid) -> Option<Self> { pub async fn get(id: &uuid::Uuid) -> Option<Self> {
sqlx::query_as("SELECT * FROM transactions WHERE id = $1") sqlx::query_as("SELECT * FROM transactions WHERE id = $1")
.bind(id) .bind(id)
@ -157,11 +166,7 @@ impl Transaction {
item.is_expired().await item.is_expired().await
}; };
if expired { if expired { Some(item) } else { None }
Some(item)
} else {
None
}
}) })
.collect() .collect()
.await; .await;

View file

@ -3,7 +3,7 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use crate::{get_pg, transaction::Transaction}; use crate::{get_pg, location::StorageConditions, transaction::Transaction};
pub fn timestamp_range(year: i32, month: u32) -> (i64, i64) { pub fn timestamp_range(year: i32, month: u32) -> (i64, i64) {
let d = chrono::NaiveDate::from_ymd_opt(year, month, 0).unwrap(); let d = chrono::NaiveDate::from_ymd_opt(year, month, 0).unwrap();
@ -31,7 +31,7 @@ pub fn timestamp_range(year: i32, month: u32) -> (i64, i64) {
/// in the real world. It may include attributes or properties that deviate from /// 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 /// the standard definition of the item. For example, different colors, sizes, or
/// configurations. /// configurations.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Variant { pub struct Variant {
/// Associated Item /// Associated Item
pub item: String, pub item: String,
@ -45,6 +45,14 @@ pub struct Variant {
pub expiry: Option<i64>, pub expiry: Option<i64>,
/// Associated barcodes /// Associated barcodes
pub barcodes: Option<Vec<i64>>, 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 { impl Variant {
@ -78,6 +86,21 @@ impl Variant {
.map(|x| x.as_i64().unwrap()) .map(|x| x.as_i64().unwrap())
.collect() .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(),
],
}
}),
} }
} }
@ -234,10 +257,29 @@ impl Variant {
(false, 0) (false, 0)
} }
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 { pub async fn stat(&self, full: bool) -> serde_json::Value {
let active_transactions = self.inventory().await; let active_transactions = self.inventory().await;
let total_price: f64 = active_transactions.iter().map(|x| x.price).sum(); let total_price: f64 = active_transactions
.iter()
.map(|x| x.price.unwrap_or_default())
.sum();
if !full { if !full {
return json!({ return json!({