Skip to content

fix: close active dispute when seller releases funds#606

Merged
grunch merged 5 commits intoMostroP2P:mainfrom
Mostrica:fix/close-dispute-on-release
Feb 23, 2026
Merged

fix: close active dispute when seller releases funds#606
grunch merged 5 commits intoMostroP2P:mainfrom
Mostrica:fix/close-dispute-on-release

Conversation

@Mostrica
Copy link
Copy Markdown
Contributor

@Mostrica Mostrica commented Feb 19, 2026

Summary

When an order with an active dispute is released by the seller, the dispute status should be updated to settled and a Nostr event should be published so admin clients see it as resolved.

Problem

Currently, if a seller releases funds while a dispute is active:

  1. The order correctly transitions to settled-hold-invoicesuccess
  2. But the dispute remains as initiated or in-progress
  3. Admin clients still show the dispute as pending/active

This is the same issue fixed in #577 for cooperative cancellation, but for the release flow.

Solution

Add dispute closure logic to release_action() in src/app/release.rs, similar to what was done in cancel.rs:

  • Check if there's an active dispute on the order
  • Update dispute status to Settled (since sats went to buyer)
  • Publish updated dispute event to Nostr

Changes

  • Added find_dispute_by_order_id import from crate::db
  • Added new_dispute_event import from crate::nip33
  • Added dispute closure logic after settle_seller_hold_invoice() succeeds

Closes #605

Summary by CodeRabbit

  • New Features

    • Automatic, best-effort dispute closure when users resolve orders via cooperative cancellation or seller release; closure attempts run as a follow-up and won’t block the primary operation.
  • Documentation

    • Added comprehensive guidance for dispute auto-close on user resolution.
    • Removed older cooperative-cancel-specific dispute document and replaced with the consolidated guidance.

When an order with an active dispute is released by the seller,
the dispute status should be updated to 'settled' and a Nostr
event should be published so admin clients see it as resolved.

This is similar to the fix in cancel.rs for cooperative cancellation
(which sets status to 'seller-refunded'), but for the release flow
where sats go to the buyer.

Closes MostroP2P#605
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a centralized best-effort function close_dispute_after_user_resolution to update dispute status and publish a Nostr event, and integrates it into cooperative cancel and release flows to close disputes after user-driven resolutions without blocking the primary operation.

Changes

Cohort / File(s) Summary
New dispute helper
src/app/dispute.rs
Add pub async fn close_dispute_after_user_resolution(pool: &Pool<Sqlite>, order: &Order, new_status: DisputeStatus, my_keys: &Keys, context: &str) — finds dispute by order, updates status, logs results, determines initiator tag, and publishes a Nostr dispute event; errors are logged and not propagated.
Release flow
src/app/release.rs
After settling seller hold/invoice in release_action, call close_dispute_after_user_resolution(..., DisputeStatus::Settled, ..., "release") as a best-effort follow-up (errors not surfaced to abort release).
Cooperative cancel flow
src/app/cancel.rs
Replace inline dispute-update + Nostr publishing with a call to close_dispute_after_user_resolution(..., DisputeStatus::SellerRefunded, ..., "cooperative cancel"), delegating dispute closure and event emission.
Documentation
docs/DISPUTE_AUTO_CLOSE_ON_COOPERATIVE_CANCEL.md, docs/DISPUTE_AUTO_CLOSE_ON_USER_RESOLUTION.md
Remove the cooperative-cancel-only doc and add a unified DISPUTE_AUTO_CLOSE_ON_USER_RESOLUTION.md describing both cooperative cancel and release scenarios, state transitions, code paths, and best-effort semantics.

Sequence Diagram(s)

sequenceDiagram
    participant ReleaseFlow as "release_action"
    participant CancelFlow as "cancel_cooperative_execution_step_2"
    participant DisputeFn as "close_dispute_after_user_resolution"
    participant DB as "SQLite Pool"
    participant Nostr as "NostrClient"

    ReleaseFlow->>DisputeFn: call(order, DisputeStatus::Settled, my_keys, "release")
    CancelFlow->>DisputeFn: call(order, DisputeStatus::SellerRefunded, my_keys, "cooperative cancel")
    DisputeFn->>DB: find_dispute_by_order_id(order.id)
    DB-->>DisputeFn: dispute (or not found)
    alt dispute found
        DisputeFn->>DB: update dispute.status = new_status
        DB-->>DisputeFn: update result
        DisputeFn->>Nostr: build & publish dispute event (tags: status, initiator, y="mostro", z="dispute")
        Nostr-->>DisputeFn: publish result
    else no dispute
        DisputeFn-->>DisputeFn: log "no dispute found"
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • Catrya
  • grunch

Poem

🐇
I hopped through code to mend the fray,
One function closes disputes away.
Release or cancel, I quietly cheer,
Best-effort fixes, no panic here.
Nostr whispers tags — the admins hear.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding dispute closure logic when a seller releases funds.
Linked Issues check ✅ Passed The PR fully addresses issue #605 by adding close_dispute_after_user_resolution function and updating dispute status to Settled after release, with proper Nostr event publishing.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the dispute closure on release flow. The refactoring of duplicate logic into a shared helper and documentation updates are both in-scope to the stated objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/app/release.rs (1)

246-246: event shadows the &UnwrappedGift function parameter.

The binding if let Ok(event) = new_dispute_event(...) at line 246 shadows the event: &UnwrappedGift parameter declared at line 165. The original event is no longer live by this point (last used at line 197), so there is no functional impact, but the shadowing is confusing. Renaming to dispute_event or close_event is cleaner.

♻️ Proposed rename
-            if let Ok(event) = new_dispute_event(my_keys, "", dispute_id.to_string(), tags) {
+            if let Ok(dispute_event) = new_dispute_event(my_keys, "", dispute_id.to_string(), tags) {
                 match get_nostr_client() {
                     Ok(client) => {
-                        if let Err(e) = client.send_event(&event).await {
+                        if let Err(e) = client.send_event(&dispute_event).await {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/release.rs` at line 246, The binding if let Ok(event) =
new_dispute_event(...) shadows the function parameter event: &UnwrappedGift;
rename the local binding to a non-conflicting name (e.g., dispute_event or
close_event) where new_dispute_event is called so the parameter event remains
unshadowed and the intent is clearer — update any subsequent uses of that local
binding accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/release.rs`:
- Line 205: The code calls find_dispute_by_order_id(...) and then mutates the
returned dispute unconditionally; add a guard to skip updating if the dispute is
already in a terminal state (e.g., SellerRefunded, Settled, Closed) so you don't
overwrite prior resolutions. Locate the block that matches if let Ok(mut
dispute) = find_dispute_by_order_id(pool, order.id).await and before mutating or
setting dispute.status = Settled, check dispute.status against your enum's
terminal variants (or only proceed when status == Open/Pending) and
return/continue when it's terminal; ensure any logging reflects the skipped
update.
- Around line 246-257: The code currently swallows errors from new_dispute_event
by using if let Ok(event) and never logging Err; change the guard to match the
Result from new_dispute_event (or use if let Err(e) => { ... } ) so that when
new_dispute_event(...) returns Err you log the error (including the error
details) before returning/continuing; keep the existing flow that calls
get_nostr_client() and client.send_event(&event).await on success, but ensure
new_dispute_event failures are explicitly logged (referencing new_dispute_event,
get_nostr_client, and the client.send_event call so you update the correct
block).

---

Nitpick comments:
In `@src/app/release.rs`:
- Line 246: The binding if let Ok(event) = new_dispute_event(...) shadows the
function parameter event: &UnwrappedGift; rename the local binding to a
non-conflicting name (e.g., dispute_event or close_event) where
new_dispute_event is called so the parameter event remains unshadowed and the
intent is clearer — update any subsequent uses of that local binding
accordingly.

Comment thread src/app/release.rs Outdated
Comment thread src/app/release.rs Outdated
Renamed DISPUTE_AUTO_CLOSE_ON_COOPERATIVE_CANCEL.md to
DISPUTE_AUTO_CLOSE_ON_USER_RESOLUTION.md and expanded it to cover
both scenarios where disputes are automatically closed:

1. Cooperative Cancellation → SellerRefunded (existing)
2. Seller Release → Settled (new)

Added comparison tables and code flow diagrams for both cases.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/DISPUTE_AUTO_CLOSE_ON_USER_RESOLUTION.md`:
- Around line 46-54: The two fenced ASCII diagram blocks containing the flows
for cancel_action() → cancel_cooperative_execution_step_2() (showing Cancel hold
invoice, Set order status to CooperativelyCanceled, Notify both parties, Close
dispute → SellerRefunded) and release_action() (Settle hold invoice, Set order
status to SettledHoldInvoice, Close dispute → Settled, Notify both parties,
Initiate payment to buyer) are missing language tags; update each opening
triple-backtick to include the language specifier text (i.e., change ``` to
```text) for both diagram blocks (the
cancel_action()/cancel_cooperative_execution_step_2() block and the
release_action() block, and the additional occurrence noted at lines 70-77) to
satisfy the project's fenced code block guideline.

Comment thread docs/DISPUTE_AUTO_CLOSE_ON_USER_RESOLUTION.md Outdated
- Log error when new_dispute_event() fails (was silently ignored)
- Rename 'event' to 'dispute_event' to avoid shadowing function parameter
- Add 'text' language specifier to markdown code blocks
@arkanoider
Copy link
Copy Markdown
Collaborator

Ok @Mostrica and @CodeRabbit, this idea is valid, but please try do thing in more nice way. You are duplicating code for nothing, the same logic in cancel.rs :

if let Ok(mut dispute) = find_dispute_by_order_id(pool, order.id).await {

is replicated here, please extract a function that you can call like this afted db fetch:

if let Ok(mut dispute) = find_dispute_by_order_id(pool, order.id).await {
    update_dispute_event( dispute.id, new_status);
    ....
}

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 20, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's look at the dispute closure logic in release.rs (the new code)
echo "=== Dispute closure logic in release.rs ==="
rg -n -A 30 "if let Ok\(mut dispute\) = find_dispute_by_order_id" src/app/release.rs

echo -e "\n=== Dispute closure logic in cancel.rs (line 118 and surrounding) ==="
# Look at the referenced logic in cancel.rs around line 118
sed -n '110,160p' src/app/cancel.rs | cat -n

Length of output: 3959


Task: Extract duplicate dispute closure logic into reusable helper function

  • Create a new helper function close_dispute_after_resolution() in a suitable location (e.g., src/app/dispute.rs or as a module-level helper)
  • The function will accept: pool, order reference, dispute status, and my_keys
  • It will encapsulate all the logic: fetch dispute, update status, log, determine initiator, build tags, publish event
  • Update cancel.rs to call the helper with DisputeStatus::SellerRefunded
  • Update release.rs to call the helper with DisputeStatus::Settled

🏁 Script executed:

#!/bin/bash
# Check if dispute.rs exists and see its structure
echo "=== Checking src/app/dispute.rs ==="
if [ -f src/app/dispute.rs ]; then
    echo "File exists. Checking public functions:"
    rg -n "^pub (async )?fn " src/app/dispute.rs
else
    echo "File does not exist."
fi

echo -e "\n=== Checking src/app/mod.rs to see module structure ==="
cat src/app/mod.rs

Length of output: 212


🏁 Script executed:

#!/bin/bash
# Get the complete dispute closure logic from release.rs
echo "=== Complete dispute closure block in release.rs ==="
sed -n '205,260p' src/app/release.rs | cat -n

Length of output: 2748


🏁 Script executed:

#!/bin/bash
# Get the complete dispute closure logic from cancel.rs (continuing from line 150)
echo "=== Complete dispute closure block in cancel.rs ==="
sed -n '118,170p' src/app/cancel.rs | cat -n

Length of output: 2578


🏁 Script executed:

#!/bin/bash
echo "=== Imports in cancel.rs ==="
head -n 30 src/app/cancel.rs

echo -e "\n=== Imports in release.rs ==="
head -n 30 src/app/release.rs

Length of output: 2185


@arkanoider, you're absolutely right—this is significant code duplication that should be extracted. I'll plan the refactoring to create a shared helper function.

Task: Extract duplicate dispute closure logic into shared helper function

The same ~50-line dispute closure block appears in both cancel.rs and release.rs. I'll:

  • Add a new public helper function close_dispute_after_user_resolution() in src/app/dispute.rs
  • Update cancel.rs to call the helper with DisputeStatus::SellerRefunded
  • Update release.rs to call the helper with DisputeStatus::Settled

🏁 Script executed:

#!/bin/bash
echo "=== Current imports in src/app/dispute.rs ==="
head -n 20 src/app/dispute.rs

Length of output: 841



🧠 Learnings used
Learnt from: arkanoider
Repo: MostroP2P/mostro PR: 376
File: src/app/cancel.rs:207-222
Timestamp: 2024-11-07T09:41:13.417Z
Learning: Prefer not to extract duplicate message sending logic into helper functions in `src/app/cancel.rs` unless there is significant benefit.

Learnt from: grunch
Repo: MostroP2P/mostro PR: 516
File: src/app/dispute.rs:171-172
Timestamp: 2025-09-04T18:55:53.663Z
Learning: In the Mostro codebase, Dispute::new(order_id, order_status) internally initializes the dispute with "Initiated" status while storing the provided order_status (like "Active" or "FiatSent") as historical data in the dispute table to track what status the order had before the dispute was created.

💡 Use @coderabbitai plan to create a plan for code changes

@Mostrica
Copy link
Copy Markdown
Contributor Author

@arkanoider You're right, I apologize for the code duplication. I'll extract the dispute closure logic into a shared helper function in src/app/dispute.rs that both cancel.rs and release.rs can call.

I'll push the refactored version shortly.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/app/dispute.rs (3)

262-262: Add a trace/debug log for the no-dispute early exit.

When find_dispute_by_order_id returns Err, the function silently returns. For a best-effort helper called on every cooperative-cancel and release path, a tracing::debug! log would distinguish "order had no dispute" from a quiet DB failure, aiding production diagnostics.

🔍 Suggested trace log
-    if let Ok(mut dispute) = find_dispute_by_order_id(pool, order.id).await {
+    let Ok(mut dispute) = find_dispute_by_order_id(pool, order.id).await else {
+        tracing::debug!("No dispute found for order {} during {}; nothing to close", order.id, context);
+        return;
+    };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dispute.rs` at line 262, The early-return when
find_dispute_by_order_id(pool, order.id).await yields Err is silent; add a
tracing::debug! (or trace!) in the else branch to log that no dispute was found
for the order (include the order id and any minimal context like order.id or
order identifier) so callers can distinguish "no dispute" from a DB error;
update the match around find_dispute_by_order_id to log this message before
returning from the function.

255-329: Add unit tests for close_dispute_after_user_resolution.

The coding guidelines require co-located tests under mod tests with descriptive names (e.g., handles_missing_dispute, logs_error_on_db_update_failure, publishes_event_on_success). The new public function has multiple distinct code paths (no dispute found, DB update error, event publish error, full success) and currently has no test coverage.

As per coding guidelines: "Co-locate tests in their modules under mod tests with descriptive names like handles_expired_hold_invoice."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dispute.rs` around lines 255 - 329, Add co-located unit tests under
mod tests for the public function close_dispute_after_user_resolution to cover
its distinct code paths: (1) when no dispute is found (handles_missing_dispute),
(2) when updating the DB fails and an error is logged
(logs_error_on_db_update_failure), (3) when creating or publishing the Nostr
event fails and appropriate errors are logged (publishes_event_error_cases), and
(4) full success where the dispute is updated and client.send_event is called
(publishes_event_on_success). Use mocks or test doubles for
find_dispute_by_order_id, dispute.update, new_dispute_event and
get_nostr_client/client.send_event to simulate each scenario, assert expected
log messages and side effects (DB update called or not, event send attempted),
and place tests in the same module as close_dispute_after_user_resolution with
descriptive test names as required.

289-326: Consider reusing publish_dispute_event to avoid tag-building duplication.

The tag construction and event-publishing block here (lines 289–326) is essentially a duplicate of publish_dispute_event (lines 32–82). Both live in the same module, so a small refactor—either widening publish_dispute_event to accept an &str initiator instead of bool, or extracting build_dispute_tags(status, initiator) as a private helper—would eliminate the duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dispute.rs` around lines 289 - 326, The code duplicates tag
construction and event publishing; refactor to reuse publish_dispute_event or
extract a shared helper: create a private function build_dispute_tags(status:
&str, initiator: &str) that returns Tags (used by new_dispute_event) and replace
the inline Tags::from_list block in the current code with a call to
build_dispute_tags; alternatively broaden publish_dispute_event to accept
initiator: &str (instead of bool) and call it here to handle tag creation and
sending (reusing get_nostr_client/new_dispute_event inside that function) so the
duplicate Tag::custom and client.send_event logic is removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/dispute.rs`:
- Around line 282-286: The match on (order.seller_dispute, order.buyer_dispute)
in dispute_initiator silently falls back to "unknown" for inconsistent DB
states; add a tracing::warn! in the fallback branch that logs the actual flag
values (order.seller_dispute and order.buyer_dispute) and contextual info (e.g.,
order id or dispute id if available) before returning "unknown" so the
inconsistency is surfaced while keeping the current best-effort behavior.

---

Nitpick comments:
In `@src/app/dispute.rs`:
- Line 262: The early-return when find_dispute_by_order_id(pool, order.id).await
yields Err is silent; add a tracing::debug! (or trace!) in the else branch to
log that no dispute was found for the order (include the order id and any
minimal context like order.id or order identifier) so callers can distinguish
"no dispute" from a DB error; update the match around find_dispute_by_order_id
to log this message before returning from the function.
- Around line 255-329: Add co-located unit tests under mod tests for the public
function close_dispute_after_user_resolution to cover its distinct code paths:
(1) when no dispute is found (handles_missing_dispute), (2) when updating the DB
fails and an error is logged (logs_error_on_db_update_failure), (3) when
creating or publishing the Nostr event fails and appropriate errors are logged
(publishes_event_error_cases), and (4) full success where the dispute is updated
and client.send_event is called (publishes_event_on_success). Use mocks or test
doubles for find_dispute_by_order_id, dispute.update, new_dispute_event and
get_nostr_client/client.send_event to simulate each scenario, assert expected
log messages and side effects (DB update called or not, event send attempted),
and place tests in the same module as close_dispute_after_user_resolution with
descriptive test names as required.
- Around line 289-326: The code duplicates tag construction and event
publishing; refactor to reuse publish_dispute_event or extract a shared helper:
create a private function build_dispute_tags(status: &str, initiator: &str) that
returns Tags (used by new_dispute_event) and replace the inline Tags::from_list
block in the current code with a call to build_dispute_tags; alternatively
broaden publish_dispute_event to accept initiator: &str (instead of bool) and
call it here to handle tag creation and sending (reusing
get_nostr_client/new_dispute_event inside that function) so the duplicate
Tag::custom and client.send_event logic is removed.

Comment thread src/app/dispute.rs
As suggested by @arkanoider, extracted the duplicate dispute closure
logic from cancel.rs and release.rs into a new shared function
close_dispute_after_user_resolution() in dispute.rs.

This eliminates ~100 lines of duplicated code while maintaining the
same best-effort error handling behavior.
@Mostrica Mostrica force-pushed the fix/close-dispute-on-release branch from d61b55e to 574c363 Compare February 20, 2026 18:14
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/app/release.rs (1)

1-1: close_dispute_after_user_resolution (in src/app/dispute.rs) is missing a /// doc comment.

The function is pub async fn with non-obvious behaviour: best-effort DB update, conditional Nostr event publishing, and silent skip when no dispute exists. Per coding guidelines, non-obvious public APIs should carry /// doc comments.

📝 Suggested doc comment
+/// Closes an active dispute tied to `order` after the users themselves resolved the
+/// trade (cooperative cancel or seller release).
+///
+/// Updates the dispute's status to `new_status` in the database and publishes an
+/// updated replaceable Nostr dispute event so admin clients reflect the resolution.
+///
+/// This is a **best-effort** operation: failures are logged but never propagated,
+/// so the primary order flow is never blocked by a dispute-closure error.
+/// If no dispute is associated with the order the function is a no-op.
+///
+/// # Arguments
+/// * `context` – Short label used in log messages (e.g. `"release"`, `"cooperative cancel"`).
 pub async fn close_dispute_after_user_resolution(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/release.rs` at line 1, Add a triple-slash doc comment above the
public async function close_dispute_after_user_resolution in src/app/dispute.rs
that documents its non-obvious behavior: describe that it performs a best-effort
database update (may fail without panicking), conditionally publishes a Nostr
event when applicable, and silently returns/does nothing if the dispute does not
exist; also mention the function is async, its parameters' role, and any
error/return semantics so callers know it won't always produce an error on
missing disputes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/release.rs`:
- Line 1: Add a triple-slash doc comment above the public async function
close_dispute_after_user_resolution in src/app/dispute.rs that documents its
non-obvious behavior: describe that it performs a best-effort database update
(may fail without panicking), conditionally publishes a Nostr event when
applicable, and silently returns/does nothing if the dispute does not exist;
also mention the function is async, its parameters' role, and any error/return
semantics so callers know it won't always produce an error on missing disputes.

@arkanoider
Copy link
Copy Markdown
Collaborator

Ok @Mostrica! Tested and works, can you just add a warn message in case of match missing and an info print of dispute event?
Like here below_

pub async fn close_dispute_after_user_resolution(
    pool: &Pool<Sqlite>,
    order: &Order,
    new_status: DisputeStatus,
    my_keys: &Keys,
    context: &str,
) {
    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")),
                    vec!["mostro".to_string()],
                ),
                Tag::custom(
                    TagKind::Custom(Cow::Borrowed("z")),
                    vec!["dispute".to_string()],
                ),
            ]);

            match new_dispute_event(my_keys, "", dispute_id.to_string(), tags) {
                Ok(event) => match get_nostr_client() {
                    Ok(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 get Nostr client for dispute event: {}", e);
                    }
                },
                Err(e) => {
                    tracing::error!(
                        "Failed to create dispute close event for dispute {}: {}",
                        dispute_id,
                        e
                    );
                }
            }
        }
    }
}

As requested by @arkanoider:
- Added tracing::warn! when dispute flags are inconsistent
- Added tracing::info! to log the dispute close event before publishing
@Mostrica
Copy link
Copy Markdown
Contributor Author

@arkanoider Done! Added both:

  • tracing::warn! for inconsistent dispute flags
  • tracing::info! to log the dispute close event before publishing

Commit: ff25d51

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/app/dispute.rs (1)

298-337: Consider reusing publish_dispute_event to reduce intra-file duplication.

Lines 299-337 rebuild the same four Nostr tags (s, initiator, y, z) and repeat the new_dispute_eventget_nostr_clientsend_event chain that publish_dispute_event (lines 20-82) already encapsulates. The only differences are (1) how the initiator string is determined and (2) error handling (Result vs. best-effort).

A lightweight refactor: change publish_dispute_event to accept initiator: &str instead of is_buyer_dispute: bool, then call it from here and swallow the error.

♻️ Sketch
-async fn publish_dispute_event(
-    dispute: &Dispute,
-    my_keys: &Keys,
-    is_buyer_dispute: bool,
-) -> Result<(), MostroError> {
-    let initiator = match is_buyer_dispute {
-        true => "buyer",
-        false => "seller",
-    };
+async fn publish_dispute_event(
+    dispute: &Dispute,
+    my_keys: &Keys,
+    initiator: &str,
+) -> Result<(), MostroError> {
     // ... rest unchanged ...

In dispute_action, derive the string before calling:

-    publish_dispute_event(&dispute, my_keys, is_buyer_dispute)
+    let initiator = if is_buyer_dispute { "buyer" } else { "seller" };
+    publish_dispute_event(&dispute, my_keys, initiator)

Then in close_dispute_after_user_resolution, replace lines 298-337 with:

+            if let Err(e) = publish_dispute_event(&dispute, my_keys, dispute_initiator).await {
+                tracing::error!("Failed to publish dispute close event: {}", e);
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dispute.rs` around lines 298 - 337, This block rebuilds Nostr tags
and duplicates the new_dispute_event → get_nostr_client → send_event flow
already implemented by publish_dispute_event; change publish_dispute_event
signature to accept initiator: &str (instead of is_buyer_dispute: bool), update
its internal tag construction to use that initiator string, then in
close_dispute_after_user_resolution derive the initiator string the same way
dispute_action does and call publish_dispute_event(&my_keys,
dispute_id.to_string(), &initiator). Call it in a best-effort way (ignore/log
the Result) instead of reimplementing
new_dispute_event/get_nostr_client/send_event here to eliminate duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/dispute.rs`:
- Around line 298-337: This block rebuilds Nostr tags and duplicates the
new_dispute_event → get_nostr_client → send_event flow already implemented by
publish_dispute_event; change publish_dispute_event signature to accept
initiator: &str (instead of is_buyer_dispute: bool), update its internal tag
construction to use that initiator string, then in
close_dispute_after_user_resolution derive the initiator string the same way
dispute_action does and call publish_dispute_event(&my_keys,
dispute_id.to_string(), &initiator). Call it in a best-effort way (ignore/log
the Result) instead of reimplementing
new_dispute_event/get_nostr_client/send_event here to eliminate duplication.

@arkanoider
Copy link
Copy Markdown
Collaborator

@Mostrica
Copy link
Copy Markdown
Contributor Author

@arkanoider Already fixed in the current head (ff25d51). The error import was removed in commit 574c363.

You might be looking at an older commit. Current state:

use tracing::info;  // line 11 - no error import

@arkanoider arkanoider self-requested a review February 20, 2026 19:16
Copy link
Copy Markdown
Collaborator

@arkanoider arkanoider left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing update on dispute status worka, dispute event is sent correctly with new state and mostro db ia updated too.

tACK.

Copy link
Copy Markdown
Member

@Catrya Catrya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK

Copy link
Copy Markdown
Contributor

@mostronatorcoder mostronatorcoder Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean refactor, LGTM ✅

The extraction of close_dispute_after_user_resolution() is a solid DRY improvement — centralizes the dispute close logic that was previously duplicated (and now needed in two places). The status mapping is semantically correct (SellerRefunded for cooperative cancel, Settled for release), and the best-effort error handling is the right call since the primary operation must not be blocked by dispute bookkeeping.

Good additions:

  • The tracing::warn! on inconsistent dispute flags is a nice improvement over the existing silent "unknown" pattern
  • Updated doc consolidating both scenarios is clear and well-structured
  • All CodeRabbit feedback was properly addressed

CI passes, no conflicts. Ready to merge.

@grunch grunch merged commit cd57784 into MostroP2P:main Feb 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dispute status not updated after successful release

4 participants