use crate::app::context::AppContext;
use crate::db::{is_user_present, update_user_rating};
use crate::util::{enqueue_order_msg, get_order, update_user_rating_event};
use mostro_core::prelude::*;
use nostr_sdk::prelude::*;
pub fn prepare_variables_for_vote(
message_sender: &str,
order: &Order,
) -> Result<(String, bool, bool), MostroError> {
let mut counterpart_trade_pubkey: String = String::new();
let mut buyer_rating: bool = false;
let mut seller_rating: bool = false;
let (seller, buyer) = match (&order.seller_pubkey, &order.buyer_pubkey) {
(Some(seller), Some(buyer)) => (seller.to_owned(), buyer.to_owned()),
(None, _) => return Err(MostroInternalErr(ServiceError::InvalidPubkey)),
(_, None) => return Err(MostroInternalErr(ServiceError::InvalidPubkey)),
};
if message_sender == buyer {
buyer_rating = true;
counterpart_trade_pubkey = order
.get_buyer_pubkey()
.map_err(MostroInternalErr)?
.to_string();
} else if message_sender == seller {
seller_rating = true;
counterpart_trade_pubkey = order
.get_seller_pubkey()
.map_err(MostroInternalErr)?
.to_string();
};
Ok((counterpart_trade_pubkey, buyer_rating, seller_rating))
}
pub async fn update_user_reputation_action(
ctx: &AppContext,
msg: Message,
event: &UnwrappedMessage,
my_keys: &Keys,
) -> Result<(), MostroError> {
let pool = ctx.pool();
let order = get_order(&msg, pool).await?;
let (counterpart_trade_pubkey, buyer_rating, seller_rating) =
prepare_variables_for_vote(&event.sender.to_string(), &order)?;
if !(order.check_status(Status::Success).is_ok()
|| (order.check_status(Status::SettledHoldInvoice).is_ok() && seller_rating))
{
return Err(MostroCantDo(CantDoReason::InvalidOrderStatus));
}
let mut update_seller_rate = false;
let mut update_buyer_rate = false;
if seller_rating && !order.seller_sent_rate {
update_seller_rate = true;
} else if buyer_rating && !order.buyer_sent_rate {
update_buyer_rate = true;
};
if !update_buyer_rate && !update_seller_rate {
return Ok(());
};
let new_rating = msg
.get_inner_message_kind()
.get_rating()
.map_err(MostroInternalErr)?;
let (normal_buyer_idkey, normal_seller_idkey) = order
.is_full_privacy_order()
.map_err(|_| MostroInternalErr(ServiceError::InvalidPubkey))?;
let mut user_to_vote = if buyer_rating {
if let Some(seller_key) = normal_seller_idkey {
is_user_present(pool, seller_key).await.map_err(|cause| {
MostroInternalErr(ServiceError::DbAccessError(cause.to_string()))
})?
} else {
return Ok(());
}
} else {
if let Some(buyer_key) = normal_buyer_idkey {
is_user_present(pool, buyer_key).await.map_err(|cause| {
MostroInternalErr(ServiceError::DbAccessError(cause.to_string()))
})?
} else {
return Ok(());
}
};
user_to_vote.update_rating(new_rating);
let reputation_event = Rating::new(
user_to_vote.total_reviews as u64,
user_to_vote.total_rating as f64,
user_to_vote.last_rating as u8,
user_to_vote.min_rating as u8,
user_to_vote.max_rating as u8,
)
.to_tags()
.map_err(|cause| MostroInternalErr(ServiceError::NostrError(cause.to_string())))?;
let days = calculate_days_since_creation(user_to_vote.created_at);
let mut tags: Vec<Tag> = reputation_event.into_iter().collect();
tags.push(Tag::custom(
TagKind::Custom(std::borrow::Cow::Borrowed("days")),
vec![days.to_string()],
));
let reputation_event = Tags::from_list(tags);
if let Err(e) = update_user_rating(
pool,
user_to_vote.pubkey,
user_to_vote.last_rating,
user_to_vote.min_rating,
user_to_vote.max_rating,
user_to_vote.total_reviews,
user_to_vote.total_rating,
)
.await
{
return Err(MostroInternalErr(ServiceError::DbAccessError(format!(
"Error updating user rating : {}",
e
))));
}
if buyer_rating || seller_rating {
update_user_rating_event(
&counterpart_trade_pubkey,
update_buyer_rate,
update_seller_rate,
reputation_event,
&msg,
my_keys,
pool,
)
.await
.map_err(|cause| {
MostroInternalErr(ServiceError::DbAccessError(format!(
"Error updating user rating event : {}",
cause
)))
})?;
enqueue_order_msg(
msg.get_inner_message_kind().request_id,
Some(order.id),
Action::RateReceived,
Some(Payload::RatingUser(new_rating)),
event.sender,
None,
)
.await;
}
Ok(())
}
fn calculate_days_since_creation(created_at: i64) -> u64 {
const SECONDS_IN_DAY: u64 = 86_400;
let now = Timestamp::now().as_secs();
u64::try_from(created_at)
.ok()
.filter(|ts| *ts > 0)
.map(|ts| now.saturating_sub(ts) / SECONDS_IN_DAY)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::settings::Settings;
use crate::config::MOSTRO_CONFIG;
use mostro_core::message::{MessageKind, Payload};
use mostro_core::order::Order;
use nostr_sdk::{Keys, Timestamp};
use sqlx::SqlitePool;
use sqlx_crud::Crud;
use uuid::Uuid;
fn init_test_settings() {
let _ = MOSTRO_CONFIG.set(Settings {
database: Default::default(),
nostr: Default::default(),
mostro: Default::default(),
lightning: Default::default(),
rpc: Default::default(),
expiration: Some(Default::default()),
anti_abuse_bond: None,
});
}
async fn create_test_pool() -> SqlitePool {
init_test_settings();
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
sqlx::migrate!().run(&pool).await.unwrap();
pool
}
fn create_test_keys() -> Keys {
Keys::generate()
}
fn create_unwrapped_message_with_pubkey(pubkey: PublicKey) -> UnwrappedMessage {
UnwrappedMessage {
message: Message::Order(MessageKind::new(
Some(Uuid::new_v4()),
Some(1),
None,
Action::RateUser,
None,
)),
signature: None,
sender: pubkey,
identity: Keys::generate().public_key(),
created_at: Timestamp::now(),
}
}
fn create_rate_user_message(order_id: Uuid, rating: u8) -> Message {
let kind = MessageKind::new(
Some(order_id),
Some(1),
None,
Action::RateUser,
Some(Payload::RatingUser(rating)),
);
Message::Order(kind)
}
fn create_test_order(
status: Status,
seller_pubkey: PublicKey,
buyer_pubkey: PublicKey,
) -> Order {
Order {
id: Uuid::new_v4(),
status: status.to_string(),
seller_pubkey: Some(seller_pubkey.to_string()),
buyer_pubkey: Some(buyer_pubkey.to_string()),
seller_sent_rate: false,
buyer_sent_rate: false,
..Default::default()
}
}
#[tokio::test]
async fn test_update_user_reputation_allows_success_status() {
let pool = create_test_pool().await;
use crate::app::context::test_utils::{test_settings, TestContextBuilder};
let ctx = TestContextBuilder::new()
.with_pool(std::sync::Arc::new(pool.clone()))
.with_settings(test_settings())
.build();
let keys = create_test_keys();
let seller_keys = create_test_keys();
let buyer_keys = create_test_keys();
let seller_pk = seller_keys.public_key();
let buyer_pk = buyer_keys.public_key();
let event = create_unwrapped_message_with_pubkey(seller_pk);
let order = create_test_order(Status::Success, seller_pk, buyer_pk);
let order = order.create(&pool).await.unwrap();
let msg = create_rate_user_message(order.id, 5);
let result = update_user_reputation_action(&ctx, msg, &event, &keys).await;
if let Err(MostroCantDo(CantDoReason::InvalidOrderStatus)) = result {
panic!("valid Success status must not be rejected");
}
}
#[tokio::test]
async fn test_update_user_reputation_rejects_settled_hold_invoice_buyer() {
let pool = create_test_pool().await;
use crate::app::context::test_utils::{test_settings, TestContextBuilder};
let ctx = TestContextBuilder::new()
.with_pool(std::sync::Arc::new(pool.clone()))
.with_settings(test_settings())
.build();
let keys = create_test_keys();
let seller_keys = create_test_keys();
let buyer_keys = create_test_keys();
let seller_pk = seller_keys.public_key();
let buyer_pk = buyer_keys.public_key();
let event = create_unwrapped_message_with_pubkey(buyer_pk);
let order = create_test_order(Status::SettledHoldInvoice, seller_pk, buyer_pk);
let order = order.create(&pool).await.unwrap();
let msg = create_rate_user_message(order.id, 5);
let result = update_user_reputation_action(&ctx, msg, &event, &keys).await;
match result {
Err(MostroCantDo(CantDoReason::InvalidOrderStatus)) => {}
_ => panic!("buyer should not be able to rate SettledHoldInvoice order"),
}
}
#[tokio::test]
async fn test_update_user_reputation_updates_buyer_and_order_flags() {
use crate::db::{add_new_user, is_user_present};
let pool = create_test_pool().await;
use crate::app::context::test_utils::{test_settings, TestContextBuilder};
let ctx = TestContextBuilder::new()
.with_pool(std::sync::Arc::new(pool.clone()))
.with_settings(test_settings())
.build();
let keys = create_test_keys();
let seller_keys = create_test_keys();
let buyer_keys = create_test_keys();
let seller_pk = seller_keys.public_key();
let buyer_pk = buyer_keys.public_key();
let seller_id_keys = create_test_keys();
let buyer_id_keys = create_test_keys();
let seller_id = seller_id_keys.public_key().to_string();
let buyer_id = buyer_id_keys.public_key().to_string();
let seller_user = User {
pubkey: seller_id.clone(),
..Default::default()
};
add_new_user(&pool, seller_user).await.unwrap();
let mut order = create_test_order(Status::Success, seller_pk, buyer_pk);
order.master_seller_pubkey = Some(seller_id.clone());
order.master_buyer_pubkey = Some(buyer_id.clone());
let order = order.create(&pool).await.unwrap();
let event = create_unwrapped_message_with_pubkey(buyer_pk);
let msg = create_rate_user_message(order.id, 5);
let result = update_user_reputation_action(&ctx, msg, &event, &keys).await;
assert!(result.is_ok());
let seller_user = is_user_present(&pool, seller_id).await.unwrap();
assert_eq!(seller_user.total_reviews, 1);
assert_eq!(seller_user.last_rating, 5);
assert_eq!(seller_user.min_rating, 5);
assert_eq!(seller_user.max_rating, 5);
assert!((seller_user.total_rating - 2.5).abs() < f64::EPSILON);
let updated_order = Order::by_id(&pool, order.id)
.await
.unwrap()
.expect("order not found");
assert!(updated_order.buyer_sent_rate);
}
#[tokio::test]
async fn test_update_user_reputation_buyer_already_rated_is_noop() {
use crate::db::{add_new_user, is_user_present};
let pool = create_test_pool().await;
use crate::app::context::test_utils::{test_settings, TestContextBuilder};
let ctx = TestContextBuilder::new()
.with_pool(std::sync::Arc::new(pool.clone()))
.with_settings(test_settings())
.build();
let keys = create_test_keys();
let seller_keys = create_test_keys();
let buyer_keys = create_test_keys();
let seller_pk = seller_keys.public_key();
let buyer_pk = buyer_keys.public_key();
let seller_id_keys = create_test_keys();
let buyer_id_keys = create_test_keys();
let seller_id = seller_id_keys.public_key().to_string();
let buyer_id = buyer_id_keys.public_key().to_string();
let seller_user = User {
pubkey: seller_id.clone(),
..Default::default()
};
add_new_user(&pool, seller_user).await.unwrap();
let mut order = create_test_order(Status::Success, seller_pk, buyer_pk);
order.master_seller_pubkey = Some(seller_id.clone());
order.master_buyer_pubkey = Some(buyer_id.clone());
order.buyer_sent_rate = true;
let order = order.create(&pool).await.unwrap();
let event = create_unwrapped_message_with_pubkey(buyer_pk);
let msg = create_rate_user_message(order.id, 5);
let result = update_user_reputation_action(&ctx, msg, &event, &keys).await;
assert!(result.is_ok());
let seller_user = is_user_present(&pool, seller_id).await.unwrap();
assert_eq!(seller_user.total_reviews, 0);
assert!((seller_user.total_rating - 0.0).abs() < f64::EPSILON);
}
#[tokio::test]
async fn test_update_user_reputation_updates_seller_and_order_flags() {
use crate::db::{add_new_user, is_user_present};
let pool = create_test_pool().await;
use crate::app::context::test_utils::{test_settings, TestContextBuilder};
let ctx = TestContextBuilder::new()
.with_pool(std::sync::Arc::new(pool.clone()))
.with_settings(test_settings())
.build();
let keys = create_test_keys();
let seller_keys = create_test_keys();
let buyer_keys = create_test_keys();
let seller_pk = seller_keys.public_key();
let buyer_pk = buyer_keys.public_key();
let seller_id_keys = create_test_keys();
let buyer_id_keys = create_test_keys();
let seller_id = seller_id_keys.public_key().to_string();
let buyer_id = buyer_id_keys.public_key().to_string();
let buyer_user = User {
pubkey: buyer_id.clone(),
..Default::default()
};
add_new_user(&pool, buyer_user).await.unwrap();
let mut order = create_test_order(Status::Success, seller_pk, buyer_pk);
order.master_seller_pubkey = Some(seller_id.clone());
order.master_buyer_pubkey = Some(buyer_id.clone());
let order = order.create(&pool).await.unwrap();
let event = create_unwrapped_message_with_pubkey(seller_pk);
let msg = create_rate_user_message(order.id, 4);
let result = update_user_reputation_action(&ctx, msg, &event, &keys).await;
assert!(result.is_ok());
let buyer_user = is_user_present(&pool, buyer_id).await.unwrap();
assert_eq!(buyer_user.total_reviews, 1);
assert_eq!(buyer_user.last_rating, 4);
assert_eq!(buyer_user.min_rating, 4);
assert_eq!(buyer_user.max_rating, 4);
assert!((buyer_user.total_rating - 2.0).abs() < f64::EPSILON);
let updated_order = Order::by_id(&pool, order.id)
.await
.unwrap()
.expect("order not found");
assert!(updated_order.seller_sent_rate);
}
#[test]
fn test_prepare_variables_for_vote_buyer() {
let seller_keys = create_test_keys();
let buyer_keys = create_test_keys();
let order = create_test_order(
Status::Success,
seller_keys.public_key(),
buyer_keys.public_key(),
);
let result = prepare_variables_for_vote(&buyer_keys.public_key().to_string(), &order);
assert!(result.is_ok());
let (_, buyer_rating, seller_rating) = result.unwrap();
assert!(buyer_rating);
assert!(!seller_rating);
}
#[test]
fn test_prepare_variables_for_vote_seller() {
let seller_keys = create_test_keys();
let buyer_keys = create_test_keys();
let order = create_test_order(
Status::Success,
seller_keys.public_key(),
buyer_keys.public_key(),
);
let result = prepare_variables_for_vote(&seller_keys.public_key().to_string(), &order);
assert!(result.is_ok());
let (_, buyer_rating, seller_rating) = result.unwrap();
assert!(!buyer_rating);
assert!(seller_rating);
}
#[test]
fn test_rating_validation_success_status() {
let seller_keys = create_test_keys();
let buyer_keys = create_test_keys();
let order = create_test_order(
Status::Success,
seller_keys.public_key(),
buyer_keys.public_key(),
);
assert!(order.check_status(Status::Success).is_ok());
let (_, _, seller_rating) =
prepare_variables_for_vote(&seller_keys.public_key().to_string(), &order).unwrap();
let can_rate_seller = order.check_status(Status::Success).is_ok()
|| (order.check_status(Status::SettledHoldInvoice).is_ok() && seller_rating);
assert!(can_rate_seller);
let (_, buyer_rating, _) =
prepare_variables_for_vote(&buyer_keys.public_key().to_string(), &order).unwrap();
let can_rate_buyer = order.check_status(Status::Success).is_ok()
|| (order.check_status(Status::SettledHoldInvoice).is_ok() && !buyer_rating);
assert!(can_rate_buyer);
}
#[test]
fn test_rating_validation_settled_hold_invoice_seller() {
let seller_keys = create_test_keys();
let buyer_keys = create_test_keys();
let order = create_test_order(
Status::SettledHoldInvoice,
seller_keys.public_key(),
buyer_keys.public_key(),
);
let (_, _, seller_rating) =
prepare_variables_for_vote(&seller_keys.public_key().to_string(), &order).unwrap();
let can_rate_seller = order.check_status(Status::Success).is_ok()
|| (order.check_status(Status::SettledHoldInvoice).is_ok() && seller_rating);
assert!(can_rate_seller);
}
#[test]
fn test_rating_validation_settled_hold_invoice_buyer_denied() {
let seller_keys = create_test_keys();
let buyer_keys = create_test_keys();
let order = create_test_order(
Status::SettledHoldInvoice,
seller_keys.public_key(),
buyer_keys.public_key(),
);
let (_, buyer_rating, _) =
prepare_variables_for_vote(&buyer_keys.public_key().to_string(), &order).unwrap();
let can_rate_buyer = order.check_status(Status::Success).is_ok()
|| (order.check_status(Status::SettledHoldInvoice).is_ok() && !buyer_rating);
assert!(!can_rate_buyer);
}
#[test]
fn test_rating_validation_invalid_status() {
let seller_keys = create_test_keys();
let buyer_keys = create_test_keys();
let order = create_test_order(
Status::Pending,
seller_keys.public_key(),
buyer_keys.public_key(),
);
let (_, buyer_rating, seller_rating) =
prepare_variables_for_vote(&seller_keys.public_key().to_string(), &order).unwrap();
let can_rate_seller = order.check_status(Status::Success).is_ok()
|| (order.check_status(Status::SettledHoldInvoice).is_ok() && seller_rating);
assert!(!can_rate_seller);
let can_rate_buyer = order.check_status(Status::Success).is_ok()
|| (order.check_status(Status::SettledHoldInvoice).is_ok() && !buyer_rating);
assert!(!can_rate_buyer);
}
#[test]
fn test_calculate_days_since_creation_normal() {
let now = Timestamp::now().as_secs();
let created_at = (now - 10 * 86_400) as i64;
let days = calculate_days_since_creation(created_at);
assert_eq!(days, 10);
}
#[test]
fn test_calculate_days_since_creation_zero() {
let days = calculate_days_since_creation(0);
assert_eq!(days, 0);
}
#[test]
fn test_calculate_days_since_creation_negative() {
let days = calculate_days_since_creation(-1);
assert_eq!(days, 0);
}
#[test]
fn test_calculate_days_since_creation_partial_day() {
let now = Timestamp::now().as_secs();
let created_at = (now - 86_400 - 43_200) as i64;
let days = calculate_days_since_creation(created_at);
assert_eq!(days, 1);
}
}