commit 1b04b3fd172fdf97207c24c8fe11ca87d85841d8
parent bf1e1279d2a44a57bee37053e339602b6ef5f67f
Author: triesap <137732411+triesap@users.noreply.github.com>
Date: Sat, 26 Apr 2025 20:44:26 +0000
Adds OrderClassified model, updates EventClassified adding price discount tags and calculate order method implementation.
Diffstat:
10 files changed, 452 insertions(+), 118 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -3,6 +3,7 @@ justfile
notes*.txt
.tmp*
.vscode
+git-diff.txt
.env
.env.*
diff --git a/src/events/job_request.rs b/src/events/job_request.rs
@@ -1,9 +1,12 @@
+use std::time::Duration;
+
use anyhow::Result;
use nostr::event::{Event, EventId, Tag, TagKind};
use nostr::filter::{Alphabet, SingleLetterTag};
use nostr::{event::Kind, key::Keys};
use nostr_sdk::Client;
use nostr_sdk::RelayPoolNotification;
+use tokio::time::sleep;
use tracing::{info, warn};
use crate::KIND_JOB_REQUEST;
@@ -11,16 +14,16 @@ use crate::handlers::job_request_order::{JobRequestOrderError, handle_job_reques
use crate::handlers::job_request_preview::handle_job_request_preview;
use crate::handlers::job_request_quote::handle_job_request_quote;
use crate::utils::nostr::{
- NostrTagsResolveError, nostr_event_job_feedback, nostr_filter_kind, nostr_filter_new_events,
- nostr_tag_at_value, nostr_tag_first_value, nostr_tag_relays_parse, nostr_tag_slice,
- nostr_tags_resolve,
+ NostrTagsResolveError, NostrUtilsError, nostr_event_job_feedback, nostr_filter_kind,
+ nostr_filter_new_events, nostr_tag_at_value, nostr_tag_first_value, nostr_tag_relays_parse,
+ nostr_tag_slice, nostr_tags_resolve,
};
use crate::utils::unit::MassUnitError;
#[derive(thiserror::Error, Debug)]
pub enum JobRequestError {
- #[error("Order: {0}")]
- JobRequestOrder(#[from] JobRequestOrderError),
+ #[error("{0}")]
+ NostrUtilsError(#[from] NostrUtilsError),
#[error("{0}")]
MassUnit(#[from] MassUnitError),
@@ -28,6 +31,9 @@ pub enum JobRequestError {
#[error("{0}")]
NostrTagsResolve(#[from] NostrTagsResolveError),
+ #[error("Order: {0}")]
+ JobRequestOrder(#[from] JobRequestOrderError),
+
#[error("Invalid job request input type: {0}")]
InvalidInputType(String),
@@ -303,6 +309,10 @@ async fn process_job_request<F, Fut>(
F: FnOnce(Event, Keys, Client, JobRequest, JobRequestInput) -> Fut,
Fut: std::future::Future<Output = Result<(), JobRequestError>>,
{
+ if cfg!(debug_assertions) {
+ sleep(Duration::from_millis(500)).await;
+ }
+
let error_event = event.clone();
let error_job_req = job_req.clone();
let error_keys = keys.clone();
diff --git a/src/handlers/job_request_order.rs b/src/handlers/job_request_order.rs
@@ -1,6 +1,9 @@
use anyhow::Result;
-use nostr::{event::Event, key::Keys};
-use nostr_sdk::Client;
+use nostr::{
+ event::{Event, Tag, TagKind},
+ key::Keys,
+};
+use nostr_sdk::{Client, client::Error as NostrClientError};
use serde::Deserialize;
use thiserror::Error;
use tracing::info;
@@ -8,28 +11,28 @@ use tracing::info;
use crate::{
events::job_request::{JobRequest, JobRequestError, JobRequestInput},
models::event_classified::EventClassified,
- utils::nostr::{NostrEventError, nostr_event_job_result, nostr_fetch_event_by_id},
+ utils::nostr::{nostr_event_job_result, nostr_fetch_event_by_id, nostr_send_event},
};
#[derive(Debug, Error)]
pub enum JobRequestOrderError {
- #[error("Failure to parse the reference event {0}")]
- ReferenceEventParse(String),
+ #[error("Failed to parse reference event: {0}")]
+ ParseReference(String),
- #[error("Failure to fetch the reference event {0}")]
- ReferenceEventFetch(String),
+ #[error("Failed to fetch reference event: {0}")]
+ FetchReference(String),
- #[error("Reference event not found {0}")]
- ReferenceEventMissing(String),
+ #[error("Reference event not found: {0}")]
+ MissingReference(String),
- #[error("Reference event does not satisfy requested {0}")]
- ReferenceEventMissingRequested(String),
+ #[error("Reference event does not meet request requirements: {0}")]
+ MissingRequested(String),
- #[error("Failure building the job response")]
- ResponseEventBuildFailure(#[from] NostrEventError),
+ #[error("Failed to send job response")]
+ ResponseSend(#[from] NostrClientError),
- #[error("Failure sending the job response")]
- ResponseEventSendFailure(#[from] nostr_sdk::client::Error),
+ #[error("Request cannot be satisfied: {0}")]
+ Unsatisfiable(String),
}
#[derive(Debug, Deserialize)]
@@ -73,51 +76,31 @@ pub async fn handle_job_request_order(
_job_req: JobRequest,
job_req_input: JobRequestInput,
) -> Result<(), JobRequestError> {
- let order_data: JobRequestOrderData = serde_json::from_str(&job_req_input.data)?;
-
- info!("handle_job_request_order order_data: {:?}", order_data);
-
- let fetched_ref_event: Option<Event> =
- nostr_fetch_event_by_id(client.clone(), &order_data.event.id.clone())
- .await
- .map_err(|_| JobRequestOrderError::ReferenceEventFetch(order_data.event.id.clone()))?;
- let ref_event: &Event =
- fetched_ref_event
- .as_ref()
- .ok_or(JobRequestOrderError::ReferenceEventMissing(
- order_data.event.id.clone(),
- ))?;
-
- let ref_classified = EventClassified::from_event(ref_event)
- .map_err(|_| JobRequestOrderError::ReferenceEventParse(order_data.event.id.clone()))?;
-
- info!(
- "handle_job_request_order ref_classified: {:?}",
- ref_classified
- );
-
- if ref_classified.prices.is_empty() {
- return Err(JobRequestError::JobRequestOrder(
- JobRequestOrderError::ReferenceEventMissingRequested("price".to_string()),
- ));
- }
-
- if ref_classified.quantities.is_empty() {
- return Err(JobRequestError::JobRequestOrder(
- JobRequestOrderError::ReferenceEventMissingRequested("quantity".to_string()),
- ));
- }
-
- let payload = "your order was received!";
- let event_result = nostr_event_job_result(&event_job_request.clone(), payload, 0, None, None)
- .map_err(JobRequestOrderError::from)?;
- let event_id = client
- .send_event_builder(event_result)
+ let order_data: JobRequestOrderData = serde_json::from_str(&job_req_input.data)
+ .map_err(|e| JobRequestOrderError::ParseReference(e.to_string()))?;
+
+ let ref_id = &order_data.event.id;
+ let ref_event = nostr_fetch_event_by_id(client.clone(), ref_id)
.await
- .map_err(JobRequestOrderError::from)?;
+ .map_err(|_| JobRequestOrderError::FetchReference(ref_id.clone()))?;
+
+ let ref_classified = EventClassified::from_event(&ref_event)
+ .map_err(|_| JobRequestOrderError::ParseReference(ref_id.clone()))?;
+
+ let order_result = ref_classified.calculate_order(&order_data.order)?;
+
+ let payload = serde_json::to_string(&order_result)?;
+ let tags = vec![Tag::custom(
+ TagKind::custom("e_ref"),
+ [ref_event.id.to_hex()],
+ )];
+
+ let job_result_event =
+ nostr_event_job_result(&event_job_request, payload, 0, None, Some(tags))?;
+
+ let job_result_event_id = nostr_send_event(client, job_result_event).await?;
+
+ info!("job request order result sent: {:?}", job_result_event_id);
- info!("handle_job_request_order sent result {:?}", {
- event_id.clone()
- });
Ok(())
}
diff --git a/src/models/event_classified.rs b/src/models/event_classified.rs
@@ -2,12 +2,20 @@ use anyhow::Result;
use nostr::{EventId, event::Event};
use serde::{Deserialize, Serialize};
-use crate::utils::{
- nostr::{
- nostr_tag_match_geohash, nostr_tag_match_l, nostr_tag_match_location,
- nostr_tag_match_summary, nostr_tag_match_title, nostr_tags_match,
+use crate::{
+ handlers::job_request_order::{JobRequestOrderDataOrder, JobRequestOrderError},
+ utils::{
+ nostr::{
+ nostr_tag_match_geohash, nostr_tag_match_l, nostr_tag_match_location,
+ nostr_tag_match_summary, nostr_tag_match_title, nostr_tags_match,
+ },
+ unit::{MassUnit, convert_mass},
},
- unit::MassUnit,
+};
+
+use super::order_classified::{
+ OrderClassifiedDiscount, OrderClassifiedPrice, OrderClassifiedQuantity, OrderClassifiedResult,
+ OrderClassifiedTotal,
};
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
@@ -25,6 +33,33 @@ pub struct EventClassifiedLocation {
}
#[derive(Debug, Serialize, Deserialize, Clone)]
+#[serde(tag = "type")]
+pub enum EventClassifiedDiscount {
+ #[serde(rename = "subtotal")]
+ Subtotal {
+ threshold: f64,
+ currency: String,
+ value: f64,
+ is_percent: bool,
+ },
+ #[serde(rename = "mass")]
+ Mass {
+ discount_unit: String,
+ threshold: f64,
+ threshold_unit: String,
+ discount_per_unit: f64,
+ currency: String,
+ },
+ #[serde(rename = "quantity")]
+ Quantity {
+ product_key: String,
+ min_count: u32,
+ discount_per_unit: f64,
+ currency: String,
+ },
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EventClassifiedQuantity {
pub amount: f64,
pub unit: MassUnit,
@@ -62,6 +97,7 @@ pub struct EventClassified {
pub listing: EventClassifiedListing,
pub prices: Vec<EventClassifiedPrice>,
pub quantities: Vec<EventClassifiedQuantity>,
+ pub discounts: Vec<EventClassifiedDiscount>,
pub location: Option<EventClassifiedLocation>,
pub geolocation: Option<EventClassifiedGeolocation>,
}
@@ -79,6 +115,7 @@ impl EventClassified {
let mut lat: Option<f64> = None;
let mut lng: Option<f64> = None;
let mut geohash: Option<String> = None;
+ let mut discounts: Vec<EventClassifiedDiscount> = Vec::new();
for tag in event.tags.iter() {
if let Some((key, values)) = nostr_tags_match(tag) {
@@ -123,6 +160,44 @@ impl EventClassified {
"lot" if !values.is_empty() => listing.lot = Some(values[0].clone()),
"profile" if !values.is_empty() => listing.profile = Some(values[0].clone()),
"year" if !values.is_empty() => listing.year = Some(values[0].clone()),
+ "price-discount-subtotal" if values.len() >= 4 => {
+ let threshold = values[0].parse().unwrap_or(0.0);
+ let currency = values[1].clone();
+ let value = values[2].parse().unwrap_or(0.0);
+ let is_percent = values[3] == "%";
+ discounts.push(EventClassifiedDiscount::Subtotal {
+ threshold,
+ currency,
+ value,
+ is_percent,
+ });
+ }
+ "price-discount-mass" if values.len() >= 5 => {
+ let discount_unit = values[0].clone();
+ let threshold = values[1].parse().unwrap_or(0.0);
+ let threshold_unit = values[2].clone();
+ let discount_per_unit = values[3].parse().unwrap_or(0.0);
+ let currency = values[4].clone();
+ discounts.push(EventClassifiedDiscount::Mass {
+ discount_unit,
+ threshold,
+ threshold_unit,
+ discount_per_unit,
+ currency,
+ });
+ }
+ "price-discount-quantity" if values.len() >= 4 => {
+ let product_key = values[0].clone();
+ let min_count = values[1].parse().unwrap_or(0);
+ let discount_per_unit = values[2].parse().unwrap_or(0.0);
+ let currency = values[3].clone();
+ discounts.push(EventClassifiedDiscount::Quantity {
+ product_key,
+ min_count,
+ discount_per_unit,
+ currency,
+ });
+ }
_ => {}
}
}
@@ -197,8 +272,187 @@ impl EventClassified {
listing,
prices,
quantities,
+ discounts,
location,
geolocation,
})
}
+
+ pub fn calculate_order(
+ &self,
+ order: &JobRequestOrderDataOrder,
+ ) -> Result<OrderClassifiedResult, JobRequestOrderError> {
+ let quantity = &order.quantity;
+ let price = &order.price;
+
+ let qty_unit = quantity
+ .unit
+ .parse::<MassUnit>()
+ .map_err(|_| JobRequestOrderError::Unsatisfiable("invalid quantity unit".into()))?;
+ let price_unit = price.quantity_unit.parse::<MassUnit>().map_err(|_| {
+ JobRequestOrderError::Unsatisfiable("invalid price quantity unit".into())
+ })?;
+
+ let total_qty = quantity.amount * quantity.count as f64;
+
+ let matched_packaging = self
+ .quantities
+ .iter()
+ .any(|q| q.unit == qty_unit && (q.amount - quantity.amount).abs() < f64::EPSILON);
+
+ if !matched_packaging {
+ return Err(JobRequestOrderError::Unsatisfiable(format!(
+ "requested packaging {} {} not available",
+ quantity.amount, quantity.unit
+ )));
+ }
+
+ let matched_tier = self.prices.iter().find(|p| {
+ p.quantity_unit == price_unit
+ && (p.quantity_amount - price.quantity_amount).abs() < f64::EPSILON
+ && p.currency.to_lowercase() == price.currency.to_lowercase()
+ });
+
+ let tier = matched_tier.ok_or_else(|| {
+ JobRequestOrderError::Unsatisfiable(format!(
+ "no matching price tier {} {} found",
+ price.quantity_amount, price.quantity_unit
+ ))
+ })?;
+
+ if (tier.amount - price.amount).abs() > f64::EPSILON {
+ return Err(JobRequestOrderError::Unsatisfiable(format!(
+ "price mismatch: expected {}, got {}",
+ tier.amount, price.amount
+ )));
+ }
+
+ let converted_qty = convert_mass(total_qty, &qty_unit, &price_unit);
+ let unit_price = tier.amount / tier.quantity_amount;
+ let subtotal = (unit_price * converted_qty * 100.0).round() / 100.0;
+
+ let mut discounts: Vec<OrderClassifiedDiscount> = Vec::new();
+ let package_key = format!(
+ "{}-{}-{}",
+ quantity.amount,
+ quantity.unit.to_lowercase(),
+ quantity.label
+ );
+
+ for d in &self.discounts {
+ match d {
+ EventClassifiedDiscount::Subtotal {
+ threshold,
+ currency,
+ value,
+ is_percent,
+ } => {
+ if subtotal < *threshold {
+ continue;
+ }
+ let amt = if *is_percent {
+ (subtotal * value / 100.0 * 100.0).round() / 100.0
+ } else {
+ (*value * 100.0).round() / 100.0
+ };
+ discounts.push(OrderClassifiedDiscount {
+ discount_type: "subtotal".into(),
+ threshold: Some(*threshold),
+ threshold_unit: None,
+ discount_per_unit: None,
+ discount_unit: None,
+ discount_percent: if *is_percent { Some(*value) } else { None },
+ discount_amount: amt,
+ currency: currency.clone(),
+ });
+ }
+ EventClassifiedDiscount::Mass {
+ discount_unit,
+ threshold,
+ threshold_unit,
+ discount_per_unit,
+ currency,
+ } => {
+ let th_unit = threshold_unit.parse::<MassUnit>().map_err(|_| {
+ JobRequestOrderError::Unsatisfiable("invalid threshold unit".into())
+ })?;
+ let dis_unit = discount_unit.parse::<MassUnit>().map_err(|_| {
+ JobRequestOrderError::Unsatisfiable("invalid discount unit".into())
+ })?;
+
+ let qty_in_th = convert_mass(total_qty, &qty_unit, &th_unit);
+ if qty_in_th < *threshold {
+ continue;
+ }
+
+ let qty_in_dis = convert_mass(total_qty, &qty_unit, &dis_unit);
+ let amt = (qty_in_dis * discount_per_unit * 100.0).round() / 100.0;
+
+ discounts.push(OrderClassifiedDiscount {
+ discount_type: "mass".into(),
+ threshold: Some(*threshold),
+ threshold_unit: Some(threshold_unit.clone()),
+ discount_per_unit: Some(*discount_per_unit),
+ discount_unit: Some(discount_unit.clone()),
+ discount_percent: None,
+ discount_amount: amt,
+ currency: currency.clone(),
+ });
+ }
+ EventClassifiedDiscount::Quantity {
+ product_key,
+ min_count,
+ discount_per_unit,
+ currency,
+ } => {
+ if product_key != &package_key || quantity.count < *min_count {
+ continue;
+ }
+
+ let amt = (*discount_per_unit * quantity.count as f64 * 100.0).round() / 100.0;
+
+ discounts.push(OrderClassifiedDiscount {
+ discount_type: "quantity".into(),
+ threshold: Some(*min_count as f64),
+ threshold_unit: None,
+ discount_per_unit: Some(*discount_per_unit),
+ discount_unit: None,
+ discount_percent: None,
+ discount_amount: amt,
+ currency: currency.clone(),
+ });
+ }
+ }
+ }
+
+ let total_discount: f64 = discounts.iter().map(|d| d.discount_amount).sum();
+ let total = ((subtotal - total_discount) * 100.0).round() / 100.0;
+
+ Ok(OrderClassifiedResult {
+ quantity: OrderClassifiedQuantity {
+ amount: quantity.amount,
+ unit: quantity.unit.clone(),
+ label: quantity.label.clone(),
+ },
+ price: OrderClassifiedPrice {
+ amount: tier.amount,
+ currency: tier.currency.clone(),
+ quantity_amount: tier.quantity_amount,
+ quantity_unit: price.quantity_unit.clone(),
+ },
+ discounts,
+ subtotal: OrderClassifiedTotal {
+ price_amount: subtotal,
+ price_currency: tier.currency.clone(),
+ quantity_amount: total_qty,
+ quantity_unit: quantity.unit.clone(),
+ },
+ total: OrderClassifiedTotal {
+ price_amount: total,
+ price_currency: tier.currency.clone(),
+ quantity_amount: total_qty,
+ quantity_unit: quantity.unit.clone(),
+ },
+ })
+ }
}
diff --git a/src/models/mod.rs b/src/models/mod.rs
@@ -1 +1,2 @@
pub mod event_classified;
+pub mod order_classified;
diff --git a/src/models/order_classified.rs b/src/models/order_classified.rs
@@ -0,0 +1,45 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct OrderClassifiedResult {
+ pub quantity: OrderClassifiedQuantity,
+ pub price: OrderClassifiedPrice,
+ pub discounts: Vec<OrderClassifiedDiscount>,
+ pub subtotal: OrderClassifiedTotal,
+ pub total: OrderClassifiedTotal,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct OrderClassifiedQuantity {
+ pub amount: f64,
+ pub unit: String,
+ pub label: String,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct OrderClassifiedPrice {
+ pub amount: f64,
+ pub currency: String,
+ pub quantity_amount: f64,
+ pub quantity_unit: String,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct OrderClassifiedDiscount {
+ pub discount_type: String,
+ pub threshold: Option<f64>,
+ pub threshold_unit: Option<String>,
+ pub discount_per_unit: Option<f64>,
+ pub discount_unit: Option<String>,
+ pub discount_percent: Option<f64>,
+ pub discount_amount: f64,
+ pub currency: String,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct OrderClassifiedTotal {
+ pub price_amount: f64,
+ pub price_currency: String,
+ pub quantity_amount: f64,
+ pub quantity_unit: String,
+}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
@@ -1,2 +1,3 @@
pub mod nostr;
+pub mod price;
pub mod unit;
diff --git a/src/utils/nostr.rs b/src/utils/nostr.rs
@@ -1,5 +1,6 @@
-use std::borrow::Cow;
+use std::{borrow::Cow, time::Duration};
+use crate::events::job_request::JobRequestError;
use anyhow::Result;
use nostr::{
event::{Event, EventBuilder, EventId, Kind, Tag, TagKind, TagStandard},
@@ -12,10 +13,38 @@ use nostr::{
types::{RelayUrl, Timestamp},
};
use nostr_sdk::Client;
-use nostr_sdk::RelayPoolNotification;
+use nostr_sdk::prelude::*;
use thiserror::Error;
-use crate::events::job_request::JobRequestError;
+#[derive(Debug, Error)]
+pub enum NostrUtilsError {
+ #[error("Client error: {0}")]
+ ClientError(#[from] nostr_sdk::client::Error),
+
+ #[error("Event error: {0}")]
+ EventError(#[from] nostr::event::Error),
+
+ #[error("Event not found: {0}")]
+ EventNotFound(String),
+
+ #[error("Event builder failure: {0}")]
+ EventBuildError(#[from] nostr::event::builder::Error),
+}
+
+#[derive(Debug, Error)]
+pub enum NostrTagsResolveError {
+ #[error("Missing public key tag in encrypted event: {0:?}")]
+ MissingPTag(nostr::Event),
+
+ #[error("Encrypted event recipient mismatch")]
+ NotRecipient,
+
+ #[error("Decryption error: {0}")]
+ DecryptionError(String),
+
+ #[error("Failed to parse decrypted tag JSON: {0}")]
+ ParseError(#[from] serde_json::Error),
+}
pub fn nostr_kind(kind: u16) -> Kind {
Kind::Custom(kind)
@@ -103,19 +132,13 @@ pub fn nostr_tag_match_summary(tag: &Tag) -> Option<String> {
}
}
-#[derive(Debug, thiserror::Error)]
-pub enum NostrEventError {
- #[error("Failed to build job result event: {0}")]
- BuildError(#[from] nostr::event::builder::Error),
-}
-
pub fn nostr_event_job_result(
job_request: &Event,
payload: impl Into<String>,
millisats: u64,
bolt11: Option<String>,
tags: Option<Vec<Tag>>,
-) -> Result<EventBuilder, NostrEventError> {
+) -> Result<EventBuilder, NostrUtilsError> {
let builder = EventBuilder::job_result(job_request.clone(), payload, millisats, bolt11)?
.tags(tags.unwrap_or_default());
Ok(builder)
@@ -126,7 +149,7 @@ pub fn nostr_event_job_feedback(
error: JobRequestError,
status: &str,
tags: Option<Vec<Tag>>,
-) -> Result<EventBuilder, NostrEventError> {
+) -> Result<EventBuilder, NostrUtilsError> {
let status = status
.parse::<DataVendingMachineStatus>()
.unwrap_or(DataVendingMachineStatus::Error);
@@ -136,38 +159,21 @@ pub fn nostr_event_job_feedback(
Ok(builder)
}
-pub async fn nostr_fetch_event_by_id(client: Client, id: &str) -> Result<Option<Event>> {
- let event_id = EventId::from_hex(id)?;
- let filter = Filter::new().id(event_id);
-
- client.connect().await;
- client.subscribe(filter, None).await?;
-
- let mut notifications = client.notifications();
- while let Ok(n) = notifications.recv().await {
- if let RelayPoolNotification::Event { event, .. } = n {
- if event.id == event_id {
- return Ok(Some(*event));
- }
- }
- }
-
- Ok(None)
+pub async fn nostr_send_event(
+ client: Client,
+ event: EventBuilder,
+) -> Result<Output<EventId>, NostrUtilsError> {
+ Ok(client.send_event_builder(event).await?)
}
-#[derive(Debug, Error)]
-pub enum NostrTagsResolveError {
- #[error("Missing public key tag in encrypted event: {0:?}")]
- MissingPTag(nostr::Event),
-
- #[error("Encrypted event recipient mismatch")]
- NotRecipient,
-
- #[error("Decryption error: {0}")]
- DecryptionError(String),
-
- #[error("Failed to parse decrypted tag JSON: {0}")]
- ParseError(#[from] serde_json::Error),
+pub async fn nostr_fetch_event_by_id(client: Client, id: &str) -> Result<Event, NostrUtilsError> {
+ let event_id = EventId::parse(id)?;
+ let filter = Filter::new().id(event_id);
+ let events = client.fetch_events(filter, Duration::from_secs(10)).await?;
+ let event = events
+ .first()
+ .ok_or_else(|| NostrUtilsError::EventNotFound(event_id.to_hex()))?;
+ Ok(event.clone())
}
pub fn nostr_tags_resolve(event: &Event, keys: &Keys) -> Result<Vec<Tag>, NostrTagsResolveError> {
diff --git a/src/utils/price.rs b/src/utils/price.rs
@@ -0,0 +1,15 @@
+use super::unit::{MassUnit, convert_mass};
+
+pub fn calculate_total_price(
+ quantity_amount: f64,
+ quantity_unit: &MassUnit,
+ quantity_count: u32,
+ price_amount: f64,
+ price_quantity_amount: f64,
+ price_quantity_unit: &MassUnit,
+) -> f64 {
+ let total_mass = quantity_amount * quantity_count as f64;
+ let total_mass_in_price_unit = convert_mass(total_mass, quantity_unit, price_quantity_unit);
+ let price_per_quantity_unit = price_amount / price_quantity_amount;
+ price_per_quantity_unit * total_mass_in_price_unit
+}
diff --git a/src/utils/unit.rs b/src/utils/unit.rs
@@ -11,27 +11,38 @@ pub enum MassUnitError {
InvalidAmount(f64),
}
-#[derive(Debug, Serialize, Deserialize, Clone)]
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum MassUnit {
G,
Kg,
+ Oz,
Lb,
}
impl MassUnit {
- pub fn to_grams(&self, amount: f64) -> Result<f64, MassUnitError> {
- if !amount.is_finite() || amount.is_nan() {
+ pub fn to_grams(&self) -> f64 {
+ match self {
+ MassUnit::G => 1.0,
+ MassUnit::Kg => 1000.0,
+ MassUnit::Oz => 28.3495,
+ MassUnit::Lb => 453.592,
+ }
+ }
+
+ pub fn amount_in_grams(&self, amount: f64) -> Result<f64, MassUnitError> {
+ if !amount.is_finite() {
return Err(MassUnitError::InvalidAmount(amount));
}
- let grams = match self {
- MassUnit::G => amount,
- MassUnit::Kg => amount * 1000.0,
- MassUnit::Lb => amount * 453.592,
+ let factor = match self {
+ MassUnit::G => 1.0,
+ MassUnit::Kg => 1000.0,
+ MassUnit::Oz => 28.3495,
+ MassUnit::Lb => 453.592,
};
- Ok(grams)
+ Ok(amount * factor)
}
}
@@ -40,6 +51,7 @@ impl fmt::Display for MassUnit {
let unit_str = match self {
MassUnit::G => "g",
MassUnit::Kg => "kg",
+ MassUnit::Oz => "oz",
MassUnit::Lb => "lb",
};
write!(f, "{unit_str}")
@@ -53,8 +65,14 @@ impl FromStr for MassUnit {
match s {
"g" => Ok(MassUnit::G),
"kg" => Ok(MassUnit::Kg),
+ "oz" => Ok(MassUnit::Oz),
"lb" => Ok(MassUnit::Lb),
other => Err(MassUnitError::InvalidUnit(other.to_string())),
}
}
}
+
+pub fn convert_mass(amount: f64, from_unit: &MassUnit, to_unit: &MassUnit) -> f64 {
+ let amount_g = amount * from_unit.to_grams();
+ amount_g / to_unit.to_grams()
+}