mostro 0.17.4

Lightning Network peer-to-peer nostr platform
//! This module handles dispute-related functionality for the P2P trading system.
//! It provides mechanisms for users to initiate disputes, notify counterparties,
//! and publish dispute events to the network.

use crate::app::context::AppContext;
use crate::db::find_dispute_by_order_id;
use crate::nip33::{create_platform_tag_values, new_dispute_event};
use crate::util::{enqueue_order_msg, get_order};
use mostro_core::prelude::*;
use nostr_sdk::prelude::*;

use sqlx_crud::traits::Crud;
use std::borrow::Cow;
use uuid::Uuid;

/// Publishes a dispute event to the Nostr network.
///
/// Creates and publishes a NIP-33 replaceable event containing dispute details,
/// including status, initiator (`buyer` or `seller`), and application metadata.
async fn publish_dispute_event(
    ctx: &AppContext,
    dispute: &Dispute,
    my_keys: &Keys,
    is_buyer_dispute: bool,
) -> Result<(), MostroError> {
    // Create initiator string
    let initiator = match is_buyer_dispute {
        true => "buyer",
        false => "seller",
    };

    // Create tags for the dispute event
    let tags = Tags::from_list(vec![
        // Status tag - indicates the current state of the dispute
        Tag::custom(
            TagKind::Custom(Cow::Borrowed("s")),
            vec![dispute.status.to_string()],
        ),
        // Who is the dispute creator
        Tag::custom(
            TagKind::Custom(Cow::Borrowed("initiator")),
            vec![initiator.to_string()],
        ),
        // Application identifier tag
        Tag::custom(
            TagKind::Custom(Cow::Borrowed("y")),
            create_platform_tag_values(ctx.settings().mostro.name.as_deref()),
        ),
        // Event type tag
        Tag::custom(
            TagKind::Custom(Cow::Borrowed("z")),
            vec!["dispute".to_string()],
        ),
    ]);

    // Create a new NIP-33 replaceable event (kind 38386 for disputes)
    // Empty content string as the information is in the tags
    let event = new_dispute_event(my_keys, "", dispute.id.to_string(), tags)
        .map_err(|_| MostroInternalErr(ServiceError::DisputeEventError))?;

    tracing::info!("Publishing dispute event: {:#?}", event);

    // Get nostr client from context and publish the event
    let client = ctx.nostr_client();
    match client.send_event(&event).await {
        Ok(_) => {
            tracing::info!(
                "Successfully published dispute event for dispute ID: {}",
                dispute.id
            );
            Ok(())
        }
        Err(e) => {
            tracing::error!("Failed to send dispute event: {}", e);
            Err(MostroInternalErr(ServiceError::NostrError(e.to_string())))
        }
    }
}

/// Gets information about the counterparty in a dispute.
///
/// Returns:
/// - Ok(true) if the dispute was initiated by the buyer
/// - Ok(false) if initiated by the seller
/// - Err(CantDoReason::InvalidPubkey) if the sender matches neither party
fn get_counterpart_info(sender: &str, buyer: &str, seller: &str) -> Result<bool, CantDoReason> {
    match sender {
        s if s == buyer => Ok(true),   // buyer is initiator
        s if s == seller => Ok(false), // seller is initiator
        _ => Err(CantDoReason::InvalidPubkey),
    }
}

/// Validates and retrieves an order from the database.
///
/// Checks that:
/// - The order exists
/// - The order status allows disputes (Active or FiatSent)
async fn get_valid_order(ctx: &AppContext, msg: &Message) -> Result<Order, MostroError> {
    // Try to fetch the order from the database
    let order = get_order(msg, ctx.pool()).await?;

    // Check if the order status is Active or FiatSent
    if order.check_status(Status::Active).is_err() && order.check_status(Status::FiatSent).is_err()
    {
        return Err(MostroCantDo(CantDoReason::NotAllowedByStatus));
    }

    Ok(order)
}

async fn notify_dispute_to_users(
    dispute: &Dispute,
    msg: &Message,
    order_id: Uuid,
    counterpart_pubkey: PublicKey,
    initiator_pubkey: PublicKey,
) -> Result<(), MostroError> {
    // Message to counterpart
    enqueue_order_msg(
        msg.get_inner_message_kind().request_id,
        Some(order_id),
        Action::DisputeInitiatedByPeer,
        Some(Payload::Dispute(dispute.clone().id, None)),
        counterpart_pubkey,
        None,
    )
    .await;

    // Message to dispute initiator
    enqueue_order_msg(
        msg.get_inner_message_kind().request_id,
        Some(order_id),
        Action::DisputeInitiatedByYou,
        Some(Payload::Dispute(dispute.clone().id, None)),
        initiator_pubkey,
        None,
    )
    .await;

    Ok(())
}

/// Main handler for dispute actions.
///
/// This function:
/// 1. Validates the order and dispute status
/// 2. Updates the order status
/// 3. Creates a new dispute record
/// 4. Notifies both parties
/// 5. Publishes the dispute event to the network
pub async fn dispute_action(
    ctx: &AppContext,
    msg: Message,
    event: &UnwrappedMessage,
    my_keys: &Keys,
) -> Result<(), MostroError> {
    let pool = ctx.pool();
    let order_id = if let Some(order_id) = msg.get_inner_message_kind().id {
        order_id
    } else {
        return Err(MostroInternalErr(ServiceError::InvalidOrderId));
    };
    // Check dispute for this order id is yet present.
    if find_dispute_by_order_id(pool, order_id).await.is_ok() {
        return Err(MostroInternalErr(ServiceError::DisputeAlreadyExists));
    }
    // Get and validate order
    let mut order = get_valid_order(ctx, &msg).await?;
    // Get seller and buyer pubkeys
    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)),
    };
    // Get message sender
    let message_sender = event.sender.to_string();
    // Get counterpart info
    let is_buyer_dispute = match get_counterpart_info(&message_sender, &buyer, &seller) {
        Ok(is_buyer_dispute) => is_buyer_dispute,
        Err(cause) => return Err(MostroCantDo(cause)),
    };

    // Create new dispute record
    let dispute = Dispute::new(order_id, order.status.clone());

    // Setup dispute
    if order.setup_dispute(is_buyer_dispute).is_ok() {
        order
            .clone()
            .update(pool)
            .await
            .map_err(|cause| MostroInternalErr(ServiceError::DbAccessError(cause.to_string())))?;
    }

    // Save dispute to database
    let dispute = dispute
        .create(pool)
        .await
        .map_err(|cause| MostroInternalErr(ServiceError::DbAccessError(cause.to_string())))?;

    // Get pubkeys of initiator and counterpart
    let (initiator_pubkey, counterpart_pubkey) = if is_buyer_dispute {
        (
            &order
                .get_buyer_pubkey()
                .map_err(|_| MostroInternalErr(ServiceError::InvalidPubkey))?,
            &order
                .get_seller_pubkey()
                .map_err(|_| MostroInternalErr(ServiceError::InvalidPubkey))?,
        )
    } else {
        (
            &order
                .get_seller_pubkey()
                .map_err(|_| MostroInternalErr(ServiceError::InvalidPubkey))?,
            &order
                .get_buyer_pubkey()
                .map_err(|_| MostroInternalErr(ServiceError::InvalidPubkey))?,
        )
    };

    notify_dispute_to_users(
        &dispute,
        &msg,
        order_id,
        *counterpart_pubkey,
        *initiator_pubkey,
    )
    .await?;

    // Publish dispute event to network
    publish_dispute_event(ctx, &dispute, my_keys, is_buyer_dispute)
        .await
        .map_err(|_| MostroInternalErr(ServiceError::DisputeEventError))?;

    Ok(())
}

/// Closes a dispute after users resolve it themselves (cooperative cancel or release).
///
/// This is a best-effort operation: if the dispute update or event publishing fails,
/// errors are logged but not propagated, since the primary order operation has already
/// succeeded.
///
/// # Arguments
/// * `pool` - Database connection pool
/// * `order` - The order associated with the dispute
/// * `new_status` - The new dispute status (e.g., SellerRefunded or Settled)
/// * `my_keys` - Mostro's keys for signing the dispute event
/// * `context` - Description of the resolution context for logging (e.g., "cooperative cancel")
pub async fn close_dispute_after_user_resolution(
    ctx: &AppContext,
    order: &Order,
    new_status: DisputeStatus,
    my_keys: &Keys,
    context: &str,
) {
    let pool = ctx.pool();
    if let Ok(mut dispute) = find_dispute_by_order_id(pool, order.id).await {
        let dispute_id = dispute.id;
        dispute.status = new_status.to_string();

        if let Err(e) = dispute.update(pool).await {
            tracing::error!(
                "Failed to update dispute {} status after {}: {}",
                dispute_id,
                context,
                e
            );
        } else {
            tracing::info!(
                "Dispute {} closed automatically after {} of order {}",
                dispute_id,
                context,
                order.id
            );

            // Determine who initiated the dispute for the event tag
            let dispute_initiator = match (order.seller_dispute, order.buyer_dispute) {
                (true, false) => "seller",
                (false, true) => "buyer",
                _ => {
                    tracing::warn!(
                        "Dispute {} for order {} has inconsistent dispute flags (seller={}, buyer={}); \
                        publishing initiator as 'unknown'",
                        dispute_id,
                        order.id,
                        order.seller_dispute,
                        order.buyer_dispute,
                    );
                    "unknown"
                }
            };

            // Publish updated dispute event to Nostr so admin clients see it as resolved
            let tags = Tags::from_list(vec![
                Tag::custom(
                    TagKind::Custom(Cow::Borrowed("s")),
                    vec![new_status.to_string()],
                ),
                Tag::custom(
                    TagKind::Custom(Cow::Borrowed("initiator")),
                    vec![dispute_initiator.to_string()],
                ),
                Tag::custom(
                    TagKind::Custom(Cow::Borrowed("y")),
                    create_platform_tag_values(ctx.settings().mostro.name.as_deref()),
                ),
                Tag::custom(
                    TagKind::Custom(Cow::Borrowed("z")),
                    vec!["dispute".to_string()],
                ),
            ]);

            match new_dispute_event(my_keys, "", dispute_id.to_string(), tags) {
                Ok(event) => {
                    let client = ctx.nostr_client();
                    tracing::info!("Publishing dispute close event: {:#?}", event);
                    if let Err(e) = client.send_event(&event).await {
                        tracing::error!("Failed to publish dispute close event: {}", e);
                    }
                }
                Err(e) => {
                    tracing::error!(
                        "Failed to create dispute close event for dispute {}: {}",
                        dispute_id,
                        e
                    );
                }
            }
        }
    }
}