377 lines
11 KiB
Rust
377 lines
11 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use futures::StreamExt;
|
|
use mongod::{Model, Sort};
|
|
use mongodb::bson::doc;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::json;
|
|
|
|
use crate::transaction::{Price, Transaction};
|
|
|
|
pub fn sort_by_timestamp() -> mongodb::bson::Document {
|
|
doc! { "timestamp": mongod::Sort::Descending }
|
|
}
|
|
|
|
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, Eq)]
|
|
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>,
|
|
}
|
|
|
|
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()),
|
|
}
|
|
}
|
|
|
|
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 filter = doc! {
|
|
"item": &self.item,
|
|
"variant": &self.variant
|
|
};
|
|
|
|
let result = Transaction::find_partial(filter, json!({}), None, None)
|
|
.await
|
|
.unwrap();
|
|
|
|
let mut ret = Vec::new();
|
|
|
|
for doc in result {
|
|
ret.push(doc._id);
|
|
}
|
|
|
|
ret
|
|
}
|
|
|
|
/// Returns the active Transaction of this Item Variant which are not yet consumed.
|
|
pub async fn inventory(&self) -> Vec<Transaction> {
|
|
let filter = doc! {
|
|
"item": &self.item,
|
|
"variant": &self.variant,
|
|
"consumed": { "$not": { "$type": "object" } }
|
|
};
|
|
|
|
Transaction::find(filter, None, None).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 filter = if let Some(dest) = destination {
|
|
doc! {
|
|
"item": &self.item,
|
|
"variant": &self.variant,
|
|
"consumed": { "destination": dest }
|
|
}
|
|
} else {
|
|
doc! {
|
|
"item": &self.item,
|
|
"variant": &self.variant,
|
|
"consumed": { "$type": "object" }
|
|
}
|
|
};
|
|
|
|
let result = Transaction::find_partial(filter, json!({}), None, None)
|
|
.await
|
|
.unwrap();
|
|
|
|
let mut ret = Vec::new();
|
|
|
|
for doc in result {
|
|
ret.push(doc._id);
|
|
}
|
|
|
|
ret
|
|
}
|
|
|
|
pub async fn demand(uuid: &str, price: Price, destination: &str) -> Option<Transaction> {
|
|
// check if transaction exists
|
|
let mut t = Transaction::get(uuid).await?;
|
|
t = t.consume(price, destination).await;
|
|
Some(t)
|
|
}
|
|
|
|
/// 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: Price,
|
|
origin: Option<&str>,
|
|
location: Option<&str>,
|
|
note: Option<&str>,
|
|
) -> Transaction {
|
|
let t = Transaction::new(&self.item, &self.variant, price, origin, location, note).await;
|
|
|
|
t.insert().await.unwrap();
|
|
|
|
t
|
|
}
|
|
|
|
/// Returns all Transactions of this Item Variant
|
|
pub async fn get_all_transactions(&self) -> Vec<Transaction> {
|
|
let filter = doc! {
|
|
"item": &self.item,
|
|
"variant": &self.variant
|
|
};
|
|
|
|
Transaction::find(filter, None, Some(doc! { "timestamp": Sort::Descending }))
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
pub async fn get_transaction_timeslice(&self, year: i32, month: u32) -> Vec<Transaction> {
|
|
let (start, end) = timestamp_range(year, month);
|
|
|
|
Transaction::find(
|
|
doc! {
|
|
"timestamp": {
|
|
"$gte": start,
|
|
"$lte": end
|
|
}
|
|
},
|
|
None,
|
|
Some(sort_by_timestamp()),
|
|
)
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
pub async fn get_unique_origins(&self) -> Vec<String> {
|
|
unique_flows(
|
|
&Transaction::unique(
|
|
doc! {
|
|
"item": &self.item,
|
|
"variant": &self.variant
|
|
},
|
|
"origin",
|
|
)
|
|
.await,
|
|
)
|
|
}
|
|
|
|
pub async fn get_unique_destinations(&self) -> Vec<String> {
|
|
unique_flows(
|
|
&Transaction::unique(
|
|
doc! {
|
|
"item": &self.item,
|
|
"variant": &self.variant
|
|
},
|
|
"consumed.destination",
|
|
)
|
|
.await,
|
|
)
|
|
}
|
|
|
|
pub async fn price_history_by_origin(&self, origin: &str, limit: Option<i64>) -> Vec<Price> {
|
|
Transaction::find(
|
|
doc! {
|
|
"item": &self.item,
|
|
"variant": &self.variant,
|
|
"origin": origin
|
|
},
|
|
limit,
|
|
Some(sort_by_timestamp()),
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.into_iter()
|
|
.map(|x| x.price)
|
|
.collect()
|
|
}
|
|
|
|
pub async fn get_latest_price(&self, origin: Option<String>) -> Price {
|
|
let mut filter = doc! {
|
|
"item": &self.item,
|
|
"variant": &self.variant
|
|
};
|
|
|
|
if let Some(origin) = origin {
|
|
filter.insert("origin", origin);
|
|
}
|
|
|
|
Transaction::find(filter, Some(1), Some(sort_by_timestamp()))
|
|
.await
|
|
.unwrap()
|
|
.first()
|
|
.unwrap()
|
|
.price
|
|
.clone()
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
|
|
pub async fn stat(&self, full: bool) -> serde_json::Value {
|
|
let active_transactions = self.inventory().await;
|
|
|
|
// fix : ignores currency
|
|
let total_price: f64 = active_transactions.iter().map(|x| x.price.value).sum();
|
|
|
|
if !full {
|
|
return json!({
|
|
"amount": active_transactions.len(),
|
|
"total_price": total_price
|
|
});
|
|
}
|
|
|
|
let all_transactions = Transaction::find(
|
|
doc! { "item": &self.item, "variant": &self.variant},
|
|
None,
|
|
None,
|
|
)
|
|
.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.value)
|
|
.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
|
|
})
|
|
}
|
|
}
|
|
|
|
pub fn unique_flows(i: &[String]) -> Vec<String> {
|
|
let mut unique_vec: Vec<String> = Vec::new();
|
|
|
|
for s in i {
|
|
// Check if the string starts with "flow::"
|
|
if let Some(suffix) = s.strip_prefix("flow::") {
|
|
// Extract the part after "flow::" and split on "::" to get the kind (ignoring id)
|
|
let parts: Vec<&str> = suffix.split("::").collect();
|
|
if let Some(kind) = parts.first() {
|
|
// Build the common prefix "flow::kind"
|
|
let common_prefix = format!("flow::{}", kind);
|
|
|
|
if !unique_vec.contains(&common_prefix) {
|
|
unique_vec.push(common_prefix);
|
|
}
|
|
}
|
|
} else {
|
|
// If the string doesn't start with "flow::", retain it
|
|
// Assumption: Except "flow::" values, everything should be already unique
|
|
unique_vec.push(s.to_string());
|
|
}
|
|
}
|
|
|
|
unique_vec
|
|
}
|