use crate::util::{enqueue_order_msg, notify_taker_reputation};
use mostro_core::prelude::*;
use nostr_sdk::prelude::*;
use sqlx::SqlitePool;
use sqlx_crud::Crud;
use tracing::info;
pub async fn hold_invoice_paid(
hash: &str,
request_id: Option<u64>,
pool: &SqlitePool,
my_keys: &Keys,
) -> Result<(), MostroError> {
let order = crate::db::find_order_by_hash(pool, hash)
.await
.map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
let buyer_pubkey = order
.get_buyer_pubkey()
.map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;
let seller_pubkey = order
.get_seller_pubkey()
.map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;
info!(
"Order Id: {} - Seller paid invoice with hash: {hash}",
order.id
);
let order_kind = order.get_order_kind().map_err(MostroInternalErr)?;
let mut order_data = SmallOrder::new(
Some(order.id),
Some(order_kind),
None,
order.amount,
order.fiat_code.clone(),
order.min_amount,
order.max_amount,
order.fiat_amount,
order.payment_method.clone(),
order.premium,
order.buyer_pubkey.as_ref().cloned(),
order.seller_pubkey.as_ref().cloned(),
None,
Some(order.created_at),
Some(order.expires_at),
);
let status;
if order.buyer_invoice.is_some() {
status = Status::Active;
order_data.status = Some(status);
let mut seller_order_data = order_data.clone();
seller_order_data.amount = order.amount.saturating_add(order.fee);
enqueue_order_msg(
request_id,
Some(order.id),
Action::BuyerTookOrder,
Some(Payload::Order(seller_order_data)),
seller_pubkey,
None,
)
.await;
let mut buyer_order_data = order_data.clone();
buyer_order_data.amount = order.amount.saturating_sub(order.fee);
enqueue_order_msg(
request_id,
Some(order.id),
Action::HoldInvoicePaymentAccepted,
Some(Payload::Order(buyer_order_data)),
buyer_pubkey,
None,
)
.await;
} else {
let new_amount = order_data.amount - order.fee;
order_data.amount = new_amount;
status = Status::WaitingBuyerInvoice;
order_data.status = Some(status);
order_data.buyer_trade_pubkey = None;
order_data.seller_trade_pubkey = None;
enqueue_order_msg(
request_id,
Some(order.id),
Action::AddInvoice,
Some(Payload::Order(order_data)),
buyer_pubkey,
None,
)
.await;
enqueue_order_msg(
request_id,
Some(order.id),
Action::WaitingBuyerInvoice,
None,
seller_pubkey,
None,
)
.await;
tracing::info!("Notifying taker reputation to maker");
notify_taker_reputation(pool, &order).await?;
}
if let Ok(updated_order) = crate::util::update_order_event(my_keys, status, &order).await {
let _ = updated_order.update(pool).await;
}
crate::db::update_order_invoice_held_at_time(pool, order.id, Timestamp::now().as_secs() as i64)
.await
.map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
Ok(())
}
pub async fn hold_invoice_settlement(hash: &str, pool: &SqlitePool) -> Result<()> {
let order = crate::db::find_order_by_hash(pool, hash).await?;
info!(
"Order Id: {} - Invoice with hash: {} was settled!",
order.id, hash
);
Ok(())
}
pub async fn hold_invoice_canceled(hash: &str, pool: &SqlitePool) -> Result<()> {
let order = crate::db::find_order_by_hash(pool, hash).await?;
info!(
"Order Id: {} - Invoice with hash: {} was canceled!",
order.id, hash
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use mostro_core::order::{Kind as OrderKind, Status};
use nostr_sdk::{Keys, Timestamp};
use sqlx::SqlitePool;
async fn create_test_pool() -> SqlitePool {
SqlitePool::connect(":memory:").await.unwrap()
}
fn create_test_keys() -> Keys {
Keys::generate()
}
#[tokio::test]
async fn test_hold_invoice_paid_structure() {
let pool = create_test_pool().await;
let hash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let request_id = Some(1u64);
let keys = Keys::parse("nsec13as48eum93hkg7plv526r9gjpa0uc52zysqm93pmnkca9e69x6tsdjmdxd")
.expect("valid test nsec");
let result = hold_invoice_paid(hash, request_id, &pool, &keys).await;
assert!(result.is_ok() || result.is_err());
}
#[tokio::test]
async fn test_hold_invoice_settlement_structure() {
let pool = create_test_pool().await;
let hash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let result = hold_invoice_settlement(hash, &pool).await;
assert!(result.is_ok() || result.is_err());
}
#[tokio::test]
async fn test_hold_invoice_canceled_structure() {
let pool = create_test_pool().await;
let hash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let result = hold_invoice_canceled(hash, &pool).await;
assert!(result.is_ok() || result.is_err());
}
mod hold_invoice_flow_tests {
use super::*;
#[test]
fn test_hash_validation() {
let valid_hashes = vec![
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210", "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", ];
let invalid_hashes = vec![
"", "short", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefXX", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdeg", ];
for hash in valid_hashes {
assert_eq!(hash.len(), 64);
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
}
for hash in invalid_hashes {
assert!(hash.len() != 64 || !hash.chars().all(|c| c.is_ascii_hexdigit()));
}
}
#[test]
fn test_order_data_creation_logic() {
let order_id = uuid::Uuid::new_v4();
let order_kind = OrderKind::Sell;
let amount = 1000i64;
let fiat_code = "USD".to_string();
let fiat_amount = 100i64;
let payment_method = "SEPA".to_string();
let premium = 5;
let created_at = Timestamp::now().as_secs() as i64;
let expires_at = created_at + 3600;
let order_data = SmallOrder::new(
Some(order_id),
Some(order_kind),
None,
amount,
fiat_code.clone(),
None,
None,
fiat_amount,
payment_method.clone(),
premium,
None,
None,
None,
Some(created_at),
Some(expires_at),
);
assert_eq!(order_data.id, Some(order_id));
assert_eq!(order_data.kind, Some(order_kind));
assert_eq!(order_data.amount, amount);
assert_eq!(order_data.fiat_code, fiat_code);
assert_eq!(order_data.fiat_amount, fiat_amount);
assert_eq!(order_data.payment_method, payment_method);
assert_eq!(order_data.premium, premium);
assert_eq!(order_data.created_at, Some(created_at));
assert_eq!(order_data.expires_at, Some(expires_at));
}
#[test]
fn test_status_transitions() {
let initial_status = Status::WaitingBuyerInvoice;
let target_status = Status::Active;
let buyer_invoice_exists = true;
let resulting_status = if buyer_invoice_exists {
Status::Active
} else {
Status::WaitingBuyerInvoice
};
assert_eq!(resulting_status, target_status);
let buyer_invoice_exists = false;
let resulting_status = if buyer_invoice_exists {
Status::Active
} else {
Status::WaitingBuyerInvoice
};
assert_eq!(resulting_status, initial_status);
}
#[test]
fn test_fee_calculation_logic() {
let original_amount = 1000i64;
let fee = 15i64; let expected_new_amount = original_amount - fee;
assert_eq!(expected_new_amount, 985);
assert!(expected_new_amount < original_amount);
assert!(fee > 0);
let zero_fee = 0i64;
assert_eq!(original_amount - zero_fee, original_amount);
let large_fee = 500i64; let result_with_large_fee = original_amount - large_fee;
assert_eq!(result_with_large_fee, 500);
assert!(result_with_large_fee > 0); }
}
mod message_flow_tests {
use super::*;
#[test]
fn test_action_types_for_buyer_invoice_flow() {
let buyer_actions = vec![Action::BuyerTookOrder, Action::HoldInvoicePaymentAccepted];
let no_invoice_actions = vec![Action::AddInvoice, Action::WaitingBuyerInvoice];
for action in buyer_actions {
assert!(!no_invoice_actions.contains(&action));
}
for action in no_invoice_actions {
assert!(
![Action::BuyerTookOrder, Action::HoldInvoicePaymentAccepted].contains(&action)
);
}
}
#[test]
fn test_payload_creation_logic() {
let order_data = SmallOrder::new(
Some(uuid::Uuid::new_v4()),
Some(OrderKind::Sell),
Some(Status::Active),
1000,
"USD".to_string(),
None,
None,
100,
"SEPA".to_string(),
0,
None,
None,
None,
Some(Timestamp::now().as_secs() as i64),
Some(Timestamp::now().as_secs() as i64 + 3600),
);
let payload_with_order = Some(Payload::Order(order_data.clone()));
assert!(payload_with_order.is_some());
let payload_none: Option<Payload> = None;
assert!(payload_none.is_none());
if let Some(Payload::Order(order)) = payload_with_order {
assert_eq!(order.amount, 1000);
assert_eq!(order.fiat_code, "USD");
assert_eq!(order.status, Some(Status::Active));
} else {
panic!("Expected Order payload");
}
}
}
mod pubkey_extraction_tests {
use super::*;
#[test]
fn test_pubkey_extraction_logic() {
let keys = create_test_keys();
let buyer_pubkey = keys.public_key();
let seller_pubkey = create_test_keys().public_key();
assert_ne!(buyer_pubkey, seller_pubkey);
let buyer_pubkey_str = buyer_pubkey.to_string();
let seller_pubkey_str = seller_pubkey.to_string();
assert!(!buyer_pubkey_str.is_empty());
assert!(!seller_pubkey_str.is_empty());
assert_ne!(buyer_pubkey_str, seller_pubkey_str);
assert!(buyer_pubkey_str.chars().all(|c| c.is_ascii_hexdigit()));
assert!(seller_pubkey_str.chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(buyer_pubkey_str.len(), 64);
assert_eq!(seller_pubkey_str.len(), 64);
}
#[test]
fn test_request_id_handling() {
let valid_request_ids = vec![Some(1u64), Some(42u64), Some(1000u64), Some(u64::MAX)];
let none_request_id: Option<u64> = None;
for request_id in valid_request_ids {
assert!(request_id.is_some());
assert!(request_id.unwrap() > 0 || request_id.unwrap() == 0);
}
assert!(none_request_id.is_none());
}
}
mod timestamp_tests {
use super::*;
#[test]
fn test_timestamp_operations() {
let current_timestamp = Timestamp::now();
let timestamp_u64 = current_timestamp.as_secs();
let timestamp_i64 = timestamp_u64 as i64;
let year_2020 = 1577836800u64; let year_2050 = 2524608000u64;
assert!(timestamp_u64 > year_2020);
assert!(timestamp_u64 < year_2050);
assert!(timestamp_i64 > 0);
assert_eq!(timestamp_u64, timestamp_i64 as u64);
}
}
}